diff options
| author | Ariel Costas Guerrero <ariel@costas.dev> | 2026-02-11 16:50:34 +0100 |
|---|---|---|
| committer | Ariel Costas Guerrero <ariel@costas.dev> | 2026-02-11 16:50:34 +0100 |
| commit | 51c5b376f9b6ad3ec05da6f8933c5b6a46c29d60 (patch) | |
| tree | 2188045051fdde29bec1e51e3b7aeadb988d6fba /src/Enmarcha.Backend | |
| parent | b2700b9ef9e34cebc90d669fd53bde91401cae52 (diff) | |
Implement better route sorting for Vitrasa
Closes #134
Diffstat (limited to 'src/Enmarcha.Backend')
| -rw-r--r-- | src/Enmarcha.Backend/Controllers/ArrivalsController.cs | 5 | ||||
| -rw-r--r-- | src/Enmarcha.Backend/Controllers/TransitController.cs | 2 | ||||
| -rw-r--r-- | src/Enmarcha.Backend/Helpers/SortingHelper.cs | 88 |
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()); + } } |
