aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-04-16 22:08:45 +0200
committerAriel Costas Guerrero <ariel@costas.dev>2026-04-16 22:08:45 +0200
commit3ae8d5c7111191957a8035887f79bf49f485c805 (patch)
tree45d5798f6e6409b4d968bb4ecb093843c649ee03 /src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
parentfc6d4cbaf78f75a5ac234862ecbf86faeb78a338 (diff)
Fix sorting shenanigans, improve stop viewingHEADmain
Diffstat (limited to 'src/Enmarcha.Backend/Controllers/RoutePlannerController.cs')
-rw-r--r--src/Enmarcha.Backend/Controllers/RoutePlannerController.cs66
1 files changed, 57 insertions, 9 deletions
diff --git a/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs b/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
index 426170d..a533520 100644
--- a/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
+++ b/src/Enmarcha.Backend/Controllers/RoutePlannerController.cs
@@ -46,7 +46,10 @@ public partial class RoutePlannerController : ControllerBase
}
[HttpGet("autocomplete")]
- public async Task<ActionResult<List<PlannerSearchResult>>> Autocomplete([FromQuery] string query)
+ public async Task<ActionResult<List<PlannerSearchResult>>> Autocomplete(
+ [FromQuery] string query,
+ [FromQuery] double? lat = null,
+ [FromQuery] double? lon = null)
{
if (string.IsNullOrWhiteSpace(query))
{
@@ -83,13 +86,33 @@ public partial class RoutePlannerController : ControllerBase
}
}
+ // Sort by distance from the map viewport center when provided
+ if (lat.HasValue && lon.HasValue)
+ {
+ finalResults = [.. finalResults.OrderBy(r => HaversineKm(lat.Value, lon.Value, r.Lat, r.Lon))];
+ }
+
return Ok(finalResults);
}
+ private static double HaversineKm(double lat1, double lon1, double lat2, double lon2)
+ {
+ const double R = 6371.0;
+ var dLat = (lat2 - lat1) * Math.PI / 180.0;
+ var dLon = (lon2 - lon1) * Math.PI / 180.0;
+ var a = Math.Sin(dLat / 2) * Math.Sin(dLat / 2)
+ + Math.Cos(lat1 * Math.PI / 180.0) * Math.Cos(lat2 * Math.PI / 180.0)
+ * Math.Sin(dLon / 2) * Math.Sin(dLon / 2);
+ return R * 2 * Math.Atan2(Math.Sqrt(a), Math.Sqrt(1 - a));
+ }
+
private async Task<List<PlannerSearchResult>> SearchStops(string query)
{
var stops = await GetCachedStopsAsync();
+ // Normalize query for better matching: strip diacritics and punctuation
+ var normalizedQuery = _feedService.NormalizeRouteNameForMatching(query);
+
// 1. Exact or prefix matches by stop code
var codeMatches = stops
.Where(s => s.StopCode != null && s.StopCode.StartsWith(query, StringComparison.OrdinalIgnoreCase))
@@ -97,10 +120,21 @@ public partial class RoutePlannerController : ControllerBase
.Take(5)
.ToList();
- // 2. Fuzzy search stops by label (Name + Code)
- var fuzzyResults = Process.ExtractSorted(
- query,
- stops.Select(s => s.Label ?? string.Empty),
+ // 2. Fuzzy search stops by name only (preferential: higher cutoff, name is more meaningful)
+ var nameOnlyFuzzy = Process.ExtractSorted(
+ normalizedQuery,
+ stops.Select(s => _feedService.NormalizeRouteNameForMatching(s.Name ?? string.Empty)),
+ cutoff: 65
+ )
+ .OrderByDescending(r => r.Score)
+ .Take(6)
+ .Select(r => stops[r.Index])
+ .ToList();
+
+ // 3. Fuzzy search stops by label (Name + Code + Desc) as fallback
+ var labelFuzzy = Process.ExtractSorted(
+ normalizedQuery,
+ stops.Select(s => _feedService.NormalizeRouteNameForMatching(s.Label ?? string.Empty)),
cutoff: 60
)
.OrderByDescending(r => r.Score)
@@ -108,8 +142,8 @@ public partial class RoutePlannerController : ControllerBase
.Select(r => stops[r.Index])
.ToList();
- // Merge stops, prioritizing code matches
- var stopResults = codeMatches.Concat(fuzzyResults)
+ // Merge stops: code matches first, then name matches, then label matches
+ var stopResults = codeMatches.Concat(nameOnlyFuzzy).Concat(labelFuzzy)
.GroupBy(s => s.StopId)
.Select(g => g.First())
.Take(6)
@@ -182,11 +216,12 @@ public partial class RoutePlannerController : ControllerBase
var allStopsRaw = await _otpService.GetStopsByBboxAsync(-9.3, 41.7, -6.7, 43.8);
- var stops = allStopsRaw.Select(s =>
+ var mappedStops = allStopsRaw.Select(s =>
{
var feedId = s.GtfsId.Split(':')[0];
var name = FeedService.NormalizeStopName(feedId, s.Name);
var code = _feedService.NormalizeStopCode(feedId, s.Code ?? string.Empty);
+ var (color, textColor) = _feedService.GetFallbackColourForFeed(feedId);
return new PlannerSearchResult
{
@@ -196,8 +231,21 @@ public partial class RoutePlannerController : ControllerBase
Lon = s.Lon,
Layer = "stop",
StopId = s.GtfsId,
- StopCode = code
+ StopCode = code,
+ Color = color,
+ TextColor = textColor
};
+ });
+
+ // For xunta stops, deduplicate by base code (strip first 2 chars)
+ // e.g. "1007958" and "2007958" refer to the same physical stop
+ var xuntaSeenBaseCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+ var stops = mappedStops.Where(s =>
+ {
+ if (s.StopId?.StartsWith("xunta:") != true) return true;
+ var code = s.StopCode ?? string.Empty;
+ var baseCode = code.Length > 2 ? code[2..] : code;
+ return xuntaSeenBaseCodes.Add(baseCode);
}).ToList();
_cache.Set(cacheKey, stops, TimeSpan.FromHours(1));