up static routes

This commit is contained in:
Tykayn 2025-09-21 16:57:24 +02:00 committed by tykayn
parent 9cca0e4998
commit 1048f4af45
14 changed files with 2959 additions and 1076 deletions

View file

@ -1,5 +1,10 @@
default: start default: start
dev: start dev: start
# Use PORT environment variable if set, otherwise default to 8080
PORT ?= 8080
# Log file for daemonized uWSGI
LOGFILE ?= uwsgi.log
start: start:
python3 -m venv venv python3 -m venv venv
. venv/bin/activate && pip install -r requirements.txt && uwsgi --http :8080 --wsgi-file backend.py --callable app . venv/bin/activate && pip install -r requirements.txt && uwsgi --http :$(PORT) --wsgi-file backend.py --callable app

View file

@ -7,6 +7,7 @@ It initializes the Falcon application and sets up the routes.
""" """
import sys import sys
import os
import falcon import falcon
# Import utility modules # Import utility modules
@ -41,6 +42,11 @@ def create_app():
RateLimitMiddleware() RateLimitMiddleware()
]) ])
# Add static file serving
logger.info("Setting up static file serving")
static_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 'oedb', 'resources', 'demo', 'static'))
app.add_static_route('/static/', static_dir)
# Check database connection before continuing # Check database connection before continuing
if not check_db_connection(): if not check_db_connection():
logger.error("Cannot start server - PostgreSQL database is not responding") logger.error("Cannot start server - PostgreSQL database is not responding")

View file

@ -93,17 +93,36 @@ The traffic jam reporting page (`/demo/traffic`) provides a specialized form for
#### OpenStreetMap Authentication #### OpenStreetMap Authentication
The traffic jam reporting page includes OAuth2 authentication with OpenStreetMap: All demo pages include OAuth2 authentication with OpenStreetMap:
- Users can authenticate with their OpenStreetMap account - Users can authenticate with their OpenStreetMap account on any demo page
- Authentication state is shared across all demo pages using localStorage
- After authentication, the user's OSM username and a link to their profile are displayed - After authentication, the user's OSM username and a link to their profile are displayed
- When submitting a traffic jam report, the OSM username is included in the event properties as `reporter:osm` - When submitting reports or creating events, the OSM username is included in the event properties as `reporter:osm`
- OAuth2 configuration parameters are stored in the `.env` file: - OAuth2 configuration parameters are stored in the `.env` file:
- `CLIENT_ID`: The OAuth2 client ID for the application - `CLIENT_ID`: The OAuth2 client ID for the application
- `CLIENT_SECRET`: The OAuth2 client secret for the application - `CLIENT_SECRET`: The OAuth2 client secret for the application
- `CLIENT_AUTORIZATIONS`: The permissions requested (default: "read_prefs") - `CLIENT_AUTORIZATIONS`: The permissions requested (default: "read_prefs")
- `CLIENT_REDIRECT`: The redirect URL after authentication - `CLIENT_REDIRECT`: The redirect URL after authentication
#### Common Styling
All demo pages share a common CSS style for consistent look and feel:
- Common CSS file located at `/static/demo/demo_styles.css`
- Improved styling for the OpenStreetMap login button
- Consistent styling for forms, buttons, and other UI elements
- Responsive design for better mobile experience
#### JavaScript Modules
The demo pages use JavaScript modules for shared functionality:
- Authentication module located at `/static/demo/demo_auth.js`
- Handles OAuth2 authentication flow with OpenStreetMap
- Stores and retrieves authentication information in localStorage
- Provides methods for checking authentication status and getting user information
## Future Improvements ## Future Improvements
Potential future improvements for the demo pages: Potential future improvements for the demo pages:

View file

@ -26,6 +26,16 @@ class DemoMainResource:
# Set content type to HTML # Set content type to HTML
resp.content_type = 'text/html' resp.content_type = 'text/html'
# Load environment variables from .env file for OAuth2 configuration
from oedb.utils.db import load_env_from_file
load_env_from_file()
# Get OAuth2 configuration parameters
import os
client_id = os.getenv("CLIENT_ID", "")
client_secret = os.getenv("CLIENT_SECRET", "")
client_redirect = os.getenv("CLIENT_REDIRECT", "")
# Create HTML response with MapLibre map # Create HTML response with MapLibre map
html = """ html = """
<!DOCTYPE html> <!DOCTYPE html>
@ -37,7 +47,9 @@ class DemoMainResource:
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script> <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 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"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css">
<link rel="stylesheet" href="/static/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script> <script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<script src="/static/demo_auth.js"></script>
<style> <style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; } body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; } #map { position: absolute; top: 0; bottom: 0; width: 100%; }
@ -89,9 +101,41 @@ class DemoMainResource:
</head> </head>
<body> <body>
<div id="map"></div> <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"> <div class="map-overlay">
<h2>OpenEventDatabase Demo</h2> <h2>OpenEventDatabase Demo</h2>
<p>This map shows current events from the OpenEventDatabase.</p> <p>This map shows current events from the OpenEventDatabase.</p>
<!-- 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>
<h3>API Endpoints:</h3> <h3>API Endpoints:</h3>
<ul> <ul>
<li><a href="/" target="_blank">/ - API Information</a></li> <li><a href="/" target="_blank">/ - API Information</a></li>

View file

@ -14,6 +14,19 @@ class DemoTrafficResource:
Handles the /demo/traffic endpoint. Handles the /demo/traffic endpoint.
""" """
def __init__(self):
"""
Initialize the resource with a Jinja2 environment.
"""
# Set up Jinja2 environment
import jinja2
import os
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
self.jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir),
autoescape=jinja2.select_autoescape(['html', 'xml'])
)
def on_get(self, req, resp): def on_get(self, req, resp):
""" """
Handle GET requests to the /demo/traffic endpoint. Handle GET requests to the /demo/traffic endpoint.
@ -92,694 +105,17 @@ class DemoTrafficResource:
except Exception as e: except Exception as e:
logger.error(f"Error during OAuth2 token exchange: {e}") logger.error(f"Error during OAuth2 token exchange: {e}")
# Create HTML response with form # Load and render the template with the appropriate variables
# Start with the common HTML header template = self.jinja_env.get_template('traffic.html')
html_header = f""" html = template.render(
<!DOCTYPE html> client_id=client_id,
<html lang="en"> client_secret=client_secret,
<head> client_redirect=client_redirect,
<meta charset="UTF-8"> client_authorizations=client_authorizations,
<meta name="viewport" content="width=device-width, initial-scale=1.0"> is_authenticated=is_authenticated,
<title>Report Traffic Jam - OpenEventDatabase</title> osm_username=osm_username,
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script> osm_user_id=osm_user_id
<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 # Set the response body and status
resp.text = html resp.text = html

View file

@ -2,8 +2,11 @@
View saved events resource for the OpenEventDatabase. View saved events resource for the OpenEventDatabase.
""" """
import os
import falcon import falcon
import jinja2
from oedb.utils.logging import logger from oedb.utils.logging import logger
from oedb.utils.db import load_env_from_file
class DemoViewEventsResource: class DemoViewEventsResource:
""" """
@ -11,6 +14,17 @@ class DemoViewEventsResource:
Handles the /demo/view-events endpoint. Handles the /demo/view-events endpoint.
""" """
def __init__(self):
"""
Initialize the resource with a Jinja2 environment.
"""
# Set up Jinja2 environment
template_dir = os.path.join(os.path.dirname(__file__), 'templates')
self.jinja_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir),
autoescape=jinja2.select_autoescape(['html', 'xml'])
)
def on_get(self, req, resp): def on_get(self, req, resp):
""" """
Handle GET requests to the /demo/view-events endpoint. Handle GET requests to the /demo/view-events endpoint.
@ -26,390 +40,21 @@ class DemoViewEventsResource:
# Set content type to HTML # Set content type to HTML
resp.content_type = 'text/html' resp.content_type = 'text/html'
# Create HTML response with MapLibre map # Load environment variables from .env file for OAuth2 configuration
html = """ load_env_from_file()
<!DOCTYPE html>
<html lang="en"> # Get OAuth2 configuration parameters
<head> client_id = os.getenv("CLIENT_ID", "")
<meta charset="UTF-8"> client_secret = os.getenv("CLIENT_SECRET", "")
<meta name="viewport" content="width=device-width, initial-scale=1.0"> client_redirect = os.getenv("CLIENT_REDIRECT", "")
<title>View Saved Events - OpenEventDatabase</title>
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script> # Load and render the template with the appropriate variables
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" /> template = self.jinja_env.get_template('view_events.html')
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script> html = template.render(
<style> client_id=client_id,
body { client_secret=client_secret,
margin: 0; client_redirect=client_redirect
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 # Set the response body and status
resp.text = html resp.text = html

View file

@ -0,0 +1,270 @@
/**
* OpenStreetMap OAuth2 authentication module for the OpenEventDatabase demo pages.
* This module handles authentication with OpenStreetMap and stores/retrieves auth info in localStorage.
*/
// Constants
const OSM_AUTH_STORAGE_KEY = 'oedb_osm_auth';
/**
* OSM Authentication class
*/
class OSMAuth {
constructor() {
// Initialize auth state
this.isAuthenticated = false;
this.username = '';
this.userId = '';
// Load auth info from localStorage
this.loadAuthInfo();
}
/**
* Load authentication information from localStorage
*/
loadAuthInfo() {
try {
const authInfo = localStorage.getItem(OSM_AUTH_STORAGE_KEY);
if (authInfo) {
const parsedAuthInfo = JSON.parse(authInfo);
this.isAuthenticated = true;
this.username = parsedAuthInfo.username || '';
this.userId = parsedAuthInfo.userId || '';
console.log('Loaded OSM auth info from localStorage:', this.username);
}
} catch (error) {
console.error('Error loading OSM auth info from localStorage:', error);
}
}
/**
* Save authentication information to localStorage
* @param {string} username - The OSM username
* @param {string} userId - The OSM user ID
*/
saveAuthInfo(username, userId) {
try {
const authInfo = {
username: username,
userId: userId,
timestamp: new Date().toISOString()
};
localStorage.setItem(OSM_AUTH_STORAGE_KEY, JSON.stringify(authInfo));
this.isAuthenticated = true;
this.username = username;
this.userId = userId;
console.log('Saved OSM auth info to localStorage:', username);
} catch (error) {
console.error('Error saving OSM auth info to localStorage:', error);
}
}
/**
* Clear authentication information from localStorage
*/
clearAuthInfo() {
try {
localStorage.removeItem(OSM_AUTH_STORAGE_KEY);
this.isAuthenticated = false;
this.username = '';
this.userId = '';
console.log('Cleared OSM auth info from localStorage');
} catch (error) {
console.error('Error clearing OSM auth info from localStorage:', error);
}
}
/**
* Check if the user is authenticated
* @returns {boolean} - True if the user is authenticated, false otherwise
*/
isUserAuthenticated() {
return this.isAuthenticated;
}
/**
* Get the OSM username
* @returns {string} - The OSM username
*/
getUsername() {
return this.username;
}
/**
* Get the OSM user ID
* @returns {string} - The OSM user ID
*/
getUserId() {
return this.userId;
}
/**
* Render the authentication section
* @param {string} clientId - The OAuth2 client ID
* @param {string} redirectUri - The OAuth2 redirect URI
* @param {string} scope - The OAuth2 scope
* @returns {string} - The HTML for the authentication section
*/
renderAuthSection(clientId, redirectUri, scope = 'read_prefs') {
let html = '<div class="auth-section">';
html += '<h3>OpenStreetMap Authentication</h3>';
if (this.isAuthenticated) {
html += '<div class="auth-info">';
html += '<div>';
html += `<p>Logged in as <strong>${this.username}</strong></p>`;
html += `<p><a href="https://www.openstreetmap.org/user/${this.username}" target="_blank">View OSM Profile</a></p>`;
html += `<input type="hidden" id="osmUsername" value="${this.username}">`;
html += `<input type="hidden" id="osmUserId" value="${this.userId}">`;
html += '</div>';
html += '</div>';
} else {
html += '<p>Authenticate with your OpenStreetMap account to include your username in reports.</p>';
html += `<a href="https://www.openstreetmap.org/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=${scope}" class="osm-login-btn">`;
html += '<span class="osm-logo"></span>';
html += 'Login with OpenStreetMap';
html += '</a>';
}
html += '</div>';
return html;
}
/**
* Process the OAuth2 callback
* @param {string} authCode - The authorization code from the callback
* @param {string} clientId - The OAuth2 client ID
* @param {string} clientSecret - The OAuth2 client secret
* @param {string} redirectUri - The OAuth2 redirect URI
* @returns {Promise} - A promise that resolves when the authentication is complete
*/
processAuthCallback(authCode, clientId, clientSecret, redirectUri) {
return new Promise((resolve, reject) => {
if (!authCode) {
reject(new Error('No authorization code provided'));
return;
}
console.log('Processing OAuth2 callback with auth code:', authCode);
// Exchange authorization code for access token
const tokenUrl = 'https://www.openstreetmap.org/oauth2/token';
const tokenData = {
grant_type: 'authorization_code',
code: authCode,
redirect_uri: redirectUri,
client_id: clientId,
client_secret: clientSecret
};
// Make the request
fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: new URLSearchParams(tokenData)
})
.then(response => {
if (!response.ok) {
throw new Error(`Token request failed with status ${response.status}`);
}
return response.json();
})
.then(tokenInfo => {
const accessToken = tokenInfo.access_token;
if (!accessToken) {
throw new Error('No access token in response');
}
// Use access token to get user information
return fetch('https://api.openstreetmap.org/api/0.6/user/details.json', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
})
.then(response => {
if (!response.ok) {
throw new Error(`User details request failed with status ${response.status}`);
}
return response.json();
})
.then(userInfo => {
const user = userInfo.user || {};
const username = user.display_name || '';
const userId = user.id || '';
if (!username) {
throw new Error('No username in user details');
}
// Save auth info to localStorage
this.saveAuthInfo(username, userId);
resolve({
username: username,
userId: userId
});
})
.catch(error => {
console.error('Error during OAuth2 authentication:', error);
reject(error);
});
});
}
}
// Create a global instance of the OSMAuth class
const osmAuth = new OSMAuth();
/**
* Initialize the authentication module
* This should be called when the page loads
*/
function initAuth() {
console.log('Initializing OSM auth module');
// Check if we have an authorization code in the URL
const urlParams = new URLSearchParams(window.location.search);
const authCode = urlParams.get('code');
if (authCode) {
console.log('Authorization code found in URL');
// Get OAuth2 configuration from the page
const clientId = document.getElementById('osmClientId')?.value || '';
const clientSecret = document.getElementById('osmClientSecret')?.value || '';
const redirectUri = document.getElementById('osmRedirectUri')?.value || '';
if (clientId && redirectUri) {
// Process the authorization code
osmAuth.processAuthCallback(authCode, clientId, clientSecret, redirectUri)
.then(userInfo => {
console.log('Authentication successful:', userInfo);
// Remove the authorization code from the URL
const url = new URL(window.location.href);
url.searchParams.delete('code');
url.searchParams.delete('state');
window.history.replaceState({}, document.title, url.toString());
// Reload the page to update the UI
window.location.reload();
})
.catch(error => {
console.error('Authentication failed:', error);
});
} else {
console.error('Missing OAuth2 configuration');
}
}
}
// Export the OSMAuth instance and helper functions
window.osmAuth = osmAuth;
window.initAuth = initAuth;
// Initialize the auth module when the page loads
document.addEventListener('DOMContentLoaded', initAuth);

View file

@ -0,0 +1,421 @@
/*
* Common CSS styles for all demo pages in the OpenEventDatabase
*/
/* General styles */
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;
}
/* Authentication section styles */
.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;
color: #333;
}
.auth-info {
display: flex;
align-items: center;
gap: 10px;
}
.auth-info img {
width: 40px;
height: 40px;
border-radius: 50%;
}
/* Improved OSM login button styling */
.osm-login-btn {
background-color: #7ebc6f;
color: white;
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 15px;
border-radius: 4px;
font-weight: bold;
text-decoration: none;
transition: all 0.2s ease;
border: none;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.osm-login-btn:hover {
background-color: #6ba75e;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
}
.osm-logo {
width: 24px;
height: 24px;
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;
}
/* Geolocation button styles */
.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); }
}
/* Issue buttons styles */
.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 link */
.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;
}
/* Map overlay styles */
.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;
}
/* Event list styles */
.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 styles */
.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 styles */
.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%;
}
.osm-login-btn{
padding: 1rem 2rem;
border-radius: 5px;
}

View file

@ -0,0 +1,512 @@
<!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" />
<link rel="stylesheet" href="/static/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<script src="/static/demo_auth.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>
<!-- 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 }}">
<!-- 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 }}" 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 %}
<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>
// 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>
<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');
// Save current marker position if it exists
let currentLngLat = marker.getLngLat ? marker.getLngLat() : null;
// Remove existing marker from the map
marker.remove();
// Set marker color based on issue type
let markerColor = '#ff3860'; // Default red color
switch(issueType) {
case 'pothole':
labelInput.value = 'Nid de poule';
issueTypeInput.value = 'road.hazard.pothole';
severitySelect.value = 'medium';
markerColor = '#ff9800';
break;
case 'obstacle':
labelInput.value = 'Obstacle';
issueTypeInput.value = 'road.hazard.obstacle';
severitySelect.value = 'high';
markerColor = '#f44336';
break;
case 'vehicle':
labelInput.value = 'Véhicule sur le bas côté de la route';
issueTypeInput.value = 'road.hazard.vehicle';
severitySelect.value = 'low';
markerColor = '#2196f3';
break;
case 'danger':
labelInput.value = 'Danger non classé';
issueTypeInput.value = 'road.hazard.danger';
severitySelect.value = 'high';
markerColor = '#9c27b0';
break;
default:
labelInput.value = 'Bouchon';
issueTypeInput.value = 'traffic.jam';
severitySelect.value = 'medium';
markerColor = '#ff3860';
}
// Create a new marker with the selected color
marker = new maplibregl.Marker({
draggable: true,
color: markerColor
});
// If there was a previous marker position, set the new marker at that position
if (currentLngLat) {
marker.setLngLat(currentLngLat).addTo(map);
}
}
// 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 - check both DOM element and osmAuth object
let osmUsernameValue = '';
// First check the DOM element (for backward compatibility)
const osmUsername = document.getElementById('osmUsername');
if (osmUsername && osmUsername.value) {
osmUsernameValue = osmUsername.value;
}
// Then check the osmAuth object (preferred method)
if (window.osmAuth && osmAuth.isUserAuthenticated()) {
osmUsernameValue = osmAuth.getUsername();
}
// Add the username to the event properties if available
if (osmUsernameValue) {
event.properties['reporter:osm'] = osmUsernameValue;
console.log(`Including OSM username in report: ${osmUsernameValue}`);
}
// 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>

View file

@ -0,0 +1,284 @@
<!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" />
<link rel="stylesheet" href="/static/demo/demo_styles.css">
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<script src="/static/demo/demo_auth.js"></script>
<style>
#map {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
</style>
</head>
<body>
<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">
<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>
<!-- 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>
<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>

View file

@ -7,3 +7,4 @@ pyproj==3.7.2
pytz==2025.2 pytz==2025.2
Requests==2.32.5 Requests==2.32.5
waitress==3.0.2 waitress==3.0.2
jinja2

16
test_app_init.py Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""
Test script to verify that the application initializes correctly.
"""
import sys
from backend import create_app
try:
print("Attempting to initialize the application...")
app = create_app()
print("Application initialized successfully!")
sys.exit(0)
except Exception as e:
print(f"Error initializing application: {e}")
sys.exit(1)

80
test_marker.html Normal file
View file

@ -0,0 +1,80 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MapLibre Marker Test</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: 0; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
.controls {
position: absolute;
top: 10px;
left: 10px;
z-index: 1;
background: white;
padding: 10px;
border-radius: 4px;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
}
button {
margin: 5px;
padding: 8px 12px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="controls">
<h3>Test Marker Color Change</h3>
<button onclick="changeMarkerColor('#ff9800')">Orange</button>
<button onclick="changeMarkerColor('#f44336')">Red</button>
<button onclick="changeMarkerColor('#2196f3')">Blue</button>
<button onclick="changeMarkerColor('#9c27b0')">Purple</button>
</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 marker for issue location
let marker = new maplibregl.Marker({
draggable: true,
color: '#ff3860' // Red color for traffic jam
}).setLngLat([2.2137, 46.2276]).addTo(map);
// Function to change marker color
function changeMarkerColor(color) {
console.log(`Changing marker color to ${color}`);
// Save current marker position
let currentLngLat = marker.getLngLat();
// Remove existing marker from the map
marker.remove();
// Create a new marker with the selected color
marker = new maplibregl.Marker({
draggable: true,
color: color
});
// Set the new marker at the previous position
marker.setLngLat(currentLngLat).addTo(map);
console.log("Marker color changed successfully");
}
</script>
</body>
</html>

1244
uwsgi.log Normal file

File diff suppressed because it is too large Load diff