oedb-backend/oedb/resources/demo/templates/traffic.html
2025-09-23 11:51:54 +02:00

1224 lines
No EOL
55 KiB
HTML

<!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>
<link rel="stylesheet" href="/static/traffic.css">
<script src="/static/demo_auth.js"></script>
<script src="/static/traffic.js" defer></script>
</head>
<body>
<div class="container">
{% include 'partials/demo_nav.html' %}
<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 }}">
<!-- Hidden Panoramax configuration (upload endpoint and token) -->
<input type="hidden" id="panoramaxUploadUrl" value="{{ panoramax_upload_url }}">
<input type="hidden" id="panoramaxToken" value="{{ panoramax_token }}">
<!-- 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 }}" >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>
// Préserve l'affichage serveur si présent, sinon laisse traffic.js/dema_auth gérer
document.addEventListener('DOMContentLoaded', function() {
const hasServerAuth = document.getElementById('osmUsername') && document.getElementById('osmUsername').value;
if (hasServerAuth) return;
if (window.osmAuth && osmAuth.renderAuthSection) {
const clientId = document.getElementById('osmClientId').value;
const redirectUri = document.getElementById('osmRedirectUri').value;
const authSection = document.getElementById('auth-section');
authSection.innerHTML = osmAuth.renderAuthSection(clientId, redirectUri);
}
});
</script>
</div>
<h3>Select Issue Type</h3>
<!-- Tab Navigation -->
<div class="tabs">
<div class="tab-item active" data-tab="road">
<i class="fas fa-road"></i> Route
</div>
<div class="tab-item" data-tab="rail">
<i class="fas fa-train"></i> Rail
</div>
<div class="tab-item" data-tab="weather">
<i class="fas fa-cloud-sun-rain"></i> Météo
</div>
<div class="tab-item" data-tab="emergency">
<i class="fas fa-exclamation-circle"></i> Urgences
</div>
<div class="tab-item" data-tab="civic">
<i class="fas fa-bicycle"></i> Cycles
</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>
Véhicule sur le bas côté de la route
</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>
Route inondée
</div>
<div class="issue-button road roadwork" onclick="fillForm('roadwork')">
<i class="fas fa-hard-hat"></i>
Travaux
</div>
<div class="issue-button road black-traffic" onclick="fillForm('black_traffic')">
<i class="fas fa-traffic-light"></i>
journée noire bison futé
</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>
Bagage abandonné
</div>
<div class="issue-button rail transport-delay" onclick="fillForm('transport_delay')">
<i class="fas fa-hourglass-half"></i>
Retard
</div>
<div class="issue-button rail major-delay" onclick="fillForm('major_transport_delay')">
<i class="fas fa-hourglass-end"></i>
Retard important
</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>
Vigilance rouge inondation
</div>
<div class="issue-button weather thunderstorm" onclick="fillForm('thunderstorm_alert')">
<i class="fas fa-bolt"></i>
Vigilance orange orages
</div>
<div class="issue-button weather fog" onclick="fillForm('fog_warning')">
<i class="fas fa-smog"></i>
Vigilance jaune brouillard
</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>
Alerte d'urgence (SAIP)
</div>
<div class="issue-button emergency daylight-saving" onclick="fillForm('daylight_saving')">
<i class="fas fa-clock"></i>
Période d'heure d'été
</div>
</div>
</div>
<!-- Civic Tab -->
<div class="tab-pane" id="civic-tab">
<div class="issue-buttons">
<div class="issue-button civic bike-obstacle" onclick="fillForm('bike_obstacle')">
<i class="fas fa-bicycle"></i>
Obstacle vélo
</div>
<div class="issue-button civic illegal-dumping" onclick="fillForm('illegal_dumping')">
<i class="fas fa-trash"></i>
Décharge sauvage
</div>
</div>
</div>
</div>
<button id="geolocateBtn" class="geolocation-btn">
<span id="geolocateSpinner" class="loading" style="display: none;"></span>
Obtenir ma position actuelle
</button>
<span id="gpsStatus" class="gps-status" title="État GPS">GPS: inconnu</span>
<form id="trafficForm">
<div class="form-group">
<!-- <label for="photo">Photo (optionnelle)</label>-->
<!-- <input type="file" id="photo" name="photo" accept="image/*" capture="environment">-->
<!-- <div class="note">Prenez une photo géolocalisée de la situation (mobile recommandé)</div>-->
<!-- <div id="photoPreviewContainer" style="margin-top:8px; display:none;">-->
<!-- <img id="photoPreview" alt="Aperçu photo" style="max-width:100%; border-radius:4px;"/>-->
<!-- </div>-->
<!-- <div class="form-row" style="margin-top:8px;">-->
<!-- <div class="form-group">-->
<!-- <label for="panoramaxTokenInput">Token Panoramax</label>-->
<!-- <input type="password" id="panoramaxTokenInput" placeholder="Jeton d'API Panoramax">-->
<!-- <div class="note">Stocké en local sur cet appareil. Utilisé pour envoyer la photo.</div>-->
<!-- </div>-->
<!-- <div class="form-group" style="align-self:flex-end;">-->
<!-- <button type="button" id="savePanoramaxTokenBtn">Enregistrer le token</button>-->
<!-- <button type="button" id="showPanoramaxTokenBtn" style="display:none;">Modifier le token</button>-->
<!-- </div>-->
<!-- </div>-->
<!-- <div class="camera-block">-->
<!-- <label>Prendre une photo avec la caméra</label>-->
<!-- <div class="camera-controls">-->
<!-- <button type="button" id="startCameraBtn">Démarrer la caméra</button>-->
<!-- <button type="button" id="capturePhotoBtn" disabled>Prendre la photo</button>-->
<!-- <button type="button" id="stopCameraBtn" disabled>Arrêter</button>-->
<!-- </div>-->
<!-- <div class="camera-preview">-->
<!-- <video id="cameraVideo" autoplay playsinline muted></video>-->
<!-- <canvas id="cameraCanvas" style="display:none;"></canvas>-->
<!-- </div>-->
<!-- <div class="note">La photo capturée sera ajoutée au champ ci-dessus.</div>-->
<!-- </div>-->
<!-- </div>-->
<div class="form-group">
<label for="label" class="required">Description du problème</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">Gravité</label>
<select id="severity" name="severity" required>
<option value="low">Faible (Problème mineur)</option>
<option value="medium" selected>Moyen (Problème modéré)</option>
<option value="high">Élevé (Problème grave)</option>
</select>
</div>
<div class="form-group">
<label for="cause">Détails supplémentaires</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">Heure de début</label>
<input type="datetime-local" id="start" name="start" required value="">
</div>
<div class="form-group">
<label for="stop" class="required">Heure estimée de fin</label>
<input type="datetime-local" id="stop" name="stop" required value="">
</div>
</div>
<div class="form-group">
<label for="where">Route/Nom du lieu</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">Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"</div>
</div>
<button id="report_issue_button" type="submit" disabled>Signaler le problème</button>
</form>
<div id="result"></div>
<a href="/demo/view-events" class="view-saved-events">
<i class="fas fa-map-marked-alt"></i> Voir tous les événements enregistrés sur la carte
</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 + 6 hours (durée par défaut des signalements)
const sixHoursLater = new Date(now.getTime() + 6 * 60 * 60 * 1000);
document.getElementById('stop').value = sixHoursLater.toISOString().slice(0, 16);
}
// Call function to set default dates
setDefaultDates();
// 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();
}
}
});
// Initialize the map
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
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
});
// 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;
// Check if this event needs reality check (created more than 1 hour ago)
const needsRealityCheck = checkIfNeedsRealityCheck(event);
// Create a marker for existing events (gray for regular, orange for those needing reality check)
const markerColor = needsRealityCheck ? '#ff9800' : '#888888';
const eventMarker = new maplibregl.Marker({
color: markerColor
})
.setLngLat(coords)
.addTo(map);
// Add popup with event details
let popupContent = `
<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>
`;
// Add reality check buttons if needed
if (needsRealityCheck) {
popupContent += `
<div class="reality-check">
<p>Is this traffic event still present?</p>
<div class="reality-check-buttons">
<button class="confirm-btn" onclick="confirmEvent('${event.properties.id}', true)">Yes, still there</button>
<button class="deny-btn" onclick="confirmEvent('${event.properties.id}', false)">No, it's gone</button>
</div>
</div>
`;
} else if (event.properties['reality_check']) {
// Show reality check information if it exists
popupContent += `
<div class="reality-check-info">
<p>Reality check: ${event.properties['reality_check']}</p>
</div>
`;
}
const popup = new maplibregl.Popup({ offset: 25 })
.setHTML(popupContent);
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);
});
}
// Function to check if an event needs a reality check (created more than 1 hour ago)
function checkIfNeedsRealityCheck(event) {
// Skip if event already has a reality check
if (event.properties['reality_check']) {
return false;
}
// Check if the event is a traffic event
if (!event.properties.what || !event.properties.what.startsWith('traffic')) {
return false;
}
// Check creation date
const createDate = event.properties.createdate;
if (!createDate) {
return false;
}
const createTime = new Date(createDate).getTime();
const currentTime = new Date().getTime();
const oneHourInMs = 60 * 60 * 1000;
// Return true if the event was created more than 1 hour ago
return (currentTime - createTime) > oneHourInMs;
}
// Fetch existing events when the map loads
map.on('load', fetchExistingTrafficEvents);
// 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 'bike_obstacle':
labelInput.value = 'Obstacle vélo';
issueTypeInput.value = 'mobility.cycling.obstacle';
severitySelect.value = 'medium';
markerColor = '#388e3c'; // Green
break;
case 'illegal_dumping':
labelInput.value = 'Décharge sauvage';
issueTypeInput.value = 'environment.dumping.illegal';
severitySelect.value = 'medium';
markerColor = '#795548'; // Brown
break;
case 'pothole':
labelInput.value = 'Nid de poule';
issueTypeInput.value = 'traffic.hazard.pothole';
severitySelect.value = 'medium';
markerColor = '#ff9800';
break;
case 'obstacle':
labelInput.value = 'Obstacle';
issueTypeInput.value = 'traffic.hazard.obstacle';
severitySelect.value = 'high';
markerColor = '#f44336';
break;
case 'vehicle':
labelInput.value = 'Véhicule sur le bas côté de la route';
issueTypeInput.value = 'traffic.hazard.vehicle';
severitySelect.value = 'low';
markerColor = '#2196f3';
break;
case 'danger':
labelInput.value = 'Danger non classé';
issueTypeInput.value = 'traffic.hazard.danger';
severitySelect.value = 'high';
markerColor = '#9c27b0';
break;
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;
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);
}
// Validate form after filling in values
validateForm();
}
// 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');
// Validate form after setting marker
validateForm();
// 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');
}
});
// 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);
});
// Photo preview
const photoInput = document.getElementById('photo');
if (photoInput) {
photoInput.addEventListener('change', function() {
const file = this.files && this.files[0];
if (!file) {
document.getElementById('photoPreviewContainer').style.display = 'none';
return;
}
const url = URL.createObjectURL(file);
const img = document.getElementById('photoPreview');
img.src = url;
document.getElementById('photoPreviewContainer').style.display = 'block';
});
}
async function uploadPhotoIfConfigured(file, lng, lat, isoDatetime) {
try {
const uploadUrl = document.getElementById('panoramaxUploadUrl')?.value || '';
const token = document.getElementById('panoramaxToken')?.value || '';
if (!uploadUrl || !file) {
return null; // pas configuré ou pas de fichier
}
const form = new FormData();
form.append('file', file, file.name || 'photo.jpg');
// Métadonnées géo/temps standard
if (typeof lng === 'number' && typeof lat === 'number') {
form.append('lon', String(lng));
form.append('lat', String(lat));
}
if (isoDatetime) {
form.append('datetime', isoDatetime);
}
const headers = {};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const res = await fetch(uploadUrl, { method: 'POST', headers, body: form });
if (!res.ok) {
const t = await res.text();
throw new Error(t || `Upload failed (${res.status})`);
}
const data = await res.json().catch(() => ({}));
// On essaie de normaliser quelques champs courants
return {
id: data.id || data.uuid || data.photo_id || null,
url: data.url || data.permalink || data.link || null,
raw: data
};
} catch (err) {
console.error('Panoramax upload error:', err);
showResult(`Erreur upload photo: ${err.message}`, 'error');
return null;
}
}
// Handle form submission
document.getElementById('trafficForm').addEventListener('submit', async function(e) {
e.preventDefault();
// Validate form before submission
if (!validateForm()) {
showResult('Please fill in all required fields and set a location on the map', 'error');
return;
}
// 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 (redundant but kept for safety)
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}`);
}
// Upload photo to Panoramax si configuré
let photoInfo = null;
const photoFile = (photoInput && photoInput.files && photoInput.files[0]) ? photoInput.files[0] : null;
if (photoFile) {
photoInfo = await uploadPhotoIfConfigured(photoFile, lngLat.lng, lngLat.lat, start);
if (photoInfo) {
event.properties['photo:service'] = 'panoramax';
if (photoInfo.id) event.properties['photo:id'] = String(photoInfo.id);
if (photoInfo.url) event.properties['photo:url'] = photoInfo.url;
}
}
// Save event to localStorage
saveEventToLocalStorage(event);
// Submit event to API
fetch('https://api.openeventdatabase.org/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="https://api.openeventdatabase.org/event/${data.id}" >View Report on Server</a> |
<a href="/demo/view-events" >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();
// Refresh the map to show the new event along with existing ones
fetchExistingTrafficEvents();
})
.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' });
}
// Function to handle event confirmation or denial
function confirmEvent(eventId, isConfirmed) {
// Get username from localStorage or prompt for it
let username = localStorage.getItem('oedb_username');
if (!username) {
username = promptForUsername();
if (!username) {
// User cancelled the prompt
return;
}
}
// Current date and time
const now = new Date();
const dateTimeString = now.toISOString();
// Create reality check string
const realityCheckStatus = isConfirmed ? 'confirmed' : 'not confirmed';
const realityCheckValue = `${dateTimeString} | ${username} | ${realityCheckStatus}`;
// Fetch the event to update
fetch(`https://api.openeventdatabase.org/event/${eventId}`)
.then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error(`Failed to fetch event ${eventId}`);
}
})
.then(event => {
// Add reality_check property
event.properties['reality_check'] = realityCheckValue;
// Update the event
return fetch(`https://api.openeventdatabase.org/event/${eventId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(event)
});
})
.then(response => {
if (response.ok) {
// Save contribution to localStorage
saveContribution(eventId, isConfirmed);
// Award points
awardPoints(3);
// Show success message
showResult(`Thank you for your contribution! You've earned 3 points.`, 'success');
// Update user info display
updateUserInfoDisplay();
// Refresh events to update the display
fetchExistingTrafficEvents();
} else {
throw new Error('Failed to update event');
}
})
.catch(error => {
console.error('Error updating event:', error);
showResult(`Error: ${error.message}`, 'error');
});
}
// Function to prompt for username
function promptForUsername() {
const username = prompt('Please enter your username:');
if (username) {
localStorage.setItem('oedb_username', username);
return username;
}
return null;
}
// Function to save contribution to localStorage
function saveContribution(eventId, isConfirmed) {
// Get existing contributions
let contributions = JSON.parse(localStorage.getItem('oedb_contributions') || '[]');
// Add new contribution
contributions.push({
eventId: eventId,
timestamp: new Date().toISOString(),
isConfirmed: isConfirmed
});
// Save back to localStorage
localStorage.setItem('oedb_contributions', JSON.stringify(contributions));
}
// Function to award points
function awardPoints(points) {
// Get current points
let currentPoints = parseInt(localStorage.getItem('oedb_points') || '0');
// Add new points
currentPoints += points;
// Save back to localStorage
localStorage.setItem('oedb_points', currentPoints.toString());
}
// Function to update user info display in side panel
function updateUserInfoDisplay() {
const username = localStorage.getItem('oedb_username') || 'Anonymous';
const points = localStorage.getItem('oedb_points') || '0';
// Check if user info panel exists, create it if not
let userInfoPanel = document.getElementById('user-info-panel');
if (!userInfoPanel) {
// Create user info panel
userInfoPanel = document.createElement('div');
userInfoPanel.id = 'user-info-panel';
userInfoPanel.className = 'user-info-panel';
// Add it to the page (after the nav-links)
const navLinks = document.querySelector('.nav-links');
navLinks.parentNode.insertBefore(userInfoPanel, navLinks.nextSibling);
// Add some CSS for the panel
const style = document.createElement('style');
style.textContent = `
.user-info-panel {
background-color: #f5f5f5;
border-radius: 4px;
padding: 10px;
margin: 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-info-panel h3 {
margin-top: 0;
margin-bottom: 10px;
color: #333;
}
.user-info-panel p {
margin: 5px 0;
}
.user-points {
font-weight: bold;
color: #0078ff;
}
.reality-check {
margin-top: 10px;
padding: 10px;
background-color: #fff3e0;
border-radius: 4px;
}
.reality-check-buttons {
display: flex;
justify-content: space-between;
margin-top: 8px;
}
.confirm-btn, .deny-btn {
padding: 5px 10px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.confirm-btn {
background-color: #4caf50;
color: white;
}
.deny-btn {
background-color: #f44336;
color: white;
}
.reality-check-info {
margin-top: 10px;
padding: 8px;
background-color: #e8f5e9;
border-radius: 4px;
font-size: 0.9em;
}
`;
document.head.appendChild(style);
}
// Update the content
userInfoPanel.innerHTML = `
<!-- <h3>User Information</h3>-->
<p> <strong>${username}</strong></p>
<p> <span class="user-points">${points}</span> points</p>
`;
}
// Initialize user info display when page loads
document.addEventListener('DOMContentLoaded', function() {
updateUserInfoDisplay();
});
</script>
</body>
</html>