From 66758ffaa4238191010ccc3dde7e5cdc6445f315 Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Thu, 6 Nov 2025 18:01:14 +0100 Subject: Seemingly fix algorithm (thanks copilot) --- .../Controllers/VigoController.cs | 117 +++++++++++++-------- .../Types/ConsolidatedCirculation.cs | 32 ++++++ .../Types/VigoSchedules.cs | 9 +- 3 files changed, 113 insertions(+), 45 deletions(-) create mode 100644 src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs (limited to 'src/Costasdev.Busurbano.Backend') diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs index e82baed..01558b6 100644 --- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs +++ b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs @@ -5,7 +5,6 @@ using Costasdev.Busurbano.Backend.Configuration; using Costasdev.Busurbano.Backend.Types; using Costasdev.VigoTransitApi; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Options; using SysFile = System.IO.File; @@ -15,9 +14,9 @@ namespace Costasdev.Busurbano.Backend.Controllers; [Route("api/vigo")] public class VigoController : ControllerBase { - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly VigoTransitApiClient _api; - private readonly AppConfiguration _configuration; + private readonly AppConfiguration _configuration; public VigoController(HttpClient http, IOptions options, ILogger logger) { @@ -47,7 +46,7 @@ public class VigoController : ControllerBase public async Task GetStopTimetable( [FromQuery] int stopId, [FromQuery] string date - ) + ) { // Validate date format if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, DateTimeStyles.None, out _)) @@ -66,7 +65,7 @@ public class VigoController : ControllerBase _logger.LogError(ex, "Stop data not found for stop {StopId} on date {Date}", stopId, date); return StatusCode(404, $"Stop data not found for stop {stopId} on date {date}"); } - catch(Exception ex) + catch (Exception ex) { _logger.LogError(ex, "Error loading stop data"); return StatusCode(500, "Error loading timetable"); @@ -94,82 +93,111 @@ public class VigoController : ControllerBase { StringBuilder outputBuffer = new(); - var now = DateTime.Now.AddSeconds(60 - DateTime.Now.Second); var realtimeTask = _api.GetStopEstimates(stopId); - var timetableTask = LoadTimetable(stopId.ToString(), now.ToString("yyyy-MM-dd")); + var timetableTask = LoadTimetable(stopId.ToString(), DateTime.Today.ToString("yyyy-MM-dd")); await Task.WhenAll(realtimeTask, timetableTask); var realTimeEstimates = realtimeTask.Result.Estimates; var timetable = timetableTask.Result; + var now = DateTime.Now.AddSeconds(60 - DateTime.Now.Second); + var endOfScope = now.AddMinutes( + realTimeEstimates.OrderByDescending(e => e.Minutes).First().Minutes + 10 + ); + + List consolidatedCirculations = []; + foreach (var estimate in realTimeEstimates) { - outputBuffer.AppendLine($"Parsing estimate with line={estimate.Line}, route={estimate.Route} and minutes={estimate.Minutes} - Arrives at {now.AddMinutes(estimate.Minutes):HH:mm}"); - var fullArrivalTime = now.AddMinutes(estimate.Minutes); + var estimatedArrivalTime = now.AddMinutes(estimate.Minutes); var possibleCirculations = timetable - .Where(c => c.Line.Trim() == estimate.Line.Trim() && c.Route.Trim() == estimate.Route.Trim()) + .Where(c => + c.Line.Trim() == estimate.Line.Trim() && + c.Route.Trim() == estimate.Route.Trim() + ) .OrderBy(c => c.CallingDateTime()) .ToArray(); - outputBuffer.AppendLine($"Found {possibleCirculations.Length} potential circulations"); - ScheduledStop? closestCirculation = null; - int closestCirculationTime = int.MaxValue; - foreach (var circulation in possibleCirculations) - { - var diffBetweenScheduleAndTrip = (int)Math.Round((fullArrivalTime - circulation.CallingDateTime()).TotalMinutes); - var diffBetweenNowAndSchedule = (int)(fullArrivalTime - now).TotalMinutes; + // Matching strategy: + // 1) Prefer a started trip whose scheduled calling time is close to the estimated arrival. + // 2) If no good started match, pick the next not-started trip (soonest in the future). + // 3) Fallbacks: if no future trips, use the best started one even if far. + const int startedMatchToleranceMinutes = 15; // how close a started trip must be to consider it a match - var tolerance = Math.Max(2, diffBetweenNowAndSchedule * 0.15); // Positive amount of minutes - if (diffBetweenScheduleAndTrip <= -tolerance) + var startedCandidates = possibleCirculations + .Where(c => c.StartingDateTime() <= now) + .Select(c => new { - break; - } + Circulation = c, + AbsDiff = Math.Abs((estimatedArrivalTime - c.CallingDateTime()).TotalMinutes) + }) + .OrderBy(x => x.AbsDiff) + .ToList(); - if (diffBetweenScheduleAndTrip < closestCirculationTime) - { - closestCirculation = circulation; - closestCirculationTime = diffBetweenScheduleAndTrip; - } + var bestStarted = startedCandidates.FirstOrDefault(); + + var futureCandidates = possibleCirculations + .Where(c => c.StartingDateTime() > now) + .ToList(); + if (bestStarted != null && bestStarted.AbsDiff <= startedMatchToleranceMinutes) + { + closestCirculation = bestStarted.Circulation; + } + else if (futureCandidates.Count > 0) + { + // pick the soonest upcoming trip for this line/route + closestCirculation = futureCandidates.First(); + } + else if (bestStarted != null) + { + // nothing upcoming today; fallback to the closest started one (even if far) + closestCirculation = bestStarted.Circulation; } if (closestCirculation == null) { + _logger.LogError("No stop arrival merged for line {Line} towards {Route} in {Minutes} minutes", estimate.Line, estimate.Route, estimate.Minutes); outputBuffer.AppendLine("**No circulation matched. List of all of them:**"); foreach (var circulation in possibleCirculations) { // Circulation A 03LP000_008003_16 stopping at 05/11/2025 22:06:00 (diff: -03:29:59.2644092) - outputBuffer.AppendLine($"Circulation {circulation.TripId} stopping at {circulation.CallingDateTime()} (diff: {fullArrivalTime - circulation.CallingDateTime():HH:mm})"); + outputBuffer.AppendLine( + $"Circulation {circulation.TripId} stopping at {circulation.CallingDateTime()} (diff: {estimatedArrivalTime - circulation.CallingDateTime():HH:mm})"); } + outputBuffer.AppendLine(); continue; } - if (closestCirculationTime > 0) - { - outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving {closestCirculationTime} minutes LATE"); - } - else if (closestCirculationTime == 0) + consolidatedCirculations.Add(new ConsolidatedCirculation { - outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving ON TIME"); - } - else - { - outputBuffer.Append($"Closest circulation is {closestCirculation.TripId} and arriving {Math.Abs(closestCirculationTime)} minutes EARLY"); - } - - outputBuffer.AppendLine( - $" -- Circulation expected at {closestCirculation.CallingDateTime():HH:mm)}"); - - outputBuffer.AppendLine(); + Line = estimate.Line, + Route = estimate.Route, + Schedule = new ScheduleData + { + Running = closestCirculation.StartingDateTime() <= now, + Minutes = (int)(closestCirculation.CallingDateTime() - now).TotalMinutes, + TripId = closestCirculation.TripId, + ServiceId = closestCirculation.ServiceId, + }, + RealTime = new RealTimeData + { + Minutes = estimate.Minutes, + Distance = estimate.Meters, + Confidence = closestCirculation.StartingDateTime() <= now + ? RealTimeConfidence.High + : RealTimeConfidence.Low + } + }); } - return Ok(outputBuffer.ToString()); + return Ok(consolidatedCirculations); } private async Task> LoadTimetable(string stopId, string dateString) @@ -179,6 +207,7 @@ public class VigoController : ControllerBase { throw new FileNotFoundException(); } + var contents = await SysFile.ReadAllTextAsync(file); return JsonSerializer.Deserialize>(contents)!; } diff --git a/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs new file mode 100644 index 0000000..d65da61 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Types/ConsolidatedCirculation.cs @@ -0,0 +1,32 @@ +namespace Costasdev.Busurbano.Backend.Types; + +public class ConsolidatedCirculation +{ + public required string Line { get; set; } + public required string Route { get; set; } + + public ScheduleData? Schedule { get; set; } + public RealTimeData? RealTime { get; set; } +} + +public class RealTimeData +{ + public required int Minutes { get; set; } + public required int Distance { get; set; } + public required RealTimeConfidence Confidence { get; set; } +} + +public class ScheduleData +{ + public bool Running { get; set; } + public required int Minutes { get; set; } + public required string ServiceId { get; set; } + public required string TripId { get; set; } +} + +public enum RealTimeConfidence +{ + NotApplicable = 0, + Low = 1, + High = 2 +} diff --git a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs index f8e5634..25fc34f 100644 --- a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs +++ b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs @@ -14,9 +14,17 @@ public class ScheduledStop public required double ShapeDistTraveled { get; set; } [JsonPropertyName("next_streets")] public required string[] NextStreets { get; set; } + [JsonPropertyName("starting_code")] public required string StartingCode { get; set; } [JsonPropertyName("starting_name")] public required string StartingName { get; set; } [JsonPropertyName("starting_time")] public required string StartingTime { get; set; } + public DateTime StartingDateTime() + { + var dt = DateTime.Today + TimeOnly.Parse(StartingTime).ToTimeSpan(); + return dt.AddSeconds(60 - dt.Second); + } + + [JsonPropertyName("calling_ssm")] public required int CallingSsm { get; set; } [JsonPropertyName("calling_time")] public required string CallingTime { get; set; } public DateTime CallingDateTime() { @@ -24,7 +32,6 @@ public class ScheduledStop return dt.AddSeconds(60 - dt.Second); } - [JsonPropertyName("calling_ssm")] public required int CallingSsm { get; set; } [JsonPropertyName("terminus_code")] public required string TerminusCode { get; set; } [JsonPropertyName("terminus_name")] public required string TerminusName { get; set; } [JsonPropertyName("terminus_time")] public required string TerminusTime { get; set; } -- cgit v1.3