diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-29 00:41:52 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2025-12-29 00:41:52 +0100 |
| commit | a304c24b32c0327436bbd8c2853e60668e161b42 (patch) | |
| tree | 08f65c05daca134cf4d2e4f779bd15d98fd66370 /src/Enmarcha.Backend/Services/OtpService.cs | |
| parent | 120a3c6bddd0fb8d9fa05df4763596956554c025 (diff) | |
Rename a lot of stuff, add Santiago real time
Diffstat (limited to 'src/Enmarcha.Backend/Services/OtpService.cs')
| -rw-r--r-- | src/Enmarcha.Backend/Services/OtpService.cs | 357 |
1 files changed, 357 insertions, 0 deletions
diff --git a/src/Enmarcha.Backend/Services/OtpService.cs b/src/Enmarcha.Backend/Services/OtpService.cs new file mode 100644 index 0000000..e4b4846 --- /dev/null +++ b/src/Enmarcha.Backend/Services/OtpService.cs @@ -0,0 +1,357 @@ +using System.Globalization; +using Enmarcha.Sources.OpenTripPlannerGql.Queries; +using Enmarcha.Backend.Configuration; +using Enmarcha.Backend.Helpers; +using Enmarcha.Backend.Types.Otp; +using Enmarcha.Backend.Types.Planner; +using Enmarcha.Backend.Types.Transit; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Enmarcha.Backend.Services; + +public class OtpService +{ + private readonly HttpClient _httpClient; + private readonly AppConfiguration _config; + private readonly IMemoryCache _cache; + private readonly ILogger<OtpService> _logger; + private readonly FareService _fareService; + private readonly LineFormatterService _lineFormatter; + private readonly FeedService _feedService; + + public OtpService(HttpClient httpClient, IOptions<AppConfiguration> config, IMemoryCache cache, ILogger<OtpService> logger, FareService fareService, LineFormatterService lineFormatter, FeedService feedService) + { + _httpClient = httpClient; + _config = config.Value; + _cache = cache; + _logger = logger; + _fareService = fareService; + _lineFormatter = lineFormatter; + _feedService = feedService; + } + + public RouteDto MapRoute(RoutesListResponse.RouteItem route) + { + var feedId = route.GtfsId.Split(':')[0]; + return new RouteDto + { + Id = route.GtfsId, + ShortName = _feedService.NormalizeRouteShortName(feedId, route.ShortName ?? string.Empty), + LongName = route.LongName, + Color = route.Color, + TextColor = route.TextColor, + SortOrder = route.SortOrder, + AgencyName = route.Agency?.Name, + TripCount = route.Patterns.Sum(p => p.TripsForDate.Count) + }; + } + + public RouteDetailsDto MapRouteDetails(RouteDetailsResponse.RouteItem route) + { + var feedId = route.GtfsId?.Split(':')[0] ?? "unknown"; + return new RouteDetailsDto + { + ShortName = _feedService.NormalizeRouteShortName(feedId, route.ShortName ?? string.Empty), + LongName = route.LongName, + Color = route.Color, + TextColor = route.TextColor, + AgencyName = route.Agency?.Name, + Patterns = route.Patterns.Select(p => MapPattern(p, feedId)).ToList() + }; + } + + private PatternDto MapPattern(RouteDetailsResponse.PatternItem pattern, string feedId) + { + return new PatternDto + { + Id = pattern.Id, + Name = pattern.Name, + Headsign = pattern.Headsign, + DirectionId = pattern.DirectionId, + Code = pattern.Code, + SemanticHash = pattern.SemanticHash, + TripCount = pattern.TripsForDate.Count, + Geometry = DecodePolyline(pattern.PatternGeometry?.Points)?.Coordinates, + Stops = pattern.Stops.Select((s, i) => new PatternStopDto + { + Id = s.GtfsId, + Code = _feedService.NormalizeStopCode(feedId, s.Code ?? string.Empty), + Name = _feedService.NormalizeStopName(feedId, s.Name), + Lat = s.Lat, + Lon = s.Lon, + ScheduledDepartures = pattern.TripsForDate + .Select(t => t.Stoptimes.ElementAtOrDefault(i)?.ScheduledDeparture ?? -1) + .Where(d => d != -1) + .OrderBy(d => d) + .ToList() + }).ToList() + }; + } + + private Leg MapLeg(OtpLeg otpLeg) + { + return new Leg + { + Mode = otpLeg.Mode, + RouteName = otpLeg.Route, + RouteShortName = otpLeg.RouteShortName, + RouteLongName = otpLeg.RouteLongName, + Headsign = otpLeg.Headsign, + AgencyName = otpLeg.AgencyName, + RouteColor = otpLeg.RouteColor, + RouteTextColor = otpLeg.RouteTextColor, + From = MapPlace(otpLeg.From), + To = MapPlace(otpLeg.To), + StartTime = DateTimeOffset.FromUnixTimeMilliseconds(otpLeg.StartTime).UtcDateTime, + EndTime = DateTimeOffset.FromUnixTimeMilliseconds(otpLeg.EndTime).UtcDateTime, + DistanceMeters = otpLeg.Distance, + Geometry = DecodePolyline(otpLeg.LegGeometry?.Points), + Steps = otpLeg.Steps.Select(MapStep).ToList(), + IntermediateStops = otpLeg.IntermediateStops.Select(MapPlace).Where(p => p != null).Cast<PlannerPlace>().ToList() + }; + } + + private PlannerPlace? MapPlace(OtpPlace? otpPlace) + { + if (otpPlace == null) return null; + var feedId = otpPlace.StopId?.Split(':')[0] ?? "unknown"; + return new PlannerPlace + { + Name = _feedService.NormalizeStopName(feedId, otpPlace.Name), + Lat = otpPlace.Lat, + Lon = otpPlace.Lon, + StopId = otpPlace.StopId, // Use string directly + StopCode = _feedService.NormalizeStopCode(feedId, otpPlace.StopCode ?? string.Empty) + }; + } + + private Step MapStep(OtpWalkStep otpStep) + { + return new Step + { + DistanceMeters = otpStep.Distance, + RelativeDirection = otpStep.RelativeDirection, + AbsoluteDirection = otpStep.AbsoluteDirection, + StreetName = otpStep.StreetName, + Lat = otpStep.Lat, + Lon = otpStep.Lon + }; + } + + private PlannerGeometry? DecodePolyline(string? encodedPoints) + { + if (string.IsNullOrEmpty(encodedPoints)) return null; + + var coordinates = Decode(encodedPoints); + return new PlannerGeometry + { + Coordinates = coordinates.Select(c => new List<double> { c.Lon, c.Lat }).ToList() + }; + } + + // Polyline decoding algorithm + private 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; + } + + public RoutePlan MapPlanResponse(PlanConnectionResponse response) + { + var itineraries = response.PlanConnection.Edges + .Select(e => MapItinerary(e.Node)) + .ToList(); + + return new RoutePlan + { + Itineraries = itineraries + }; + } + + private Itinerary MapItinerary(PlanConnectionResponse.Node node) + { + var legs = node.Legs.Select(MapLeg).ToList(); + var fares = _fareService.CalculateFare(legs); + + return new Itinerary + { + DurationSeconds = node.DurationSeconds, + StartTime = DateTime.Parse(node.Start8601, null, DateTimeStyles.RoundtripKind), + EndTime = DateTime.Parse(node.End8601, null, DateTimeStyles.RoundtripKind), + WalkDistanceMeters = node.WalkDistance, + WalkTimeSeconds = node.WalkSeconds, + TransitTimeSeconds = node.DurationSeconds - node.WalkSeconds - node.WaitingSeconds, + WaitingTimeSeconds = node.WaitingSeconds, + Legs = legs, + CashFare = fares.CashFareEuro, + CashFareIsTotal = fares.CashFareIsTotal, + CardFare = fares.CardFareEuro, + CardFareIsTotal = fares.CardFareIsTotal + }; + } + + private Leg MapLeg(PlanConnectionResponse.Leg leg) + { + var feedId = leg.From.Stop?.GtfsId?.Split(':')[0] ?? "unknown"; + var shortName = _feedService.NormalizeRouteShortName(feedId, leg.Route?.ShortName ?? string.Empty); + var headsign = leg.Headsign; + + if (feedId == "vitrasa") + { + headsign = headsign?.Replace("*", ""); + if (headsign == "FORA DE SERVIZO.G.B.") + { + headsign = "García Barbón, 7 (fora de servizo)"; + } + + switch (shortName) + { + case "A" when headsign != null && headsign.StartsWith("\"1\""): + shortName = "A1"; + headsign = headsign.Replace("\"1\"", ""); + break; + case "6": + headsign = headsign?.Replace("\"", ""); + break; + case "FUT": + if (headsign == "CASTELAO-CAMELIAS-G.BARBÓN.M.GARRIDO") + { + shortName = "MAR"; + headsign = "MARCADOR ⚽: CASTELAO-CAMELIAS-G.BARBÓN.M.GARRIDO"; + } + else if (headsign == "P. ESPAÑA-T.VIGO-S.BADÍA") + { + shortName = "RIO"; + headsign = "RÍO ⚽: P. ESPAÑA-T.VIGO-S.BADÍA"; + } + else if (headsign == "NAVIA-BOUZAS-URZAIZ-G. ESPINO") + { + shortName = "GOL"; + headsign = "GOL ⚽: NAVIA-BOUZAS-URZAIZ-G. ESPINO"; + } + break; + } + } + + var color = leg.Route?.Color; + var textColor = leg.Route?.TextColor; + + if (string.IsNullOrEmpty(color) || color == "FFFFFF") + { + var (fallbackColor, fallbackTextColor) = _feedService.GetFallbackColourForFeed(feedId); + color = fallbackColor.Replace("#", ""); + textColor = fallbackTextColor.Replace("#", ""); + } + else if (string.IsNullOrEmpty(textColor) || textColor == "000000") + { + textColor = ContrastHelper.GetBestTextColour(color).Replace("#", ""); + } + + return new Leg + { + Mode = leg.Mode, + FeedId = feedId, + RouteId = leg.Route?.GtfsId, + RouteName = leg.Route?.LongName, + RouteShortName = shortName, + RouteLongName = leg.Route?.LongName, + Headsign = headsign, + AgencyName = leg.Route?.Agency?.Name, + RouteColor = color, + RouteTextColor = textColor, + From = MapPlace(leg.From), + To = MapPlace(leg.To), + StartTime = DateTime.Parse(leg.Start.ScheduledTime8601, null, DateTimeStyles.RoundtripKind), + EndTime = DateTime.Parse(leg.End.ScheduledTime8601, null, DateTimeStyles.RoundtripKind), + DistanceMeters = leg.Distance, + Geometry = DecodePolyline(leg.LegGeometry?.Points), + Steps = leg.Steps.Select(MapStep).ToList(), + IntermediateStops = leg.StopCalls.Select(sc => MapPlace(sc.StopLocation)).ToList() + }; + } + + private PlannerPlace MapPlace(PlanConnectionResponse.LegPosition pos) + { + var feedId = pos.Stop?.GtfsId?.Split(':')[0] ?? "unknown"; + return new PlannerPlace + { + Name = _feedService.NormalizeStopName(feedId, pos.Name), + Lat = pos.Latitude, + Lon = pos.Longitude, + StopId = pos.Stop?.GtfsId, + StopCode = _feedService.NormalizeStopCode(feedId, pos.Stop?.Code ?? string.Empty), + ZoneId = pos.Stop?.ZoneId + }; + } + + private PlannerPlace MapPlace(PlanConnectionResponse.StopLocation stop) + { + var feedId = stop.GtfsId?.Split(':')[0] ?? "unknown"; + return new PlannerPlace + { + Name = _feedService.NormalizeStopName(feedId, stop.Name), + Lat = stop.Latitude, + Lon = stop.Longitude, + StopId = stop.GtfsId, + StopCode = _feedService.NormalizeStopCode(feedId, stop.Code ?? string.Empty) + }; + } + + private Step MapStep(PlanConnectionResponse.Step step) + { + return new Step + { + DistanceMeters = step.Distance, + RelativeDirection = step.RelativeDirection, + AbsoluteDirection = step.AbsoluteDirection, + StreetName = step.StreetName, + Lat = step.Latitude, + Lon = step.Longitude + }; + } +} |
