aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend
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
parenta304c24b32c0327436bbd8c2853e60668e161b42 (diff)
Ñapa: include stops in geocoding search
Diffstat (limited to 'src/Enmarcha.Backend')
-rw-r--r--src/Enmarcha.Backend/Controllers/RoutePlannerController.cs93
-rw-r--r--src/Enmarcha.Backend/Controllers/TileController.cs2
-rw-r--r--src/Enmarcha.Backend/Enmarcha.Backend.csproj3
-rw-r--r--src/Enmarcha.Backend/Services/NominatimGeocodingService.cs2
4 files changed, 94 insertions, 6 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>();
+ }
+ }
}
diff --git a/src/Enmarcha.Backend/Controllers/TileController.cs b/src/Enmarcha.Backend/Controllers/TileController.cs
index 3459997..4065ecd 100644
--- a/src/Enmarcha.Backend/Controllers/TileController.cs
+++ b/src/Enmarcha.Backend/Controllers/TileController.cs
@@ -186,7 +186,7 @@ public class TileController : ControllerBase
"vitrasa" or "tussa" or "tranvias" => "bus",
"xunta" => "coach",
"renfe" or "feve" => "train",
- _ => "unknown",
+ _ => throw new ArgumentException("Feed ID not a known type", feedId)
};
}
diff --git a/src/Enmarcha.Backend/Enmarcha.Backend.csproj b/src/Enmarcha.Backend/Enmarcha.Backend.csproj
index 463d985..941286b 100644
--- a/src/Enmarcha.Backend/Enmarcha.Backend.csproj
+++ b/src/Enmarcha.Backend/Enmarcha.Backend.csproj
@@ -18,8 +18,9 @@
<PackageReference Include="NetTopologySuite" />
<PackageReference Include="NetTopologySuite.IO.GeoJSON" />
<PackageReference Include="NetTopologySuite.IO.VectorTiles.Mapbox" />
-
+
<PackageReference Include="CsvHelper" />
+ <PackageReference Include="FuzzySharp" />
</ItemGroup>
<ItemGroup>
diff --git a/src/Enmarcha.Backend/Services/NominatimGeocodingService.cs b/src/Enmarcha.Backend/Services/NominatimGeocodingService.cs
index 8c4b8a5..e8eccb5 100644
--- a/src/Enmarcha.Backend/Services/NominatimGeocodingService.cs
+++ b/src/Enmarcha.Backend/Services/NominatimGeocodingService.cs
@@ -26,7 +26,7 @@ public class NominatimGeocodingService : IGeocodingService
// Nominatim requires a User-Agent
if (!_httpClient.DefaultRequestHeaders.Contains("User-Agent"))
{
- _httpClient.DefaultRequestHeaders.Add("User-Agent", "Enmarcha/0.1 testing only, will replace soon. Written 2025-12-28 (https://enmarcha.app)");
+ _httpClient.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Compatible; Enmarcha/0.1; https://enmarcha.app)");
}
}