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
-
-
-
-
-
-
-
-
-
- """
+ # 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 "
- for what_type in sorted_what_types:
- event_count = len(events_by_what[what_type])
- html += f'{what_type} ({event_count} events) '
- html += " "
-
- # 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 += ""
-
- # Sort events by label
- sorted_events = sorted(events, key=lambda x: x.get('label', ''))
-
- for event in sorted_events:
- event_id = event.get('id')
- event_label = event.get('label', 'Unnamed Event')
- coordinates = event.get('coordinates', [0, 0])
-
- html += f'{event_label} '
- html += f'[map ] '
-
- 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
-
-
-
-
-
-
-
Search Results
-
-
-
-
-
-
-
-
-
-
- ID
- Label
- Type
- What
- Where
- Start
- Stop
-
-
-
-
-
-
-
-
-
-
- Download CSV
- Download JSON
-
-
-
-
-
-
-
- """
+ # 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.
-
-
-
-
-
Filter by Event Type
-
- Select All
- Deselect All
-
-
- Loading event types...
-
-
-
-
-
-
- """
+ # 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 = '';
+
+ // 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 = '';
+
+ // 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'} \nType: ${event.properties.what || 'Unknown'}
\nStart: ${event.properties.start || 'Unknown'}
\nEnd: ${event.properties.stop || 'Unknown'}
`;
- if (needsRealityCheck) {
- popupContent += `\n\n
Is this traffic event still present?
\n
\nYes, still there \nNo, it's gone \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 = `\nUser Information \nUsername: ${username}
\nPoints: ${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}
` : ''}
+ Delete
+ `);
+
+ 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 = `
+
+
+
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 %}
+
+
+
+ {% 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 %}
+
+
+
+{% 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.
+
+
+
+
+
Filter by Event Type
+
+ Select All
+ Deselect All
+
+
+ Loading event types...
+
+
+{% 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 %}
+
+
+
+
+
+
Search Results
+
+
+
+
+
+
+
+
+
+
+ ID
+ Label
+ Type
+ What
+ Where
+ Start
+ Stop
+
+
+
+
+
+
+
+
+
+
+ Download CSV
+ Download JSON
+
+
+{% 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 %}
+
+ {% else %}
+
Authenticate with your OpenStreetMap account to include your username in the traffic report.
+
+
+ Login with OpenStreetMap
+
+ {% endif %}
+
+
+
+
+Select Issue Type
+
+
+
+
+ Route
+
+
+ Rail
+
+
+ Météo
+
+
+ Urgences
+
+
+ Cycles
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Report Details
+
+
+
+
+
+
+ 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.
+
+
+ Login with OpenStreetMap
+
+
+
+
+
+
+
+
+
+
+ Refresh
+
+
+ Clear All
+
+
+
+{% 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: