aboutsummaryrefslogtreecommitdiff
path: root/src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-29 00:41:52 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-29 00:41:52 +0100
commita304c24b32c0327436bbd8c2853e60668e161b42 (patch)
tree08f65c05daca134cf4d2e4f779bd15d98fd66370 /src/Costasdev.Busurbano.Backend/Controllers/TileController.cs
parent120a3c6bddd0fb8d9fa05df4763596956554c025 (diff)
Rename a lot of stuff, add Santiago real time
Diffstat (limited to 'src/Costasdev.Busurbano.Backend/Controllers/TileController.cs')
-rw-r--r--src/Costasdev.Busurbano.Backend/Controllers/TileController.cs217
1 files changed, 0 insertions, 217 deletions
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)
- )];
- }
-}