From 11cd3236c5609da673e8b327b91c6fd8806e5b56 Mon Sep 17 00:00:00 2001 From: Tykayn Date: Sat, 27 Sep 2025 01:10:47 +0200 Subject: [PATCH] QA page --- backend.py | 3 + oedb/resources/demo/demo_main.py | 18 +- oedb/resources/demo/static/event-types.js | 501 ++++++++++++++++++ oedb/resources/demo/templates/edit.html | 32 +- .../demo/templates/partials/demo_nav.html | 1 - oedb/resources/demo/templates/traffic.html | 80 ++- .../resources/demo/templates/traffic_new.html | 2 +- oedb/resources/event_form.py | 105 +++- oedb/resources/live.py | 82 ++- oedb/resources/quality_assurance.py | 493 +++++++++++++++++ static/edit.js | 191 +++++++ static/event-types.js | 501 ++++++++++++++++++ static/map_by_what.js | 14 +- 13 files changed, 1952 insertions(+), 71 deletions(-) create mode 100644 oedb/resources/demo/static/event-types.js create mode 100644 oedb/resources/quality_assurance.py create mode 100644 static/event-types.js diff --git a/backend.py b/backend.py index 0382593..db827f2 100644 --- a/backend.py +++ b/backend.py @@ -29,6 +29,7 @@ from oedb.resources.live import live from oedb.resources.rss import rss_latest, rss_by_family from oedb.resources.event_form import event_form from oedb.resources.db_dump import db_dump_list, db_dump_create +from oedb.resources.quality_assurance import quality_assurance def create_app(): """ @@ -84,6 +85,7 @@ def create_app(): app.add_route('/event/{id}', event) # Handle single event requests app.add_route('/event', event) # Handle event collection requests app.add_route('/stats', stats) # Handle stats requests + app.add_route('/quality_assurance', quality_assurance) # Handle quality assurance requests app.add_route('/demo', demo) # Handle demo page requests app.add_route('/demo/add', event_form) # Handle event submission form app.add_route('/demo/by-what', demo, suffix='by_what') # Handle events by type page @@ -94,6 +96,7 @@ def create_app(): app.add_route('/demo/traffic', demo, suffix='traffic') # Handle traffic jam reporting page app.add_route('/demo/view-events', demo, suffix='view_events') # Handle view saved events page app.add_route('/demo/stats', demo_stats) # Handle stats by what page + app.add_route('/demo/property-stats', demo, suffix='property_stats') # Handle property statistics page app.add_route('/demo/live', live) # Live page app.add_route('/rss', rss_latest) # RSS latest 200 app.add_route('/rss/by/{family}', rss_by_family) # RSS by family diff --git a/oedb/resources/demo/demo_main.py b/oedb/resources/demo/demo_main.py index f16521b..31f0ffe 100644 --- a/oedb/resources/demo/demo_main.py +++ b/oedb/resources/demo/demo_main.py @@ -487,19 +487,25 @@ class DemoMainResource: const properties = feature.properties; // Extraire les informations principales - const title = properties.title || 'Événement sans titre'; + const title = properties.label || properties.title || 'Événement sans titre'; const what = properties.what || 'Non spécifié'; const when = properties.when ? formatDate(properties.when) : 'Date inconnue'; const description = properties.description || 'Aucune description disponible'; - // Créer le HTML de la popup + // Créer le HTML de la popup avec titre cliquable pour édition + const editLink = properties.id ? `/demo/edit/${properties.id}` : '#'; + return `
-

${title}

+

+ + ${title} + +

Type: ${what}

Date: ${when}

Description: ${description}

-

Voir détails

+ ${properties.id ? `

✏️ Modifier l'événement

` : ''}
`; } @@ -786,7 +792,9 @@ class DemoMainResource: // Create popup content let popupContent = '
'; - popupContent += `

${properties.label || 'Event'}

`; + const eventTitle = properties.label || 'Event'; + const editLink = properties.id ? `/demo/edit/${properties.id}` : '#'; + popupContent += `

${eventTitle}

`; // Display all properties popupContent += '
'; diff --git a/oedb/resources/demo/static/event-types.js b/oedb/resources/demo/static/event-types.js new file mode 100644 index 0000000..91b068d --- /dev/null +++ b/oedb/resources/demo/static/event-types.js @@ -0,0 +1,501 @@ +// Configuration partagée des types d'événements avec leurs emojis et descriptions +window.EVENT_TYPES = { + // Community / OSM + 'community.osm.event': { + emoji: '🗺️', + label: 'Événement OpenStreetMap', + category: 'Communauté', + description: 'Événement lié à la communauté OpenStreetMap' + }, + + // Culture / Arts + 'culture.arts': { + emoji: '🎨', + label: 'Arts et culture', + category: 'Culture', + description: 'Événement artistique et culturel' + }, + 'culture.geek': { + emoji: '🤓', + label: 'Culture geek', + category: 'Culture', + description: 'Événement geek, technologie, gaming' + }, + 'culture.music': { + emoji: '🎵', + label: 'Musique', + category: 'Culture', + description: 'Événement musical général' + }, + + // Music specific + 'music.festival': { + emoji: '🎪', + label: 'Festival de musique', + category: 'Musique', + description: 'Festival musical' + }, + + // Power / Energy + 'power.production.unavail': { + emoji: '⚡', + label: 'Production électrique indisponible', + category: 'Énergie', + description: 'Arrêt ou réduction de production électrique' + }, + + // Sale / Commerce + 'sale': { + emoji: '🛒', + label: 'Vente / Commerce', + category: 'Commerce', + description: 'Événement commercial, vente, marché' + }, + + // Time / Temporal + 'time.daylight.summer': { + emoji: '☀️', + label: 'Heure d\'été', + category: 'Temps', + description: 'Passage à l\'heure d\'été' + }, + + // Tourism + 'tourism.exhibition': { + emoji: '🖼️', + label: 'Exposition', + category: 'Tourisme', + description: 'Exposition, salon, foire' + }, + + // Traffic / Transportation + 'traffic.accident': { + emoji: '💥', + label: 'Accident', + category: 'Circulation', + description: 'Accident de la circulation' + }, + 'traffic.incident': { + emoji: '⚠️', + label: 'Incident de circulation', + category: 'Circulation', + description: 'Incident sur la route' + }, + 'traffic.obstacle': { + emoji: '🚧', + label: 'Obstacle', + category: 'Circulation', + description: 'Obstacle sur la voie' + }, + 'traffic.partially_closed': { + emoji: '🚦', + label: 'Voie partiellement fermée', + category: 'Circulation', + description: 'Fermeture partielle de voie' + }, + 'traffic.roadwork': { + emoji: '⛑️', + label: 'Travaux routiers', + category: 'Circulation', + description: 'Travaux sur la chaussée' + } +}; + +// Fonction pour obtenir les suggestions d'autocomplétion +function getEventTypeSuggestions(input) { + const inputLower = input.toLowerCase(); + const suggestions = []; + + for (const [key, config] of Object.entries(window.EVENT_TYPES)) { + // Recherche dans la clé, le label et la catégorie + const searchableText = `${key} ${config.label} ${config.category}`.toLowerCase(); + + if (searchableText.includes(inputLower)) { + suggestions.push({ + value: key, + label: `${config.emoji} ${config.label}`, + category: config.category, + fullText: `${key} - ${config.label}` + }); + } + } + + // Trier par pertinence (correspondance exacte en premier) + suggestions.sort((a, b) => { + const aExact = a.value.toLowerCase().startsWith(inputLower); + const bExact = b.value.toLowerCase().startsWith(inputLower); + + if (aExact && !bExact) return -1; + if (!aExact && bExact) return 1; + + return a.value.localeCompare(b.value); + }); + + return suggestions.slice(0, 10); // Limiter à 10 suggestions +} + +// Fonction pour initialiser l'autocomplétion sur un champ +function initializeEventTypeAutocomplete(inputElement, onSelect) { + if (!inputElement) return; + + let suggestionsContainer = null; + let currentSuggestions = []; + let selectedIndex = -1; + let selectorButton = null; + + // Créer le bouton de sélection + function createSelectorButton() { + selectorButton = document.createElement('button'); + selectorButton.type = 'button'; + selectorButton.className = 'event-type-selector-btn'; + selectorButton.innerHTML = '📋 Types d\'événements'; + selectorButton.style.cssText = ` + margin-top: 5px; + padding: 6px 12px; + background-color: #0078ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + display: inline-block; + `; + + selectorButton.addEventListener('click', function() { + showAllEventTypes(); + }); + + const parent = inputElement.parentElement; + parent.appendChild(selectorButton); + } + + // Créer le conteneur de suggestions + function createSuggestionsContainer() { + suggestionsContainer = document.createElement('div'); + suggestionsContainer.className = 'autocomplete-suggestions'; + suggestionsContainer.style.cssText = ` + position: absolute; + top: 100%; + left: 0; + right: 0; + max-height: 300px; + overflow-y: auto; + background: white; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 1000; + display: none; + margin-top: 2px; + `; + + // Positionner le conteneur parent en relatif + const parent = inputElement.parentElement; + if (parent.style.position !== 'relative') { + parent.style.position = 'relative'; + } + parent.appendChild(suggestionsContainer); + } + + // Afficher tous les types d'événements dans un beau sélecteur + function showAllEventTypes() { + const allTypes = Object.entries(window.EVENT_TYPES).map(([key, config]) => ({ + value: key, + emoji: config.emoji, + label: config.label, + category: config.category, + description: config.description || '', + fullText: `${key} - ${config.label}` + })); + + showEventTypeSelector(allTypes); + } + + // Afficher le sélecteur de types d'événements + function showEventTypeSelector(types) { + if (!suggestionsContainer) return; + + currentSuggestions = types; + selectedIndex = -1; + + // Grouper par catégorie + const groupedTypes = {}; + types.forEach(type => { + if (!groupedTypes[type.category]) { + groupedTypes[type.category] = []; + } + groupedTypes[type.category].push(type); + }); + + let html = ` +
+ 🏷️ Types d'événements disponibles + +
+ `; + + // Afficher chaque catégorie + Object.entries(groupedTypes).forEach(([category, categoryTypes]) => { + html += ` +
+
+ ${category} +
+ `; + + categoryTypes.forEach((type, index) => { + const globalIndex = types.indexOf(type); + html += ` +
+ ${type.emoji} +
+
${type.label}
+
${type.value}
+ ${type.description ? `
${type.description}
` : ''} +
+
+ `; + }); + + html += '
'; + }); + + suggestionsContainer.innerHTML = html; + suggestionsContainer.style.display = 'block'; + + // Ajouter les gestionnaires d'événements + const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); + suggestionItems.forEach((item, index) => { + const dataIndex = parseInt(item.dataset.index); + item.addEventListener('click', () => selectSuggestion(dataIndex)); + }); + } + + // Afficher les suggestions + function showSuggestions(suggestions) { + if (!suggestionsContainer) return; + + currentSuggestions = suggestions; + selectedIndex = -1; + + if (suggestions.length === 0) { + suggestionsContainer.style.display = 'none'; + return; + } + + suggestionsContainer.innerHTML = suggestions.map((suggestion, index) => ` +
+ ${suggestion.label.split(' ')[0]} + ${suggestion.fullText} + ${suggestion.category} +
+ `).join(''); + + suggestionsContainer.style.display = 'block'; + + // Ajouter les gestionnaires d'événements aux suggestions + const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); + suggestionItems.forEach((item, index) => { + item.addEventListener('click', () => selectSuggestion(index)); + item.addEventListener('mouseenter', () => highlightSuggestion(index)); + }); + } + + // Mettre en surbrillance une suggestion + function highlightSuggestion(index) { + const items = suggestionsContainer.querySelectorAll('.suggestion-item'); + items.forEach((item, i) => { + item.style.backgroundColor = i === index ? '#f0f8ff' : 'white'; + }); + selectedIndex = index; + } + + // Sélectionner une suggestion + function selectSuggestion(index) { + if (index >= 0 && index < currentSuggestions.length) { + const suggestion = currentSuggestions[index]; + inputElement.value = suggestion.value; + suggestionsContainer.style.display = 'none'; + + if (onSelect) { + onSelect(suggestion); + } + + // Déclencher l'événement input pour les validations + inputElement.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + // Masquer les suggestions + function hideSuggestions() { + if (suggestionsContainer) { + suggestionsContainer.style.display = 'none'; + } + } + + // Créer le conteneur de suggestions et le bouton + createSuggestionsContainer(); + createSelectorButton(); + + // Gestionnaire d'événements pour l'input + inputElement.addEventListener('input', function(e) { + const value = e.target.value.trim(); + + if (value.length === 0) { + showAllEventTypes(); + return; + } + + if (value.length < 2) { + hideSuggestions(); + return; + } + + const suggestions = getEventTypeSuggestions(value); + showFilteredSuggestions(suggestions); + }); + + // Afficher l'autocomplétion au focus + inputElement.addEventListener('focus', function(e) { + const value = e.target.value.trim(); + + if (value.length === 0) { + showAllEventTypes(); + } else if (value.length >= 2) { + const suggestions = getEventTypeSuggestions(value); + showFilteredSuggestions(suggestions); + } + }); + + // Afficher l'autocomplétion au keyup + inputElement.addEventListener('keyup', function(e) { + // Ignorer les touches de navigation + if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key)) { + return; + } + + const value = e.target.value.trim(); + + if (value.length === 0) { + showAllEventTypes(); + } else if (value.length >= 2) { + const suggestions = getEventTypeSuggestions(value); + showFilteredSuggestions(suggestions); + } + }); + + // Fonction pour afficher les suggestions filtrées + function showFilteredSuggestions(suggestions) { + if (!suggestionsContainer) return; + + currentSuggestions = suggestions; + selectedIndex = -1; + + if (suggestions.length === 0) { + suggestionsContainer.innerHTML = ` +
+ Aucun type d'événement trouvé +
+ `; + suggestionsContainer.style.display = 'block'; + return; + } + + let html = ` +
+ ${suggestions.length} résultat${suggestions.length > 1 ? 's' : ''} trouvé${suggestions.length > 1 ? 's' : ''} +
+ `; + + suggestions.forEach((suggestion, index) => { + const config = window.EVENT_TYPES[suggestion.value] || {}; + html += ` +
+ ${config.emoji || '📍'} +
+
${config.label || suggestion.value}
+
${suggestion.value}
+
+ ${config.category || ''} +
+ `; + }); + + suggestionsContainer.innerHTML = html; + suggestionsContainer.style.display = 'block'; + + // Ajouter les gestionnaires d'événements aux suggestions + const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); + suggestionItems.forEach((item, index) => { + item.addEventListener('click', () => selectSuggestion(index)); + }); + } + + // Gestionnaire d'événements pour le clavier + inputElement.addEventListener('keydown', function(e) { + if (!suggestionsContainer || suggestionsContainer.style.display === 'none') { + return; + } + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + const nextIndex = selectedIndex < currentSuggestions.length - 1 ? selectedIndex + 1 : 0; + highlightSuggestion(nextIndex); + break; + + case 'ArrowUp': + e.preventDefault(); + const prevIndex = selectedIndex > 0 ? selectedIndex - 1 : currentSuggestions.length - 1; + highlightSuggestion(prevIndex); + break; + + case 'Enter': + e.preventDefault(); + if (selectedIndex >= 0) { + selectSuggestion(selectedIndex); + } + break; + + case 'Escape': + hideSuggestions(); + break; + } + }); + + // Masquer les suggestions quand on clique ailleurs + document.addEventListener('click', function(e) { + if (!inputElement.contains(e.target) && !suggestionsContainer?.contains(e.target)) { + hideSuggestions(); + } + }); + + // Masquer les suggestions quand le champ perd le focus + inputElement.addEventListener('blur', function() { + // Délai pour permettre le clic sur une suggestion + setTimeout(hideSuggestions, 200); + }); +} diff --git a/oedb/resources/demo/templates/edit.html b/oedb/resources/demo/templates/edit.html index a11564d..47bad90 100644 --- a/oedb/resources/demo/templates/edit.html +++ b/oedb/resources/demo/templates/edit.html @@ -12,11 +12,30 @@ + {% endblock %} {% block header %}Edit Event{% endblock %} {% block content %} +
+

Propriétés actuelles de l'événement

+
+ + + + + + + + + + + +
PropriétéValeurType
+
+
+
@@ -38,7 +57,10 @@
-
Category of the event (e.g., sport.match.football, culture.festival)
+
+ Category of the event (e.g., sport.match.football, culture.festival)
+ 💡 Tapez au moins 2 caractères pour voir les suggestions avec emojis +
@@ -72,6 +94,14 @@
Click on the map to set the event location
+
+ + + Utile si les coordonnées longitude/latitude ont été inversées + +
diff --git a/oedb/resources/demo/templates/partials/demo_nav.html b/oedb/resources/demo/templates/partials/demo_nav.html index d96ee0b..32420a4 100644 --- a/oedb/resources/demo/templates/partials/demo_nav.html +++ b/oedb/resources/demo/templates/partials/demo_nav.html @@ -23,4 +23,3 @@ } }); - diff --git a/oedb/resources/demo/templates/traffic.html b/oedb/resources/demo/templates/traffic.html index 5f72871..e077091 100644 --- a/oedb/resources/demo/templates/traffic.html +++ b/oedb/resources/demo/templates/traffic.html @@ -12,6 +12,7 @@ +
@@ -187,11 +188,6 @@
- - GPS: inconnu
@@ -269,7 +265,13 @@
-
Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"
+
+ + Cliquez pour vous géolocaliser +
+
Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton de géolocalisation
@@ -622,10 +624,14 @@ } // Handle geolocation button click - document.getElementById('geolocateBtn').addEventListener('click', function() { - // Show loading spinner - document.getElementById('geolocateSpinner').style.display = 'inline-block'; + document.getElementById('geolocateMapBtn').addEventListener('click', function() { + // Update button text and disable it + const originalText = this.textContent; + this.textContent = '📍 Localisation en cours...'; this.disabled = true; + + const statusElement = document.getElementById('geolocateStatus'); + statusElement.textContent = 'Localisation en cours...'; // Check if geolocation is available if (navigator.geolocation) { @@ -644,12 +650,17 @@ zoom: 14 }); - // Hide loading spinner - document.getElementById('geolocateSpinner').style.display = 'none'; - document.getElementById('geolocateBtn').disabled = false; - + // Reset button + document.getElementById('geolocateMapBtn').textContent = '📍 Position obtenue'; + document.getElementById('geolocateMapBtn').disabled = false; + document.getElementById('geolocateMapBtn').style.backgroundColor = '#28a745'; + + const statusElement = document.getElementById('geolocateStatus'); + statusElement.textContent = `Position: ${lat.toFixed(4)}, ${lng.toFixed(4)}`; + statusElement.style.color = '#28a745'; + // Show success message - showResult('Current location detected successfully', 'success'); + showResult('Position actuelle détectée avec succès', 'success'); // Validate form after setting marker validateForm(); @@ -684,26 +695,34 @@ }, // Error callback function(error) { - // Hide loading spinner - document.getElementById('geolocateSpinner').style.display = 'none'; - document.getElementById('geolocateBtn').disabled = false; - + // Reset button + document.getElementById('geolocateMapBtn').textContent = '📍 Réessayer la géolocalisation'; + document.getElementById('geolocateMapBtn').disabled = false; + document.getElementById('geolocateMapBtn').style.backgroundColor = '#dc3545'; + + const statusElement = document.getElementById('geolocateStatus'); + // Show error message - let errorMsg = 'Unable to get your location. '; + let errorMsg = 'Impossible d\'obtenir votre position. '; switch(error.code) { case error.PERMISSION_DENIED: - errorMsg += 'You denied the request for geolocation.'; + errorMsg += 'Vous avez refusé la demande de géolocalisation.'; + statusElement.textContent = 'Permission refusée'; break; case error.POSITION_UNAVAILABLE: - errorMsg += 'Location information is unavailable.'; + errorMsg += 'Les informations de position ne sont pas disponibles.'; + statusElement.textContent = 'Position indisponible'; break; case error.TIMEOUT: - errorMsg += 'The request to get your location timed out.'; + errorMsg += 'La demande de géolocalisation a expiré.'; + statusElement.textContent = 'Temps dépassé'; break; case error.UNKNOWN_ERROR: - errorMsg += 'An unknown error occurred.'; + errorMsg += 'Une erreur inconnue s\'est produite.'; + statusElement.textContent = 'Erreur inconnue'; break; } + statusElement.style.color = '#dc3545'; showResult(errorMsg, 'error'); }, // Options @@ -714,12 +733,17 @@ } ); } else { - // Hide loading spinner - document.getElementById('geolocateSpinner').style.display = 'none'; - document.getElementById('geolocateBtn').disabled = false; - + // Reset button + document.getElementById('geolocateMapBtn').textContent = '📍 Non supporté'; + document.getElementById('geolocateMapBtn').disabled = true; + document.getElementById('geolocateMapBtn').style.backgroundColor = '#6c757d'; + + const statusElement = document.getElementById('geolocateStatus'); + statusElement.textContent = 'Géolocalisation non supportée'; + statusElement.style.color = '#dc3545'; + // Show error message - showResult('Geolocation is not supported by your browser', 'error'); + showResult('La géolocalisation n\'est pas supportée par votre navigateur', 'error'); } }); diff --git a/oedb/resources/demo/templates/traffic_new.html b/oedb/resources/demo/templates/traffic_new.html index 426d915..ac529ac 100644 --- a/oedb/resources/demo/templates/traffic_new.html +++ b/oedb/resources/demo/templates/traffic_new.html @@ -260,7 +260,7 @@
Cliquez sur la carte pour définir la localisation du problème ou utilisez le bouton "Obtenir ma position actuelle"
- +
diff --git a/oedb/resources/event_form.py b/oedb/resources/event_form.py index 9cf4246..48c2fe3 100644 --- a/oedb/resources/event_form.py +++ b/oedb/resources/event_form.py @@ -38,6 +38,7 @@ class EventFormResource: + + + +
+
+

+ + Contrôle Qualité des Événements +

+

+ Analyse des 1000 derniers événements pour détecter les problèmes divers +

+ +
+
+
+

Filtres

+ +
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+

Total événements problématiques: """ + str(len(problematic_events)) + """

+ """ + + for issue_type, count in sorted(issue_counts.items(), key=lambda x: x[1], reverse=True): + issue_label = issue_type.replace('_', ' ').title() + html += f"

{issue_label}: {count}

" + + html += """ +
+
+
+
+ +
+
+ """ + + # Add events + for event in problematic_events: + properties = event.get('properties', {}) + event_title = properties.get('label', properties.get('name', f'Événement #{event["id"]}')) + event_what = properties.get('what', 'Non spécifié') + + # Get severity classes for the card + max_severity = 'low' + for issue in event['issues']: + if issue['severity'] == 'high': + max_severity = 'high' + break + elif issue['severity'] == 'medium' and max_severity == 'low': + max_severity = 'medium' + + html += f""" +
+
+
+
+
+

{event_title}

+

+ Type: {event_what} | + Coordonnées: + {event.get('latitude', 'N/A')}, {event.get('longitude', 'N/A')} + +

+
+
+ +
+ +
+
Problèmes détectés:
+
    + """ + + for issue in event['issues']: + severity_icon = { + 'high': '🔴', + 'medium': '🟡', + 'low': '🟢' + }.get(issue['severity'], '⚪') + + html += f""" +
  • + {severity_icon} {issue['type'].replace('_', ' ').title()}: + {issue['description']} +
  • + """ + + html += """ +
+
+ +
+ + Créé: """ + str(event.get('createdate', 'N/A')) + """ | + Modifié: """ + str(event.get('lastupdate', 'N/A')) + """ + +
+
+
+ """ + + if not problematic_events: + html += """ +
+ + Aucun problème détecté dans les 1000 derniers événements ! 🎉 +
+ """ + + html += """ +
+
+
+
+
+ + + + + """ + + return html + +# Create a global instance +quality_assurance = QualityAssuranceResource() diff --git a/static/edit.js b/static/edit.js index 0e53d01..fc0fe66 100644 --- a/static/edit.js +++ b/static/edit.js @@ -19,6 +19,9 @@ function initializeForm() { const properties = window.eventData.properties || {}; const geometry = window.eventData.geometry || {}; + // Remplir le tableau des propriétés + populatePropertiesTable(); + // Remplir les champs du formulaire document.getElementById('label').value = properties.label || ''; document.getElementById('type').value = properties.type || 'scheduled'; @@ -102,6 +105,54 @@ function initializeMap() { console.log('✅ Carte initialisée'); } +function populatePropertiesTable() { + if (!window.eventData || !window.eventData.properties) { + return; + } + + const tableBody = document.getElementById('propertiesTableBody'); + const properties = window.eventData.properties; + + // Vider le tableau + tableBody.innerHTML = ''; + + // Trier les propriétés alphabétiquement + const sortedKeys = Object.keys(properties).sort(); + + // Ajouter chaque propriété au tableau + sortedKeys.forEach(key => { + const value = properties[key]; + const valueType = typeof value; + + const row = document.createElement('tr'); + row.style.borderBottom = '1px solid #eee'; + + // Formater la valeur pour l'affichage + let displayValue = ''; + if (value === null) { + displayValue = 'null'; + } else if (value === undefined) { + displayValue = 'undefined'; + } else if (typeof value === 'object') { + displayValue = `
${JSON.stringify(value, null, 2)}
`; + } else if (typeof value === 'string' && value.length > 50) { + displayValue = `${value.substring(0, 50)}...`; + } else { + displayValue = String(value); + } + + row.innerHTML = ` + ${key} + ${displayValue} + ${valueType} + `; + + tableBody.appendChild(row); + }); + + console.log('✅ Tableau des propriétés rempli'); +} + function setupEventHandlers() { // Gestionnaire de soumission du formulaire const form = document.getElementById('eventForm'); @@ -114,6 +165,34 @@ function setupEventHandlers() { if (deleteButton) { deleteButton.addEventListener('click', handleDelete); } + + // Gestionnaire du bouton d'inversion des coordonnées + const swapButton = document.getElementById('swapCoordinatesButton'); + if (swapButton) { + swapButton.addEventListener('click', handleSwapCoordinates); + + // Vérifier si c'est un Point pour activer/désactiver le bouton + const geometry = window.eventData?.geometry; + if (!geometry || geometry.type !== 'Point') { + swapButton.disabled = true; + swapButton.textContent = '🔄 Non disponible (pas un Point)'; + swapButton.style.backgroundColor = '#6c757d'; + } + } + + // Initialiser l'autocomplétion pour le champ "what" + const whatInput = document.getElementById('what'); + if (whatInput && window.initializeEventTypeAutocomplete) { + initializeEventTypeAutocomplete(whatInput, function(suggestion) { + console.log('Type d\'événement sélectionné:', suggestion); + + // Émettre un événement de validation du formulaire + setTimeout(() => { + whatInput.dispatchEvent(new Event('input', { bubbles: true })); + }, 100); + }); + console.log('✅ Autocomplétion initialisée pour le champ "what"'); + } } async function handleFormSubmit(e) { @@ -230,3 +309,115 @@ async function handleDelete() { `; } } + +async function handleSwapCoordinates() { + const eventId = document.getElementById('eventId').value; + const resultElement = document.getElementById('result'); + const geometry = window.eventData?.geometry; + + // Vérifications de sécurité + if (!geometry || geometry.type !== 'Point') { + resultElement.innerHTML = ` +
+ Erreur: L'inversion des coordonnées n'est disponible que pour les événements de type Point. +
+ `; + resultElement.style.display = 'block'; + return; + } + + const currentCoords = geometry.coordinates; + if (!Array.isArray(currentCoords) || currentCoords.length < 2) { + resultElement.innerHTML = ` +
+ Erreur: Coordonnées invalides détectées. +
+ `; + resultElement.style.display = 'block'; + return; + } + + // Afficher les coordonnées actuelles et futures + const [currentLon, currentLat] = currentCoords; + const newCoords = [currentLat, currentLon]; // Inverser longitude et latitude + + const confirmMessage = `Voulez-vous inverser les coordonnées ? + +Actuelles: [${currentLon.toFixed(6)}, ${currentLat.toFixed(6)}] (lon, lat) +Nouvelles: [${newCoords[0].toFixed(6)}, ${newCoords[1].toFixed(6)}] (lat→lon, lon→lat) + +Cette action sera sauvegardée immédiatement sur le serveur.`; + + if (!confirm(confirmMessage)) { + return; + } + + resultElement.innerHTML = '
⏳ Inversion des coordonnées en cours...
'; + resultElement.style.display = 'block'; + + try { + // Créer l'événement modifié avec coordonnées inversées + const updatedEvent = { + ...window.eventData, + geometry: { + ...geometry, + coordinates: newCoords + } + }; + + // Envoyer la mise à jour au serveur + const response = await fetch(`https://api.openeventdatabase.org/event/${eventId}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(updatedEvent) + }); + + if (response.ok) { + // Mettre à jour les données locales + window.eventData = updatedEvent; + + // Mettre à jour le marqueur sur la carte + if (currentMarker) { + currentMarker.setLngLat(newCoords); + } + + // Centrer la carte sur les nouvelles coordonnées + if (map) { + map.flyTo({ + center: newCoords, + zoom: Math.max(map.getZoom(), 12) + }); + } + + // Mettre à jour le tableau des propriétés + populatePropertiesTable(); + + resultElement.innerHTML = ` +
+ + Succès: Les coordonnées ont été inversées avec succès.
+ Anciennes: [${currentLon.toFixed(6)}, ${currentLat.toFixed(6)}]
+ Nouvelles: [${newCoords[0].toFixed(6)}, ${newCoords[1].toFixed(6)}]
+
+ `; + + console.log('✅ Coordonnées inversées avec succès:', { + avant: [currentLon, currentLat], + après: newCoords + }); + } else { + const errorData = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorData}`); + } + } catch (error) { + console.error('Erreur lors de l\'inversion des coordonnées:', error); + resultElement.innerHTML = ` +
+ + Erreur: Impossible d'inverser les coordonnées. ${error.message} +
+ `; + } +} diff --git a/static/event-types.js b/static/event-types.js new file mode 100644 index 0000000..91b068d --- /dev/null +++ b/static/event-types.js @@ -0,0 +1,501 @@ +// Configuration partagée des types d'événements avec leurs emojis et descriptions +window.EVENT_TYPES = { + // Community / OSM + 'community.osm.event': { + emoji: '🗺️', + label: 'Événement OpenStreetMap', + category: 'Communauté', + description: 'Événement lié à la communauté OpenStreetMap' + }, + + // Culture / Arts + 'culture.arts': { + emoji: '🎨', + label: 'Arts et culture', + category: 'Culture', + description: 'Événement artistique et culturel' + }, + 'culture.geek': { + emoji: '🤓', + label: 'Culture geek', + category: 'Culture', + description: 'Événement geek, technologie, gaming' + }, + 'culture.music': { + emoji: '🎵', + label: 'Musique', + category: 'Culture', + description: 'Événement musical général' + }, + + // Music specific + 'music.festival': { + emoji: '🎪', + label: 'Festival de musique', + category: 'Musique', + description: 'Festival musical' + }, + + // Power / Energy + 'power.production.unavail': { + emoji: '⚡', + label: 'Production électrique indisponible', + category: 'Énergie', + description: 'Arrêt ou réduction de production électrique' + }, + + // Sale / Commerce + 'sale': { + emoji: '🛒', + label: 'Vente / Commerce', + category: 'Commerce', + description: 'Événement commercial, vente, marché' + }, + + // Time / Temporal + 'time.daylight.summer': { + emoji: '☀️', + label: 'Heure d\'été', + category: 'Temps', + description: 'Passage à l\'heure d\'été' + }, + + // Tourism + 'tourism.exhibition': { + emoji: '🖼️', + label: 'Exposition', + category: 'Tourisme', + description: 'Exposition, salon, foire' + }, + + // Traffic / Transportation + 'traffic.accident': { + emoji: '💥', + label: 'Accident', + category: 'Circulation', + description: 'Accident de la circulation' + }, + 'traffic.incident': { + emoji: '⚠️', + label: 'Incident de circulation', + category: 'Circulation', + description: 'Incident sur la route' + }, + 'traffic.obstacle': { + emoji: '🚧', + label: 'Obstacle', + category: 'Circulation', + description: 'Obstacle sur la voie' + }, + 'traffic.partially_closed': { + emoji: '🚦', + label: 'Voie partiellement fermée', + category: 'Circulation', + description: 'Fermeture partielle de voie' + }, + 'traffic.roadwork': { + emoji: '⛑️', + label: 'Travaux routiers', + category: 'Circulation', + description: 'Travaux sur la chaussée' + } +}; + +// Fonction pour obtenir les suggestions d'autocomplétion +function getEventTypeSuggestions(input) { + const inputLower = input.toLowerCase(); + const suggestions = []; + + for (const [key, config] of Object.entries(window.EVENT_TYPES)) { + // Recherche dans la clé, le label et la catégorie + const searchableText = `${key} ${config.label} ${config.category}`.toLowerCase(); + + if (searchableText.includes(inputLower)) { + suggestions.push({ + value: key, + label: `${config.emoji} ${config.label}`, + category: config.category, + fullText: `${key} - ${config.label}` + }); + } + } + + // Trier par pertinence (correspondance exacte en premier) + suggestions.sort((a, b) => { + const aExact = a.value.toLowerCase().startsWith(inputLower); + const bExact = b.value.toLowerCase().startsWith(inputLower); + + if (aExact && !bExact) return -1; + if (!aExact && bExact) return 1; + + return a.value.localeCompare(b.value); + }); + + return suggestions.slice(0, 10); // Limiter à 10 suggestions +} + +// Fonction pour initialiser l'autocomplétion sur un champ +function initializeEventTypeAutocomplete(inputElement, onSelect) { + if (!inputElement) return; + + let suggestionsContainer = null; + let currentSuggestions = []; + let selectedIndex = -1; + let selectorButton = null; + + // Créer le bouton de sélection + function createSelectorButton() { + selectorButton = document.createElement('button'); + selectorButton.type = 'button'; + selectorButton.className = 'event-type-selector-btn'; + selectorButton.innerHTML = '📋 Types d\'événements'; + selectorButton.style.cssText = ` + margin-top: 5px; + padding: 6px 12px; + background-color: #0078ff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + display: inline-block; + `; + + selectorButton.addEventListener('click', function() { + showAllEventTypes(); + }); + + const parent = inputElement.parentElement; + parent.appendChild(selectorButton); + } + + // Créer le conteneur de suggestions + function createSuggestionsContainer() { + suggestionsContainer = document.createElement('div'); + suggestionsContainer.className = 'autocomplete-suggestions'; + suggestionsContainer.style.cssText = ` + position: absolute; + top: 100%; + left: 0; + right: 0; + max-height: 300px; + overflow-y: auto; + background: white; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.15); + z-index: 1000; + display: none; + margin-top: 2px; + `; + + // Positionner le conteneur parent en relatif + const parent = inputElement.parentElement; + if (parent.style.position !== 'relative') { + parent.style.position = 'relative'; + } + parent.appendChild(suggestionsContainer); + } + + // Afficher tous les types d'événements dans un beau sélecteur + function showAllEventTypes() { + const allTypes = Object.entries(window.EVENT_TYPES).map(([key, config]) => ({ + value: key, + emoji: config.emoji, + label: config.label, + category: config.category, + description: config.description || '', + fullText: `${key} - ${config.label}` + })); + + showEventTypeSelector(allTypes); + } + + // Afficher le sélecteur de types d'événements + function showEventTypeSelector(types) { + if (!suggestionsContainer) return; + + currentSuggestions = types; + selectedIndex = -1; + + // Grouper par catégorie + const groupedTypes = {}; + types.forEach(type => { + if (!groupedTypes[type.category]) { + groupedTypes[type.category] = []; + } + groupedTypes[type.category].push(type); + }); + + let html = ` +
+ 🏷️ Types d'événements disponibles + +
+ `; + + // Afficher chaque catégorie + Object.entries(groupedTypes).forEach(([category, categoryTypes]) => { + html += ` +
+
+ ${category} +
+ `; + + categoryTypes.forEach((type, index) => { + const globalIndex = types.indexOf(type); + html += ` +
+ ${type.emoji} +
+
${type.label}
+
${type.value}
+ ${type.description ? `
${type.description}
` : ''} +
+
+ `; + }); + + html += '
'; + }); + + suggestionsContainer.innerHTML = html; + suggestionsContainer.style.display = 'block'; + + // Ajouter les gestionnaires d'événements + const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); + suggestionItems.forEach((item, index) => { + const dataIndex = parseInt(item.dataset.index); + item.addEventListener('click', () => selectSuggestion(dataIndex)); + }); + } + + // Afficher les suggestions + function showSuggestions(suggestions) { + if (!suggestionsContainer) return; + + currentSuggestions = suggestions; + selectedIndex = -1; + + if (suggestions.length === 0) { + suggestionsContainer.style.display = 'none'; + return; + } + + suggestionsContainer.innerHTML = suggestions.map((suggestion, index) => ` +
+ ${suggestion.label.split(' ')[0]} + ${suggestion.fullText} + ${suggestion.category} +
+ `).join(''); + + suggestionsContainer.style.display = 'block'; + + // Ajouter les gestionnaires d'événements aux suggestions + const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); + suggestionItems.forEach((item, index) => { + item.addEventListener('click', () => selectSuggestion(index)); + item.addEventListener('mouseenter', () => highlightSuggestion(index)); + }); + } + + // Mettre en surbrillance une suggestion + function highlightSuggestion(index) { + const items = suggestionsContainer.querySelectorAll('.suggestion-item'); + items.forEach((item, i) => { + item.style.backgroundColor = i === index ? '#f0f8ff' : 'white'; + }); + selectedIndex = index; + } + + // Sélectionner une suggestion + function selectSuggestion(index) { + if (index >= 0 && index < currentSuggestions.length) { + const suggestion = currentSuggestions[index]; + inputElement.value = suggestion.value; + suggestionsContainer.style.display = 'none'; + + if (onSelect) { + onSelect(suggestion); + } + + // Déclencher l'événement input pour les validations + inputElement.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + // Masquer les suggestions + function hideSuggestions() { + if (suggestionsContainer) { + suggestionsContainer.style.display = 'none'; + } + } + + // Créer le conteneur de suggestions et le bouton + createSuggestionsContainer(); + createSelectorButton(); + + // Gestionnaire d'événements pour l'input + inputElement.addEventListener('input', function(e) { + const value = e.target.value.trim(); + + if (value.length === 0) { + showAllEventTypes(); + return; + } + + if (value.length < 2) { + hideSuggestions(); + return; + } + + const suggestions = getEventTypeSuggestions(value); + showFilteredSuggestions(suggestions); + }); + + // Afficher l'autocomplétion au focus + inputElement.addEventListener('focus', function(e) { + const value = e.target.value.trim(); + + if (value.length === 0) { + showAllEventTypes(); + } else if (value.length >= 2) { + const suggestions = getEventTypeSuggestions(value); + showFilteredSuggestions(suggestions); + } + }); + + // Afficher l'autocomplétion au keyup + inputElement.addEventListener('keyup', function(e) { + // Ignorer les touches de navigation + if (['ArrowDown', 'ArrowUp', 'Enter', 'Escape'].includes(e.key)) { + return; + } + + const value = e.target.value.trim(); + + if (value.length === 0) { + showAllEventTypes(); + } else if (value.length >= 2) { + const suggestions = getEventTypeSuggestions(value); + showFilteredSuggestions(suggestions); + } + }); + + // Fonction pour afficher les suggestions filtrées + function showFilteredSuggestions(suggestions) { + if (!suggestionsContainer) return; + + currentSuggestions = suggestions; + selectedIndex = -1; + + if (suggestions.length === 0) { + suggestionsContainer.innerHTML = ` +
+ Aucun type d'événement trouvé +
+ `; + suggestionsContainer.style.display = 'block'; + return; + } + + let html = ` +
+ ${suggestions.length} résultat${suggestions.length > 1 ? 's' : ''} trouvé${suggestions.length > 1 ? 's' : ''} +
+ `; + + suggestions.forEach((suggestion, index) => { + const config = window.EVENT_TYPES[suggestion.value] || {}; + html += ` +
+ ${config.emoji || '📍'} +
+
${config.label || suggestion.value}
+
${suggestion.value}
+
+ ${config.category || ''} +
+ `; + }); + + suggestionsContainer.innerHTML = html; + suggestionsContainer.style.display = 'block'; + + // Ajouter les gestionnaires d'événements aux suggestions + const suggestionItems = suggestionsContainer.querySelectorAll('.suggestion-item'); + suggestionItems.forEach((item, index) => { + item.addEventListener('click', () => selectSuggestion(index)); + }); + } + + // Gestionnaire d'événements pour le clavier + inputElement.addEventListener('keydown', function(e) { + if (!suggestionsContainer || suggestionsContainer.style.display === 'none') { + return; + } + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + const nextIndex = selectedIndex < currentSuggestions.length - 1 ? selectedIndex + 1 : 0; + highlightSuggestion(nextIndex); + break; + + case 'ArrowUp': + e.preventDefault(); + const prevIndex = selectedIndex > 0 ? selectedIndex - 1 : currentSuggestions.length - 1; + highlightSuggestion(prevIndex); + break; + + case 'Enter': + e.preventDefault(); + if (selectedIndex >= 0) { + selectSuggestion(selectedIndex); + } + break; + + case 'Escape': + hideSuggestions(); + break; + } + }); + + // Masquer les suggestions quand on clique ailleurs + document.addEventListener('click', function(e) { + if (!inputElement.contains(e.target) && !suggestionsContainer?.contains(e.target)) { + hideSuggestions(); + } + }); + + // Masquer les suggestions quand le champ perd le focus + inputElement.addEventListener('blur', function() { + // Délai pour permettre le clic sur une suggestion + setTimeout(hideSuggestions, 200); + }); +} diff --git a/static/map_by_what.js b/static/map_by_what.js index 5b1ad51..561dca1 100644 --- a/static/map_by_what.js +++ b/static/map_by_what.js @@ -240,13 +240,23 @@ function createPopupContent(properties) { const title = properties.label || properties.name || 'Événement'; const what = properties.what || 'Non spécifié'; const description = properties.description || 'Aucune description disponible'; + const eventId = properties.id; return `
-

${title}

+ ${eventId ? + `

${title}

` : + `

${title}

` + }

Type: ${what}

Description: ${description}

- ${properties.id ? `

Modifier

` : ''} + ${eventId ? + `
+ ✏️ Modifier + 👁️ Détails +
` : + `

ID non disponible

` + }
`; }