$ 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