aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorAriel Costas Guerrero <ariel@costas.dev>2026-02-11 16:50:34 +0100
committerAriel Costas Guerrero <ariel@costas.dev>2026-02-11 16:50:34 +0100
commit51c5b376f9b6ad3ec05da6f8933c5b6a46c29d60 (patch)
tree2188045051fdde29bec1e51e3b7aeadb988d6fba /src
parentb2700b9ef9e34cebc90d669fd53bde91401cae52 (diff)
Implement better route sorting for Vitrasa
Closes #134
Diffstat (limited to 'src')
-rw-r--r--src/Enmarcha.Backend/Controllers/ArrivalsController.cs5
-rw-r--r--src/Enmarcha.Backend/Controllers/TransitController.cs2
-rw-r--r--src/Enmarcha.Backend/Helpers/SortingHelper.cs88
3 files changed, 90 insertions, 5 deletions
diff --git a/src/Enmarcha.Backend/Controllers/ArrivalsController.cs b/src/Enmarcha.Backend/Controllers/ArrivalsController.cs
index b93b3c9..5c64efa 100644
--- a/src/Enmarcha.Backend/Controllers/ArrivalsController.cs
+++ b/src/Enmarcha.Backend/Controllers/ArrivalsController.cs
@@ -157,10 +157,7 @@ public partial class ArrivalsController : ControllerBase
Longitude = stop.Lon
},
Routes = [.. stop.Routes
- .OrderBy(
- r => r.ShortName,
- Comparer<string?>.Create(SortingHelper.SortRouteShortNames)
- )
+ .OrderBy(r => SortingHelper.GetRouteSortKey(r.ShortName, r.GtfsId))
.Select(r => new RouteInfo
{
GtfsId = r.GtfsId,
diff --git a/src/Enmarcha.Backend/Controllers/TransitController.cs b/src/Enmarcha.Backend/Controllers/TransitController.cs
index 93129f9..a70f46e 100644
--- a/src/Enmarcha.Backend/Controllers/TransitController.cs
+++ b/src/Enmarcha.Backend/Controllers/TransitController.cs
@@ -68,7 +68,7 @@ public class TransitController : ControllerBase
var routes = response.Data.Routes
.Select(_otpService.MapRoute)
- .OrderBy(r => r.ShortName, Comparer<string?>.Create(SortingHelper.SortRouteShortNames))
+ .OrderBy(r => SortingHelper.GetRouteSortKey(r.ShortName, r.Id))
.ToList();
_cache.Set(cacheKey, routes, TimeSpan.FromHours(1));
diff --git a/src/Enmarcha.Backend/Helpers/SortingHelper.cs b/src/Enmarcha.Backend/Helpers/SortingHelper.cs
index c70dab2..e2267af 100644
--- a/src/Enmarcha.Backend/Helpers/SortingHelper.cs
+++ b/src/Enmarcha.Backend/Helpers/SortingHelper.cs
@@ -2,6 +2,10 @@ namespace Enmarcha.Backend.Helpers;
public class SortingHelper
{
+ /// <summary>
+ /// Generic route short name comparison. Non-numeric names sort first, then numeric by value,
+ /// then alphabetical tiebreak. Used for per-stop sorting where route IDs may not be available.
+ /// </summary>
public static int SortRouteShortNames(string? a, string? b)
{
if (a == null && b == null) return 0;
@@ -32,4 +36,88 @@ public class SortingHelper
return string.Compare(a, b, StringComparison.OrdinalIgnoreCase);
}
+ /// <summary>
+ /// Feed-aware route sort key. For Vitrasa, applies custom group ordering:
+ /// Circular (C*) → Regular numbered → Hospital (H*) → Others (N*, PSA*, U*, specials).
+ /// For other feeds, uses generic numeric-then-alphabetic ordering.
+ /// </summary>
+ public static (int Group, string Prefix, int Number, string Name) GetRouteSortKey(
+ string? shortName, string? routeId)
+ {
+ if (string.IsNullOrEmpty(shortName))
+ return (99, "", int.MaxValue, shortName ?? "");
+
+ var feed = routeId?.Split(':')[0];
+
+ if (feed == "vitrasa")
+ {
+ int group = GetVitrasaRouteGroup(shortName);
+ // For "Others" group, sub-sort by alphabetic prefix to keep N*, PSA*, U* etc. grouped
+ string prefix = group == 3 ? ExtractAlphaPrefix(shortName) : "";
+ int number = ExtractNumber(shortName);
+
+ // For routes with no number in short name (like "A"), use the GTFS route ID number
+ if (number == int.MaxValue && routeId != null)
+ {
+ var idPart = routeId.Split(':').Last();
+ if (int.TryParse(idPart, out int idNumber))
+ number = idNumber;
+ }
+
+ return (group, prefix, number, shortName);
+ }
+
+ // Generic: non-numeric names first, then by number, then alphabetical
+ int genericNumber = ExtractNumber(shortName);
+ bool hasDigits = genericNumber != int.MaxValue;
+ return (hasDigits ? 1 : 0, "", genericNumber, shortName);
+ }
+
+ /// <summary>
+ /// Vitrasa route groups:
+ /// 0 = Circular (C1, C3d, C3i)
+ /// 1 = Regular numbered routes (4A, 6, 10, A, etc.)
+ /// 2 = Hospital (H, H1, H2, H3)
+ /// 3 = Others (N*, PSA*, U*, LZD, PTL)
+ /// </summary>
+ private static int GetVitrasaRouteGroup(string shortName)
+ {
+ // Circular: "C" followed by a digit
+ if (shortName.Length > 1 && shortName[0] == 'C' && char.IsDigit(shortName[1]))
+ return 0;
+
+ // Hospital: starts with "H"
+ if (shortName[0] == 'H')
+ return 2;
+
+ // Night: "N" followed by a digit
+ if (shortName[0] == 'N' && shortName.Length > 1 && char.IsDigit(shortName[1]))
+ return 3;
+
+ // PSA shuttle lines
+ if (shortName.StartsWith("PSA", StringComparison.OrdinalIgnoreCase))
+ return 3;
+
+ // University: "U" followed by a digit
+ if (shortName[0] == 'U' && shortName.Length > 1 && char.IsDigit(shortName[1]))
+ return 3;
+
+ // Multi-letter codes with no digits (LZD, PTL)
+ if (shortName.Length >= 2 && shortName.All(char.IsLetter))
+ return 3;
+
+ // Everything else is regular (numbered routes like 4A, 6, 10, single letters like A)
+ return 1;
+ }
+
+ private static int ExtractNumber(string name)
+ {
+ var digits = new string(name.Where(char.IsDigit).ToArray());
+ return int.TryParse(digits, out int number) ? number : int.MaxValue;
+ }
+
+ private static string ExtractAlphaPrefix(string name)
+ {
+ return new string(name.TakeWhile(char.IsLetter).ToArray());
+ }
}