This commit is contained in:
Tykayn 2025-09-26 17:38:30 +02:00 committed by tykayn
parent 2bb77d2300
commit 98c40b2447
16 changed files with 1836 additions and 361 deletions

View file

@ -88,6 +88,7 @@ def create_app():
app.add_route('/demo/add', event_form) # Handle event submission form
app.add_route('/demo/by-what', demo, suffix='by_what') # Handle events by type page
app.add_route('/demo/map-by-what', demo, suffix='map_by_what') # Handle map by event type page
app.add_route('/demo/map-by-what/{event_type}', demo, suffix='map_by_what_type') # Handle map by specific event type
app.add_route('/demo/edit/{id}', demo, suffix='edit') # Handle event editing page
app.add_route('/demo/by_id/{id}', demo, suffix='by_id') # Handle view single event by id
app.add_route('/demo/traffic', demo, suffix='traffic') # Handle traffic jam reporting page

160
doc/api_endpoints.md Normal file
View file

@ -0,0 +1,160 @@
# API Endpoints Documentation
This document provides a comprehensive list of all API endpoints available in the OpenEventDatabase (OEDB) backend.
## Main API Endpoints
### Root Endpoint
- **URL**: `/`
- **Method**: GET
- **Description**: Provides general information about the API, including version and available endpoints.
- **Response**: JSON object with API information.
### Event Endpoints
#### Get Events
- **URL**: `/event`
- **Method**: GET
- **Description**: Retrieves a collection of events based on query parameters.
- **Query Parameters**:
- `what`: Filter events by type (e.g., `sport.match.football`)
- `when`: Filter events by time period (e.g., `last7days`, `today`, `tomorrow`)
- `limit`: Maximum number of events to return (default: 100)
- `bbox`: Bounding box for geographic filtering (format: `min_lon,min_lat,max_lon,max_lat`)
- **Response**: GeoJSON FeatureCollection of events.
#### Create Event
- **URL**: `/event`
- **Method**: POST
- **Description**: Creates a new event.
- **Request Body**: GeoJSON Feature representing the event.
- **Response**: JSON object with the created event ID.
#### Get Event by ID
- **URL**: `/event/{id}`
- **Method**: GET
- **Description**: Retrieves a specific event by its ID.
- **Response**: GeoJSON Feature of the requested event.
#### Update Event
- **URL**: `/event/{id}`
- **Method**: PUT
- **Description**: Updates an existing event.
- **Request Body**: GeoJSON Feature with updated event data.
- **Response**: JSON object with the updated event ID.
#### Delete Event
- **URL**: `/event/{id}`
- **Method**: DELETE
- **Description**: Deletes an event by its ID.
- **Response**: JSON object confirming deletion.
#### Search Events
- **URL**: `/event/search`
- **Method**: GET
- **Description**: Advanced search for events with more complex filtering options.
- **Query Parameters**: Various search parameters (see `/doc/api_search.md` for details).
- **Response**: GeoJSON FeatureCollection of matching events.
### Statistics Endpoint
- **URL**: `/stats`
- **Method**: GET
- **Description**: Provides statistics about the database, including event counts and recent updates.
- **Response**: JSON object with statistics.
### RSS Feeds
#### Latest Events RSS
- **URL**: `/rss`
- **Method**: GET
- **Description**: Provides an RSS feed of the latest 200 events.
- **Response**: XML RSS feed.
#### Events by Family RSS
- **URL**: `/rss/by/{family}`
- **Method**: GET
- **Description**: Provides an RSS feed of events filtered by family (e.g., sport, culture).
- **Response**: XML RSS feed.
### Database Dumps
#### List Database Dumps
- **URL**: `/db/dumps`
- **Method**: GET
- **Description**: Lists all available database dumps.
- **Response**: JSON object with list of dumps.
#### Create Database Dumps
- **URL**: `/db/dumps/create`
- **Method**: POST
- **Description**: Creates new database dumps in SQL and GeoJSON formats.
- **Response**: JSON object with information about the created dumps.
## Demo/UI Endpoints
These endpoints provide web interfaces for interacting with the API:
### Main Demo Page
- **URL**: `/demo`
- **Method**: GET
- **Description**: Main demo page with a map interface to explore events.
### Event Submission Form
- **URL**: `/demo/add`
- **Method**: GET
- **Description**: Form for submitting new events.
### Events by Type
- **URL**: `/demo/by-what`
- **Method**: GET
- **Description**: Page showing events grouped by their type.
### Map by Event Type
- **URL**: `/demo/map-by-what`
- **Method**: GET
- **Description**: Map interface showing events colored by their type.
### Event Editing
- **URL**: `/demo/edit/{id}`
- **Method**: GET
- **Description**: Interface for editing an existing event.
### View Event by ID
- **URL**: `/demo/by_id/{id}`
- **Method**: GET
- **Description**: Page showing details of a specific event.
### Traffic Jam Reporting
- **URL**: `/demo/traffic`
- **Method**: GET
- **Description**: Interface for reporting traffic jams.
### View Saved Events
- **URL**: `/demo/view-events`
- **Method**: GET
- **Description**: Page showing events saved by the user.
### Statistics by Event Type
- **URL**: `/demo/stats`
- **Method**: GET
- **Description**: Page showing statistics grouped by event type.
### Live Events
- **URL**: `/demo/live`
- **Method**: GET
- **Description**: Real-time view of events with auto-refresh.
## Static Files
- **URL**: `/static/`
- **Method**: GET
- **Description**: Serves static files (CSS, JavaScript, images) for the demo interfaces.
## Additional Documentation
For more detailed information about specific aspects of the API, refer to these documents:
- [API Query Parameters](/doc/api_query_params.md)
- [API Search](/doc/api_search.md)
- [API Load Testing](/doc/api_load_test.md)
- [Anti-Spam Measures](/doc/anti_spam.md)
- [Demo Endpoint](/doc/demo_endpoint.md)

View file

@ -37,9 +37,10 @@ logging.basicConfig(
logger = logging.getLogger(__name__)
class AgendaGeekScraper:
def __init__(self, api_url: str = "https://api.openeventdatabase.org", dry_run: bool = False):
def __init__(self, api_url: str = "https://api.openeventdatabase.org", dry_run: bool = False, page: int = 1):
self.api_url = api_url.rstrip('/')
self.dry_run = dry_run
self.page = page
self.session = requests.Session()
self.session.headers.update({
'User-Agent': 'OEDB-AgendaGeek-Scraper/1.0 (+https://github.com/cquest/oedb)'
@ -47,7 +48,7 @@ class AgendaGeekScraper:
def get_events_list(self) -> List[str]:
"""Récupère la liste des liens d'événements depuis la page principale"""
url = "https://lagendageek.com/tevents/page/10"
url = f"https://lagendageek.com/tevents/page/{self.page}"
logger.info(f"🔍 Récupération de la liste des événements depuis {url}")
try:
@ -395,7 +396,8 @@ class AgendaGeekScraper:
def main():
parser = argparse.ArgumentParser(description='Scraper Agenda Geek vers OEDB')
parser.add_argument('--limit', type=int, default=5, help='Nombre d\'événements à traiter')
parser.add_argument('--limit', type=int, default=20, help='Nombre d\'événements à traiter')
parser.add_argument('--page', type=int, default=1, help='Numéro de page du site')
parser.add_argument('--offset', type=int, default=0, help='Nombre d\'événements à ignorer')
parser.add_argument('--api-url', default='https://api.openeventdatabase.org', help='URL de l\'API OEDB')
parser.add_argument('--dry-run', action='store_true', help='Mode test sans envoi vers l\'API')
@ -406,7 +408,7 @@ def main():
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
scraper = AgendaGeekScraper(api_url=args.api_url, dry_run=args.dry_run)
scraper = AgendaGeekScraper(api_url=args.api_url, dry_run=args.dry_run, page=args.page)
scraper.process_events(limit=args.limit, offset=args.offset)
if __name__ == "__main__":

View file

@ -0,0 +1,187 @@
2025-09-26 17:16:58,979 - INFO - 🚀 Début du traitement - Limite: 15, Offset: 5
2025-09-26 17:16:58,979 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/10
2025-09-26 17:17:02,288 - INFO - ✅ 20 événements trouvés sur la page
2025-09-26 17:17:02,288 - INFO - 📊 Traitement de 15 événements (6 à 20 sur 20)
2025-09-26 17:17:02,288 - INFO - 🔄 [1/15] Traitement de https://lagendageek.com/tevent/fusion-event/
2025-09-26 17:17:05,978 - INFO - ✅ Événement créé avec succès: ID aaacbb4a-be1c-467d-9f78-3db61861937b
2025-09-26 17:17:06,979 - INFO - 🔄 [2/15] Traitement de https://lagendageek.com/tevent/manga-ten-expo-pompaire/
2025-09-26 17:17:11,004 - INFO - ✅ Événement créé avec succès: ID f298700e-4f2b-4682-a978-a364be0cccfc
2025-09-26 17:17:12,004 - INFO - 🔄 [3/15] Traitement de https://lagendageek.com/tevent/geek-la-gaillarde/
2025-09-26 17:17:15,890 - INFO - ✅ Événement créé avec succès: ID 7ce8f4c0-56b4-4cb3-acf7-56e919d4b9d6
2025-09-26 17:17:16,890 - INFO - 🔄 [4/15] Traitement de https://lagendageek.com/tevent/geek-legends-lons-le-saunier-4/
2025-09-26 17:17:19,827 - INFO - 🚀 Début du traitement - Limite: 15, Offset: 5
2025-09-26 17:17:19,827 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/10
2025-09-26 17:17:20,787 - INFO - ✅ Événement créé avec succès: ID f96f9167-1271-4804-bebe-eb3f282c69b3
2025-09-26 17:17:21,788 - INFO - 🔄 [5/15] Traitement de https://lagendageek.com/tevent/korean-tours-festival-2026/
2025-09-26 17:17:23,000 - INFO - ✅ 20 événements trouvés sur la page
2025-09-26 17:17:23,000 - INFO - 📊 Traitement de 15 événements (6 à 20 sur 20)
2025-09-26 17:17:23,000 - INFO - 🔄 [1/15] Traitement de https://lagendageek.com/tevent/fusion-event/
2025-09-26 17:17:25,823 - INFO - ✅ Événement créé avec succès: ID 41e49d2f-a3f7-4b4c-886b-9c651fbad423
2025-09-26 17:17:26,824 - INFO - 🔄 [6/15] Traitement de https://lagendageek.com/tevent/festival-international-des-jeux-2026/
2025-09-26 17:17:26,978 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:27,979 - INFO - 🔄 [2/15] Traitement de https://lagendageek.com/tevent/manga-ten-expo-pompaire/
2025-09-26 17:17:30,822 - INFO - ✅ Événement créé avec succès: ID b1750ba1-fb5f-4a6f-974d-48dbad081ce6
2025-09-26 17:17:31,701 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:31,823 - INFO - 🔄 [7/15] Traitement de https://lagendageek.com/tevent/japan-expo-marseille-2026/
2025-09-26 17:17:32,702 - INFO - 🔄 [3/15] Traitement de https://lagendageek.com/tevent/geek-la-gaillarde/
2025-09-26 17:17:35,542 - INFO - ✅ Événement créé avec succès: ID 4e145c54-3aed-4ad1-8c76-a14a13019585
2025-09-26 17:17:36,311 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:36,543 - INFO - 🔄 [8/15] Traitement de https://lagendageek.com/tevent/manga-ten-expo-puilboreau/
2025-09-26 17:17:37,311 - INFO - 🔄 [4/15] Traitement de https://lagendageek.com/tevent/geek-legends-lons-le-saunier-4/
2025-09-26 17:17:40,290 - INFO - ✅ Événement créé avec succès: ID a7ac00a9-1946-48a9-afbe-2f388a274d32
2025-09-26 17:17:41,033 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:41,291 - INFO - 🔄 [9/15] Traitement de https://lagendageek.com/tevent/aka-to-kin-2026/
2025-09-26 17:17:42,033 - INFO - 🔄 [5/15] Traitement de https://lagendageek.com/tevent/korean-tours-festival-2026/
2025-09-26 17:17:45,558 - INFO - ✅ Événement créé avec succès: ID 3ede724b-acfc-4dc9-a7a6-4bbaf651ecf9
2025-09-26 17:17:46,088 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:46,558 - INFO - 🔄 [10/15] Traitement de https://lagendageek.com/tevent/un-week-end-au-soleil-levant-2026/
2025-09-26 17:17:47,088 - INFO - 🔄 [6/15] Traitement de https://lagendageek.com/tevent/festival-international-des-jeux-2026/
2025-09-26 17:17:50,631 - INFO - ✅ Événement créé avec succès: ID 4fbe7d72-b2df-4ea7-9e6a-39ee8f1f2714
2025-09-26 17:17:50,815 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:51,632 - INFO - 🔄 [11/15] Traitement de https://lagendageek.com/tevent/caramanga-2026/
2025-09-26 17:17:51,815 - INFO - 🔄 [7/15] Traitement de https://lagendageek.com/tevent/japan-expo-marseille-2026/
2025-09-26 17:17:55,267 - INFO - ✅ Événement créé avec succès: ID 097f54b9-e039-4493-8e1e-c472e3534c0e
2025-09-26 17:17:55,468 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:17:56,268 - INFO - 🔄 [12/15] Traitement de https://lagendageek.com/tevent/ready-set-romance-2026/
2025-09-26 17:17:56,468 - INFO - 🔄 [8/15] Traitement de https://lagendageek.com/tevent/manga-ten-expo-puilboreau/
2025-09-26 17:18:00,079 - INFO - ✅ Événement créé avec succès: ID 3688b1aa-8967-4f7a-816c-be66e026b55a
2025-09-26 17:18:00,236 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:01,080 - INFO - 🔄 [13/15] Traitement de https://lagendageek.com/tevent/manga-ten-expo-aiffres/
2025-09-26 17:18:01,236 - INFO - 🔄 [9/15] Traitement de https://lagendageek.com/tevent/aka-to-kin-2026/
2025-09-26 17:18:05,298 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:05,552 - INFO - ✅ Événement créé avec succès: ID 2c22c1fc-7736-4a7a-bc10-72c19d16e3e6
2025-09-26 17:18:06,299 - INFO - 🔄 [10/15] Traitement de https://lagendageek.com/tevent/un-week-end-au-soleil-levant-2026/
2025-09-26 17:18:06,552 - INFO - 🔄 [14/15] Traitement de https://lagendageek.com/tevent/paris-manga-sci-fi-show-39e-edition/
2025-09-26 17:18:10,028 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:10,635 - INFO - ✅ Événement créé avec succès: ID 50e4b035-b8cc-4d86-9d4c-e00804a26e4d
2025-09-26 17:18:11,029 - INFO - 🔄 [11/15] Traitement de https://lagendageek.com/tevent/caramanga-2026/
2025-09-26 17:18:11,636 - INFO - 🔄 [15/15] Traitement de https://lagendageek.com/tevent/symphony-on-titan-10/
2025-09-26 17:18:14,718 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:15,317 - INFO - ✅ Événement créé avec succès: ID 8b0e0708-803f-44b3-bc1c-a327e6678c02
2025-09-26 17:18:15,719 - INFO - 🔄 [12/15] Traitement de https://lagendageek.com/tevent/ready-set-romance-2026/
2025-09-26 17:18:16,317 - INFO - 🏁 Traitement terminé - Succès: 15, Erreurs: 0
2025-09-26 17:18:19,495 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:20,495 - INFO - 🔄 [13/15] Traitement de https://lagendageek.com/tevent/manga-ten-expo-aiffres/
2025-09-26 17:18:24,020 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:25,021 - INFO - 🔄 [14/15] Traitement de https://lagendageek.com/tevent/paris-manga-sci-fi-show-39e-edition/
2025-09-26 17:18:28,610 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:29,611 - INFO - 🔄 [15/15] Traitement de https://lagendageek.com/tevent/symphony-on-titan-10/
2025-09-26 17:18:33,074 - INFO - ⚠️ Événement déjà existant (conflit)
2025-09-26 17:18:34,074 - INFO - 🏁 Traitement terminé - Succès: 15, Erreurs: 0
2025-09-26 17:19:56,059 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-09-26 17:22:50,865 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-09-26 17:22:50,865 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/11
2025-09-26 17:22:54,108 - INFO - ✅ 20 événements trouvés sur la page
2025-09-26 17:22:54,108 - INFO - 📊 Traitement de 20 événements (1 à 20 sur 20)
2025-09-26 17:22:54,108 - INFO - 🔄 [1/20] Traitement de https://lagendageek.com/tevent/les-plus-belles-musiques-des-films-de-miyazaki-9/
2025-09-26 17:22:57,706 - INFO - 🏃‍♂️ DRY RUN - Événement qui serait envoyé:
2025-09-26 17:22:57,706 - INFO - {
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [
5.4540272,
43.5274379
]
},
"properties": {
"label": "Les Plus Belles Musiques des Films de Miyazaki",
"type": "scheduled",
"what": "culture.geek",
"start": "2026-03-14T20:30:00+00:00",
"stop": "2026-03-14T21:15:00+00:00",
"where": "Théâtre du jeu de paume, 21 rue de l'Opéra, Aix en Provence, 13100, France",
"description": "Quatuor de musiciens passionnés de pop culture, le Grissini project vous interprète les plus belles musiques des films danimation dHayao Miyazaki. \nAu programme, les plus beaux thèmes : “Le Voyage de Chihiro”, “Mon Voisin Totoro”, “Le Château ambulant”, “Princesse Mononoké”, “Le Vent se lève”, “Kiki la petite sorcière”, “Ponyo sur la falaise”,…\nEt tant dautres ! \nAu croisement de la musique classique et de la pop culture, une évasion le temps dun concert dans le Japon fantastique des studios Ghibli, au son des mélodies de Joe Hisaishi. \nLes Artistes :\nRomain Vaudé | Piano\nMaja Samuelsson ou Stella Siecinska | Chant\nFlorestan Raes | Violon\nClara Germont | Violoncelle",
"source:name": "L'Agenda Geek",
"source:url": "https://lagendageek.com/tevent/les-plus-belles-musiques-des-films-de-miyazaki-9/",
"source:uid": "85107-1773520200-1773522900@lagendageek.com",
"url": "https://lagendageek.com/tevent/les-plus-belles-musiques-des-films-de-miyazaki-9/"
}
}
2025-09-26 17:22:58,707 - INFO - 🔄 [2/20] Traitement de https://lagendageek.com/tevent/salon-du-manga-a-cambrai-2026/
2025-09-26 17:23:13,137 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-09-26 17:23:13,137 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/11
2025-09-26 17:23:17,425 - INFO - ✅ 20 événements trouvés sur la page
2025-09-26 17:23:17,425 - INFO - 📊 Traitement de 20 événements (1 à 20 sur 20)
2025-09-26 17:23:17,425 - INFO - 🔄 [1/20] Traitement de https://lagendageek.com/tevent/les-plus-belles-musiques-des-films-de-miyazaki-9/
2025-09-26 17:23:21,400 - INFO - ✅ Événement créé avec succès: ID 97318052-05d6-420a-b7a7-7323587c69cd
2025-09-26 17:23:22,400 - INFO - 🔄 [2/20] Traitement de https://lagendageek.com/tevent/salon-du-manga-a-cambrai-2026/
2025-09-26 17:23:26,785 - INFO - ✅ Événement créé avec succès: ID 91cde5d3-1856-482c-b26e-daccfe205776
2025-09-26 17:23:27,785 - INFO - 🔄 [3/20] Traitement de https://lagendageek.com/tevent/manga-ten-expo-montamise/
2025-09-26 17:23:31,599 - INFO - ✅ Événement créé avec succès: ID 561e571a-e8ff-4294-9983-bccef1b8a2e0
2025-09-26 17:23:32,599 - INFO - 🔄 [4/20] Traitement de https://lagendageek.com/tevent/gresimaginaire-2026/
2025-09-26 17:23:36,432 - INFO - ✅ Événement créé avec succès: ID 364686b5-2827-4549-b755-18c6edbf5e68
2025-09-26 17:23:37,433 - INFO - 🔄 [5/20] Traitement de https://lagendageek.com/tevent/geek-collector-vesoul-6/
2025-09-26 17:23:41,224 - INFO - ✅ Événement créé avec succès: ID e0e4a486-5558-498b-bb1c-39b02b95e843
2025-09-26 17:23:42,224 - INFO - 🔄 [6/20] Traitement de https://lagendageek.com/tevent/trolls-legendes-2026/
2025-09-26 17:23:45,859 - INFO - ✅ Événement créé avec succès: ID ad4f54c3-db63-48ae-abe2-c9030520b5f4
2025-09-26 17:23:46,860 - INFO - 🔄 [7/20] Traitement de https://lagendageek.com/tevent/manga-ten-expo-saint-leger-sous-cholet/
2025-09-26 17:23:50,504 - INFO - ✅ Événement créé avec succès: ID 856852dc-23bd-431b-b044-fd93a920c568
2025-09-26 17:23:51,504 - INFO - 🔄 [8/20] Traitement de https://lagendageek.com/tevent/angers-geekfest-2026/
2025-09-26 17:23:55,307 - INFO - ✅ Événement créé avec succès: ID 796cc0b3-b41f-48cf-bc73-38088388d75f
2025-09-26 17:23:56,308 - INFO - 🔄 [9/20] Traitement de https://lagendageek.com/tevent/tgs-montpellier-occitanie-game-show-2026/
2025-09-26 17:24:00,060 - INFO - ✅ Événement créé avec succès: ID c39a76c6-4eb3-4b2b-aad6-719dd4b95e7e
2025-09-26 17:24:01,060 - INFO - 🔄 [10/20] Traitement de https://lagendageek.com/tevent/marche-du-geek/
2025-09-26 17:24:05,081 - INFO - ✅ Événement créé avec succès: ID dcbcc6e4-4916-4a01-8751-d19a42b1b9cc
2025-09-26 17:24:06,082 - INFO - 🔄 [11/20] Traitement de https://lagendageek.com/tevent/necronomicon-2026-8eme-edition/
2025-09-26 17:24:09,879 - INFO - ✅ Événement créé avec succès: ID 0d0cdb50-921d-4ff3-91c0-0061e9b08df1
2025-09-26 17:24:10,879 - INFO - 🔄 [12/20] Traitement de https://lagendageek.com/tevent/gamefest-charleville-m-11/
2025-09-26 17:24:14,606 - INFO - ✅ Événement créé avec succès: ID 19fee274-bc24-4ca7-9839-0ab5c6ade607
2025-09-26 17:24:15,606 - INFO - 🔄 [13/20] Traitement de https://lagendageek.com/tevent/11eme-vide-grenier-du-geek-de-nancy/
2025-09-26 17:24:19,413 - INFO - ✅ Événement créé avec succès: ID 5c8cdb6d-2efe-4d12-aa2c-ad96f6a95f24
2025-09-26 17:24:20,413 - INFO - 🔄 [14/20] Traitement de https://lagendageek.com/tevent/luxcon-2026/
2025-09-26 17:24:23,984 - INFO - ✅ Événement créé avec succès: ID 39a2a818-44a4-4acc-9322-e26bf8f138fd
2025-09-26 17:24:24,984 - INFO - 🔄 [15/20] Traitement de https://lagendageek.com/tevent/manga-ten-expo-louverne/
2025-09-26 17:24:28,806 - INFO - ✅ Événement créé avec succès: ID 988c8408-c882-401e-a3af-3e56ddbcd7eb
2025-09-26 17:24:29,806 - INFO - 🔄 [16/20] Traitement de https://lagendageek.com/tevent/mangazur-2026/
2025-09-26 17:24:33,649 - INFO - ✅ Événement créé avec succès: ID c9698756-7d64-4d6e-b3b9-983698b45a36
2025-09-26 17:24:34,649 - INFO - 🔄 [17/20] Traitement de https://lagendageek.com/tevent/play-azur-festival-2026/
2025-09-26 17:24:38,474 - INFO - ✅ Événement créé avec succès: ID a17a3b8f-eb98-4570-a9e8-83f4df729772
2025-09-26 17:24:39,474 - INFO - 🔄 [18/20] Traitement de https://lagendageek.com/tevent/manga-ten-expo-chartres-de-bretagne/
2025-09-26 17:24:43,220 - INFO - ✅ Événement créé avec succès: ID 11f56563-2488-402a-8391-df9b6ec6048f
2025-09-26 17:24:44,221 - INFO - 🔄 [19/20] Traitement de https://lagendageek.com/tevent/geek-collector-remiremont-1/
2025-09-26 17:24:47,904 - INFO - ✅ Événement créé avec succès: ID 6a638653-44c3-4b27-81c9-818cdfeba1f5
2025-09-26 17:24:48,904 - INFO - 🔄 [20/20] Traitement de https://lagendageek.com/tevent/geek-collector-saone-1/
2025-09-26 17:24:52,503 - INFO - ✅ Événement créé avec succès: ID 2724f778-12cf-4736-affc-5b4736a6cdab
2025-09-26 17:24:53,504 - INFO - 🏁 Traitement terminé - Succès: 20, Erreurs: 0
2025-09-26 17:31:02,356 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-09-26 17:31:02,356 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/12
2025-09-26 17:31:05,337 - INFO - ✅ 14 événements trouvés sur la page
2025-09-26 17:31:05,337 - INFO - 📊 Traitement de 14 événements (1 à 14 sur 14)
2025-09-26 17:31:05,337 - INFO - 🔄 [1/14] Traitement de https://lagendageek.com/tevent/jap-and-co-2026/
2025-09-26 17:31:09,095 - INFO - ✅ Événement créé avec succès: ID 66bc6e6d-f386-47f5-8f42-9650c137b659
2025-09-26 17:31:10,096 - INFO - 🔄 [2/14] Traitement de https://lagendageek.com/tevent/geek-legends-oyonnax-1/
2025-09-26 17:31:13,726 - INFO - ✅ Événement créé avec succès: ID f7e4e973-8b84-4df5-8158-d7854351a2c1
2025-09-26 17:31:14,727 - INFO - 🔄 [3/14] Traitement de https://lagendageek.com/tevent/margny-compiegne-geek-convention-2026/
2025-09-26 17:31:18,376 - INFO - ✅ Événement créé avec succès: ID 0389acba-3309-46c0-8b81-8f5b1074c55c
2025-09-26 17:31:19,377 - INFO - 🔄 [4/14] Traitement de https://lagendageek.com/tevent/manga-mania-2026/
2025-09-26 17:31:20,101 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-09-26 17:31:20,101 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/13
2025-09-26 17:31:22,153 - INFO - 🚀 Début du traitement - Limite: 20, Offset: 0
2025-09-26 17:31:22,153 - INFO - 🔍 Récupération de la liste des événements depuis https://lagendageek.com/tevents/page/14
2025-09-26 17:31:22,843 - INFO - ✅ 0 événements trouvés sur la page
2025-09-26 17:31:22,843 - ERROR - ❌ Aucun événement trouvé
2025-09-26 17:31:23,086 - INFO - ✅ Événement créé avec succès: ID 5bd2bc22-95f7-4d25-9d10-de80c747dfda
2025-09-26 17:31:24,087 - INFO - 🔄 [5/14] Traitement de https://lagendageek.com/tevent/japan-addict-z-2026/
2025-09-26 17:31:24,902 - INFO - ✅ 0 événements trouvés sur la page
2025-09-26 17:31:24,902 - ERROR - ❌ Aucun événement trouvé
2025-09-26 17:31:27,784 - INFO - ✅ Événement créé avec succès: ID 57a3cf0f-1bc8-4249-9423-ba0fd9e1bd97
2025-09-26 17:31:28,785 - INFO - 🔄 [6/14] Traitement de https://lagendageek.com/tevent/convention-geek-unchained-2026/
2025-09-26 17:31:32,336 - INFO - ✅ Événement créé avec succès: ID 4a5f2fe2-c151-4f87-bba2-27c4f3b144e1
2025-09-26 17:31:33,336 - INFO - 🔄 [7/14] Traitement de https://lagendageek.com/tevent/tgs-springbreak-2026/
2025-09-26 17:31:37,070 - INFO - ✅ Événement créé avec succès: ID 34c196bb-99cc-4879-9654-a1aed1883dcf
2025-09-26 17:31:38,070 - INFO - 🔄 [8/14] Traitement de https://lagendageek.com/tevent/japan-tours-festival-2026/
2025-09-26 17:31:41,706 - INFO - ✅ Événement créé avec succès: ID cda8ad1f-ab01-4ecd-bc8a-281b7ae22a80
2025-09-26 17:31:42,707 - INFO - 🔄 [9/14] Traitement de https://lagendageek.com/tevent/magic-odyssey-1ere-edition/
2025-09-26 17:31:46,672 - INFO - ✅ Événement créé avec succès: ID 2279aea0-d0a0-4292-a6f0-676d5db001cd
2025-09-26 17:31:47,672 - INFO - 🔄 [10/14] Traitement de https://lagendageek.com/tevent/japan-expo-2026/
2025-09-26 17:31:51,385 - INFO - ✅ Événement créé avec succès: ID 4c346ef7-7b2c-463b-8e46-e84b756e4c8b
2025-09-26 17:31:52,386 - INFO - 🔄 [11/14] Traitement de https://lagendageek.com/tevent/manga-deauville-2026/
2025-09-26 17:31:56,045 - INFO - ✅ Événement créé avec succès: ID ab08bdf2-a6f8-4dfc-904f-fc696abaa6c9
2025-09-26 17:31:57,046 - INFO - 🔄 [12/14] Traitement de https://lagendageek.com/tevent/japan-chatels-festival/
2025-09-26 17:32:00,523 - INFO - ✅ Événement créé avec succès: ID a993b526-7442-411d-a5e5-05be4ec2cc70
2025-09-26 17:32:01,523 - INFO - 🔄 [13/14] Traitement de https://lagendageek.com/tevent/brussels-games-festival-2026/
2025-09-26 17:32:05,369 - INFO - ✅ Événement créé avec succès: ID 48a6c0d6-76c1-4cb6-84b4-f5197ebd41b5
2025-09-26 17:32:06,370 - INFO - 🔄 [14/14] Traitement de https://lagendageek.com/tevent/steven-spielberg-hommage-80-ans/
2025-09-26 17:32:10,298 - INFO - ✅ Événement créé avec succès: ID c73cd045-b199-404f-a256-cd14977abe95
2025-09-26 17:32:11,299 - INFO - 🏁 Traitement terminé - Succès: 14, Erreurs: 0

View file

@ -207,6 +207,39 @@ class DemoResource:
logger.error(f"Error processing GET request to /demo/map-by-what: {e}")
resp.status = falcon.HTTP_500
resp.text = f"Error: {str(e)}"
def on_get_map_by_what_type(self, req, resp, event_type):
"""
Handle GET requests to the /demo/map-by-what/{type} endpoint.
Returns an HTML page with a MapLibre map showing events of a specific type,
colored by temporality (past/present/future) with a detailed table.
Args:
req: The request object.
resp: The response object.
event_type: The event type to filter by.
"""
logger.info(f"Processing GET request to /demo/map-by-what/{event_type}")
try:
# Set content type to HTML
resp.content_type = 'text/html'
# Render the template with the event type
template = self.jinja_env.get_template('map_by_what_type.html')
html = template.render(
event_type=event_type,
event_type_label=event_type.replace('_', ' ').title()
)
# Set the response body and status
resp.text = html
resp.status = falcon.HTTP_200
logger.success(f"Successfully processed GET request to /demo/map-by-what/{event_type}")
except Exception as e:
logger.error(f"Error processing GET request to /demo/map-by-what/{event_type}: {e}")
resp.status = falcon.HTTP_500
resp.text = f"Error: {str(e)}"
events_by_what = defaultdict(list)
if events_data.get('features'):

View file

@ -44,6 +44,8 @@ class DemoMainResource:
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenEventDatabase Demo</title>
<link rel="icon" type="image/png" href="/static/oedb.png">
<link rel="icon" type="image/png" href="/static/oedb.png">
<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">
@ -287,17 +289,12 @@ class DemoMainResource:
<script>
// Fonction pour gérer les listes dépliantes et sections collapsibles
document.addEventListener('DOMContentLoaded', function() {
const endpointsHeader = document.getElementById('endpoints_list_header');
const endpointsList = document.getElementById('endpoints_list');
const demoPagesHeader = document.getElementById('demo_pages_list_header');
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) {
if (header && list) {
header.addEventListener('click', function() {
if (list.style.display === 'none' || list.style.display === '') {
list.style.display = 'block';
@ -308,16 +305,7 @@ class DemoMainResource:
}
});
}
// Initialiser les listes et sections comme repliées
endpointsList.style.display = 'none';
demoPagesList.style.display = 'none';
infoPanelContent.style.display = 'none';
// Ajouter les écouteurs d'événements
toggleList(endpointsHeader, endpointsList);
toggleList(demoPagesHeader, demoPagesList);
toggleList(infoPanelHeader, infoPanelContent);
}
// Toggle pour le panneau de filtres via le titre "Filtres"
if (filtersHeader && filtersPanel) {
@ -331,8 +319,9 @@ class DemoMainResource:
}
});
// Variable globale pour stocker les marqueurs d'événements
// Variables globales pour stocker les marqueurs d'événements et le premier chargement
window.eventMarkers = [];
window.isFirstLoad = true;
function addEventsToMap(geojsonData) {
if (!geojsonData || !geojsonData.features) return;
@ -533,6 +522,8 @@ class DemoMainResource:
// Function to fetch events from the API
function fetchEvents() {
console.log('🔄 Chargement des événements...', isFirstLoad ? '(Premier chargement)' : '(Rechargement)');
// Fetch events from the API - using the local API endpoint
fetch('https://api.openeventdatabase.org/event?')
.then(response => response.json())
@ -543,7 +534,7 @@ class DemoMainResource:
// Render histogram for retrieved events
try { renderEventsHistogram(data.features); } catch(e) { console.warn('Histogram error', e); }
// Fit map to events bounds
// Fit map to events bounds (seulement au premier chargement)
fitMapToBounds(data);
} else {
console.log('No events found');
@ -826,9 +817,11 @@ class DemoMainResource:
return (currentTime - createTime) > oneHourInMs;
}
// Function to fit map to events bounds
// Function to fit map to events bounds (only on first load)
function fitMapToBounds(geojson) {
if (geojson.features.length === 0) return;
if (geojson.features.length === 0 || !window.isFirstLoad) return;
console.log('🎯 Premier chargement - Ajustement de la vue sur les événements');
// Create a bounds object
const bounds = new maplibregl.LngLatBounds();
@ -843,6 +836,10 @@ class DemoMainResource:
padding: 50,
maxZoom: 12
});
// 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');
}
// Function to update user information display

View file

@ -520,6 +520,7 @@ button:hover {
position: fixed;
bottom: 0.5rem;
right: 0.5rem;
padding: 2rem 1rem;
}
/* Tab styles */
@ -626,3 +627,11 @@ button{
border-radius: 5px;
background-color: #79a2d1;
}
#eventsHistogram{
height: 150px;
}
.maplibregl-ctrl-attrib + .maplibregl-ctrl-attrib {
display: none;
}

View file

@ -170,21 +170,24 @@ document.getElementById('deleteButton').addEventListener('click', function() {
const eventId = document.getElementById('eventId').value;
// Show confirmation dialog
if (confirm('Are you sure you want to delete this event? This action cannot be undone.')) {
// Submit delete request to API
fetch(`/event/${eventId}`, {
if (confirm('Êtes-vous sûr de vouloir supprimer cet événement ? Cette action ne peut pas être annulée.')) {
// Show loading message
showResult('Suppression en cours...', 'info');
// Submit delete request to external API
fetch(`https://api.openeventdatabase.org/event/${eventId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => {
if (response.ok) {
showResult('Event deleted successfully', 'success');
if (response.ok || response.status === 204) {
showResult('✅ Événement supprimé avec succès !', 'success');
// Add link to go back to map
const resultElement = document.getElementById('result');
resultElement.innerHTML += `<p><a href="/demo">Back to Map</a></p>`;
resultElement.innerHTML += `<p><a href="/demo">Retour à la carte</a></p>`;
// Disable form controls
const formElements = document.querySelectorAll('#eventForm input, #eventForm select, #eventForm button');
@ -192,18 +195,27 @@ document.getElementById('deleteButton').addEventListener('click', function() {
element.disabled = true;
});
// Redirect to demo page after 2 seconds
// Redirect to demo page after 3 seconds
setTimeout(() => {
window.location.href = '/demo';
}, 2000);
}, 3000);
} else if (response.status === 404) {
throw new Error('Événement non trouvé sur l\'API externe');
} else if (response.status === 403) {
throw new Error('Accès non autorisé - un secret pourrait être requis pour supprimer cet événement');
} else {
return response.text().then(text => {
throw new Error(text || response.statusText);
throw new Error(text || `Erreur HTTP ${response.status}: ${response.statusText}`);
});
}
})
.catch(error => {
showResult(`Error deleting event: ${error.message}`, 'error');
console.error('Erreur lors de la suppression:', error);
if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) {
showResult('❌ Erreur de connexion : Impossible de joindre l\'API api.openeventdatabase.org', 'error');
} else {
showResult(`❌ Erreur lors de la suppression : ${error.message}`, 'error');
}
});
}
});

View file

@ -3,7 +3,6 @@
// Global variables
let map;
let marker;
let geocoder;
let currentPosition;
let currentIssueType = null;
let photoFiles = [];
@ -15,10 +14,14 @@ document.addEventListener('DOMContentLoaded', function() {
initMap();
initTabs();
initForm();
setupFormValidation();
// Get Panoramax configuration
panoramaxUploadUrl = document.getElementById('panoramaxUploadUrl').value;
panoramaxToken = document.getElementById('panoramaxToken').value;
const panoramaxUploadUrlElement = document.getElementById('panoramaxUploadUrl');
const panoramaxTokenElement = document.getElementById('panoramaxToken');
panoramaxUploadUrl = panoramaxUploadUrlElement ? panoramaxUploadUrlElement.value : '';
panoramaxToken = panoramaxTokenElement ? panoramaxTokenElement.value : '';
// Set up photo upload
const photoInput = document.getElementById('photo');
@ -26,13 +29,6 @@ document.addEventListener('DOMContentLoaded', function() {
photoInput.addEventListener('change', handlePhotoUpload);
}
// Set up form submission
const reportForm = document.getElementById('reportForm');
if (reportForm) {
reportForm.addEventListener('submit', submitReport);
}
});
// Initialize the map
function initMap() {
// Create the map
@ -142,6 +138,14 @@ function initForm() {
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
stopTimeInput.value = oneHourLater.toISOString().slice(0, 16);
}
// Set up form submission after DOM is loaded
const reportForm = document.getElementById('trafficForm');
if (reportForm) {
reportForm.addEventListener('submit', submitReport);
} else {
console.warn('Traffic form not found in DOM');
}
}
// Update the coordinates display when the marker is moved
@ -167,64 +171,6 @@ function updateCoordinates() {
}
}
// Fill the form with predefined values based on the selected issue type
function fillForm(issueType) {
currentIssueType = issueType;
// Get the form elements
const whatInput = document.getElementById('what');
const labelInput = document.getElementById('label');
const descriptionInput = document.getElementById('description');
// Set default values based on the issue type
switch (issueType) {
case 'pothole':
whatInput.value = 'road.hazard.pothole';
labelInput.value = 'Nid de poule';
descriptionInput.value = 'Nid de poule sur la chaussée';
break;
case 'obstacle':
whatInput.value = 'road.hazard.obstacle';
labelInput.value = 'Obstacle sur la route';
descriptionInput.value = 'Obstacle sur la chaussée';
break;
case 'vehicle':
whatInput.value = 'road.hazard.vehicle';
labelInput.value = 'Véhicule sur le bas côté';
descriptionInput.value = 'Véhicule arrêté sur le bas côté de la route';
break;
case 'danger':
whatInput.value = 'road.hazard.danger';
labelInput.value = 'Danger sur la route';
descriptionInput.value = 'Situation dangereuse sur la route';
break;
case 'accident':
whatInput.value = 'road.accident';
labelInput.value = 'Accident de la route';
descriptionInput.value = 'Accident de la circulation';
break;
case 'flooded_road':
whatInput.value = 'road.hazard.flood';
labelInput.value = 'Route inondée';
descriptionInput.value = 'Route inondée, circulation difficile';
break;
case 'roadwork':
whatInput.value = 'road.works';
labelInput.value = 'Travaux routiers';
descriptionInput.value = 'Travaux en cours sur la chaussée';
break;
case 'traffic_jam':
whatInput.value = 'road.traffic.jam';
labelInput.value = 'Embouteillage';
descriptionInput.value = 'Circulation dense, embouteillage';
break;
// Add more cases for other issue types
}
// Scroll to the form
document.getElementById('reportForm').scrollIntoView({ behavior: 'smooth' });
}
// Handle photo upload
function handlePhotoUpload(event) {
const files = event.target.files;
@ -376,3 +322,292 @@ async function uploadPhotos(files) {
return urls;
}
// Setup form validation
function setupFormValidation() {
const form = document.getElementById('trafficForm');
if (!form) return;
// Get all form fields that need validation
const requiredFields = [
{id: 'label', name: 'Description du problème'},
{id: 'severity', name: 'Gravité'},
{id: 'start', name: 'Heure de début'},
{id: 'stop', name: 'Heure de fin'}
];
const optionalFields = [
{id: 'cause', name: 'Détails supplémentaires'},
{id: 'where', name: 'Route/Nom du lieu'}
];
// Add event listeners for real-time validation
[...requiredFields, ...optionalFields].forEach(field => {
const element = document.getElementById(field.id);
if (element) {
element.addEventListener('input', validateForm);
element.addEventListener('change', validateForm);
element.addEventListener('blur', validateForm);
}
});
// Add listener for map clicks (location validation)
if (map) {
map.on('click', () => {
setTimeout(validateForm, 100); // Small delay to ensure marker is set
});
}
// Initial validation
validateForm();
}
});
// Clear error message for a field
function clearFieldError(element) {
const existingError = element.parentNode.querySelector('.field-error');
if (existingError) {
existingError.remove();
}
}
// Validate the entire form
function validateForm() {
const form = document.getElementById('trafficForm');
const submitButton = document.getElementById('report_issue_button');
if (!form || !submitButton) {
console.warn('🔍 Validation: Formulaire ou bouton de soumission non trouvé');
return false;
}
console.group('🔍 Validation du formulaire traffic');
let isValid = true;
let firstInvalidField = null;
const errors = [];
const validFields = [];
// Check required fields
const requiredFields = [
{id: 'label', name: 'Description du problème', minLength: 3},
{id: 'severity', name: 'Gravité'},
{id: 'start', name: 'Heure de début'},
{id: 'stop', name: 'Heure de fin'}
];
console.log('📝 Vérification des champs requis...');
requiredFields.forEach(field => {
const element = document.getElementById(field.id);
if (element) {
let fieldValid = true;
let errorMessage = '';
const value = element.value.trim();
// Check if field is empty
if (!value) {
fieldValid = false;
errorMessage = `${field.name} est requis`;
console.error(`${field.name}: champ vide`);
}
// Check minimum length for text fields
else if (field.minLength && value.length < field.minLength) {
fieldValid = false;
errorMessage = `${field.name} doit contenir au moins ${field.minLength} caractères`;
console.error(`${field.name}: trop court (${value.length}/${field.minLength} caractères) - "${value}"`);
} else {
console.log(`${field.name}: OK - "${value}"`);
validFields.push(field.name);
}
// Visual feedback
if (fieldValid) {
element.classList.remove('error');
element.classList.add('valid');
clearFieldError(element);
} else {
element.classList.remove('valid');
element.classList.add('error');
showFieldError(element, errorMessage);
isValid = false;
if (!firstInvalidField) {
firstInvalidField = element;
}
errors.push(errorMessage);
}
} else {
console.warn(`⚠️ ${field.name}: élément non trouvé dans le DOM`);
}
});
// Check date logic (start time should be before stop time)
console.log('📅 Vérification de la logique des dates...');
const startElement = document.getElementById('start');
const stopElement = document.getElementById('stop');
if (startElement && stopElement && startElement.value && stopElement.value) {
const startTime = new Date(startElement.value);
const stopTime = new Date(stopElement.value);
console.log(`📅 Heure début: ${startTime.toLocaleString()}`);
console.log(`📅 Heure fin: ${stopTime.toLocaleString()}`);
if (startTime >= stopTime) {
stopElement.classList.remove('valid');
stopElement.classList.add('error');
showFieldError(stopElement, 'L\'heure de fin doit être après l\'heure de début');
isValid = false;
if (!firstInvalidField) {
firstInvalidField = stopElement;
}
errors.push('L\'heure de fin doit être après l\'heure de début');
console.error(`❌ Dates: L'heure de fin (${stopTime.toLocaleString()}) doit être après l'heure de début (${startTime.toLocaleString()})`);
} else {
console.log(`✅ Dates: Logique correcte (durée: ${Math.round((stopTime - startTime) / 1000 / 60)} minutes)`);
validFields.push('Logique des dates');
}
} else {
console.warn('⚠️ Dates: Impossible de vérifier la logique (éléments ou valeurs manquants)');
}
// Check if location is set (marker exists)
console.log('📍 Vérification de la localisation...');
if (!marker || !marker.getLngLat()) {
isValid = false;
errors.push('Veuillez sélectionner une localisation sur la carte');
console.error('❌ Localisation: Aucun marqueur placé sur la carte');
// Highlight map container
const mapContainer = document.getElementById('map');
if (mapContainer) {
mapContainer.classList.add('error');
if (!firstInvalidField) {
firstInvalidField = mapContainer;
}
}
} else {
const lngLat = marker.getLngLat();
console.log(`✅ Localisation: Marqueur placé à [${lngLat.lng.toFixed(6)}, ${lngLat.lat.toFixed(6)}]`);
validFields.push('Localisation');
// Remove error highlight from map
const mapContainer = document.getElementById('map');
if (mapContainer) {
mapContainer.classList.remove('error');
}
}
// Update submit button state
if (isValid) {
submitButton.disabled = false;
submitButton.classList.remove('disabled');
submitButton.textContent = 'Signaler le problème';
console.log(`🎉 VALIDATION RÉUSSIE! Tous les champs sont valides:`);
validFields.forEach(field => console.log(`${field}`));
console.log('🔓 Bouton de soumission débloqué');
} else {
submitButton.disabled = true;
submitButton.classList.add('disabled');
submitButton.textContent = `Signaler le problème (${errors.length} erreur${errors.length > 1 ? 's' : ''})`;
console.warn(`⚠️ VALIDATION ÉCHOUÉE! ${errors.length} erreur${errors.length > 1 ? 's' : ''} trouvée${errors.length > 1 ? 's' : ''}:`);
errors.forEach((error, index) => console.error(` ${index + 1}. ${error}`));
console.log('🔒 Bouton de soumission bloqué');
if (validFields.length > 0) {
console.log(`Champs valides (${validFields.length}):`);
validFields.forEach(field => console.log(`${field}`));
}
}
// Focus on first invalid field if validation was triggered by user action
if (!isValid && firstInvalidField && document.activeElement !== firstInvalidField) {
// Only auto-focus if the user isn't currently typing in another field
if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT') {
firstInvalidField.focus();
console.log(`🎯 Focus placé sur le premier champ invalide: ${firstInvalidField.id}`);
}
}
console.groupEnd();
return isValid;
}
// Fill the form with predefined values based on the selected issue type
function fillForm(issueType) {
currentIssueType = issueType;
// Get the form elements
const whatInput = document.getElementById('what');
const labelInput = document.getElementById('label');
const descriptionInput = document.getElementById('description');
// Set default values based on the issue type
switch (issueType) {
case 'pothole':
whatInput.value = 'road.hazard.pothole';
labelInput.value = 'Nid de poule';
descriptionInput.value = 'Nid de poule sur la chaussée';
break;
case 'obstacle':
whatInput.value = 'road.hazard.obstacle';
labelInput.value = 'Obstacle sur la route';
descriptionInput.value = 'Obstacle sur la chaussée';
break;
case 'vehicle':
whatInput.value = 'road.hazard.vehicle';
labelInput.value = 'Véhicule sur le bas côté';
descriptionInput.value = 'Véhicule arrêté sur le bas côté de la route';
break;
case 'danger':
whatInput.value = 'road.hazard.danger';
labelInput.value = 'Danger sur la route';
descriptionInput.value = 'Situation dangereuse sur la route';
break;
case 'accident':
whatInput.value = 'road.accident';
labelInput.value = 'Accident de la route';
descriptionInput.value = 'Accident de la circulation';
break;
case 'flooded_road':
whatInput.value = 'road.hazard.flood';
labelInput.value = 'Route inondée';
descriptionInput.value = 'Route inondée, circulation difficile';
break;
case 'roadwork':
whatInput.value = 'road.works';
labelInput.value = 'Travaux routiers';
descriptionInput.value = 'Travaux en cours sur la chaussée';
break;
case 'traffic_jam':
whatInput.value = 'road.traffic.jam';
labelInput.value = 'Embouteillage';
descriptionInput.value = 'Circulation dense, embouteillage';
break;
// Add more cases for other issue types
}
// Scroll to the form
const formElement = document.getElementById('trafficForm');
if (formElement) {
formElement.scrollIntoView({behavior: 'smooth'});
}
}
// Show error message for a field
function showFieldError(element, message) {
// Remove existing error message
clearFieldError(element);
// Create error message element
const errorDiv = document.createElement('div');
errorDiv.className = 'field-error';
errorDiv.textContent = message;
// Insert error message after the field
element.parentNode.insertBefore(errorDiv, element.nextSibling);
}

View file

@ -1,45 +1,348 @@
{% extends "layout.html" %}
{% block title %}Event {{ id }} - OpenEventDatabase{% endblock %}
{% block title %}Événement {{ properties.label|default(id) }} - OpenEventDatabase{% endblock %}
{% block css %}
<style>
#map { width:100%; height: 360px; border:1px solid #ddd; border-radius:4px; }
table { width:100%; border-collapse: collapse; margin-top:12px; }
th, td { padding: 6px 8px; border-bottom: 1px solid #eee; text-align:left; }
th { background:#f9fafb; }
.event-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.event-title {
margin: 0 0 10px 0;
color: #333;
font-size: 1.8rem;
}
.event-subtitle {
color: #666;
font-size: 1rem;
margin: 0;
}
#map {
width: 100%;
height: 400px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 20px;
}
.properties-table {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.table-header {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
}
.table-header h2 {
margin: 0;
color: #333;
font-size: 1.3rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px 20px;
border-bottom: 1px solid #eee;
text-align: left;
vertical-align: top;
}
th {
background: #f9fafb;
font-weight: 600;
color: #495057;
width: 200px;
}
td {
word-wrap: break-word;
max-width: 0;
}
.what-link {
display: inline-block;
padding: 4px 12px;
background: #0078ff;
color: white !important;
text-decoration: none;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
transition: background-color 0.2s;
}
.what-link:hover {
background: #0056b3;
text-decoration: none;
}
.nav-links {
margin-top: 10px;
}
.nav-links a {
color: #0078ff;
text-decoration: none;
margin-right: 15px;
font-weight: 500;
}
.nav-links a:hover {
text-decoration: underline;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
padding: 20px;
color: #6c757d;
}
</style>
{% endblock %}
{% block header %}Évènement {{ id }}{% endblock %}
{% block header %}
<div class="event-header">
<h1 class="event-title">{{ properties.label|default('Événement sans titre') }}</h1>
<p class="event-subtitle">ID: {{ id }}</p>
<div class="nav-links">
<a href="/demo"><i class="fas fa-home"></i> Accueil</a>
<a href="/demo/view-events"><i class="fas fa-table"></i> Tous les événements</a>
<a href="https://api.openeventdatabase.org/event/{{ id }}" target="_blank"><i class="fas fa-code"></i> API JSON</a>
</div>
</div>
{% endblock %}
{% block content %}
<div id="map"></div>
<div id="loading-map" class="loading">
<i class="fas fa-spinner fa-spin"></i>
Chargement de la carte...
</div>
<div id="map-error" class="error-message" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
Erreur lors du chargement de la carte
</div>
<div id="map" style="display: none;"></div>
<div class="properties-table">
<div class="table-header">
<h2><i class="fas fa-info-circle"></i> Propriétés de l'événement</h2>
</div>
<table>
<thead><tr><th>Clé</th><th>Valeur</th></tr></thead>
<thead>
<tr>
<th>Propriété</th>
<th>Valeur</th>
</tr>
</thead>
<tbody>
{% for key, value in properties.items()|sort %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
<td><strong>{{ key }}</strong></td>
<td>
{% if key == 'what' %}
<a href="/demo/map-by-what/{{ value }}" class="what-link">
<i class="fas fa-tag"></i> {{ value }}
</a>
<small style="display: block; margin-top: 5px; color: #6c757d;">
Cliquez pour voir tous les événements de ce type
</small>
{% elif key in ['start', 'stop', 'createdate', 'lastupdate'] and value %}
{{ value }}
<small style="display: block; color: #6c757d;">
<script>
document.write(formatDate('{{ value }}'));
</script>
</small>
{% elif key == 'url' and value %}
<a href="{{ value }}" target="_blank" style="color: #0078ff;">
{{ value }}
<i class="fas fa-external-link-alt" style="font-size: 0.8em;"></i>
</a>
{% elif value is string and value.startswith('http') %}
<a href="{{ value }}" target="_blank" style="color: #0078ff;">
{{ value }}
<i class="fas fa-external-link-alt" style="font-size: 0.8em;"></i>
</a>
{% else %}
{{ value|safe if value else '-' }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script>
const f = JSON.parse('{{ feature_json|safe }}');
// Fonction pour formater les dates
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (e) {
return dateString;
}
}
// Formater les dates dans le tableau après le chargement de la page
function formatDatesInTable() {
const dateElements = document.querySelectorAll('.date-display[data-date]');
dateElements.forEach(element => {
const dateValue = element.getAttribute('data-date');
if (dateValue) {
const formattedDate = formatDate(dateValue);
element.textContent = formattedDate;
element.title = dateValue; // Garder la date originale en tooltip
}
});
}
// Gestion des erreurs et logging
function logError(message, error) {
console.error(message, error);
document.getElementById('loading-map').style.display = 'none';
document.getElementById('map-error').style.display = 'block';
document.getElementById('map-error').innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
${message}: ${error.message || error}
`;
}
// Formater les dates dans le tableau
formatDatesInTable();
// Initialisation de la carte
try {
console.log('🗺️ Initialisation de la carte...');
// Parser les données de l'événement de manière sécurisée
let eventFeature;
try {
const featureJson = '{{ feature_json|safe }}';
console.log('📄 JSON reçu:', featureJson);
eventFeature = JSON.parse(featureJson);
console.log('✅ Données de l\'événement parsées:', eventFeature);
} catch (parseError) {
throw new Error(`Impossible de parser les données JSON: ${parseError.message}`);
}
// Vérifier la structure des données
if (!eventFeature) {
throw new Error('Aucune donnée d\'événement trouvée');
}
// Déterminer les coordonnées pour la carte
let center = [2.3522, 48.8566]; // Paris par défaut
let hasValidGeometry = false;
if (eventFeature.geometry &&
eventFeature.geometry.type === 'Point' &&
eventFeature.geometry.coordinates &&
Array.isArray(eventFeature.geometry.coordinates) &&
eventFeature.geometry.coordinates.length === 2) {
const coords = eventFeature.geometry.coordinates;
// Vérifier que les coordonnées sont des nombres valides
if (typeof coords[0] === 'number' && typeof coords[1] === 'number' &&
!isNaN(coords[0]) && !isNaN(coords[1])) {
center = coords;
hasValidGeometry = true;
console.log('📍 Coordonnées trouvées:', center);
}
}
if (!hasValidGeometry) {
console.warn('⚠️ Pas de coordonnées valides, utilisation du centre par défaut');
}
// Créer la carte
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: f.geometry && f.geometry.coordinates ? f.geometry.coordinates : [2.3522,48.8566],
zoom: 12
center: center,
zoom: hasValidGeometry ? 14 : 6
});
// Ajouter les contrôles
map.addControl(new maplibregl.NavigationControl());
if (f.geometry && f.geometry.type === 'Point') {
new maplibregl.Marker().setLngLat(f.geometry.coordinates).addTo(map);
map.addControl(new maplibregl.AttributionControl({
customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}));
// Quand la carte est chargée
map.on('load', function() {
console.log('✅ Carte chargée avec succès');
document.getElementById('loading-map').style.display = 'none';
document.getElementById('map').style.display = 'block';
// Ajouter le marqueur si on a des coordonnées valides
if (hasValidGeometry) {
const marker = new maplibregl.Marker({
color: '#0078ff'
})
.setLngLat(center)
.addTo(map);
// Créer une popup avec les informations de l'événement
const properties = eventFeature.properties || {};
const popupContent = `
<div style="max-width: 250px;">
<h3 style="margin: 0 0 10px 0;">${properties.label || 'Événement'}</h3>
${properties.what ? `<p><strong>Type:</strong> ${properties.what}</p>` : ''}
${properties.start ? `<p><strong>Début:</strong> ${formatDate(properties.start)}</p>` : ''}
${properties.where ? `<p><strong>Lieu:</strong> ${properties.where}</p>` : ''}
</div>
`;
const popup = new maplibregl.Popup({ offset: 25 })
.setHTML(popupContent);
marker.setPopup(popup);
console.log('📍 Marqueur ajouté à la carte');
}
});
// Gestion des erreurs de carte
map.on('error', function(e) {
logError('Erreur lors du chargement de la carte', e.error || e);
});
} catch (error) {
logError('Erreur lors de l\'initialisation de la carte', error);
}
</script>
{% endblock %}

View file

@ -7,7 +7,7 @@
<!-- Common CSS -->
<link rel="stylesheet" href="/static/demo_styles.css">
<link rel="icon" type="image/png" href="/static/oedb.png">
<!-- Page-specific CSS -->
{% block css %}{% endblock %}

View file

@ -0,0 +1,533 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Événements {{ event_type_label }} - OpenEventDatabase</title>
<link rel="icon" type="image/png" href="/static/oedb.png">
<!-- MapLibre GL JS -->
<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" />
<!-- Font Awesome -->
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background-color: #f8f9fa;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
margin: 0 0 10px 0;
color: #333;
}
.nav-links {
margin-top: 15px;
}
.nav-links a {
color: #0078ff;
text-decoration: none;
margin-right: 15px;
font-weight: 500;
}
.nav-links a:hover {
text-decoration: underline;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
height: calc(100vh - 200px);
}
.map-container, .table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
#map {
width: 100%;
height: 100%;
}
.table-container {
display: flex;
flex-direction: column;
}
.table-header {
padding: 20px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
}
.table-header h2 {
margin: 0;
color: #333;
}
.legend {
display: flex;
gap: 20px;
margin-top: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 3px rgba(0,0,0,0.3);
}
.legend-past { background-color: #6c757d; }
.legend-current { background-color: #28a745; }
.legend-future { background-color: #007bff; }
.table-content {
flex: 1;
overflow-y: auto;
padding: 0;
}
.events-table {
width: 100%;
border-collapse: collapse;
}
.events-table th,
.events-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.events-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
z-index: 1;
}
.events-table tr:hover {
background: #f8f9fa;
}
.event-title {
font-weight: 600;
color: #333;
}
.event-title a {
color: #0078ff;
text-decoration: none;
}
.event-title a:hover {
text-decoration: underline;
}
.event-date {
font-size: 14px;
color: #6c757d;
}
.event-status {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-past {
background: #6c757d;
color: white;
}
.status-current {
background: #28a745;
color: white;
}
.status-future {
background: #007bff;
color: white;
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.error {
text-align: center;
padding: 40px;
color: #dc3545;
}
.stats {
display: flex;
gap: 20px;
margin-top: 10px;
font-size: 14px;
color: #6c757d;
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
height: auto;
}
.map-container {
height: 400px;
}
.table-container {
height: 500px;
}
}
</style>
</head>
<body>
<div class="header">
<h1>
<i class="fas fa-map-marked-alt"></i>
Événements {{ event_type_label }}
</h1>
<p>Visualisation et liste détaillée des événements de type "{{ event_type }}"</p>
<div class="nav-links">
<a href="/demo"><i class="fas fa-home"></i> Accueil</a>
<a href="/demo/map-by-what"><i class="fas fa-list"></i> Tous les types</a>
<a href="/demo/view-events"><i class="fas fa-table"></i> Vue tableau</a>
</div>
</div>
<div class="content-grid">
<div class="map-container">
<div id="map"></div>
</div>
<div class="table-container">
<div class="table-header">
<h2><i class="fas fa-table"></i> Liste des événements</h2>
<div class="legend">
<div class="legend-item">
<div class="legend-color legend-past"></div>
<span>Passés</span>
</div>
<div class="legend-item">
<div class="legend-color legend-current"></div>
<span>En cours</span>
</div>
<div class="legend-item">
<div class="legend-color legend-future"></div>
<span>À venir</span>
</div>
</div>
<div class="stats">
<span id="total-events">Total: 0</span>
<span id="past-events">Passés: 0</span>
<span id="current-events">En cours: 0</span>
<span id="future-events">À venir: 0</span>
</div>
</div>
<div class="table-content">
<div id="loading" class="loading">
<i class="fas fa-spinner fa-spin"></i>
Chargement des événements...
</div>
<div id="error" class="error" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
Erreur lors du chargement des événements
</div>
<table class="events-table" id="events-table" style="display: none;">
<thead>
<tr>
<th>Statut</th>
<th>Événement</th>
<th>Dates</th>
<th>Lieu</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="events-tbody">
</tbody>
</table>
</div>
</div>
</div>
<script>
// Configuration
const eventType = '{{ event_type }}';
let map;
let markers = [];
let eventsData = [];
// Initialiser la carte
function initMap() {
map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [2.3522, 48.8566], // Paris par défaut
zoom: 6
});
map.addControl(new maplibregl.NavigationControl());
map.addControl(new maplibregl.AttributionControl({
customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}));
}
// Déterminer le statut temporel d'un événement
function getEventStatus(start, stop) {
const now = new Date();
const startDate = new Date(start);
const stopDate = new Date(stop);
if (now < startDate) {
return 'future';
} else if (now > stopDate) {
return 'past';
} else {
return 'current';
}
}
// Obtenir la couleur selon le statut
function getStatusColor(status) {
switch (status) {
case 'past': return '#6c757d';
case 'current': return '#28a745';
case 'future': return '#007bff';
default: return '#6c757d';
}
}
// Obtenir le label selon le statut
function getStatusLabel(status) {
switch (status) {
case 'past': return 'Passé';
case 'current': return 'En cours';
case 'future': return 'À venir';
default: return 'Inconnu';
}
}
// Ajouter les marqueurs sur la carte
function addMarkersToMap(events) {
// Supprimer les anciens marqueurs
markers.forEach(marker => marker.remove());
markers = [];
const bounds = new maplibregl.LngLatBounds();
events.forEach(event => {
if (!event.geometry || !event.geometry.coordinates) return;
const coords = event.geometry.coordinates;
const properties = event.properties || {};
const status = getEventStatus(properties.start, properties.stop);
const color = getStatusColor(status);
// Créer l'élément marqueur
const el = document.createElement('div');
el.style.width = '20px';
el.style.height = '20px';
el.style.borderRadius = '50%';
el.style.backgroundColor = color;
el.style.border = '3px 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 = `
<div style="max-width: 300px;">
<h3 style="margin: 0 0 10px 0;">${properties.label || 'Événement'}</h3>
<p><strong>Statut:</strong> <span class="event-status status-${status}">${getStatusLabel(status)}</span></p>
<p><strong>Début:</strong> ${new Date(properties.start).toLocaleString('fr-FR')}</p>
<p><strong>Fin:</strong> ${new Date(properties.stop).toLocaleString('fr-FR')}</p>
${properties.where ? `<p><strong>Lieu:</strong> ${properties.where}</p>` : ''}
${properties.description ? `<p><strong>Description:</strong> ${properties.description}</p>` : ''}
<div style="margin-top: 15px;">
<a href="/demo/by_id/${properties.id}" style="color: #0078ff; font-weight: bold;">Voir les détails</a>
</div>
</div>
`;
const popup = new maplibregl.Popup({ offset: 25 })
.setHTML(popupContent);
const marker = new maplibregl.Marker(el)
.setLngLat(coords)
.setPopup(popup)
.addTo(map);
markers.push(marker);
bounds.extend(coords);
});
// Ajuster la vue pour englober tous les marqueurs
if (events.length > 0) {
map.fitBounds(bounds, { padding: 50 });
}
}
// Remplir le tableau
function populateTable(events) {
const tbody = document.getElementById('events-tbody');
tbody.innerHTML = '';
// Trier par date (futurs d'abord, puis en cours, puis passés)
const sortedEvents = events.sort((a, b) => {
const statusA = getEventStatus(a.properties.start, a.properties.stop);
const statusB = getEventStatus(b.properties.start, b.properties.stop);
const statusOrder = { 'future': 0, 'current': 1, 'past': 2 };
if (statusOrder[statusA] !== statusOrder[statusB]) {
return statusOrder[statusA] - statusOrder[statusB];
}
return new Date(a.properties.start) - new Date(b.properties.start);
});
sortedEvents.forEach(event => {
const props = event.properties || {};
const status = getEventStatus(props.start, props.stop);
const row = document.createElement('tr');
row.innerHTML = `
<td>
<span class="event-status status-${status}">${getStatusLabel(status)}</span>
</td>
<td>
<div class="event-title">
<a href="/demo/by_id/${props.id}">${props.label || 'Événement sans titre'}</a>
</div>
</td>
<td>
<div class="event-date">
<strong>Début:</strong> ${new Date(props.start).toLocaleDateString('fr-FR')}<br>
<strong>Fin:</strong> ${new Date(props.stop).toLocaleDateString('fr-FR')}
</div>
</td>
<td>${props.where || '-'}</td>
<td>
<a href="/demo/by_id/${props.id}" style="color: #0078ff; text-decoration: none;">
<i class="fas fa-eye"></i> Détails
</a>
</td>
`;
tbody.appendChild(row);
});
}
// Mettre à jour les statistiques
function updateStats(events) {
const stats = {
total: events.length,
past: 0,
current: 0,
future: 0
};
events.forEach(event => {
const status = getEventStatus(event.properties.start, event.properties.stop);
stats[status]++;
});
document.getElementById('total-events').textContent = `Total: ${stats.total}`;
document.getElementById('past-events').textContent = `Passés: ${stats.past}`;
document.getElementById('current-events').textContent = `En cours: ${stats.current}`;
document.getElementById('future-events').textContent = `À venir: ${stats.future}`;
}
// Charger les événements
async function loadEvents() {
try {
const response = await fetch(`https://api.openeventdatabase.org/event?what=${eventType}&limit=1000`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
eventsData = data.features || [];
// Masquer le loading
document.getElementById('loading').style.display = 'none';
if (eventsData.length === 0) {
document.getElementById('error').style.display = 'block';
document.getElementById('error').innerHTML = `
<i class="fas fa-info-circle"></i>
Aucun événement trouvé pour le type "${eventType}"
`;
return;
}
// Afficher le tableau
document.getElementById('events-table').style.display = 'table';
// Peupler la carte et le tableau
addMarkersToMap(eventsData);
populateTable(eventsData);
updateStats(eventsData);
} catch (error) {
console.error('Erreur lors du chargement des événements:', error);
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
document.getElementById('error').innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
Erreur lors du chargement: ${error.message}
`;
}
}
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
initMap();
loadEvents();
});
</script>
</body>
</html>

View file

@ -4,13 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Traffic Jam - OpenEventDatabase</title>
<link rel="icon" type="image/png" href="/static/oedb.png">
<link rel="icon" type="image/png" href="/static/oedb.png">
<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="/static/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<link rel="stylesheet" href="/static/traffic.css">
<script src="/static/demo_auth.js"></script>
<script src="/static/traffic.js" defer></script>
</head>
<body>
<div class="container">

View file

@ -260,7 +260,7 @@
<div class="note">Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"</div>
</div>
<button id="report_issue_button" type="submit" disabled>Signaler le problème</button>
<button id="report_issue_button" type="submit">Signaler le problème</button>
</form>
<div id="result"></div>

View file

@ -149,7 +149,7 @@ class LiveResource:
</div>
</div>
<script>
const API_URL = 'https://api.openeventdatabase.org/event?limit=1000';
const API_URL = 'https://api.openeventdatabase.org/event?when=last30days&limit=1000';
let chart;
let allFeatures = [];
let familySet = new Set();

View file

@ -17,3 +17,5 @@ harakiri = 30
max-requests = 1000
socket-timeout = 120
log-date = true
py-autoreload = 1
touch-reload = %p