This commit is contained in:
Tykayn 2025-09-26 17:38:30 +02:00 committed by tykayn
parent 2bb77d2300
commit 98c40b2447
16 changed files with 1836 additions and 361 deletions

View file

@ -1,45 +1,348 @@
{% extends "layout.html" %}
{% block title %}Event {{ id }} - OpenEventDatabase{% endblock %}
{% block title %}Événement {{ properties.label|default(id) }} - OpenEventDatabase{% endblock %}
{% block css %}
<style>
#map { width:100%; height: 360px; border:1px solid #ddd; border-radius:4px; }
table { width:100%; border-collapse: collapse; margin-top:12px; }
th, td { padding: 6px 8px; border-bottom: 1px solid #eee; text-align:left; }
th { background:#f9fafb; }
.event-header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.event-title {
margin: 0 0 10px 0;
color: #333;
font-size: 1.8rem;
}
.event-subtitle {
color: #666;
font-size: 1rem;
margin: 0;
}
#map {
width: 100%;
height: 400px;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 20px;
}
.properties-table {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
.table-header {
background: #f8f9fa;
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
}
.table-header h2 {
margin: 0;
color: #333;
font-size: 1.3rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px 20px;
border-bottom: 1px solid #eee;
text-align: left;
vertical-align: top;
}
th {
background: #f9fafb;
font-weight: 600;
color: #495057;
width: 200px;
}
td {
word-wrap: break-word;
max-width: 0;
}
.what-link {
display: inline-block;
padding: 4px 12px;
background: #0078ff;
color: white !important;
text-decoration: none;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
transition: background-color 0.2s;
}
.what-link:hover {
background: #0056b3;
text-decoration: none;
}
.nav-links {
margin-top: 10px;
}
.nav-links a {
color: #0078ff;
text-decoration: none;
margin-right: 15px;
font-weight: 500;
}
.nav-links a:hover {
text-decoration: underline;
}
.error-message {
background: #f8d7da;
color: #721c24;
padding: 12px;
border-radius: 4px;
margin-bottom: 20px;
border: 1px solid #f5c6cb;
}
.loading {
text-align: center;
padding: 20px;
color: #6c757d;
}
</style>
{% endblock %}
{% block header %}Évènement {{ id }}{% endblock %}
{% block header %}
<div class="event-header">
<h1 class="event-title">{{ properties.label|default('Événement sans titre') }}</h1>
<p class="event-subtitle">ID: {{ id }}</p>
<div class="nav-links">
<a href="/demo"><i class="fas fa-home"></i> Accueil</a>
<a href="/demo/view-events"><i class="fas fa-table"></i> Tous les événements</a>
<a href="https://api.openeventdatabase.org/event/{{ id }}" target="_blank"><i class="fas fa-code"></i> API JSON</a>
</div>
</div>
{% endblock %}
{% block content %}
<div id="map"></div>
<table>
<thead><tr><th>Clé</th><th>Valeur</th></tr></thead>
<tbody>
{% for key, value in properties.items()|sort %}
<tr>
<td>{{ key }}</td>
<td>{{ value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<div id="loading-map" class="loading">
<i class="fas fa-spinner fa-spin"></i>
Chargement de la carte...
</div>
<div id="map-error" class="error-message" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
Erreur lors du chargement de la carte
</div>
<div id="map" style="display: none;"></div>
<div class="properties-table">
<div class="table-header">
<h2><i class="fas fa-info-circle"></i> Propriétés de l'événement</h2>
</div>
<table>
<thead>
<tr>
<th>Propriété</th>
<th>Valeur</th>
</tr>
</thead>
<tbody>
{% for key, value in properties.items()|sort %}
<tr>
<td><strong>{{ key }}</strong></td>
<td>
{% if key == 'what' %}
<a href="/demo/map-by-what/{{ value }}" class="what-link">
<i class="fas fa-tag"></i> {{ value }}
</a>
<small style="display: block; margin-top: 5px; color: #6c757d;">
Cliquez pour voir tous les événements de ce type
</small>
{% elif key in ['start', 'stop', 'createdate', 'lastupdate'] and value %}
{{ value }}
<small style="display: block; color: #6c757d;">
<script>
document.write(formatDate('{{ value }}'));
</script>
</small>
{% elif key == 'url' and value %}
<a href="{{ value }}" target="_blank" style="color: #0078ff;">
{{ value }}
<i class="fas fa-external-link-alt" style="font-size: 0.8em;"></i>
</a>
{% elif value is string and value.startswith('http') %}
<a href="{{ value }}" target="_blank" style="color: #0078ff;">
{{ value }}
<i class="fas fa-external-link-alt" style="font-size: 0.8em;"></i>
</a>
{% else %}
{{ value|safe if value else '-' }}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
{% block scripts %}
<script>
const f = JSON.parse('{{ feature_json|safe }}');
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: f.geometry && f.geometry.coordinates ? f.geometry.coordinates : [2.3522,48.8566],
zoom: 12
});
map.addControl(new maplibregl.NavigationControl());
if (f.geometry && f.geometry.type === 'Point') {
new maplibregl.Marker().setLngLat(f.geometry.coordinates).addTo(map);
// Fonction pour formater les dates
function formatDate(dateString) {
try {
const date = new Date(dateString);
return date.toLocaleDateString('fr-FR', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch (e) {
return dateString;
}
}
// Formater les dates dans le tableau après le chargement de la page
function formatDatesInTable() {
const dateElements = document.querySelectorAll('.date-display[data-date]');
dateElements.forEach(element => {
const dateValue = element.getAttribute('data-date');
if (dateValue) {
const formattedDate = formatDate(dateValue);
element.textContent = formattedDate;
element.title = dateValue; // Garder la date originale en tooltip
}
});
}
// Gestion des erreurs et logging
function logError(message, error) {
console.error(message, error);
document.getElementById('loading-map').style.display = 'none';
document.getElementById('map-error').style.display = 'block';
document.getElementById('map-error').innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
${message}: ${error.message || error}
`;
}
// Formater les dates dans le tableau
formatDatesInTable();
// Initialisation de la carte
try {
console.log('🗺️ Initialisation de la carte...');
// Parser les données de l'événement de manière sécurisée
let eventFeature;
try {
const featureJson = '{{ feature_json|safe }}';
console.log('📄 JSON reçu:', featureJson);
eventFeature = JSON.parse(featureJson);
console.log('✅ Données de l\'événement parsées:', eventFeature);
} catch (parseError) {
throw new Error(`Impossible de parser les données JSON: ${parseError.message}`);
}
// Vérifier la structure des données
if (!eventFeature) {
throw new Error('Aucune donnée d\'événement trouvée');
}
// Déterminer les coordonnées pour la carte
let center = [2.3522, 48.8566]; // Paris par défaut
let hasValidGeometry = false;
if (eventFeature.geometry &&
eventFeature.geometry.type === 'Point' &&
eventFeature.geometry.coordinates &&
Array.isArray(eventFeature.geometry.coordinates) &&
eventFeature.geometry.coordinates.length === 2) {
const coords = eventFeature.geometry.coordinates;
// Vérifier que les coordonnées sont des nombres valides
if (typeof coords[0] === 'number' && typeof coords[1] === 'number' &&
!isNaN(coords[0]) && !isNaN(coords[1])) {
center = coords;
hasValidGeometry = true;
console.log('📍 Coordonnées trouvées:', center);
}
}
if (!hasValidGeometry) {
console.warn('⚠️ Pas de coordonnées valides, utilisation du centre par défaut');
}
// Créer la carte
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: center,
zoom: hasValidGeometry ? 14 : 6
});
// Ajouter les contrôles
map.addControl(new maplibregl.NavigationControl());
map.addControl(new maplibregl.AttributionControl({
customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}));
// Quand la carte est chargée
map.on('load', function() {
console.log('✅ Carte chargée avec succès');
document.getElementById('loading-map').style.display = 'none';
document.getElementById('map').style.display = 'block';
// Ajouter le marqueur si on a des coordonnées valides
if (hasValidGeometry) {
const marker = new maplibregl.Marker({
color: '#0078ff'
})
.setLngLat(center)
.addTo(map);
// Créer une popup avec les informations de l'événement
const properties = eventFeature.properties || {};
const popupContent = `
<div style="max-width: 250px;">
<h3 style="margin: 0 0 10px 0;">${properties.label || 'Événement'}</h3>
${properties.what ? `<p><strong>Type:</strong> ${properties.what}</p>` : ''}
${properties.start ? `<p><strong>Début:</strong> ${formatDate(properties.start)}</p>` : ''}
${properties.where ? `<p><strong>Lieu:</strong> ${properties.where}</p>` : ''}
</div>
`;
const popup = new maplibregl.Popup({ offset: 25 })
.setHTML(popupContent);
marker.setPopup(popup);
console.log('📍 Marqueur ajouté à la carte');
}
});
// Gestion des erreurs de carte
map.on('error', function(e) {
logError('Erreur lors du chargement de la carte', e.error || e);
});
} catch (error) {
logError('Erreur lors de l\'initialisation de la carte', error);
}
</script>
{% endblock %}

View file

@ -7,7 +7,7 @@
<!-- Common CSS -->
<link rel="stylesheet" href="/static/demo_styles.css">
<link rel="icon" type="image/png" href="/static/oedb.png">
<!-- Page-specific CSS -->
{% block css %}{% endblock %}

View file

@ -0,0 +1,533 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Événements {{ event_type_label }} - OpenEventDatabase</title>
<link rel="icon" type="image/png" href="/static/oedb.png">
<!-- MapLibre GL JS -->
<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" />
<!-- Font Awesome -->
<script defer src="https://use.fontawesome.com/releases/v5.15.4/js/all.js"></script>
<style>
body {
margin: 0;
padding: 20px;
font-family: Arial, sans-serif;
background-color: #f8f9fa;
}
.header {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.header h1 {
margin: 0 0 10px 0;
color: #333;
}
.nav-links {
margin-top: 15px;
}
.nav-links a {
color: #0078ff;
text-decoration: none;
margin-right: 15px;
font-weight: 500;
}
.nav-links a:hover {
text-decoration: underline;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
height: calc(100vh - 200px);
}
.map-container, .table-container {
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
overflow: hidden;
}
#map {
width: 100%;
height: 100%;
}
.table-container {
display: flex;
flex-direction: column;
}
.table-header {
padding: 20px;
border-bottom: 1px solid #eee;
background: #f8f9fa;
}
.table-header h2 {
margin: 0;
color: #333;
}
.legend {
display: flex;
gap: 20px;
margin-top: 10px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 0 3px rgba(0,0,0,0.3);
}
.legend-past { background-color: #6c757d; }
.legend-current { background-color: #28a745; }
.legend-future { background-color: #007bff; }
.table-content {
flex: 1;
overflow-y: auto;
padding: 0;
}
.events-table {
width: 100%;
border-collapse: collapse;
}
.events-table th,
.events-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.events-table th {
background: #f8f9fa;
font-weight: 600;
color: #495057;
position: sticky;
top: 0;
z-index: 1;
}
.events-table tr:hover {
background: #f8f9fa;
}
.event-title {
font-weight: 600;
color: #333;
}
.event-title a {
color: #0078ff;
text-decoration: none;
}
.event-title a:hover {
text-decoration: underline;
}
.event-date {
font-size: 14px;
color: #6c757d;
}
.event-status {
display: inline-block;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
}
.status-past {
background: #6c757d;
color: white;
}
.status-current {
background: #28a745;
color: white;
}
.status-future {
background: #007bff;
color: white;
}
.loading {
text-align: center;
padding: 40px;
color: #6c757d;
}
.error {
text-align: center;
padding: 40px;
color: #dc3545;
}
.stats {
display: flex;
gap: 20px;
margin-top: 10px;
font-size: 14px;
color: #6c757d;
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
height: auto;
}
.map-container {
height: 400px;
}
.table-container {
height: 500px;
}
}
</style>
</head>
<body>
<div class="header">
<h1>
<i class="fas fa-map-marked-alt"></i>
Événements {{ event_type_label }}
</h1>
<p>Visualisation et liste détaillée des événements de type "{{ event_type }}"</p>
<div class="nav-links">
<a href="/demo"><i class="fas fa-home"></i> Accueil</a>
<a href="/demo/map-by-what"><i class="fas fa-list"></i> Tous les types</a>
<a href="/demo/view-events"><i class="fas fa-table"></i> Vue tableau</a>
</div>
</div>
<div class="content-grid">
<div class="map-container">
<div id="map"></div>
</div>
<div class="table-container">
<div class="table-header">
<h2><i class="fas fa-table"></i> Liste des événements</h2>
<div class="legend">
<div class="legend-item">
<div class="legend-color legend-past"></div>
<span>Passés</span>
</div>
<div class="legend-item">
<div class="legend-color legend-current"></div>
<span>En cours</span>
</div>
<div class="legend-item">
<div class="legend-color legend-future"></div>
<span>À venir</span>
</div>
</div>
<div class="stats">
<span id="total-events">Total: 0</span>
<span id="past-events">Passés: 0</span>
<span id="current-events">En cours: 0</span>
<span id="future-events">À venir: 0</span>
</div>
</div>
<div class="table-content">
<div id="loading" class="loading">
<i class="fas fa-spinner fa-spin"></i>
Chargement des événements...
</div>
<div id="error" class="error" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
Erreur lors du chargement des événements
</div>
<table class="events-table" id="events-table" style="display: none;">
<thead>
<tr>
<th>Statut</th>
<th>Événement</th>
<th>Dates</th>
<th>Lieu</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="events-tbody">
</tbody>
</table>
</div>
</div>
</div>
<script>
// Configuration
const eventType = '{{ event_type }}';
let map;
let markers = [];
let eventsData = [];
// Initialiser la carte
function initMap() {
map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [2.3522, 48.8566], // Paris par défaut
zoom: 6
});
map.addControl(new maplibregl.NavigationControl());
map.addControl(new maplibregl.AttributionControl({
customAttribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}));
}
// Déterminer le statut temporel d'un événement
function getEventStatus(start, stop) {
const now = new Date();
const startDate = new Date(start);
const stopDate = new Date(stop);
if (now < startDate) {
return 'future';
} else if (now > stopDate) {
return 'past';
} else {
return 'current';
}
}
// Obtenir la couleur selon le statut
function getStatusColor(status) {
switch (status) {
case 'past': return '#6c757d';
case 'current': return '#28a745';
case 'future': return '#007bff';
default: return '#6c757d';
}
}
// Obtenir le label selon le statut
function getStatusLabel(status) {
switch (status) {
case 'past': return 'Passé';
case 'current': return 'En cours';
case 'future': return 'À venir';
default: return 'Inconnu';
}
}
// Ajouter les marqueurs sur la carte
function addMarkersToMap(events) {
// Supprimer les anciens marqueurs
markers.forEach(marker => marker.remove());
markers = [];
const bounds = new maplibregl.LngLatBounds();
events.forEach(event => {
if (!event.geometry || !event.geometry.coordinates) return;
const coords = event.geometry.coordinates;
const properties = event.properties || {};
const status = getEventStatus(properties.start, properties.stop);
const color = getStatusColor(status);
// Créer l'élément marqueur
const el = document.createElement('div');
el.style.width = '20px';
el.style.height = '20px';
el.style.borderRadius = '50%';
el.style.backgroundColor = color;
el.style.border = '3px solid white';
el.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)';
el.style.cursor = 'pointer';
// Créer le contenu de la popup
const popupContent = `
<div style="max-width: 300px;">
<h3 style="margin: 0 0 10px 0;">${properties.label || 'Événement'}</h3>
<p><strong>Statut:</strong> <span class="event-status status-${status}">${getStatusLabel(status)}</span></p>
<p><strong>Début:</strong> ${new Date(properties.start).toLocaleString('fr-FR')}</p>
<p><strong>Fin:</strong> ${new Date(properties.stop).toLocaleString('fr-FR')}</p>
${properties.where ? `<p><strong>Lieu:</strong> ${properties.where}</p>` : ''}
${properties.description ? `<p><strong>Description:</strong> ${properties.description}</p>` : ''}
<div style="margin-top: 15px;">
<a href="/demo/by_id/${properties.id}" style="color: #0078ff; font-weight: bold;">Voir les détails</a>
</div>
</div>
`;
const popup = new maplibregl.Popup({ offset: 25 })
.setHTML(popupContent);
const marker = new maplibregl.Marker(el)
.setLngLat(coords)
.setPopup(popup)
.addTo(map);
markers.push(marker);
bounds.extend(coords);
});
// Ajuster la vue pour englober tous les marqueurs
if (events.length > 0) {
map.fitBounds(bounds, { padding: 50 });
}
}
// Remplir le tableau
function populateTable(events) {
const tbody = document.getElementById('events-tbody');
tbody.innerHTML = '';
// Trier par date (futurs d'abord, puis en cours, puis passés)
const sortedEvents = events.sort((a, b) => {
const statusA = getEventStatus(a.properties.start, a.properties.stop);
const statusB = getEventStatus(b.properties.start, b.properties.stop);
const statusOrder = { 'future': 0, 'current': 1, 'past': 2 };
if (statusOrder[statusA] !== statusOrder[statusB]) {
return statusOrder[statusA] - statusOrder[statusB];
}
return new Date(a.properties.start) - new Date(b.properties.start);
});
sortedEvents.forEach(event => {
const props = event.properties || {};
const status = getEventStatus(props.start, props.stop);
const row = document.createElement('tr');
row.innerHTML = `
<td>
<span class="event-status status-${status}">${getStatusLabel(status)}</span>
</td>
<td>
<div class="event-title">
<a href="/demo/by_id/${props.id}">${props.label || 'Événement sans titre'}</a>
</div>
</td>
<td>
<div class="event-date">
<strong>Début:</strong> ${new Date(props.start).toLocaleDateString('fr-FR')}<br>
<strong>Fin:</strong> ${new Date(props.stop).toLocaleDateString('fr-FR')}
</div>
</td>
<td>${props.where || '-'}</td>
<td>
<a href="/demo/by_id/${props.id}" style="color: #0078ff; text-decoration: none;">
<i class="fas fa-eye"></i> Détails
</a>
</td>
`;
tbody.appendChild(row);
});
}
// Mettre à jour les statistiques
function updateStats(events) {
const stats = {
total: events.length,
past: 0,
current: 0,
future: 0
};
events.forEach(event => {
const status = getEventStatus(event.properties.start, event.properties.stop);
stats[status]++;
});
document.getElementById('total-events').textContent = `Total: ${stats.total}`;
document.getElementById('past-events').textContent = `Passés: ${stats.past}`;
document.getElementById('current-events').textContent = `En cours: ${stats.current}`;
document.getElementById('future-events').textContent = `À venir: ${stats.future}`;
}
// Charger les événements
async function loadEvents() {
try {
const response = await fetch(`https://api.openeventdatabase.org/event?what=${eventType}&limit=1000`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
eventsData = data.features || [];
// Masquer le loading
document.getElementById('loading').style.display = 'none';
if (eventsData.length === 0) {
document.getElementById('error').style.display = 'block';
document.getElementById('error').innerHTML = `
<i class="fas fa-info-circle"></i>
Aucun événement trouvé pour le type "${eventType}"
`;
return;
}
// Afficher le tableau
document.getElementById('events-table').style.display = 'table';
// Peupler la carte et le tableau
addMarkersToMap(eventsData);
populateTable(eventsData);
updateStats(eventsData);
} catch (error) {
console.error('Erreur lors du chargement des événements:', error);
document.getElementById('loading').style.display = 'none';
document.getElementById('error').style.display = 'block';
document.getElementById('error').innerHTML = `
<i class="fas fa-exclamation-triangle"></i>
Erreur lors du chargement: ${error.message}
`;
}
}
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
initMap();
loadEvents();
});
</script>
</body>
</html>

View file

@ -4,13 +4,14 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Report Traffic Jam - OpenEventDatabase</title>
<link rel="icon" type="image/png" href="/static/oedb.png">
<link rel="icon" type="image/png" href="/static/oedb.png">
<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">

View file

@ -260,7 +260,7 @@
<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>
<button id="report_issue_button" type="submit">Signaler le problème</button>
</form>
<div id="result"></div>