From 65ac2d37fb82de4fa38d0d18f9a019b0b9114e49 Mon Sep 17 00:00:00 2001 From: Cliff Flood Date: Wed, 16 Sep 2026 17:07:45 -0400 Subject: [PATCH] feat(transit): name Hamilton's routes, which ship as internal keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not one of HSR's 80 route ids equals the number on the bus. Hamilton was registered without a routeLabel, so every vehicle in the city read as "Route 5783" where a rider would say 5 DELAWARE — 228 of them in an afternoon sample. The mechanism to fix it already existed and simply was never pointed at this feed. Two details the table cannot skip: Hamilton runs two generations of route id at once. `5688` and `5780` are both the 2 BARTON, and both arrive in the live feed, so both are listed. That is why 80 ids collapse to 46 distinct routes. The seasonal and event shuttles publish a name where the local routes publish a number — TC230, PEACH, ROCKTON. Those are already what a rider would say, so they survive as published rather than being forced into a number they do not have. Numbers are unpadded to match the rest of the registry: routes.txt says `02`, a rider says 2. Ids and names are from the City's own published GTFS routes.txt, which sits under the same open-data licence as the realtime feed. Checked against the live feed: all 30 route ids in service translate, none fall through. --- src/data/transitFeeds.js | 105 +++++++++++++++++++++++++++++++++ src/data/transitFeeds.test.mjs | 17 ++++++ 2 files changed, 122 insertions(+) diff --git a/src/data/transitFeeds.js b/src/data/transitFeeds.js index 818efe245..cdf4b030b 100644 --- a/src/data/transitFeeds.js +++ b/src/data/transitFeeds.js @@ -184,6 +184,110 @@ function guelphRouteLabel(routeId) { return GUELPH_ROUTE_NAMES[routeId] || routeId; } +/** + * Hamilton publishes an internal key for every route: not one of HSR's 80 route + * ids equals the number on the bus, so an untranslated feed reads as + * "Route 5783" city-wide. Two generations of id are live at once — `5688` and + * `5780` are both the 2 BARTON — and both are listed, because both arrive. + * + * Numbers are unpadded to match the rest of the registry (`routes.txt` says + * `02`; a rider says 2). The seasonal and event shuttles publish a name rather + * than a number — `TC230`, `PEACH`, `ROCKTON` — and those are already what a + * rider would say, so they survive as published. Ids and names are from the + * City's own published GTFS `routes.txt`, under the same terms as the feed. + */ +const HAMILTON_ROUTE_NAMES = Object.freeze({ + 5687: '1', + 5688: '2', + 5689: '3', + 5690: '4', + 5691: '5', + 5692: '6', + 5693: '7', + 5694: '8', + 5695: '9', + 5696: '10', + 5697: '11', + 5698: '12', + 5699: '16', + 5700: '18', + 5701: '20', + 5702: '21', + 5703: '22', + 5704: '23', + 5705: '24', + 5706: '25', + 5707: '26', + 5708: '27', + 5709: '33', + 5710: '34', + 5711: '35', + 5712: '41', + 5713: '42', + 5714: '43', + 5715: '44', + 5716: '51', + 5717: '52', + 5718: '55', + 5719: '56', + 5720: '99', + 5731: 'X-IND', + 5780: '2', + 5781: '3', + 5782: '4', + 5783: '5', + 5784: '6', + 5785: '7', + 5786: '8', + 5787: '9', + 5789: '11', + 5790: '12', + 5791: '16', + 5792: '18', + 5793: '20', + 5794: '21', + 5795: '22', + 5796: '23', + 5797: '24', + 5798: '25', + 5799: '26', + 5800: '27', + 5801: '33', + 5802: '34', + 5803: '35', + 5805: '42', + 5806: '43', + 5807: '44', + 5808: '51', + 5809: '52', + 5811: '56', + 5812: '99', + 5820: 'TC230', + 5823: 'X IND', + 5824: 'TC-700', + 5825: 'TC-300', + 5826: 'CAN', + 5827: 'PEACH', + 5828: '1', + 5829: '10', + 5830: '41', + 5831: '55', + 5832: 'TC300', + 5833: 'TC700', + 5834: 'TC730', + 5835: 'ANCFAIR', + 5836: 'ROCKTON', +}); + +/** + * Hamilton route id to the number a rider would say. + * @param {string|null} routeId + * @returns {string|null} + */ +function hamiltonRouteLabel(routeId) { + return HAMILTON_ROUTE_NAMES[routeId] || routeId; +} + /** * YRT publishes Viva bus rapid transit by route id (`601`), while every rider, * station sign and map calls it "Viva Blue". Plain local routes are already the @@ -478,6 +582,7 @@ export const TRANSIT_FEED_REGISTRY = Object.freeze([ note: 'The attribution wording is prescribed by the licence, so it is used verbatim as the credit rather than paraphrased to an operator name. Names, crests, logos and marks are excluded from the grant, so the credit stays text.', }), defaultMode: 'bus', + routeLabel: hamiltonRouteLabel, }), Object.freeze({ id: 'durham-region', diff --git a/src/data/transitFeeds.test.mjs b/src/data/transitFeeds.test.mjs index cc5481afc..22281e46d 100644 --- a/src/data/transitFeeds.test.mjs +++ b/src/data/transitFeeds.test.mjs @@ -432,6 +432,23 @@ test('YRT local branches read as the branch letter, not the padded id', () => { assert.equal(transitRouteLabel(yrt, '9101'), '9101'); }); +test('Hamilton route ids are internal keys, read as the number on the bus', () => { + // Not one of HSR's 80 route ids equals its published short name, so every + // vehicle reads as "Route 5783" until the table translates it. Hamilton also + // carries two generations of id for the same route — `5688` and `5780` are + // both the 2 BARTON — so both must land on the same answer. + const hsr = getTransitFeed('hsr-hamilton'); + assert.equal(transitRouteLabel(hsr, '5780'), '2'); + assert.equal(transitRouteLabel(hsr, '5688'), '2', 'the older id agrees'); + assert.equal(transitRouteLabel(hsr, '5687'), '1'); + assert.equal(transitRouteLabel(hsr, '5829'), '10', 'the B-Line'); + assert.equal(transitRouteLabel(hsr, '5793'), '20', 'the A-Line'); + // Published names that are not numbers are already what a rider would say, + // so they survive as published rather than being forced into a number. + assert.equal(transitRouteLabel(hsr, '5820'), 'TC230'); + assert.equal(transitRouteLabel(hsr, '9999'), '9999', 'unknown survives'); +}); + test('Burlington route ids are joined, never prefix-stripped', () => { // `351` is route 1 and `3510` is route 10, so no amount of trimming a `35` // prefix works — the table is the only correct answer.