add live page

This commit is contained in:
Tykayn 2025-09-26 11:57:54 +02:00 committed by tykayn
parent 114bcca24e
commit eb8c42d0c0
19 changed files with 2759 additions and 199 deletions

View file

@ -50,6 +50,8 @@ class DemoMainResource:
<link rel="stylesheet" href="/static/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<script src="/static/demo_auth.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="/static/social.js"></script>
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
.logo{
@ -243,72 +245,43 @@ class DemoMainResource:
<h2>
<img src="/static/oedb.png" class="logo" />
OpenEventDatabase Demo</h2>
<p>This map shows current events from the OpenEventDatabase.</p>
<!-- Event addition buttons - always visible -->
<p><a href="/demo/traffic" class="add-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;">+ Traffic event</a></p>
<p><a href="/demo/add" class="add-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;">+ Any Event</a></p>
<p><a href="/demo/live" class="live-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;"> Live</a></p>
<!-- Collapsible information section -->
<h3 id="info_panel_header" class="collapsible-header">Information Panel <span class="toggle-icon"></span></h3>
<div id="info_panel_content" class="collapsible-content">
<!-- User Information Panel -->
<div id="user-info-panel" class="user-info-panel" style="display: none; background-color: #f5f5f5; border-radius: 4px; padding: 10px; margin: 10px 0; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">
<h3 style="margin-top: 0; margin-bottom: 10px; color: #333;">User Information</h3>
<p>Username: <strong id="username-display">Anonymous</strong></p>
<p>Points: <span id="points-display" style="font-weight: bold; color: #0078ff;">0</span></p>
<br/>
<br/>
<!-- Filtres pour les événements -->
<div class="event-filters" id="filters_panel" style="margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: 4px; display:none;">
<h3 id="filters_header" style="margin-top: 0; color: #0078ff; cursor:pointer;">Filtres</h3>
<div style="margin-top: 10px;">
<label style="display: block; margin-bottom: 5px;">Type d'événement:</label>
<select id="event-type-filter" style="width: 100%; padding: 5px; border-radius: 4px; border: 1px solid #ddd;">
<option value="">Tous</option>
<option value="traffic">Traffic</option>
<option value="weather">Météo</option>
<option value="gathering">Rassemblement</option>
<option value="incident">Incident</option>
</select>
</div>
<!-- Authentication section -->
<!--
# <div id="auth-section" class="auth-section">
# <h3>OpenStreetMap Authentication</h3>
#
<a href="https://www.openstreetmap.org/oauth2/authorize?client_id={client_id}&redirect_uri={client_redirect}&response_type=code&scope=read_prefs" class="osm-login-btn">
<span class="osm-logo"></span>
Login with OpenStreetMap
</a>
<script>
# // Replace server-side auth section with JavaScript-rendered version if available
document.addEventListener('DOMContentLoaded', function() {
fetchEvents();
if (window.osmAuth) {
const clientId = document.getElementById('osmClientId').value;
const redirectUri = document.getElementById('osmRedirectUri').value;
const authSection = document.getElementById('auth-section');
// Only replace if osmAuth is loaded and has renderAuthSection method
if (osmAuth.renderAuthSection) {
authSection.innerHTML = osmAuth.renderAuthSection(clientId, redirectUri);
}
}
});
</script>
</div> -->
<h3 id="endpoints_list_header">API Endpoints:</h3>
<ul id="endpoints_list">
<li><a href="/" >/ - API Information</a></li>
<li><a href="/event" >/event - Get Events</a></li>
<li><a href="/stats" >/stats - Database Statistics</a></li>
</ul>
<h3 id="demo_pages_list_header">Demo Pages:</h3>
<ul id="demo_pages_list">
<li><a href="/demo/search" >/demo/search - Advanced Search</a></li>
<li><a href="/demo/by-what" >/demo/by-what - Events by Type</a></li>
<li><a href="/demo/map-by-what" >/demo/map-by-what - Map by Event Type</a></li>
<li><a href="/demo/traffic" >/demo/traffic - Report Traffic Jam</a></li>
<li><a href="/demo/view-events" >/demo/view-events - View Saved Events</a></li>
<li><a href="/event?what=music" >Search Music Events</a></li>
<li><a href="/event?what=sport" >Search Sport Events</a></li>
</ul>
<p class="sources" style="text-align: center; margin-top: 10px;">
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" title="View Source Code on Cipherbliss" style="font-size: 24px;">
<i class="fas fa-code-branch"></i> sources
</a>
</p>
<div style="margin-top:12px; display:flex; align-items:center; gap:8px;">
<input type="checkbox" id="autoRefreshToggle" checked>
<label for="autoRefreshToggle" style="margin:0;">Rafraîchissement automatique (30s)</label>
</div>
</div>
<div class="event-filters" style="margin-top: 10px; padding: 10px; background-color: #fff; border: 1px solid #e5e7eb; border-radius: 4px;">
<h3 style="margin-top: 0; color: #0078ff;">Histogramme des évènements</h3>
<canvas id="eventsHistogram" style="width:100%; height:220px;"></canvas>
</div>
</div>
<script>
@ -320,6 +293,8 @@ class DemoMainResource:
const demoPagesList = document.getElementById('demo_pages_list');
const infoPanelHeader = document.getElementById('info_panel_header');
const infoPanelContent = document.getElementById('info_panel_content');
const filtersPanel = document.getElementById('filters_panel');
const filtersHeader = document.getElementById('filters_header');
// Fonction pour basculer l'affichage d'une liste ou section
function toggleList(header, list) {
@ -343,8 +318,98 @@ class DemoMainResource:
toggleList(endpointsHeader, endpointsList);
toggleList(demoPagesHeader, demoPagesList);
toggleList(infoPanelHeader, infoPanelContent);
// Toggle pour le panneau de filtres via le titre "Filtres"
if (filtersHeader && filtersPanel) {
filtersHeader.addEventListener('click', function() {
if (filtersPanel.style.display === 'none' || filtersPanel.style.display === '') {
filtersPanel.style.display = 'block';
} else {
filtersPanel.style.display = 'none';
}
});
}
});
// Variable globale pour stocker les marqueurs d'événements
window.eventMarkers = [];
function addEventsToMap(geojsonData) {
if (!geojsonData || !geojsonData.features) return;
geojsonData.features.forEach(feature => {
// Créer un élément HTML pour le marqueur
const el = document.createElement('div');
el.className = 'event-marker';
el.style.width = '20px';
el.style.height = '20px';
el.style.borderRadius = '50%';
// Déterminer la couleur selon le type d'événement
let color = '#0078ff';
const eventType = feature.properties.what;
if (eventType) {
if (eventType.includes('traffic')) color = '#F44336';
else if (eventType.includes('weather')) color = '#4CAF50';
else if (eventType.includes('gathering')) color = '#FF9800';
else if (eventType.includes('incident')) color = '#9C27B0';
}
el.style.backgroundColor = color;
el.style.border = '2px solid white';
el.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)';
el.style.cursor = 'pointer';
// Créer le contenu de la popup
const popupContent = createEventPopupContent(feature);
// Créer la popup
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: true
}).setHTML(popupContent);
// Créer et ajouter le marqueur
const marker = new maplibregl.Marker(el)
.setLngLat(feature.geometry.coordinates)
.setPopup(popup)
.addTo(map);
// Ajouter à la liste des marqueurs
window.eventMarkers.push(marker);
});
}
function createEventPopupContent(feature) {
const properties = feature.properties;
// Extraire les informations principales
const title = properties.title || 'Événement sans titre';
const what = properties.what || 'Non spécifié';
const when = properties.when ? formatDate(properties.when) : 'Date inconnue';
const description = properties.description || 'Aucune description disponible';
// Créer le HTML de la popup
return `
<div class="event-popup">
<h3 style="margin-top: 0; color: #0078ff;">${title}</h3>
<p><strong>Type:</strong> ${what}</p>
<p><strong>Date:</strong> ${when}</p>
<p><strong>Description:</strong> ${description}</p>
<p><a href="/demo/view/${properties.id}" style="color: #0078ff; font-weight: bold;">Voir détails</a></p>
</div>
`;
}
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleString();
} catch (e) {
return dateString;
}
}
// Map style URLs
const mapStyles = {
default: 'https://tiles.openfreemap.org/styles/liberty',
@ -401,10 +466,50 @@ class DemoMainResource:
// Style switcher functionality
let currentStyle = 'default';
let eventsData = null;
let histogramChart = null;
let refreshIntervalId = null;
// Array to store markers so they can be removed on refresh
// Store markers with their family/type for filtering
let currentMarkers = [];
function getFamily(what) {
if (!what) return 'unknown';
const s = String(what);
const dot = s.indexOf('.');
return dot === -1 ? s : s.slice(0, dot);
}
function applyTypeFilter() {
const sel = document.getElementById('event-type-filter');
const val = sel ? sel.value : '';
currentMarkers.forEach(rec => {
const el = rec.marker.getElement();
if (!val) {
el.style.display = '';
} else {
el.style.display = (rec.family === val) ? '' : 'none';
}
});
// Also filter vector circle layer if present
try {
if (!val) {
map.setFilter('events-circle', null);
} else {
const len = val.length;
// Show features where what starts with selected family
const filter = [
"any",
["!", ["has", "what"]],
["==", ["slice", ["get", "what"], 0, len], val]
];
map.setFilter('events-circle', filter);
}
} catch (e) {
// Layer may not be ready yet; ignore
}
}
// Fetch events when the map is loaded and every 30 seconds thereafter
@ -413,10 +518,18 @@ class DemoMainResource:
fetchEvents();
// Set up interval to fetch events every 30 seconds
setInterval(fetchEvents, 30000);
setupAutoRefresh();
console.log('Event refresh interval set: events will update every 30 seconds');
});
function setupAutoRefresh() {
const cb = document.getElementById('autoRefreshToggle');
const start = () => { if (!refreshIntervalId) { refreshIntervalId = setInterval(fetchEvents, 30000); } };
const stop = () => { if (refreshIntervalId) { clearInterval(refreshIntervalId); refreshIntervalId = null; } };
if (cb && cb.checked) start(); else stop();
if (cb) cb.addEventListener('change', () => { if (cb.checked) start(); else stop(); });
}
// Function to fetch events from the API
function fetchEvents() {
@ -427,6 +540,8 @@ class DemoMainResource:
if (data.features && data.features.length > 0) {
// Add events to the map
addEventsToMap(data);
// Render histogram for retrieved events
try { renderEventsHistogram(data.features); } catch(e) { console.warn('Histogram error', e); }
// Fit map to events bounds
fitMapToBounds(data);
@ -440,12 +555,49 @@ class DemoMainResource:
showErrorToast(`Erreur de chargement des événements: ${error.message}`);
});
}
function bucket10(dateStr) {
const d = new Date(dateStr);
if (isNaN(d.getTime())) return null;
d.setSeconds(0,0);
const m = d.getMinutes();
d.setMinutes(m - (m % 10));
return d.toISOString();
}
function renderEventsHistogram(features) {
const counts = new Map();
features.forEach(f => {
const p = f.properties || {};
const t = p.createdate || p.start || p.lastupdate;
const b = bucket10(t);
if (!b) return;
counts.set(b, (counts.get(b) || 0) + 1);
});
const labels = Array.from(counts.keys()).sort();
const data = labels.map(k => counts.get(k));
const ctx = document.getElementById('eventsHistogram');
if (!ctx) return;
if (histogramChart) histogramChart.destroy();
histogramChart = new Chart(ctx, {
type: 'bar',
data: { labels, datasets: [{ label:'Évènements / 10 min', data, backgroundColor:'#3b82f6' }] },
options: {
// maintainAspectRatio: false,
scales: {
x: { ticks: { callback: (v,i) => new Date(labels[i]).toLocaleString() } },
y: { beginAtZero: true }
}
}
});
}
// Function to add events to the map
function addEventsToMap(geojson) {
// Remove all existing markers
if (currentMarkers.length > 0) {
currentMarkers.forEach(marker => marker.remove());
currentMarkers.forEach(rec => rec.marker.remove());
currentMarkers = [];
console.log('Removed existing markers');
}
@ -569,7 +721,12 @@ class DemoMainResource:
let iconColor = '#0078ff'; // Default color
// Map event types to icons
if (eventType.startsWith('weather')) {
// Travaux detection (label or what)
const labelLower = String(properties.label || '').toLowerCase();
if (labelLower.includes('travaux') || eventType.includes('roadwork')) {
iconClass = 'hard-hat';
iconColor = '#ff9800';
} else if (eventType.startsWith('weather')) {
iconClass = 'cloud';
iconColor = '#00d1b2'; // Teal
} else if (eventType.startsWith('traffic')) {
@ -607,10 +764,13 @@ class DemoMainResource:
.setLngLat(coordinates)
.setPopup(popup)
.addTo(map);
// Store marker reference for later removal
currentMarkers.push(marker);
// Store marker with its family for filtering
currentMarkers.push({ marker, family: getFamily(eventType) });
});
// Re-apply current filter on fresh markers
applyTypeFilter();
}
// Function to calculate relative time (e.g., "2 hours 30 minutes ago")
@ -862,6 +1022,12 @@ class DemoMainResource:
// Initialisation des gestionnaires d'événements pour le toast d'erreur
initErrorToast();
// Hook filters
const typeSel = document.getElementById('event-type-filter');
const applyBtn = document.getElementById('apply-filters');
if (typeSel) typeSel.addEventListener('change', applyTypeFilter);
if (applyBtn) applyBtn.addEventListener('click', applyTypeFilter);
});
// Fonction pour initialiser le toast d'erreur
@ -892,7 +1058,21 @@ class DemoMainResource:
}, 6000);
}
// Initialiser automatiquement le mode social quand la carte est chargée
map.on('load', function() {
// Vérifier si l'objet social existe
if (window.oedbSocial) {
console.log('Initialisation automatique du mode social...');
setTimeout(() => {
// Trouver le bouton d'activation du mode social et simuler un clic
const socialButton = document.querySelector('.toggle-social-btn');
if (socialButton) {
socialButton.click();
console.log('Mode social activé automatiquement');
}
}, 2000); // Attendre 2 secondes pour que tout soit bien chargé
}
});
</script>
</body>
</html>