demo signal buttons

This commit is contained in:
Tykayn 2025-09-21 13:35:01 +02:00 committed by tykayn
parent e274e91dcb
commit 634c652d57
6 changed files with 1680 additions and 980 deletions

View file

@ -63,6 +63,7 @@ def create_app():
app.add_route('/demo/map-by-what', demo, suffix='map_by_what') # Handle map by event type page
app.add_route('/demo/edit/{id}', demo, suffix='edit') # Handle event editing page
app.add_route('/demo/traffic', demo, suffix='traffic') # Handle traffic jam reporting page
app.add_route('/demo/view-events', demo, suffix='view_events') # Handle view saved events page
logger.success("Application initialized successfully")
return app

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,28 @@
"""
Demo package for the OpenEventDatabase.
This package contains modules for the demo endpoints.
"""
from oedb.resources.demo.demo_main import demo_main
from oedb.resources.demo.demo_traffic import demo_traffic
from oedb.resources.demo.demo_view_events import demo_view_events
# Import DemoResource class from the original demo.py file
import sys
import os
import importlib.util
# Get the path to the original demo.py file
demo_py_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'demo.py')
# Load the demo.py module
spec = importlib.util.spec_from_file_location("oedb.resources.demo_original", demo_py_path)
demo_original = importlib.util.module_from_spec(spec)
sys.modules["oedb.resources.demo_original"] = demo_original
spec.loader.exec_module(demo_original)
# Get the demo object from the original module
demo = demo_original.demo
# Export the demo resources and the demo object
__all__ = ['demo_main', 'demo_traffic', 'demo_view_events', 'demo']

View file

@ -0,0 +1,417 @@
"""
Main demo page resource for the OpenEventDatabase.
"""
import falcon
from oedb.utils.logging import logger
class DemoMainResource:
"""
Resource for the main demo page.
Handles the /demo endpoint.
"""
def on_get(self, req, resp):
"""
Handle GET requests to the /demo endpoint.
Returns an HTML page with a MapLibre map showing current events.
Args:
req: The request object.
resp: The response object.
"""
logger.info("Processing GET request to /demo")
try:
# Set content type to HTML
resp.content_type = 'text/html'
# Create HTML response with MapLibre map
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenEventDatabase Demo</title>
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<style>
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;
}
.map-style-control {
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);
z-index: 1;
}
.map-style-control button {
display: block;
margin-bottom: 5px;
padding: 5px 10px;
background: #fff;
border: 1px solid #ddd;
border-radius: 3px;
cursor: pointer;
width: 100%;
text-align: left;
}
.map-style-control button:hover {
background: #f5f5f5;
}
.map-style-control button.active {
background: #0078ff;
color: white;
border-color: #0056b3;
}
h2 { margin-top: 0; }
ul { padding-left: 20px; }
a { color: #0078ff; text-decoration: none; }
a:hover { text-decoration: underline; }
.event-popup { max-width: 300px; }
</style>
</head>
<body>
<div id="map"></div>
<div class="map-overlay">
<h2>OpenEventDatabase Demo</h2>
<p>This map shows current events from the OpenEventDatabase.</p>
<h3>API Endpoints:</h3>
<ul>
<li><a href="/" target="_blank">/ - API Information</a></li>
<li><a href="/event" target="_blank">/event - Get Events</a></li>
<li><a href="/stats" target="_blank">/stats - Database Statistics</a></li>
</ul>
<h3>Demo Pages:</h3>
<ul>
<li><a href="/demo/search" target="_blank">/demo/search - Advanced Search</a></li>
<li><a href="/demo/by-what" target="_blank">/demo/by-what - Events by Type</a></li>
<li><a href="/demo/map-by-what" target="_blank">/demo/map-by-what - Map by Event Type</a></li>
<li><a href="/demo/traffic" target="_blank">/demo/traffic - Report Traffic Jam</a></li>
<li><a href="/demo/view-events" target="_blank">/demo/view-events - View Saved Events</a></li>
<li><a href="/event?what=music" target="_blank">Search Music Events</a></li>
<li><a href="/event?what=sport" target="_blank">Search Sport Events</a></li>
</ul>
<p><a href="/demo/add" class="add-event-btn" style="display: block; text-align: center; margin-top: 15px; padding: 8px; background-color: #0078ff; color: white; border-radius: 4px; font-weight: bold;">+ Add New Event</a></p>
<p style="text-align: center; margin-top: 10px;">
<a href="https://source.cipherbliss.com/tykayn/oedb-backend" target="_blank" title="View Source Code on Cipherbliss" style="font-size: 24px;">
<i class="fas fa-code-branch"></i>
</a>
</p>
</div>
<div class="map-style-control">
<h4 style="margin-top: 0; margin-bottom: 10px;">Map Style</h4>
<button id="style-default" class="active">Default</button>
<button id="style-osm-vector">OSM Vector</button>
<button id="style-osm-raster">OSM Raster</button>
</div>
<script>
// Map style URLs
const mapStyles = {
default: 'https://demotiles.maplibre.org/style.json',
osmVector: 'https://cdn.jsdelivr.net/gh/openmaptiles/osm-bright-gl-style@master/style-cdn.json',
osmRaster: {
version: 8,
sources: {
'osm-raster': {
type: 'raster',
tiles: [
'https://tile.openstreetmap.org/{z}/{x}/{y}.png'
],
tileSize: 256,
attribution: '© <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors'
}
},
layers: [
{
id: 'osm-raster-layer',
type: 'raster',
source: 'osm-raster',
minzoom: 0,
maxzoom: 19
}
]
}
};
// Initialize the map with default style
const map = new maplibregl.Map({
container: 'map',
style: mapStyles.default,
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" target="_blank">OpenStreetMap</a> contributors'
}));
// Style switcher functionality
let currentStyle = 'default';
let eventsData = null;
document.getElementById('style-default').addEventListener('click', () => {
if (currentStyle !== 'default') {
switchMapStyle('default');
}
});
document.getElementById('style-osm-vector').addEventListener('click', () => {
if (currentStyle !== 'osmVector') {
switchMapStyle('osmVector');
}
});
document.getElementById('style-osm-raster').addEventListener('click', () => {
if (currentStyle !== 'osmRaster') {
switchMapStyle('osmRaster');
}
});
function switchMapStyle(styleName) {
// Update active button
document.querySelectorAll('.map-style-control button').forEach(button => {
button.classList.remove('active');
});
document.getElementById(`style-${styleName.replace('osm', 'osm-')}`).classList.add('active');
// Save current center and zoom
const center = map.getCenter();
const zoom = map.getZoom();
// Save events data if already loaded
if (map.getSource('events')) {
try {
eventsData = map.getSource('events')._data;
} catch (e) {
console.error('Error saving events data:', e);
}
}
// Apply new style
map.setStyle(mapStyles[styleName]);
// Restore center and zoom after style is loaded
map.once('style.load', () => {
map.setCenter(center);
map.setZoom(zoom);
// Restore events data if available
if (eventsData) {
addEventsToMap(eventsData);
} else {
fetchEvents();
}
});
currentStyle = styleName;
}
// Fetch events when the map is loaded
map.on('load', function() {
fetchEvents();
});
// Function to fetch events from the API
function fetchEvents() {
// Fetch events from the API - using default behavior to get currently active events
fetch('/event')
.then(response => response.json())
.then(data => {
if (data.features && data.features.length > 0) {
// Add events to the map
addEventsToMap(data);
// Fit map to events bounds
fitMapToBounds(data);
} else {
console.log('No events found');
}
})
.catch(error => {
console.error('Error fetching events:', error);
});
}
// Function to add events to the map
function addEventsToMap(geojson) {
// Add a GeoJSON source for events
map.addSource('events', {
type: 'geojson',
data: geojson
});
// Add a circle layer for events
map.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
geojson.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 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}" target="_blank">${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>';
// Add edit link
popupContent += `<div style="margin-top: 10px; text-align: center;">
<a href="/demo/edit/${properties.id}" class="edit-event-btn" style="display: inline-block; padding: 5px 10px; background-color: #0078ff; color: white; border-radius: 4px; text-decoration: none; font-weight: bold;">Edit Event</a>
</div>`;
popupContent += '</div>';
// Create popup
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: true
}).setHTML(popupContent);
// Get event type for icon selection
const eventType = properties.what || 'unknown';
// Define icon based on event type
let iconClass = 'info-circle'; // Default icon
let iconColor = '#0078ff'; // Default color
// Map event types to icons
if (eventType.startsWith('weather')) {
iconClass = 'cloud';
iconColor = '#00d1b2'; // Teal
} else if (eventType.startsWith('traffic')) {
iconClass = 'car';
iconColor = '#ff3860'; // Red
} else if (eventType.startsWith('sport')) {
iconClass = 'futbol';
iconColor = '#3273dc'; // Blue
} else if (eventType.startsWith('culture')) {
iconClass = 'theater-masks';
iconColor = '#ffdd57'; // Yellow
} else if (eventType.startsWith('health')) {
iconClass = 'heartbeat';
iconColor = '#ff3860'; // Red
} else if (eventType.startsWith('education')) {
iconClass = 'graduation-cap';
iconColor = '#3273dc'; // Blue
} else if (eventType.startsWith('politics')) {
iconClass = 'landmark';
iconColor = '#209cee'; // Light blue
} else if (eventType.startsWith('nature')) {
iconClass = 'leaf';
iconColor = '#23d160'; // Green
}
// Create custom HTML element for marker
const el = document.createElement('div');
el.className = 'marker';
el.innerHTML = `<span class="icon" style="background-color: white; border-radius: 50%; padding: 8px; box-shadow: 0 0 5px rgba(0,0,0,0.3);">
<i class="fas fa-${iconClass}" style="color: ${iconColor}; font-size: 16px;"></i>
</span>`;
// Add marker with popup
new maplibregl.Marker(el)
.setLngLat(coordinates)
.setPopup(popup)
.addTo(map);
});
}
// 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
});
}
</script>
</body>
</html>
"""
# Set the response body and status
resp.text = html
resp.status = falcon.HTTP_200
logger.success("Successfully processed GET request to /demo")
except Exception as e:
logger.error(f"Error processing GET request to /demo: {e}")
resp.status = falcon.HTTP_500
resp.text = f"Error: {str(e)}"
# Create a global instance of DemoMainResource
demo_main = DemoMainResource()

View file

@ -0,0 +1,794 @@
"""
Traffic jam reporting resource for the OpenEventDatabase.
"""
import falcon
import os
import requests
from oedb.utils.logging import logger
from oedb.utils.db import load_env_from_file
class DemoTrafficResource:
"""
Resource for the traffic jam reporting page.
Handles the /demo/traffic endpoint.
"""
def on_get(self, req, resp):
"""
Handle GET requests to the /demo/traffic endpoint.
Returns an HTML page with a form for reporting traffic jams.
Args:
req: The request object.
resp: The response object.
"""
logger.info("Processing GET request to /demo/traffic")
try:
# Set content type to HTML
resp.content_type = 'text/html'
# Load environment variables from .env file
load_env_from_file()
# Get OAuth2 configuration parameters
client_id = os.getenv("CLIENT_ID", "")
client_secret = os.getenv("CLIENT_SECRET", "")
client_authorizations = os.getenv("CLIENT_AUTORIZATIONS", "read_prefs")
client_redirect = os.getenv("CLIENT_REDIRECT", "")
# Check if we have an authorization code in the query parameters
auth_code = req.params.get('code', None)
auth_state = req.params.get('state', None)
# Variables to track authentication state
is_authenticated = False
osm_username = ""
osm_user_id = ""
# If we have an authorization code, exchange it for an access token
if auth_code:
logger.info(f"Received authorization code: {auth_code}")
try:
# Exchange authorization code for access token
token_url = "https://www.openstreetmap.org/oauth2/token"
token_data = {
"grant_type": "authorization_code",
"code": auth_code,
"redirect_uri": client_redirect,
"client_id": client_id,
"client_secret": client_secret
}
token_response = requests.post(token_url, data=token_data)
token_response.raise_for_status()
token_info = token_response.json()
access_token = token_info.get("access_token")
if access_token:
# Use access token to get user information
user_url = "https://api.openstreetmap.org/api/0.6/user/details.json"
headers = {"Authorization": f"Bearer {access_token}"}
user_response = requests.get(user_url, headers=headers)
user_response.raise_for_status()
user_info = user_response.json()
# Extract user information
user = user_info.get("user", {})
osm_username = user.get("display_name", "")
osm_user_id = user.get("id", "")
if osm_username:
is_authenticated = True
logger.info(f"User authenticated: {osm_username} (ID: {osm_user_id})")
else:
logger.error("Failed to get OSM username from user details")
else:
logger.error("Failed to get access token from token response")
except Exception as e:
logger.error(f"Error during OAuth2 token exchange: {e}")
# Create HTML response with form
# Start with the common HTML header
html_header = f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Traffic Jam - OpenEventDatabase</title>
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
<style>
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;
}}
.geolocation-btn {{
background-color: #28a745;
margin-bottom: 10px;
}}
.geolocation-btn:hover {{
background-color: #218838;
}}
.loading {{
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
vertical-align: middle;
}}
@keyframes spin {{
to {{ transform: rotate(360deg); }}
}}
.auth-section {{
background-color: #f8f9fa;
padding: 15px;
border-radius: 5px;
margin-bottom: 20px;
border: 1px solid #e9ecef;
}}
.auth-section h3 {{
margin-top: 0;
margin-bottom: 10px;
}}
.auth-info {{
display: flex;
align-items: center;
gap: 10px;
}}
.auth-info img {{
width: 40px;
height: 40px;
border-radius: 50%;
}}
.osm-login-btn {{
background-color: #7ebc6f;
display: inline-flex;
align-items: center;
gap: 8px;
}}
.osm-login-btn:hover {{
background-color: #6ba75e;
}}
.osm-logo {{
width: 20px;
height: 20px;
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm-1.66 4.322c.944 0 1.71.766 1.71 1.71s-.766 1.71-1.71 1.71-1.71-.766-1.71-1.71.766-1.71 1.71-1.71zm6.482 13.356H4.322v-1.71h12.5v1.71zm0-3.322H4.322v-1.71h12.5v1.71zm0-3.322H4.322v-1.71h12.5v1.71z"/></svg>');
background-repeat: no-repeat;
background-position: center;
}}
.issue-buttons {{
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 20px;
}}
.issue-button {{
flex: 1;
min-width: 120px;
text-align: center;
padding: 15px 10px;
border-radius: 5px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}}
.issue-button i {{
font-size: 24px;
}}
.issue-button.pothole {{
background-color: #ff9800;
color: white;
}}
.issue-button.obstacle {{
background-color: #f44336;
color: white;
}}
.issue-button.vehicle {{
background-color: #2196f3;
color: white;
}}
.issue-button.danger {{
background-color: #9c27b0;
color: white;
}}
.issue-button:hover {{
transform: translateY(-3px);
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}}
.view-saved-events {{
display: block;
text-align: center;
margin-top: 20px;
padding: 10px;
background-color: #f8f9fa;
border-radius: 5px;
text-decoration: none;
color: #333;
border: 1px solid #ddd;
}}
.view-saved-events:hover {{
background-color: #e9ecef;
}}
</style>
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
</head>
<body>
<div class="container">
<div class="nav-links">
<a href="/demo">&larr; Back to Map</a>
<a href="/">API Information</a>
<a href="/event">View Events</a>
<a href="/demo/view-events">View Saved Events</a>
</div>
<h1>Report Road Issue</h1>
<div class="auth-section">
<h3>OpenStreetMap Authentication</h3>
"""
# Add authentication section based on authentication status
if is_authenticated:
auth_section = f"""
<div class="auth-info">
<div>
<p>Logged in as <strong>{osm_username}</strong></p>
<p><a href="https://www.openstreetmap.org/user/{osm_username}" target="_blank">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:
auth_section = f"""
<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 button">
<span class="osm-logo"></span>
Login with OpenStreetMap
</a>
"""
# Add the rest of the HTML template
html_footer = """
</div>
<h3>Select Issue Type</h3>
<div class="issue-buttons">
<div class="issue-button pothole" onclick="fillForm('pothole')">
<i class="fas fa-dot-circle"></i>
Pothole
</div>
<div class="issue-button obstacle" onclick="fillForm('obstacle')">
<i class="fas fa-exclamation-triangle"></i>
Obstacle
</div>
<div class="issue-button vehicle" onclick="fillForm('vehicle')">
<i class="fas fa-car"></i>
Vehicle on Side
</div>
<div class="issue-button danger" onclick="fillForm('danger')">
<i class="fas fa-skull-crossbones"></i>
Danger
</div>
</div>
<button id="geolocateBtn" class="geolocation-btn">
<span id="geolocateSpinner" class="loading" style="display: none;"></span>
Get My Current Location
</button>
<form id="trafficForm">
<div class="form-group">
<label for="label" class="required">Issue Description</label>
<input type="text" id="label" name="label" placeholder="e.g., Large pothole on Highway A1" required>
</div>
<input type="hidden" id="issueType" name="issueType" value="traffic.jam">
<div class="form-row">
<div class="form-group">
<label for="severity" class="required">Severity</label>
<select id="severity" name="severity" required>
<option value="low">Low (Minor issue)</option>
<option value="medium" selected>Medium (Moderate issue)</option>
<option value="high">High (Severe issue)</option>
</select>
</div>
<div class="form-group">
<label for="cause">Additional Details</label>
<input type="text" id="cause" name="cause" placeholder="e.g., Size, specific location details">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="start" class="required">Report Time</label>
<input type="datetime-local" id="start" name="start" required value="">
</div>
<div class="form-group">
<label for="stop" class="required">Estimated Clear Time</label>
<input type="datetime-local" id="stop" name="stop" required value="">
</div>
</div>
<div class="form-group">
<label for="where">Road/Location Name</label>
<input type="text" id="where" name="where" placeholder="e.g., Highway A1, Main Street">
</div>
<div class="form-group">
<label class="required">Location</label>
<div id="map"></div>
<div class="note">Click on the map to set the issue location or use the "Get My Current Location" button</div>
</div>
<button type="submit">Report Issue</button>
</form>
<div id="result"></div>
<a href="/demo/view-events" class="view-saved-events">
<i class="fas fa-map-marked-alt"></i> View All Saved Events on Map
</a>
</div>
<script>
// Set default date values (current time and +1 hour)
function setDefaultDates() {
const now = new Date();
const nowISO = now.toISOString().slice(0, 16); // Format: YYYY-MM-DDThh:mm
// Set start time to current time
document.getElementById('start').value = nowISO;
// Set end time to current time + 1 hour
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
document.getElementById('stop').value = oneHourLater.toISOString().slice(0, 16);
}
// Call function to set default dates
setDefaultDates();
// Initialize the map
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
center: [2.2137, 46.2276], // Default center (center of metropolitan France)
zoom: 5
});
// Add navigation controls
map.addControl(new maplibregl.NavigationControl());
// Add marker for issue location
let marker = new maplibregl.Marker({
draggable: true,
color: '#ff3860' // Red color for traffic jam
});
// Add marker on map click
map.on('click', function(e) {
marker.setLngLat(e.lngLat).addTo(map);
});
// Function to fill the form based on issue type
function fillForm(issueType) {
const labelInput = document.getElementById('label');
const issueTypeInput = document.getElementById('issueType');
const severitySelect = document.getElementById('severity');
switch(issueType) {
case 'pothole':
labelInput.value = 'Pothole in the road';
issueTypeInput.value = 'road.hazard.pothole';
severitySelect.value = 'medium';
marker.setColor('#ff9800');
break;
case 'obstacle':
labelInput.value = 'Obstacle on the road';
issueTypeInput.value = 'road.hazard.obstacle';
severitySelect.value = 'high';
marker.setColor('#f44336');
break;
case 'vehicle':
labelInput.value = 'Vehicle on the side of the road';
issueTypeInput.value = 'road.hazard.vehicle';
severitySelect.value = 'low';
marker.setColor('#2196f3');
break;
case 'danger':
labelInput.value = 'Dangerous situation on the road';
issueTypeInput.value = 'road.hazard.danger';
severitySelect.value = 'high';
marker.setColor('#9c27b0');
break;
default:
labelInput.value = '';
issueTypeInput.value = 'traffic.jam';
severitySelect.value = 'medium';
marker.setColor('#ff3860');
}
}
// Handle geolocation button click
document.getElementById('geolocateBtn').addEventListener('click', function() {
// Show loading spinner
document.getElementById('geolocateSpinner').style.display = 'inline-block';
this.disabled = true;
// Check if geolocation is available
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
// Success callback
function(position) {
const lat = position.coords.latitude;
const lng = position.coords.longitude;
// Set marker at current location
marker.setLngLat([lng, lat]).addTo(map);
// Center map on current location
map.flyTo({
center: [lng, lat],
zoom: 14
});
// Hide loading spinner
document.getElementById('geolocateSpinner').style.display = 'none';
document.getElementById('geolocateBtn').disabled = false;
// Show success message
showResult('Current location detected successfully', 'success');
// Try to get address using reverse geocoding
fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}`)
.then(response => response.json())
.then(data => {
if (data && data.address) {
// Extract road name or location
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;
}
}
})
.catch(error => {
console.error('Error getting address:', error);
});
},
// Error callback
function(error) {
// Hide loading spinner
document.getElementById('geolocateSpinner').style.display = 'none';
document.getElementById('geolocateBtn').disabled = false;
// Show error message
let errorMsg = 'Unable to get your location. ';
switch(error.code) {
case error.PERMISSION_DENIED:
errorMsg += 'You denied the request for geolocation.';
break;
case error.POSITION_UNAVAILABLE:
errorMsg += 'Location information is unavailable.';
break;
case error.TIMEOUT:
errorMsg += 'The request to get your location timed out.';
break;
case error.UNKNOWN_ERROR:
errorMsg += 'An unknown error occurred.';
break;
}
showResult(errorMsg, 'error');
},
// Options
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 0
}
);
} else {
// Hide loading spinner
document.getElementById('geolocateSpinner').style.display = 'none';
document.getElementById('geolocateBtn').disabled = false;
// Show error message
showResult('Geolocation is not supported by your browser', 'error');
}
});
// Handle form submission
document.getElementById('trafficForm').addEventListener('submit', function(e) {
e.preventDefault();
// Get form values
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;
// Check if marker is set
if (!marker.getLngLat()) {
showResult('Please set a location by clicking on the map or using the geolocation button', '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: 'unscheduled', // Road issues are typically unscheduled
what: issueType, // Category for the issue
'issue:severity': severity, // Custom property for severity
start: start,
stop: stop
}
};
// Add optional properties if provided
if (cause) {
event.properties['issue:details'] = cause;
}
if (where) {
event.properties.where = where;
}
// Add OSM username if authenticated
const osmUsername = document.getElementById('osmUsername');
if (osmUsername && osmUsername.value) {
event.properties['reporter:osm'] = osmUsername.value;
console.log(`Including OSM username in report: ${osmUsername.value}`);
}
// Save event to localStorage
saveEventToLocalStorage(event);
// Submit event to API
fetch('/event', {
method: 'POST',
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 => {
// Update the event in localStorage with the server-assigned ID
if (data.id) {
updateEventInLocalStorage(event, data.id);
}
showResult(`Issue reported successfully with ID: ${data.id}`, 'success');
// Add links to view the event
const resultElement = document.getElementById('result');
resultElement.innerHTML += `
<p>
<a href="/event/${data.id}" target="_blank">View Report on Server</a> |
<a href="/demo/view-events" target="_blank">View Saved Reports</a> |
<a href="/demo">Back to Map</a>
</p>`;
// Reset form
document.getElementById('trafficForm').reset();
// Set default dates again
setDefaultDates();
// Remove marker
marker.remove();
})
.catch(error => {
showResult(`Error reporting issue: ${error.message}`, 'error');
});
});
// Function to save event to localStorage
function saveEventToLocalStorage(event) {
// Get existing events from localStorage
let savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]');
// Add timestamp to event for sorting
event.timestamp = new Date().toISOString();
// Add event to array
savedEvents.push(event);
// Save back to localStorage
localStorage.setItem('oedb_events', JSON.stringify(savedEvents));
console.log('Event saved to localStorage:', event);
}
// Function to update event in localStorage with server ID
function updateEventInLocalStorage(event, serverId) {
// Get existing events from localStorage
let savedEvents = JSON.parse(localStorage.getItem('oedb_events') || '[]');
// Find the event by its timestamp (assuming it was just added)
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) {
// Add server ID to the event
savedEvents[eventIndex].properties.id = serverId;
// Save back to localStorage
localStorage.setItem('oedb_events', JSON.stringify(savedEvents));
console.log('Event updated in localStorage with server ID:', serverId);
}
}
// 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' });
}
</script>
</body>
</html>
"""
# Concatenate the HTML parts to form the complete template
html = html_header + auth_section + html_footer
# Set the response body and status
resp.text = html
resp.status = falcon.HTTP_200
logger.success("Successfully processed GET request to /demo/traffic")
except Exception as e:
logger.error(f"Error processing GET request to /demo/traffic: {e}")
resp.status = falcon.HTTP_500
resp.text = f"Error: {str(e)}"
# Create a global instance of DemoTrafficResource
demo_traffic = DemoTrafficResource()

View file

@ -0,0 +1,424 @@
"""
View saved events resource for the OpenEventDatabase.
"""
import falcon
from oedb.utils.logging import logger
class DemoViewEventsResource:
"""
Resource for viewing saved events.
Handles the /demo/view-events endpoint.
"""
def on_get(self, req, resp):
"""
Handle GET requests to the /demo/view-events endpoint.
Returns an HTML page with a map showing events stored in localStorage.
Args:
req: The request object.
resp: The response object.
"""
logger.info("Processing GET request to /demo/view-events")
try:
# Set content type to HTML
resp.content_type = 'text/html'
# Create HTML response with MapLibre map
html = """
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>View Saved Events - OpenEventDatabase</title>
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<style>
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: 15px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
max-width: 300px;
max-height: 90vh;
overflow-y: auto;
}
.map-overlay h2 {
margin-top: 0;
margin-bottom: 10px;
}
.nav-links {
margin-bottom: 15px;
}
.nav-links a {
color: #0078ff;
text-decoration: none;
margin-right: 15px;
}
.nav-links a:hover {
text-decoration: underline;
}
.event-list {
margin-top: 15px;
max-height: 60vh;
overflow-y: auto;
}
.event-item {
padding: 10px;
border-bottom: 1px solid #eee;
cursor: pointer;
}
.event-item:hover {
background-color: #f5f5f5;
}
.event-item h3 {
margin: 0 0 5px 0;
font-size: 16px;
}
.event-item p {
margin: 0;
font-size: 14px;
color: #666;
}
.event-item .event-date {
font-size: 12px;
color: #999;
}
.event-item .event-type {
display: inline-block;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
color: white;
margin-right: 5px;
}
.event-type.pothole {
background-color: #ff9800;
}
.event-type.obstacle {
background-color: #f44336;
}
.event-type.vehicle {
background-color: #2196f3;
}
.event-type.danger {
background-color: #9c27b0;
}
.event-type.traffic {
background-color: #ff3860;
}
.event-type.other {
background-color: #777;
}
.no-events {
padding: 15px;
background-color: #f8f9fa;
border-radius: 5px;
text-align: center;
color: #666;
}
.controls {
margin-top: 15px;
display: flex;
gap: 10px;
}
.controls button {
flex: 1;
padding: 8px;
background-color: #f8f9fa;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.controls button:hover {
background-color: #e9ecef;
}
.controls button.danger {
color: #dc3545;
}
.popup-content {
max-width: 300px;
}
.popup-content h3 {
margin-top: 0;
margin-bottom: 10px;
}
.popup-content table {
width: 100%;
border-collapse: collapse;
}
.popup-content table td {
padding: 4px;
border-bottom: 1px solid #eee;
}
.popup-content table td:first-child {
font-weight: bold;
width: 40%;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="map-overlay">
<h2>Your Saved Events</h2>
<div class="nav-links">
<a href="/demo">&larr; Back to Map</a>
<a href="/demo/traffic">Report New Issue</a>
</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>
<script>
// Initialize the map
const map = new maplibregl.Map({
container: 'map',
style: 'https://demotiles.maplibre.org/style.json',
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'
}));
// Store markers for later reference
let markers = [];
// 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 (savedEvents.length === 0) {
// Show message if no events
eventList.innerHTML = `
<div class="no-events">
<i class="fas fa-info-circle"></i>
<p>No saved events found.</p>
<p>Report road issues using the <a href="/demo/traffic">Report Issue</a> page.</p>
</div>
`;
return;
}
// Sort events by timestamp (newest first)
savedEvents.sort((a, b) => {
return new Date(b.timestamp) - new Date(a.timestamp);
});
// Create bounds object for fitting map to events
const bounds = new maplibregl.LngLatBounds();
// Add each event to the map and list
savedEvents.forEach((event, index) => {
// Get event properties
const properties = event.properties;
const coordinates = event.geometry.coordinates;
const label = properties.label || 'Unnamed Event';
const what = properties.what || 'unknown';
const timestamp = event.timestamp ? new Date(event.timestamp) : new Date();
const formattedDate = timestamp.toLocaleString();
// Determine event type and color
let eventType = 'other';
let markerColor = '#777';
if (what.includes('pothole')) {
eventType = 'pothole';
markerColor = '#ff9800';
} else if (what.includes('obstacle')) {
eventType = 'obstacle';
markerColor = '#f44336';
} else if (what.includes('vehicle')) {
eventType = 'vehicle';
markerColor = '#2196f3';
} else if (what.includes('danger')) {
eventType = 'danger';
markerColor = '#9c27b0';
} else if (what.includes('traffic')) {
eventType = 'traffic';
markerColor = '#ff3860';
}
// Create marker
const marker = new maplibregl.Marker({
color: markerColor
})
.setLngLat(coordinates)
.addTo(map);
// Store marker reference
markers.push(marker);
// Extend bounds with event coordinates
bounds.extend(coordinates);
// Create popup content
let popupContent = `<div class="popup-content">
<h3>${label}</h3>
<table>`;
// Add event properties to popup
for (const key in properties) {
if (key === 'label') continue; // Skip label as it's already in the title
let displayValue = properties[key];
let displayKey = key;
// Format key for display
displayKey = displayKey.replace(/:/g, ' ').replace(/([A-Z])/g, ' $1');
displayKey = displayKey.charAt(0).toUpperCase() + displayKey.slice(1);
// Format value for display
if (key === 'start' || key === 'stop') {
try {
displayValue = new Date(displayValue).toLocaleString();
} catch (e) {
// Keep original value if date parsing fails
}
}
popupContent += `
<tr>
<td>${displayKey}</td>
<td>${displayValue}</td>
</tr>`;
}
// Add timestamp to popup
popupContent += `
<tr>
<td>Reported</td>
<td>${formattedDate}</td>
</tr>`;
popupContent += `</table></div>`;
// Create popup
const popup = new maplibregl.Popup({
closeButton: true,
closeOnClick: true
}).setHTML(popupContent);
// Attach popup to marker
marker.setPopup(popup);
// Add event to list
const eventItem = document.createElement('div');
eventItem.className = 'event-item';
eventItem.innerHTML = `
<span class="event-type ${eventType}">${eventType}</span>
<h3>${label}</h3>
<p>${properties.where || 'Unknown location'}</p>
<p class="event-date">${formattedDate}</p>
`;
// Add click event to list item
eventItem.addEventListener('click', () => {
// Fly to event location
map.flyTo({
center: coordinates,
zoom: 14
});
// Open popup
marker.togglePopup();
});
// Add to list
eventList.appendChild(eventItem);
});
// Fit map to bounds if there are events
if (!bounds.isEmpty()) {
map.fitBounds(bounds, {
padding: 50,
maxZoom: 12
});
}
}
// Handle refresh button click
document.getElementById('refresh-btn').addEventListener('click', function() {
loadEvents();
});
// Handle clear button click
document.getElementById('clear-btn').addEventListener('click', function() {
if (confirm('Are you sure you want to clear all saved events? This cannot be undone.')) {
// Clear localStorage
localStorage.removeItem('oedb_events');
// Reload events
loadEvents();
}
});
</script>
</body>
</html>
"""
# Set the response body and status
resp.text = html
resp.status = falcon.HTTP_200
logger.success("Successfully processed GET request to /demo/view-events")
except Exception as e:
logger.error(f"Error processing GET request to /demo/view-events: {e}")
resp.status = falcon.HTTP_500
resp.text = f"Error: {str(e)}"
# Create a global instance of DemoViewEventsResource
demo_view_events = DemoViewEventsResource()