2025-09-21 16:57:24 +02:00
|
|
|
<!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">← 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>
|
2025-09-21 17:30:47 +02:00
|
|
|
<p><a href="https://www.openstreetmap.org/user/{{ osm_username }}" >View OSM Profile</a></p>
|
2025-09-21 16:57:24 +02:00
|
|
|
<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>
|
2025-09-21 19:18:43 +02:00
|
|
|
|
|
|
|
<!-- Tab Navigation -->
|
|
|
|
<div class="tabs">
|
|
|
|
<div class="tab-item active" data-tab="road">
|
|
|
|
<i class="fas fa-road"></i> Road
|
2025-09-21 16:57:24 +02:00
|
|
|
</div>
|
2025-09-21 19:18:43 +02:00
|
|
|
<div class="tab-item" data-tab="rail">
|
|
|
|
<i class="fas fa-train"></i> Rail
|
2025-09-21 16:57:24 +02:00
|
|
|
</div>
|
2025-09-21 19:18:43 +02:00
|
|
|
<div class="tab-item" data-tab="weather">
|
|
|
|
<i class="fas fa-cloud-sun-rain"></i> Weather
|
2025-09-21 16:57:24 +02:00
|
|
|
</div>
|
2025-09-21 19:18:43 +02:00
|
|
|
<div class="tab-item" data-tab="emergency">
|
|
|
|
<i class="fas fa-exclamation-circle"></i> Emergency
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Tab Content -->
|
|
|
|
<div class="tab-content">
|
|
|
|
<!-- Road Tab -->
|
|
|
|
<div class="tab-pane active" id="road-tab">
|
|
|
|
<div class="issue-buttons">
|
|
|
|
<div class="issue-button road pothole" onclick="fillForm('pothole')">
|
|
|
|
<i class="fas fa-dot-circle"></i>
|
|
|
|
Pothole
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road obstacle" onclick="fillForm('obstacle')">
|
|
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
|
|
Obstacle
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road vehicle" onclick="fillForm('vehicle')">
|
|
|
|
<i class="fas fa-car"></i>
|
|
|
|
Vehicle on Side
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road danger" onclick="fillForm('danger')">
|
|
|
|
<i class="fas fa-skull-crossbones"></i>
|
|
|
|
Danger
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road accident" onclick="fillForm('accident')">
|
|
|
|
<i class="fas fa-car-crash"></i>
|
|
|
|
Accident
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road flooded-road" onclick="fillForm('flooded_road')">
|
|
|
|
<i class="fas fa-water"></i>
|
|
|
|
Flooded Road
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road roadwork" onclick="fillForm('roadwork')">
|
|
|
|
<i class="fas fa-hard-hat"></i>
|
|
|
|
Roadwork
|
|
|
|
</div>
|
|
|
|
<div class="issue-button road black-traffic" onclick="fillForm('black_traffic')">
|
|
|
|
<i class="fas fa-traffic-light"></i>
|
|
|
|
Black Traffic
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Rail Tab -->
|
|
|
|
<div class="tab-pane" id="rail-tab">
|
|
|
|
<div class="issue-buttons">
|
|
|
|
<div class="issue-button rail unattended-luggage" onclick="fillForm('unattended_luggage')">
|
|
|
|
<i class="fas fa-suitcase"></i>
|
|
|
|
Unattended Luggage
|
|
|
|
</div>
|
|
|
|
<div class="issue-button rail transport-delay" onclick="fillForm('transport_delay')">
|
|
|
|
<i class="fas fa-hourglass-half"></i>
|
|
|
|
Delay
|
|
|
|
</div>
|
|
|
|
<div class="issue-button rail major-delay" onclick="fillForm('major_transport_delay')">
|
|
|
|
<i class="fas fa-hourglass-end"></i>
|
|
|
|
Major Delay
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Weather Tab -->
|
|
|
|
<div class="tab-pane" id="weather-tab">
|
|
|
|
<div class="issue-buttons">
|
|
|
|
<div class="issue-button weather flood-danger" onclick="fillForm('flood_danger')">
|
|
|
|
<i class="fas fa-water"></i>
|
|
|
|
Flood Alert
|
|
|
|
</div>
|
|
|
|
<div class="issue-button weather thunderstorm" onclick="fillForm('thunderstorm_alert')">
|
|
|
|
<i class="fas fa-bolt"></i>
|
|
|
|
Thunderstorm
|
|
|
|
</div>
|
|
|
|
<div class="issue-button weather fog" onclick="fillForm('fog_warning')">
|
|
|
|
<i class="fas fa-smog"></i>
|
|
|
|
Fog Warning
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Emergency Tab -->
|
|
|
|
<div class="tab-pane" id="emergency-tab">
|
|
|
|
<div class="issue-buttons">
|
|
|
|
<div class="issue-button emergency emergency-alert" onclick="fillForm('emergency_alert')">
|
|
|
|
<i class="fas fa-exclamation-circle"></i>
|
|
|
|
Emergency Alert
|
|
|
|
</div>
|
|
|
|
<div class="issue-button emergency daylight-saving" onclick="fillForm('daylight_saving')">
|
|
|
|
<i class="fas fa-clock"></i>
|
|
|
|
Daylight Saving
|
|
|
|
</div>
|
|
|
|
</div>
|
2025-09-21 16:57:24 +02:00
|
|
|
</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>
|
|
|
|
|
2025-09-21 19:18:43 +02:00
|
|
|
<button id="report_issue_button" type="submit" disabled>Report Issue</button>
|
2025-09-21 16:57:24 +02:00
|
|
|
</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();
|
|
|
|
|
2025-09-21 19:18:43 +02:00
|
|
|
// Tab switching functionality
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// Get all tab items
|
|
|
|
const tabItems = document.querySelectorAll('.tab-item');
|
|
|
|
|
|
|
|
// Add click event listener to each tab item
|
|
|
|
tabItems.forEach(tab => {
|
|
|
|
tab.addEventListener('click', function() {
|
|
|
|
// Remove active class from all tab items
|
|
|
|
tabItems.forEach(item => item.classList.remove('active'));
|
|
|
|
|
|
|
|
// Add active class to clicked tab item
|
|
|
|
this.classList.add('active');
|
|
|
|
|
|
|
|
// Get the tab name from data-tab attribute
|
|
|
|
const tabName = this.getAttribute('data-tab');
|
|
|
|
|
|
|
|
// Get all tab panes
|
|
|
|
const tabPanes = document.querySelectorAll('.tab-pane');
|
|
|
|
|
|
|
|
// Remove active class from all tab panes
|
|
|
|
tabPanes.forEach(pane => pane.classList.remove('active'));
|
|
|
|
|
|
|
|
// Add active class to the corresponding tab pane
|
|
|
|
document.getElementById(tabName + '-tab').classList.add('active');
|
|
|
|
|
|
|
|
// Save active tab to localStorage
|
|
|
|
localStorage.setItem('activeTab', tabName);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Restore active tab from localStorage
|
|
|
|
const activeTab = localStorage.getItem('activeTab');
|
|
|
|
if (activeTab) {
|
|
|
|
// Find the tab item with the saved tab name
|
|
|
|
const tabItem = document.querySelector(`.tab-item[data-tab="${activeTab}"]`);
|
|
|
|
if (tabItem) {
|
|
|
|
// Trigger click event on the tab item
|
|
|
|
tabItem.click();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-09-21 16:57:24 +02:00
|
|
|
// 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
|
|
|
|
});
|
2025-09-21 19:18:43 +02:00
|
|
|
|
2025-09-21 16:57:24 +02:00
|
|
|
// Add navigation controls
|
|
|
|
map.addControl(new maplibregl.NavigationControl());
|
2025-09-21 19:18:43 +02:00
|
|
|
|
2025-09-21 16:57:24 +02:00
|
|
|
// Add marker for issue location
|
|
|
|
let marker = new maplibregl.Marker({
|
|
|
|
draggable: true,
|
|
|
|
color: '#ff3860' // Red color for traffic jam
|
|
|
|
});
|
2025-09-21 19:18:43 +02:00
|
|
|
|
|
|
|
// Store existing traffic event markers
|
|
|
|
let existingMarkers = [];
|
|
|
|
|
|
|
|
// Function to fetch and display existing traffic events
|
|
|
|
function fetchExistingTrafficEvents() {
|
|
|
|
// Clear existing markers first
|
|
|
|
existingMarkers.forEach(marker => marker.remove());
|
|
|
|
existingMarkers = [];
|
|
|
|
|
|
|
|
// Fetch traffic events from the API
|
|
|
|
fetch('https://api.openeventdatabase.org/event?what=traffic')
|
|
|
|
.then(response => {
|
|
|
|
if (response.ok) {
|
|
|
|
return response.json();
|
|
|
|
} else {
|
|
|
|
throw new Error('Failed to fetch existing traffic events');
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.then(data => {
|
|
|
|
if (data && data.features && Array.isArray(data.features)) {
|
|
|
|
// Add markers for each event
|
|
|
|
data.features.forEach(event => {
|
|
|
|
if (event.geometry && event.geometry.type === 'Point') {
|
|
|
|
const coords = event.geometry.coordinates;
|
|
|
|
// Create a gray marker for existing events
|
|
|
|
const eventMarker = new maplibregl.Marker({
|
|
|
|
color: '#888888' // Gray color for existing events
|
|
|
|
})
|
|
|
|
.setLngLat(coords)
|
|
|
|
.addTo(map);
|
|
|
|
|
|
|
|
// Add popup with event details
|
|
|
|
const popup = new maplibregl.Popup({ offset: 25 })
|
|
|
|
.setHTML(`
|
|
|
|
<h3>${event.properties.label || 'Traffic Event'}</h3>
|
|
|
|
<p>Type: ${event.properties.what || 'Unknown'}</p>
|
|
|
|
<p>Start: ${event.properties.start || 'Unknown'}</p>
|
|
|
|
<p>End: ${event.properties.stop || 'Unknown'}</p>
|
|
|
|
`);
|
|
|
|
|
|
|
|
eventMarker.setPopup(popup);
|
|
|
|
|
|
|
|
// Store marker reference for later removal
|
|
|
|
existingMarkers.push(eventMarker);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
console.log(`Loaded ${existingMarkers.length} existing traffic events`);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(error => {
|
|
|
|
console.error('Error fetching traffic events:', error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// Fetch existing events when the map loads
|
|
|
|
map.on('load', fetchExistingTrafficEvents);
|
2025-09-21 16:57:24 +02:00
|
|
|
|
|
|
|
// 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';
|
2025-09-21 19:18:43 +02:00
|
|
|
issueTypeInput.value = 'traffic.hazard.pothole';
|
2025-09-21 16:57:24 +02:00
|
|
|
severitySelect.value = 'medium';
|
|
|
|
markerColor = '#ff9800';
|
|
|
|
break;
|
|
|
|
case 'obstacle':
|
|
|
|
labelInput.value = 'Obstacle';
|
2025-09-21 19:18:43 +02:00
|
|
|
issueTypeInput.value = 'traffic.hazard.obstacle';
|
2025-09-21 16:57:24 +02:00
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#f44336';
|
|
|
|
break;
|
|
|
|
case 'vehicle':
|
|
|
|
labelInput.value = 'Véhicule sur le bas côté de la route';
|
2025-09-21 19:18:43 +02:00
|
|
|
issueTypeInput.value = 'traffic.hazard.vehicle';
|
2025-09-21 16:57:24 +02:00
|
|
|
severitySelect.value = 'low';
|
|
|
|
markerColor = '#2196f3';
|
|
|
|
break;
|
|
|
|
case 'danger':
|
|
|
|
labelInput.value = 'Danger non classé';
|
2025-09-21 19:18:43 +02:00
|
|
|
issueTypeInput.value = 'traffic.hazard.danger';
|
2025-09-21 16:57:24 +02:00
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#9c27b0';
|
|
|
|
break;
|
2025-09-21 19:18:43 +02:00
|
|
|
case 'emergency_alert':
|
|
|
|
labelInput.value = 'Alerte d\'urgence (SAIP)';
|
|
|
|
issueTypeInput.value = 'alert.emergency';
|
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#e91e63'; // Pink
|
|
|
|
break;
|
|
|
|
case 'daylight_saving':
|
|
|
|
labelInput.value = 'Période d\'heure d\'été';
|
|
|
|
issueTypeInput.value = 'time.daylight.summer';
|
|
|
|
severitySelect.value = 'low';
|
|
|
|
markerColor = '#ffc107'; // Amber
|
|
|
|
break;
|
|
|
|
case 'accident':
|
|
|
|
labelInput.value = 'Accident de la route';
|
|
|
|
issueTypeInput.value = 'traffic.accident';
|
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#d32f2f'; // Dark red
|
|
|
|
break;
|
|
|
|
case 'flooded_road':
|
|
|
|
labelInput.value = 'Route inondée';
|
|
|
|
issueTypeInput.value = 'traffic.closed.flood';
|
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#1976d2'; // Blue
|
|
|
|
break;
|
|
|
|
case 'black_traffic':
|
|
|
|
labelInput.value = 'Période noire bison futé vers la province';
|
|
|
|
issueTypeInput.value = 'traffic.forecast.black.out';
|
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#212121'; // Black
|
|
|
|
break;
|
|
|
|
case 'roadwork':
|
|
|
|
labelInput.value = 'Travaux';
|
|
|
|
issueTypeInput.value = 'traffic.roadwork';
|
|
|
|
severitySelect.value = 'medium';
|
|
|
|
markerColor = '#ff5722'; // Deep orange
|
|
|
|
break;
|
|
|
|
case 'flood_danger':
|
|
|
|
labelInput.value = 'Vigilance rouge inondation';
|
|
|
|
issueTypeInput.value = 'weather.danger.flood';
|
|
|
|
severitySelect.value = 'high';
|
|
|
|
markerColor = '#b71c1c'; // Dark red
|
|
|
|
break;
|
|
|
|
case 'thunderstorm_alert':
|
|
|
|
labelInput.value = 'Vigilance orange orages';
|
|
|
|
issueTypeInput.value = 'weather.alert.thunderstorm';
|
|
|
|
severitySelect.value = 'medium';
|
|
|
|
markerColor = '#ff9800'; // Orange
|
|
|
|
break;
|
|
|
|
case 'fog_warning':
|
|
|
|
labelInput.value = 'Vigilance jaune brouillard';
|
|
|
|
issueTypeInput.value = 'weather.warning.fog';
|
|
|
|
severitySelect.value = 'low';
|
|
|
|
markerColor = '#ffeb3b'; // Yellow
|
|
|
|
break;
|
|
|
|
case 'unattended_luggage':
|
|
|
|
labelInput.value = 'Bagage abandonné';
|
|
|
|
issueTypeInput.value = 'public_transport.incident.unattended_luggage';
|
|
|
|
severitySelect.value = 'medium';
|
|
|
|
markerColor = '#673ab7'; // Deep purple
|
|
|
|
break;
|
|
|
|
case 'transport_delay':
|
|
|
|
labelInput.value = 'Retard';
|
|
|
|
issueTypeInput.value = 'public_transport.delay';
|
|
|
|
severitySelect.value = 'low';
|
|
|
|
markerColor = '#ffc107'; // Amber
|
|
|
|
break;
|
|
|
|
case 'major_transport_delay':
|
|
|
|
labelInput.value = 'Retard important';
|
|
|
|
issueTypeInput.value = 'public_transport.delay.major';
|
|
|
|
severitySelect.value = 'medium';
|
|
|
|
markerColor = '#ff5722'; // Deep orange
|
|
|
|
break;
|
2025-09-21 16:57:24 +02:00
|
|
|
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);
|
|
|
|
}
|
2025-09-21 19:18:43 +02:00
|
|
|
|
|
|
|
// Validate form after filling in values
|
|
|
|
validateForm();
|
2025-09-21 16:57:24 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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');
|
|
|
|
|
2025-09-21 19:18:43 +02:00
|
|
|
// Validate form after setting marker
|
|
|
|
validateForm();
|
|
|
|
|
2025-09-21 16:57:24 +02:00
|
|
|
// 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');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-09-21 19:18:43 +02:00
|
|
|
// Form validation function
|
|
|
|
function validateForm() {
|
|
|
|
// Get all required form fields
|
|
|
|
const requiredFields = document.querySelectorAll('#trafficForm [required]');
|
|
|
|
const submitButton = document.getElementById('report_issue_button');
|
|
|
|
|
|
|
|
// Check if all required fields are filled
|
|
|
|
let isValid = true;
|
|
|
|
|
|
|
|
// Check each required field
|
|
|
|
requiredFields.forEach(field => {
|
|
|
|
if (!field.value.trim()) {
|
|
|
|
isValid = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Check if marker is set
|
|
|
|
if (!marker || !marker.getLngLat()) {
|
|
|
|
isValid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enable or disable submit button based on form validity
|
|
|
|
submitButton.disabled = !isValid;
|
|
|
|
|
|
|
|
// Add or remove disabled class
|
|
|
|
if (isValid) {
|
|
|
|
submitButton.classList.remove('disabled');
|
|
|
|
} else {
|
|
|
|
submitButton.classList.add('disabled');
|
|
|
|
}
|
|
|
|
|
|
|
|
return isValid;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add event listeners to form fields to trigger validation
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
|
// Get all form fields
|
|
|
|
const formFields = document.querySelectorAll('#trafficForm input, #trafficForm select');
|
|
|
|
|
|
|
|
// Add input event listener to each field
|
|
|
|
formFields.forEach(field => {
|
|
|
|
field.addEventListener('input', validateForm);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add change event listener to each field
|
|
|
|
formFields.forEach(field => {
|
|
|
|
field.addEventListener('change', validateForm);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Validate form on page load
|
|
|
|
validateForm();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Update validation when marker is set
|
|
|
|
map.on('click', function() {
|
|
|
|
// Validate form after marker is set
|
|
|
|
setTimeout(validateForm, 100);
|
|
|
|
});
|
|
|
|
|
2025-09-21 16:57:24 +02:00
|
|
|
// Handle form submission
|
|
|
|
document.getElementById('trafficForm').addEventListener('submit', function(e) {
|
|
|
|
e.preventDefault();
|
|
|
|
|
2025-09-21 19:18:43 +02:00
|
|
|
// Validate form before submission
|
|
|
|
if (!validateForm()) {
|
|
|
|
showResult('Please fill in all required fields and set a location on the map', 'error');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-09-21 16:57:24 +02:00
|
|
|
// 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;
|
|
|
|
|
2025-09-21 19:18:43 +02:00
|
|
|
// Check if marker is set (redundant but kept for safety)
|
2025-09-21 16:57:24 +02:00
|
|
|
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
|
2025-09-21 19:18:43 +02:00
|
|
|
fetch('https://api.openeventdatabase.org/event', {
|
2025-09-21 16:57:24 +02:00
|
|
|
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>
|
2025-09-21 19:18:43 +02:00
|
|
|
<a href="https://api.openeventdatabase.org/event/${data.id}" >View Report on Server</a> |
|
2025-09-21 17:30:47 +02:00
|
|
|
<a href="/demo/view-events" >View Saved Reports</a> |
|
2025-09-21 16:57:24 +02:00
|
|
|
<a href="/demo">Back to Map</a>
|
|
|
|
</p>`;
|
|
|
|
|
|
|
|
// Reset form
|
|
|
|
document.getElementById('trafficForm').reset();
|
|
|
|
// Set default dates again
|
|
|
|
setDefaultDates();
|
|
|
|
// Remove marker
|
|
|
|
marker.remove();
|
2025-09-21 19:18:43 +02:00
|
|
|
|
|
|
|
// Refresh the map to show the new event along with existing ones
|
|
|
|
fetchExistingTrafficEvents();
|
2025-09-21 16:57:24 +02:00
|
|
|
})
|
|
|
|
.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>
|