[*] Added code
This commit is contained in:
commit
8a05d72528
5 changed files with 327 additions and 0 deletions
1
.env.example
Normal file
1
.env.example
Normal file
|
@ -0,0 +1 @@
|
||||||
|
CTS_TOKEN=
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
.env
|
||||||
|
data/
|
46
download_data.py
Executable file
46
download_data.py
Executable file
|
@ -0,0 +1,46 @@
|
||||||
|
#!/bin/env python3
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import requests
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
import requests.auth
|
||||||
|
|
||||||
|
TO_DOWNLOAD = [
|
||||||
|
('https://data.strasbourg.eu/api/explore/v2.1/catalog/datasets/lignes_tram/records', 'lines_tram.json', 'LINES_TRAM'),
|
||||||
|
('https://data.strasbourg.eu/api/explore/v2.1/catalog/datasets/lignes_de_bus/records?refine=type_ligne%3A%22BHNS%22 ', 'lines_bhns.json', 'LINES_BHNS'),
|
||||||
|
]
|
||||||
|
|
||||||
|
TO_DOWNLOAD_CTS = [
|
||||||
|
('https://api.cts-strasbourg.eu/v1/siri/2.0/stoppoints-discovery?includeLinesDestinations=true', 'stoppoints.json', 'STOPPOINTS'),
|
||||||
|
('https://api.cts-strasbourg.eu/v1/siri/2.0/lines-discovery', 'lines.json', 'LINES'),
|
||||||
|
('https://api.cts-strasbourg.eu/v1/siri/2.0/estimated-timetable?LineRef=A&LineRef=B&LineRef=C&LineRef=D&LineRef=E&LineRef=F&GetStopIdInsteadOfStopCode=true', 'timetable.json', 'TIMETABLE')
|
||||||
|
]
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
CTS_TOKEN = os.getenv('CTS_TOKEN')
|
||||||
|
|
||||||
|
OUT = "data"
|
||||||
|
js_data = ""
|
||||||
|
|
||||||
|
os.makedirs(pathlib.Path(OUT), exist_ok=True)
|
||||||
|
|
||||||
|
for url, file, var in TO_DOWNLOAD:
|
||||||
|
rep = requests.get(url)
|
||||||
|
d = rep.json()
|
||||||
|
with open(pathlib.Path(OUT) / file, 'w') as f:
|
||||||
|
json.dump(d, f, indent=2)
|
||||||
|
js_data += 'const ' + var + ' = ' + json.dumps(d) + '\n'
|
||||||
|
|
||||||
|
for url, file, var in TO_DOWNLOAD_CTS:
|
||||||
|
rep = requests.get(url, auth=requests.auth.HTTPBasicAuth(CTS_TOKEN, ''))
|
||||||
|
d = rep.json()
|
||||||
|
with open(pathlib.Path(OUT) / file, 'w') as f:
|
||||||
|
json.dump(d, f, indent=2)
|
||||||
|
js_data += 'const ' + var + ' = ' + json.dumps(d) + '\n'
|
||||||
|
|
||||||
|
with open(pathlib.Path(OUT) / "data.js", 'w') as f:
|
||||||
|
f.write(js_data)
|
39
index.html
Normal file
39
index.html
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Test</title>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||||
|
crossorigin=""
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #090909;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<script
|
||||||
|
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""
|
||||||
|
></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@7/turf.min.js"></script>
|
||||||
|
<script src="data/data.js"></script>
|
||||||
|
<script src="main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
239
main.js
Normal file
239
main.js
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
// LINES_TRAM
|
||||||
|
// LINES_BHNS
|
||||||
|
// LINES
|
||||||
|
// STOPPOINTS
|
||||||
|
|
||||||
|
const LINES_OVERRIDE = {
|
||||||
|
'Place d\'Islande': ['F'],
|
||||||
|
'Gare Centrale': ['C', 'A'],
|
||||||
|
'Homme de Fer': ['C', 'A']
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = L.map('map');
|
||||||
|
// https://cartodb-basemaps-a.global.ssl.fastly.net/dark_nolabels/{z}/{x}/{y}.png
|
||||||
|
// https://tile.openstreetmap.org/{z}/{x}/{y}.png
|
||||||
|
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
maxZoom: 19,
|
||||||
|
attribution: '© carto.com'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
const lignes = {};
|
||||||
|
|
||||||
|
// Dessigner les lignes de tram
|
||||||
|
LINES_TRAM.results.forEach(line => {
|
||||||
|
// Récupération des points de la ligne
|
||||||
|
const points = line.geo_shape.geometry.coordinates;
|
||||||
|
// Récupération de la couleur
|
||||||
|
const color = LINES.LinesDelivery.AnnotatedLineRef.filter(d => d.LineRef === line.ligne)[0].Extension.RouteColor;
|
||||||
|
|
||||||
|
// Ligne extérieur
|
||||||
|
const polyline = L.polyline(L.GeoJSON.coordsToLatLngs(points), {
|
||||||
|
color: '#' + color,
|
||||||
|
weight: 6,
|
||||||
|
}).addTo(map);
|
||||||
|
// Ligne blanche centrale
|
||||||
|
const polyline_white = L.polyline(L.GeoJSON.coordsToLatLngs(points), {
|
||||||
|
color: '#ffffff',
|
||||||
|
weight: 2,
|
||||||
|
opacity: 0.5
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Stockage des points pour traitement ultérieur
|
||||||
|
lignes[line.ligne] = points;
|
||||||
|
});
|
||||||
|
|
||||||
|
const stops = {}
|
||||||
|
const stations = {}
|
||||||
|
|
||||||
|
// Pre-parsing des arrêts pour dessin
|
||||||
|
// (en gros on AVG les coordéonnes)
|
||||||
|
STOPPOINTS.StopPointsDelivery.AnnotatedStopPointRef.filter(stop => stop.Lines.filter(line => line.Extension.RouteType === "tram").length > 0).forEach(stop => {
|
||||||
|
stations[stop.StopPointRef] = [
|
||||||
|
stop.Location.Longitude,
|
||||||
|
stop.Location.Latitude
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!(stop.StopName in stops)) {
|
||||||
|
stops[stop.StopName] = {
|
||||||
|
name: stop.StopName,
|
||||||
|
lines: new Set(stop.Lines.map(l => l.LineRef)),
|
||||||
|
lat: stop.Location.Latitude,
|
||||||
|
lng: stop.Location.Longitude,
|
||||||
|
cnt: 1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
stops[stop.StopName].lat += stop.Location.Latitude;
|
||||||
|
stops[stop.StopName].lng += stop.Location.Longitude;
|
||||||
|
stops[stop.StopName].cnt += 1;
|
||||||
|
stop.Lines.map(l => l.LineRef).forEach(stops[stop.StopName].lines.add, stops[stop.StopName].lines);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Dessin des coordonnées
|
||||||
|
for (const name in stops) {
|
||||||
|
const stop = stops[name];
|
||||||
|
let line_name;
|
||||||
|
|
||||||
|
// On trouve sur quel ligne l'arrêt se trouve, pour snap sur la ligne
|
||||||
|
if (name in LINES_OVERRIDE) {
|
||||||
|
line_name = LINES_OVERRIDE[name]
|
||||||
|
} else {
|
||||||
|
for (const line of stop.lines) {
|
||||||
|
if (line in lignes) {
|
||||||
|
line_name = [line];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stop.lat /= stop.cnt;
|
||||||
|
stop.lng /= stop.cnt;
|
||||||
|
|
||||||
|
// On place l'arrêt
|
||||||
|
if (line_name !== undefined) {
|
||||||
|
for(the_name of line_name) {
|
||||||
|
const tramline = turf.lineString(lignes[the_name]);
|
||||||
|
const pt = turf.point([stop.lng, stop.lat]);
|
||||||
|
// Snap l'arrêt sur la ligne
|
||||||
|
const snapped = turf.nearestPointOnLine(tramline, pt);
|
||||||
|
|
||||||
|
const marker = L.circleMarker(L.latLng(snapped.geometry.coordinates[1], snapped.geometry.coordinates[0]), {
|
||||||
|
radius: 6,
|
||||||
|
color: "#1d1d1d",
|
||||||
|
fillColor: "#ffffff",
|
||||||
|
weight: 3,
|
||||||
|
fillOpacity: 1
|
||||||
|
}).addTo(map);
|
||||||
|
marker.bindPopup(stop.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trams en cours de circulation
|
||||||
|
let trams = {};
|
||||||
|
// Cache des segments de lignes
|
||||||
|
// (parce que c'est lour à calculer donc autant le faire qu'une fois pôour chaque segment)
|
||||||
|
const segments_cache = {}
|
||||||
|
|
||||||
|
// Mise à jour périodique
|
||||||
|
setInterval(() => {
|
||||||
|
const current_date = new Date();
|
||||||
|
const current_trams = new Set();
|
||||||
|
// On parcours la delivery
|
||||||
|
for (const delivery of TIMETABLE.ServiceDelivery.EstimatedTimetableDelivery) {
|
||||||
|
const cycle_name = delivery.ShortestPossibleCycle;
|
||||||
|
// Les timeframes
|
||||||
|
for (const frame of delivery.EstimatedJourneyVersionFrame) {
|
||||||
|
// Et les journeys
|
||||||
|
for (const journey of frame.EstimatedVehicleJourney) {
|
||||||
|
let previous = undefined;
|
||||||
|
const tram_unique_id = cycle_name + ";;" + journey.LineRef + ";;" + journey.FramedVehicleJourneyRef.DatedVehicleJourneySAERef
|
||||||
|
// console.log(tram_unique_id)
|
||||||
|
// On parcours les différentes stations sur le chemin
|
||||||
|
for (const call of journey.EstimatedCalls) {
|
||||||
|
if (previous === undefined) {
|
||||||
|
previous = call;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// On récupère l'heure de départ de la stations précédente, l'heure d'arrivée à la stations actuelle
|
||||||
|
const previous_dep = new Date(previous.ExpectedDepartureTime);
|
||||||
|
const call_arrival = new Date(call.ExpectedArrivalTime);
|
||||||
|
const call_departure = new Date(call.ExpectedDepartureTime);
|
||||||
|
|
||||||
|
// Si l'heure actuelle est entre le départ de la station précédente et l'arrivée
|
||||||
|
// de la station actuelle on sait que le tram est sur ce tronçon
|
||||||
|
if (previous_dep.getTime() <= current_date.getTime() && current_date.getTime() <= call_arrival.getTime()) {
|
||||||
|
// On calcul le pourcentage d'avancement sur le tronçon
|
||||||
|
// (interpolation linéaire à la con)
|
||||||
|
const percent = (current_date.getTime() - previous_dep.getTime()) / (call_arrival.getTime() - previous_dep.getTime());
|
||||||
|
|
||||||
|
// On place le tram
|
||||||
|
if (previous.StopPointRef in stations && call.StopPointRef in stations && journey.LineRef in lignes) {
|
||||||
|
let seg;
|
||||||
|
const segments_cache_id = journey.LineRef + ';;' + previous.StopPointRef + ';;' + call.StopPointRef;
|
||||||
|
if (segments_cache_id in segments_cache) {
|
||||||
|
seg = segments_cache[segments_cache_id];
|
||||||
|
} else {
|
||||||
|
seg = turf.lineSlice(stations[previous.StopPointRef], stations[call.StopPointRef], turf.lineString(lignes[journey.LineRef]));
|
||||||
|
segments_cache[segments_cache_id] = seg;
|
||||||
|
}
|
||||||
|
const len = turf.length(seg);
|
||||||
|
const position = turf.along(seg, len * percent);
|
||||||
|
|
||||||
|
if (tram_unique_id in trams) {
|
||||||
|
const last_position = map.project(trams[tram_unique_id].getLatLng(), 5)
|
||||||
|
const current_position = map.project(L.latLng(position.geometry.coordinates[1], position.geometry.coordinates[0]), 5)
|
||||||
|
const x = current_position.x - last_position.x;
|
||||||
|
const y = current_position.y - last_position.y;
|
||||||
|
const angle = Math.atan2(y, x);
|
||||||
|
|
||||||
|
const elem = document.getElementById(`tram;;${tram_unique_id}`)
|
||||||
|
if (elem !== null) {
|
||||||
|
elem.style.transform = `rotate(${angle}rad)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
trams[tram_unique_id].setLatLng(L.latLng(position.geometry.coordinates[1], position.geometry.coordinates[0]));
|
||||||
|
} else {
|
||||||
|
const color = LINES.LinesDelivery.AnnotatedLineRef.filter(d => d.LineRef === journey.LineRef)[0].Extension.RouteColor;
|
||||||
|
/*const marker = L.circleMarker(L.latLng(position.geometry.coordinates[1], position.geometry.coordinates[0]), {
|
||||||
|
radius: 4,
|
||||||
|
color: "#1d1d1d",
|
||||||
|
fillColor: "#" + color,
|
||||||
|
weight: 2,
|
||||||
|
fillOpacity: 1
|
||||||
|
}).addTo(map);*/
|
||||||
|
const content = `
|
||||||
|
<svg viewBox="-10 -10 20 20" width="15" height="15" xmlns="http://www.w3.org/2000/svg" id="tram;;${tram_unique_id}">
|
||||||
|
<polygon points="-7.5, 5 2.5, 5 7.5, 0 2.5, -5 -7.5, -5" fill="#${color}" stroke="#1d1d1b" stroke-width="2" class="tram"></polygon>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
const myIcon = L.divIcon({className: 'tram-icon', html: content});
|
||||||
|
const marker = L.marker(L.latLng(position.geometry.coordinates[1], position.geometry.coordinates[0]), {icon: myIcon}).addTo(map);
|
||||||
|
marker.bindPopup(tram_unique_id);
|
||||||
|
trams[tram_unique_id] = marker;
|
||||||
|
console.log("Added " + tram_unique_id);
|
||||||
|
}
|
||||||
|
current_trams.add(tram_unique_id);
|
||||||
|
}
|
||||||
|
// Si l'heure actuelle et entre l'heure d'arrivée et de départ à une station on place le tram sur cette station.
|
||||||
|
} else if (call_arrival.getTime() <= current_date.getTime() && current_date.getTime() <= call_departure.getTime()) {
|
||||||
|
if (tram_unique_id in trams) {
|
||||||
|
trams[tram_unique_id].setLatLng(L.latLng([stations[call.StopPointRef][1], stations[call.StopPointRef][0]]));
|
||||||
|
} else {
|
||||||
|
const color = LINES.LinesDelivery.AnnotatedLineRef.filter(d => d.LineRef === journey.LineRef)[0].Extension.RouteColor;
|
||||||
|
const content = `
|
||||||
|
<svg viewBox="-10 -10 20 20" width="15" height="15" xmlns="http://www.w3.org/2000/svg" id="tram;;${tram_unique_id}">
|
||||||
|
<polygon points="-7.5, 5 2.5, 5 7.5, 0 2.5, -5 -7.5, -5" fill="#${color}" stroke="#1d1d1b" stroke-width="2" class="tram"></polygon>
|
||||||
|
</svg>
|
||||||
|
`
|
||||||
|
const myIcon = L.divIcon({className: 'tram-icon', html: content});
|
||||||
|
const marker = L.marker(L.latLng([stations[call.StopPointRef][1], stations[call.StopPointRef][0]]), {icon: myIcon}).addTo(map);
|
||||||
|
marker.bindPopup(tram_unique_id);
|
||||||
|
trams[tram_unique_id] = marker;
|
||||||
|
console.log("Added " + tram_unique_id);
|
||||||
|
}
|
||||||
|
current_trams.add(tram_unique_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
previous = call;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On vire les anciens trams
|
||||||
|
for(const key of Object.keys(trams)) {
|
||||||
|
if (!current_trams.has(key)) {
|
||||||
|
console.log("Removed " + key);
|
||||||
|
trams[key].remove();
|
||||||
|
delete trams[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, 200);
|
||||||
|
|
||||||
|
var latLngBounds = L.latLngBounds([[48.52343058579338, 7.681799380771575], [48.62985324979267, 7.820267901766859]]);
|
||||||
|
map.fitBounds(latLngBounds);
|
||||||
|
|
||||||
|
//
|
Loading…
Add table
Reference in a new issue