From 236d760d20a5ade402f8e4e4da92332a09f0bcde Mon Sep 17 00:00:00 2001 From: Ariel Costas Guerrero Date: Thu, 6 Nov 2025 00:29:25 +0100 Subject: Begin (poorly) implementing merging scheduled and real-time data --- Costasdev.Busurbano.sln.DotSettings.user | 4 + package-lock.json | 6 + .../Types/VigoSchedules.cs | 38 +++++ src/Costasdev.Busurbano.Backend/VigoController.cs | 181 +++++++++++++++------ src/frontend/app/components/TimetableTable.css | 6 +- src/frontend/package-lock.json | 3 + 6 files changed, 185 insertions(+), 53 deletions(-) create mode 100644 package-lock.json create mode 100644 src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs diff --git a/Costasdev.Busurbano.sln.DotSettings.user b/Costasdev.Busurbano.sln.DotSettings.user index 5e721e3..1cbbce0 100644 --- a/Costasdev.Busurbano.sln.DotSettings.user +++ b/Costasdev.Busurbano.sln.DotSettings.user @@ -1,5 +1,9 @@  + ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f5e7798 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "Costasdev.Busurbano", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs new file mode 100644 index 0000000..d6012e5 --- /dev/null +++ b/src/Costasdev.Busurbano.Backend/Types/VigoSchedules.cs @@ -0,0 +1,38 @@ +using System.Text.Json.Serialization; + +namespace Costasdev.Busurbano.Backend.Types; + +public class ScheduledStop +{ + [JsonPropertyName("line")] public required Line Line { get; set; } + [JsonPropertyName("trip")] public required Trip Trip { get; set; } + [JsonPropertyName("route_id")] public required string RouteId { get; set; } + [JsonPropertyName("departure_time")] public required string DepartureTime { get; set; } + + public DateTime DepartureDateTime() + { + var dt = DateTime.Today + TimeOnly.Parse(DepartureTime).ToTimeSpan(); + return dt.AddSeconds(60 - dt.Second); + } + + [JsonPropertyName("stop_sequence")] public required int StopSequence { get; set; } + + [JsonPropertyName("shape_dist_traveled")] + public required float ShapeDistTraveled { get; set; } + + [JsonPropertyName("next_streets")] public required string[] NextStreets { get; set; } +} + +public class Line +{ + [JsonPropertyName("name")] public required string Name { get; set; } + [JsonPropertyName("colour")] public required string Colour { get; set; } +} + +public class Trip +{ + [JsonPropertyName("id")] public required string Id { get; set; } + [JsonPropertyName("service_id")] public required string ServiceId { get; set; } + [JsonPropertyName("headsign")] public required string Headsign { get; set; } + [JsonPropertyName("direction_id")] public required int DirectionId { get; set; } +} diff --git a/src/Costasdev.Busurbano.Backend/VigoController.cs b/src/Costasdev.Busurbano.Backend/VigoController.cs index 147c22a..6d321b8 100644 --- a/src/Costasdev.Busurbano.Backend/VigoController.cs +++ b/src/Costasdev.Busurbano.Backend/VigoController.cs @@ -1,7 +1,11 @@ +using System.Globalization; +using System.Text; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Memory; using Costasdev.VigoTransitApi; using System.Text.Json; +using Costasdev.Busurbano.Backend.Types; +using Costasdev.VigoTransitApi.Types; namespace Costasdev.Busurbano.Backend; @@ -21,23 +25,13 @@ public class VigoController : ControllerBase } [HttpGet("GetStopEstimates")] - public async Task Run() + public async Task Run( + [FromQuery] int stopId + ) { - var argumentAvailable = Request.Query.TryGetValue("id", out var requestedStopIdString); - if (!argumentAvailable) - { - return BadRequest("Please provide a stop id as a query parameter with the name 'id'."); - } - - var argumentNumber = int.TryParse(requestedStopIdString, out var requestedStopId); - if (!argumentNumber) - { - return BadRequest("The provided stop id is not a valid number."); - } - try { - var response = await _api.GetStopEstimates(requestedStopId); + var response = await _api.GetStopEstimates(stopId); // Return only the estimates array, not the stop metadata return new OkObjectResult(response.Estimates); } @@ -48,56 +42,30 @@ public class VigoController : ControllerBase } [HttpGet("GetStopTimetable")] - public async Task GetStopTimetable() + public async Task GetStopTimetable( + [FromQuery] int stopId, + [FromQuery] string date + ) { - // Get date parameter (default to today if not provided) - var dateString = Request.Query.TryGetValue("date", out var requestedDate) - ? requestedDate.ToString() - : DateTime.Today.ToString("yyyy-MM-dd"); - // Validate date format - if (!DateTime.TryParseExact(dateString, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.None, out _)) + if (!DateTime.TryParseExact(date, "yyyy-MM-dd", null, DateTimeStyles.None, out _)) { return BadRequest("Invalid date format. Please use yyyy-MM-dd format."); } - // Get stopId parameter - if (!Request.Query.TryGetValue("stopId", out var requestedStopIdString)) - { - return BadRequest("Please provide a stop id as a query parameter with the name 'stopId'."); - } - - if (!int.TryParse(requestedStopIdString, out var requestedStopId)) - { - return BadRequest("The provided stop id is not a valid number."); - } - // Create cache key - var cacheKey = $"timetable_{dateString}_{requestedStopId}"; + var cacheKey = $"timetable_{date}_{stopId}"; // Try to get from cache first if (_cache.TryGetValue(cacheKey, out var cachedData)) { + Response.Headers.Append("App-CacheUsage", "HIT"); return new OkObjectResult(cachedData); } try { - // Fetch data from external API - var url = $"https://costas.dev/static-storage/vitrasa_svc/stops/{dateString}/{requestedStopId}.json"; - var response = await _httpClient.GetAsync(url); - - if (!response.IsSuccessStatusCode) - { - if (response.StatusCode == System.Net.HttpStatusCode.NotFound) - { - return NotFound($"Timetable data not found for stop {requestedStopId} on {dateString}"); - } - return StatusCode((int)response.StatusCode, "Error fetching timetable data"); - } - - var jsonContent = await response.Content.ReadAsStringAsync(); - var timetableData = JsonSerializer.Deserialize(jsonContent); + var timetableData = await LoadTimetable(stopId.ToString(), date); // Cache the data for 12 hours var cacheOptions = new MemoryCacheEntryOptions @@ -109,11 +77,12 @@ public class VigoController : ControllerBase _cache.Set(cacheKey, timetableData, cacheOptions); + Response.Headers.Append("App-CacheUsage", "MISS"); return new OkObjectResult(timetableData); } catch (HttpRequestException ex) { - return StatusCode(500, $"Error fetching timetable data: {ex.Message}"); + return StatusCode((int?)ex.StatusCode ?? 500, $"Error fetching timetable data: {ex.Message}"); } catch (JsonException ex) { @@ -124,5 +93,117 @@ public class VigoController : ControllerBase return StatusCode(500, $"Unexpected error: {ex.Message}"); } } -} + /*private StopEstimate[] LoadDebugEstimates() + { + var file = @"C:\Users\ariel\Desktop\GetStopEstimates.json"; + var contents = System.IO.File.ReadAllText(file); + return JsonSerializer.Deserialize(contents, JsonSerializerOptions.Web)!; + } + + private ScheduledStop[] LoadDebugTimetable() + { + var file = @"C:\Users\ariel\Desktop\GetStopTimetable.json"; + var contents = System.IO.File.ReadAllText(file); + return JsonSerializer.Deserialize(contents)!; + }*/ + + [HttpGet("GetStopArrivalsMerged")] + public async Task GetStopArrivalsMerged( + [FromQuery] int stopId + ) + { + 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")); + + Task.WaitAll(realtimeTask, timetableTask); + + var realTimeEstimates = realtimeTask.Result.Estimates; + var timetable = timetableTask.Result; + + /*var now = DateTime.Today.AddHours(17).AddMinutes(59); + var realTimeEstimates = LoadDebugEstimates(); + var timetable = LoadDebugTimetable();*/ + + 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)}"); + var fullArrivalTime = now.AddMinutes(estimate.Minutes); + + var possibleCirculations = timetable + .Where(c => c.Line.Name.Trim() == estimate.Line.Trim() && c.Trip.Headsign.Trim() == estimate.Route.Trim()) + .OrderBy(c => c.DepartureDateTime()) + .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.DepartureDateTime()).TotalMinutes); + var diffBetweenNowAndSchedule = (int)(fullArrivalTime - now).TotalMinutes; + + var tolerance = Math.Max(2, diffBetweenNowAndSchedule * 0.15); // Positive amount of minutes + if (diffBetweenScheduleAndTrip <= -tolerance) + { + break; + } + + if (diffBetweenScheduleAndTrip < closestCirculationTime) + { + closestCirculation = circulation; + closestCirculationTime = diffBetweenScheduleAndTrip; + } + + } + + if (closestCirculation == null) + { + 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.Trip.Id} stopping at {circulation.DepartureDateTime()} (diff: {fullArrivalTime - circulation.DepartureDateTime():HH:mm})"); + } + outputBuffer.AppendLine(); + + continue; + } + + if (closestCirculationTime > 0) + { + outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving {closestCirculationTime} minutes LATE"); + } else if (closestCirculationTime == 0) + { + outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving ON TIME"); + } + else + { + outputBuffer.Append($"Closest circulation is {closestCirculation.Trip.Id} and arriving {Math.Abs(closestCirculationTime)} minutes EARLY"); + } + + outputBuffer.AppendLine( + $" -- Circulation expected at {closestCirculation.DepartureDateTime():HH:mm)}"); + + outputBuffer.AppendLine(); + } + + return Ok(outputBuffer.ToString()); + } + + private async Task> LoadTimetable(string stopId, string dateString) + { + var url = $"https://www.costas.dev/static-storage/vitrasa_svc/stops/{dateString}/{stopId}.json"; + var response = await _httpClient.GetAsync(url); + + response.EnsureSuccessStatusCode(); + + var jsonContent = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize>(jsonContent) ?? []; + } +} diff --git a/src/frontend/app/components/TimetableTable.css b/src/frontend/app/components/TimetableTable.css index 52bd9ae..8980fb4 100644 --- a/src/frontend/app/components/TimetableTable.css +++ b/src/frontend/app/components/TimetableTable.css @@ -18,15 +18,15 @@ } .timetable-card { - background: var(--surface-future, #fff); + background-color: var(--surface-future, #fff); border: 1px solid var(--card-border, #e0e0e0); border-radius: 10px; padding: 1.25rem; - transition: background 0.2s ease, border 0.2s ease; + transition: background-color 0.2s ease, border 0.2s ease; } .timetable-card.timetable-past { - background: var(--surface-past, #f3f3f3); + background-color: var(--surface-past, #f3f3f3); color: var(--text-secondary, #aaa); border: 1px solid #e0e0e0; } diff --git a/src/frontend/package-lock.json b/src/frontend/package-lock.json index 7de81f1..e8bdee3 100644 --- a/src/frontend/package-lock.json +++ b/src/frontend/package-lock.json @@ -9,6 +9,8 @@ "version": "0.0.0", "dependencies": { "@fontsource-variable/roboto": "^5.2.8", + "@react-router/node": "^7.9.4", + "@react-router/serve": "^7.9.4", "framer-motion": "^12.23.24", "fuse.js": "^7.1.0", "i18next-browser-languagedetector": "^8.2.0", @@ -3384,6 +3386,7 @@ "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", -- cgit v1.3