From a23e7994594b6f85a9c2897d1847d94448cdf478 Mon Sep 17 00:00:00 2001
From: Ronto4 <34981840+Ronto4@users.noreply.github.com>
Date: Fri, 10 Apr 2026 23:58:51 +0200
Subject: [PATCH] feat: improve transit map
* Use line colours for markers.
* Auto-refresh every ten seconds.
* Show trip paths when clicking a vehicle.
* Improve display of via and continuing notes in trip headsign.
Known issues:
* When a trip or popup text (= headsign) changes, the update is not properly propagated to the map.
* The heading of a vehicle is not included.
---
.../Components/Maps/LeafletMap.razor | 88 ++++++++++++++----
TransitWebViewer/TransitWebViewer.csproj | 2 +-
TransitWebViewer/wwwroot/leafletInterop.js | 90 ++++++++++++++++++-
3 files changed, 160 insertions(+), 20 deletions(-)
diff --git a/TransitWebViewer/Components/Maps/LeafletMap.razor b/TransitWebViewer/Components/Maps/LeafletMap.razor
index 975b814..9a5d3f8 100644
--- a/TransitWebViewer/Components/Maps/LeafletMap.razor
+++ b/TransitWebViewer/Components/Maps/LeafletMap.razor
@@ -3,32 +3,88 @@
@inject IJSRuntime JsRuntime
@inject NavigationManager NavigationManager
-
+
@code {
private IJSObjectReference? _leafletMap;
string TtssProxyUrl => $"{NavigationManager.BaseUri}api/vip";
-
+ private TtssApi? _ttssApi = null;
+
+ TtssApi TtssApi
+ {
+ get
+ {
+ if (_ttssApi is null)
+ {
+ _ttssApi =
+ new TtssApi
+ {
+ Language = "de",
+ BaseUri = new Uri("https://www.swp-potsdam.de/"),
+ ProxyUri = new Uri(TtssProxyUrl),
+ // ProxyUri = new Uri("http://localhost:5025/api/vip"),
+ };
+ }
+
+ return _ttssApi;
+ }
+ }
+
private Timer? Timer { get; set; } = null;
private async Task VehicleUpdate()
{
- var ttssApi = new TtssApi
+ var dotNetReference = DotNetObjectReference.Create(this);
+ var vehiclesResponse = await TtssApi.GetVehiclesAsync(new VehicleRequest());
+ var vehicles = vehiclesResponse.Vehicles.Where(vehicle => vehicle is { IsDeleted: false, Position: not null }).ToArray();
+ foreach (var vehicle in vehicles)
+ {
+ const string lineBreak = "
";
+ var direction = new Func(() =>
+ {
+ var direction = vehicle.Direction ?? "unbekannt";
+ var firstSpace = direction.IndexOf(" ", StringComparison.Ordinal);
+ if (firstSpace > -1 && direction[..firstSpace] == direction[(firstSpace + 1)..(firstSpace * 2 + 1)])
+ {
+ direction = direction[firstSpace..];
+ }
+
+ direction = direction.Replace("->", $"{lineBreak}-> ");
+ return string.Join(lineBreak, direction.Split("//").Select(part => part.Trim()).ToArray());
+ })();
+ await JsRuntime.InvokeVoidAsync(
+ "leafletMap.vehicleAt",
+ dotNetReference,
+ _leafletMap,
+ vehicle.VehicleId,
+ vehicle.Position!.Latitude,
+ vehicle.Position.Longitude,
+ direction,
+ vehicle.Color?.Replace("0x", "#") ?? "#000000",
+ vehicle.TripId
+ );
+ }
+
+ // See https://github.com/dotnet/aspnetcore/issues/8743#issuecomment-476261870 for the cast to `object`.
+ await JsRuntime.InvokeVoidAsync("leafletMap.keepOnlyVehicles", (object)vehicles.Select(vehicle => vehicle.VehicleId).ToArray());
+ }
+
+ [JSInvokable(nameof(OnOpenTripMarker))]
+ public async Task OnOpenTripMarker(string? tripId)
+ {
+ if (tripId is null)
{
- Language = "de",
- BaseUri = new Uri("https://www.swp-potsdam.de/"),
- ProxyUri = new Uri(TtssProxyUrl),
- // ProxyUri = new Uri("http://localhost:5177"),
- };
- var vehicles = await ttssApi.GetVehiclesAsync(new VehicleRequest());
- foreach (var vehicle in vehicles.Vehicles)
+ await Console.Error.WriteLineAsync("Cannot render trip path as no trip is was set.");
+ return;
+ }
+
+ var paths = await TtssApi.GetTripPathsAsync(new TripPathsRequest { TripId = tripId });
+ foreach (var path in paths.Paths)
{
- if (vehicle.IsDeleted) continue;
- if (vehicle.Position is null) continue;
- await JsRuntime.InvokeAsync(
- "leafletMap.vehicleAt", _leafletMap, vehicle.Position.Latitude, vehicle.Position.Longitude, vehicle.Direction ?? "unbekannt"
+ await JsRuntime.InvokeVoidAsync(
+ "leafletMap.drawRoute", _leafletMap, path.Waypoints, path.Color, true, tripId
);
}
}
@@ -42,9 +98,7 @@
"leafletMap.initialize", "map", 52.395833, 13.061389, 13
);
- await VehicleUpdate();
-
- // Timer = new Timer(_ => Task.Run(VehicleUpdate), null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
+ Timer = new Timer(_ => Task.Run(VehicleUpdate), null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
}
}
diff --git a/TransitWebViewer/TransitWebViewer.csproj b/TransitWebViewer/TransitWebViewer.csproj
index 38e21f6..4a5c353 100644
--- a/TransitWebViewer/TransitWebViewer.csproj
+++ b/TransitWebViewer/TransitWebViewer.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/TransitWebViewer/wwwroot/leafletInterop.js b/TransitWebViewer/wwwroot/leafletInterop.js
index c5dd802..f1083c2 100644
--- a/TransitWebViewer/wwwroot/leafletInterop.js
+++ b/TransitWebViewer/wwwroot/leafletInterop.js
@@ -1,3 +1,11 @@
+const vehicles = {};
+const routes = {};
+const removeAllRoutes = () => {
+ for (const [tripId, polyline] of Object.entries(routes)) {
+ polyline.remove();
+ delete routes[tripId];
+ }
+};
window.leafletMap = {
initialize: function (mapId, lat, lng, zoom) {
// Initialize the Leaflet map
@@ -12,8 +20,86 @@ window.leafletMap = {
return map;
},
- vehicleAt: function (map, lat, lng, popupText) {
- return L.marker([lat, lng]).addTo(map)
+ vehicleAt: function (dotNetObject, map, vehicleId, lat, lng, popupText, colour, tripId) {
+ if (vehicles[vehicleId]) {
+ const [marker, oldTripId] = vehicles[vehicleId];
+ marker.setLatLng([lat, lng]);
+ marker.color = colour;
+ marker.setTooltipContent(popupText);
+ if (marker.isPopupOpen() && marker.getPopup().content !== popupText) {
+ console.log(`Resetting popup old '${marker.getPopup().content}' new '${popupText}'.`);
+ marker.closePopup();
+ marker.openPopup();
+ }
+ if (oldTripId !== tripId) {
+ console.log(`Resetting trip ${tripId} (was ${oldTripId}) (${popupText}).`);
+ if (routes[oldTripId]) {
+ console.log("Redrawing!");
+ removeAllRoutes();
+ dotNetObject.invokeMethodAsync('OnOpenTripMarker', tripId);
+ }
+ vehicles[vehicleId] = [marker, tripId];
+ }
+ return;
+ }
+ const marker = L.circleMarker([lat, lng], { color: colour });
+ vehicles[vehicleId] = [marker, tripId];
+ return marker
+ .addTo(map)
+ .on('popupopen', _event => {
+ if (routes[tripId]) {
+ return;
+ }
+ removeAllRoutes();
+ dotNetObject.invokeMethodAsync('OnOpenTripMarker', tripId);
+ })
+ // .on('popupclose', _event => {
+ // if (routes[tripId]) {
+ // routes[tripId].remove();
+ // delete routes[tripId];
+ // } else {
+ // console.warn(`Could not remove path of trip ${tripId}.`);
+ // }
+ // })
.bindPopup(popupText);
+ // return L.marker([lat, lng]).addTo(map)
+ // .bindPopup(popupText);
+ },
+
+ keepOnlyVehicles: function (vehicleIds, a) {
+ for (const [vehicleId, [marker, tripId]] of Object.entries(vehicles)) {
+ if (vehicleIds.includes(vehicleId)) {
+ continue;
+ }
+ marker.remove();
+ if (routes[tripId]) {
+ routes[tripId].remove();
+ delete routes[tripId];
+ }
+ delete vehicles[vehicleId];
+ }
+ },
+
+ drawRoute: function (map, points, colour, focus, tripId) {
+ const leafletPoints = points.map(point => new L.LatLng(point.latitude, point.longitude));
+ const polyline = new L.polyline(leafletPoints, {
+ color: colour,
+ weight: 3,
+ opacity: 1,
+ smoothFactor: 1,
+ });
+ if (tripId) {
+ if (routes[tripId]) {
+ routes[tripId].remove();
+ }
+ routes[tripId] = polyline;
+ } else {
+ console.warn(`Could not store polyline for colour ${colour}.`);
+ }
+ if (focus) {
+ map.fitBounds(polyline.getBounds());
+ }
+ polyline.addTo(map);
+ return polyline.bringToBack();
}
};