gb‑transit

Tools

cif2gtfs

Build a GTFS feed from the CIF timetable in the British rail DTD feed. No database, one command.

$ npm install -g cif2gtfs$ cif2gtfs build --source RJTTF918.ZIP --out gtfs.zip

This is the tool that produces the published feed. Its output is byte-identical to what dtd2mysql --gtfs writes from the same feed files, without needing the database in between.

Usage

cif2gtfs build [OPTIONS]

  --source PATH        a DTD timetable zip, or a directory of them. Repeat it to
                       combine sources
  --out PATH           where to write. A path ending .zip produces a zip, anything
                       else a directory of text files (defaults to ./gtfs.zip)
  --range RANGE        how far ahead to build, e.g. "3 months" (defaults to '3 MONTH')
  --today YYYY-MM-DD   the date to build for (defaults to the current date)
  --remove-passing-points BOOL
                       whether to drop the locations a service runs through
                       without stopping (defaults to true)
  --config PATH        a YAML file of the above, plus enrichers and extensions

GTFS_RANGE, GTFS_TODAY and GTFS_REMOVE_PASSING_POINTS are read as well, for compatibility with dtd2mysql. The flags override them.

--today exists so a build can be reproduced. Without it the feed is a function of the day it ran and cannot be compared to yesterday’s.

Combining sources

The DTD feed is published as a weekly full refresh followed by daily incrementals. Pass them in the order they were published and the result is the same as importing them in that order:

cif2gtfs build \
  --source RJTTF918.ZIP \
  --source RJTTC919.ZIP \
  --source RJTTC920.ZIP \
  --out gtfs.zip --range "6 months"

Or point it at the directory you download into and let it work that out:

cif2gtfs build --source ./feeds --out gtfs.zip --range "6 months"

A directory contributes every RJTTFxxx.ZIP and RJTTCxxx.ZIP it holds, ordered by sequence number and starting at the most recent full refresh — anything before that refresh is superseded by it. The fares, routeing and NFM64 feeds are ignored, so a directory holding all four is fine.

This is not the same as sorting by filename: as text every RJTTC sorts before every RJTTF, which would put the refresh after the incrementals that amend it.

Passing points

Half the CIF’s intermediate location records are places a service runs through without stopping, and 892,000 of them are at a station the feed publishes. They are not calls: nobody boards and nobody alights. By default they are dropped.

--remove-passing-points=false keeps them, as calls with pickup_type and drop_off_type of 1 and the pass time as both the arrival and the departure. Over three months of the whole network that is 3.43 million stop times against 2.84 million, of which 592,000 are passed rather than called at. The trips, routes and calendars are identical either way; stops.txt gains 59 stops.

A passing point names its platform like any other call, and falls back to the station where the pass record gives none. The platform a train runs through is a real platform: 89% of passing calls land on a boarding point the feed already publishes because something stops there, so the id a passing call carries is the one a stopping call at that platform carries. The 05:00 Victoria to Gatwick passes Clapham Junction platform 15 as 9100CLPHMJC15 — the same stop 1,837 boardable calls use.

The 12 stops that are new are real platforms this window has no calls at: Pilning 2, New Cross Gate 3 and 4, Wembley Central 3 and 4, Clapham Junction Main 8, Finsbury Park 3, Grove Park 2, New Barnet 2, Rotherham Central 4T, and Wixams 1 and 2.

One caveat if you use both feeds: a call publishes its public time and a passing point has only its working time, and the CIF’s two clocks do not always agree. In 23 places out of 3.43 million a pass reads as later than the call after it.

What it produces

agency.txt, stops.txt, transfers.txt, links.txt, routes.txt, trips.txt, stop_times.txt, calendar.txt and calendar_dates.txt. The same input and --today always produce the same bytes.

A build that finds no schedules in the window fails rather than writing an empty feed.

Building three months of the whole GB network takes around 45 seconds and 5 GB of memory.

Using this data describes what the output means: the identifiers it assigns, how it expresses splits and joins, and the columns it adds.

Getting the timetable

The timetable feed comes from the DTD SFTP server, which needs a Rail Data Marketplace subscription. dtd2mysql --download-timetable will fetch it, or take it from wherever you already keep it.

Requirements

Node.js 22 or later. Date handling uses Temporal, through temporal-polyfill, which hands over to the built-in global on the versions that have one — Node 26 and later.

cif2gtfs on npm · source · GPL v3