split templates

This commit is contained in:
Tykayn 2025-09-26 15:08:33 +02:00 committed by tykayn
parent 6548460478
commit 9aa8da5872
22 changed files with 2980 additions and 2384 deletions

103
CHANGES.md Normal file
View file

@ -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
}
]
}
```

View file

@ -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

193
oedb/resources/db_dump.py Normal file
View file

@ -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()

File diff suppressed because it is too large Load diff

View file

@ -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,

View file

@ -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,

View file

@ -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;
}

View file

@ -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: '© <a href="https://www.openstreetmap.org/copyright" >OpenStreetMap</a> 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 += `<p><a href="/event/${data.id}" >View Event</a> | <a href="/demo">Back to Map</a></p>`;
})
.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 += `<p><a href="/demo">Back to Map</a></p>`;
// 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');
});
}
});

View file

@ -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;
}

View file

@ -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: '© <a href="https://www.openstreetmap.org/copyright" >OpenStreetMap</a> 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 = '<p>Loading events...</p>';
// 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 =
`<p>Found ${data.features.length} events across ${eventTypes.size} different types.</p>`;
} else {
document.getElementById('event-info').innerHTML = '<p>No events found.</p>';
document.getElementById('filter-list').innerHTML = '<li>No event types available.</li>';
}
})
.catch(error => {
console.error('Error fetching events:', error);
document.getElementById('event-info').innerHTML =
`<p>Error loading events: ${error.message}</p>`;
});
}
// 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 = '<div class="event-popup">';
popupContent += `<h3>${properties.label || 'Event'}</h3>`;
popupContent += `<p><strong>Type:</strong> ${type}</p>`;
// Display all properties
popupContent += '<div style="max-height: 300px; overflow-y: auto;">';
popupContent += '<table style="width: 100%; border-collapse: collapse;">';
// Sort properties alphabetically for better organization
const sortedKeys = Object.keys(properties).sort();
for (const key of sortedKeys) {
// Skip the label as it's already displayed as the title
if (key === 'label') continue;
const value = properties[key];
let displayValue;
// Format the value based on its type
if (value === null || value === undefined) {
displayValue = '<em>null</em>';
} else if (typeof value === 'object') {
displayValue = `<pre style="margin: 0; white-space: pre-wrap;">${JSON.stringify(value, null, 2)}</pre>`;
} else if (typeof value === 'string' && value.startsWith('http')) {
displayValue = `<a href="${value}" >${value}</a>`;
} else {
displayValue = String(value);
}
popupContent += `
<tr style="border-bottom: 1px solid #eee;">
<td style="padding: 4px; font-weight: bold; vertical-align: top;">${key}:</td>
<td style="padding: 4px;">${displayValue}</td>
</tr>`;
}
popupContent += '</table>';
popupContent += '</div>';
popupContent += '</div>';
// 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
});
}

View file

@ -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;
}

View file

@ -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 = '<div class="event-popup">';
popupContent += `<h3>${properties.label || 'Event'}</h3>`;
// Display key properties
if (properties.what) {
popupContent += `<p><strong>Type:</strong> ${properties.what}</p>`;
}
if (properties.where) {
popupContent += `<p><strong>Where:</strong> ${properties.where}</p>`;
}
if (properties.start) {
popupContent += `<p><strong>Start:</strong> ${properties.start}</p>`;
}
if (properties.stop) {
popupContent += `<p><strong>End:</strong> ${properties.stop}</p>`;
}
// Add link to view full event
popupContent += `<p><a href="/event/${properties.id}" >View Event</a></p>`;
popupContent += '</div>';
// 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);
});

File diff suppressed because it is too large Load diff

View file

@ -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);
}
}
});

View file

@ -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: '© <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> 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 = '<div class="no-events">No saved events found. Use the demo map to save events.</div>';
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(`
<h3>${event.label || 'Unnamed Event'}</h3>
<p><strong>Type:</strong> ${event.what || 'Unknown'}</p>
<p><strong>Start:</strong> ${formatDate(event.start)}</p>
<p><strong>End:</strong> ${formatDate(event.stop)}</p>
${event.description ? `<p><strong>Description:</strong> ${event.description}</p>` : ''}
<button class="delete-event-btn" onclick="deleteEvent(${index})">Delete</button>
`);
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 = `
<div class="event-header">
<h3>${event.label || 'Unnamed Event'}</h3>
<div class="event-actions">
<button class="zoom-btn" onclick="zoomToEvent(${coordinates[0]}, ${coordinates[1]})">
<i class="fas fa-search-location"></i>
</button>
<button class="delete-btn" onclick="deleteEvent(${index})">
<i class="fas fa-trash-alt"></i>
</button>
</div>
</div>
<div class="event-details">
<p><strong>Type:</strong> ${event.what || 'Unknown'}</p>
<p><strong>When:</strong> ${formatDate(event.start)} to ${formatDate(event.stop)}</p>
${event.description ? `<p><strong>Description:</strong> ${event.description}</p>` : ''}
</div>
`;
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();
}
}

View file

@ -0,0 +1,53 @@
{% extends "layout.html" %}
{% block title %}Events by Type - OpenEventDatabase{% endblock %}
{% block css %}
<style>
ul { padding-left: 20px; }
li { margin-bottom: 8px; }
.event-count {
color: #666;
font-size: 0.9em;
}
</style>
{% endblock %}
{% block header %}Events by Type{% endblock %}
{% block content %}
<p>This page lists all events from the OpenEventDatabase organized by their type.</p>
{% if events_by_what %}
<!-- Quick navigation -->
<h2>Quick Navigation</h2>
<ul>
{% for what_type in sorted_what_types %}
<li>
<a href="#what-{{ what_type|replace(' ', '-') }}">{{ what_type }}</a>
<span class="event-count">({{ events_by_what[what_type]|length }} events)</span>
</li>
{% endfor %}
</ul>
<!-- Sections for each event type -->
{% for what_type in sorted_what_types %}
<h2 id="what-{{ what_type|replace(' ', '-') }}">
{{ what_type }}
<span class="event-count">({{ events_by_what[what_type]|length }} events)</span>
</h2>
<ul>
{% for event in events_by_what[what_type]|sort(attribute='label') %}
<li>
<a href="/event/{{ event.id }}">{{ event.label or 'Unnamed Event' }}</a>
<small>
[<a href="https://www.openstreetmap.org/?mlat={{ event.coordinates[1] }}&mlon={{ event.coordinates[0] }}&zoom=15">map</a>]
</small>
</li>
{% endfor %}
</ul>
{% endfor %}
{% else %}
<p>No events found in the database.</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,90 @@
{% extends "layout.html" %}
{% block title %}Edit Event - OpenEventDatabase{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/edit.css">
{% endblock %}
{% block head %}
<script src="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.css" type="text/css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
{% endblock %}
{% block header %}Edit Event{% endblock %}
{% block content %}
<form id="eventForm">
<input type="hidden" id="eventId" value="{{ id }}">
<div class="form-group">
<label for="label" class="required">Event Name</label>
<input type="text" id="label" name="label" required>
</div>
<div class="form-row">
<div class="form-group">
<label for="type" class="required">Event Type</label>
<select id="type" name="type" required>
<option value="scheduled">Scheduled</option>
<option value="forecast">Forecast</option>
<option value="unscheduled">Unscheduled</option>
</select>
</div>
<div class="form-group">
<label for="what" class="required">What</label>
<input type="text" id="what" name="what" placeholder="e.g., sport.match.football" required>
<div class="note">Category of the event (e.g., sport.match.football, culture.festival)</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="what_series">What: Series</label>
<input type="text" id="what_series" name="what_series" placeholder="e.g., Euro 2024">
<div class="note">Series or group the event belongs to (e.g., Euro 2024, Summer Festival 2023)</div>
</div>
<div class="form-group">
<label for="where">Where</label>
<input type="text" id="where" name="where" placeholder="e.g., Stadium Name">
<div class="note">Specific location name (e.g., Eiffel Tower, Wembley Stadium)</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start" class="required">Start Time</label>
<input type="datetime-local" id="start" name="start" required value="">
</div>
<div class="form-group">
<label for="stop" class="required">End Time</label>
<input type="datetime-local" id="stop" name="stop" required value="">
</div>
</div>
<div class="form-group">
<label class="required">Location</label>
<div id="map"></div>
<div class="note">Click on the map to set the event location</div>
</div>
<div style="display: flex; gap: 10px;">
<button type="submit">Update Event</button>
<button type="button" id="deleteButton" style="background-color: #dc3545;">Delete Event</button>
</div>
</form>
<div id="result"></div>
{% endblock %}
{% block scripts %}
<script>
// Event data from API
const eventData = JSON.parse('{{ event_data|tojson|safe }}');
</script>
<script src="/static/edit.js"></script>
{% endblock %}

View file

@ -0,0 +1,36 @@
{% extends "layout.html" %}
{% block title %}Map by Event Type - OpenEventDatabase{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/map_by_what.css">
{% endblock %}
{% block header %}Map by Event Type{% endblock %}
{% block content %}
<div id="map"></div>
<div class="map-overlay">
<p>This map shows events from the OpenEventDatabase filtered by their type.</p>
<p>Use the filter panel on the right to show/hide different event types.</p>
<div id="event-info">
<p>Loading events...</p>
</div>
</div>
<div class="filter-overlay">
<h3>Filter by Event Type</h3>
<div>
<button id="select-all">Select All</button>
<button id="deselect-all">Deselect All</button>
</div>
<ul id="filter-list" class="filter-list">
<li>Loading event types...</li>
</ul>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/map_by_what.js"></script>
{% endblock %}

View file

@ -0,0 +1,166 @@
{% extends "layout.html" %}
{% block title %}Search Events - OpenEventDatabase{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/search.css">
{% endblock %}
{% block head %}
<script src="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.js"></script>
<link rel="stylesheet" href="https://unpkg.com/@mapbox/mapbox-gl-draw@1.4.3/dist/mapbox-gl-draw.css" type="text/css" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
{% endblock %}
{% block header %}Search Events{% endblock %}
{% block content %}
<form id="searchForm">
<div class="form-row">
<div class="form-group">
<label for="what">Event Type</label>
<input type="text" id="what" name="what" placeholder="e.g., sport.match.football">
<div class="note">Category of the event (e.g., sport.match.football, culture.festival)</div>
</div>
<div class="form-group">
<label for="type">Event Type</label>
<select id="type" name="type">
<option value="">Any</option>
<option value="scheduled">Scheduled</option>
<option value="forecast">Forecast</option>
<option value="unscheduled">Unscheduled</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="when">When</label>
<select id="when" name="when">
<option value="now">Now</option>
<option value="today">Today</option>
<option value="yesterday">Yesterday</option>
<option value="tomorrow">Tomorrow</option>
<option value="lasthour">Last Hour</option>
<option value="nexthour">Next Hour</option>
<option value="last7days">Last 7 Days</option>
<option value="next7days">Next 7 Days</option>
<option value="last30days">Last 30 Days</option>
<option value="next30days">Next 30 Days</option>
<option value="custom">Custom Range</option>
</select>
</div>
<div class="form-group" id="customDateGroup" style="display: none;">
<label for="start">Start Date</label>
<input type="datetime-local" id="start" name="start">
</div>
<div class="form-group" id="customDateEndGroup" style="display: none;">
<label for="stop">End Date</label>
<input type="datetime-local" id="stop" name="stop">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="near">Near (Longitude, Latitude, Distance in meters)</label>
<input type="text" id="near" name="near" placeholder="e.g., 2.3522,48.8566,10000">
<div class="note">Search for events near a specific location (e.g., 2.3522,48.8566,10000 for events within 10km of Paris)</div>
</div>
<div class="form-group">
<label for="bbox">Bounding Box (East, South, West, North)</label>
<input type="text" id="bbox" name="bbox" placeholder="e.g., -5.0,41.0,10.0,52.0">
<div class="note">Search for events within a geographic bounding box</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="where_osm">OpenStreetMap ID</label>
<input type="text" id="where_osm" name="where:osm" placeholder="e.g., R12345">
<div class="note">Search for events associated with a specific OpenStreetMap ID</div>
</div>
<div class="form-group">
<label for="where_wikidata">Wikidata ID</label>
<input type="text" id="where_wikidata" name="where:wikidata" placeholder="e.g., Q90">
<div class="note">Search for events associated with a specific Wikidata ID</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="limit">Result Limit</label>
<input type="number" id="limit" name="limit" value="200" min="1" max="1000">
<div class="note">Maximum number of results to return (default: 200)</div>
</div>
<div class="form-group">
<label for="geom">Geometry Detail</label>
<select id="geom" name="geom">
<option value="">Default (Centroid)</option>
<option value="full">Full Geometry</option>
<option value="only">Geometry Only</option>
<option value="0.01">Simplified (0.01)</option>
</select>
<div class="note">Controls the level of detail in the geometry portion of the response</div>
</div>
</div>
<div class="form-group">
<label>Search Area (Draw on Map)</label>
<div id="map"></div>
<div class="note">Draw a polygon on the map to define the search area, or use the form fields above</div>
</div>
<button type="submit">Search Events</button>
</form>
<div id="result"></div>
<div id="resultsContainer" style="display: none;">
<h2>Search Results</h2>
<div class="tabs-container">
<div class="tab-buttons">
<div class="tab-button active" data-tab="map-tab">Map View</div>
<div class="tab-button" data-tab="table-tab">Table View</div>
</div>
<div id="map-tab" class="tab-content active">
<div id="resultsMap" style="width: 100%; height: 500px;"></div>
</div>
<div id="table-tab" class="tab-content">
<table class="results-table" id="resultsTable">
<thead>
<tr>
<th>ID</th>
<th>Label</th>
<th>Type</th>
<th>What</th>
<th>Where</th>
<th>Start</th>
<th>Stop</th>
</tr>
</thead>
<tbody>
<!-- Results will be added here dynamically -->
</tbody>
</table>
</div>
</div>
<div class="download-buttons">
<button id="downloadCsv" class="download-button">Download CSV</button>
<button id="downloadJson" class="download-button">Download JSON</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/search.js"></script>
{% endblock %}

View file

@ -0,0 +1,275 @@
{% extends "layout.html" %}
{% block title %}Report Traffic Jam - OpenEventDatabase{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/traffic.css">
{% endblock %}
{% block header %}Report Road Issue{% endblock %}
{% block content %}
<!-- Hidden OAuth2 configuration for the JavaScript module -->
<input type="hidden" id="osmClientId" value="{{ client_id }}">
<input type="hidden" id="osmClientSecret" value="{{ client_secret }}">
<input type="hidden" id="osmRedirectUri" value="{{ client_redirect }}">
<!-- Hidden Panoramax configuration (upload endpoint and token) -->
<input type="hidden" id="panoramaxUploadUrl" value="{{ panoramax_upload_url }}">
<input type="hidden" id="panoramaxToken" value="{{ panoramax_token }}">
<!-- Authentication section will be rendered by JavaScript or server-side -->
<div id="auth-section">
{% if is_authenticated %}
<div class="auth-info">
<div>
<p>Logged in as <strong>{{ osm_username }}</strong></p>
<p><a href="https://www.openstreetmap.org/user/{{ osm_username }}" >View OSM Profile</a></p>
<input type="hidden" id="osmUsername" value="{{ osm_username }}">
<input type="hidden" id="osmUserId" value="{{ osm_user_id }}">
</div>
</div>
{% else %}
<p>Authenticate with your OpenStreetMap account to include your username in the traffic report.</p>
<a href="https://www.openstreetmap.org/oauth2/authorize?client_id={{ client_id }}&redirect_uri={{ client_redirect }}&response_type=code&scope={{ client_authorizations }}" class="osm-login-btn">
<span class="osm-logo"></span>
Login with OpenStreetMap
</a>
{% endif %}
<script>
// Préserve l'affichage serveur si présent, sinon laisse traffic.js/dema_auth gérer
document.addEventListener('DOMContentLoaded', function() {
const hasServerAuth = document.getElementById('osmUsername') && document.getElementById('osmUsername').value;
if (hasServerAuth) return;
if (window.osmAuth && osmAuth.renderAuthSection) {
const clientId = document.getElementById('osmClientId').value;
const redirectUri = document.getElementById('osmRedirectUri').value;
const authSection = document.getElementById('auth-section');
authSection.innerHTML = osmAuth.renderAuthSection(clientId, redirectUri);
}
});
</script>
</div>
<h3>Select Issue Type</h3>
<!-- Tab Navigation -->
<div class="tabs">
<div class="tab-item active" data-tab="road">
<i class="fas fa-road"></i> Route
</div>
<div class="tab-item" data-tab="rail">
<i class="fas fa-train"></i> Rail
</div>
<div class="tab-item" data-tab="weather">
<i class="fas fa-cloud-sun-rain"></i> Météo
</div>
<div class="tab-item" data-tab="emergency">
<i class="fas fa-exclamation-circle"></i> Urgences
</div>
<div class="tab-item" data-tab="civic">
<i class="fas fa-bicycle"></i> Cycles
</div>
</div>
<!-- Tab Content -->
<div class="tab-content">
<!-- Road Tab -->
<div class="tab-pane active" id="road-tab">
<div class="issue-buttons">
<div class="issue-button road pothole" onclick="fillForm('pothole')">
<i class="fas fa-dot-circle"></i>
Pothole
</div>
<div class="issue-button road obstacle" onclick="fillForm('obstacle')">
<i class="fas fa-exclamation-triangle"></i>
Obstacle
</div>
<div class="issue-button road vehicle" onclick="fillForm('vehicle')">
<i class="fas fa-car"></i>
Véhicule sur le bas côté de la route
</div>
<div class="issue-button road danger" onclick="fillForm('danger')">
<i class="fas fa-skull-crossbones"></i>
Danger
</div>
<div class="issue-button road accident" onclick="fillForm('accident')">
<i class="fas fa-car-crash"></i>
Accident
</div>
<div class="issue-button road flooded-road" onclick="fillForm('flooded_road')">
<i class="fas fa-water"></i>
Route inondée
</div>
<div class="issue-button road roadwork" onclick="fillForm('roadwork')">
<i class="fas fa-hard-hat"></i>
Travaux
</div>
<div class="issue-button road traffic-jam" onclick="fillForm('traffic_jam')">
<i class="fas fa-traffic-light"></i>
Embouteillage
</div>
</div>
</div>
<!-- Rail Tab -->
<div class="tab-pane" id="rail-tab">
<div class="issue-buttons">
<div class="issue-button rail delay" onclick="fillForm('rail_delay')">
<i class="fas fa-clock"></i>
Retard
</div>
<div class="issue-button rail cancellation" onclick="fillForm('rail_cancellation')">
<i class="fas fa-ban"></i>
Annulation
</div>
<div class="issue-button rail works" onclick="fillForm('rail_works')">
<i class="fas fa-tools"></i>
Travaux
</div>
<div class="issue-button rail incident" onclick="fillForm('rail_incident')">
<i class="fas fa-exclamation-triangle"></i>
Incident
</div>
</div>
</div>
<!-- Weather Tab -->
<div class="tab-pane" id="weather-tab">
<div class="issue-buttons">
<div class="issue-button weather storm" onclick="fillForm('weather_storm')">
<i class="fas fa-bolt"></i>
Orage
</div>
<div class="issue-button weather flood" onclick="fillForm('weather_flood')">
<i class="fas fa-water"></i>
Inondation
</div>
<div class="issue-button weather snow" onclick="fillForm('weather_snow')">
<i class="fas fa-snowflake"></i>
Neige
</div>
<div class="issue-button weather fog" onclick="fillForm('weather_fog')">
<i class="fas fa-smog"></i>
Brouillard
</div>
<div class="issue-button weather heat" onclick="fillForm('weather_heat')">
<i class="fas fa-temperature-high"></i>
Canicule
</div>
</div>
</div>
<!-- Emergency Tab -->
<div class="tab-pane" id="emergency-tab">
<div class="issue-buttons">
<div class="issue-button emergency fire" onclick="fillForm('emergency_fire')">
<i class="fas fa-fire"></i>
Incendie
</div>
<div class="issue-button emergency medical" onclick="fillForm('emergency_medical')">
<i class="fas fa-ambulance"></i>
Urgence médicale
</div>
<div class="issue-button emergency police" onclick="fillForm('emergency_police')">
<i class="fas fa-shield-alt"></i>
Intervention police
</div>
<div class="issue-button emergency evacuation" onclick="fillForm('emergency_evacuation')">
<i class="fas fa-running"></i>
Évacuation
</div>
</div>
</div>
<!-- Civic Tab -->
<div class="tab-pane" id="civic-tab">
<div class="issue-buttons">
<div class="issue-button civic bike-lane" onclick="fillForm('civic_bike_lane')">
<i class="fas fa-bicycle"></i>
Problème piste cyclable
</div>
<div class="issue-button civic sidewalk" onclick="fillForm('civic_sidewalk')">
<i class="fas fa-walking"></i>
Problème trottoir
</div>
<div class="issue-button civic lighting" onclick="fillForm('civic_lighting')">
<i class="fas fa-lightbulb"></i>
Éclairage défectueux
</div>
<div class="issue-button civic garbage" onclick="fillForm('civic_garbage')">
<i class="fas fa-trash"></i>
Déchets
</div>
</div>
</div>
</div>
<h3>Report Details</h3>
<form id="reportForm" class="report-form">
<div class="form-group">
<label for="what" class="required">Type d'événement</label>
<input type="text" id="what" name="what" required placeholder="e.g., road.hazard.pothole">
<div class="note">Catégorie de l'événement (e.g., road.hazard.pothole, road.traffic.jam)</div>
</div>
<div class="form-group">
<label for="label" class="required">Titre</label>
<input type="text" id="label" name="label" required placeholder="e.g., Nid de poule sur la D123">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" name="description" rows="3" placeholder="Description détaillée du problème"></textarea>
</div>
<div class="form-row">
<div class="form-group">
<label for="type">Type de rapport</label>
<select id="type" name="type">
<option value="unscheduled">Non planifié (incident)</option>
<option value="scheduled">Planifié (travaux)</option>
<option value="forecast">Prévision</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start" class="required">Début</label>
<input type="datetime-local" id="start" name="start" required>
</div>
<div class="form-group">
<label for="stop" class="required">Fin (estimée)</label>
<input type="datetime-local" id="stop" name="stop" required>
</div>
</div>
<div class="form-group">
<label for="photo">Photos</label>
<input type="file" id="photo" name="photo" accept="image/*" multiple>
<div class="note">Vous pouvez ajouter plusieurs photos (optionnel)</div>
<div id="photoPreview" class="photo-preview-container"></div>
</div>
<div class="form-group">
<label class="required">Localisation</label>
<div id="map"></div>
<div class="note">Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"</div>
</div>
<button id="report_issue_button" type="submit" disabled>Signaler le problème</button>
</form>
<div id="result"></div>
<a href="/demo/view-events" class="view-saved-events">
<i class="fas fa-map-marked-alt"></i> Voir tous les événements enregistrés sur la carte
</a>
{% endblock %}
{% block scripts %}
<script src="/static/traffic.js"></script>
{% endblock %}

View file

@ -0,0 +1,157 @@
{% extends "layout.html" %}
{% block title %}View Saved Events - OpenEventDatabase{% endblock %}
{% block css %}
<style>
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.map-overlay {
position: absolute;
top: 0;
right: 0;
background: rgba(255, 255, 255, 0.9);
margin: 20px;
padding: 15px;
width: 300px;
border-radius: 5px;
max-height: 90vh;
overflow-y: auto;
}
.event-list {
margin-top: 15px;
max-height: 60vh;
overflow-y: auto;
}
.event-item {
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
padding: 10px;
}
.event-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.event-header h3 {
margin: 0;
font-size: 16px;
}
.event-actions {
display: flex;
gap: 5px;
}
.event-details {
font-size: 14px;
}
.event-details p {
margin: 5px 0;
}
.controls {
display: flex;
gap: 10px;
margin-top: 15px;
}
.no-events {
padding: 10px;
background: #f8f8f8;
border-radius: 4px;
text-align: center;
color: #666;
}
.zoom-btn, .delete-btn {
background: none;
border: none;
cursor: pointer;
padding: 5px;
}
.zoom-btn {
color: #0078ff;
}
.delete-btn {
color: #dc3545;
}
.danger {
background-color: #dc3545;
}
.danger:hover {
background-color: #c82333;
}
</style>
{% endblock %}
{% block header %}Your Saved Events{% endblock %}
{% block content %}
<div id="map"></div>
<!-- Hidden OAuth2 configuration for the JavaScript module -->
<input type="hidden" id="osmClientId" value="{{ client_id }}">
<input type="hidden" id="osmClientSecret" value="{{ client_secret }}">
<input type="hidden" id="osmRedirectUri" value="{{ client_redirect }}">
<div class="map-overlay">
<!-- Authentication section -->
<div id="auth-section" class="auth-section">
<h3>OpenStreetMap Authentication</h3>
<p>Authenticate with your OpenStreetMap account to include your username in reports.</p>
<a href="https://www.openstreetmap.org/oauth2/authorize?client_id={{ client_id }}&redirect_uri={{ client_redirect }}&response_type=code&scope=read_prefs" class="osm-login-btn">
<span class="osm-logo"></span>
Login with OpenStreetMap
</a>
<script>
// Replace server-side auth section with JavaScript-rendered version if available
document.addEventListener('DOMContentLoaded', function() {
if (window.osmAuth) {
const clientId = document.getElementById('osmClientId').value;
const redirectUri = document.getElementById('osmRedirectUri').value;
const authSection = document.getElementById('auth-section');
// Only replace if osmAuth is loaded and has renderAuthSection method
if (osmAuth.renderAuthSection) {
authSection.innerHTML = osmAuth.renderAuthSection(clientId, redirectUri);
}
}
});
</script>
</div>
<div id="event-count"></div>
<div id="event-list" class="event-list"></div>
<div class="controls">
<button id="refresh-btn">
<i class="fas fa-sync-alt"></i> Refresh
</button>
<button id="clear-btn" class="danger">
<i class="fas fa-trash-alt"></i> Clear All
</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/view_events.js"></script>
{% endblock %}

View file

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