aboutsummaryrefslogtreecommitdiff
path: root/src/Enmarcha.Backend
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
parentfc6d4cbaf78f75a5ac234862ecbf86faeb78a338 (diff)
Fix sorting shenanigans, improve stop viewingHEADmain
Diffstat (limited to 'src/Enmarcha.Backend')
-rw-r--r--src/Enmarcha.Backend/Controllers/RoutePlannerController.cs66
-rw-r--r--src/Enmarcha.Backend/Controllers/TileController.cs16
-rw-r--r--src/Enmarcha.Backend/Program.cs14
-rw-r--r--src/Enmarcha.Backend/Types/Planner/PlannerResponse.cs2
4 files changed, 81 insertions, 17 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));
diff --git a/src/Enmarcha.Backend/Controllers/TileController.cs b/src/Enmarcha.Backend/Controllers/TileController.cs
index 0d96a14..896fdb9 100644
--- a/src/Enmarcha.Backend/Controllers/TileController.cs
+++ b/src/Enmarcha.Backend/Controllers/TileController.cs
@@ -98,6 +98,7 @@ public class TileController : ControllerBase
var stopsLayer = new Layer { Name = "stops" };
var features = new List<Feature>();
+ var xuntaSeenBaseCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
responseBody.Data?.StopsByBbox?.ForEach(stop =>
{
@@ -110,6 +111,17 @@ public class TileController : ControllerBase
return;
}
+ // For xunta stops, deduplicate by stripping the first 2 chars of the code
+ // (e.g. "1007958" and "2007958" refer to the same physical stop)
+ if (feedId == "xunta" && codeWithinFeed.Length > 2)
+ {
+ var baseCode = codeWithinFeed[2..];
+ if (!xuntaSeenBaseCodes.Add(baseCode))
+ {
+ return;
+ }
+ }
+
// TODO: Duplicate from ArrivalsController
var (Color, TextColor) = _feedService.GetFallbackColourForFeed(idParts[0]);
var distinctRoutes = GetDistinctRoutes(feedId, stop.Routes ?? []);
@@ -127,7 +139,9 @@ public class TileController : ControllerBase
{ "code", $"{idParts[0]}:{codeWithinFeed}" },
{ "name", FeedService.NormalizeStopName(feedId, stop.Name) },
{ "icon", GetIconNameForFeed(feedId) },
- { "transitKind", TransitKindClassifier.StringByFeed(feedId) }
+ { "transitKind", TransitKindClassifier.StringByFeed(feedId) },
+ { "color", Color },
+ { "textColor", TextColor }
}
};
diff --git a/src/Enmarcha.Backend/Program.cs b/src/Enmarcha.Backend/Program.cs
index 785afe5..8988bc5 100644
--- a/src/Enmarcha.Backend/Program.cs
+++ b/src/Enmarcha.Backend/Program.cs
@@ -209,6 +209,13 @@ builder.Services.AddSingleton<FareService>();
builder.Services.AddScoped<IPushNotificationService, PushNotificationService>();
builder.Services.AddHostedService<AlertPhaseNotificationHostedService>();
+builder.Services.AddScoped<IArrivalsProcessor, VitrasaRealTimeProcessor>();
+builder.Services.AddScoped<IArrivalsProcessor, CorunaRealTimeProcessor>();
+builder.Services.AddScoped<IArrivalsProcessor, TussaRealTimeProcessor>();
+builder.Services.AddScoped<IArrivalsProcessor, CtagShuttleRealTimeProcessor>();
+builder.Services.AddScoped<IArrivalsProcessor, VitrasaUsageProcessor>();
+builder.Services.AddScoped<IArrivalsProcessor, RenfeRealTimeProcessor>();
+
builder.Services.AddScoped<IArrivalsProcessor, FilterAndSortProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, NextStopsProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, ShapeProcessor>();
@@ -217,13 +224,6 @@ builder.Services.AddScoped<IArrivalsProcessor, XuntaNormalizationProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, TranviasNormalizationProcessor>();
builder.Services.AddScoped<IArrivalsProcessor, ColourProcessor>();
-builder.Services.AddScoped<IArrivalsProcessor, VitrasaRealTimeProcessor>();
-builder.Services.AddScoped<IArrivalsProcessor, CorunaRealTimeProcessor>();
-builder.Services.AddScoped<IArrivalsProcessor, TussaRealTimeProcessor>();
-builder.Services.AddScoped<IArrivalsProcessor, CtagShuttleRealTimeProcessor>();
-builder.Services.AddScoped<IArrivalsProcessor, VitrasaUsageProcessor>();
-builder.Services.AddScoped<IArrivalsProcessor, RenfeRealTimeProcessor>();
-
builder.Services.AddScoped<ArrivalsPipeline>();
// builder.Services.AddKeyedScoped<IGeocodingService, NominatimGeocodingService>("Nominatim");
diff --git a/src/Enmarcha.Backend/Types/Planner/PlannerResponse.cs b/src/Enmarcha.Backend/Types/Planner/PlannerResponse.cs
index 3d48831..af88ccc 100644
--- a/src/Enmarcha.Backend/Types/Planner/PlannerResponse.cs
+++ b/src/Enmarcha.Backend/Types/Planner/PlannerResponse.cs
@@ -85,4 +85,6 @@ public class PlannerSearchResult
public string? Layer { get; set; }
public string? StopId { get; set; }
public string? StopCode { get; set; }
+ public string? Color { get; set; }
+ public string? TextColor { get; set; }
}