diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-23 21:33:17 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-23 21:33:17 +0100 |
| commit | 4a866f5352a51916ddb9849b2d68213856196c9c (patch) | |
| tree | 3ba01ba01d5f6931adaf708b76ffccdd798fc78b /src/Costasdev.Busurbano.Backend | |
| parent | 87417c313b455ba0dee19708528cc8d0b830a276 (diff) | |
Full real-time page, coruña real time
Diffstat (limited to 'src/Costasdev.Busurbano.Backend')
14 files changed, 494 insertions, 12 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs index 934935e..61a003e 100644 --- a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs @@ -2,6 +2,7 @@ using Costasdev.Busurbano.Backend.GraphClient; using Costasdev.Busurbano.Backend.GraphClient.App; using Costasdev.Busurbano.Backend.Services; +using Costasdev.Busurbano.Backend.Types; using Costasdev.Busurbano.Backend.Types.Arrivals; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; @@ -87,7 +88,8 @@ public partial class ArrivalsController : ControllerBase //var isRunning = departureTime < nowLocal; // TODO: Handle this properly, since many times it's "tomorrow" but not handled properly - if (minutesToArrive < ArrivalsAtStopContent.PastArrivalMinutesIncluded) + var threshold = ShouldFetchPastArrivals(id) ? ArrivalsAtStopContent.PastArrivalMinutesIncluded : 0; + if (minutesToArrive < threshold) { continue; } @@ -97,6 +99,7 @@ public partial class ArrivalsController : ControllerBase TripId = item.Trip.GtfsId, Route = new RouteInfo { + GtfsId = item.Trip.Route.GtfsId, ShortName = item.Trip.RouteShortName, Colour = item.Trip.Route.Color ?? "FFFFFF", TextColour = item.Trip.Route.TextColor ?? "000000" @@ -122,7 +125,8 @@ public partial class ArrivalsController : ControllerBase StopCode = stop.Code, IsReduced = reduced, Arrivals = arrivals, - NowLocal = nowLocal + NowLocal = nowLocal, + StopLocation = new Position { Latitude = stop.Lat, Longitude = stop.Lon } }); var feedId = id.Split(':')[0]; @@ -131,6 +135,18 @@ public partial class ArrivalsController : ControllerBase { StopCode = _feedService.NormalizeStopCode(feedId, stop.Code), StopName = _feedService.NormalizeStopName(feedId, stop.Name), + StopLocation = new Position + { + Latitude = stop.Lat, + Longitude = stop.Lon + }, + Routes = stop.Routes.Select(r => new RouteInfo + { + GtfsId = r.GtfsId, + ShortName = _feedService.NormalizeRouteShortName(feedId, r.ShortName ?? ""), + Colour = r.Color ?? "FFFFFF", + TextColour = r.TextColor ?? "000000" + }).ToList(), Arrivals = arrivals }); } diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs index 0e9d21b..f3fe51c 100644 --- a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs @@ -97,6 +97,7 @@ public class TileController : ControllerBase return; } + // TODO: Duplicate from ArrivalsController var (Color, TextColor) = _feedService.GetFallbackColourForFeed(idParts[0]); var distinctRoutes = GetDistinctRoutes(feedId, stop.Routes ?? []); @@ -166,7 +167,7 @@ public class TileController : ControllerBase foreach (var route in routes) { - var seenId = _feedService.NormalizeRouteShortName(feedId, route.ShortName ?? string.Empty); + var seenId = _feedService.GetUniqueRouteShortName(feedId, route.ShortName ?? string.Empty); route.ShortName = seenId; if (seen.Contains(seenId)) diff --git a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj index 3bff631..5e2283c 100644 --- a/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj +++ b/src/Costasdev.Busurbano.Backend/Costasdev.Busurbano.Backend.csproj @@ -22,6 +22,6 @@ </ItemGroup> <ItemGroup> - <Folder Include="wwwroot\" /> + <ProjectReference Include="..\Costasdev.Busurbano.Sources.TranviasCoruna\Costasdev.Busurbano.Sources.TranviasCoruna.csproj" /> </ItemGroup> </Project> diff --git a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs index cf2907c..a349f9a 100644 --- a/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs +++ b/src/Costasdev.Busurbano.Backend/GraphClient/App/ArrivalsAtStop.cs @@ -20,6 +20,14 @@ public class ArrivalsAtStopContent : IGraphRequest<ArrivalsAtStopContent.Args> stop(id:""{args.Id}"") {{ code name + lat + lon + routes {{ + gtfsId + shortName + color + textColor + }} arrivals: stoptimesWithoutPatterns(numberOfDepartures: 100, startTime: {startTimeUnix}, timeRange: 14400) {{ headsign scheduledDeparture @@ -31,6 +39,7 @@ public class ArrivalsAtStopContent : IGraphRequest<ArrivalsAtStopContent.Args> serviceId routeShortName route {{ + gtfsId color textColor longName @@ -42,6 +51,8 @@ public class ArrivalsAtStopContent : IGraphRequest<ArrivalsAtStopContent.Args> stoptimes {{ stop {{ name + lat + lon }} scheduledDeparture }} @@ -63,6 +74,12 @@ public class ArrivalsAtStopResponse : AbstractGraphResponse [JsonPropertyName("name")] public required string Name { get; set; } + [JsonPropertyName("lat")] public double Lat { get; set; } + + [JsonPropertyName("lon")] public double Lon { get; set; } + + [JsonPropertyName("routes")] public List<RouteDetails> Routes { get; set; } = []; + [JsonPropertyName("arrivals")] public List<Arrival> Arrivals { get; set; } = []; } @@ -115,6 +132,8 @@ public class ArrivalsAtStopResponse : AbstractGraphResponse public class StopDetails { [JsonPropertyName("name")] public required string Name { get; set; } + [JsonPropertyName("lat")] public double Lat { get; set; } + [JsonPropertyName("lon")] public double Lon { get; set; } } public class DepartureStoptime @@ -125,6 +144,11 @@ public class ArrivalsAtStopResponse : AbstractGraphResponse public class RouteDetails { + [JsonPropertyName("gtfsId")] public required string GtfsId { get; set; } + public string GtfsIdValue => GtfsId.Split(':', 2)[1]; + + [JsonPropertyName("shortName")] public string? ShortName { get; set; } + [JsonPropertyName("color")] public string? Color { get; set; } [JsonPropertyName("textColor")] public string? TextColor { get; set; } diff --git a/src/Costasdev.Busurbano.Backend/Program.cs b/src/Costasdev.Busurbano.Backend/Program.cs index c34f00e..7d52c29 100644 --- a/src/Costasdev.Busurbano.Backend/Program.cs +++ b/src/Costasdev.Busurbano.Backend/Program.cs @@ -3,6 +3,7 @@ using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.Services; using Costasdev.Busurbano.Backend.Services.Processors; using Costasdev.Busurbano.Backend.Services.Providers; +using Costasdev.Busurbano.Sources.TranviasCoruna; var builder = WebApplication.CreateBuilder(args); @@ -17,10 +18,13 @@ builder.Services builder.Services.AddHttpClient(); builder.Services.AddMemoryCache(); + builder.Services.AddSingleton<ShapeTraversalService>(); builder.Services.AddSingleton<FeedService>(); builder.Services.AddScoped<IArrivalsProcessor, VitrasaRealTimeProcessor>(); +builder.Services.AddScoped<IArrivalsProcessor, CorunaRealTimeProcessor>(); + builder.Services.AddScoped<IArrivalsProcessor, FilterAndSortProcessor>(); builder.Services.AddScoped<IArrivalsProcessor, NextStopsProcessor>(); builder.Services.AddScoped<IArrivalsProcessor, MarqueeProcessor>(); diff --git a/src/Costasdev.Busurbano.Backend/Services/ArrivalsPipeline.cs b/src/Costasdev.Busurbano.Backend/Services/ArrivalsPipeline.cs index 8699a1e..3c9368c 100644 --- a/src/Costasdev.Busurbano.Backend/Services/ArrivalsPipeline.cs +++ b/src/Costasdev.Busurbano.Backend/Services/ArrivalsPipeline.cs @@ -1,3 +1,4 @@ +using Costasdev.Busurbano.Backend.Types; using Costasdev.Busurbano.Backend.Types.Arrivals; namespace Costasdev.Busurbano.Backend.Services; @@ -19,6 +20,8 @@ public class ArrivalsContext /// </summary> public bool IsReduced { get; set; } + public Position? StopLocation { get; set; } + public required List<Arrival> Arrivals { get; set; } public required DateTime NowLocal { get; set; } } diff --git a/src/Costasdev.Busurbano.Backend/Services/FeedService.cs b/src/Costasdev.Busurbano.Backend/Services/FeedService.cs index 48f9338..6cebcf2 100644 --- a/src/Costasdev.Busurbano.Backend/Services/FeedService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/FeedService.cs @@ -13,6 +13,9 @@ public class FeedService { "Rúa da Salguera Entrada", "Rúa da Salgueira" }, { "Rúa da Salgueira Entrada", "Rúa da Salgueira" }, { "Estrada de Miraflores", "Estrada Miraflores" }, + { "Avda. de Europa", "Avda. Europa" }, + { "Avda. de Galicia", "Avda. Galicia" }, + { "Avda. de Vigo", "Avda. Vigo" }, { "FORA DE SERVIZO.G.B.", "" }, { "Praza de Fernando O Católico", "" }, { "Rúa da Travesía de Vigo", "Travesía de Vigo" }, @@ -26,7 +29,8 @@ public class FeedService { "Avda. das ", " " }, { "Riós", "Ríos" }, { "Avda. Beiramar Porto Pesqueiro Berbés", "Berbés" }, - { "Conde de Torrecedeira", "Torrecedeira" } + { "Conde de Torrecedeira", "Torrecedeira" }, + }; public (string Color, string TextColor) GetFallbackColourForFeed(string feed) @@ -65,12 +69,23 @@ public class FeedService var lineStr = shortName.Substring(5); if (int.TryParse(lineStr, out int line)) { - return $"{contract}.{line}"; + return $"{contract}.{line:D2}"; } } return shortName; } + public string GetUniqueRouteShortName(string feedId, string shortName) + { + if (feedId == "xunta" && shortName.StartsWith("XG") && shortName.Length >= 8) + { + var contract = shortName.Substring(2, 3); + return $"XG{contract}"; + } + + return NormalizeRouteShortName(feedId, shortName); + } + public string NormalizeStopName(string feedId, string name) { if (feedId == "vitrasa") @@ -115,7 +130,7 @@ public class FeedService { if (nextStops.Count == 0) return null; - if (feedId == "vitrasa") + if (feedId == "vitrasa" || feedId == "coruna") { var streets = nextStops .Select(GetStreetName) diff --git a/src/Costasdev.Busurbano.Backend/Services/Processors/AbstractProcessor.cs b/src/Costasdev.Busurbano.Backend/Services/Processors/AbstractProcessor.cs new file mode 100644 index 0000000..343f511 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Services/Processors/AbstractProcessor.cs @@ -0,0 +1,56 @@ +using Costasdev.Busurbano.Backend.Services; + +public abstract class AbstractRealTimeProcessor : IArrivalsProcessor +{ + public abstract Task ProcessAsync(ArrivalsContext context); + + protected static List<(double Lat, double Lon)> Decode(string encodedPoints) + { + if (string.IsNullOrEmpty(encodedPoints)) + return new List<(double, double)>(); + + var poly = new List<(double, double)>(); + char[] polylineChars = encodedPoints.ToCharArray(); + int index = 0; + + int currentLat = 0; + int currentLng = 0; + int next5bits; + int sum; + int shifter; + + while (index < polylineChars.Length) + { + // calculate next latitude + sum = 0; + shifter = 0; + do + { + next5bits = (int)polylineChars[index++] - 63; + sum |= (next5bits & 31) << shifter; + shifter += 5; + } while (next5bits >= 32 && index < polylineChars.Length); + + if (index >= polylineChars.Length) + break; + + currentLat += (sum & 1) == 1 ? ~(sum >> 1) : (sum >> 1); + + // calculate next longitude + sum = 0; + shifter = 0; + do + { + next5bits = (int)polylineChars[index++] - 63; + sum |= (next5bits & 31) << shifter; + shifter += 5; + } while (next5bits >= 32 && index < polylineChars.Length); + + currentLng += (sum & 1) == 1 ? ~(sum >> 1) : (sum >> 1); + + poly.Add((Convert.ToDouble(currentLat) / 100000.0, Convert.ToDouble(currentLng) / 100000.0)); + } + + return poly; + } +} diff --git a/src/Costasdev.Busurbano.Backend/Services/Processors/CorunaRealTimeProcessor.cs b/src/Costasdev.Busurbano.Backend/Services/Processors/CorunaRealTimeProcessor.cs new file mode 100644 index 0000000..2ac1554 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Services/Processors/CorunaRealTimeProcessor.cs @@ -0,0 +1,189 @@ +using Costasdev.Busurbano.Backend.Configuration; +using Costasdev.Busurbano.Backend.GraphClient.App; +using Costasdev.Busurbano.Backend.Types; +using Costasdev.Busurbano.Backend.Types.Arrivals; +using Costasdev.VigoTransitApi; +using Costasdev.Busurbano.Sources.TranviasCoruna; +using Microsoft.Extensions.Options; +using Arrival = Costasdev.Busurbano.Backend.Types.Arrivals.Arrival; + +namespace Costasdev.Busurbano.Backend.Services.Processors; + +public class CorunaRealTimeProcessor : AbstractRealTimeProcessor +{ + private readonly CorunaRealtimeEstimatesProvider _realtime; + private readonly FeedService _feedService; + private readonly ILogger<CorunaRealTimeProcessor> _logger; + private readonly ShapeTraversalService _shapeService; + + public CorunaRealTimeProcessor( + HttpClient http, + FeedService feedService, + ILogger<CorunaRealTimeProcessor> logger, + ShapeTraversalService shapeService) + { + _realtime = new CorunaRealtimeEstimatesProvider(http); + _feedService = feedService; + _logger = logger; + _shapeService = shapeService; + } + + public override async Task ProcessAsync(ArrivalsContext context) + { + if (!context.StopId.StartsWith("coruna:")) return; + + var normalizedCode = _feedService.NormalizeStopCode("coruna", context.StopCode); + if (!int.TryParse(normalizedCode, out var numericStopId)) return; + + try + { + // Load schedule + var todayDate = context.NowLocal.Date.ToString("yyyy-MM-dd"); + + Epsg25829? stopLocation = null; + if (context.StopLocation != null) + { + stopLocation = _shapeService.TransformToEpsg25829(context.StopLocation.Latitude, context.StopLocation.Longitude); + } + + var realtime = await _realtime.GetEstimatesForStop(numericStopId); + + var usedTripIds = new HashSet<string>(); + var newArrivals = new List<Arrival>(); + + foreach (var estimate in realtime) + { + var bestMatch = context.Arrivals + .Where(a => !usedTripIds.Contains(a.TripId)) + .Where(a => a.Route.RouteIdInGtfs.Trim() == estimate.RouteId.Trim()) + .Select(a => + { + return new + { + Arrival = a, + TimeDiff = estimate.Minutes - a.Estimate.Minutes, // RealTime - Schedule + RouteMatch = true + }; + }) + .Where(x => x.RouteMatch) // Strict route matching + .Where(x => x.TimeDiff >= -7 && x.TimeDiff <= 75) // Allow 7m early (RealTime < Schedule) or 75m late (RealTime > Schedule) + .OrderBy(x => Math.Abs(x.TimeDiff)) // Best time fit + .FirstOrDefault(); + + if (bestMatch == null) + { + continue; + } + + var arrival = bestMatch.Arrival; + _logger.LogInformation("Matched Coruña real-time for line {Line}: {Scheduled}m -> {RealTime}m (diff: {Diff}m)", + arrival.Route.ShortName, arrival.Estimate.Minutes, estimate.Minutes, bestMatch.TimeDiff); + + var scheduledMinutes = arrival.Estimate.Minutes; + arrival.Estimate.Minutes = estimate.Minutes; + arrival.Estimate.Precision = ArrivalPrecision.Confident; + + // Calculate delay badge + var delayMinutes = estimate.Minutes - scheduledMinutes; + if (delayMinutes != 0) + { + arrival.Delay = new DelayBadge { Minutes = delayMinutes }; + } + + // Calculate position + if (stopLocation != null) + { + Position? currentPosition = null; + int? stopShapeIndex = null; + + if (arrival.RawOtpTrip is ArrivalsAtStopResponse.Arrival otpArrival && + otpArrival.Trip.Geometry?.Points != null) + { + var decodedPoints = Decode(otpArrival.Trip.Geometry.Points) + .Select(p => new Position { Latitude = p.Lat, Longitude = p.Lon }) + .ToList(); + + var shape = _shapeService.CreateShapeFromWgs84(decodedPoints); + + // Ensure meters is positive + var meters = Math.Max(0, estimate.Metres); + var result = _shapeService.GetBusPosition(shape, stopLocation, meters); + + currentPosition = result.BusPosition; + stopShapeIndex = result.StopIndex; + + if (currentPosition != null) + { + _logger.LogInformation("Calculated position from OTP geometry for trip {TripId}: {Lat}, {Lon}", arrival.TripId, currentPosition.Latitude, currentPosition.Longitude); + } + + // Populate Shape GeoJSON + if (!context.IsReduced && currentPosition != null) + { + var features = new List<object>(); + features.Add(new + { + type = "Feature", + geometry = new + { + type = "LineString", + coordinates = decodedPoints.Select(p => new[] { p.Longitude, p.Latitude }).ToList() + }, + properties = new { type = "route" } + }); + + // Add stops if available + if (otpArrival.Trip.Stoptimes != null) + { + foreach (var stoptime in otpArrival.Trip.Stoptimes) + { + features.Add(new + { + type = "Feature", + geometry = new + { + type = "Point", + coordinates = new[] { stoptime.Stop.Lon, stoptime.Stop.Lat } + }, + properties = new + { + type = "stop", + name = stoptime.Stop.Name + } + }); + } + } + + arrival.Shape = new + { + type = "FeatureCollection", + features = features + }; + } + } + + if (currentPosition != null) + { + arrival.CurrentPosition = currentPosition; + arrival.StopShapeIndex = stopShapeIndex; + } + } + + usedTripIds.Add(arrival.TripId); + + } + + context.Arrivals.AddRange(newArrivals); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching Vitrasa real-time data for stop {StopId}", context.StopId); + } + } + + private static bool IsRouteMatch(string a, string b) + { + return a == b || a.Contains(b) || b.Contains(a); + } + +} diff --git a/src/Costasdev.Busurbano.Backend/Services/Processors/ShapeProcessor.cs b/src/Costasdev.Busurbano.Backend/Services/Processors/ShapeProcessor.cs index 300ce70..93e4a4f 100644 --- a/src/Costasdev.Busurbano.Backend/Services/Processors/ShapeProcessor.cs +++ b/src/Costasdev.Busurbano.Backend/Services/Processors/ShapeProcessor.cs @@ -20,6 +20,9 @@ public class ShapeProcessor : IArrivalsProcessor foreach (var arrival in context.Arrivals) { + // If shape is already populated (e.g. by VitrasaRealTimeProcessor), skip + if (arrival.Shape != null) continue; + if (arrival.RawOtpTrip is not ArrivalsAtStopResponse.Arrival otpArrival) continue; var encodedPoints = otpArrival.Trip.Geometry?.Points; @@ -34,14 +37,46 @@ public class ShapeProcessor : IArrivalsProcessor var points = Decode(encodedPoints); if (points.Count == 0) continue; - arrival.Shape = new + var features = new List<object>(); + + // Route LineString + features.Add(new { type = "Feature", geometry = new { type = "LineString", coordinates = points.Select(p => new[] { p.Lon, p.Lat }).ToList() + }, + properties = new { type = "route" } + }); + + // Stops + if (otpArrival.Trip.Stoptimes != null) + { + foreach (var stoptime in otpArrival.Trip.Stoptimes) + { + features.Add(new + { + type = "Feature", + geometry = new + { + type = "Point", + coordinates = new[] { stoptime.Stop.Lon, stoptime.Stop.Lat } + }, + properties = new + { + type = "stop", + name = stoptime.Stop.Name + } + }); } + } + + arrival.Shape = new + { + type = "FeatureCollection", + features = features }; } catch (Exception ex) diff --git a/src/Costasdev.Busurbano.Backend/Services/Processors/VitrasaRealTimeProcessor.cs b/src/Costasdev.Busurbano.Backend/Services/Processors/VitrasaRealTimeProcessor.cs index 7c98cfb..a16425f 100644 --- a/src/Costasdev.Busurbano.Backend/Services/Processors/VitrasaRealTimeProcessor.cs +++ b/src/Costasdev.Busurbano.Backend/Services/Processors/VitrasaRealTimeProcessor.cs @@ -1,23 +1,35 @@ +using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.GraphClient.App; +using Costasdev.Busurbano.Backend.Types; using Costasdev.Busurbano.Backend.Types.Arrivals; using Costasdev.VigoTransitApi; +using Microsoft.Extensions.Options; namespace Costasdev.Busurbano.Backend.Services.Processors; -public class VitrasaRealTimeProcessor : IArrivalsProcessor +public class VitrasaRealTimeProcessor : AbstractRealTimeProcessor { private readonly VigoTransitApiClient _api; private readonly FeedService _feedService; private readonly ILogger<VitrasaRealTimeProcessor> _logger; + private readonly ShapeTraversalService _shapeService; + private readonly AppConfiguration _configuration; - public VitrasaRealTimeProcessor(HttpClient http, FeedService feedService, ILogger<VitrasaRealTimeProcessor> logger) + public VitrasaRealTimeProcessor( + HttpClient http, + FeedService feedService, + ILogger<VitrasaRealTimeProcessor> logger, + ShapeTraversalService shapeService, + IOptions<AppConfiguration> options) { _api = new VigoTransitApiClient(http); _feedService = feedService; _logger = logger; + _shapeService = shapeService; + _configuration = options.Value; } - public async Task ProcessAsync(ArrivalsContext context) + public override async Task ProcessAsync(ArrivalsContext context) { if (!context.StopId.StartsWith("vitrasa:")) return; @@ -26,6 +38,15 @@ public class VitrasaRealTimeProcessor : IArrivalsProcessor try { + // Load schedule + var todayDate = context.NowLocal.Date.ToString("yyyy-MM-dd"); + + Epsg25829? stopLocation = null; + if (context.StopLocation != null) + { + stopLocation = _shapeService.TransformToEpsg25829(context.StopLocation.Latitude, context.StopLocation.Longitude); + } + var realtime = await _api.GetStopEstimates(numericStopId); var estimates = realtime.Estimates .Where(e => !string.IsNullOrWhiteSpace(e.Route) && !e.Route.Trim().EndsWith('*')) @@ -110,6 +131,85 @@ public class VitrasaRealTimeProcessor : IArrivalsProcessor arrival.Headsign.Destination = estimate.Route; } + // Calculate position + if (stopLocation != null) + { + Position? currentPosition = null; + int? stopShapeIndex = null; + + if (arrival.RawOtpTrip is ArrivalsAtStopResponse.Arrival otpArrival && + otpArrival.Trip.Geometry?.Points != null) + { + var decodedPoints = Decode(otpArrival.Trip.Geometry.Points) + .Select(p => new Position { Latitude = p.Lat, Longitude = p.Lon }) + .ToList(); + + var shape = _shapeService.CreateShapeFromWgs84(decodedPoints); + + // Ensure meters is positive + var meters = Math.Max(0, estimate.Meters); + var result = _shapeService.GetBusPosition(shape, stopLocation, meters); + + currentPosition = result.BusPosition; + stopShapeIndex = result.StopIndex; + + if (currentPosition != null) + { + _logger.LogInformation("Calculated position from OTP geometry for trip {TripId}: {Lat}, {Lon}", arrival.TripId, currentPosition.Latitude, currentPosition.Longitude); + } + + // Populate Shape GeoJSON + if (!context.IsReduced && currentPosition != null) + { + var features = new List<object>(); + features.Add(new + { + type = "Feature", + geometry = new + { + type = "LineString", + coordinates = decodedPoints.Select(p => new[] { p.Longitude, p.Latitude }).ToList() + }, + properties = new { type = "route" } + }); + + // Add stops if available + if (otpArrival.Trip.Stoptimes != null) + { + foreach (var stoptime in otpArrival.Trip.Stoptimes) + { + features.Add(new + { + type = "Feature", + geometry = new + { + type = "Point", + coordinates = new[] { stoptime.Stop.Lon, stoptime.Stop.Lat } + }, + properties = new + { + type = "stop", + name = stoptime.Stop.Name + } + }); + } + } + + arrival.Shape = new + { + type = "FeatureCollection", + features = features + }; + } + } + + if (currentPosition != null) + { + arrival.CurrentPosition = currentPosition; + arrival.StopShapeIndex = stopShapeIndex; + } + } + usedTripIds.Add(arrival.TripId); } else @@ -126,9 +226,10 @@ public class VitrasaRealTimeProcessor : IArrivalsProcessor TripId = $"vitrasa:rt:{estimate.Line}:{estimate.Route}:{estimate.Minutes}", Route = new RouteInfo { + GtfsId = $"vitrasa:{estimate.Line}", ShortName = estimate.Line, Colour = template?.Route.Colour ?? "FFFFFF", - TextColour = template?.Route.TextColour ?? "000000" + TextColour = template?.Route.TextColour ?? "000000", }, Headsign = new HeadsignInfo { diff --git a/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs b/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs index 63f4a2e..c3c66f4 100644 --- a/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs +++ b/src/Costasdev.Busurbano.Backend/Services/ShapeTraversalService.cs @@ -95,6 +95,26 @@ public class ShapeTraversalService return FindClosestPointIndex(shape.Points, location); } + public Shape CreateShapeFromWgs84(List<Position> points) + { + var shape = new Shape(); + var inverseTransform = _transformation.MathTransform.Inverse(); + + foreach (var point in points) + { + var transformed = inverseTransform.Transform(new[] { point.Longitude, point.Latitude }); + shape.Points.Add(new Epsg25829 { X = transformed[0], Y = transformed[1] }); + } + return shape; + } + + public Epsg25829 TransformToEpsg25829(double lat, double lon) + { + var inverseTransform = _transformation.MathTransform.Inverse(); + var transformed = inverseTransform.Transform(new[] { lon, lat }); + return new Epsg25829 { X = transformed[0], Y = transformed[1] }; + } + /// <summary> /// Calculates the bus position by reverse-traversing the shape from the stop location /// </summary> diff --git a/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs b/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs index 65ef606..f13babf 100644 --- a/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs +++ b/src/Costasdev.Busurbano.Backend/Types/Arrivals/Arrival.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using Costasdev.Busurbano.Backend.Types; namespace Costasdev.Busurbano.Backend.Types.Arrivals; @@ -25,6 +26,12 @@ public class Arrival [JsonPropertyName("shape")] public object? Shape { get; set; } + [JsonPropertyName("currentPosition")] + public Position? CurrentPosition { get; set; } + + [JsonPropertyName("stopShapeIndex")] + public int? StopShapeIndex { get; set; } + [JsonIgnore] public List<string> NextStops { get; set; } = []; @@ -34,6 +41,11 @@ public class Arrival public class RouteInfo { + [JsonPropertyName("gtfsId")] + public required string GtfsId { get; set; } + + public string RouteIdInGtfs => GtfsId.Split(':', 2)[1]; + [JsonPropertyName("shortName")] public required string ShortName { get; set; } diff --git a/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs b/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs index 8c5438c..9a2cec7 100644 --- a/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs +++ b/src/Costasdev.Busurbano.Backend/Types/Arrivals/StopArrivalsResponse.cs @@ -10,6 +10,12 @@ public class StopArrivalsResponse [JsonPropertyName("stopName")] public required string StopName { get; set; } + [JsonPropertyName("stopLocation")] + public Position? StopLocation { get; set; } + + [JsonPropertyName("routes")] + public List<RouteInfo> Routes { get; set; } = []; + [JsonPropertyName("arrivals")] public List<Arrival> Arrivals { get; set; } = []; } |
