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
dev: start
# Use PORT environment variable if set, otherwise default to 8080
PORT ?= 8080
# Log file for daemonized uWSGI
LOGFILE ?= uwsgi.log
start:
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 os
import falcon
# Import utility modules
@ -41,6 +42,11 @@ def create_app():
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
if not check_db_connection():
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
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
- 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:
- `CLIENT_ID`: The OAuth2 client ID for the application
- `CLIENT_SECRET`: The OAuth2 client secret for the application
- `CLIENT_AUTORIZATIONS`: The permissions requested (default: "read_prefs")
- `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
Potential future improvements for the demo pages:

View file

@ -26,6 +26,16 @@ class DemoMainResource:
# Set content type to 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
html = """
<!DOCTYPE html>
@ -37,7 +47,9 @@ class DemoMainResource:
<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">
<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>
<style>
body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
#map { position: absolute; top: 0; bottom: 0; width: 100%; }
@ -89,9 +101,41 @@ class DemoMainResource:
</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>OpenEventDatabase Demo</h2>
<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>
<ul>
<li><a href="/" target="_blank">/ - API Information</a></li>

View file

@ -14,6 +14,19 @@ class DemoTrafficResource:
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):
"""
Handle GET requests to the /demo/traffic endpoint.
@ -92,694 +105,17 @@ class DemoTrafficResource:
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
# Load and render the template with the appropriate variables
template = self.jinja_env.get_template('traffic.html')
html = template.render(
client_id=client_id,
client_secret=client_secret,
client_redirect=client_redirect,
client_authorizations=client_authorizations,
is_authenticated=is_authenticated,
osm_username=osm_username,
osm_user_id=osm_user_id
)
# Set the response body and status
resp.text = html

View file

@ -2,8 +2,11 @@
View saved events resource for the OpenEventDatabase.
"""
import os
import falcon
import jinja2
from oedb.utils.logging import logger
from oedb.utils.db import load_env_from_file
class DemoViewEventsResource:
"""
@ -11,6 +14,17 @@ class DemoViewEventsResource:
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):
"""
Handle GET requests to the /demo/view-events endpoint.
@ -26,390 +40,21 @@ class DemoViewEventsResource:
# 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>
# Load environment variables from .env file for OAuth2 configuration
load_env_from_file()
<div class="map-overlay">
<h2>Your Saved Events</h2>
# Get OAuth2 configuration parameters
client_id = os.getenv("CLIENT_ID", "")
client_secret = os.getenv("CLIENT_SECRET", "")
client_redirect = os.getenv("CLIENT_REDIRECT", "")
<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>
"""
# Load and render the template with the appropriate variables
template = self.jinja_env.get_template('view_events.html')
html = template.render(
client_id=client_id,
client_secret=client_secret,
client_redirect=client_redirect
)
# Set the response body and status
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
Requests==2.32.5
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