aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend/Controllers
diff options
context:
space:
mode:
Diffstat (limited to 'src/Costasdev.Busurbano.Backend/Controllers')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs249
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs102
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TileController.cs217
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs110
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs127
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs147
6 files changed, 0 insertions, 952 deletions
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs b/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
deleted file mode 100644
index a0e8505..0000000
--- a/src/Costasdev.Busurbano.Backend/Controllers/ArrivalsController.cs
+++ /dev/null
@@ -1,249 +0,0 @@
-using System.Net;
-using Costasdev.Busurbano.Backend.Configuration;
-using Costasdev.Busurbano.Backend.Helpers;
-using Costasdev.Busurbano.Backend.Services;
-using Costasdev.Busurbano.Backend.Types;
-using Costasdev.Busurbano.Backend.Types.Arrivals;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Options;
-
-namespace Costasdev.Busurbano.Backend.Controllers;
-
-[ApiController]
-[Route("api/stops")]
-public partial class ArrivalsController : ControllerBase
-{
- private readonly ILogger<ArrivalsController> _logger;
- private readonly IMemoryCache _cache;
- private readonly HttpClient _httpClient;
- private readonly ArrivalsPipeline _pipeline;
- private readonly FeedService _feedService;
- private readonly AppConfiguration _config;
-
- public ArrivalsController(
- ILogger<ArrivalsController> logger,
- IMemoryCache cache,
- HttpClient httpClient,
- ArrivalsPipeline pipeline,
- FeedService feedService,
- IOptions<AppConfiguration> configOptions
- )
- {
- _logger = logger;
- _cache = cache;
- _httpClient = httpClient;
- _pipeline = pipeline;
- _feedService = feedService;
- _config = configOptions.Value;
- }
-
- [HttpGet("arrivals")]
- public async Task<IActionResult> GetArrivals(
- [FromQuery] string id,
- [FromQuery] bool reduced
- )
- {
- var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid");
- var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz);
- var todayLocal = nowLocal.Date;
-
- var requestContent = ArrivalsAtStopContent.Query(
- new ArrivalsAtStopContent.Args(id, reduced)
- );
-
- var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
- request.Content = JsonContent.Create(new GraphClientRequest
- {
- Query = requestContent
- });
-
- var response = await _httpClient.SendAsync(request);
- var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<ArrivalsAtStopResponse>>();
-
- if (responseBody is not { IsSuccess: true } || responseBody.Data?.Stop == null)
- {
- LogErrorFetchingStopData(response.StatusCode, await response.Content.ReadAsStringAsync());
- return StatusCode(500, "Error fetching stop data");
- }
-
- var stop = responseBody.Data.Stop;
- _logger.LogInformation("Fetched {Count} arrivals for stop {StopName} ({StopId})", stop.Arrivals.Count, stop.Name, id);
-
- List<Arrival> arrivals = [];
- foreach (var item in stop.Arrivals)
- {
- // Discard trip without pickup at stop
- if (item.PickupTypeParsed.Equals(ArrivalsAtStopResponse.PickupType.None))
- {
- continue;
- }
-
- // Discard on last stop
- if (item.Trip.ArrivalStoptime.Stop.GtfsId == id)
- {
- continue;
- }
-
- if (item.Trip.Geometry?.Points != null)
- {
- _logger.LogDebug("Trip {TripId} has geometry", item.Trip.GtfsId);
- }
-
- // Calculate departure time using the service day in the feed's timezone (Europe/Madrid)
- // This ensures we treat ScheduledDepartureSeconds as relative to the local midnight of the service day
- var serviceDayLocal = TimeZoneInfo.ConvertTime(DateTimeOffset.FromUnixTimeSeconds(item.ServiceDay), tz);
- var departureTime = serviceDayLocal.Date.AddSeconds(item.ScheduledDepartureSeconds);
-
- var minutesToArrive = (int)(departureTime - nowLocal).TotalMinutes;
- //var isRunning = departureTime < nowLocal;
-
- Arrival arrival = new()
- {
- 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"
- },
- Headsign = new HeadsignInfo
- {
- Destination = item.Headsign
- },
- Estimate = new ArrivalDetails
- {
- Minutes = minutesToArrive,
- Precision = departureTime < nowLocal.AddMinutes(-1) ? ArrivalPrecision.Past : ArrivalPrecision.Scheduled
- },
- RawOtpTrip = item
- };
-
- arrivals.Add(arrival);
- }
-
- await _pipeline.ExecuteAsync(new ArrivalsContext
- {
- StopId = id,
- StopCode = stop.Code,
- IsReduced = reduced,
- Arrivals = arrivals,
- NowLocal = nowLocal,
- StopLocation = new Position { Latitude = stop.Lat, Longitude = stop.Lon }
- });
-
- var feedId = id.Split(':')[0];
-
- // Time after an arrival's time to still include it in the response. This is useful without real-time data, for delayed buses.
- var timeThreshold = GetThresholdForFeed(id);
-
- var (fallbackColor, fallbackTextColor) = _feedService.GetFallbackColourForFeed(feedId);
-
- return Ok(new StopArrivalsResponse
- {
- StopCode = _feedService.NormalizeStopCode(feedId, stop.Code),
- StopName = _feedService.NormalizeStopName(feedId, stop.Name),
- StopLocation = new Position
- {
- Latitude = stop.Lat,
- Longitude = stop.Lon
- },
- Routes = [.. stop.Routes
- .OrderBy(
- r => r.ShortName,
- Comparer<string?>.Create(SortingHelper.SortRouteShortNames)
- )
- .Select(r => new RouteInfo
- {
- GtfsId = r.GtfsId,
- ShortName = _feedService.NormalizeRouteShortName(feedId, r.ShortName ?? ""),
- Colour = r.Color ?? fallbackColor,
- TextColour = r.TextColor is null or "000000" ?
- ContrastHelper.GetBestTextColour(r.Color ?? fallbackColor) :
- r.TextColor
- })],
- Arrivals = [.. arrivals.Where(a => a.Estimate.Minutes >= timeThreshold)]
- });
-
- }
-
- private static int GetThresholdForFeed(string id)
- {
- string feedId = id.Split(':', 2)[0];
-
- if (feedId is "vitrasa" or "coruna")
- {
- return 0;
- }
-
- return -30;
- }
-
- [HttpGet]
- public async Task<IActionResult> GetStops([FromQuery] string ids)
- {
- if (string.IsNullOrWhiteSpace(ids))
- {
- return BadRequest("Ids parameter is required");
- }
-
- var stopIds = ids.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
- var requestContent = StopsInfoContent.Query(new StopsInfoContent.Args(stopIds));
-
- var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
- request.Content = JsonContent.Create(new GraphClientRequest
- {
- Query = requestContent
- });
-
- var response = await _httpClient.SendAsync(request);
- var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopsInfoResponse>>();
-
- if (responseBody is not { IsSuccess: true } || responseBody.Data?.Stops == null)
- {
- return StatusCode(500, "Error fetching stops data");
- }
-
- var result = responseBody.Data.Stops.ToDictionary(
- s => s.GtfsId,
- s =>
- {
- var feedId = s.GtfsId.Split(':', 2)[0];
- var (fallbackColor, _) = _feedService.GetFallbackColourForFeed(feedId);
-
- return new
- {
- id = s.GtfsId,
- code = _feedService.NormalizeStopCode(feedId, s.Code ?? ""),
- name = s.Name,
- routes = s.Routes
- .OrderBy(r => r.ShortName, Comparer<string?>.Create(SortingHelper.SortRouteShortNames))
- .Select(r => new
- {
- shortName = _feedService.NormalizeRouteShortName(feedId, r.ShortName ?? ""),
- colour = r.Color ?? fallbackColor,
- textColour = r.TextColor is null or "000000" ?
- ContrastHelper.GetBestTextColour(r.Color ?? fallbackColor) :
- r.TextColor
- })
- .ToList()
- };
- }
- );
-
- return Ok(result);
- }
-
- [HttpGet("search")]
- public IActionResult SearchStops([FromQuery] string q)
- {
- // Placeholder for future implementation with Postgres and fuzzy searching
- return Ok(new List<object>());
- }
-
- [LoggerMessage(LogLevel.Error, "Error fetching stop data, received {statusCode} {responseBody}")]
- partial void LogErrorFetchingStopData(HttpStatusCode statusCode, string responseBody);
-}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs b/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs
deleted file mode 100644
index a7faf44..0000000
--- a/src/Costasdev.Busurbano.Backend/Controllers/RoutePlannerController.cs
+++ /dev/null
@@ -1,102 +0,0 @@
-using System.Net;
-using Costasdev.Busurbano.Backend.Configuration;
-using Costasdev.Busurbano.Backend.Services;
-using Costasdev.Busurbano.Backend.Types.Planner;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
-
-namespace Costasdev.Busurbano.Backend.Controllers;
-
-[ApiController]
-[Route("api/planner")]
-public partial class RoutePlannerController : ControllerBase
-{
- private readonly ILogger<RoutePlannerController> _logger;
- private readonly OtpService _otpService;
- private readonly IGeocodingService _geocodingService;
- private readonly AppConfiguration _config;
- private readonly HttpClient _httpClient;
-
- public RoutePlannerController(
- ILogger<RoutePlannerController> logger,
- OtpService otpService,
- IGeocodingService geocodingService,
- IOptions<AppConfiguration> config,
- HttpClient httpClient
- )
- {
- _logger = logger;
- _otpService = otpService;
- _geocodingService = geocodingService;
- _config = config.Value;
- _httpClient = httpClient;
- }
-
- [HttpGet("autocomplete")]
- public async Task<ActionResult<List<PlannerSearchResult>>> Autocomplete([FromQuery] string query)
- {
- if (string.IsNullOrWhiteSpace(query))
- {
- return BadRequest("Query cannot be empty");
- }
-
- var results = await _geocodingService.GetAutocompleteAsync(query);
- return Ok(results);
- }
-
- [HttpGet("reverse")]
- public async Task<ActionResult<PlannerSearchResult>> Reverse([FromQuery] double lat, [FromQuery] double lon)
- {
- var result = await _geocodingService.GetReverseGeocodeAsync(lat, lon);
- if (result == null)
- {
- return NotFound();
- }
- return Ok(result);
- }
-
- [HttpGet("plan")]
- public async Task<ActionResult<RoutePlan>> Plan(
- [FromQuery] double fromLat,
- [FromQuery] double fromLon,
- [FromQuery] double toLat,
- [FromQuery] double toLon,
- [FromQuery] DateTimeOffset? time,
- [FromQuery] bool arriveBy = false)
- {
- try
- {
- var requestContent = PlanConnectionContent.Query(
- new PlanConnectionContent.Args(fromLat, fromLon, toLat, toLon, time ?? DateTimeOffset.Now, arriveBy)
- );
-
- var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
- request.Content = JsonContent.Create(new GraphClientRequest
- {
- Query = requestContent
- });
-
- var response = await _httpClient.SendAsync(request);
- var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<PlanConnectionResponse>>();
-
- if (responseBody is not { IsSuccess: true })
- {
- LogErrorFetchingRoutes(response.StatusCode, await response.Content.ReadAsStringAsync());
- return StatusCode(500, "An error occurred while planning the route.");
- }
-
- var plan = _otpService.MapPlanResponse(responseBody.Data!);
- return Ok(plan);
- }
- catch (Exception e)
- {
- _logger.LogError("Exception planning route: {e}", e);
- return StatusCode(500, "An error occurred while planning the route.");
- }
- }
-
- [LoggerMessage(LogLevel.Error, "Error fetching route planning, received {statusCode} {responseBody}")]
- partial void LogErrorFetchingRoutes(HttpStatusCode? statusCode, string responseBody);
-}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
deleted file mode 100644
index 0e550a3..0000000
--- a/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
+++ /dev/null
@@ -1,217 +0,0 @@
-using NetTopologySuite.Features;
-using NetTopologySuite.IO.VectorTiles;
-using NetTopologySuite.IO.VectorTiles.Mapbox;
-
-using Microsoft.AspNetCore.Mvc;
-
-using Microsoft.Extensions.Caching.Memory;
-using System.Text.Json;
-using Costasdev.Busurbano.Backend.Helpers;
-using Costasdev.Busurbano.Backend.Services;
-using Costasdev.Busurbano.Backend.Configuration;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries;
-using Microsoft.Extensions.Options;
-
-namespace Costasdev.Busurbano.Backend.Controllers;
-
-[ApiController]
-[Route("api/tiles")]
-public class TileController : ControllerBase
-{
- private readonly ILogger<TileController> _logger;
- private readonly IMemoryCache _cache;
- private readonly HttpClient _httpClient;
- private readonly FeedService _feedService;
- private readonly AppConfiguration _config;
-
- public TileController(
- ILogger<TileController> logger,
- IMemoryCache cache,
- HttpClient httpClient,
- FeedService feedService,
- IOptions<AppConfiguration> configOptions
- )
- {
- _logger = logger;
- _cache = cache;
- _httpClient = httpClient;
- _feedService = feedService;
- _config = configOptions.Value;
- }
-
- [HttpGet("stops/{z:int}/{x:int}/{y:int}")]
- public async Task<IActionResult> Stops(int z, int x, int y)
- {
- if (z is < 9 or > 20)
- {
- return BadRequest("Zoom level out of range (9-20)");
- }
-
- var cacheHit = _cache.TryGetValue($"stops-tile-{z}-{x}-{y}", out byte[]? cachedTile);
- if (cacheHit && cachedTile != null)
- {
- Response.Headers.Append("X-Cache-Hit", "true");
- return File(cachedTile, "application/x-protobuf");
- }
-
- // Calculate bounding box in EPSG:4326
- var n = Math.Pow(2, z);
- var lonMin = x / n * 360.0 - 180.0;
- var lonMax = (x + 1) / n * 360.0 - 180.0;
-
- var latMaxRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * y / n)));
- var latMax = latMaxRad * 180.0 / Math.PI;
-
- var latMinRad = Math.Atan(Math.Sinh(Math.PI * (1 - 2 * (y + 1) / n)));
- var latMin = latMinRad * 180.0 / Math.PI;
-
- var requestContent = StopTileRequestContent.Query(new StopTileRequestContent.Bbox(lonMin, latMin, lonMax, latMax));
- var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
- request.Content = JsonContent.Create(new GraphClientRequest
- {
- Query = requestContent
- });
-
- var response = await _httpClient.SendAsync(request);
- var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopTileResponse>>();
-
- if (responseBody is not { IsSuccess: true })
- {
- _logger.LogError(
- "Error fetching stop data, received {StatusCode} {ResponseBody}",
- response.StatusCode,
- await response.Content.ReadAsStringAsync()
- );
- return StatusCode(500, "Error fetching stop data");
- }
-
- var tileDef = new NetTopologySuite.IO.VectorTiles.Tiles.Tile(x, y, z);
- VectorTile vt = new() { TileId = tileDef.Id };
- var stopsLayer = new Layer { Name = "stops" };
-
- responseBody.Data?.StopsByBbox?.ForEach(stop =>
- {
- var idParts = stop.GtfsId.Split(':', 2);
- string feedId = idParts[0];
- string codeWithinFeed = _feedService.NormalizeStopCode(feedId, stop.Code ?? string.Empty);
-
- if (_feedService.IsStopHidden($"{feedId}:{codeWithinFeed}"))
- {
- return;
- }
-
- // TODO: Duplicate from ArrivalsController
- var (Color, TextColor) = _feedService.GetFallbackColourForFeed(idParts[0]);
- var distinctRoutes = GetDistinctRoutes(feedId, stop.Routes ?? []);
-
- Feature feature = new()
- {
- Geometry = new NetTopologySuite.Geometries.Point(stop.Lon, stop.Lat),
- Attributes = new AttributesTable
- {
- // The ID will be used to request the arrivals
- { "id", stop.GtfsId },
- // The feed is the first part of the GTFS ID
- { "feed", idParts[0] },
- // The public identifier, usually feed:code or feed:id, recognisable by users and in other systems
- { "code", $"{idParts[0]}:{codeWithinFeed}" },
- { "name", _feedService.NormalizeStopName(feedId, stop.Name) },
- { "icon", GetIconNameForFeed(feedId) },
- { "transitKind", GetTransitKind(feedId) },
- // Routes
- { "routes", JsonSerializer
- .Serialize(
- distinctRoutes.Select(r => {
- var colour = r.Color ?? Color;
- string textColour;
-
- if (r.Color is null) // None is present, use fallback
- {
- textColour = TextColor;
- }
- else if (r.TextColor is null || r.TextColor.EndsWith("000000"))
- {
- // Text colour not provided, or default-black; check the better contrasting
- textColour = ContrastHelper.GetBestTextColour(colour);
- }
- else
- {
- // Use provided text colour
- textColour = r.TextColor;
- }
-
- return new {
- shortName = r.ShortName,
- colour,
- textColour
- };
- }))
- }
- }
- };
-
- stopsLayer.Features.Add(feature);
- });
-
- vt.Layers.Add(stopsLayer);
-
- using var ms = new MemoryStream();
- vt.Write(ms, minLinealExtent: 1, minPolygonalExtent: 2);
-
- _cache.Set($"stops-tile-{z}-{x}-{y}", ms.ToArray(), TimeSpan.FromMinutes(15));
- Response.Headers.Append("X-Cache-Hit", "false");
-
- return File(ms.ToArray(), "application/x-protobuf");
- }
-
- private string GetIconNameForFeed(string feedId)
- {
- return feedId switch
- {
- "vitrasa" => "stop-vitrasa",
- "santiago" => "stop-santiago",
- "coruna" => "stop-coruna",
- "xunta" => "stop-xunta",
- "renfe" => "stop-renfe",
- "feve" => "stop-feve",
- _ => "stop-generic",
- };
- }
-
- private string GetTransitKind(string feedId)
- {
- return feedId switch
- {
- "vitrasa" or "santiago" or "coruna" => "bus",
- "xunta" => "coach",
- "renfe" or "feve" => "train",
- _ => "unknown",
- };
- }
-
- private List<StopTileResponse.Route> GetDistinctRoutes(string feedId, List<StopTileResponse.Route> routes)
- {
- List<StopTileResponse.Route> distinctRoutes = [];
- HashSet<string> seen = new();
-
- foreach (var route in routes)
- {
- var seenId = _feedService.GetUniqueRouteShortName(feedId, route.ShortName ?? string.Empty);
- route.ShortName = seenId;
-
- if (seen.Contains(seenId))
- {
- continue;
- }
-
- seen.Add(seenId);
- distinctRoutes.Add(route);
- }
-
- return [.. distinctRoutes.OrderBy(
- r => r.ShortName,
- Comparer<string?>.Create(SortingHelper.SortRouteShortNames)
- )];
- }
-}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs
deleted file mode 100644
index 0542054..0000000
--- a/src/Costasdev.Busurbano.Backend/Controllers/TrafficDataController.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System.Globalization;
-using System.Text;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Caching.Memory;
-using NetTopologySuite.Features;
-using NetTopologySuite.IO;
-
-namespace Costasdev.Busurbano.Backend.Controllers;
-
-[ApiController]
-[Route("api")]
-public class TrafficDataController : ControllerBase
-{
- private readonly ILogger<TrafficDataController> _logger;
- private readonly IMemoryCache _cache;
- private readonly HttpClient _httpClient;
-
- public TrafficDataController(
- ILogger<TrafficDataController> logger,
- IMemoryCache cache,
- HttpClient httpClient
- )
- {
- _logger = logger;
- _cache = cache;
- _httpClient = httpClient;
- }
-
- [HttpGet("traffic")]
- public async Task<IActionResult> Get()
- {
- var trafficData = _cache.GetOrCreate("vigo-traffic-geojson", entry =>
- {
- var data = GetTrafficData();
-
- entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
-
- return data.Result;
- });
-
- if (string.IsNullOrEmpty(trafficData))
- {
- return StatusCode(404);
- }
-
- return Content(trafficData, "application/json", Encoding.UTF8);
- }
-
- private async Task<string> GetTrafficData()
- {
- var resp = await _httpClient.GetAsync("https://datos.vigo.org/data/trafico/treal.geojson");
- var body = resp.Content.ReadAsStringAsync().Result;
-
- var reader = new GeoJsonReader();
- var featureCollection = reader.Read<FeatureCollection>(body);
-
- var filteredFeatures = new FeatureCollection();
- foreach (var kvp in featureCollection)
- {
- var newAttributes = new AttributesTable();
-
- if (
- !kvp.Attributes.Exists("actualizacion") ||
- !kvp.Attributes.Exists("style")
- )
- {
- continue;
- }
-
- var updateParsed = DateTime.TryParseExact(
- kvp.Attributes["actualizacion"].ToString(),
- "dd/MM/yyyy H:mm:ss",
- null,
- DateTimeStyles.None,
- out var updatedAt
- );
-
- if (!updateParsed || updatedAt < DateTime.Today)
- {
- continue;
- }
-
- var style = kvp.Attributes["style"].ToString();
-
- if (style == "#SINDATOS")
- {
- continue;
- }
-
- var vehiculosAttribute = (kvp.Attributes["vehiculos"] ?? "0").ToString();
-
- var vehiclesParsed = int.TryParse(vehiculosAttribute, out var vehicles);
- if (!vehiclesParsed || vehicles <= 0)
- {
- continue;
- }
-
- newAttributes.Add("updatedAt", updatedAt.ToString("O"));
- newAttributes.Add("style", style);
- newAttributes.Add("vehicles", vehicles);
-
- kvp.Attributes = newAttributes;
- filteredFeatures.Add(kvp);
- }
-
- var writer = new GeoJsonWriter();
-
- return writer.Write(filteredFeatures);
- }
-}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs b/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs
deleted file mode 100644
index b519ea7..0000000
--- a/src/Costasdev.Busurbano.Backend/Controllers/TransitController.cs
+++ /dev/null
@@ -1,127 +0,0 @@
-using Costasdev.Busurbano.Backend.Configuration;
-using Costasdev.Busurbano.Backend.Helpers;
-using Costasdev.Busurbano.Backend.Services;
-using Costasdev.Busurbano.Backend.Types.Transit;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql;
-using Costasdev.Busurbano.Sources.OpenTripPlannerGql.Queries;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Options;
-
-namespace Costasdev.Busurbano.Backend.Controllers;
-
-[ApiController]
-[Route("api/transit")]
-public class TransitController : ControllerBase
-{
- private readonly ILogger<TransitController> _logger;
- private readonly OtpService _otpService;
- private readonly AppConfiguration _config;
- private readonly HttpClient _httpClient;
- private readonly IMemoryCache _cache;
-
- public TransitController(
- ILogger<TransitController> logger,
- OtpService otpService,
- IOptions<AppConfiguration> config,
- HttpClient httpClient,
- IMemoryCache cache
- )
- {
- _logger = logger;
- _otpService = otpService;
- _config = config.Value;
- _httpClient = httpClient;
- _cache = cache;
- }
-
- [HttpGet("routes")]
- public async Task<ActionResult<List<RouteDto>>> GetRoutes([FromQuery] string[] feeds)
- {
- if (feeds.Length == 0)
- {
- feeds = ["santiago", "vitrasa", "coruna", "feve"];
- }
-
- var serviceDate = DateTime.Now.ToString("yyyy-MM-dd");
- var cacheKey = $"routes_{string.Join("_", feeds)}_{serviceDate}";
- if (_cache.TryGetValue(cacheKey, out List<RouteDto>? cachedRoutes))
- {
- return Ok(cachedRoutes);
- }
-
- try
- {
- var query = RoutesListContent.Query(new RoutesListContent.Args(feeds, serviceDate));
- var response = await SendOtpQueryAsync<RoutesListResponse>(query);
-
- if (response?.Data == null)
- {
- return StatusCode(500, "Failed to fetch routes from OTP.");
- }
-
- var routes = response.Data.Routes
- .Select(_otpService.MapRoute)
- .Where(r => r.TripCount > 0)
- .OrderBy(r => r.ShortName, Comparer<string?>.Create(SortingHelper.SortRouteShortNames))
- .ToList();
-
- _cache.Set(cacheKey, routes, TimeSpan.FromHours(1));
-
- return Ok(routes);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error fetching routes");
- return StatusCode(500, "An error occurred while fetching routes.");
- }
- }
-
- [HttpGet("routes/{id}")]
- public async Task<ActionResult<RouteDetailsDto>> GetRouteDetails(string id)
- {
- var serviceDate = DateTime.Now.ToString("yyyy-MM-dd");
- var cacheKey = $"route_details_{id}_{serviceDate}";
-
- if (_cache.TryGetValue(cacheKey, out RouteDetailsDto? cachedDetails))
- {
- return Ok(cachedDetails);
- }
-
- try
- {
- var query = RouteDetailsContent.Query(new RouteDetailsContent.Args(id, serviceDate));
- var response = await SendOtpQueryAsync<RouteDetailsResponse>(query);
-
- if (response?.Data?.Route == null)
- {
- return NotFound();
- }
-
- var details = _otpService.MapRouteDetails(response.Data.Route);
- _cache.Set(cacheKey, details, TimeSpan.FromHours(1));
-
- return Ok(details);
- }
- catch (Exception e)
- {
- _logger.LogError(e, "Error fetching route details for {Id}", id);
- return StatusCode(500, "An error occurred while fetching route details.");
- }
- }
-
- private async Task<GraphClientResponse<T>?> SendOtpQueryAsync<T>(string query) where T : AbstractGraphResponse
- {
- var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
- request.Content = JsonContent.Create(new GraphClientRequest { Query = query });
-
- var response = await _httpClient.SendAsync(request);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError("OTP query failed with status {StatusCode}", response.StatusCode);
- return null;
- }
-
- return await response.Content.ReadFromJsonAsync<GraphClientResponse<T>>();
- }
-}
diff --git a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs b/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
deleted file mode 100644
index 642cccb..0000000
--- a/src/Costasdev.Busurbano.Backend/Controllers/VigoController.cs
+++ /dev/null
@@ -1,147 +0,0 @@
-using Costasdev.Busurbano.Backend.Configuration;
-using Costasdev.Busurbano.Backend.Services;
-using Costasdev.Busurbano.Backend.Services.Providers;
-using Costasdev.VigoTransitApi;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
-
-namespace Costasdev.Busurbano.Backend.Controllers;
-
-[ApiController]
-[Route("api/vigo")]
-public partial class VigoController : ControllerBase
-{
- private readonly ILogger<VigoController> _logger;
- private readonly VigoTransitApiClient _api;
- private readonly AppConfiguration _configuration;
- private readonly ShapeTraversalService _shapeService;
- private readonly VitrasaTransitProvider _vitrasaProvider;
- private readonly RenfeTransitProvider _renfeProvider;
-
- public VigoController(
- HttpClient http,
- IOptions<AppConfiguration> options,
- ILogger<VigoController> logger,
- ShapeTraversalService shapeService,
- VitrasaTransitProvider vitrasaProvider,
- RenfeTransitProvider renfeProvider)
- {
- _logger = logger;
- _api = new VigoTransitApiClient(http);
- _configuration = options.Value;
- _shapeService = shapeService;
- _vitrasaProvider = vitrasaProvider;
- _renfeProvider = renfeProvider;
- }
-
- [HttpGet("GetShape")]
- public async Task<IActionResult> GetShape(
- [FromQuery] string shapeId,
- [FromQuery] int? startPointIndex = null,
- [FromQuery] double? busLat = null,
- [FromQuery] double? busLon = null,
- [FromQuery] int? busShapeIndex = null,
- [FromQuery] double? stopLat = null,
- [FromQuery] double? stopLon = null,
- [FromQuery] int? stopShapeIndex = null
- )
- {
- var path = await _shapeService.GetShapePathAsync(shapeId, 0);
- if (path == null)
- {
- return NotFound();
- }
-
- // Determine bus point
- object? busPoint = null;
- if (busShapeIndex.HasValue && busShapeIndex.Value >= 0 && busShapeIndex.Value < path.Count)
- {
- var p = path[busShapeIndex.Value];
- busPoint = new { lat = p.Latitude, lon = p.Longitude, index = busShapeIndex.Value };
- }
- else if (busLat.HasValue && busLon.HasValue)
- {
- var idx = await _shapeService.FindClosestPointIndexAsync(shapeId, busLat.Value, busLon.Value);
- if (idx.HasValue && idx.Value >= 0 && idx.Value < path.Count)
- {
- var p = path[idx.Value];
- busPoint = new { lat = p.Latitude, lon = p.Longitude, index = idx.Value };
- }
- }
- else if (startPointIndex.HasValue && startPointIndex.Value >= 0 && startPointIndex.Value < path.Count)
- {
- var p = path[startPointIndex.Value];
- busPoint = new { lat = p.Latitude, lon = p.Longitude, index = startPointIndex.Value };
- }
-
- // Determine stop point
- object? stopPoint = null;
- if (stopShapeIndex.HasValue && stopShapeIndex.Value >= 0 && stopShapeIndex.Value < path.Count)
- {
- var p = path[stopShapeIndex.Value];
- stopPoint = new { lat = p.Latitude, lon = p.Longitude, index = stopShapeIndex.Value };
- }
- else if (stopLat.HasValue && stopLon.HasValue)
- {
- var idx = await _shapeService.FindClosestPointIndexAsync(shapeId, stopLat.Value, stopLon.Value);
- if (idx.HasValue && idx.Value >= 0 && idx.Value < path.Count)
- {
- var p = path[idx.Value];
- stopPoint = new { lat = p.Latitude, lon = p.Longitude, index = idx.Value };
- }
- }
-
- // Convert to GeoJSON LineString
- var coordinates = path.Select(p => new[] { p.Longitude, p.Latitude }).ToList();
-
- var geoJson = new
- {
- type = "Feature",
- geometry = new
- {
- type = "LineString",
- coordinates = coordinates
- },
- properties = new
- {
- busPoint,
- stopPoint
- }
- };
-
- return Ok(geoJson);
- }
-
- [HttpGet("GetConsolidatedCirculations")]
- public async Task<IActionResult> GetConsolidatedCirculations(
- [FromQuery] string stopId
- )
- {
- // Use Europe/Madrid timezone consistently to avoid UTC/local skew
- var tz = TimeZoneInfo.FindSystemTimeZoneById("Europe/Madrid");
- var nowLocal = TimeZoneInfo.ConvertTime(DateTime.UtcNow, tz);
-
- ITransitProvider provider;
- string effectiveStopId;
-
- if (stopId.StartsWith("renfe:"))
- {
- provider = _renfeProvider;
- effectiveStopId = stopId.Substring("renfe:".Length);
- }
- else if (stopId.StartsWith("vitrasa:"))
- {
- provider = _vitrasaProvider;
- effectiveStopId = stopId.Substring("vitrasa:".Length);
- }
- else
- {
- // Legacy/Default
- provider = _vitrasaProvider;
- effectiveStopId = stopId;
- }
-
- var result = await provider.GetCirculationsAsync(effectiveStopId, nowLocal);
- return Ok(result);
- }
-}