2025-09-21 13:35:01 +02:00
"""
Main demo page resource for the OpenEventDatabase .
"""
import falcon
from oedb . utils . logging import logger
class DemoMainResource :
"""
Resource for the main demo page .
Handles the / demo endpoint .
"""
def on_get ( self , req , resp ) :
"""
Handle GET requests to the / demo endpoint .
Returns an HTML page with a MapLibre map showing current events .
Args :
req : The request object .
resp : The response object .
"""
logger . info ( " Processing GET request to /demo " )
try :
# Set content type to HTML
resp . content_type = ' text/html '
2025-09-21 16:57:24 +02:00
# Load environment variables from .env file for OAuth2 configuration
from oedb . utils . db import load_env_from_file
load_env_from_file ( )
# Get OAuth2 configuration parameters
import os
client_id = os . getenv ( " CLIENT_ID " , " " )
client_secret = os . getenv ( " CLIENT_SECRET " , " " )
client_redirect = os . getenv ( " CLIENT_REDIRECT " , " " )
2025-09-21 13:35:01 +02:00
# Create HTML response with MapLibre map
html = """
< ! DOCTYPE html >
< html lang = " en " >
< head >
< meta charset = " UTF-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1.0 " >
< title > OpenEventDatabase Demo < / title >
2025-09-26 17:38:30 +02:00
< link rel = " icon " type = " image/png " href = " /static/oedb.png " >
< link rel = " icon " type = " image/png " href = " /static/oedb.png " >
2025-09-21 13:35:01 +02:00
< script src = " https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js " > < / script >
< link href = " https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css " rel = " stylesheet " / >
< link rel = " stylesheet " href = " https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css " >
2025-09-21 16:57:24 +02:00
< link rel = " stylesheet " href = " /static/demo_styles.css " >
2025-09-21 13:35:01 +02:00
< script defer src = " https://use.fontawesome.com/releases/v5.15.4/js/all.js " > < / script >
2025-09-21 16:57:24 +02:00
< script src = " /static/demo_auth.js " > < / script >
2025-09-26 11:57:54 +02:00
< script src = " https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js " > < / script >
< script src = " /static/social.js " > < / script >
2025-09-21 13:35:01 +02:00
< style >
body { margin : 0 ; padding : 0 ; font - family : Arial , sans - serif ; }
2025-09-23 11:51:54 +02:00
. logo {
width : 1.5 rem ;
height : 1.5 rem ;
}
. sources {
font - size : 0.8 rem
}
2025-09-21 13:35:01 +02:00
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
. map - overlay {
position : absolute ;
top : 10 px ;
left : 10 px ;
background : rgba ( 255 , 255 , 255 , 0.9 ) ;
padding : 10 px ;
border - radius : 5 px ;
box - shadow : 0 0 10 px rgba ( 0 , 0 , 0 , 0.1 ) ;
max - width : 300 px ;
2025-09-23 12:53:52 +02:00
transition : all 0.3 s ease ;
z - index : 10 ;
}
/ * Responsive styles for the menu * /
@media ( max - width : 768 px ) {
. map - overlay {
max - width : 85 % ;
width : 85 % ;
font - size : 0.9 em ;
}
. map - overlay h2 {
font - size : 1.2 em ;
}
2025-09-21 13:35:01 +02:00
}
2025-09-23 12:53:52 +02:00
@media ( max - width : 480 px ) {
. map - overlay {
max - width : 90 % ;
width : 90 % ;
font - size : 0.85 em ;
}
}
/ * Styles pour listes dépliantes et sections collapsibles * /
#endpoints_list, #demo_pages_list, .collapsible-content {
2025-09-23 11:51:54 +02:00
display : none ;
overflow : hidden ;
transition : all 0.3 s ease ;
}
2025-09-23 12:53:52 +02:00
#endpoints_list_header, #demo_pages_list_header, .collapsible-header {
2025-09-23 11:51:54 +02:00
cursor : pointer ;
user - select : none ;
position : relative ;
padding - right : 20 px ;
2025-09-23 12:53:52 +02:00
margin - top : 15 px ;
margin - bottom : 10 px ;
}
. toggle - icon {
position : absolute ;
right : 0 ;
font - size : 0.8 em ;
transition : transform 0.3 s ease ;
2025-09-23 11:51:54 +02:00
}
#endpoints_list_header:after, #demo_pages_list_header:after {
content : ' ▼ ' ;
position : absolute ;
right : 0 ;
font - size : 0.8 em ;
transition : transform 0.3 s ease ;
}
2025-09-23 12:53:52 +02:00
#endpoints_list_header.active:after, #demo_pages_list_header.active:after,
. collapsible - header . active . toggle - icon {
2025-09-23 11:51:54 +02:00
transform : rotate ( 180 deg ) ;
}
2025-09-21 13:35:01 +02:00
. map - style - control {
position : absolute ;
top : 10 px ;
right : 10 px ;
background : rgba ( 255 , 255 , 255 , 0.9 ) ;
padding : 10 px ;
border - radius : 5 px ;
box - shadow : 0 0 10 px rgba ( 0 , 0 , 0 , 0.1 ) ;
z - index : 1 ;
}
. map - style - control button {
display : block ;
margin - bottom : 5 px ;
padding : 5 px 10 px ;
background : #fff;
border : 1 px solid #ddd;
border - radius : 3 px ;
cursor : pointer ;
width : 100 % ;
text - align : left ;
}
. map - style - control button : hover {
background : #f5f5f5;
}
. map - style - control button . active {
background : #0078ff;
color : white ;
border - color : #0056b3;
}
h2 { margin - top : 0 ; }
ul { padding - left : 20 px ; }
a { color : #0078ff; text-decoration: none; }
2025-09-23 12:01:30 +02:00
a svg {
margin - right : 1 ch ;
}
a : hover { text - decoration : underline ; color : white ; }
2025-09-21 13:35:01 +02:00
. event - popup { max - width : 300 px ; }
2025-09-23 12:01:30 +02:00
/ * Style pour le toast d ' erreur */
. error - toast {
position : fixed ;
top : 20 px ;
right : 20 px ;
background - color : #f44336;
color : white ;
padding : 15 px ;
border - radius : 5 px ;
box - shadow : 0 2 px 10 px rgba ( 0 , 0 , 0 , 0.2 ) ;
z - index : 1000 ;
max - width : 350 px ;
opacity : 0 ;
visibility : hidden ;
transition : opacity 0.3 s , visibility 0.3 s ;
}
. error - toast . show {
opacity : 1 ;
visibility : visible ;
}
. error - toast a {
color : #fff;
text - decoration : underline ;
font - weight : bold ;
}
. error - toast - close {
position : absolute ;
right : 10 px ;
top : 10 px ;
cursor : pointer ;
font - weight : bold ;
}
/ * Style pour le bouton de feedback * /
. feedback - button {
position : fixed ;
left : 10 px ;
bottom : 20 px ;
background - color : #0078ff;
color : white ;
padding : 10 px 15 px ;
border - radius : 5 px ;
box - shadow : 0 2 px 10 px rgba ( 0 , 0 , 0 , 0.2 ) ;
z - index : 900 ;
text - decoration : none ;
font - weight : bold ;
display : flex ;
align - items : center ;
}
. feedback - button i {
margin - right : 8 px ;
}
. feedback - button : hover {
background - color : #0056b3;
text - decoration : none ;
}
2025-09-27 00:18:03 +02:00
/ * Styles pour les marqueurs personnalisés avec forme de goutte * /
. custom - marker {
cursor : pointer ;
user - select : none ;
}
. marker - drop {
width : 36 px ;
height : 46 px ;
position : relative ;
background : #fff;
border - radius : 50 % 50 % 50 % 0 ;
transform : rotate ( - 45 deg ) ;
filter : drop - shadow ( 2 px 2 px 4 px rgba ( 0 , 0 , 0 , 0.3 ) ) ;
display : flex ;
align - items : center ;
justify - content : center ;
}
. marker - drop : after {
content : ' ' ;
width : 8 px ;
height : 8 px ;
position : absolute ;
background : white ;
border - radius : 50 % ;
top : 50 % ;
left : 50 % ;
transform : translate ( - 50 % , - 50 % ) ;
z - index : 1 ;
}
. marker - emoji {
font - size : 18 px ;
transform : rotate ( 45 deg ) ;
z - index : 2 ;
position : relative ;
line - height : 1 ;
}
. custom - marker : hover . marker - drop {
transform : rotate ( - 45 deg ) scale ( 1.1 ) ;
transition : transform 0.2 s ease ;
}
2025-09-21 13:35:01 +02:00
< / style >
< / head >
< body >
< div id = " map " > < / div >
2025-09-23 12:01:30 +02:00
< ! - - Toast d ' erreur pour les échecs de fetch -->
< div id = " error-toast " class = " error-toast " >
< span class = " error-toast-close " > & times ; < / span >
< p id = " error-message " > Une erreur s ' est produite lors du chargement des événements.</p>
< p > Consultez le < a href = " https://forum.openstreetmap.fr/t/openeventdatabase-reboot/37649 " target = " _blank " > forum OSM < / a > pour plus d ' informations.</p>
< / div >
< ! - - Bouton de feedback fixe - - >
< a href = " https://forum.openstreetmap.fr/t/openeventdatabase-reboot/37649 " class = " feedback-button " target = " _blank " >
< i class = " fas fa-comments " > < / i > Feedback sur le forum OSM
< / a >
2025-09-21 16:57:24 +02:00
< ! - - Hidden OAuth2 configuration for the JavaScript module - - >
< input type = " hidden " id = " osmClientId " value = " {client_id} " >
< input type = " hidden " id = " osmClientSecret " value = " {client_secret} " >
< input type = " hidden " id = " osmRedirectUri " value = " {client_redirect} " >
2025-09-21 13:35:01 +02:00
< div class = " map-overlay " >
2025-09-23 11:51:54 +02:00
< h2 >
< img src = " /static/oedb.png " class = " logo " / >
OpenEventDatabase Demo < / h2 >
2025-09-26 11:57:54 +02:00
2025-09-23 12:53:52 +02:00
< ! - - 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 >
2025-09-26 11:57:54 +02:00
< 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 >
2025-09-27 00:39:18 +02:00
< p > < a href = " /demo/property-stats " class = " stats-event-btn " style = " display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #28a745; color: white; border-radius: 4px; font-weight: bold; " > 📊 Statistiques propriétés < / a > < / p >
2025-09-26 11:57:54 +02:00
2025-09-23 12:18:10 +02:00
2025-09-23 12:53:52 +02:00
< ! - - Collapsible information section - - >
2025-09-26 11:57:54 +02:00
< br / >
< br / >
< ! - - Filtres pour les événements - - >
2025-09-27 00:39:18 +02:00
< div class = " event-filters " id = " filters_panel " style = " margin-top: 15px; padding: 10px; background-color: #f5f5f5; border-radius: 4px; " >
2025-09-26 11:57:54 +02:00
< 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 >
2025-09-23 12:53:52 +02:00
< / div >
2025-09-27 00:39:18 +02:00
2025-09-23 12:53:52 +02:00
< / div >
2025-09-26 11:57:54 +02:00
< div class = " event-filters " style = " margin-top: 10px; padding: 10px; background-color: #fff; border: 1px solid #e5e7eb; border-radius: 4px; " >
2025-09-27 00:39:18 +02:00
< 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 ( 30 s ) < / label >
< / div >
2025-09-26 11:57:54 +02:00
< canvas id = " eventsHistogram " style = " width:100 % ; height:220px; " > < / canvas >
< / div >
2025-09-21 13:35:01 +02:00
< / div >
< script >
2025-09-27 00:18:03 +02:00
/ / Configuration des critères d ' emojis pour les marqueurs de carte
window . EMOJI_CRITERIA = {
/ / Emoji mammouth pour les événements contenant " mammouth "
mammoth : {
emoji : ' 🦣 ' ,
criteria : ( name , description , what ) = > {
const text = ( name + ' ' + description ) . toLowerCase ( ) ;
return text . includes ( ' mammouth ' ) ;
}
} ,
/ / Emoji notes de musique pour orchestres , concerts , fanfares ou types musicaux
music : {
emoji : ' 🎵 ' ,
criteria : ( name , description , what ) = > {
const text = ( name + ' ' + description + ' ' + what ) . toLowerCase ( ) ;
return text . includes ( ' orchestr ' ) | | text . includes ( ' concert ' ) | |
text . includes ( ' fanfare ' ) | | text . includes ( ' music ' ) ;
}
} ,
/ / Emoji éclair pour les types contenant " power "
power : {
emoji : ' ⚡ ' ,
criteria : ( name , description , what ) = > {
return ( what | | ' ' ) . toLowerCase ( ) . includes ( ' power ' ) ;
}
} ,
/ / Emoji vélo pour les types contenant " bike "
bike : {
emoji : ' 🚴 ' ,
criteria : ( name , description , what ) = > {
return ( what | | ' ' ) . toLowerCase ( ) . includes ( ' bike ' ) ;
}
2025-09-27 00:39:18 +02:00
} ,
/ / Emoji casque de chantier pour les travaux
construction : {
emoji : ' ⛑️ ' ,
criteria : ( name , description , what ) = > {
const text = ( name + ' ' + description + ' ' + what ) . toLowerCase ( ) ;
return text . includes ( ' travaux ' ) ;
}
} ,
/ / Emoji soleil pour les types contenant " daylight "
daylight : {
emoji : ' ☀️ ' ,
criteria : ( name , description , what ) = > {
return ( what | | ' ' ) . toLowerCase ( ) . includes ( ' daylight ' ) ;
}
} ,
/ / Emoji carte pour les types contenant " community.osm "
osm_community : {
emoji : ' 🗺️ ' ,
criteria : ( name , description , what ) = > {
return ( what | | ' ' ) . toLowerCase ( ) . includes ( ' community.osm ' ) ;
}
2025-09-27 00:18:03 +02:00
}
} ;
/ / Fonction pour déterminer l ' emoji approprié pour un événement
function getEventEmoji ( properties ) {
const name = properties . name | | properties . label | | ' ' ;
const description = properties . description | | ' ' ;
const what = properties . what | | ' ' ;
/ / Parcourir les critères dans l ' ordre de priorité
for ( const [ key , config ] of Object . entries ( window . EMOJI_CRITERIA ) ) {
if ( config . criteria ( name , description , what ) ) {
return config . emoji ;
}
}
/ / Emoji par défaut selon le type d ' événement
if ( what . includes ( ' traffic ' ) ) return ' 🚗 ' ;
if ( what . includes ( ' weather ' ) ) return ' 🌤️ ' ;
if ( what . includes ( ' gathering ' ) ) return ' 👥 ' ;
if ( what . includes ( ' incident ' ) ) return ' ⚠️ ' ;
return ' 📍 ' ; / / Emoji par défaut
}
2025-09-23 12:53:52 +02:00
/ / Fonction pour gérer les listes dépliantes et sections collapsibles
2025-09-23 11:51:54 +02:00
document . addEventListener ( ' DOMContentLoaded ' , function ( ) {
2025-09-26 11:57:54 +02:00
const filtersPanel = document . getElementById ( ' filters_panel ' ) ;
const filtersHeader = document . getElementById ( ' filters_header ' ) ;
2025-09-23 11:51:54 +02:00
2025-09-23 12:53:52 +02:00
/ / Fonction pour basculer l ' affichage d ' une liste ou section
2025-09-23 11:51:54 +02:00
function toggleList ( header , list ) {
2025-09-26 17:38:30 +02:00
if ( header & & list ) {
header . addEventListener ( ' click ' , function ( ) {
if ( list . style . display == = ' none ' | | list . style . display == = ' ' ) {
list . style . display = ' block ' ;
header . classList . add ( ' active ' ) ;
} else {
list . style . display = ' none ' ;
header . classList . remove ( ' active ' ) ;
}
} ) ;
}
2025-09-23 11:51:54 +02:00
}
2025-09-26 11:57:54 +02:00
/ / 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 ' ;
}
} ) ;
}
2025-09-23 11:51:54 +02:00
} ) ;
2025-09-26 17:38:30 +02:00
/ / Variables globales pour stocker les marqueurs d ' événements et le premier chargement
2025-09-26 11:57:54 +02:00
window . eventMarkers = [ ] ;
2025-09-26 17:38:30 +02:00
window . isFirstLoad = true ;
2025-09-26 11:57:54 +02:00
function addEventsToMap ( geojsonData ) {
if ( ! geojsonData | | ! geojsonData . features ) return ;
geojsonData . features . forEach ( feature = > {
2025-09-27 00:18:03 +02:00
/ / Créer un élément HTML pour le marqueur avec emoji et forme de goutte
2025-09-26 11:57:54 +02:00
const el = document . createElement ( ' div ' ) ;
2025-09-27 00:18:03 +02:00
el . className = ' custom-marker ' ;
2025-09-26 11:57:54 +02:00
2025-09-27 00:18:03 +02:00
/ / Créer la forme de goutte en arrière - plan
el . innerHTML = `
< div class = " marker-drop " >
< div class = " marker-emoji " > $ { getEventEmoji ( feature . properties ) } < / div >
< / div >
` ;
2025-09-26 11:57:54 +02:00
/ / 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
2025-09-27 01:10:47 +02:00
const title = properties . label | | properties . title | | ' Événement sans titre ' ;
2025-09-26 11:57:54 +02:00
const what = properties . what | | ' Non spécifié ' ;
const when = properties . when ? formatDate ( properties . when ) : ' Date inconnue ' ;
const description = properties . description | | ' Aucune description disponible ' ;
2025-09-27 01:10:47 +02:00
/ / Créer le HTML de la popup avec titre cliquable pour édition
const editLink = properties . id ? ` / demo / edit / $ { properties . id } ` : ' # ' ;
2025-09-26 11:57:54 +02:00
return `
< div class = " event-popup " >
2025-09-27 01:10:47 +02:00
< h3 style = " margin-top: 0; " >
< a href = " $ {editLink} " style = " color: #0078ff; text-decoration: none; " title = " Cliquer pour modifier cet événement " >
$ { title }
< / a >
< / h3 >
2025-09-26 11:57:54 +02:00
< p > < strong > Type : < / strong > $ { what } < / p >
< p > < strong > Date : < / strong > $ { when } < / p >
< p > < strong > Description : < / strong > $ { description } < / p >
2025-09-27 01:10:47 +02:00
$ { properties . id ? ` < p > < a href = " /demo/edit/$ {properties.id} " style = " color: #0078ff; font-weight: bold; " > ✏ ️ Modifier l ' événement</a></p>` : ' ' }
2025-09-26 11:57:54 +02:00
< / div >
` ;
}
function formatDate ( dateString ) {
try {
const date = new Date ( dateString ) ;
return date . toLocaleString ( ) ;
} catch ( e ) {
return dateString ;
}
}
2025-09-27 00:18:03 +02:00
/ / Fonction pour lire les paramètres de requête
function getUrlParams ( ) {
const params = new URLSearchParams ( window . location . search ) ;
return {
lat : params . get ( ' lat ' ) ,
lon : params . get ( ' lon ' ) ,
zoom : params . get ( ' zoom ' )
} ;
}
/ / Fonction pour mettre à jour l ' URL avec les paramètres de position de la carte
function updateUrlParams ( lat , lon , zoom ) {
const url = new URL ( window . location ) ;
url . searchParams . set ( ' lat ' , lat . toFixed ( 6 ) ) ;
url . searchParams . set ( ' lon ' , lon . toFixed ( 6 ) ) ;
url . searchParams . set ( ' zoom ' , zoom . toFixed ( 2 ) ) ;
window . history . replaceState ( null , ' ' , url ) ;
}
/ / Vérifier si des paramètres de position sont présents dans l ' URL
const urlParams = getUrlParams ( ) ;
const hasPositionParams = urlParams . lat & & urlParams . lon & & urlParams . zoom ;
2025-09-21 13:35:01 +02:00
/ / Map style URLs
const mapStyles = {
2025-09-23 11:26:44 +02:00
default : ' https://tiles.openfreemap.org/styles/liberty ' ,
2025-09-21 13:35:01 +02:00
osmVector : ' https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@master/style-cdn.json ' ,
osmRaster : {
version : 8 ,
sources : {
' osm-raster ' : {
type : ' raster ' ,
tiles : [
' https://tile.openstreetmap.org/ {z} / {x} / {y} .png '
] ,
tileSize : 256 ,
2025-09-21 17:30:47 +02:00
attribution : ' © <a href= " https://www.openstreetmap.org/copyright " >OpenStreetMap</a> contributors '
2025-09-21 13:35:01 +02:00
}
} ,
layers : [
{
id : ' osm-raster-layer ' ,
type : ' raster ' ,
source : ' osm-raster ' ,
minzoom : 0 ,
maxzoom : 19
}
]
}
} ;
2025-09-27 00:18:03 +02:00
/ / Déterminer le centre et le zoom initial
let initialCenter = [ 2.3522 , 48.8566 ] ; / / Default center ( Paris )
let initialZoom = 4 ;
if ( hasPositionParams ) {
initialCenter = [ parseFloat ( urlParams . lon ) , parseFloat ( urlParams . lat ) ] ;
initialZoom = parseFloat ( urlParams . zoom ) ;
console . log ( ` 📍 Position depuis l ' URL: lat=$ {urlParams.lat} , lon=$ {urlParams.lon} , zoom=$ {urlParams.zoom} `);
}
2025-09-21 13:35:01 +02:00
/ / Initialize the map with default style
const map = new maplibregl . Map ( {
container : ' map ' ,
style : mapStyles . default ,
2025-09-27 00:18:03 +02:00
center : initialCenter ,
zoom : initialZoom
2025-09-21 13:35:01 +02:00
} ) ;
/ / Add navigation controls
map . addControl ( new maplibregl . NavigationControl ( ) ) ;
2025-09-23 12:53:52 +02:00
/ / Add geolocation control
map . addControl ( new maplibregl . GeolocateControl ( {
positionOptions : {
enableHighAccuracy : true
} ,
trackUserLocation : true ,
showUserHeading : true
} ) ) ;
2025-09-21 13:35:01 +02:00
/ / Add attribution control with OpenStreetMap attribution
map . addControl ( new maplibregl . AttributionControl ( {
2025-09-21 17:30:47 +02:00
customAttribution : ' © <a href= " https://www.openstreetmap.org/copyright " >OpenStreetMap</a> contributors '
2025-09-21 13:35:01 +02:00
} ) ) ;
2025-09-27 00:18:03 +02:00
/ / Ajouter un listener pour mettre à jour l ' URL quand la carte bouge
let updateUrlTimeout ;
map . on ( ' moveend ' , function ( ) {
/ / Utiliser un timeout pour éviter de trop nombreuses mises à jour
clearTimeout ( updateUrlTimeout ) ;
updateUrlTimeout = setTimeout ( ( ) = > {
const center = map . getCenter ( ) ;
const zoom = map . getZoom ( ) ;
updateUrlParams ( center . lat , center . lng , zoom ) ;
} , 300 ) ; / / Attendre 300 ms après la fin du mouvement
} ) ;
2025-09-21 13:35:01 +02:00
/ / Style switcher functionality
let currentStyle = ' default ' ;
let eventsData = null ;
2025-09-26 11:57:54 +02:00
let histogramChart = null ;
let refreshIntervalId = null ;
2025-09-23 12:53:52 +02:00
2025-09-26 11:57:54 +02:00
/ / Store markers with their family / type for filtering
2025-09-23 12:53:52 +02:00
let currentMarkers = [ ] ;
2025-09-23 12:18:10 +02:00
2025-09-26 11:57:54 +02:00
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
}
}
2025-09-23 12:18:10 +02:00
2025-09-21 13:35:01 +02:00
2025-09-23 12:53:52 +02:00
/ / Fetch events when the map is loaded and every 30 seconds thereafter
2025-09-21 13:35:01 +02:00
map . on ( ' load ' , function ( ) {
2025-09-23 12:53:52 +02:00
/ / Initial fetch
2025-09-21 13:35:01 +02:00
fetchEvents ( ) ;
2025-09-23 12:53:52 +02:00
/ / Set up interval to fetch events every 30 seconds
2025-09-26 11:57:54 +02:00
setupAutoRefresh ( ) ;
2025-09-23 12:53:52 +02:00
console . log ( ' Event refresh interval set: events will update every 30 seconds ' ) ;
2025-09-21 13:35:01 +02:00
} ) ;
2025-09-26 11:57:54 +02:00
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 ( ) ; } ) ;
}
2025-09-21 13:35:01 +02:00
/ / Function to fetch events from the API
function fetchEvents ( ) {
2025-09-26 17:38:30 +02:00
console . log ( ' 🔄 Chargement des événements... ' , isFirstLoad ? ' (Premier chargement) ' : ' (Rechargement) ' ) ;
2025-09-23 12:01:30 +02:00
/ / Fetch events from the API - using the local API endpoint
2025-09-23 12:19:03 +02:00
fetch ( ' https://api.openeventdatabase.org/event? ' )
2025-09-21 13:35:01 +02:00
. then ( response = > response . json ( ) )
. then ( data = > {
if ( data . features & & data . features . length > 0 ) {
/ / Add events to the map
addEventsToMap ( data ) ;
2025-09-26 11:57:54 +02:00
/ / Render histogram for retrieved events
try { renderEventsHistogram ( data . features ) ; } catch ( e ) { console . warn ( ' Histogram error ' , e ) ; }
2025-09-26 17:38:30 +02:00
/ / Fit map to events bounds ( seulement au premier chargement )
2025-09-21 13:35:01 +02:00
fitMapToBounds ( data ) ;
} else {
console . log ( ' No events found ' ) ;
}
} )
. catch ( error = > {
console . error ( ' Error fetching events: ' , error ) ;
2025-09-23 12:01:30 +02:00
/ / Afficher le toast d ' erreur
showErrorToast ( ` Erreur de chargement des événements : $ { error . message } ` ) ;
2025-09-21 13:35:01 +02:00
} ) ;
}
2025-09-26 11:57:54 +02:00
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 }
}
}
} ) ;
}
2025-09-21 13:35:01 +02:00
/ / Function to add events to the map
function addEventsToMap ( geojson ) {
2025-09-23 12:53:52 +02:00
/ / Remove all existing markers
if ( currentMarkers . length > 0 ) {
2025-09-26 11:57:54 +02:00
currentMarkers . forEach ( rec = > rec . marker . remove ( ) ) ;
2025-09-23 12:53:52 +02:00
currentMarkers = [ ] ;
console . log ( ' Removed existing markers ' ) ;
}
2025-09-21 13:35:01 +02:00
2025-09-23 12:53:52 +02:00
/ / Check if the source already exists
if ( map . getSource ( ' events ' ) ) {
/ / Update the existing source with new data
map . getSource ( ' events ' ) . setData ( geojson ) ;
console . log ( ' Updated existing events source with new data ' ) ;
} else {
/ / Add a new GeoJSON source for events
map . addSource ( ' events ' , {
type : ' geojson ' ,
data : geojson
} ) ;
/ / Add a circle layer for events
map . addLayer ( {
id : ' events-circle ' ,
type : ' circle ' ,
source : ' events ' ,
paint : {
' circle-radius ' : 8 ,
' circle-color ' : ' #FF5722 ' ,
' circle-stroke-width ' : 2 ,
' circle-stroke-color ' : ' #FFFFFF '
}
} ) ;
console . log ( ' Added new events source and layer ' ) ;
}
2025-09-21 13:35:01 +02:00
/ / Add popups for events
geojson . features . forEach ( feature = > {
const coordinates = feature . geometry . coordinates . slice ( ) ;
const properties = feature . properties ;
/ / Create popup content
let popupContent = ' <div class= " event-popup " > ' ;
2025-09-27 01:10:47 +02:00
const eventTitle = properties . label | | ' Event ' ;
const editLink = properties . id ? ` / demo / edit / $ { properties . id } ` : ' # ' ;
popupContent + = ` < h3 > < a href = " $ {editLink} " style = " color: #0078ff; text-decoration: none; " title = " Cliquer pour modifier cet événement " > $ { eventTitle } < / a > < / h3 > ` ;
2025-09-21 13:35:01 +02:00
/ / Display all properties
popupContent + = ' <div style= " max-height: 300px; overflow-y: auto; " > ' ;
popupContent + = ' <table style= " width: 100 % ; border-collapse: collapse; " > ' ;
/ / Sort properties alphabetically for better organization
const sortedKeys = Object . keys ( properties ) . sort ( ) ;
for ( const key of sortedKeys ) {
/ / Skip the label as it ' s already displayed as the title
if ( key == = ' label ' ) continue ;
const value = properties [ key ] ;
let displayValue ;
/ / Format the value based on its type
if ( value == = null | | value == = undefined ) {
displayValue = ' <em>null</em> ' ;
} else if ( typeof value == = ' object ' ) {
displayValue = ` < pre style = " margin: 0; white-space: pre-wrap; " > $ { JSON . stringify ( value , null , 2 ) } < / pre > ` ;
} else if ( typeof value == = ' string ' & & value . startsWith ( ' http ' ) ) {
2025-09-21 17:30:47 +02:00
displayValue = ` < a href = " $ {value} " > $ { value } < / a > ` ;
2025-09-21 19:18:43 +02:00
} else if ( typeof value == = ' string ' & & ( key == = ' start ' | | key == = ' stop ' | | key . includes ( ' date ' ) | | key . includes ( ' time ' ) ) ) {
/ / For date fields , show both the original date and the relative time
const relativeTime = getRelativeTimeString ( value ) ;
displayValue = ` $ { value } < span style = " color: #666; font-style: italic; " > ( il y a $ { relativeTime } ) < / span > ` ;
2025-09-21 13:35:01 +02:00
} else {
displayValue = String ( value ) ;
}
popupContent + = `
< tr style = " border-bottom: 1px solid #eee; " >
< td style = " padding: 4px; font-weight: bold; vertical-align: top; " > $ { key } : < / td >
< td style = " padding: 4px; " > $ { displayValue } < / td >
< / tr > ` ;
}
popupContent + = ' </table> ' ;
popupContent + = ' </div> ' ;
2025-09-21 23:59:01 +02:00
/ / Check if this event needs reality check ( traffic events created more than 1 hour ago )
const needsRealityCheck = checkIfNeedsRealityCheck ( feature ) ;
/ / Add reality check buttons if needed
if ( needsRealityCheck ) {
popupContent + = `
< div class = " reality-check " >
< p > Is this traffic event still present ? < / p >
< div class = " reality-check-buttons " >
< button class = " confirm-btn " onclick = " confirmEvent( ' $ {properties.id} ' , true) " > Yes , still there < / button >
< button class = " deny-btn " onclick = " confirmEvent( ' $ {properties.id} ' , false) " > No , it ' s gone</button>
< / div >
< / div >
` ;
} else if ( properties [ ' reality_check ' ] ) {
/ / Show reality check information if it exists
popupContent + = `
< div class = " reality-check-info " >
< p > Reality check : $ { properties [ ' reality_check ' ] } < / p >
< / div >
` ;
}
2025-09-21 13:35:01 +02:00
/ / Add edit link
popupContent + = ` < div style = " margin-top: 10px; text-align: center; " >
< a href = " /demo/edit/$ {properties.id} " class = " edit-event-btn " style = " display: inline-block; padding: 5px 10px; background-color: #0078ff; color: white; border-radius: 4px; text-decoration: none; font-weight: bold; " > Edit Event < / a >
< / div > ` ;
popupContent + = ' </div> ' ;
/ / Create popup
const popup = new maplibregl . Popup ( {
closeButton : true ,
closeOnClick : true
} ) . setHTML ( popupContent ) ;
/ / Get event type for icon selection
const eventType = properties . what | | ' unknown ' ;
/ / Define icon based on event type
let iconClass = ' info-circle ' ; / / Default icon
let iconColor = ' #0078ff ' ; / / Default color
/ / Map event types to icons
2025-09-26 11:57:54 +02:00
/ / 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 ' ) ) {
2025-09-21 13:35:01 +02:00
iconClass = ' cloud ' ;
iconColor = ' #00d1b2 ' ; / / Teal
} else if ( eventType . startsWith ( ' traffic ' ) ) {
iconClass = ' car ' ;
iconColor = ' #ff3860 ' ; / / Red
} else if ( eventType . startsWith ( ' sport ' ) ) {
iconClass = ' futbol ' ;
iconColor = ' #3273dc ' ; / / Blue
} else if ( eventType . startsWith ( ' culture ' ) ) {
iconClass = ' theater-masks ' ;
iconColor = ' #ffdd57 ' ; / / Yellow
} else if ( eventType . startsWith ( ' health ' ) ) {
iconClass = ' heartbeat ' ;
iconColor = ' #ff3860 ' ; / / Red
} else if ( eventType . startsWith ( ' education ' ) ) {
iconClass = ' graduation-cap ' ;
iconColor = ' #3273dc ' ; / / Blue
} else if ( eventType . startsWith ( ' politics ' ) ) {
iconClass = ' landmark ' ;
iconColor = ' #209cee ' ; / / Light blue
} else if ( eventType . startsWith ( ' nature ' ) ) {
iconClass = ' leaf ' ;
iconColor = ' #23d160 ' ; / / Green
}
2025-09-27 00:18:03 +02:00
/ / Create custom HTML element for marker with emoji and drop shape
2025-09-21 13:35:01 +02:00
const el = document . createElement ( ' div ' ) ;
2025-09-27 00:18:03 +02:00
el . className = ' custom-marker ' ;
/ / Créer la forme de goutte en arrière - plan avec emoji
el . innerHTML = `
< div class = " marker-drop " >
< div class = " marker-emoji " > $ { getEventEmoji ( properties ) } < / div >
< / div >
` ;
2025-09-21 13:35:01 +02:00
2025-09-23 12:53:52 +02:00
/ / Add marker with popup and store reference
const marker = new maplibregl . Marker ( el )
. setLngLat ( coordinates )
. setPopup ( popup )
. addTo ( map ) ;
2025-09-26 11:57:54 +02:00
/ / Store marker with its family for filtering
currentMarkers . push ( { marker , family : getFamily ( eventType ) } ) ;
2025-09-21 13:35:01 +02:00
} ) ;
2025-09-26 11:57:54 +02:00
/ / Re - apply current filter on fresh markers
applyTypeFilter ( ) ;
2025-09-21 13:35:01 +02:00
}
2025-09-21 19:18:43 +02:00
/ / Function to calculate relative time ( e . g . , " 2 hours 30 minutes ago " )
function getRelativeTimeString ( dateString ) {
if ( ! dateString ) return ' ' ;
/ / Parse the date string
const date = new Date ( dateString ) ;
if ( isNaN ( date . getTime ( ) ) ) return dateString ; / / Return original if invalid
/ / Calculate time difference in milliseconds
const now = new Date ( ) ;
const diffMs = now - date ;
/ / Convert to hours and minutes
const diffHours = Math . floor ( diffMs / ( 1000 * 60 * 60 ) ) ;
const diffMinutes = Math . floor ( ( diffMs % ( 1000 * 60 * 60 ) ) / ( 1000 * 60 ) ) ;
/ / Format the relative time string
let relativeTime = ' ' ;
if ( diffHours > 0 ) {
relativeTime + = ` $ { diffHours } heure $ { diffHours > 1 ? ' s ' : ' ' } ` ;
}
if ( diffMinutes > 0 | | diffHours == = 0 ) {
if ( diffHours > 0 ) relativeTime + = ' ' ;
relativeTime + = ` $ { diffMinutes } minute $ { diffMinutes > 1 ? ' s ' : ' ' } ` ;
}
return relativeTime | | " à l instant " ;
}
2025-09-21 23:59:01 +02:00
/ / Function to check if an event needs a reality check ( created more than 1 hour ago )
function checkIfNeedsRealityCheck ( event ) {
2025-09-22 11:44:25 +02:00
/ / Skip if event already has a reality check
if ( event . properties [ ' reality_check ' ] ) {
return false ;
}
/ / Only for traffic events
2025-09-21 23:59:01 +02:00
if ( ! event . properties . what | | ! event . properties . what . startsWith ( ' traffic ' ) ) {
return false ;
}
2025-09-22 11:44:25 +02:00
/ / Must have a creation date
const createDate = event . properties . createdate ;
if ( ! createDate ) {
return false ;
}
const createTime = new Date ( createDate ) . getTime ( ) ;
if ( isNaN ( createTime ) ) {
return false ;
}
const currentTime = new Date ( ) . getTime ( ) ;
const oneHourInMs = 60 * 60 * 1000 ;
return ( currentTime - createTime ) > oneHourInMs ;
2025-09-21 23:59:01 +02:00
}
2025-09-26 17:38:30 +02:00
/ / Function to fit map to events bounds ( only on first load )
2025-09-21 13:35:01 +02:00
function fitMapToBounds ( geojson ) {
2025-09-26 17:38:30 +02:00
if ( geojson . features . length == = 0 | | ! window . isFirstLoad ) return ;
console . log ( ' 🎯 Premier chargement - Ajustement de la vue sur les événements ' ) ;
2025-09-21 13:35:01 +02:00
/ / Create a bounds object
const bounds = new maplibregl . LngLatBounds ( ) ;
2025-09-26 17:38:30 +02:00
2025-09-21 13:35:01 +02:00
/ / Extend bounds with each feature
geojson . features . forEach ( feature = > {
bounds . extend ( feature . geometry . coordinates ) ;
} ) ;
2025-09-26 17:38:30 +02:00
2025-09-21 13:35:01 +02:00
/ / Fit map to bounds with padding
map . fitBounds ( bounds , {
padding : 50 ,
maxZoom : 12
} ) ;
2025-09-26 17:38:30 +02:00
/ / Marquer que le premier chargement est terminé
window . isFirstLoad = false ;
console . log ( ' ✅ Vue initiale définie, les prochains rafraîchissements ne déplaceront plus la carte ' ) ;
2025-09-21 13:35:01 +02:00
}
2025-09-21 23:59:01 +02:00
/ / Function to update user information display
function updateUserInfoDisplay ( ) {
const username = localStorage . getItem ( ' oedb_username ' ) ;
const points = localStorage . getItem ( ' oedb_points ' ) ;
const userInfoPanel = document . getElementById ( ' user-info-panel ' ) ;
/ / Only show the panel if the user has a username or points
if ( username | | points ) {
userInfoPanel . style . display = ' block ' ;
/ / Update username display
if ( username ) {
document . getElementById ( ' username-display ' ) . textContent = username ;
}
/ / Update points display
if ( points ) {
document . getElementById ( ' points-display ' ) . textContent = points ;
}
}
/ / Add CSS for reality check buttons if not already added
if ( ! document . getElementById ( ' reality-check-styles ' ) ) {
const style = document . createElement ( ' style ' ) ;
style . id = ' reality-check-styles ' ;
style . textContent = `
. reality - check {
margin - top : 10 px ;
padding : 10 px ;
background - color : #fff3e0;
border - radius : 4 px ;
}
. reality - check - buttons {
display : flex ;
justify - content : space - between ;
margin - top : 8 px ;
}
. confirm - btn , . deny - btn {
padding : 5 px 10 px ;
border : none ;
border - radius : 4 px ;
cursor : pointer ;
font - weight : bold ;
}
. confirm - btn {
background - color : #4caf50;
color : white ;
}
. deny - btn {
background - color : #f44336;
color : white ;
}
. reality - check - info {
margin - top : 10 px ;
padding : 8 px ;
background - color : #e8f5e9;
border - radius : 4 px ;
font - size : 0.9 em ;
}
` ;
document . head . appendChild ( style ) ;
}
}
/ / Function to handle event confirmation or denial
function confirmEvent ( eventId , isConfirmed ) {
/ / Get username from localStorage or prompt for it
let username = localStorage . getItem ( ' oedb_username ' ) ;
if ( ! username ) {
username = promptForUsername ( ) ;
if ( ! username ) {
/ / User cancelled the prompt
return ;
}
}
/ / Current date and time
const now = new Date ( ) ;
const dateTimeString = now . toISOString ( ) ;
/ / Create reality check string
const realityCheckStatus = isConfirmed ? ' confirmed ' : ' not confirmed ' ;
const realityCheckValue = ` $ { dateTimeString } | $ { username } | $ { realityCheckStatus } ` ;
/ / Fetch the event to update
fetch ( ` https : / / api . openeventdatabase . org / event / $ { eventId } ` )
. then ( response = > {
if ( response . ok ) {
return response . json ( ) ;
} else {
throw new Error ( ` Failed to fetch event $ { eventId } ` ) ;
}
} )
. then ( event = > {
/ / Add reality_check property
event . properties [ ' reality_check ' ] = realityCheckValue ;
/ / Update the event
return fetch ( ` https : / / api . openeventdatabase . org / event / $ { eventId } ` , {
method : ' PUT ' ,
headers : {
' Content-Type ' : ' application/json '
} ,
body : JSON . stringify ( event )
} ) ;
} )
. then ( response = > {
if ( response . ok ) {
/ / Save contribution to localStorage
saveContribution ( eventId , isConfirmed ) ;
/ / Award points
awardPoints ( 3 ) ;
/ / Show success message
alert ( ` Thank you for your contribution ! You ' ve earned 3 points.`);
/ / Update user info display
updateUserInfoDisplay ( ) ;
/ / Refresh events to update the display
fetchEvents ( ) ;
} else {
throw new Error ( ' Failed to update event ' ) ;
}
} )
. catch ( error = > {
console . error ( ' Error updating event: ' , error ) ;
alert ( ` Error : $ { error . message } ` ) ;
} ) ;
}
/ / Function to prompt for username
function promptForUsername ( ) {
const username = prompt ( ' Please enter your username: ' ) ;
if ( username ) {
localStorage . setItem ( ' oedb_username ' , username ) ;
return username ;
}
return null ;
}
/ / Function to save contribution to localStorage
function saveContribution ( eventId , isConfirmed ) {
/ / Get existing contributions
let contributions = JSON . parse ( localStorage . getItem ( ' oedb_contributions ' ) | | ' [] ' ) ;
/ / Add new contribution
contributions . push ( {
eventId : eventId ,
timestamp : new Date ( ) . toISOString ( ) ,
isConfirmed : isConfirmed
} ) ;
/ / Save back to localStorage
localStorage . setItem ( ' oedb_contributions ' , JSON . stringify ( contributions ) ) ;
}
/ / Function to award points
function awardPoints ( points ) {
/ / Get current points
let currentPoints = parseInt ( localStorage . getItem ( ' oedb_points ' ) | | ' 0 ' ) ;
/ / Add new points
currentPoints + = points ;
/ / Save back to localStorage
localStorage . setItem ( ' oedb_points ' , currentPoints . toString ( ) ) ;
}
/ / Update user info when the page loads
document . addEventListener ( ' DOMContentLoaded ' , function ( ) {
updateUserInfoDisplay ( ) ;
2025-09-23 12:01:30 +02:00
/ / Initialisation des gestionnaires d ' événements pour le toast d ' erreur
initErrorToast ( ) ;
2025-09-26 11:57:54 +02:00
/ / 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 ) ;
2025-09-21 23:59:01 +02:00
} ) ;
2025-09-23 12:01:30 +02:00
/ / Fonction pour initialiser le toast d ' erreur
function initErrorToast ( ) {
const errorToast = document . getElementById ( ' error-toast ' ) ;
const closeButton = errorToast . querySelector ( ' .error-toast-close ' ) ;
/ / Fermer le toast au clic sur le bouton de fermeture
closeButton . addEventListener ( ' click ' , function ( ) {
errorToast . classList . remove ( ' show ' ) ;
} ) ;
}
/ / Fonction pour afficher le toast d ' erreur
function showErrorToast ( message ) {
const errorToast = document . getElementById ( ' error-toast ' ) ;
const messageElement = document . getElementById ( ' error-message ' ) ;
/ / Définir le message d ' erreur
messageElement . textContent = message ;
/ / Afficher le toast
errorToast . classList . add ( ' show ' ) ;
/ / Faire disparaître le toast après 6 secondes
setTimeout ( function ( ) {
errorToast . classList . remove ( ' show ' ) ;
} , 6000 ) ;
}
2025-09-23 12:18:10 +02:00
2025-09-26 11:57:54 +02:00
/ / 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é
}
} ) ;
2025-09-21 13:35:01 +02:00
< / script >
< / body >
< / html >
"""
# Set the response body and status
resp . text = html
resp . status = falcon . HTTP_200
logger . success ( " Successfully processed GET request to /demo " )
except Exception as e :
logger . error ( f " Error processing GET request to /demo: { e } " )
resp . status = falcon . HTTP_500
resp . text = f " Error: { str ( e ) } "
# Create a global instance of DemoMainResource
demo_main = DemoMainResource ( )