378 lines
No EOL
12 KiB
JavaScript
378 lines
No EOL
12 KiB
JavaScript
// Traffic.js - JavaScript for the traffic reporting page
|
|
|
|
// Global variables
|
|
let map;
|
|
let marker;
|
|
let geocoder;
|
|
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();
|
|
|
|
// Get Panoramax configuration
|
|
panoramaxUploadUrl = document.getElementById('panoramaxUploadUrl').value;
|
|
panoramaxToken = document.getElementById('panoramaxToken').value;
|
|
|
|
// Set up photo upload
|
|
const photoInput = document.getElementById('photo');
|
|
if (photoInput) {
|
|
photoInput.addEventListener('change', handlePhotoUpload);
|
|
}
|
|
|
|
// Set up form submission
|
|
const reportForm = document.getElementById('reportForm');
|
|
if (reportForm) {
|
|
reportForm.addEventListener('submit', submitReport);
|
|
}
|
|
});
|
|
|
|
// 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: '© <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> 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);
|
|
}
|
|
}
|
|
|
|
// 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]
|
|
});
|
|
}
|
|
}
|
|
|
|
// 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
|
|
document.getElementById('reportForm').scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
|
|
// 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;
|
|
} |