// Traffic.js - JavaScript for the traffic reporting page // Global variables let map; let marker; let currentPosition; let currentIssueType = null; let photoFiles = []; let panoramaxUploadUrl = ''; let panoramaxToken = ''; // Initialize the map when the page loads document.addEventListener('DOMContentLoaded', function () { initMap(); initTabs(); initForm(); setupFormValidation(); // Get Panoramax configuration const panoramaxUploadUrlElement = document.getElementById('panoramaxUploadUrl'); const panoramaxTokenElement = document.getElementById('panoramaxToken'); panoramaxUploadUrl = panoramaxUploadUrlElement ? panoramaxUploadUrlElement.value : ''; panoramaxToken = panoramaxTokenElement ? panoramaxTokenElement.value : ''; // Set up photo upload const photoInput = document.getElementById('photo'); if (photoInput) { photoInput.addEventListener('change', handlePhotoUpload); } // Initialize the map function initMap() { // Create the map 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 attribution control with OpenStreetMap attribution map.addControl(new maplibregl.AttributionControl({ customAttribution: '© OpenStreetMap contributors' })); // Try to get the user's current location if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function (position) { currentPosition = [position.coords.longitude, position.coords.latitude]; // Center the map on the user's location map.flyTo({ center: currentPosition, zoom: 12 }); // Add a marker at the user's location marker = new maplibregl.Marker({ draggable: true }) .setLngLat(currentPosition) .addTo(map); // Update coordinates when marker is dragged marker.on('dragend', updateCoordinates); // Update the coordinates display updateCoordinates(); }, function (error) { console.error('Error getting location:', error); } ); } // Add click handler to the map map.on('click', function (e) { // If we don't have a marker yet, create one if (!marker) { marker = new maplibregl.Marker({ draggable: true }) .setLngLat(e.lngLat) .addTo(map); // Update coordinates when marker is dragged marker.on('dragend', updateCoordinates); } else { // Otherwise, move the existing marker marker.setLngLat(e.lngLat); } // Update the coordinates display updateCoordinates(); }); } // Initialize the tabs function initTabs() { const tabItems = document.querySelectorAll('.tab-item'); const tabPanes = document.querySelectorAll('.tab-pane'); tabItems.forEach(item => { item.addEventListener('click', function () { // Remove active class from all tabs tabItems.forEach(tab => tab.classList.remove('active')); tabPanes.forEach(pane => pane.classList.remove('active')); // Add active class to clicked tab this.classList.add('active'); // Show the corresponding tab content const tabId = this.getAttribute('data-tab'); document.getElementById(tabId + '-tab').classList.add('active'); }); }); } // Initialize the form function initForm() { // Set the current date and time as the default const now = new Date(); const dateTimeString = now.toISOString().slice(0, 16); const startTimeInput = document.getElementById('start'); if (startTimeInput) { startTimeInput.value = dateTimeString; } const stopTimeInput = document.getElementById('stop'); if (stopTimeInput) { // Set default end time to 1 hour from now const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000); stopTimeInput.value = oneHourLater.toISOString().slice(0, 16); } // Set up form submission after DOM is loaded const reportForm = document.getElementById('trafficForm'); if (reportForm) { reportForm.addEventListener('submit', submitReport); } else { console.warn('Traffic form not found in DOM'); } } // Update the coordinates display when the marker is moved function updateCoordinates() { if (!marker) return; const lngLat = marker.getLngLat(); currentPosition = [lngLat.lng, lngLat.lat]; // Update the coordinates display const coordinatesElement = document.getElementById('coordinates'); if (coordinatesElement) { coordinatesElement.textContent = `${lngLat.lat.toFixed(6)}, ${lngLat.lng.toFixed(6)}`; } // Update the hidden coordinates input const coordinatesInput = document.getElementById('coordinates-input'); if (coordinatesInput) { coordinatesInput.value = JSON.stringify({ type: 'Point', coordinates: [lngLat.lng, lngLat.lat] }); } } // Handle photo upload function handlePhotoUpload(event) { const files = event.target.files; if (!files || files.length === 0) return; // Store the files for later upload photoFiles = Array.from(files); // Show preview of the photos const previewContainer = document.getElementById('photoPreview'); if (previewContainer) { previewContainer.innerHTML = ''; photoFiles.forEach(file => { const reader = new FileReader(); reader.onload = function (e) { const img = document.createElement('img'); img.src = e.target.result; img.className = 'photo-preview'; previewContainer.appendChild(img); }; reader.readAsDataURL(file); }); previewContainer.style.display = 'flex'; } } // Submit the report async function submitReport(event) { event.preventDefault(); // Show loading message const resultElement = document.getElementById('result'); resultElement.textContent = 'Submitting report...'; resultElement.className = ''; resultElement.style.display = 'block'; try { // Check if we have coordinates if (!currentPosition) { throw new Error('Please select a location on the map'); } // Get form values const formData = new FormData(event.target); const eventData = { type: formData.get('type') || 'unscheduled', what: formData.get('what'), label: formData.get('label'), description: formData.get('description'), start: new Date(formData.get('start')).toISOString(), stop: new Date(formData.get('stop')).toISOString(), geometry: { type: 'Point', coordinates: currentPosition } }; // Add username if authenticated const osmUsername = document.getElementById('osmUsername'); if (osmUsername && osmUsername.value) { eventData.source = { name: 'OpenStreetMap user', id: osmUsername.value }; } // Upload photos to Panoramax if available let photoUrls = []; if (photoFiles.length > 0 && panoramaxUploadUrl && panoramaxToken) { photoUrls = await uploadPhotos(photoFiles); if (photoUrls.length > 0) { eventData.media = photoUrls.map(url => ({ type: 'image', url: url })); } } // Submit the event to the API const response = await fetch('/event', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(eventData) }); if (!response.ok) { const errorText = await response.text(); throw new Error(errorText || response.statusText); } const data = await response.json(); // Show success message resultElement.textContent = `Report submitted successfully! Event ID: ${data.id}`; resultElement.className = 'success'; // Reset the form event.target.reset(); photoFiles = []; const previewContainer = document.getElementById('photoPreview'); if (previewContainer) { previewContainer.innerHTML = ''; previewContainer.style.display = 'none'; } // Initialize the form again initForm(); } catch (error) { // Show error message resultElement.textContent = `Error: ${error.message}`; resultElement.className = 'error'; } } // Upload photos to Panoramax async function uploadPhotos(files) { const urls = []; for (const file of files) { try { const uploadData = new FormData(); uploadData.append('file', file); const response = await fetch(panoramaxUploadUrl, { method: 'POST', headers: { 'Authorization': `Token ${panoramaxToken}` }, body: uploadData }); if (!response.ok) { console.error('Failed to upload photo:', await response.text()); continue; } const data = await response.json(); if (data.url) { urls.push(data.url); } } catch (error) { console.error('Error uploading photo:', error); } } return urls; } // Setup form validation function setupFormValidation() { const form = document.getElementById('trafficForm'); if (!form) return; // Get all form fields that need validation const requiredFields = [ {id: 'label', name: 'Description du problème'}, {id: 'severity', name: 'Gravité'}, {id: 'start', name: 'Heure de début'}, {id: 'stop', name: 'Heure de fin'} ]; const optionalFields = [ {id: 'cause', name: 'Détails supplémentaires'}, {id: 'where', name: 'Route/Nom du lieu'} ]; // Add event listeners for real-time validation [...requiredFields, ...optionalFields].forEach(field => { const element = document.getElementById(field.id); if (element) { element.addEventListener('input', validateForm); element.addEventListener('change', validateForm); element.addEventListener('blur', validateForm); } }); // Add listener for map clicks (location validation) if (map) { map.on('click', () => { setTimeout(validateForm, 100); // Small delay to ensure marker is set }); } // Initial validation validateForm(); } }); // Clear error message for a field function clearFieldError(element) { const existingError = element.parentNode.querySelector('.field-error'); if (existingError) { existingError.remove(); } } // Validate the entire form function validateForm() { const form = document.getElementById('trafficForm'); const submitButton = document.getElementById('report_issue_button'); if (!form || !submitButton) { console.warn('🔍 Validation: Formulaire ou bouton de soumission non trouvé'); return false; } console.group('🔍 Validation du formulaire traffic'); let isValid = true; let firstInvalidField = null; const errors = []; const validFields = []; // Check required fields const requiredFields = [ {id: 'label', name: 'Description du problème', minLength: 3}, {id: 'severity', name: 'Gravité'}, {id: 'start', name: 'Heure de début'}, {id: 'stop', name: 'Heure de fin'} ]; console.log('📝 Vérification des champs requis...'); requiredFields.forEach(field => { const element = document.getElementById(field.id); if (element) { let fieldValid = true; let errorMessage = ''; const value = element.value.trim(); // Check if field is empty if (!value) { fieldValid = false; errorMessage = `${field.name} est requis`; console.error(`❌ ${field.name}: champ vide`); } // Check minimum length for text fields else if (field.minLength && value.length < field.minLength) { fieldValid = false; errorMessage = `${field.name} doit contenir au moins ${field.minLength} caractères`; console.error(`❌ ${field.name}: trop court (${value.length}/${field.minLength} caractères) - "${value}"`); } else { console.log(`✅ ${field.name}: OK - "${value}"`); validFields.push(field.name); } // Visual feedback if (fieldValid) { element.classList.remove('error'); element.classList.add('valid'); clearFieldError(element); } else { element.classList.remove('valid'); element.classList.add('error'); showFieldError(element, errorMessage); isValid = false; if (!firstInvalidField) { firstInvalidField = element; } errors.push(errorMessage); } } else { console.warn(`⚠️ ${field.name}: élément non trouvé dans le DOM`); } }); // Check date logic (start time should be before stop time) console.log('📅 Vérification de la logique des dates...'); const startElement = document.getElementById('start'); const stopElement = document.getElementById('stop'); if (startElement && stopElement && startElement.value && stopElement.value) { const startTime = new Date(startElement.value); const stopTime = new Date(stopElement.value); console.log(`📅 Heure début: ${startTime.toLocaleString()}`); console.log(`📅 Heure fin: ${stopTime.toLocaleString()}`); if (startTime >= stopTime) { stopElement.classList.remove('valid'); stopElement.classList.add('error'); showFieldError(stopElement, 'L\'heure de fin doit être après l\'heure de début'); isValid = false; if (!firstInvalidField) { firstInvalidField = stopElement; } errors.push('L\'heure de fin doit être après l\'heure de début'); console.error(`❌ Dates: L'heure de fin (${stopTime.toLocaleString()}) doit être après l'heure de début (${startTime.toLocaleString()})`); } else { console.log(`✅ Dates: Logique correcte (durée: ${Math.round((stopTime - startTime) / 1000 / 60)} minutes)`); validFields.push('Logique des dates'); } } else { console.warn('⚠️ Dates: Impossible de vérifier la logique (éléments ou valeurs manquants)'); } // Check if location is set (marker exists) console.log('📍 Vérification de la localisation...'); if (!marker || !marker.getLngLat()) { isValid = false; errors.push('Veuillez sélectionner une localisation sur la carte'); console.error('❌ Localisation: Aucun marqueur placé sur la carte'); // Highlight map container const mapContainer = document.getElementById('map'); if (mapContainer) { mapContainer.classList.add('error'); if (!firstInvalidField) { firstInvalidField = mapContainer; } } } else { const lngLat = marker.getLngLat(); console.log(`✅ Localisation: Marqueur placé à [${lngLat.lng.toFixed(6)}, ${lngLat.lat.toFixed(6)}]`); validFields.push('Localisation'); // Remove error highlight from map const mapContainer = document.getElementById('map'); if (mapContainer) { mapContainer.classList.remove('error'); } } // Update submit button state if (isValid) { submitButton.disabled = false; submitButton.classList.remove('disabled'); submitButton.textContent = 'Signaler le problème'; console.log(`🎉 VALIDATION RÉUSSIE! Tous les champs sont valides:`); validFields.forEach(field => console.log(` ✅ ${field}`)); console.log('🔓 Bouton de soumission débloqué'); } else { submitButton.disabled = true; submitButton.classList.add('disabled'); submitButton.textContent = `Signaler le problème (${errors.length} erreur${errors.length > 1 ? 's' : ''})`; console.warn(`⚠️ VALIDATION ÉCHOUÉE! ${errors.length} erreur${errors.length > 1 ? 's' : ''} trouvée${errors.length > 1 ? 's' : ''}:`); errors.forEach((error, index) => console.error(` ${index + 1}. ${error}`)); console.log('🔒 Bouton de soumission bloqué'); if (validFields.length > 0) { console.log(`Champs valides (${validFields.length}):`); validFields.forEach(field => console.log(` ✅ ${field}`)); } } // Focus on first invalid field if validation was triggered by user action if (!isValid && firstInvalidField && document.activeElement !== firstInvalidField) { // Only auto-focus if the user isn't currently typing in another field if (document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'SELECT') { firstInvalidField.focus(); console.log(`🎯 Focus placé sur le premier champ invalide: ${firstInvalidField.id}`); } } console.groupEnd(); return isValid; } // Fill the form with predefined values based on the selected issue type function fillForm(issueType) { currentIssueType = issueType; // Get the form elements const whatInput = document.getElementById('what'); const labelInput = document.getElementById('label'); const descriptionInput = document.getElementById('description'); // Set default values based on the issue type switch (issueType) { case 'pothole': whatInput.value = 'road.hazard.pothole'; labelInput.value = 'Nid de poule'; descriptionInput.value = 'Nid de poule sur la chaussée'; break; case 'obstacle': whatInput.value = 'road.hazard.obstacle'; labelInput.value = 'Obstacle sur la route'; descriptionInput.value = 'Obstacle sur la chaussée'; break; case 'vehicle': whatInput.value = 'road.hazard.vehicle'; labelInput.value = 'Véhicule sur le bas côté'; descriptionInput.value = 'Véhicule arrêté sur le bas côté de la route'; break; case 'danger': whatInput.value = 'road.hazard.danger'; labelInput.value = 'Danger sur la route'; descriptionInput.value = 'Situation dangereuse sur la route'; break; case 'accident': whatInput.value = 'road.accident'; labelInput.value = 'Accident de la route'; descriptionInput.value = 'Accident de la circulation'; break; case 'flooded_road': whatInput.value = 'road.hazard.flood'; labelInput.value = 'Route inondée'; descriptionInput.value = 'Route inondée, circulation difficile'; break; case 'roadwork': whatInput.value = 'road.works'; labelInput.value = 'Travaux routiers'; descriptionInput.value = 'Travaux en cours sur la chaussée'; break; case 'traffic_jam': whatInput.value = 'road.traffic.jam'; labelInput.value = 'Embouteillage'; descriptionInput.value = 'Circulation dense, embouteillage'; break; // Add more cases for other issue types } // Scroll to the form const formElement = document.getElementById('trafficForm'); if (formElement) { formElement.scrollIntoView({behavior: 'smooth'}); } } // Show error message for a field function showFieldError(element, message) { // Remove existing error message clearFieldError(element); // Create error message element const errorDiv = document.createElement('div'); errorDiv.className = 'field-error'; errorDiv.textContent = message; // Insert error message after the field element.parentNode.insertBefore(errorDiv, element.nextSibling); }