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(); } };