traffic with osm auth
This commit is contained in:
parent
6d755ee8dc
commit
e274e91dcb
4 changed files with 649 additions and 5 deletions
|
@ -5,8 +5,10 @@ Demo resource for the OpenEventDatabase.
|
|||
import falcon
|
||||
import requests
|
||||
import json
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from oedb.utils.logging import logger
|
||||
from oedb.utils.db import load_env_from_file
|
||||
|
||||
class DemoResource:
|
||||
"""
|
||||
|
@ -36,7 +38,7 @@ class DemoResource:
|
|||
resp.content_type = 'text/html'
|
||||
|
||||
# Fetch the event data from the API
|
||||
response = requests.get(f'http://localhost/event/{id}')
|
||||
response = requests.get(f'http://api.openevent/event/{id}')
|
||||
|
||||
if response.status_code != 200:
|
||||
resp.status = falcon.HTTP_404
|
||||
|
@ -508,6 +510,7 @@ class DemoResource:
|
|||
<li><a href="/demo/search" target="_blank">/demo/search - Advanced Search</a></li>
|
||||
<li><a href="/demo/by-what" target="_blank">/demo/by-what - Events by Type</a></li>
|
||||
<li><a href="/demo/map-by-what" target="_blank">/demo/map-by-what - Map by Event Type</a></li>
|
||||
<li><a href="/demo/traffic" target="_blank">/demo/traffic - Report Traffic Jam</a></li>
|
||||
<li><a href="/event?what=music" target="_blank">Search Music Events</a></li>
|
||||
<li><a href="/event?what=sport" target="_blank">Search Sport Events</a></li>
|
||||
</ul>
|
||||
|
@ -2303,5 +2306,603 @@ class DemoResource:
|
|||
resp.status = falcon.HTTP_500
|
||||
resp.text = f"Error: {str(e)}"
|
||||
|
||||
def on_get_traffic(self, req, resp):
|
||||
"""
|
||||
Handle GET requests to the /demo/traffic endpoint.
|
||||
Returns an HTML page with a form for reporting traffic jams.
|
||||
|
||||
Args:
|
||||
req: The request object.
|
||||
resp: The response object.
|
||||
"""
|
||||
logger.info("Processing GET request to /demo/traffic")
|
||||
|
||||
try:
|
||||
# Set content type to HTML
|
||||
resp.content_type = 'text/html'
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_env_from_file()
|
||||
|
||||
# Get OAuth2 configuration parameters
|
||||
client_id = os.getenv("CLIENT_ID", "")
|
||||
client_secret = os.getenv("CLIENT_SECRET", "")
|
||||
client_authorizations = os.getenv("CLIENT_AUTORIZATIONS", "read_prefs")
|
||||
client_redirect = os.getenv("CLIENT_REDIRECT", "")
|
||||
|
||||
# Check if we have an authorization code in the query parameters
|
||||
auth_code = req.params.get('code', None)
|
||||
auth_state = req.params.get('state', None)
|
||||
|
||||
# Variables to track authentication state
|
||||
is_authenticated = False
|
||||
osm_username = ""
|
||||
osm_user_id = ""
|
||||
|
||||
# If we have an authorization code, exchange it for an access token
|
||||
if auth_code:
|
||||
logger.info(f"Received authorization code: {auth_code}")
|
||||
|
||||
try:
|
||||
# Exchange authorization code for access token
|
||||
token_url = "https://www.openstreetmap.org/oauth2/token"
|
||||
token_data = {
|
||||
"grant_type": "authorization_code",
|
||||
"code": auth_code,
|
||||
"redirect_uri": client_redirect,
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret
|
||||
}
|
||||
|
||||
token_response = requests.post(token_url, data=token_data)
|
||||
token_response.raise_for_status()
|
||||
token_info = token_response.json()
|
||||
|
||||
access_token = token_info.get("access_token")
|
||||
|
||||
if access_token:
|
||||
# Use access token to get user information
|
||||
user_url = "https://api.openstreetmap.org/api/0.6/user/details.json"
|
||||
headers = {"Authorization": f"Bearer {access_token}"}
|
||||
|
||||
user_response = requests.get(user_url, headers=headers)
|
||||
user_response.raise_for_status()
|
||||
user_info = user_response.json()
|
||||
|
||||
# Extract user information
|
||||
user = user_info.get("user", {})
|
||||
osm_username = user.get("display_name", "")
|
||||
osm_user_id = user.get("id", "")
|
||||
|
||||
if osm_username:
|
||||
is_authenticated = True
|
||||
logger.info(f"User authenticated: {osm_username} (ID: {osm_user_id})")
|
||||
else:
|
||||
logger.error("Failed to get OSM username from user details")
|
||||
else:
|
||||
logger.error("Failed to get access token from token response")
|
||||
except Exception as e:
|
||||
logger.error(f"Error during OAuth2 token exchange: {e}")
|
||||
|
||||
# Create HTML response with form
|
||||
# Start with the common HTML header
|
||||
html_header = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Report Traffic Jam - OpenEventDatabase</title>
|
||||
<script src="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.js"></script>
|
||||
<link href="https://unpkg.com/maplibre-gl@3.0.0/dist/maplibre-gl.css" rel="stylesheet" />
|
||||
<style>
|
||||
body {{
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.container {{
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
background-color: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}}
|
||||
h1 {{
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}}
|
||||
.form-group {{
|
||||
margin-bottom: 15px;
|
||||
}}
|
||||
label {{
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}}
|
||||
input[type="text"],
|
||||
input[type="datetime-local"],
|
||||
select,
|
||||
textarea {{
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}}
|
||||
.required:after {{
|
||||
content: " *";
|
||||
color: red;
|
||||
}}
|
||||
.form-row {{
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}}
|
||||
.form-row .form-group {{
|
||||
flex: 1;
|
||||
}}
|
||||
button {{
|
||||
background-color: #0078ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 15px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}}
|
||||
button:hover {{
|
||||
background-color: #0056b3;
|
||||
}}
|
||||
.note {{
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}}
|
||||
#map {{
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 4px;
|
||||
}}
|
||||
#result {{
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
display: none;
|
||||
}}
|
||||
#result.success {{
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}}
|
||||
#result.error {{
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}}
|
||||
.nav-links {{
|
||||
margin-bottom: 20px;
|
||||
}}
|
||||
.nav-links a {{
|
||||
color: #0078ff;
|
||||
text-decoration: none;
|
||||
margin-right: 15px;
|
||||
}}
|
||||
.nav-links a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
.geolocation-btn {{
|
||||
background-color: #28a745;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.geolocation-btn:hover {{
|
||||
background-color: #218838;
|
||||
}}
|
||||
.loading {{
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255,255,255,.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: #fff;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}}
|
||||
@keyframes spin {{
|
||||
to {{ transform: rotate(360deg); }}
|
||||
}}
|
||||
.auth-section {{
|
||||
background-color: #f8f9fa;
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 20px;
|
||||
border: 1px solid #e9ecef;
|
||||
}}
|
||||
.auth-section h3 {{
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}}
|
||||
.auth-info {{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}}
|
||||
.auth-info img {{
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
}}
|
||||
.osm-login-btn {{
|
||||
background-color: #7ebc6f;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}}
|
||||
.osm-login-btn:hover {{
|
||||
background-color: #6ba75e;
|
||||
}}
|
||||
.osm-logo {{
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white"><path d="M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm-1.66 4.322c.944 0 1.71.766 1.71 1.71s-.766 1.71-1.71 1.71-1.71-.766-1.71-1.71.766-1.71 1.71-1.71zm6.482 13.356H4.322v-1.71h12.5v1.71zm0-3.322H4.322v-1.71h12.5v1.71zm0-3.322H4.322v-1.71h12.5v1.71z"/></svg>');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="nav-links">
|
||||
<a href="/demo">← Back to Map</a>
|
||||
<a href="/">API Information</a>
|
||||
<a href="/event">View Events</a>
|
||||
</div>
|
||||
|
||||
<h1>Report Traffic Jam</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>
|
||||
|
||||
<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">Traffic Jam Description</label>
|
||||
<input type="text" id="label" name="label" placeholder="e.g., Heavy traffic on Highway A1" required value="bouchon">
|
||||
</div>
|
||||
|
||||
<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 (Slow moving)</option>
|
||||
<option value="medium" selected>Medium (Very slow)</option>
|
||||
<option value="high">High (Standstill)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="cause">Cause (if known)</label>
|
||||
<input type="text" id="cause" name="cause" placeholder="e.g., Accident, Construction, Weather">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="start" class="required">Start Time</label>
|
||||
<input type="datetime-local" id="start" name="start" required value="">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="stop" class="required">Estimated End 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 traffic jam location or use the "Get My Current Location" button</div>
|
||||
</div>
|
||||
|
||||
<button type="submit">Report Traffic Jam</button>
|
||||
</form>
|
||||
|
||||
<div id="result"></div>
|
||||
</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 traffic jam 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);
|
||||
});
|
||||
|
||||
// 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 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', // Traffic jams are typically unscheduled
|
||||
what: 'traffic.jam', // Category for traffic jams
|
||||
'traffic:severity': severity, // Custom property for severity
|
||||
start: start,
|
||||
stop: stop
|
||||
}
|
||||
};
|
||||
|
||||
// Add optional properties if provided
|
||||
if (cause) {
|
||||
event.properties['traffic:cause'] = 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}`);
|
||||
}
|
||||
|
||||
// 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 => {
|
||||
showResult(`Traffic jam reported successfully with ID: ${data.id}`, 'success');
|
||||
|
||||
// Add link to view the event
|
||||
const resultElement = document.getElementById('result');
|
||||
resultElement.innerHTML += `<p><a href="/event/${data.id}" target="_blank">View Report</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 traffic jam: ${error.message}`, 'error');
|
||||
});
|
||||
});
|
||||
|
||||
// Show result message
|
||||
function showResult(message, type) {
|
||||
const resultElement = document.getElementById('result');
|
||||
resultElement.textContent = message;
|
||||
resultElement.className = type;
|
||||
resultElement.style.display = 'block';
|
||||
|
||||
// Scroll to result
|
||||
resultElement.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# Concatenate the HTML parts to form the complete template
|
||||
html = html_header + auth_section + html_footer
|
||||
|
||||
# Set the response body and status
|
||||
resp.text = html
|
||||
resp.status = falcon.HTTP_200
|
||||
logger.success("Successfully processed GET request to /demo/traffic")
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing GET request to /demo/traffic: {e}")
|
||||
resp.status = falcon.HTTP_500
|
||||
resp.text = f"Error: {str(e)}"
|
||||
|
||||
# Create a global instance of DemoResource
|
||||
demo = DemoResource()
|
Loading…
Add table
Add a link
Reference in a new issue