Skip to content

feat(adapter): add Helsinki (HSL) GTFS adapter - #166

Open
ciotlosm wants to merge 1 commit into
mainfrom
feat/helsinki-adapter
Open

feat(adapter): add Helsinki (HSL) GTFS adapter#166
ciotlosm wants to merge 1 commit into
mainfrom
feat/helsinki-adapter

Conversation

@ciotlosm

Copy link
Copy Markdown
Member

Adds a new adapter package, @n3ary/gtfs-adapter-helsinki, for HSL's (Helsinki Region Transport) GTFS feed. One of the per-feed adapters under gtfs-adapters/adapters/, alongside the existing cluj-napoca.

Why this is simpler than the cluj adapter

  • HSL publishes a complete, daily-regenerated GTFS zip at a stable URL. No Transitous seed + Tranzy reconciliation needed — HSL IS the canonical source.
  • HSL publishes standard GTFS-RT protobuf at realtime.hsl.fi. No HFP conversion needed (worried about this going in — turned out HSL's public API is standard GTFS-RT).
  • HSL's open data is public. No API key, no rate limit.
  • HSL ships route_color + route_text_color per route. No per-feed color computation.
  • HSL has no per-feed networks. No _route_tags / network derive pipeline.

Adapter surface

Subpath What it does
/ingest ingestBuild(). Single GET against https://gtfs.hsl.fi/gtfs.zip, returns the bytes. No zip-content verification (the static pipeline catches malformed GTFS via the SQL load errors). REQUIRED_SECRETS is empty.
/static staticExtension(feedConfig) — adds _neary_config for the app's timing-aware travel-time math, no-op fillComputedColumns. Exports hslRouteType + computeHslBrandColor for the app's category chip rendering. HSL's pathway_mode=1 → metro is handled by hslRouteType (HSL's metro lines use route_type=0 historically, with pathway_mode as the real signal).
/rt helsinkiQuirk (called by the generic gtfs-rt proxy per fetched FeedMessage). Two HSL-specific fixes: (1) synthesize a trip_id for VehiclePosition / TripUpdate entities when HSL omits one — HSL uses the composite route_id + start_date + start_time + direction_id instead (per HSL GTFS-RT docs). Format: hsl:<route_id>:<start_date>:<start_time>:<direction_id>. (2) split the HSL <operator>/<vehicle> vehicle id, set vehicle.label to the bare vehicle number. Plus splitHslVehicleId / syntheticHslTripId helpers, exported for tests.

Files

  • adapters/helsinki/package.json — package manifest
  • adapters/helsinki/src/ingest/index.tsingestBuild()
  • adapters/helsinki/src/rt/helsinki.ts + index.ts — quirk + barrel
  • adapters/helsinki/src/static/{extension,index,route-types}.ts — static pipeline
  • adapters/helsinki/tests/{helsinki,ingest,extension,route-types}.test.ts — 44 tests
  • adapters/helsinki/{LICENSE,README.md,tsconfig.*,vitest.config.js}

44 unit tests pass, pnpm check clean, pnpm build emits dist/ (gitignored).

Out of scope (separate PRs)

  • n3ary/gtfs/feeds/helsinki/config.json — wires the adapter into the static pipeline
  • n3ary/gtfs-publisher/feeds/helsinki/config.json — wires the adapter into the RT publisher's feeds.json

This PR is the adapter only. Once it lands, the feed configs in the other two repos wire it in.

New adapter package @n3ary/gtfs-adapter-helsinki for the
n3ary transit family. Adapts HSL's daily GTFS feed plus
the standard GTFS-RT protobuf feeds (vehicle positions, trip
updates, service alerts) at realtime.hsl.fi.

Why this is simpler than the cluj adapter:
- HSL publishes a complete, daily-regenerated GTFS zip
  (no Transitous seed + Tranzy reconciliation needed)
- HSL publishes standard GTFS-RT protobuf (no HFP conversion
  -- the worry going in -- the GTFS-RT surface is the
  public realtime API)
- HSL's open data is public (no API key, no rate limit)
- HSL ships route_color + route_text_color per route already
  (no per-feed color computation needed)
- HSL has no per-feed networks (no _route_tags / network
  derive pipeline)

Adapter surface:

  /ingest  - DEFAULT_GTFS_URL, REQUIRED_SECRETS (empty),
             DEFAULT_AGENCY_ID, ingestBuild(). Single GET
             against https://gtfs.hsl.fi/gtfs.zip, returns
             the bytes. No zip-content verification (the
             static pipeline catches malformed GTFS via the
             SQL load errors; a fetch-verify round-trip would
             add a zip-reader dep for marginal value).

  /static  - staticExtension(feedConfig) -- adds _neary_config
             for the app's timing-aware travel-time math, no-op
             fillComputedColumns. Exports hslRouteType +
             computeHslBrandColor for the app's category chip
             rendering. HSL's pathway_mode=1 -> metro is
             handled by hslRouteType (HSL's metro lines use
             route_type=0 historically, with pathway_mode as
             the real signal).

  /rt      - helsinkiQuirk (called by the generic gtfs-rt
             proxy per fetched FeedMessage). Two HSL-specific
             fixes:
               1. Synthesize a trip_id for VehiclePosition
                  and TripUpdate entities when HSL omits one.
                  HSL's real-time feed uses the composite
                  route_id + start_date + start_time +
                  direction_id instead of trip_id (per
                  https://hsldevcom.github.io/gtfs_rt/). The
                  static feed regenerates trip_ids on every
                  publish, so the composite is the only stable
                  join key. Format:
                  hsl:<route_id>:<start_date>:<start_time>:
                  <direction_id>.
               2. Split the HSL <operator>/<vehicle> vehicle
                  id. Sets vehicle.label to the bare vehicle
                  number while keeping vehicle.id as the full
                  composite (so consumers can filter by
                  operator).
             Plus splitHslVehicleId / syntheticHslTripId
             helpers, exported for tests.

44 unit tests, all green. pnpm check clean. pnpm build
emits a dist/ tree (gitignored).

Separate PRs still needed in:
  - n3ary/gtfs            feeds/helsinki/config.json
  - n3ary/gtfs-publisher  feeds/helsinki/config.json
This PR is the adapter only; the feed configs wire it in
and the publisher's feeds.json can be regenerated against
the new package.
ciotlosm added a commit to n3ary/gtfs-publisher that referenced this pull request Aug 23, 2026
Wires the new @n3ary/gtfs-adapter-helsinki adapter (n3ary/
gtfs-adapters#166) into the static pipeline. Mirrors the
cluj-napoca config shape:

- id: helsinki
- source: adapter-driven, publisher @n3ary/gtfs-adapter-helsinki
- secrets: empty (HSL is public open data, no API key)
- license: CC BY 4.0 (HSL open data, attribution required)
- smoke: tripIdPattern matches the synthetic hsl:<route>:
  <date>:<time>:<dir> format the adapter's RT quirk produces
- timing: HSL-defaulted speeds (metro + tram + bus backbone
  means peak ~22 km/h, higher than cluj's 14 km/h). Peak
  windows 7-9 and 15:30-17:30. Night 23:00-05:30. Dwell 25s.

Sister PR: n3ary/gtfs-adapters#166 (the adapter itself)
and a follow-up in n3ary/gtfs-publisher to add the RT
config.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant