up pages
This commit is contained in:
parent
2bb77d2300
commit
98c40b2447
16 changed files with 1836 additions and 361 deletions
|
@ -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
160
doc/api_endpoints.md
Normal 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)
|
|
@ -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__":
|
||||
|
|
187
extractors/agenda_geek_scraper.log
Normal file
187
extractors/agenda_geek_scraper.log
Normal 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 d’animation d’Hayao 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 d’autres ! \nAu croisement de la musique classique et de la pop culture, une évasion le temps d’un 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
|
|
@ -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'):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -3,7 +3,6 @@
|
|||
// Global variables
|
||||
let map;
|
||||
let marker;
|
||||
let geocoder;
|
||||
let currentPosition;
|
||||
let currentIssueType = null;
|
||||
let photoFiles = [];
|
||||
|
@ -11,14 +10,18 @@ let panoramaxUploadUrl = '';
|
|||
let panoramaxToken = '';
|
||||
|
||||
// Initialize the map when the page loads
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
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,15 +29,8 @@ 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() {
|
||||
function initMap() {
|
||||
// Create the map
|
||||
map = new maplibregl.Map({
|
||||
container: 'map',
|
||||
|
@ -54,7 +50,7 @@ function initMap() {
|
|||
// Try to get the user's current location
|
||||
if (navigator.geolocation) {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(position) {
|
||||
function (position) {
|
||||
currentPosition = [position.coords.longitude, position.coords.latitude];
|
||||
|
||||
// Center the map on the user's location
|
||||
|
@ -76,14 +72,14 @@ function initMap() {
|
|||
// Update the coordinates display
|
||||
updateCoordinates();
|
||||
},
|
||||
function(error) {
|
||||
function (error) {
|
||||
console.error('Error getting location:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Add click handler to the map
|
||||
map.on('click', function(e) {
|
||||
map.on('click', function (e) {
|
||||
// If we don't have a marker yet, create one
|
||||
if (!marker) {
|
||||
marker = new maplibregl.Marker({
|
||||
|
@ -102,15 +98,15 @@ function initMap() {
|
|||
// Update the coordinates display
|
||||
updateCoordinates();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the tabs
|
||||
function initTabs() {
|
||||
function initTabs() {
|
||||
const tabItems = document.querySelectorAll('.tab-item');
|
||||
const tabPanes = document.querySelectorAll('.tab-pane');
|
||||
|
||||
tabItems.forEach(item => {
|
||||
item.addEventListener('click', function() {
|
||||
item.addEventListener('click', function () {
|
||||
// Remove active class from all tabs
|
||||
tabItems.forEach(tab => tab.classList.remove('active'));
|
||||
tabPanes.forEach(pane => pane.classList.remove('active'));
|
||||
|
@ -123,10 +119,10 @@ function initTabs() {
|
|||
document.getElementById(tabId + '-tab').classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the form
|
||||
function initForm() {
|
||||
function initForm() {
|
||||
// Set the current date and time as the default
|
||||
const now = new Date();
|
||||
const dateTimeString = now.toISOString().slice(0, 16);
|
||||
|
@ -142,10 +138,18 @@ 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
|
||||
function updateCoordinates() {
|
||||
function updateCoordinates() {
|
||||
if (!marker) return;
|
||||
|
||||
const lngLat = marker.getLngLat();
|
||||
|
@ -165,68 +169,10 @@ function updateCoordinates() {
|
|||
coordinates: [lngLat.lng, lngLat.lat]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
function handlePhotoUpload(event) {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
|
@ -240,7 +186,7 @@ function handlePhotoUpload(event) {
|
|||
|
||||
photoFiles.forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
reader.onload = function (e) {
|
||||
const img = document.createElement('img');
|
||||
img.src = e.target.result;
|
||||
img.className = 'photo-preview';
|
||||
|
@ -251,10 +197,10 @@ function handlePhotoUpload(event) {
|
|||
|
||||
previewContainer.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Submit the report
|
||||
async function submitReport(event) {
|
||||
async function submitReport(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// Show loading message
|
||||
|
@ -341,10 +287,10 @@ async function submitReport(event) {
|
|||
resultElement.textContent = `Error: ${error.message}`;
|
||||
resultElement.className = 'error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Upload photos to Panoramax
|
||||
async function uploadPhotos(files) {
|
||||
async function uploadPhotos(files) {
|
||||
const urls = [];
|
||||
|
||||
for (const file of files) {
|
||||
|
@ -375,4 +321,293 @@ 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);
|
||||
}
|
|
@ -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>
|
||||
<table>
|
||||
<thead><tr><th>Clé</th><th>Valeur</th></tr></thead>
|
||||
<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>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>
|
||||
</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 %}
|
|
@ -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 %}
|
||||
|
||||
|
|
533
oedb/resources/demo/templates/map_by_what_type.html
Normal file
533
oedb/resources/demo/templates/map_by_what_type.html
Normal 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>
|
|
@ -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">
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -17,3 +17,5 @@ harakiri = 30
|
|||
max-requests = 1000
|
||||
socket-timeout = 120
|
||||
log-date = true
|
||||
py-autoreload = 1
|
||||
touch-reload = %p
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue