aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2025-12-29 01:19:55 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2025-12-29 01:20:09 +0100
commit1c164a81b928f4ca200220e0111d303cad80164c (patch)
treeb8283400006a68a191fc9240bce8ba2eda3195a9 /src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
parenta304c24b32c0327436bbd8c2853e60668e161b42 (diff)
Ñapa: include stops in geocoding search
Diffstat (limited to 'src/Enmarcha.Backend/Controllers/RoutePlannerController.cs')
-rw-r--r--src/Enmarcha.Backend/Controllers/RoutePlannerController.cs93
1 files changed, 90 insertions, 3 deletions
diff --git a/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs b/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
index c7201b0..7a03a24 100644
--- a/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
+++ b/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
@@ -4,7 +4,9 @@ using Enmarcha.Sources.OpenTripPlannerGql.Queries;
using Enmarcha.Backend.Configuration;
using Enmarcha.Backend.Services;
using Enmarcha.Backend.Types.Planner;
+using FuzzySharp;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Enmarcha.Backend.Controllers;
@@ -18,13 +20,19 @@ public partial class RoutePlannerController : ControllerBase
private readonly IGeocodingService _geocodingService;
private readonly AppConfiguration _config;
private readonly HttpClient _httpClient;
+ private readonly IMemoryCache _cache;
+ private readonly FeedService _feedService;
+
+ private const string GaliciaBounds = "-9.3,43.8,-6.7,41.7";
public RoutePlannerController(
ILogger<RoutePlannerController> logger,
OtpService otpService,
IGeocodingService geocodingService,
IOptions<AppConfiguration> config,
- HttpClient httpClient
+ HttpClient httpClient,
+ IMemoryCache cache,
+ FeedService feedService
)
{
_logger = logger;
@@ -32,6 +40,8 @@ public partial class RoutePlannerController : ControllerBase
_geocodingService = geocodingService;
_config = config.Value;
_httpClient = httpClient;
+ _cache = cache;
+ _feedService = feedService;
}
[HttpGet("autocomplete")]
@@ -42,8 +52,33 @@ public partial class RoutePlannerController : ControllerBase
return BadRequest("Query cannot be empty");
}
- var results = await _geocodingService.GetAutocompleteAsync(query);
- return Ok(results);
+ var nominatimTask = _geocodingService.GetAutocompleteAsync(query);
+ var stopsTask = GetCachedStopsAsync();
+
+ await Task.WhenAll(nominatimTask, stopsTask);
+
+ var nominatimResults = await nominatimTask;
+ var allStops = await stopsTask;
+
+ // Fuzzy search stops
+ var fuzzyResults = Process.ExtractSorted(
+ query,
+ allStops.Select(s => s.Name ?? string.Empty),
+ cutoff: 60
+ ).Take(5).Select(r => allStops[r.Index]).ToList();
+
+ // Merge results: stops first, then nominatim, deduplicating by coordinates (approx)
+ var finalResults = new List<PlannerSearchResult>(fuzzyResults);
+
+ foreach (var res in nominatimResults)
+ {
+ if (!finalResults.Any(f => Math.Abs(f.Lat - res.Lat) < 0.0001 && Math.Abs(f.Lon - res.Lon) < 0.0001))
+ {
+ finalResults.Add(res);
+ }
+ }
+
+ return Ok(finalResults);
}
[HttpGet("reverse")]
@@ -99,4 +134,56 @@ public partial class RoutePlannerController : ControllerBase
[LoggerMessage(LogLevel.Error, "Error fetching route planning, received {statusCode} {responseBody}")]
partial void LogErrorFetchingRoutes(HttpStatusCode? statusCode, string responseBody);
+
+ private async Task<List<PlannerSearchResult>> GetCachedStopsAsync()
+ {
+ const string cacheKey = "otp_all_stops";
+ if (_cache.TryGetValue(cacheKey, out List<PlannerSearchResult>? cachedStops) && cachedStops != null)
+ {
+ return cachedStops;
+ }
+
+ try
+ {
+ // Galicia bounds: minLon, minLat, maxLon, maxLat
+ var bbox = new StopTileRequestContent.Bbox(-9.3, 41.7, -6.7, 43.8);
+ var query = StopTileRequestContent.Query(bbox);
+
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{_config.OpenTripPlannerBaseUrl}/gtfs/v1");
+ request.Content = JsonContent.Create(new GraphClientRequest { Query = query });
+
+ var response = await _httpClient.SendAsync(request);
+ var responseBody = await response.Content.ReadFromJsonAsync<GraphClientResponse<StopTileResponse>>();
+
+ if (responseBody is not { IsSuccess: true } || responseBody.Data?.StopsByBbox == null)
+ {
+ _logger.LogError("Error fetching stops from OTP for caching");
+ return new List<PlannerSearchResult>();
+ }
+
+ var stops = responseBody.Data.StopsByBbox.Select(s =>
+ {
+ var feedId = s.GtfsId.Split(':')[0];
+ var name = _feedService.NormalizeStopName(feedId, s.Name);
+ var code = _feedService.NormalizeStopCode(feedId, s.Code ?? string.Empty);
+
+ return new PlannerSearchResult
+ {
+ Name = name,
+ Label = string.IsNullOrWhiteSpace(code) ? name : $"{name} ({code})",
+ Lat = s.Lat,
+ Lon = s.Lon,
+ Layer = "stop"
+ };
+ }).ToList();
+
+ _cache.Set(cacheKey, stops, TimeSpan.FromHours(18));
+ return stops;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Exception fetching stops from OTP for caching");
+ return new List<PlannerSearchResult>();
+ }
+ }
}