@@ -279,8 +280,8 @@ class DemoMainResource:
// Function to fetch events from the API
function fetchEvents() {
- // Fetch events from the API - using default behavior to get currently active events
- fetch('/event')
+ // Fetch events from the API - using the external API endpoint
+ fetch('https://api.openeventdatabase.org/event?')
.then(response => response.json())
.then(data => {
if (data.features && data.features.length > 0) {
@@ -349,6 +350,10 @@ class DemoMainResource:
displayValue = `
${JSON.stringify(value, null, 2)}
`;
} else if (typeof value === 'string' && value.startsWith('http')) {
displayValue = `${value}`;
+ } else if (typeof value === 'string' && (key === 'start' || key === 'stop' || key.includes('date') || key.includes('time'))) {
+ // For date fields, show both the original date and the relative time
+ const relativeTime = getRelativeTimeString(value);
+ displayValue = `${value} (il y a ${relativeTime})`;
} else {
displayValue = String(value);
}
@@ -425,6 +430,35 @@ class DemoMainResource:
});
}
+ // Function to calculate relative time (e.g., "2 hours 30 minutes ago")
+ function getRelativeTimeString(dateString) {
+ if (!dateString) return '';
+
+ // Parse the date string
+ const date = new Date(dateString);
+ if (isNaN(date.getTime())) return dateString; // Return original if invalid
+
+ // Calculate time difference in milliseconds
+ const now = new Date();
+ const diffMs = now - date;
+
+ // Convert to hours and minutes
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
+ const diffMinutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
+
+ // Format the relative time string
+ let relativeTime = '';
+ if (diffHours > 0) {
+ relativeTime += `${diffHours} heure${diffHours > 1 ? 's' : ''}`;
+ }
+ if (diffMinutes > 0 || diffHours === 0) {
+ if (diffHours > 0) relativeTime += ' ';
+ relativeTime += `${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}`;
+ }
+
+ return relativeTime || "à l instant";
+ }
+
// Function to fit map to events bounds
function fitMapToBounds(geojson) {
if (geojson.features.length === 0) return;
diff --git a/oedb/resources/demo/static/demo_styles.css b/oedb/resources/demo/static/demo_styles.css
index b19b586..2669fc9 100644
--- a/oedb/resources/demo/static/demo_styles.css
+++ b/oedb/resources/demo/static/demo_styles.css
@@ -418,4 +418,96 @@ button:hover {
.osm-login-btn{
padding: 1rem 2rem;
border-radius: 5px;
+}
+
+#report_issue_button{
+ position: fixed;
+ bottom: 0.5rem;
+ right: 0.5rem;
+}
+
+/* Tab styles */
+.tabs {
+ display: flex;
+ border-bottom: 1px solid #ddd;
+ margin-bottom: 20px;
+}
+
+.tab-item {
+ padding: 10px 20px;
+ cursor: pointer;
+ border: 1px solid transparent;
+ border-bottom: none;
+ margin-right: 5px;
+ border-radius: 5px 5px 0 0;
+ background-color: #f8f9fa;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.tab-item i {
+ font-size: 16px;
+}
+
+.tab-item:hover {
+ background-color: #e9ecef;
+}
+
+.tab-item.active {
+ background-color: white;
+ border-color: #ddd;
+ border-bottom-color: white;
+ margin-bottom: -1px;
+ font-weight: bold;
+}
+
+.tab-content {
+ margin-bottom: 20px;
+}
+
+.tab-pane {
+ display: none;
+}
+
+.tab-pane.active {
+ display: block;
+}
+
+/* Family-based colors for issue buttons */
+.issue-button.road {
+ background-color: #2196f3; /* Blue for road issues */
+ color: white;
+}
+
+.issue-button.rail {
+ background-color: #673ab7; /* Purple for rail issues */
+ color: white;
+}
+
+.issue-button.weather {
+ background-color: #ff9800; /* Orange for weather issues */
+ color: white;
+}
+
+.issue-button.emergency {
+ background-color: #f44336; /* Red for emergency issues */
+ color: white;
+}
+
+/* Disabled button styles */
+button:disabled,
+button[disabled] {
+ background-color: #cccccc !important;
+ color: #666666 !important;
+ cursor: not-allowed;
+ opacity: 0.7;
+}
+
+/* Invalid form field styles */
+input:invalid,
+select:invalid {
+ border-color: #f44336;
+ background-color: #fff8f8;
}
\ No newline at end of file
diff --git a/oedb/resources/demo/templates/traffic.html b/oedb/resources/demo/templates/traffic.html
index 39e74a0..651a1c0 100644
--- a/oedb/resources/demo/templates/traffic.html
+++ b/oedb/resources/demo/templates/traffic.html
@@ -63,22 +63,111 @@
Select Issue Type
-
-
-
- Pothole
+
+
+
+
+ Road
-
-
- Obstacle
+
+ Rail
-
-
- Vehicle on Side
+
+ Weather
-
-
- Danger
+
+ Emergency
+
+
+
+
+
+
+
+
+
+
+ Pothole
+
+
+
+ Obstacle
+
+
+
+ Vehicle on Side
+
+
+
+ Danger
+
+
+
+ Accident
+
+
+
+ Flooded Road
+
+
+
+ Roadwork
+
+
+
+ Black Traffic
+
+
+
+
+
+
+
+
+
+ Unattended Luggage
+
+
+
+ Delay
+
+
+
+ Major Delay
+
+
+
+
+
+
+
+
+
+ Flood Alert
+
+
+
+ Thunderstorm
+
+
+
+ Fog Warning
+
+
+
+
+
+
+
+
+
+ Emergency Alert
+
+
+
+ Daylight Saving
+
+
@@ -134,7 +223,7 @@
Click on the map to set the issue location or use the "Get My Current Location" button
-
+
@@ -161,6 +250,49 @@
// 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',
@@ -168,15 +300,73 @@
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;
+ // 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(`
+
${event.properties.label || 'Traffic Event'}
+
Type: ${event.properties.what || 'Unknown'}
+
Start: ${event.properties.start || 'Unknown'}
+
End: ${event.properties.stop || 'Unknown'}
+ `);
+
+ 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);
// Add marker on map click
map.on('click', function(e) {
@@ -201,28 +391,100 @@
switch(issueType) {
case 'pothole':
labelInput.value = 'Nid de poule';
- issueTypeInput.value = 'road.hazard.pothole';
+ issueTypeInput.value = 'traffic.hazard.pothole';
severitySelect.value = 'medium';
markerColor = '#ff9800';
break;
case 'obstacle':
labelInput.value = 'Obstacle';
- issueTypeInput.value = 'road.hazard.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 = 'road.hazard.vehicle';
+ issueTypeInput.value = 'traffic.hazard.vehicle';
severitySelect.value = 'low';
markerColor = '#2196f3';
break;
case 'danger':
labelInput.value = 'Danger non classé';
- issueTypeInput.value = 'road.hazard.danger';
+ 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';
@@ -240,6 +502,9 @@
if (currentLngLat) {
marker.setLngLat(currentLngLat).addTo(map);
}
+
+ // Validate form after filling in values
+ validateForm();
}
// Handle geolocation button click
@@ -272,6 +537,9 @@
// 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())
@@ -341,10 +609,75 @@
}
});
+ // 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);
+ });
+
// Handle form submission
document.getElementById('trafficForm').addEventListener('submit', 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;
@@ -354,7 +687,7 @@
const stop = document.getElementById('stop').value;
const where = document.getElementById('where').value;
- // Check if marker is set
+ // 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;
@@ -413,7 +746,7 @@
saveEventToLocalStorage(event);
// Submit event to API
- fetch('/event', {
+ fetch('https://api.openeventdatabase.org/event', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -441,7 +774,7 @@
const resultElement = document.getElementById('result');
resultElement.innerHTML += `