From 9aa8da5872687426e68181ea0a57913fcc94aa05 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Fri, 26 Sep 2025 15:08:33 +0200 Subject: [PATCH] split templates --- CHANGES.md | 103 + backend.py | 3 + oedb/resources/db_dump.py | 193 ++ oedb/resources/demo.py | 1703 +---------------- oedb/resources/demo/demo_traffic.py | 2 +- oedb/resources/demo/demo_view_events.py | 2 +- oedb/resources/demo/static/edit.css | 98 + oedb/resources/demo/static/edit.js | 209 ++ oedb/resources/demo/static/map_by_what.css | 112 ++ oedb/resources/demo/static/map_by_what.js | 302 +++ oedb/resources/demo/static/search.css | 153 ++ oedb/resources/demo/static/search.js | 412 ++++ oedb/resources/demo/static/traffic.js | 1056 ++++------ oedb/resources/demo/static/traffic_tabs.js | 63 + oedb/resources/demo/static/view_events.js | 174 ++ oedb/resources/demo/templates/by_what.html | 53 + oedb/resources/demo/templates/edit.html | 90 + .../resources/demo/templates/map_by_what.html | 36 + oedb/resources/demo/templates/search.html | 166 ++ .../resources/demo/templates/traffic_new.html | 275 +++ .../demo/templates/view_events_new.html | 157 ++ oedb/resources/live.py | 2 +- 22 files changed, 2980 insertions(+), 2384 deletions(-) create mode 100644 CHANGES.md create mode 100644 oedb/resources/db_dump.py create mode 100644 oedb/resources/demo/static/edit.css create mode 100644 oedb/resources/demo/static/edit.js create mode 100644 oedb/resources/demo/static/map_by_what.css create mode 100644 oedb/resources/demo/static/map_by_what.js create mode 100644 oedb/resources/demo/static/search.css create mode 100644 oedb/resources/demo/static/search.js create mode 100644 oedb/resources/demo/static/traffic_tabs.js create mode 100644 oedb/resources/demo/static/view_events.js create mode 100644 oedb/resources/demo/templates/by_what.html create mode 100644 oedb/resources/demo/templates/edit.html create mode 100644 oedb/resources/demo/templates/map_by_what.html create mode 100644 oedb/resources/demo/templates/search.html create mode 100644 oedb/resources/demo/templates/traffic_new.html create mode 100644 oedb/resources/demo/templates/view_events_new.html diff --git a/CHANGES.md b/CHANGES.md new file mode 100644 index 0000000..8513d71 --- /dev/null +++ b/CHANGES.md @@ -0,0 +1,103 @@ +# Changes Implemented + +## 1. Delete Button for Events + +The delete button was already implemented in the event edit page: +- The button exists in `/oedb/resources/demo/templates/edit.html` (line 77) +- The JavaScript functionality to send a DELETE request is implemented in `/oedb/resources/demo/static/edit.js` (lines 167-209) +- When clicked, the button sends a DELETE request to `/event/{id}` and handles the response + +## 2. Force Atlas Graph in Live Page + +Modified the force atlas graph in the live page to use event types from the last 1000 events: +- Updated the API URL in `/oedb/resources/live.py` from: + ```javascript + const API_URL = 'https://api.openeventdatabase.org/event?when=last7days&limit=2000'; + ``` + to: + ```javascript + const API_URL = 'https://api.openeventdatabase.org/event?limit=1000'; + ``` +- The existing implementation already groups events by "what" field in the `buildFamilyGraph` function (lines 321-348) + +## 3. Database Dump Endpoints + +Created new endpoints for database dumps: + +1. Created a new file `/oedb/resources/db_dump.py` with two resource classes: + - `DbDumpListResource`: Lists existing database dumps + - `DbDumpCreateResource`: Creates new dumps in SQL and GeoJSON formats + +2. Implemented features: + - Created a directory to store database dumps + - Used `pg_dump` to create SQL dumps + - Queried the database and converted to GeoJSON for GeoJSON dumps + - Included timestamps in the filenames (e.g., `oedb_dump_20250926_145800.sql`) + - Added proper error handling and logging + +3. Updated `/backend.py` to: + - Import the new resources + - Register the new endpoints: + - `/db/dumps`: Lists all available database dumps + - `/db/dumps/create`: Creates new database dumps + +## Usage + +### Listing Database Dumps + +Send a GET request to `/db/dumps` to get a list of all available database dumps: + +``` +GET /db/dumps +``` + +Response: +```json +{ + "dumps": [ + { + "filename": "oedb_dump_20250926_145800.sql", + "path": "/db/dumps/oedb_dump_20250926_145800.sql", + "size": 1234567, + "created": "2025-09-26T14:58:00", + "type": "sql" + }, + { + "filename": "oedb_dump_20250926_145800.geojson", + "path": "/db/dumps/oedb_dump_20250926_145800.geojson", + "size": 7654321, + "created": "2025-09-26T14:58:00", + "type": "geojson" + } + ] +} +``` + +### Creating Database Dumps + +Send a POST request to `/db/dumps/create` to create new database dumps: + +``` +POST /db/dumps/create +``` + +Response: +```json +{ + "message": "Database dumps created successfully", + "dumps": [ + { + "filename": "oedb_dump_20250926_145800.sql", + "path": "/db/dumps/oedb_dump_20250926_145800.sql", + "type": "sql", + "size": 1234567 + }, + { + "filename": "oedb_dump_20250926_145800.geojson", + "path": "/db/dumps/oedb_dump_20250926_145800.geojson", + "type": "geojson", + "size": 7654321 + } + ] +} +``` \ No newline at end of file diff --git a/backend.py b/backend.py index 9e30b8e..0b23558 100644 --- a/backend.py +++ b/backend.py @@ -28,6 +28,7 @@ from oedb.resources.demo import demo, demo_stats from oedb.resources.live import live from oedb.resources.rss import rss_latest, rss_by_family from oedb.resources.event_form import event_form +from oedb.resources.db_dump import db_dump_list, db_dump_create def create_app(): """ @@ -95,6 +96,8 @@ def create_app(): app.add_route('/demo/live', live) # Live page app.add_route('/rss', rss_latest) # RSS latest 200 app.add_route('/rss/by/{family}', rss_by_family) # RSS by family + app.add_route('/db/dumps', db_dump_list) # List database dumps + app.add_route('/db/dumps/create', db_dump_create) # Create database dumps logger.success("Application initialized successfully") return app diff --git a/oedb/resources/db_dump.py b/oedb/resources/db_dump.py new file mode 100644 index 0000000..4c8634d --- /dev/null +++ b/oedb/resources/db_dump.py @@ -0,0 +1,193 @@ +""" +Database dump resource for the OpenEventDatabase. +Provides endpoints to list and create database dumps. +""" + +import os +import subprocess +import datetime +import falcon +import psycopg2.extras +import json +from pathlib import Path +from oedb.utils.db import db_connect +from oedb.utils.serialization import dumps +from oedb.utils.logging import logger + +# Directory to store database dumps +DUMPS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../dumps')) + +# Ensure the dumps directory exists +os.makedirs(DUMPS_DIR, exist_ok=True) + +class DbDumpListResource: + """ + Resource for listing database dumps. + Handles the /db/dumps endpoint. + """ + + def on_get(self, req, resp): + """ + Handle GET requests to the /db/dumps endpoint. + Lists all available database dumps. + + Args: + req: The request object. + resp: The response object. + """ + logger.info("Processing GET request to /db/dumps") + + try: + # Get list of dump files + dump_files = [] + for ext in ['sql', 'geojson']: + for file_path in Path(DUMPS_DIR).glob(f'*.{ext}'): + stat = file_path.stat() + dump_files.append({ + 'filename': file_path.name, + 'path': f'/db/dumps/{file_path.name}', + 'size': stat.st_size, + 'created': datetime.datetime.fromtimestamp(stat.st_ctime).isoformat(), + 'type': ext + }) + + # Sort by creation time (newest first) + dump_files.sort(key=lambda x: x['created'], reverse=True) + + resp.text = dumps({'dumps': dump_files}) + resp.status = falcon.HTTP_200 + logger.success("Successfully processed GET request to /db/dumps") + except Exception as e: + logger.error(f"Error processing GET request to /db/dumps: {e}") + resp.status = falcon.HTTP_500 + resp.text = dumps({"error": str(e)}) + +class DbDumpCreateResource: + """ + Resource for creating database dumps. + Handles the /db/dumps/create endpoint. + """ + + def on_post(self, req, resp): + """ + Handle POST requests to the /db/dumps/create endpoint. + Creates a new database dump in SQL and GeoJSON formats. + + Args: + req: The request object. + resp: The response object. + """ + logger.info("Processing POST request to /db/dumps/create") + + try: + # Generate timestamp for filenames + timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + + # Create SQL dump + sql_filename = f"oedb_dump_{timestamp}.sql" + sql_path = os.path.join(DUMPS_DIR, sql_filename) + + # Get database connection parameters from environment + dbname = os.getenv("DB_NAME", "oedb") + host = os.getenv("DB_HOST", "localhost") + user = os.getenv("DB_USER", "postgres") + password = os.getenv("POSTGRES_PASSWORD", "") + + # Set PGPASSWORD environment variable for pg_dump + env = os.environ.copy() + env["PGPASSWORD"] = password + + # Execute pg_dump command + pg_dump_cmd = [ + "pg_dump", + "-h", host, + "-U", user, + "-d", dbname, + "-f", sql_path + ] + + logger.info(f"Creating SQL dump: {sql_filename}") + subprocess.run(pg_dump_cmd, env=env, check=True) + + # Create GeoJSON dump + geojson_filename = f"oedb_dump_{timestamp}.geojson" + geojson_path = os.path.join(DUMPS_DIR, geojson_filename) + + # Connect to database + db = db_connect() + cur = db.cursor(cursor_factory=psycopg2.extras.RealDictCursor) + + # Query all events + logger.info(f"Creating GeoJSON dump: {geojson_filename}") + cur.execute("SELECT * FROM events;") + rows = cur.fetchall() + + # Convert to GeoJSON + features = [] + for row in rows: + # Extract geometry + geom = None + if row.get('events_where'): + try: + geom = json.loads(row['events_where']) + except: + pass + + # Create feature + feature = { + "type": "Feature", + "geometry": geom or {"type": "Point", "coordinates": [0, 0]}, + "properties": { + "id": row.get('events_id'), + "what": row.get('events_what'), + "label": row.get('events_label'), + "when": { + "start": row.get('events_when', {}).get('lower', None), + "stop": row.get('events_when', {}).get('upper', None) + }, + "tags": row.get('events_tags'), + "createdate": row.get('createdate'), + "lastupdate": row.get('lastupdate') + } + } + features.append(feature) + + # Write GeoJSON file + with open(geojson_path, 'w') as f: + json.dump({ + "type": "FeatureCollection", + "features": features + }, f) + + # Return information about created dumps + resp.text = dumps({ + "message": "Database dumps created successfully", + "dumps": [ + { + "filename": sql_filename, + "path": f"/db/dumps/{sql_filename}", + "type": "sql", + "size": os.path.getsize(sql_path) + }, + { + "filename": geojson_filename, + "path": f"/db/dumps/{geojson_filename}", + "type": "geojson", + "size": os.path.getsize(geojson_path) + } + ] + }) + resp.status = falcon.HTTP_201 + logger.success("Successfully processed POST request to /db/dumps/create") + except Exception as e: + logger.error(f"Error processing POST request to /db/dumps/create: {e}") + resp.status = falcon.HTTP_500 + resp.text = dumps({"error": str(e)}) + finally: + if 'db' in locals() and db: + cur.close() + db.close() + +# Create resource instances +db_dump_list = DbDumpListResource() +db_dump_create = DbDumpCreateResource() \ No newline at end of file diff --git a/oedb/resources/demo.py b/oedb/resources/demo.py index 827b7ae..01b091f 100644 --- a/oedb/resources/demo.py +++ b/oedb/resources/demo.py @@ -62,419 +62,15 @@ class DemoResource: event_data = response.json() - # Create HTML response with form - html = f""" - - - - - - Edit Event - OpenEventDatabase - - - - - - - - -
- - -

Edit Event

- -
- - -
- - -
- -
-
- - -
- -
- - -
Category of the event (e.g., sport.match.football, culture.festival)
-
-
- -
-
- - -
Series or group the event belongs to (e.g., Euro 2024, Summer Festival 2023)
-
- -
- - -
Specific location name (e.g., Eiffel Tower, Wembley Stadium)
-
-
- -
-
- - -
- -
- - -
-
- -
- -
-
Click on the map to set the event location
-
- -
- - -
-
- -
-
- - - - - """ + # Render the template with the event data + template = self.jinja_env.get_template('edit.html') + html = template.render( + id=id, + event_data=json.dumps(event_data) + ) # Set the response body and status - resp.text = html.replace('{event_data}', json.dumps(event_data)) + resp.text = html resp.status = falcon.HTTP_200 logger.success(f"Successfully processed GET request to /demo/edit for event ID: {id}") except Exception as e: @@ -536,96 +132,16 @@ class DemoResource: 'coordinates': feature.get('geometry', {}).get('coordinates', [0, 0]) }) - # Create HTML response - html = """ - - - - - - Events by Type - OpenEventDatabase - - - - - -

Events by Type

-

This page lists all events from the OpenEventDatabase organized by their type.

- """ + # Load and render the template with the appropriate variables + template = self.jinja_env.get_template('by_what.html') - # Add event types and their events - if events_by_what: - # Sort event types alphabetically - sorted_what_types = sorted(events_by_what.keys()) - - # Add quick navigation - html += "

Quick Navigation

" - - # Add sections for each event type - for what_type in sorted_what_types: - events = events_by_what[what_type] - html += f'

{what_type} ({len(events)} events)

' - html += "" - else: - html += "

No events found in the database.

" + # Sort event types alphabetically if we have events + sorted_what_types = sorted(events_by_what.keys()) if events_by_what else [] - html += """ - - - """ + html = template.render( + events_by_what=events_by_what, + sorted_what_types=sorted_what_types + ) # Set the response body and status resp.text = html @@ -651,746 +167,9 @@ class DemoResource: # Set content type to HTML resp.content_type = 'text/html' - # Create HTML response with search form - html = """ - - - - - - Search Events - OpenEventDatabase - - - - - - - -
- - -

Search Events

- -
-
-
- - -
Category of the event (e.g., sport.match.football, culture.festival)
-
- -
- - -
-
- -
-
- - -
- - - - -
- -
-
- - -
Search for events near a specific location (e.g., 2.3522,48.8566,10000 for events within 10km of Paris)
-
- -
- - -
Search for events within a geographic bounding box
-
-
- -
-
- - -
Search for events associated with a specific OpenStreetMap ID
-
- -
- - -
Search for events associated with a specific Wikidata ID
-
-
- -
-
- - -
Maximum number of results to return (default: 200)
-
- -
- - -
Controls the level of detail in the geometry portion of the response
-
-
- -
- -
-
Draw a polygon on the map to define the search area, or use the form fields above
-
- - -
- -
- - -
- - - - - """ + # Render the template + template = self.jinja_env.get_template('search.html') + html = template.render() # Set the response body and status resp.text = html @@ -1416,449 +195,9 @@ class DemoResource: # Set content type to HTML resp.content_type = 'text/html' - # Create HTML response with MapLibre map and filtering controls - html = """ - - - - - - Map by Event Type - OpenEventDatabase - - - - - -
- -
-

Map by Event Type

- -

This map shows events from the OpenEventDatabase filtered by their type.

-

Use the filter panel on the right to show/hide different event types.

-
-

Loading events...

-
-
- -
-

Filter by Event Type

-
- - -
- -
- - - - - """ + # Render the template + template = self.jinja_env.get_template('map_by_what.html') + html = template.render() # Set the response body and status resp.text = html diff --git a/oedb/resources/demo/demo_traffic.py b/oedb/resources/demo/demo_traffic.py index 770a5a7..c14608a 100644 --- a/oedb/resources/demo/demo_traffic.py +++ b/oedb/resources/demo/demo_traffic.py @@ -106,7 +106,7 @@ class DemoTrafficResource: logger.error(f"Error during OAuth2 token exchange: {e}") # Load and render the template with the appropriate variables - template = self.jinja_env.get_template('traffic.html') + template = self.jinja_env.get_template('traffic_new.html') html = template.render( client_id=client_id, client_secret=client_secret, diff --git a/oedb/resources/demo/demo_view_events.py b/oedb/resources/demo/demo_view_events.py index 06bb1e1..d7423e3 100644 --- a/oedb/resources/demo/demo_view_events.py +++ b/oedb/resources/demo/demo_view_events.py @@ -49,7 +49,7 @@ class DemoViewEventsResource: client_redirect = os.getenv("CLIENT_REDIRECT", "") # Load and render the template with the appropriate variables - template = self.jinja_env.get_template('view_events.html') + template = self.jinja_env.get_template('view_events_new.html') html = template.render( client_id=client_id, client_secret=client_secret, diff --git a/oedb/resources/demo/static/edit.css b/oedb/resources/demo/static/edit.css new file mode 100644 index 0000000..bda0b6f --- /dev/null +++ b/oedb/resources/demo/static/edit.css @@ -0,0 +1,98 @@ +body { + margin: 0; + padding: 20px; + font-family: Arial, sans-serif; + background-color: #f5f5f5; +} +.container { + max-width: 1000px; + margin: 0 auto; + background-color: white; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} +h1 { + margin-top: 0; + color: #333; +} +.form-group { + margin-bottom: 15px; +} +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} +input[type="text"], +input[type="datetime-local"], +select, +textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; +} +.required:after { + content: " *"; + color: red; +} +.form-row { + display: flex; + gap: 15px; +} +.form-row .form-group { + flex: 1; +} +button { + background-color: #0078ff; + color: white; + border: none; + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} +button:hover { + background-color: #0056b3; +} +.note { + font-size: 12px; + color: #666; + margin-top: 5px; +} +#map { + width: 100%; + height: 300px; + margin-bottom: 15px; + border-radius: 4px; +} +#result { + margin-top: 20px; + padding: 10px; + border-radius: 4px; + display: none; +} +#result.success { + background-color: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} +#result.error { + background-color: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} +.nav-links { + margin-bottom: 20px; +} +.nav-links a { + color: #0078ff; + text-decoration: none; + margin-right: 15px; +} +.nav-links a:hover { + text-decoration: underline; +} \ No newline at end of file diff --git a/oedb/resources/demo/static/edit.js b/oedb/resources/demo/static/edit.js new file mode 100644 index 0000000..b5ce3f6 --- /dev/null +++ b/oedb/resources/demo/static/edit.js @@ -0,0 +1,209 @@ +// Initialize the map +const map = new maplibregl.Map({ + container: 'map', + style: 'https://tiles.openfreemap.org/styles/liberty', + center: [2.2137, 46.2276], // Default center (center of metropolitan France) + zoom: 5 +}); + +// Add navigation controls +map.addControl(new maplibregl.NavigationControl()); + +// Add attribution control with OpenStreetMap attribution +map.addControl(new maplibregl.AttributionControl({ + customAttribution: '© OpenStreetMap contributors' +})); + +// Add marker for event location +let marker = new maplibregl.Marker({ + draggable: true +}); + +// Function to populate form with event data +function populateForm() { + if (!eventData || !eventData.properties) { + showResult('Error loading event data', 'error'); + return; + } + + const properties = eventData.properties; + + // Set form values + document.getElementById('label').value = properties.label || ''; + document.getElementById('type').value = properties.type || 'scheduled'; + document.getElementById('what').value = properties.what || ''; + + // Handle optional fields + if (properties['what:series']) { + document.getElementById('what_series').value = properties['what:series']; + } + + if (properties.where) { + document.getElementById('where').value = properties.where; + } + + // Format dates for datetime-local input + if (properties.start) { + const startDate = new Date(properties.start); + document.getElementById('start').value = startDate.toISOString().slice(0, 16); + } + + if (properties.stop) { + const stopDate = new Date(properties.stop); + document.getElementById('stop').value = stopDate.toISOString().slice(0, 16); + } + + // Set marker on map + if (eventData.geometry && eventData.geometry.coordinates) { + const coords = eventData.geometry.coordinates; + marker.setLngLat(coords).addTo(map); + + // Center map on event location + map.flyTo({ + center: coords, + zoom: 10 + }); + } +} + +// Call function to populate form +populateForm(); + +// Add marker on map click +map.on('click', function(e) { + marker.setLngLat(e.lngLat).addTo(map); +}); + +// Function to show result message +function showResult(message, type) { + const resultElement = document.getElementById('result'); + resultElement.textContent = message; + resultElement.className = type; + resultElement.style.display = 'block'; + + // Scroll to result + resultElement.scrollIntoView({ behavior: 'smooth' }); +} + +// Handle form submission +document.getElementById('eventForm').addEventListener('submit', function(e) { + e.preventDefault(); + + // Get event ID + const eventId = document.getElementById('eventId').value; + + // Get form values + const label = document.getElementById('label').value; + const type = document.getElementById('type').value; + const what = document.getElementById('what').value; + const what_series = document.getElementById('what_series').value; + const where = document.getElementById('where').value; + const start = document.getElementById('start').value; + const stop = document.getElementById('stop').value; + + // Check if marker is set + if (!marker.getLngLat()) { + showResult('Please set a location by clicking on the map', 'error'); + return; + } + + // Get marker coordinates + const lngLat = marker.getLngLat(); + + // Create event object + const event = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [lngLat.lng, lngLat.lat] + }, + properties: { + label: label, + type: type, + what: what, + start: start, + stop: stop + } + }; + + // Add optional properties if provided + if (what_series) { + event.properties['what:series'] = what_series; + } + + if (where) { + event.properties.where = where; + } + + // Submit event to API + fetch(`/event/${eventId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(event) + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + return response.text().then(text => { + throw new Error(text || response.statusText); + }); + } + }) + .then(data => { + showResult(`Event updated successfully with ID: ${data.id}`, 'success'); + + // Add link to view the event + const resultElement = document.getElementById('result'); + resultElement.innerHTML += `

View Event | Back to Map

`; + }) + .catch(error => { + showResult(`Error: ${error.message}`, 'error'); + }); +}); + +// Handle delete button click +document.getElementById('deleteButton').addEventListener('click', function() { + // Get event ID + 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}`, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => { + if (response.ok) { + showResult('Event deleted successfully', 'success'); + + // Add link to go back to map + const resultElement = document.getElementById('result'); + resultElement.innerHTML += `

Back to Map

`; + + // Disable form controls + const formElements = document.querySelectorAll('#eventForm input, #eventForm select, #eventForm button'); + formElements.forEach(element => { + element.disabled = true; + }); + + // Redirect to demo page after 2 seconds + setTimeout(() => { + window.location.href = '/demo'; + }, 2000); + } else { + return response.text().then(text => { + throw new Error(text || response.statusText); + }); + } + }) + .catch(error => { + showResult(`Error deleting event: ${error.message}`, 'error'); + }); + } +}); \ No newline at end of file diff --git a/oedb/resources/demo/static/map_by_what.css b/oedb/resources/demo/static/map_by_what.css new file mode 100644 index 0000000..efa558a --- /dev/null +++ b/oedb/resources/demo/static/map_by_what.css @@ -0,0 +1,112 @@ +body { + margin: 0; + padding: 0; + font-family: Arial, sans-serif; +} +#map { + position: absolute; + top: 0; + bottom: 0; + width: 100%; +} + +.map-overlay { + position: absolute; + top: 10px; + left: 10px; + background: rgba(255, 255, 255, 0.9); + padding: 10px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + max-width: 300px; + max-height: 90vh; + overflow-y: auto; +} + +.filter-overlay { + position: absolute; + top: 10px; + right: 10px; + background: rgba(255, 255, 255, 0.9); + padding: 10px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + max-width: 300px; + max-height: 90vh; + overflow-y: auto; +} + +h2, h3 { + margin-top: 0; +} +ul { + padding-left: 20px; +} +a { + color: #0078ff; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} +.event-popup { + max-width: 300px; +} + +.filter-list { + list-style: none; + padding: 0; + margin: 0; +} + +.filter-item { + margin-bottom: 8px; + display: flex; + align-items: center; +} + +.filter-item input { + margin-right: 8px; +} + +.filter-item label { + cursor: pointer; + flex-grow: 1; +} + +.filter-count { + color: #666; + font-size: 0.8em; + margin-left: 5px; +} + +.color-dot { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 50%; + margin-right: 5px; +} + +.nav { + margin-bottom: 15px; +} + +.nav a { + margin-right: 15px; +} + +button { + background-color: #0078ff; + color: white; + border: none; + padding: 5px 10px; + border-radius: 3px; + cursor: pointer; + margin-right: 5px; + margin-bottom: 5px; +} + +button:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/oedb/resources/demo/static/map_by_what.js b/oedb/resources/demo/static/map_by_what.js new file mode 100644 index 0000000..577fc2d --- /dev/null +++ b/oedb/resources/demo/static/map_by_what.js @@ -0,0 +1,302 @@ +// Initialize the map +const map = new maplibregl.Map({ + container: 'map', + style: 'https://tiles.openfreemap.org/styles/liberty', + center: [2.3522, 48.8566], // Default center (Paris) + zoom: 4 +}); + +// Add navigation controls +map.addControl(new maplibregl.NavigationControl()); + +// Add attribution control with OpenStreetMap attribution +map.addControl(new maplibregl.AttributionControl({ + customAttribution: '© OpenStreetMap contributors' +})); + +// Store all events and their types +let allEvents = null; +let eventTypes = new Set(); +let eventsByType = {}; +let markersByType = {}; +let colorsByType = {}; + +// Generate a color for each event type +function getColorForType(type, index) { + // Predefined colors for better visual distinction + const colors = [ + '#FF5722', '#E91E63', '#9C27B0', '#673AB7', '#3F51B5', + '#2196F3', '#03A9F4', '#00BCD4', '#009688', '#4CAF50', + '#8BC34A', '#CDDC39', '#FFEB3B', '#FFC107', '#FF9800' + ]; + + return colors[index % colors.length]; +} + +// Fetch events when the map is loaded +map.on('load', function() { + fetchEvents(); +}); + +// Function to fetch events from the API +function fetchEvents() { + // Update event info + document.getElementById('event-info').innerHTML = '

Loading events...

'; + + // Fetch events from the public API - using limit=1000 to get more events + fetch('https://api.openeventdatabase.org/event?limit=1000') + .then(response => response.json()) + .then(data => { + if (data.features && data.features.length > 0) { + // Store all events + allEvents = data; + + // Process events by type + processEventsByType(data); + + // Create filter UI + createFilterUI(); + + // Add all events to the map initially + addAllEventsToMap(); + + // Fit map to events bounds + fitMapToBounds(data); + + // Update event info + document.getElementById('event-info').innerHTML = + `

Found ${data.features.length} events across ${eventTypes.size} different types.

`; + } else { + document.getElementById('event-info').innerHTML = '

No events found.

'; + document.getElementById('filter-list').innerHTML = '
  • No event types available.
  • '; + } + }) + .catch(error => { + console.error('Error fetching events:', error); + document.getElementById('event-info').innerHTML = + `

    Error loading events: ${error.message}

    `; + }); +} + +// Process events by their "what" type +function processEventsByType(data) { + eventTypes = new Set(); + eventsByType = {}; + + // Group events by their "what" type + data.features.forEach(feature => { + const properties = feature.properties; + const what = properties.what || 'Unknown'; + + // Add to set of event types + eventTypes.add(what); + + // Add to events by type + if (!eventsByType[what]) { + eventsByType[what] = []; + } + eventsByType[what].push(feature); + }); + + // Assign colors to each type + let index = 0; + eventTypes.forEach(type => { + colorsByType[type] = getColorForType(type, index); + index++; + }); +} + +// Create the filter UI +function createFilterUI() { + const filterList = document.getElementById('filter-list'); + filterList.innerHTML = ''; + + // Sort event types alphabetically + const sortedTypes = Array.from(eventTypes).sort(); + + // Create a checkbox for each event type + sortedTypes.forEach(type => { + const count = eventsByType[type].length; + const color = colorsByType[type]; + + const li = document.createElement('li'); + li.className = 'filter-item'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + checkbox.id = `filter-${type}`; + checkbox.checked = true; + checkbox.addEventListener('change', () => { + toggleEventType(type, checkbox.checked); + }); + + const colorDot = document.createElement('span'); + colorDot.className = 'color-dot'; + colorDot.style.backgroundColor = color; + + const label = document.createElement('label'); + label.htmlFor = `filter-${type}`; + label.appendChild(colorDot); + label.appendChild(document.createTextNode(type)); + + const countSpan = document.createElement('span'); + countSpan.className = 'filter-count'; + countSpan.textContent = `(${count})`; + label.appendChild(countSpan); + + li.appendChild(checkbox); + li.appendChild(label); + filterList.appendChild(li); + }); + + // Add event listeners for select/deselect all buttons + document.getElementById('select-all').addEventListener('click', selectAllEventTypes); + document.getElementById('deselect-all').addEventListener('click', deselectAllEventTypes); +} + +// Add all events to the map +function addAllEventsToMap() { + // Clear existing markers + clearAllMarkers(); + + // Add markers for each event type + Object.keys(eventsByType).forEach(type => { + addEventsOfTypeToMap(type); + }); +} + +// Add events of a specific type to the map +function addEventsOfTypeToMap(type) { + if (!markersByType[type]) { + markersByType[type] = []; + } + + const events = eventsByType[type]; + const color = colorsByType[type]; + + events.forEach(feature => { + const coordinates = feature.geometry.coordinates.slice(); + const properties = feature.properties; + + // Create popup content + let popupContent = '
    '; + popupContent += `

    ${properties.label || 'Event'}

    `; + popupContent += `

    Type: ${type}

    `; + + // Display all properties + popupContent += '
    '; + popupContent += ''; + + // Sort properties alphabetically for better organization + const sortedKeys = Object.keys(properties).sort(); + + for (const key of sortedKeys) { + // Skip the label as it's already displayed as the title + if (key === 'label') continue; + + const value = properties[key]; + let displayValue; + + // Format the value based on its type + if (value === null || value === undefined) { + displayValue = 'null'; + } else if (typeof value === 'object') { + displayValue = `
    ${JSON.stringify(value, null, 2)}
    `; + } else if (typeof value === 'string' && value.startsWith('http')) { + displayValue = `${value}`; + } else { + displayValue = String(value); + } + + popupContent += ` + + + + `; + } + + popupContent += '
    ${key}:${displayValue}
    '; + popupContent += '
    '; + + popupContent += '
    '; + + // Create popup + const popup = new maplibregl.Popup({ + closeButton: true, + closeOnClick: true + }).setHTML(popupContent); + + // Add marker with popup + const marker = new maplibregl.Marker({ + color: color + }) + .setLngLat(coordinates) + .setPopup(popup) + .addTo(map); + + // Store marker reference + markersByType[type].push(marker); + }); +} + +// Toggle visibility of events by type +function toggleEventType(type, visible) { + if (!markersByType[type]) return; + + markersByType[type].forEach(marker => { + if (visible) { + marker.addTo(map); + } else { + marker.remove(); + } + }); +} + +// Select all event types +function selectAllEventTypes() { + const checkboxes = document.querySelectorAll('#filter-list input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + checkbox.checked = true; + const type = checkbox.id.replace('filter-', ''); + toggleEventType(type, true); + }); +} + +// Deselect all event types +function deselectAllEventTypes() { + const checkboxes = document.querySelectorAll('#filter-list input[type="checkbox"]'); + checkboxes.forEach(checkbox => { + checkbox.checked = false; + const type = checkbox.id.replace('filter-', ''); + toggleEventType(type, false); + }); +} + +// Clear all markers from the map +function clearAllMarkers() { + Object.keys(markersByType).forEach(type => { + if (markersByType[type]) { + markersByType[type].forEach(marker => marker.remove()); + } + }); + markersByType = {}; +} + +// Function to fit map to events bounds +function fitMapToBounds(geojson) { + if (geojson.features.length === 0) return; + + // Create a bounds object + const bounds = new maplibregl.LngLatBounds(); + + // Extend bounds with each feature + geojson.features.forEach(feature => { + bounds.extend(feature.geometry.coordinates); + }); + + // Fit map to bounds with padding + map.fitBounds(bounds, { + padding: 50, + maxZoom: 12 + }); +} \ No newline at end of file diff --git a/oedb/resources/demo/static/search.css b/oedb/resources/demo/static/search.css new file mode 100644 index 0000000..db093e6 --- /dev/null +++ b/oedb/resources/demo/static/search.css @@ -0,0 +1,153 @@ +body { + margin: 0; + padding: 20px; + font-family: Arial, sans-serif; + background-color: #f5f5f5; +} +.container { + max-width: 1200px; + margin: 0 auto; + background-color: white; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} +h1 { + margin-top: 0; + color: #333; +} +.nav-links { + margin-bottom: 20px; +} +.nav-links a { + color: #0078ff; + text-decoration: none; + margin-right: 15px; +} +.nav-links a:hover { + text-decoration: underline; +} +.tabs-container { + margin-top: 20px; +} +.tab-content { + display: none; + padding: 20px; + border: 1px solid #ddd; + border-top: none; +} +.tab-content.active { + display: block; +} +.tab-buttons { + display: flex; + border-bottom: 1px solid #ddd; +} +.tab-button { + padding: 10px 20px; + background-color: #f1f1f1; + border: 1px solid #ddd; + border-bottom: none; + cursor: pointer; + margin-right: 5px; +} +.tab-button.active { + background-color: white; + border-bottom: 1px solid white; + margin-bottom: -1px; +} +#map { + width: 100%; + height: 500px; + margin-top: 20px; + border-radius: 4px; +} +.results-table { + width: 100%; + border-collapse: collapse; + margin-top: 20px; +} +.results-table th, .results-table td { + padding: 8px; + text-align: left; + border-bottom: 1px solid #ddd; +} +.results-table th { + background-color: #f2f2f2; +} +.download-buttons { + margin-top: 20px; + text-align: right; +} +.download-button { + display: inline-block; + padding: 8px 16px; + background-color: #0078ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-left: 10px; + text-decoration: none; +} +.download-button:hover { + background-color: #0056b3; +} +.form-row { + display: flex; + gap: 15px; + margin-bottom: 15px; +} +.form-group { + flex: 1; +} +label { + display: block; + margin-bottom: 5px; + font-weight: bold; +} +input[type="text"], +input[type="datetime-local"], +input[type="number"], +select, +textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; +} +.note { + font-size: 12px; + color: #666; + margin-top: 5px; +} +button { + background-color: #0078ff; + color: white; + border: none; + padding: 10px 15px; + border-radius: 4px; + cursor: pointer; + font-size: 16px; +} +button:hover { + background-color: #0056b3; +} +#result { + margin-top: 20px; + padding: 10px; + border-radius: 4px; + display: none; +} +#result.success { + background-color: #d4edda; + border: 1px solid #c3e6cb; + color: #155724; +} +#result.error { + background-color: #f8d7da; + border: 1px solid #f5c6cb; + color: #721c24; +} \ No newline at end of file diff --git a/oedb/resources/demo/static/search.js b/oedb/resources/demo/static/search.js new file mode 100644 index 0000000..5e47051 --- /dev/null +++ b/oedb/resources/demo/static/search.js @@ -0,0 +1,412 @@ +// Initialize the map +const map = new maplibregl.Map({ + container: 'map', + style: 'https://tiles.openfreemap.org/styles/liberty', + center: [2.3522, 48.8566], // Default center (Paris) + zoom: 4 +}); + +// Add navigation controls +map.addControl(new maplibregl.NavigationControl()); + +// Add draw controls for polygon +let drawnPolygon = null; +let drawingMode = false; +let points = []; +let lineString = null; +let polygonFill = null; + +// Add a button to toggle drawing mode +const drawButton = document.createElement('button'); +drawButton.textContent = 'Draw Polygon'; +drawButton.style.position = 'absolute'; +drawButton.style.top = '10px'; +drawButton.style.right = '10px'; +drawButton.style.zIndex = '1'; +drawButton.style.padding = '5px 10px'; +drawButton.style.backgroundColor = '#0078ff'; +drawButton.style.color = 'white'; +drawButton.style.border = 'none'; +drawButton.style.borderRadius = '3px'; +drawButton.style.cursor = 'pointer'; +document.getElementById('map').appendChild(drawButton); + +drawButton.addEventListener('click', () => { + drawingMode = !drawingMode; + drawButton.textContent = drawingMode ? 'Cancel Drawing' : 'Draw Polygon'; + + if (!drawingMode) { + // Clear the drawing + points = []; + if (lineString) { + map.removeLayer('line-string'); + map.removeSource('line-string'); + lineString = null; + } + if (polygonFill) { + map.removeLayer('polygon-fill'); + map.removeSource('polygon-fill'); + polygonFill = null; + } + } +}); + +// Handle map click events for drawing +map.on('click', (e) => { + if (!drawingMode) return; + + const coords = [e.lngLat.lng, e.lngLat.lat]; + points.push(coords); + + // If we have at least 3 points, create a polygon + if (points.length >= 3) { + const polygonCoords = [...points, points[0]]; // Close the polygon + + // Create or update the line string + if (lineString) { + map.removeLayer('line-string'); + map.removeSource('line-string'); + } + + lineString = { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: polygonCoords + } + }; + + map.addSource('line-string', { + type: 'geojson', + data: lineString + }); + + map.addLayer({ + id: 'line-string', + type: 'line', + source: 'line-string', + paint: { + 'line-color': '#0078ff', + 'line-width': 2 + } + }); + + // Create or update the polygon fill + if (polygonFill) { + map.removeLayer('polygon-fill'); + map.removeSource('polygon-fill'); + } + + polygonFill = { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [polygonCoords] + } + }; + + map.addSource('polygon-fill', { + type: 'geojson', + data: polygonFill + }); + + map.addLayer({ + id: 'polygon-fill', + type: 'fill', + source: 'polygon-fill', + paint: { + 'fill-color': '#0078ff', + 'fill-opacity': 0.2 + } + }); + + // Store the drawn polygon for search + drawnPolygon = { + type: 'Polygon', + coordinates: [polygonCoords] + }; + } +}); + +// Handle custom date range selection +document.getElementById('when').addEventListener('change', function() { + const customDateGroup = document.getElementById('customDateGroup'); + const customDateEndGroup = document.getElementById('customDateEndGroup'); + + if (this.value === 'custom') { + customDateGroup.style.display = 'block'; + customDateEndGroup.style.display = 'block'; + } else { + customDateGroup.style.display = 'none'; + customDateEndGroup.style.display = 'none'; + } +}); + +// Handle form submission +document.getElementById('searchForm').addEventListener('submit', function(e) { + e.preventDefault(); + + // Show loading message + const resultElement = document.getElementById('result'); + resultElement.textContent = 'Searching...'; + resultElement.className = ''; + resultElement.style.display = 'block'; + + // Get form values + const formData = new FormData(this); + const params = new URLSearchParams(); + + // Add form fields to params + for (const [key, value] of formData.entries()) { + if (value) { + params.append(key, value); + } + } + + // Handle custom date range + if (formData.get('when') === 'custom') { + params.delete('when'); + } else { + params.delete('start'); + params.delete('stop'); + } + + // Prepare the request + let url = '/event/search'; + let method = 'POST'; + let body = null; + + // If we have a drawn polygon, use it for the search + if (drawnPolygon) { + body = JSON.stringify({ + geometry: drawnPolygon + }); + } else if (formData.get('near') || formData.get('bbox')) { + // If we have near or bbox parameters, use GET request + url = '/event?' + params.toString(); + method = 'GET'; + } else { + // Default to a simple point search in Paris if no spatial filter is provided + body = JSON.stringify({ + geometry: { + type: 'Point', + coordinates: [2.3522, 48.8566] + } + }); + } + + // Make the request + fetch(url + (method === 'GET' ? '' : '?' + params.toString()), { + method: method, + headers: { + 'Content-Type': 'application/json' + }, + body: method === 'POST' ? body : null + }) + .then(response => { + if (response.ok) { + return response.json(); + } else { + return response.text().then(text => { + throw new Error(text || response.statusText); + }); + } + }) + .then(data => { + // Show success message + resultElement.textContent = `Found ${data.features ? data.features.length : 0} events`; + resultElement.className = 'success'; + + // Display results + displayResults(data); + }) + .catch(error => { + // Show error message + resultElement.textContent = `Error: ${error.message}`; + resultElement.className = 'error'; + + // Hide results container + document.getElementById('resultsContainer').style.display = 'none'; + }); +}); + +// Function to display search results +function displayResults(data) { + // Show results container + document.getElementById('resultsContainer').style.display = 'block'; + + // Initialize results map + const resultsMap = new maplibregl.Map({ + container: 'resultsMap', + style: 'https://tiles.openfreemap.org/styles/liberty', + center: [2.3522, 48.8566], // Default center (Paris) + zoom: 4 + }); + + // Add navigation controls to results map + resultsMap.addControl(new maplibregl.NavigationControl()); + + // Add events to the map + resultsMap.on('load', function() { + // Add events as a source + resultsMap.addSource('events', { + type: 'geojson', + data: data + }); + + // Add a circle layer for events + resultsMap.addLayer({ + id: 'events-circle', + type: 'circle', + source: 'events', + paint: { + 'circle-radius': 8, + 'circle-color': '#FF5722', + 'circle-stroke-width': 2, + 'circle-stroke-color': '#FFFFFF' + } + }); + + // Add popups for events + if (data.features) { + data.features.forEach(feature => { + const coordinates = feature.geometry.coordinates.slice(); + const properties = feature.properties; + + // Create popup content + let popupContent = '
    '; + popupContent += `

    ${properties.label || 'Event'}

    `; + + // Display key properties + if (properties.what) { + popupContent += `

    Type: ${properties.what}

    `; + } + if (properties.where) { + popupContent += `

    Where: ${properties.where}

    `; + } + if (properties.start) { + popupContent += `

    Start: ${properties.start}

    `; + } + if (properties.stop) { + popupContent += `

    End: ${properties.stop}

    `; + } + + // Add link to view full event + popupContent += `

    View Event

    `; + + popupContent += '
    '; + + // Create popup + const popup = new maplibregl.Popup({ + closeButton: true, + closeOnClick: true + }).setHTML(popupContent); + + // Add marker with popup + new maplibregl.Marker({ + color: '#FF5722' + }) + .setLngLat(coordinates) + .setPopup(popup) + .addTo(resultsMap); + }); + + // Fit map to events bounds + if (data.features.length > 0) { + const bounds = new maplibregl.LngLatBounds(); + + data.features.forEach(feature => { + bounds.extend(feature.geometry.coordinates); + }); + + resultsMap.fitBounds(bounds, { + padding: 50, + maxZoom: 12 + }); + } + } + }); + + // Populate table with results + const tableBody = document.getElementById('resultsTable').getElementsByTagName('tbody')[0]; + tableBody.innerHTML = ''; + + if (data.features) { + data.features.forEach(feature => { + const properties = feature.properties; + + const row = tableBody.insertRow(); + row.insertCell(0).textContent = properties.id || ''; + row.insertCell(1).textContent = properties.label || ''; + row.insertCell(2).textContent = properties.type || ''; + row.insertCell(3).textContent = properties.what || ''; + row.insertCell(4).textContent = properties.where || ''; + row.insertCell(5).textContent = properties.start || ''; + row.insertCell(6).textContent = properties.stop || ''; + }); + } + + // Store the data for download + window.searchResults = data; +} + +// Handle tab switching +document.querySelectorAll('.tab-button').forEach(button => { + button.addEventListener('click', () => { + // Remove active class from all buttons and content + document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); + + // Add active class to clicked button and corresponding content + button.classList.add('active'); + document.getElementById(button.dataset.tab).classList.add('active'); + }); +}); + +// Handle CSV download +document.getElementById('downloadCsv').addEventListener('click', () => { + if (!window.searchResults || !window.searchResults.features) { + alert('No search results to download'); + return; + } + + // Convert GeoJSON to CSV + let csv = 'id,label,type,what,where,start,stop,longitude,latitude\n'; + + window.searchResults.features.forEach(feature => { + const p = feature.properties; + const coords = feature.geometry.coordinates; + + csv += `"${p.id || ''}","${p.label || ''}","${p.type || ''}","${p.what || ''}","${p.where || ''}","${p.start || ''}","${p.stop || ''}",${coords[0]},${coords[1]}\n`; + }); + + // Create download link + const blob = new Blob([csv], { type: 'text/csv' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'search_results.csv'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}); + +// Handle JSON download +document.getElementById('downloadJson').addEventListener('click', () => { + if (!window.searchResults) { + alert('No search results to download'); + return; + } + + // Create download link + const blob = new Blob([JSON.stringify(window.searchResults, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'search_results.json'; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); +}); \ No newline at end of file diff --git a/oedb/resources/demo/static/traffic.js b/oedb/resources/demo/static/traffic.js index ed2787e..f0e875d 100644 --- a/oedb/resources/demo/static/traffic.js +++ b/oedb/resources/demo/static/traffic.js @@ -1,720 +1,378 @@ -// Logique JavaScript spécifique à /demo/traffic +// Traffic.js - JavaScript for the traffic reporting page -// Variables globales faibles (map, marker) +// Global variables let map; let marker; -let existingMarkers = []; -const PANORAMAX_TOKEN_STORAGE_KEY = 'oedb_panoramax_token'; -let mediaStream = null; +let geocoder; +let currentPosition; +let currentIssueType = null; +let photoFiles = []; +let panoramaxUploadUrl = ''; +let panoramaxToken = ''; -// Fonction pour créer un marqueur personnalisé avec emoji -function createCustomMarker(emoji, backgroundColor) { - const markerElement = document.createElement('div'); - markerElement.className = 'custom-marker'; - markerElement.style.width = '30px'; - markerElement.style.height = '30px'; - markerElement.style.borderRadius = '50%'; - markerElement.style.backgroundColor = backgroundColor; - markerElement.style.display = 'flex'; - markerElement.style.justifyContent = 'center'; - markerElement.style.alignItems = 'center'; - markerElement.style.fontSize = '16px'; - markerElement.style.boxShadow = '0 2px 4px rgba(0,0,0,0.3)'; - markerElement.innerHTML = emoji; - return markerElement; -} - -function setDefaultDates() { - const now = new Date(); - const nowISO = now.toISOString().slice(0, 16); - document.getElementById('start').value = nowISO; - const sixHoursLater = new Date(now.getTime() + 6 * 60 * 60 * 1000); - document.getElementById('stop').value = sixHoursLater.toISOString().slice(0, 16); -} - -function initTabs() { - const tabItems = document.querySelectorAll('.tab-item'); - tabItems.forEach(tab => { - tab.addEventListener('click', function() { - tabItems.forEach(item => item.classList.remove('active')); - this.classList.add('active'); - const tabName = this.getAttribute('data-tab'); - document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.remove('active')); - document.getElementById(tabName + '-tab').classList.add('active'); - localStorage.setItem('activeTab', tabName); - }); - }); - const activeTab = localStorage.getItem('activeTab'); - if (activeTab) { - const tabItem = document.querySelector(`.tab-item[data-tab="${activeTab}"]`); - if (tabItem) tabItem.click(); - } -} +// Initialize the map when the page loads +document.addEventListener('DOMContentLoaded', function() { + initMap(); + initTabs(); + initForm(); + + // Get Panoramax configuration + panoramaxUploadUrl = document.getElementById('panoramaxUploadUrl').value; + panoramaxToken = document.getElementById('panoramaxToken').value; + + // Set up photo upload + const photoInput = document.getElementById('photo'); + if (photoInput) { + photoInput.addEventListener('change', handlePhotoUpload); + } + + // Set up form submission + const reportForm = document.getElementById('reportForm'); + if (reportForm) { + reportForm.addEventListener('submit', submitReport); + } +}); +// Initialize the map function initMap() { - map = new maplibregl.Map({ - container: 'map', - style: 'https://tiles.openfreemap.org/styles/liberty', - center: [2.2137, 46.2276], - zoom: 5 - }); - map.addControl(new maplibregl.NavigationControl()); - marker = new maplibregl.Marker({ draggable: true, color: '#ff3860' }); - map.on('load', fetchExistingTrafficEvents); - map.on('click', function(e) { - marker.setLngLat(e.lngLat).addTo(map); - setTimeout(validateForm, 100); - }); + // Create the map + map = new maplibregl.Map({ + container: 'map', + style: 'https://tiles.openfreemap.org/styles/liberty', + center: [2.2137, 46.2276], // Default center (center of metropolitan France) + zoom: 5 + }); + + // Add navigation controls + map.addControl(new maplibregl.NavigationControl()); + + // Add attribution control with OpenStreetMap attribution + map.addControl(new maplibregl.AttributionControl({ + customAttribution: '© OpenStreetMap contributors' + })); + + // Try to get the user's current location + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + function(position) { + currentPosition = [position.coords.longitude, position.coords.latitude]; + + // Center the map on the user's location + map.flyTo({ + center: currentPosition, + zoom: 12 + }); + + // Add a marker at the user's location + marker = new maplibregl.Marker({ + draggable: true + }) + .setLngLat(currentPosition) + .addTo(map); + + // Update coordinates when marker is dragged + marker.on('dragend', updateCoordinates); + + // Update the coordinates display + updateCoordinates(); + }, + function(error) { + console.error('Error getting location:', error); + } + ); + } + + // Add click handler to the map + map.on('click', function(e) { + // If we don't have a marker yet, create one + if (!marker) { + marker = new maplibregl.Marker({ + draggable: true + }) + .setLngLat(e.lngLat) + .addTo(map); + + // Update coordinates when marker is dragged + marker.on('dragend', updateCoordinates); + } else { + // Otherwise, move the existing marker + marker.setLngLat(e.lngLat); + } + + // Update the coordinates display + updateCoordinates(); + }); } -function setGpsStatus(text, ok = null) { - const el = document.getElementById('gpsStatus'); - if (!el) return; - el.textContent = `GPS: ${text}`; - if (ok === true) { el.style.color = '#2e7d32'; } - else if (ok === false) { el.style.color = '#c62828'; } - else { el.style.color = '#555'; } +// Initialize the tabs +function initTabs() { + const tabItems = document.querySelectorAll('.tab-item'); + const tabPanes = document.querySelectorAll('.tab-pane'); + + tabItems.forEach(item => { + item.addEventListener('click', function() { + // Remove active class from all tabs + tabItems.forEach(tab => tab.classList.remove('active')); + tabPanes.forEach(pane => pane.classList.remove('active')); + + // Add active class to clicked tab + this.classList.add('active'); + + // Show the corresponding tab content + const tabId = this.getAttribute('data-tab'); + document.getElementById(tabId + '-tab').classList.add('active'); + }); + }); } -function fetchExistingTrafficEvents() { - existingMarkers.forEach(m => m.remove()); - existingMarkers = []; - fetch('https://api.openeventdatabase.org/event?what=traffic') - .then(r => { if (!r.ok) throw new Error('Failed to fetch existing traffic events'); return r.json(); }) - .then(data => { - if (!data || !Array.isArray(data.features)) return; - data.features.forEach(event => { - if (event.geometry && event.geometry.type === 'Point') { - const coords = event.geometry.coordinates; - const needsRealityCheck = checkIfNeedsRealityCheck(event); - let markerColor = needsRealityCheck ? '#ff9800' : '#888888'; - let markerOptions = { color: markerColor }; - - // Check if event title contains "vélo" or "travaux" - const eventTitle = event.properties.label || ''; - if (eventTitle.toLowerCase().includes('vélo')) { - markerOptions = { - element: createCustomMarker('🚲', markerColor) - }; - } else if (eventTitle.toLowerCase().includes('travaux')) { - markerOptions = { - element: createCustomMarker('🚧', markerColor) - }; - } - - const em = new maplibregl.Marker(markerOptions).setLngLat(coords).addTo(map); - let popupContent = `\n

    ${event.properties.label || 'Traffic Event'}

    \n

    Type: ${event.properties.what || 'Unknown'}

    \n

    Start: ${event.properties.start || 'Unknown'}

    \n

    End: ${event.properties.stop || 'Unknown'}

    `; - if (needsRealityCheck) { - popupContent += `\n
    \n

    Is this traffic event still present?

    \n
    \n\n\n
    \n
    `; - } else if (event.properties['reality_check']) { - popupContent += `\n
    \n

    Reality check: ${event.properties['reality_check']}

    \n
    `; - } - em.setPopup(new maplibregl.Popup({ offset: 25 }).setHTML(popupContent)); - existingMarkers.push(em); - } - }); - }); +// Initialize the form +function initForm() { + // Set the current date and time as the default + const now = new Date(); + const dateTimeString = now.toISOString().slice(0, 16); + + const startTimeInput = document.getElementById('start'); + if (startTimeInput) { + startTimeInput.value = dateTimeString; + } + + const stopTimeInput = document.getElementById('stop'); + if (stopTimeInput) { + // Set default end time to 1 hour from now + const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000); + stopTimeInput.value = oneHourLater.toISOString().slice(0, 16); + } } -function checkIfNeedsRealityCheck(event) { - if (event.properties['reality_check']) return false; - if (!event.properties.what || !event.properties.what.startsWith('traffic')) return false; - const createDate = event.properties.createdate; - if (!createDate) return false; - const createTime = new Date(createDate).getTime(); - const currentTime = new Date().getTime(); - return (currentTime - createTime) > (60 * 60 * 1000); +// Update the coordinates display when the marker is moved +function updateCoordinates() { + if (!marker) return; + + const lngLat = marker.getLngLat(); + currentPosition = [lngLat.lng, lngLat.lat]; + + // Update the coordinates display + const coordinatesElement = document.getElementById('coordinates'); + if (coordinatesElement) { + coordinatesElement.textContent = `${lngLat.lat.toFixed(6)}, ${lngLat.lng.toFixed(6)}`; + } + + // Update the hidden coordinates input + const coordinatesInput = document.getElementById('coordinates-input'); + if (coordinatesInput) { + coordinatesInput.value = JSON.stringify({ + type: 'Point', + coordinates: [lngLat.lng, lngLat.lat] + }); + } } +// Fill the form with predefined values based on the selected issue type function fillForm(issueType) { - const labelInput = document.getElementById('label'); - const issueTypeInput = document.getElementById('issueType'); - const severitySelect = document.getElementById('severity'); - let currentLngLat = marker.getLngLat ? marker.getLngLat() : null; - marker.remove(); - let markerColor = '#ff3860'; - switch(issueType) { - case 'bike_obstacle': - labelInput.value = 'Obstacle vélo'; - issueTypeInput.value = 'mobility.cycling.obstacle'; - severitySelect.value = 'medium'; - markerColor = '#388e3c'; - break; - case 'illegal_dumping': - labelInput.value = 'Décharge sauvage'; - issueTypeInput.value = 'environment.dumping.illegal'; - severitySelect.value = 'medium'; - markerColor = '#795548'; - break; - case 'pothole': - labelInput.value = 'Nid de poule'; - issueTypeInput.value = 'traffic.hazard.pothole'; - severitySelect.value = 'medium'; - markerColor = '#ff9800'; - break; - case 'obstacle': - labelInput.value = 'Obstacle'; - issueTypeInput.value = 'traffic.hazard.obstacle'; - severitySelect.value = 'high'; - markerColor = '#f44336'; - break; - case 'vehicle': - labelInput.value = 'Véhicule sur le bas côté de la route'; - issueTypeInput.value = 'traffic.hazard.vehicle'; - severitySelect.value = 'low'; - markerColor = '#2196f3'; - break; - case 'danger': - labelInput.value = 'Danger non classé'; - issueTypeInput.value = 'traffic.hazard.danger'; - severitySelect.value = 'high'; - markerColor = '#9c27b0'; - break; - case 'emergency_alert': - labelInput.value = "Alerte d'urgence (SAIP)"; - issueTypeInput.value = 'alert.emergency'; - severitySelect.value = 'high'; - markerColor = '#e91e63'; - break; - case 'daylight_saving': - labelInput.value = "Période d'heure d'été"; - issueTypeInput.value = 'time.daylight.summer'; - severitySelect.value = 'low'; - markerColor = '#ffc107'; - break; - case 'accident': - labelInput.value = 'Accident de la route'; - issueTypeInput.value = 'traffic.accident'; - severitySelect.value = 'high'; - markerColor = '#d32f2f'; - break; - case 'flooded_road': - labelInput.value = 'Route inondée'; - issueTypeInput.value = 'traffic.closed.flood'; - severitySelect.value = 'high'; - markerColor = '#1976d2'; - break; - case 'black_traffic': - labelInput.value = 'Période noire bison futé vers la province'; - issueTypeInput.value = 'traffic.forecast.black.out'; - severitySelect.value = 'high'; - markerColor = '#212121'; - break; - case 'roadwork': - labelInput.value = 'Travaux'; - issueTypeInput.value = 'traffic.roadwork'; - severitySelect.value = 'medium'; - markerColor = '#ff5722'; - break; - case 'flood_danger': - labelInput.value = 'Vigilance rouge inondation'; - issueTypeInput.value = 'weather.danger.flood'; - severitySelect.value = 'high'; - markerColor = '#b71c1c'; - break; - case 'thunderstorm_alert': - labelInput.value = 'Vigilance orange orages'; - issueTypeInput.value = 'weather.alert.thunderstorm'; - severitySelect.value = 'medium'; - markerColor = '#ff9800'; - break; - case 'fog_warning': - labelInput.value = 'Vigilance jaune brouillard'; - issueTypeInput.value = 'weather.warning.fog'; - severitySelect.value = 'low'; - markerColor = '#ffeb3b'; - break; - case 'unattended_luggage': - labelInput.value = 'Bagage abandonné'; - issueTypeInput.value = 'public_transport.incident.unattended_luggage'; - severitySelect.value = 'medium'; - markerColor = '#673ab7'; - break; - case 'transport_delay': - labelInput.value = 'Retard'; - issueTypeInput.value = 'public_transport.delay'; - severitySelect.value = 'low'; - markerColor = '#ffc107'; - break; - case 'major_transport_delay': - labelInput.value = 'Retard important'; - issueTypeInput.value = 'public_transport.delay.major'; - severitySelect.value = 'medium'; - markerColor = '#ff5722'; - break; - default: - labelInput.value = 'Bouchon'; - issueTypeInput.value = 'traffic.jam'; - severitySelect.value = 'medium'; - markerColor = '#ff3860'; - } - marker = new maplibregl.Marker({ draggable: true, color: markerColor }); - if (currentLngLat) marker.setLngLat(currentLngLat).addTo(map); - validateForm(); + 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' }); } -document.getElementById('geolocateBtn').addEventListener('click', function() { - document.getElementById('geolocateSpinner').style.display = 'inline-block'; - this.disabled = true; - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition(function(position) { - const lat = position.coords.latitude; - const lng = position.coords.longitude; - marker.setLngLat([lng, lat]).addTo(map); - map.flyTo({ center: [lng, lat], zoom: 14 }); - document.getElementById('geolocateSpinner').style.display = 'none'; - document.getElementById('geolocateBtn').disabled = false; - showResult('Current location detected successfully', 'success'); - setGpsStatus('actif', true); - validateForm(); - fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`) - .then(r => r.json()) - .then(data => { - if (data && data.address) { - let location = ''; - if (data.address.road) { - location = data.address.road; - if (data.address.city) location += `, ${data.address.city}`; - } else if (data.address.suburb) { - location = data.address.suburb; - if (data.address.city) location += `, ${data.address.city}`; - } - if (location) document.getElementById('where').value = location; - } - }); - }, function(error) { - document.getElementById('geolocateSpinner').style.display = 'none'; - document.getElementById('geolocateBtn').disabled = false; - let msg = 'Unable to get your location. '; - switch(error.code) { - case error.PERMISSION_DENIED: msg += 'You denied the request for geolocation.'; break; - case error.POSITION_UNAVAILABLE: msg += 'Location information is unavailable.'; break; - case error.TIMEOUT: msg += 'The request to get your location timed out.'; break; - default: msg += 'An unknown error occurred.'; - } - showResult(msg, 'error'); - setGpsStatus('inactif', false); - }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }); - } else { - document.getElementById('geolocateSpinner').style.display = 'none'; - document.getElementById('geolocateBtn').disabled = false; - showResult('Geolocation is not supported by your browser', 'error'); - setGpsStatus('non supporté', false); - } -}); - -function validateForm() { - const requiredFields = document.querySelectorAll('#trafficForm [required]'); - const submitButton = document.getElementById('report_issue_button'); - let isValid = true; - requiredFields.forEach(field => { if (!field.value.trim()) isValid = false; }); - if (!marker || !marker.getLngLat()) isValid = false; - submitButton.disabled = !isValid; - if (isValid) submitButton.classList.remove('disabled'); else submitButton.classList.add('disabled'); - return isValid; +// Handle photo upload +function handlePhotoUpload(event) { + const files = event.target.files; + if (!files || files.length === 0) return; + + // Store the files for later upload + photoFiles = Array.from(files); + + // Show preview of the photos + const previewContainer = document.getElementById('photoPreview'); + if (previewContainer) { + previewContainer.innerHTML = ''; + + photoFiles.forEach(file => { + const reader = new FileReader(); + reader.onload = function(e) { + const img = document.createElement('img'); + img.src = e.target.result; + img.className = 'photo-preview'; + previewContainer.appendChild(img); + }; + reader.readAsDataURL(file); + }); + + previewContainer.style.display = 'flex'; + } } -document.addEventListener('DOMContentLoaded', function() { - const formFields = document.querySelectorAll('#trafficForm input, #trafficForm select'); - formFields.forEach(f => { f.addEventListener('input', validateForm); f.addEventListener('change', validateForm); }); - validateForm(); - - // Charger token panoramax depuis localStorage - const stored = localStorage.getItem(PANORAMAX_TOKEN_STORAGE_KEY); - if (stored) { - const input = document.getElementById('panoramaxTokenInput'); - if (input) { - input.value = stored; - input.style.display = 'none'; - const label = document.querySelector("label[for='panoramaxTokenInput']"); - if (label) label.style.display = 'none'; - } - const saveBtn = document.getElementById('savePanoramaxTokenBtn'); - const showBtn = document.getElementById('showPanoramaxTokenBtn'); - if (saveBtn) saveBtn.style.display = 'none'; - if (showBtn) showBtn.style.display = ''; - } - const saveBtn = document.getElementById('savePanoramaxTokenBtn'); - if (saveBtn) { - saveBtn.addEventListener('click', function() { - const val = document.getElementById('panoramaxTokenInput')?.value || ''; - if (val) { - localStorage.setItem(PANORAMAX_TOKEN_STORAGE_KEY, val); - showResult('Token Panoramax enregistré localement', 'success'); - // Masquer champ + bouton save, afficher bouton show - const input = document.getElementById('panoramaxTokenInput'); - if (input) input.style.display = 'none'; - const label = document.querySelector("label[for='panoramaxTokenInput']"); - if (label) label.style.display = 'none'; - saveBtn.style.display = 'none'; - const showBtn = document.getElementById('showPanoramaxTokenBtn'); - if (showBtn) showBtn.style.display = ''; - } else { - localStorage.removeItem(PANORAMAX_TOKEN_STORAGE_KEY); - showResult('Token Panoramax supprimé du stockage local', 'success'); - } - }); - } - const showBtn = document.getElementById('showPanoramaxTokenBtn'); - if (showBtn) { - showBtn.addEventListener('click', function() { - const input = document.getElementById('panoramaxTokenInput'); - const label = document.querySelector("label[for='panoramaxTokenInput']"); - if (input) input.style.display = ''; - if (label) label.style.display = ''; - const saveBtn = document.getElementById('savePanoramaxTokenBtn'); - if (saveBtn) saveBtn.style.display = ''; - showBtn.style.display = 'none'; - }); - } - // État GPS initial - setGpsStatus('inconnu'); -}); - -// Aperçu photo -const photoInput = document.getElementById('photo'); -if (photoInput) { - photoInput.addEventListener('change', function() { - const file = this.files && this.files[0]; - const ctn = document.getElementById('photoPreviewContainer'); - if (!file) { ctn.style.display = 'none'; return; } - const url = URL.createObjectURL(file); - const img = document.getElementById('photoPreview'); - img.src = url; - ctn.style.display = 'block'; - }); +// Submit the report +async function submitReport(event) { + event.preventDefault(); + + // Show loading message + const resultElement = document.getElementById('result'); + resultElement.textContent = 'Submitting report...'; + resultElement.className = ''; + resultElement.style.display = 'block'; + + try { + // Check if we have coordinates + if (!currentPosition) { + throw new Error('Please select a location on the map'); + } + + // Get form values + const formData = new FormData(event.target); + const eventData = { + type: formData.get('type') || 'unscheduled', + what: formData.get('what'), + label: formData.get('label'), + description: formData.get('description'), + start: new Date(formData.get('start')).toISOString(), + stop: new Date(formData.get('stop')).toISOString(), + geometry: { + type: 'Point', + coordinates: currentPosition + } + }; + + // Add username if authenticated + const osmUsername = document.getElementById('osmUsername'); + if (osmUsername && osmUsername.value) { + eventData.source = { + name: 'OpenStreetMap user', + id: osmUsername.value + }; + } + + // Upload photos to Panoramax if available + let photoUrls = []; + if (photoFiles.length > 0 && panoramaxUploadUrl && panoramaxToken) { + photoUrls = await uploadPhotos(photoFiles); + if (photoUrls.length > 0) { + eventData.media = photoUrls.map(url => ({ + type: 'image', + url: url + })); + } + } + + // Submit the event to the API + const response = await fetch('/event', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(eventData) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(errorText || response.statusText); + } + + const data = await response.json(); + + // Show success message + resultElement.textContent = `Report submitted successfully! Event ID: ${data.id}`; + resultElement.className = 'success'; + + // Reset the form + event.target.reset(); + photoFiles = []; + const previewContainer = document.getElementById('photoPreview'); + if (previewContainer) { + previewContainer.innerHTML = ''; + previewContainer.style.display = 'none'; + } + + // Initialize the form again + initForm(); + } catch (error) { + // Show error message + resultElement.textContent = `Error: ${error.message}`; + resultElement.className = 'error'; + } } -async function readExifGps(file) { - // Lecture minimale EXIF pour récupérer GPSLatitude/GPSLongitude si présents - try { - const buffer = await file.arrayBuffer(); - const view = new DataView(buffer); - // Vérifier JPEG - if (view.getUint16(0, false) !== 0xFFD8) return null; - let offset = 2; - const length = view.byteLength; - while (offset < length) { - if (view.getUint16(offset, false) === 0xFFE1) { // APP1 - const app1Len = view.getUint16(offset + 2, false); - // "Exif\0\0" - if (view.getUint32(offset + 4, false) === 0x45786966 && view.getUint16(offset + 8, false) === 0x0000) { - let tiffOffset = offset + 10; - const little = view.getUint16(tiffOffset, false) === 0x4949; // 'II' - const getU16 = (pos) => view.getUint16(pos, little); - const getU32 = (pos) => view.getUint32(pos, little); - const firstIFDOffset = getU32(tiffOffset + 4) + tiffOffset; - // Parcourir 0th IFD pour trouver GPS IFD pointer (tag 0x8825) - const entries = getU16(firstIFDOffset); - let gpsIFDPointer = 0; - for (let i = 0; i < entries; i++) { - const entryOffset = firstIFDOffset + 2 + i * 12; - const tag = getU16(entryOffset); - if (tag === 0x8825) { // GPSInfoIFDPointer - gpsIFDPointer = getU32(entryOffset + 8) + tiffOffset; - break; - } - } - if (!gpsIFDPointer) return null; - const gpsCount = getU16(gpsIFDPointer); - let latRef = 'N', lonRef = 'E'; - let latVals = null, lonVals = null; - const readRational = (pos) => { - const num = getU32(pos); - const den = getU32(pos + 4); - return den ? (num / den) : 0; - }; - for (let i = 0; i < gpsCount; i++) { - const eOff = gpsIFDPointer + 2 + i * 12; - const tag = getU16(eOff); - const type = getU16(eOff + 2); - const count = getU32(eOff + 4); - let valueOffset = eOff + 8; - let valuePtr = getU32(valueOffset) + tiffOffset; - if (tag === 0x0001) { // GPSLatitudeRef - const c = view.getUint8(valueOffset); - latRef = String.fromCharCode(c); - } else if (tag === 0x0002 && type === 5 && count === 3) { // GPSLatitude - latVals = [readRational(valuePtr), readRational(valuePtr + 8), readRational(valuePtr + 16)]; - } else if (tag === 0x0003) { // GPSLongitudeRef - const c = view.getUint8(valueOffset); - lonRef = String.fromCharCode(c); - } else if (tag === 0x0004 && type === 5 && count === 3) { // GPSLongitude - lonVals = [readRational(valuePtr), readRational(valuePtr + 8), readRational(valuePtr + 16)]; - } - } - if (!latVals || !lonVals) return null; - const toDecimal = (dms) => dms[0] + dms[1] / 60 + dms[2] / 3600; - let lat = toDecimal(latVals); - let lng = toDecimal(lonVals); - if (latRef === 'S') lat = -lat; - if (lonRef === 'W') lng = -lng; - return { lat, lng }; - } - offset += 2 + app1Len; - } else if ((view.getUint16(offset, false) & 0xFFF0) === 0xFFE0) { - const segLen = view.getUint16(offset + 2, false); - offset += 2 + segLen; - } else { - break; - } - } - return null; - } catch (e) { - return null; - } -} - -async function uploadPhotoIfConfigured(file, lng, lat, isoDatetime) { - try { - const uploadUrl = document.getElementById('panoramaxUploadUrl')?.value || ''; - // Priorité au token utilisateur (input/localStorage), sinon fallback hidden server - const token = (document.getElementById('panoramaxTokenInput')?.value || localStorage.getItem(PANORAMAX_TOKEN_STORAGE_KEY) || document.getElementById('panoramaxToken')?.value || ''); - if (!uploadUrl || !file) return null; - // Exiger EXIF GPS - const exifLoc = await readExifGps(file); - if (!exifLoc) { - showResult("La photo n'a pas de géolocalisation EXIF, envoi Panoramax interdit.", 'error'); - return null; - } - const form = new FormData(); - form.append('file', file, file.name || 'photo.jpg'); - // Utiliser la géolocalisation EXIF uniquement - form.append('lon', String(exifLoc.lng)); - form.append('lat', String(exifLoc.lat)); - if (isoDatetime) form.append('datetime', isoDatetime); - const headers = {}; - if (token) headers['Authorization'] = `Bearer ${token}`; - const res = await fetch(uploadUrl, { method: 'POST', headers, body: form }); - if (!res.ok) { throw new Error(await res.text() || `Upload failed (${res.status})`); } - const data = await res.json().catch(() => ({})); - return { id: data.id || data.uuid || data.photo_id || null, url: data.url || data.permalink || data.link || null, raw: data }; - } catch (err) { - console.error('Panoramax upload error:', err); - showResult(`Erreur upload photo: ${err.message}`, 'error'); - return null; - } -} - -document.getElementById('trafficForm').addEventListener('submit', async function(e) { - e.preventDefault(); - if (!validateForm()) { showResult('Please fill in all required fields and set a location on the map', 'error'); return; } - const label = document.getElementById('label').value; - const issueType = document.getElementById('issueType').value; - const severity = document.getElementById('severity').value; - const cause = document.getElementById('cause').value; - const start = document.getElementById('start').value; - const stop = document.getElementById('stop').value; - const where = document.getElementById('where').value; - const lngLat = marker.getLngLat(); - const event = { type: 'Feature', geometry: { type: 'Point', coordinates: [lngLat.lng, lngLat.lat] }, properties: { label, type: 'unscheduled', what: issueType, 'issue:severity': severity, start, stop } }; - if (cause) event.properties['issue:details'] = cause; - if (where) event.properties.where = where; - let osmUsernameValue = ''; - const osmUsername = document.getElementById('osmUsername'); - if (osmUsername && osmUsername.value) osmUsernameValue = osmUsername.value; - if (window.osmAuth && osmAuth.isUserAuthenticated()) osmUsernameValue = osmAuth.getUsername(); - if (osmUsernameValue) event.properties['reporter:osm'] = osmUsernameValue; - let photoInfo = null; - const photoFile = (photoInput && photoInput.files && photoInput.files[0]) ? photoInput.files[0] : null; - if (photoFile) { - photoInfo = await uploadPhotoIfConfigured(photoFile, lngLat.lng, lngLat.lat, start); - if (photoInfo) { - event.properties['photo:service'] = 'panoramax'; - if (photoInfo.id) { - event.properties['photo:id'] = String(photoInfo.id); - // Tag panoramax (uuid) - event.properties['panoramax'] = String(photoInfo.id); - } - if (photoInfo.url) event.properties['photo:url'] = photoInfo.url; - } - } - saveEventToLocalStorage(event); - fetch('https://api.openeventdatabase.org/event', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }) - .then(response => { if (response.ok) return response.json(); return response.text().then(text => { throw new Error(text || response.statusText); }); }) - .then(data => { - if (data.id) updateEventInLocalStorage(event, data.id); - showResult(`Issue reported successfully with ID: ${data.id}`, 'success'); - const resultElement = document.getElementById('result'); - resultElement.innerHTML += `\n

    \nView Report on Server |\nView Saved Reports |\nBack to Map\n

    `; - document.getElementById('trafficForm').reset(); - setDefaultDates(); - marker.remove(); - fetchExistingTrafficEvents(); - }) - .catch(error => { showResult(`Error reporting issue: ${error.message}`, 'error'); }); -}); - -function saveEventToLocalStorage(event) { - let savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]'); - event.timestamp = new Date().toISOString(); - savedEvents.push(event); - localStorage.setItem('oedb_events', JSON.stringify(savedEvents)); -} - -function updateEventInLocalStorage(event, serverId) { - let savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]'); - const eventIndex = savedEvents.findIndex(e => e.timestamp === event.timestamp && e.geometry.coordinates[0] === event.geometry.coordinates[0] && e.geometry.coordinates[1] === event.geometry.coordinates[1]); - if (eventIndex !== -1) { - savedEvents[eventIndex].properties.id = serverId; - localStorage.setItem('oedb_events', JSON.stringify(savedEvents)); - } -} - -function showResult(message, type) { - const resultElement = document.getElementById('result'); - resultElement.textContent = message; - resultElement.className = type; - resultElement.style.display = 'block'; - resultElement.scrollIntoView({ behavior: 'smooth' }); -} - -function confirmEvent(eventId, isConfirmed) { - let username = localStorage.getItem('oedb_username'); - if (!username) { - username = promptForUsername(); - if (!username) return; - } - const now = new Date(); - const dateTimeString = now.toISOString(); - const realityCheckStatus = isConfirmed ? 'confirmed' : 'not confirmed'; - const realityCheckValue = `${dateTimeString} | ${username} | ${realityCheckStatus}`; - fetch(`https://api.openeventdatabase.org/event/${eventId}`) - .then(r => { if (!r.ok) throw new Error(`Failed to fetch event ${eventId}`); return r.json(); }) - .then(event => { - event.properties['reality_check'] = realityCheckValue; - return fetch(`https://api.openeventdatabase.org/event/${eventId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(event) }); - }) - .then(r => { if (!r.ok) throw new Error('Failed to update event'); saveContribution(eventId, isConfirmed); awardPoints(3); showResult(`Thank you for your contribution! You've earned 3 points.`, 'success'); updateUserInfoDisplay(); fetchExistingTrafficEvents(); }) - .catch(err => { console.error('Error updating event:', err); showResult(`Error: ${err.message}`, 'error'); }); -} - -function promptForUsername() { - const username = prompt('Please enter your username:'); - if (username) { localStorage.setItem('oedb_username', username); return username; } - return null; -} - -function saveContribution(eventId, isConfirmed) { - let contributions = JSON.parse(localStorage.getItem('oedb_contributions') || '[]'); - contributions.push({ eventId, timestamp: new Date().toISOString(), isConfirmed }); - localStorage.setItem('oedb_contributions', JSON.stringify(contributions)); -} - -function awardPoints(points) { - let currentPoints = parseInt(localStorage.getItem('oedb_points') || '0'); - currentPoints += points; - localStorage.setItem('oedb_points', currentPoints.toString()); -} - -function updateUserInfoDisplay() { - const username = localStorage.getItem('oedb_username') || 'Anonymous'; - const points = localStorage.getItem('oedb_points') || '0'; - let userInfoPanel = document.getElementById('user-info-panel'); - if (!userInfoPanel) { - userInfoPanel = document.createElement('div'); - userInfoPanel.id = 'user-info-panel'; - userInfoPanel.className = 'user-info-panel'; - const navLinks = document.querySelector('.nav-links'); - navLinks.parentNode.insertBefore(userInfoPanel, navLinks.nextSibling); - } - userInfoPanel.innerHTML = `\n

    User Information

    \n

    Username: ${username}

    \n

    Points: ${points}

    `; -} - -// Initialize collapsible panels -function initCollapsiblePanels() { - const headers = document.querySelectorAll('.collapsible-header'); - headers.forEach(header => { - header.addEventListener('click', function() { - this.classList.toggle('active'); - const content = this.nextElementSibling; - content.classList.toggle('active'); - }); - }); -} - -document.addEventListener('DOMContentLoaded', function() { - setDefaultDates(); - initTabs(); - initMap(); - updateUserInfoDisplay(); - initCollapsiblePanels(); -}); - -// Contrôles Caméra -const startCameraBtn = document.getElementById('startCameraBtn'); -const capturePhotoBtn = document.getElementById('capturePhotoBtn'); -const stopCameraBtn = document.getElementById('stopCameraBtn'); -const cameraVideo = document.getElementById('cameraVideo'); -const cameraCanvas = document.getElementById('cameraCanvas'); - -async function startCamera() { - try { - mediaStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' }, audio: false }); - cameraVideo.srcObject = mediaStream; - capturePhotoBtn.disabled = false; - stopCameraBtn.disabled = false; - startCameraBtn.disabled = true; - } catch (e) { - showResult(`Impossible d'accéder à la caméra: ${e.message}`, 'error'); - } -} - -function stopCamera() { - if (mediaStream) { - mediaStream.getTracks().forEach(t => t.stop()); - mediaStream = null; - } - cameraVideo.srcObject = null; - capturePhotoBtn.disabled = true; - stopCameraBtn.disabled = true; - startCameraBtn.disabled = false; -} - -function dataURLToFile(dataUrl, filename) { - const arr = dataUrl.split(','); - const mime = arr[0].match(/:(.*?);/)[1]; - const bstr = atob(arr[1]); - let n = bstr.length; - const u8arr = new Uint8Array(n); - while (n--) { u8arr[n] = bstr.charCodeAt(n); } - return new File([u8arr], filename, { type: mime }); -} - -function capturePhoto() { - try { - const width = cameraVideo.videoWidth; - const height = cameraVideo.videoHeight; - if (!width || !height) { showResult('Vidéo non prête', 'error'); return; } - cameraCanvas.width = width; - cameraCanvas.height = height; - const ctx = cameraCanvas.getContext('2d'); - ctx.drawImage(cameraVideo, 0, 0, width, height); - const dataUrl = cameraCanvas.toDataURL('image/jpeg', 0.92); - const file = dataURLToFile(dataUrl, 'camera_capture.jpg'); - // Remplit le file input pour réutiliser le flux existant - const dt = new DataTransfer(); - dt.items.add(file); - const input = document.getElementById('photo'); - input.files = dt.files; - // Déclenche l’aperçu - const url = URL.createObjectURL(file); - const img = document.getElementById('photoPreview'); - img.src = url; - document.getElementById('photoPreviewContainer').style.display = 'block'; - showResult('Photo capturée depuis la caméra', 'success'); - } catch (e) { - showResult(`Échec capture photo: ${e.message}`, 'error'); - } -} - -if (startCameraBtn && capturePhotoBtn && stopCameraBtn) { - startCameraBtn.addEventListener('click', startCamera); - capturePhotoBtn.addEventListener('click', capturePhoto); - stopCameraBtn.addEventListener('click', stopCamera); -} - -// Expose functions used in inline HTML popups -window.fillForm = fillForm; -window.confirmEvent = confirmEvent; - +// Upload photos to Panoramax +async function uploadPhotos(files) { + const urls = []; + + for (const file of files) { + try { + const uploadData = new FormData(); + uploadData.append('file', file); + + const response = await fetch(panoramaxUploadUrl, { + method: 'POST', + headers: { + 'Authorization': `Token ${panoramaxToken}` + }, + body: uploadData + }); + + if (!response.ok) { + console.error('Failed to upload photo:', await response.text()); + continue; + } + + const data = await response.json(); + if (data.url) { + urls.push(data.url); + } + } catch (error) { + console.error('Error uploading photo:', error); + } + } + + return urls; +} \ No newline at end of file diff --git a/oedb/resources/demo/static/traffic_tabs.js b/oedb/resources/demo/static/traffic_tabs.js new file mode 100644 index 0000000..63c3a09 --- /dev/null +++ b/oedb/resources/demo/static/traffic_tabs.js @@ -0,0 +1,63 @@ +// traffic_tabs.js - Handle query parameters for tab selection in traffic.html + +document.addEventListener('DOMContentLoaded', function() { + // Get all tab items + const tabItems = document.querySelectorAll('.tab-item'); + + // Function to activate a tab + function activateTab(tabName) { + // Find the tab item with the given tab name + const tabItem = document.querySelector(`.tab-item[data-tab="${tabName}"]`); + if (!tabItem) return; + + // Remove active class from all tab items + tabItems.forEach(item => item.classList.remove('active')); + + // Add active class to the selected tab item + tabItem.classList.add('active'); + + // Get all tab panes + const tabPanes = document.querySelectorAll('.tab-pane'); + + // Remove active class from all tab panes + tabPanes.forEach(pane => pane.classList.remove('active')); + + // Add active class to the corresponding tab pane + document.getElementById(tabName + '-tab').classList.add('active'); + + // Save active tab to localStorage + localStorage.setItem('activeTab', tabName); + + // Update URL with query parameter + const url = new URL(window.location.href); + url.searchParams.set('tab', tabName); + history.replaceState(null, '', url); + } + + // Add click event listener to each tab item + tabItems.forEach(tab => { + tab.addEventListener('click', function() { + // Get the tab name from data-tab attribute + const tabName = this.getAttribute('data-tab'); + + // Activate the tab + activateTab(tabName); + }); + }); + + // Check for tab query parameter + const urlParams = new URLSearchParams(window.location.search); + const tabParam = urlParams.get('tab'); + + if (tabParam) { + // Activate the tab from query parameter + activateTab(tabParam); + } else { + // Restore active tab from localStorage if no query parameter + const activeTab = localStorage.getItem('activeTab'); + if (activeTab) { + // Activate the tab from localStorage + activateTab(activeTab); + } + } +}); \ No newline at end of file diff --git a/oedb/resources/demo/static/view_events.js b/oedb/resources/demo/static/view_events.js new file mode 100644 index 0000000..6b034dc --- /dev/null +++ b/oedb/resources/demo/static/view_events.js @@ -0,0 +1,174 @@ +// view_events.js - JavaScript for the view saved events page + +// Global variables +let map; +let markers = []; + +// Initialize the map when the page loads +document.addEventListener('DOMContentLoaded', function() { + initMap(); + + // Set up refresh button + const refreshBtn = document.getElementById('refresh-btn'); + if (refreshBtn) { + refreshBtn.addEventListener('click', loadEvents); + } + + // Set up clear button + const clearBtn = document.getElementById('clear-btn'); + if (clearBtn) { + clearBtn.addEventListener('click', clearEvents); + } +}); + +// Initialize the map +function initMap() { + // Create the map + map = new maplibregl.Map({ + container: 'map', + style: 'https://tiles.openfreemap.org/styles/liberty', + center: [2.2137, 46.2276], // Default center (center of metropolitan France) + zoom: 5 + }); + + // Add navigation controls + map.addControl(new maplibregl.NavigationControl()); + + // Add attribution control with OpenStreetMap attribution + map.addControl(new maplibregl.AttributionControl({ + customAttribution: '© OpenStreetMap contributors' + })); + + // Load events when the map is loaded + map.on('load', function() { + loadEvents(); + }); +} + +// Function to load events from localStorage +function loadEvents() { + // Clear existing markers + markers.forEach(marker => marker.remove()); + markers = []; + + // Get events from localStorage + const savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]'); + + // Update event count + document.getElementById('event-count').textContent = + `${savedEvents.length} event${savedEvents.length !== 1 ? 's' : ''} found`; + + // Clear event list + const eventList = document.getElementById('event-list'); + eventList.innerHTML = ''; + + // If no events, show message + if (savedEvents.length === 0) { + eventList.innerHTML = '
    No saved events found. Use the demo map to save events.
    '; + return; + } + + // Create bounds object to fit map to all markers + const bounds = new maplibregl.LngLatBounds(); + + // Add markers and list items for each event + savedEvents.forEach((event, index) => { + // Skip events without geometry + if (!event.geometry || !event.geometry.coordinates) return; + + // Get coordinates + const coordinates = event.geometry.coordinates; + + // Add marker to map + const marker = new maplibregl.Marker() + .setLngLat(coordinates) + .addTo(map); + + // Add popup with event info + const popup = new maplibregl.Popup({ offset: 25 }) + .setHTML(` +

    ${event.label || 'Unnamed Event'}

    +

    Type: ${event.what || 'Unknown'}

    +

    Start: ${formatDate(event.start)}

    +

    End: ${formatDate(event.stop)}

    + ${event.description ? `

    Description: ${event.description}

    ` : ''} + + `); + + marker.setPopup(popup); + markers.push(marker); + + // Extend bounds to include this marker + bounds.extend(coordinates); + + // Add to event list + const eventItem = document.createElement('div'); + eventItem.className = 'event-item'; + eventItem.innerHTML = ` +
    +

    ${event.label || 'Unnamed Event'}

    +
    + + +
    +
    +
    +

    Type: ${event.what || 'Unknown'}

    +

    When: ${formatDate(event.start)} to ${formatDate(event.stop)}

    + ${event.description ? `

    Description: ${event.description}

    ` : ''} +
    + `; + eventList.appendChild(eventItem); + }); + + // Fit map to bounds if we have any markers + if (!bounds.isEmpty()) { + map.fitBounds(bounds, { padding: 50 }); + } +} + +// Format date for display +function formatDate(dateString) { + if (!dateString) return 'Unknown'; + + const date = new Date(dateString); + return date.toLocaleString(); +} + +// Zoom to a specific event +function zoomToEvent(lng, lat) { + map.flyTo({ + center: [lng, lat], + zoom: 14 + }); +} + +// Delete an event +function deleteEvent(index) { + // Get events from localStorage + const savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]'); + + // Remove the event at the specified index + savedEvents.splice(index, 1); + + // Save the updated events back to localStorage + localStorage.setItem('oedb_events', JSON.stringify(savedEvents)); + + // Reload the events + loadEvents(); +} + +// Clear all events +function clearEvents() { + if (confirm('Are you sure you want to delete all saved events?')) { + // Clear events from localStorage + localStorage.removeItem('oedb_events'); + + // Reload the events + loadEvents(); + } +} \ No newline at end of file diff --git a/oedb/resources/demo/templates/by_what.html b/oedb/resources/demo/templates/by_what.html new file mode 100644 index 0000000..420acfc --- /dev/null +++ b/oedb/resources/demo/templates/by_what.html @@ -0,0 +1,53 @@ +{% extends "layout.html" %} + +{% block title %}Events by Type - OpenEventDatabase{% endblock %} + +{% block css %} + +{% endblock %} + +{% block header %}Events by Type{% endblock %} + +{% block content %} +

    This page lists all events from the OpenEventDatabase organized by their type.

    + +{% if events_by_what %} + +

    Quick Navigation

    + + + + {% for what_type in sorted_what_types %} +

    + {{ what_type }} + ({{ events_by_what[what_type]|length }} events) +

    + + {% endfor %} +{% else %} +

    No events found in the database.

    +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/oedb/resources/demo/templates/edit.html b/oedb/resources/demo/templates/edit.html new file mode 100644 index 0000000..70ebc01 --- /dev/null +++ b/oedb/resources/demo/templates/edit.html @@ -0,0 +1,90 @@ +{% extends "layout.html" %} + +{% block title %}Edit Event - OpenEventDatabase{% endblock %} + +{% block css %} + +{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block header %}Edit Event{% endblock %} + +{% block content %} +
    + + +
    + + +
    + +
    +
    + + +
    + +
    + + +
    Category of the event (e.g., sport.match.football, culture.festival)
    +
    +
    + +
    +
    + + +
    Series or group the event belongs to (e.g., Euro 2024, Summer Festival 2023)
    +
    + +
    + + +
    Specific location name (e.g., Eiffel Tower, Wembley Stadium)
    +
    +
    + +
    +
    + + +
    + +
    + + +
    +
    + +
    + +
    +
    Click on the map to set the event location
    +
    + +
    + + +
    +
    + +
    +{% endblock %} + +{% block scripts %} + + +{% endblock %} \ No newline at end of file diff --git a/oedb/resources/demo/templates/map_by_what.html b/oedb/resources/demo/templates/map_by_what.html new file mode 100644 index 0000000..c52627a --- /dev/null +++ b/oedb/resources/demo/templates/map_by_what.html @@ -0,0 +1,36 @@ +{% extends "layout.html" %} + +{% block title %}Map by Event Type - OpenEventDatabase{% endblock %} + +{% block css %} + +{% endblock %} + +{% block header %}Map by Event Type{% endblock %} + +{% block content %} +
    + +
    +

    This map shows events from the OpenEventDatabase filtered by their type.

    +

    Use the filter panel on the right to show/hide different event types.

    +
    +

    Loading events...

    +
    +
    + +
    +

    Filter by Event Type

    +
    + + +
    + +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/oedb/resources/demo/templates/search.html b/oedb/resources/demo/templates/search.html new file mode 100644 index 0000000..63a4917 --- /dev/null +++ b/oedb/resources/demo/templates/search.html @@ -0,0 +1,166 @@ +{% extends "layout.html" %} + +{% block title %}Search Events - OpenEventDatabase{% endblock %} + +{% block css %} + +{% endblock %} + +{% block head %} + + + +{% endblock %} + +{% block header %}Search Events{% endblock %} + +{% block content %} +
    +
    +
    + + +
    Category of the event (e.g., sport.match.football, culture.festival)
    +
    + +
    + + +
    +
    + +
    +
    + + +
    + + + + +
    + +
    +
    + + +
    Search for events near a specific location (e.g., 2.3522,48.8566,10000 for events within 10km of Paris)
    +
    + +
    + + +
    Search for events within a geographic bounding box
    +
    +
    + +
    +
    + + +
    Search for events associated with a specific OpenStreetMap ID
    +
    + +
    + + +
    Search for events associated with a specific Wikidata ID
    +
    +
    + +
    +
    + + +
    Maximum number of results to return (default: 200)
    +
    + +
    + + +
    Controls the level of detail in the geometry portion of the response
    +
    +
    + +
    + +
    +
    Draw a polygon on the map to define the search area, or use the form fields above
    +
    + + +
    + +
    + + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/oedb/resources/demo/templates/traffic_new.html b/oedb/resources/demo/templates/traffic_new.html new file mode 100644 index 0000000..23f1f23 --- /dev/null +++ b/oedb/resources/demo/templates/traffic_new.html @@ -0,0 +1,275 @@ +{% extends "layout.html" %} + +{% block title %}Report Traffic Jam - OpenEventDatabase{% endblock %} + +{% block css %} + +{% endblock %} + +{% block header %}Report Road Issue{% endblock %} + +{% block content %} + + + + + + + + + +
    + {% if is_authenticated %} +
    +
    +

    Logged in as {{ osm_username }}

    +

    View OSM Profile

    + + +
    +
    + {% else %} +

    Authenticate with your OpenStreetMap account to include your username in the traffic report.

    + + {% endif %} + + +
    + +

    Select Issue Type

    + + +
    +
    + Route +
    +
    + Rail +
    +
    + Météo +
    +
    + Urgences +
    +
    + Cycles +
    +
    + + +
    + +
    +
    +
    + + Pothole +
    +
    + + Obstacle +
    +
    + + Véhicule sur le bas côté de la route +
    +
    + + Danger +
    +
    + + Accident +
    +
    + + Route inondée +
    +
    + + Travaux +
    +
    + + Embouteillage +
    +
    +
    + + +
    +
    +
    + + Retard +
    +
    + + Annulation +
    +
    + + Travaux +
    +
    + + Incident +
    +
    +
    + + +
    +
    +
    + + Orage +
    +
    + + Inondation +
    +
    + + Neige +
    +
    + + Brouillard +
    +
    + + Canicule +
    +
    +
    + + +
    +
    +
    + + Incendie +
    +
    + + Urgence médicale +
    +
    + + Intervention police +
    +
    + + Évacuation +
    +
    +
    + + +
    +
    +
    + + Problème piste cyclable +
    +
    + + Problème trottoir +
    +
    + + Éclairage défectueux +
    +
    + + Déchets +
    +
    +
    +
    + +

    Report Details

    + +
    +
    + + +
    Catégorie de l'événement (e.g., road.hazard.pothole, road.traffic.jam)
    +
    + +
    + + +
    + +
    + + +
    + +
    +
    + + +
    +
    + +
    +
    + + +
    + +
    + + +
    +
    + +
    + + +
    Vous pouvez ajouter plusieurs photos (optionnel)
    +
    +
    + +
    + +
    +
    Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"
    +
    + + +
    + +
    + + + Voir tous les événements enregistrés sur la carte + +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/oedb/resources/demo/templates/view_events_new.html b/oedb/resources/demo/templates/view_events_new.html new file mode 100644 index 0000000..95f7793 --- /dev/null +++ b/oedb/resources/demo/templates/view_events_new.html @@ -0,0 +1,157 @@ +{% extends "layout.html" %} + +{% block title %}View Saved Events - OpenEventDatabase{% endblock %} + +{% block css %} + +{% endblock %} + +{% block header %}Your Saved Events{% endblock %} + +{% block content %} +
    + + + + + + +
    + +
    +

    OpenStreetMap Authentication

    +

    Authenticate with your OpenStreetMap account to include your username in reports.

    + + +
    + +
    + +
    + +
    + + +
    +
    +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/oedb/resources/live.py b/oedb/resources/live.py index 9a5386e..a4e21fc 100644 --- a/oedb/resources/live.py +++ b/oedb/resources/live.py @@ -149,7 +149,7 @@ class LiveResource: