Spaces:
Sleeping
Sleeping
tyriaa
commited on
Commit
·
8b1e2ee
1
Parent(s):
83e56c3
4rd commitekk
Browse files- Dockerfile +24 -0
- __pycache__/get_arrivals.cpython-310.pyc +0 -0
- __pycache__/get_departures.cpython-310.pyc +0 -0
- __pycache__/get_perturbations.cpython-310.pyc +0 -0
- app.py +100 -0
- get_departures.py +65 -0
- get_perturbations.py +90 -0
- requirements.txt +5 -0
- static/css/style.css +195 -0
- static/js/traffic.js +85 -0
- templates/lille_traffic.html +360 -0
Dockerfile
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Utiliser une image Python officielle comme image de base
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Définir le répertoire de travail dans le conteneur
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copier les fichiers de dépendances
|
8 |
+
COPY requirements.txt .
|
9 |
+
|
10 |
+
# Installer les dépendances
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Copier le reste du code de l'application
|
14 |
+
COPY . .
|
15 |
+
|
16 |
+
# Variables d'environnement
|
17 |
+
ENV FLASK_APP=app.py
|
18 |
+
ENV FLASK_ENV=production
|
19 |
+
|
20 |
+
# Exposer le port sur lequel l'application s'exécute
|
21 |
+
EXPOSE 5003
|
22 |
+
|
23 |
+
# Commande pour démarrer l'application avec Gunicorn
|
24 |
+
CMD ["gunicorn", "--bind", "0.0.0.0:5003", "app:app"]
|
__pycache__/get_arrivals.cpython-310.pyc
ADDED
Binary file (1.68 kB). View file
|
|
__pycache__/get_departures.cpython-310.pyc
ADDED
Binary file (1.69 kB). View file
|
|
__pycache__/get_perturbations.cpython-310.pyc
ADDED
Binary file (3.27 kB). View file
|
|
app.py
ADDED
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, jsonify, render_template
|
2 |
+
import os
|
3 |
+
from dotenv import load_dotenv
|
4 |
+
from get_perturbations import PerturbationScraper
|
5 |
+
from get_departures import get_all_departures
|
6 |
+
|
7 |
+
# Charger les variables d'environnement
|
8 |
+
load_dotenv()
|
9 |
+
|
10 |
+
app = Flask(__name__)
|
11 |
+
|
12 |
+
TOMTOM_API_KEY = os.getenv("TOMTOM_API_KEY")
|
13 |
+
app.config['TOMTOM_API_KEY'] = os.getenv('TOMTOM_API_KEY')
|
14 |
+
app.config['NAVITIA_API_KEY'] = os.getenv('NAVITIA_API_KEY', '34aa3a57-969c-40bf-b05a-cc2d8beb17f7')
|
15 |
+
|
16 |
+
# Coordonnées du centre de Lille
|
17 |
+
LILLE_CENTER = {"lat": 50.6292, "lon": 3.0573}
|
18 |
+
|
19 |
+
def get_lille_traffic():
|
20 |
+
"""
|
21 |
+
Récupère les informations de trafic de Lille
|
22 |
+
Returns:
|
23 |
+
dict: Informations de trafic organisées par type de transport
|
24 |
+
"""
|
25 |
+
scraper = PerturbationScraper()
|
26 |
+
perturbations = scraper.get_perturbations()
|
27 |
+
|
28 |
+
# Organiser les perturbations par type de transport
|
29 |
+
organized_traffic = {
|
30 |
+
'metro': {'name': 'Métro', 'lines': []},
|
31 |
+
'tram': {'name': 'Tramway', 'lines': []},
|
32 |
+
'bus': {'name': 'Bus', 'lines': []}
|
33 |
+
}
|
34 |
+
|
35 |
+
type_mapping = {
|
36 |
+
'metro_lines': 'metro',
|
37 |
+
'tram_lines': 'tram',
|
38 |
+
'bus_lines': 'bus'
|
39 |
+
}
|
40 |
+
|
41 |
+
for p in perturbations:
|
42 |
+
line_type = type_mapping.get(p['line_type'])
|
43 |
+
if line_type in organized_traffic:
|
44 |
+
organized_traffic[line_type]['lines'].append({
|
45 |
+
'name': p['line'],
|
46 |
+
'trace': p['line_trace'],
|
47 |
+
'alerts': p['alerts']
|
48 |
+
})
|
49 |
+
|
50 |
+
return organized_traffic
|
51 |
+
|
52 |
+
@app.route('/')
|
53 |
+
def index():
|
54 |
+
"""Page d'accueil avec la carte et les perturbations"""
|
55 |
+
traffic_info = get_lille_traffic()
|
56 |
+
return render_template('lille_traffic.html',
|
57 |
+
api_key=TOMTOM_API_KEY,
|
58 |
+
center=LILLE_CENTER,
|
59 |
+
transport_types=traffic_info)
|
60 |
+
|
61 |
+
@app.route('/api/traffic')
|
62 |
+
def get_traffic_api():
|
63 |
+
"""
|
64 |
+
Endpoint API pour obtenir les informations de trafic
|
65 |
+
"""
|
66 |
+
traffic_info = get_lille_traffic()
|
67 |
+
return jsonify(traffic_info)
|
68 |
+
|
69 |
+
@app.route('/api/perturbations/<line_name>')
|
70 |
+
def get_line_perturbations(line_name):
|
71 |
+
"""
|
72 |
+
Endpoint API pour obtenir les perturbations d'une ligne spécifique
|
73 |
+
"""
|
74 |
+
scraper = PerturbationScraper()
|
75 |
+
perturbations = scraper.get_perturbations()
|
76 |
+
|
77 |
+
line_perturbations = next(
|
78 |
+
(p for p in perturbations if p['line'] == line_name),
|
79 |
+
None
|
80 |
+
)
|
81 |
+
|
82 |
+
if line_perturbations:
|
83 |
+
return jsonify({
|
84 |
+
'line': line_name,
|
85 |
+
'trace': line_perturbations['line_trace'],
|
86 |
+
'alerts': line_perturbations['alerts']
|
87 |
+
})
|
88 |
+
else:
|
89 |
+
return jsonify({'error': 'Ligne non trouvée'}), 404
|
90 |
+
|
91 |
+
@app.route('/api/departures')
|
92 |
+
def get_station_departures():
|
93 |
+
try:
|
94 |
+
departures = get_all_departures(app.config['NAVITIA_API_KEY'])
|
95 |
+
return jsonify(departures)
|
96 |
+
except Exception as e:
|
97 |
+
return jsonify({'error': str(e)}), 500
|
98 |
+
|
99 |
+
if __name__ == '__main__':
|
100 |
+
app.run(debug=True, port=5003)
|
get_departures.py
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from datetime import datetime
|
3 |
+
import pytz
|
4 |
+
|
5 |
+
def get_station_departures(api_key, station_id):
|
6 |
+
"""
|
7 |
+
Récupère les prochains départs d'une gare spécifique
|
8 |
+
"""
|
9 |
+
headers = {
|
10 |
+
'Authorization': api_key
|
11 |
+
}
|
12 |
+
|
13 |
+
# On utilise l'API des départs pour la gare spécifique
|
14 |
+
url = f'https://api.navitia.io/v1/coverage/sncf/stop_points/{station_id}/departures'
|
15 |
+
|
16 |
+
try:
|
17 |
+
response = requests.get(url, headers=headers)
|
18 |
+
response.raise_for_status()
|
19 |
+
|
20 |
+
data = response.json()
|
21 |
+
departures = []
|
22 |
+
|
23 |
+
for departure in data.get('departures', []):
|
24 |
+
# On extrait les informations pertinentes
|
25 |
+
departure_time = departure['stop_date_time']['departure_date_time']
|
26 |
+
direction = departure['display_informations']['direction']
|
27 |
+
line_name = departure['display_informations']['commercial_mode']
|
28 |
+
headsign = departure['display_informations']['headsign']
|
29 |
+
|
30 |
+
# Conversion du temps de départ
|
31 |
+
dt = datetime.strptime(departure_time, "%Y%m%dT%H%M%S")
|
32 |
+
paris_tz = pytz.timezone('Europe/Paris')
|
33 |
+
dt = paris_tz.localize(dt)
|
34 |
+
formatted_time = dt.strftime("%H:%M")
|
35 |
+
|
36 |
+
departures.append({
|
37 |
+
'time': formatted_time,
|
38 |
+
'direction': direction,
|
39 |
+
'type': line_name,
|
40 |
+
'train_number': headsign
|
41 |
+
})
|
42 |
+
|
43 |
+
return departures
|
44 |
+
|
45 |
+
except requests.exceptions.RequestException as e:
|
46 |
+
print(f"Erreur lors de la récupération des départs: {e}")
|
47 |
+
return []
|
48 |
+
|
49 |
+
def get_all_departures(api_key):
|
50 |
+
"""
|
51 |
+
Récupère les départs des deux gares principales de Lille
|
52 |
+
"""
|
53 |
+
# IDs des gares de Lille
|
54 |
+
stations = {
|
55 |
+
'Lille Europe': 'stop_point:SNCF:87223263:Train',
|
56 |
+
'Lille Flandres': 'stop_point:SNCF:87286005:Train'
|
57 |
+
}
|
58 |
+
|
59 |
+
all_departures = {}
|
60 |
+
|
61 |
+
for station_name, station_id in stations.items():
|
62 |
+
departures = get_station_departures(api_key, station_id)
|
63 |
+
all_departures[station_name] = departures
|
64 |
+
|
65 |
+
return all_departures
|
get_perturbations.py
ADDED
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
from datetime import datetime
|
3 |
+
|
4 |
+
class PerturbationScraper:
|
5 |
+
def __init__(self):
|
6 |
+
self.headers = {
|
7 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
8 |
+
}
|
9 |
+
self.api_url = 'https://mobilille.fr/api/v1/lines'
|
10 |
+
|
11 |
+
def get_perturbations(self):
|
12 |
+
try:
|
13 |
+
response = requests.get(self.api_url, headers=self.headers)
|
14 |
+
response.raise_for_status()
|
15 |
+
lines = response.json()
|
16 |
+
|
17 |
+
perturbations = []
|
18 |
+
|
19 |
+
for line in lines:
|
20 |
+
# Filtrer les alertes importantes (perturbations, déviations, travaux)
|
21 |
+
important_alerts = [
|
22 |
+
alert for alert in line.get('alerts', [])
|
23 |
+
if any(keyword in alert.get('alert_title', '').lower()
|
24 |
+
for keyword in ['perturbation', 'déviation', 'travaux'])
|
25 |
+
and 'descente à la demande' not in alert.get('alert_title', '').lower()
|
26 |
+
and 'forte affluence' not in alert.get('alert_title', '').lower()
|
27 |
+
]
|
28 |
+
|
29 |
+
if important_alerts:
|
30 |
+
perturbation = {
|
31 |
+
'line': line.get('line_name', 'Inconnue'),
|
32 |
+
'line_trace': line.get('line_trace', ''),
|
33 |
+
'line_type': line.get('line_type', ''),
|
34 |
+
'alerts': []
|
35 |
+
}
|
36 |
+
|
37 |
+
for alert in important_alerts:
|
38 |
+
date_modif = datetime.strptime(alert.get('date_modif', ''), '%Y-%m-%d %H:%M:%S')
|
39 |
+
formatted_time = date_modif.strftime('%d/%m/%Y à %H:%M')
|
40 |
+
|
41 |
+
perturbation['alerts'].append({
|
42 |
+
'title': alert.get('alert_title', ''),
|
43 |
+
'description': alert.get('message_app', ''),
|
44 |
+
'update_time': formatted_time
|
45 |
+
})
|
46 |
+
|
47 |
+
perturbations.append(perturbation)
|
48 |
+
|
49 |
+
# Trier les perturbations par type de ligne (métro, tram, bus)
|
50 |
+
type_order = {'metro_lines': 0, 'tram_lines': 1, 'bus_lines': 2}
|
51 |
+
perturbations.sort(key=lambda x: type_order.get(x['line_type'], 999))
|
52 |
+
|
53 |
+
return perturbations
|
54 |
+
|
55 |
+
except Exception as e:
|
56 |
+
print(f"Erreur lors de la récupération des perturbations : {e}")
|
57 |
+
return []
|
58 |
+
|
59 |
+
def main():
|
60 |
+
scraper = PerturbationScraper()
|
61 |
+
perturbations = scraper.get_perturbations()
|
62 |
+
|
63 |
+
print("\n=== Perturbations en cours ===\n")
|
64 |
+
if not perturbations:
|
65 |
+
print("Aucune perturbation n'est actuellement signalée.")
|
66 |
+
else:
|
67 |
+
current_type = None
|
68 |
+
for p in perturbations:
|
69 |
+
# Afficher un en-tête pour chaque type de transport
|
70 |
+
line_type = p['line_type']
|
71 |
+
if line_type != current_type:
|
72 |
+
if current_type is not None:
|
73 |
+
print("\n" + "=" * 50 + "\n")
|
74 |
+
current_type = line_type
|
75 |
+
type_name = {
|
76 |
+
'metro_lines': '🚇 Métro',
|
77 |
+
'tram_lines': '🚊 Tramway',
|
78 |
+
'bus_lines': '🚌 Bus'
|
79 |
+
}.get(line_type, 'Autre')
|
80 |
+
print(f"{type_name}:")
|
81 |
+
|
82 |
+
print(f"\nLigne {p['line']} - {p['line_trace']}")
|
83 |
+
for alert in p['alerts']:
|
84 |
+
print(f"\n🚨 {alert['title']}")
|
85 |
+
print(f"{alert['description']}")
|
86 |
+
print(f"\nℹ️ Mise à jour : {alert['update_time']}")
|
87 |
+
print("-" * 50)
|
88 |
+
|
89 |
+
if __name__ == "__main__":
|
90 |
+
main()
|
requirements.txt
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
Flask==3.0.0
|
2 |
+
requests==2.31.0
|
3 |
+
python-dotenv==1.0.0
|
4 |
+
pytz==2023.3
|
5 |
+
gunicorn==21.2.0
|
static/css/style.css
ADDED
@@ -0,0 +1,195 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
* {
|
2 |
+
margin: 0;
|
3 |
+
padding: 0;
|
4 |
+
box-sizing: border-box;
|
5 |
+
}
|
6 |
+
|
7 |
+
body {
|
8 |
+
font-family: 'Open Sans', sans-serif;
|
9 |
+
line-height: 1.6;
|
10 |
+
color: #333;
|
11 |
+
background-color: #f0f2f5;
|
12 |
+
min-height: 100vh;
|
13 |
+
}
|
14 |
+
|
15 |
+
/* En-tête de page */
|
16 |
+
.page-header {
|
17 |
+
background: #fff;
|
18 |
+
padding: 1rem;
|
19 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
20 |
+
text-align: center;
|
21 |
+
}
|
22 |
+
|
23 |
+
.page-header h1 {
|
24 |
+
font-size: 1.75rem;
|
25 |
+
color: #1a1a1a;
|
26 |
+
margin-bottom: 0.25rem;
|
27 |
+
}
|
28 |
+
|
29 |
+
.last-update {
|
30 |
+
font-size: 0.9rem;
|
31 |
+
color: #666;
|
32 |
+
}
|
33 |
+
|
34 |
+
/* Container principal */
|
35 |
+
.container {
|
36 |
+
display: flex;
|
37 |
+
padding: 1rem;
|
38 |
+
gap: 1rem;
|
39 |
+
min-height: calc(100vh - 100px);
|
40 |
+
}
|
41 |
+
|
42 |
+
/* Panel de trafic */
|
43 |
+
.traffic-panel {
|
44 |
+
flex: 0 0 350px;
|
45 |
+
background: #fff;
|
46 |
+
border-radius: 8px;
|
47 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
48 |
+
padding: 1rem;
|
49 |
+
height: fit-content;
|
50 |
+
}
|
51 |
+
|
52 |
+
.traffic-info h2 {
|
53 |
+
font-size: 1.25rem;
|
54 |
+
margin-bottom: 1rem;
|
55 |
+
color: #1a1a1a;
|
56 |
+
}
|
57 |
+
|
58 |
+
/* Sections de transport */
|
59 |
+
.transport-section {
|
60 |
+
margin-bottom: 1.5rem;
|
61 |
+
}
|
62 |
+
|
63 |
+
.transport-section h3 {
|
64 |
+
font-size: 1.1rem;
|
65 |
+
color: #333;
|
66 |
+
margin-bottom: 0.75rem;
|
67 |
+
padding-bottom: 0.5rem;
|
68 |
+
border-bottom: 1px solid #eee;
|
69 |
+
}
|
70 |
+
|
71 |
+
/* Liste des lignes */
|
72 |
+
.line-list {
|
73 |
+
display: flex;
|
74 |
+
flex-wrap: wrap;
|
75 |
+
gap: 0.5rem;
|
76 |
+
}
|
77 |
+
|
78 |
+
.line-button {
|
79 |
+
padding: 0.5rem 1rem;
|
80 |
+
border: none;
|
81 |
+
border-radius: 4px;
|
82 |
+
background: #f5f5f5;
|
83 |
+
color: #333;
|
84 |
+
font-size: 0.9rem;
|
85 |
+
cursor: pointer;
|
86 |
+
transition: all 0.2s ease;
|
87 |
+
}
|
88 |
+
|
89 |
+
.line-button:hover {
|
90 |
+
background: #e0e0e0;
|
91 |
+
}
|
92 |
+
|
93 |
+
.line-button.active {
|
94 |
+
background: #007bff;
|
95 |
+
color: #fff;
|
96 |
+
}
|
97 |
+
|
98 |
+
/* Message pas de perturbation */
|
99 |
+
.no-disruption {
|
100 |
+
color: #666;
|
101 |
+
font-style: italic;
|
102 |
+
padding: 0.5rem 0;
|
103 |
+
}
|
104 |
+
|
105 |
+
/* Détails des perturbations */
|
106 |
+
.perturbation-details {
|
107 |
+
margin-top: 1rem;
|
108 |
+
padding-top: 1rem;
|
109 |
+
border-top: 1px solid #eee;
|
110 |
+
}
|
111 |
+
|
112 |
+
.details-header {
|
113 |
+
margin-bottom: 1rem;
|
114 |
+
}
|
115 |
+
|
116 |
+
.details-header h3 {
|
117 |
+
font-size: 1.1rem;
|
118 |
+
color: #1a1a1a;
|
119 |
+
margin-bottom: 0.25rem;
|
120 |
+
}
|
121 |
+
|
122 |
+
.line-trace {
|
123 |
+
font-size: 0.9rem;
|
124 |
+
color: #666;
|
125 |
+
}
|
126 |
+
|
127 |
+
/* Alertes */
|
128 |
+
.alert {
|
129 |
+
margin-bottom: 1rem;
|
130 |
+
padding: 1rem;
|
131 |
+
background: #fff3cd;
|
132 |
+
border-left: 4px solid #ffc107;
|
133 |
+
border-radius: 4px;
|
134 |
+
}
|
135 |
+
|
136 |
+
.alert-title {
|
137 |
+
font-weight: 600;
|
138 |
+
color: #856404;
|
139 |
+
margin-bottom: 0.5rem;
|
140 |
+
}
|
141 |
+
|
142 |
+
.alert-description {
|
143 |
+
color: #333;
|
144 |
+
font-size: 0.95rem;
|
145 |
+
margin-bottom: 0.5rem;
|
146 |
+
}
|
147 |
+
|
148 |
+
.alert-time {
|
149 |
+
font-size: 0.85rem;
|
150 |
+
color: #666;
|
151 |
+
}
|
152 |
+
|
153 |
+
/* Container de la carte */
|
154 |
+
.map-container {
|
155 |
+
flex: 1;
|
156 |
+
min-height: calc(100vh - 132px);
|
157 |
+
border-radius: 8px;
|
158 |
+
overflow: hidden;
|
159 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
160 |
+
}
|
161 |
+
|
162 |
+
#leaflet-map {
|
163 |
+
width: 100%;
|
164 |
+
height: 100%;
|
165 |
+
}
|
166 |
+
|
167 |
+
/* Styles spécifiques pour les types de transport */
|
168 |
+
.line-button.metro {
|
169 |
+
border-left: 3px solid #e31837;
|
170 |
+
}
|
171 |
+
|
172 |
+
.line-button.tram {
|
173 |
+
border-left: 3px solid #e17000;
|
174 |
+
}
|
175 |
+
|
176 |
+
.line-button.bus {
|
177 |
+
border-left: 3px solid #007ac9;
|
178 |
+
}
|
179 |
+
|
180 |
+
/* Responsive */
|
181 |
+
@media (max-width: 1024px) {
|
182 |
+
.container {
|
183 |
+
flex-direction: column;
|
184 |
+
}
|
185 |
+
|
186 |
+
.traffic-panel {
|
187 |
+
flex: none;
|
188 |
+
width: 100%;
|
189 |
+
}
|
190 |
+
|
191 |
+
.map-container {
|
192 |
+
height: 400px;
|
193 |
+
min-height: 400px;
|
194 |
+
}
|
195 |
+
}
|
static/js/traffic.js
ADDED
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
let map;
|
2 |
+
let markers = [];
|
3 |
+
|
4 |
+
function initMap(apiKey) {
|
5 |
+
// Coordonnées inversées pour TomTom (longitude, latitude)
|
6 |
+
const LILLE_CENTER = [50.6292, 3.0573];
|
7 |
+
|
8 |
+
map = tt.map({
|
9 |
+
key: apiKey,
|
10 |
+
container: 'map',
|
11 |
+
center: LILLE_CENTER,
|
12 |
+
zoom: 12,
|
13 |
+
style: 'tomtom://vector/1/basic-main'
|
14 |
+
});
|
15 |
+
|
16 |
+
// Ajouter les contrôles de zoom
|
17 |
+
map.addControl(new tt.NavigationControl());
|
18 |
+
|
19 |
+
// Ajouter la couche de trafic
|
20 |
+
map.on('load', function() {
|
21 |
+
map.addTier({
|
22 |
+
source: 'vectorTiles',
|
23 |
+
serviceUrl: 'https://api.tomtom.com/traffic/map/4/tile/flow/absolute/{z}/{x}/{y}.pbf',
|
24 |
+
serviceKey: apiKey,
|
25 |
+
minZoom: 8,
|
26 |
+
maxZoom: 22,
|
27 |
+
style: 'tomtom://traffic-flow'
|
28 |
+
});
|
29 |
+
|
30 |
+
updateTrafficIncidents();
|
31 |
+
});
|
32 |
+
}
|
33 |
+
|
34 |
+
function clearMarkers() {
|
35 |
+
markers.forEach(marker => marker.remove());
|
36 |
+
markers = [];
|
37 |
+
}
|
38 |
+
|
39 |
+
function updateTrafficIncidents() {
|
40 |
+
fetch('/incidents')
|
41 |
+
.then(response => response.json())
|
42 |
+
.then(data => {
|
43 |
+
clearMarkers();
|
44 |
+
if (data.incidents) {
|
45 |
+
data.incidents.forEach(incident => {
|
46 |
+
const coordinates = incident.geometry.coordinates;
|
47 |
+
|
48 |
+
// Créer un marqueur pour chaque incident
|
49 |
+
const marker = new tt.Marker()
|
50 |
+
.setLngLat([coordinates[1], coordinates[0]])
|
51 |
+
.addTo(map);
|
52 |
+
|
53 |
+
markers.push(marker);
|
54 |
+
|
55 |
+
// Créer le contenu du popup
|
56 |
+
let popupContent = `
|
57 |
+
<div class="incident-popup">
|
58 |
+
<h3>Incident</h3>
|
59 |
+
<p>${incident.properties.events[0].description}</p>
|
60 |
+
`;
|
61 |
+
|
62 |
+
if (incident.properties.delay) {
|
63 |
+
popupContent += `<p>Délai: ${incident.properties.delay}</p>`;
|
64 |
+
}
|
65 |
+
|
66 |
+
if (incident.properties.startTime) {
|
67 |
+
const startTime = new Date(incident.properties.startTime);
|
68 |
+
popupContent += `<p>Début: ${startTime.toLocaleString()}</p>`;
|
69 |
+
}
|
70 |
+
|
71 |
+
popupContent += '</div>';
|
72 |
+
|
73 |
+
// Ajouter le popup au marqueur
|
74 |
+
const popup = new tt.Popup({offset: 30})
|
75 |
+
.setHTML(popupContent);
|
76 |
+
|
77 |
+
marker.setPopup(popup);
|
78 |
+
});
|
79 |
+
}
|
80 |
+
})
|
81 |
+
.catch(error => console.error('Erreur:', error));
|
82 |
+
}
|
83 |
+
|
84 |
+
// Mettre à jour les incidents toutes les 5 minutes
|
85 |
+
setInterval(updateTrafficIncidents, 5 * 60 * 1000);
|
templates/lille_traffic.html
ADDED
@@ -0,0 +1,360 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="fr">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Trafic à Lille</title>
|
7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
9 |
+
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
|
10 |
+
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
|
11 |
+
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
12 |
+
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
|
13 |
+
<style>
|
14 |
+
/* Styles de base */
|
15 |
+
* {
|
16 |
+
margin: 0;
|
17 |
+
padding: 0;
|
18 |
+
box-sizing: border-box;
|
19 |
+
}
|
20 |
+
|
21 |
+
body {
|
22 |
+
font-family: 'Open Sans', sans-serif;
|
23 |
+
line-height: 1.6;
|
24 |
+
background: #f5f5f5;
|
25 |
+
}
|
26 |
+
|
27 |
+
.page-header {
|
28 |
+
background-color: #004494;
|
29 |
+
color: white;
|
30 |
+
padding: 1rem;
|
31 |
+
text-align: center;
|
32 |
+
}
|
33 |
+
|
34 |
+
.page-header h1 {
|
35 |
+
margin: 0;
|
36 |
+
font-size: 1.5rem;
|
37 |
+
color: white;
|
38 |
+
}
|
39 |
+
|
40 |
+
.page-header .last-update {
|
41 |
+
color: #e0e0e0;
|
42 |
+
font-size: 0.9rem;
|
43 |
+
margin-top: 0.5rem;
|
44 |
+
}
|
45 |
+
|
46 |
+
.container {
|
47 |
+
display: grid;
|
48 |
+
grid-template-columns: 45% 55%;
|
49 |
+
gap: 1rem;
|
50 |
+
padding: 1rem;
|
51 |
+
height: calc(100vh - 60px);
|
52 |
+
margin: 0 50px;
|
53 |
+
}
|
54 |
+
|
55 |
+
.traffic-panel {
|
56 |
+
background: white;
|
57 |
+
border-radius: 8px;
|
58 |
+
padding: 1.5rem;
|
59 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
60 |
+
overflow-y: auto;
|
61 |
+
height: 100%;
|
62 |
+
}
|
63 |
+
|
64 |
+
/* Styles pour les départs */
|
65 |
+
#departures-container {
|
66 |
+
display: grid;
|
67 |
+
grid-template-columns: 1fr 1fr;
|
68 |
+
gap: 2rem;
|
69 |
+
margin-bottom: 2rem;
|
70 |
+
}
|
71 |
+
|
72 |
+
.station-departures {
|
73 |
+
background: #f8f9fa;
|
74 |
+
border-radius: 8px;
|
75 |
+
padding: 1rem;
|
76 |
+
}
|
77 |
+
|
78 |
+
.station-departures h4 {
|
79 |
+
font-size: 1.2rem;
|
80 |
+
color: #1a1a1a;
|
81 |
+
margin-bottom: 1rem;
|
82 |
+
padding-bottom: 0.5rem;
|
83 |
+
border-bottom: 2px solid #007bff;
|
84 |
+
}
|
85 |
+
|
86 |
+
.departure-item {
|
87 |
+
display: grid;
|
88 |
+
grid-template-columns: auto 1fr;
|
89 |
+
gap: 1rem;
|
90 |
+
padding: 0.8rem;
|
91 |
+
background: white;
|
92 |
+
border-radius: 6px;
|
93 |
+
margin-bottom: 0.5rem;
|
94 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.05);
|
95 |
+
}
|
96 |
+
|
97 |
+
.departure-time {
|
98 |
+
font-size: 1.3rem;
|
99 |
+
font-weight: bold;
|
100 |
+
color: #1a1a1a;
|
101 |
+
grid-row: span 2;
|
102 |
+
display: flex;
|
103 |
+
align-items: center;
|
104 |
+
}
|
105 |
+
|
106 |
+
.departure-info {
|
107 |
+
display: flex;
|
108 |
+
flex-direction: column;
|
109 |
+
}
|
110 |
+
|
111 |
+
.departure-direction {
|
112 |
+
font-size: 1.1rem;
|
113 |
+
color: #333;
|
114 |
+
font-weight: 500;
|
115 |
+
}
|
116 |
+
|
117 |
+
.departure-details {
|
118 |
+
display: flex;
|
119 |
+
gap: 1rem;
|
120 |
+
color: #666;
|
121 |
+
font-size: 0.9rem;
|
122 |
+
}
|
123 |
+
|
124 |
+
/* Map container */
|
125 |
+
.map-container {
|
126 |
+
border-radius: 8px;
|
127 |
+
overflow: hidden;
|
128 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
129 |
+
height: 100%;
|
130 |
+
}
|
131 |
+
|
132 |
+
#leaflet-map {
|
133 |
+
width: 100%;
|
134 |
+
height: 100%;
|
135 |
+
}
|
136 |
+
|
137 |
+
/* Autres sections de trafic */
|
138 |
+
.transport-section {
|
139 |
+
margin-top: 2rem;
|
140 |
+
}
|
141 |
+
|
142 |
+
.transport-section h3 {
|
143 |
+
font-size: 1.1rem;
|
144 |
+
color: #333;
|
145 |
+
margin-bottom: 1rem;
|
146 |
+
}
|
147 |
+
|
148 |
+
@media (max-width: 1024px) {
|
149 |
+
.container {
|
150 |
+
grid-template-columns: 1fr;
|
151 |
+
}
|
152 |
+
|
153 |
+
#departures-container {
|
154 |
+
grid-template-columns: 1fr;
|
155 |
+
}
|
156 |
+
|
157 |
+
.map-container {
|
158 |
+
height: 400px;
|
159 |
+
}
|
160 |
+
}
|
161 |
+
</style>
|
162 |
+
</head>
|
163 |
+
<body>
|
164 |
+
<div class="page-header">
|
165 |
+
<h1>Trafic à Lille</h1>
|
166 |
+
<p class="last-update">Dernière mise à jour : <span id="pageLoadTime"></span></p>
|
167 |
+
</div>
|
168 |
+
|
169 |
+
<div class="container">
|
170 |
+
<div class="traffic-panel">
|
171 |
+
<div class="traffic-info">
|
172 |
+
<h2>Trafic en temps réel</h2>
|
173 |
+
<div class="last-update">
|
174 |
+
Dernière mise à jour : <span id="update-time"></span>
|
175 |
+
</div>
|
176 |
+
|
177 |
+
<!-- Section des départs -->
|
178 |
+
<div class="transport-section">
|
179 |
+
<h3>Prochains départs des trains</h3>
|
180 |
+
<h5>Source : API SNCF</h5>
|
181 |
+
<div id="departures-container">
|
182 |
+
<div class="station-departures" id="lille-europe">
|
183 |
+
<h4>Lille Europe</h4>
|
184 |
+
<div class="departures-list"></div>
|
185 |
+
</div>
|
186 |
+
<div class="station-departures" id="lille-flandres">
|
187 |
+
<h4>Lille Flandres</h4>
|
188 |
+
<div class="departures-list"></div>
|
189 |
+
</div>
|
190 |
+
|
191 |
+
</div>
|
192 |
+
</div>
|
193 |
+
|
194 |
+
<!-- Sections existantes -->
|
195 |
+
<div class="transport-section">
|
196 |
+
|
197 |
+
<div id="metro-lines" class="line-list">
|
198 |
+
</div>
|
199 |
+
</div>
|
200 |
+
|
201 |
+
<h3>Lignes perturbées :</h3>
|
202 |
+
<h5>Source : MobiLille</h5>
|
203 |
+
{% for type_key, transport in transport_types.items() %}
|
204 |
+
<div class="transport-section">
|
205 |
+
<h3>{{ transport.name }}</h3>
|
206 |
+
{% if transport.lines %}
|
207 |
+
<div class="line-list">
|
208 |
+
{% for line in transport.lines %}
|
209 |
+
<button class="line-button {{ type_key }}"
|
210 |
+
data-line="{{ line.name }}"
|
211 |
+
title="{{ line.trace }}">
|
212 |
+
{{ line.name }}
|
213 |
+
</button>
|
214 |
+
{% endfor %}
|
215 |
+
</div>
|
216 |
+
{% else %}
|
217 |
+
<p class="no-disruption">Aucune perturbation</p>
|
218 |
+
{% endif %}
|
219 |
+
</div>
|
220 |
+
{% endfor %}
|
221 |
+
|
222 |
+
</div>
|
223 |
+
|
224 |
+
<div id="perturbation-details" class="perturbation-details">
|
225 |
+
<!-- Les détails des perturbations seront injectés ici -->
|
226 |
+
</div>
|
227 |
+
</div>
|
228 |
+
|
229 |
+
<div class="map-container">
|
230 |
+
<div id="leaflet-map"></div>
|
231 |
+
</div>
|
232 |
+
</div>
|
233 |
+
|
234 |
+
<script>
|
235 |
+
let map;
|
236 |
+
let trafficLayer;
|
237 |
+
|
238 |
+
function updateLastUpdateTime() {
|
239 |
+
const now = new Date();
|
240 |
+
document.getElementById('pageLoadTime').textContent = now.toLocaleString('fr-FR');
|
241 |
+
}
|
242 |
+
|
243 |
+
function initMap() {
|
244 |
+
if (map) {
|
245 |
+
map.remove();
|
246 |
+
}
|
247 |
+
|
248 |
+
map = L.map('leaflet-map').setView([{{ center.lat }}, {{ center.lon }}], 12);
|
249 |
+
|
250 |
+
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
|
251 |
+
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors © <a href="https://carto.com/attributions">CARTO</a>',
|
252 |
+
subdomains: 'abcd',
|
253 |
+
maxZoom: 19
|
254 |
+
}).addTo(map);
|
255 |
+
|
256 |
+
trafficLayer = L.tileLayer('https://api.tomtom.com/traffic/map/4/tile/flow/relative0/{z}/{x}/{y}.png?tileSize=256&key={{ api_key }}', {
|
257 |
+
minZoom: 10,
|
258 |
+
maxZoom: 18,
|
259 |
+
opacity: 0.75
|
260 |
+
}).addTo(map);
|
261 |
+
|
262 |
+
return map;
|
263 |
+
}
|
264 |
+
|
265 |
+
function showPerturbations(line) {
|
266 |
+
const detailsContainer = document.getElementById('perturbation-details');
|
267 |
+
|
268 |
+
fetch(`/api/perturbations/${line}`)
|
269 |
+
.then(response => response.json())
|
270 |
+
.then(data => {
|
271 |
+
if (data.error) {
|
272 |
+
console.error(data.error);
|
273 |
+
return;
|
274 |
+
}
|
275 |
+
|
276 |
+
let html = `
|
277 |
+
<div class="details-header">
|
278 |
+
<h3>Ligne ${data.line}</h3>
|
279 |
+
<p class="line-trace">${data.trace}</p>
|
280 |
+
</div>
|
281 |
+
`;
|
282 |
+
|
283 |
+
if (data.alerts && data.alerts.length > 0) {
|
284 |
+
data.alerts.forEach(alert => {
|
285 |
+
html += `
|
286 |
+
<div class="alert">
|
287 |
+
<div class="alert-title">${alert.title}</div>
|
288 |
+
<div class="alert-description">${alert.description}</div>
|
289 |
+
<div class="alert-time">Mise à jour : ${alert.update_time}</div>
|
290 |
+
</div>
|
291 |
+
`;
|
292 |
+
});
|
293 |
+
} else {
|
294 |
+
html += '<p class="no-disruption">Aucune perturbation signalée pour cette ligne.</p>';
|
295 |
+
}
|
296 |
+
|
297 |
+
detailsContainer.innerHTML = html;
|
298 |
+
detailsContainer.style.display = 'block';
|
299 |
+
})
|
300 |
+
.catch(error => {
|
301 |
+
console.error('Erreur lors de la récupération des perturbations:', error);
|
302 |
+
});
|
303 |
+
}
|
304 |
+
|
305 |
+
// Fonction pour mettre à jour les départs
|
306 |
+
function updateDepartures() {
|
307 |
+
fetch('/api/departures')
|
308 |
+
.then(response => response.json())
|
309 |
+
.then(data => {
|
310 |
+
// Mise à jour pour chaque gare
|
311 |
+
for (const [station, departures] of Object.entries(data)) {
|
312 |
+
const stationId = station.toLowerCase().replace(' ', '-');
|
313 |
+
const container = document.querySelector(`#${stationId} .departures-list`);
|
314 |
+
container.innerHTML = '';
|
315 |
+
|
316 |
+
departures.slice(0, 5).forEach(departure => {
|
317 |
+
const departureElement = document.createElement('div');
|
318 |
+
departureElement.className = 'departure-item';
|
319 |
+
departureElement.innerHTML = `
|
320 |
+
<span class="departure-time">${departure.time}</span>
|
321 |
+
<div class="departure-info">
|
322 |
+
<span class="departure-direction">${departure.direction}</span>
|
323 |
+
<div class="departure-details">
|
324 |
+
<span class="departure-type">${departure.type}</span>
|
325 |
+
<span class="departure-number">${departure.train_number}</span>
|
326 |
+
</div>
|
327 |
+
</div>
|
328 |
+
`;
|
329 |
+
container.appendChild(departureElement);
|
330 |
+
});
|
331 |
+
}
|
332 |
+
})
|
333 |
+
.catch(error => console.error('Erreur:', error));
|
334 |
+
}
|
335 |
+
|
336 |
+
document.addEventListener('DOMContentLoaded', function() {
|
337 |
+
initMap();
|
338 |
+
updateLastUpdateTime();
|
339 |
+
setInterval(updateLastUpdateTime, 300000); // Mise à jour toutes les 5 minutes
|
340 |
+
|
341 |
+
// Mettre à jour les départs toutes les 2 minutes
|
342 |
+
updateDepartures();
|
343 |
+
setInterval(updateDepartures, 120000);
|
344 |
+
|
345 |
+
// Ajouter les écouteurs d'événements pour les boutons
|
346 |
+
document.querySelectorAll('.line-button').forEach(button => {
|
347 |
+
button.addEventListener('click', (e) => {
|
348 |
+
// Retirer la classe active de tous les boutons
|
349 |
+
document.querySelectorAll('.line-button').forEach(btn => {
|
350 |
+
btn.classList.remove('active');
|
351 |
+
});
|
352 |
+
// Ajouter la classe active au bouton cliqué
|
353 |
+
e.target.classList.add('active');
|
354 |
+
showPerturbations(button.dataset.line);
|
355 |
+
});
|
356 |
+
});
|
357 |
+
});
|
358 |
+
</script>
|
359 |
+
</body>
|
360 |
+
</html>
|