oedb-backend/oedb/resources/demo/demo_view_events.py
2025-09-21 13:35:01 +02:00

424 lines
No EOL
18 KiB
Python

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