add live page
This commit is contained in:
parent
114bcca24e
commit
eb8c42d0c0
19 changed files with 2759 additions and 199 deletions
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue