tyriaa commited on
Commit
8b1e2ee
·
1 Parent(s): 83e56c3

4rd commitekk

Browse files
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: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors &copy; <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>