Create routable network from GTFS files

Back to Public transport

This page describes how the model constructs a routable public transport network from raw GTFS source files. The configuration is in cfg/main/SourceData/GTFS.dms, which includes LoadFeeds.dms and RelevantSelection.dms from cfg/main/SourceData/GTFS; in the configuration tree the container is /SourceData/Infrastructure/GTFS.


GTFS data model

GTFS (General Transit Feed Specification) is a widely used open standard for public transport timetables. The model reads the following GTFS files:

File Contents
agency.txt Transit agencies whose services are included in the feed
calendar.txt Weekly service schedules with start and end dates
calendar_dates.txt Exceptions to the calendar, such as added or removed service days
routes.txt Transit routes; a route groups trips that are presented to passengers as a single service
shapes.txt Geographic alignments of vehicle paths
stop_times.txt Scheduled arrival and departure times at each stop for each trip
stops.txt Stop locations with coordinates, names, and identifiers
trips.txt Individual trips, each belonging to a route and operating on a service calendar

For the Netherlands, GTFS feeds are publicly available from the national data portal gtfs.openov.nl. The model reads one feed, the folder of GTFS_file_date, with the eight files listed in LoadFeeds/File_Structure.


Step 1: loading GTFS text files into an MMD cache

GTFS files are plain text CSV files. Reading these repeatedly during analysis would be slow, so the model first converts them once to a GeoDMS MMD cache with LoadFeeds/Write, one table per file. LoadFeeds/Load reads each csv from %NetworkModel_Dir%/Infrastructuur/GTFS/<GTFS_file_date>/ with the template LoadFeeds/LoadCSVThroughGDAL_T. All subsequent steps read the cache via LoadFeeds/Read:

'%LocalDataProjDir%/GTFS/' + GTFS_file_date + '.mmd'

The GTFS_file_date parameter (e.g. '20231003') identifies the feed download date. It follows from the preset of AnalysisMoment; see step 2 of How to run the model.


Step 2: date selection and service filtering

The model performs its analysis for one calendar date D, ModelParameters/Impl/Analysis_date. Like the feed, it follows from AnalysisMoment: the second Tuesday of October of the reference year, for example '20251014' for Y2025 (#125). All times are seconds since 00:00 of D on one axis of 48 hours (Time), without wrapping at midnight: a time after midnight is 86,400 or more, and a period that crosses midnight, such as 23:00 to 01:00, is one interval. Departure moments always lie on D itself.

Three service days

The network is built from three service days (LoadFeeds/ServiceDays):

service day which trips position on the axis
D−1 the part after midnight: stop times of 24:00 or later GTFS time − 24 h
D all trips, including their times of 24:00 or later GTFS time
D+1 whole trips that start before the end of the analysis window GTFS time + 24 h

In a GTFS feed a service day continues after midnight with times of 24:00 and later. On a Tuesday in the feed of 16 March 2026, for example, 41,700 of the 2.31 million departures are after midnight (35,500 between 24:00 and 25:00), while the day itself has only 33 departures before 01:00. Public transport between midnight and about 02:00 therefore comes almost entirely from D−1.

The analysis window runs from Impl/PT_FirstDepartureMoment, the first departure moment, to Impl/PT_LastArrivalMoment, the last departure moment plus MaxPTTime and the waiting time at home. D+1 therefore only contributes when the window crosses midnight; with departures at 23:00 to 23:45 on 1 October 2024 it adds 117 links.

The dates of D−1 and D+1 follow from a day number, not from arithmetic on the digits of yyyymmdd, which on the first day of a month would give a date that does not exist: Templates/DayNr_T converts yyyymmdd to a day number (days since 1 January 1970) and Templates/CivilDate_T converts back.

Checks on the date

When the network is built, three checks on the chosen date are computed (ExplicitSuppliers of RelevantSelection/ScheduledLinks); each stops the run with a failed integrity check:

  • Check_ServiceDaysComplete: every service day the window needs has at least Advanced/MinServiceDayTripShare (0.95) of the trips of the busiest day of the same weekday in the feed. D is always needed, D−1 when the window starts before Impl/PrevServiceDayTailEnd (06:00), D+1 when it ends after midnight. A feed is complete for only a few weeks: in the feed of 16 March 2026 a Tuesday three weeks later has 6% and four weeks later 9% fewer trips than the first Tuesday, and from July less than half. A public holiday runs a Sunday service and fails as well. ServiceDays/Report lists the numbers per day.
  • Check_WeekdayMatchesCongestionDay: the weekday of D matches Advanced/CongestionSpeed_DaySelection, the day of the TomTom car speeds ('tuesday' by default; 'Weekday', 'Weekend' and 'Week' accept any day of that kind).
  • Check_NoDSTChange: D, and D+1 when the window crosses midnight, is not the last Sunday of March or October. GTFS counts the times of a service day from noon minus 12 hours, which on those days is not midnight.

Which services run

The Services unit aggregates the calendar_dates.txt exceptions per service identifier and day (Today_exc, Yesterday_exc, Tomorrow_exc): the sum of exception_type for that date, where 1 means service is added and 2 means service is removed. A trip runs on a day when this sum is 1. The model reads only calendar_dates.txt, not the weekly patterns of calendar.txt; the OVapi feeds put every service day in calendar_dates.txt.


Step 3: constructing RawStops and the filtered Stops set

All stop records from stops.txt are first loaded into RawStops. Here, WGS84 coordinates (stop_lat, stop_lon) are converted to Dutch Rijksdriehoek (rdc) coordinates using LatLongWgs842RD.

A stop with a latitude or longitude of 0 in the feed gets the point of its parent_station (IsWithoutPoint, Parent_rel), or keeps its own point when the parent has none. Otherwise all such stops would lie at one point, form one stop cluster and allow transfers between them at a distance of 0 m. In the feed of 25 September 2026 this concerns 187 stops, 45 of them with stop times: German platforms of the ICE from Amsterdam to Berlin and Hamburg and of DB trains near Münster, Dortmund and Lünen, each with a parent that has a point.

The StopUsed attribute then marks each stop as active if at least one stop time record in RawStopTimes links to it on one of the three service days:

StopUsed := any(RawStopTimes/SelToday, RawStopTimes/RawStop_rel)
         || any(RawStopTimes/SelYesterday, RawStopTimes/RawStop_rel)
         || any(RawStopTimes/SelTomorrow, RawStopTimes/RawStop_rel)

The public transport nodes (PT_Hubs) cluster only the stops with a departure on D, so a stop used only by a night trip of D−1 does not join two clusters.

The filtered Stops unit in RelevantSelection is derived from RawStops by selecting only those stops where StopUsed is true. For each stop, a mode is assigned based on the most frequent transport mode serving it across all StopTimes (modus(StopTimes/Mode_rel, StopTimes/Stop_rel)). GTFS/PublicTransportNet/Stops, the same set of stops, adds the attributes that the public transport model needs later.

Stop clusters

Find_StopClusters identifies adjacent platforms of the same station and stops on both sides of a street: it uses join_near_values to find all pairs of stops within DistanceStopClusters (100 m) of each other. Its c_Stop_Stop_rel is used to drop the legs that start and end within one cluster (IsStartEndWithinSameStopCluster in OD_extra_attributen.dms).

NS station mapping

Find_Trainstations_Clusters spatially joins each stop against a list of known train stations within DistanceTrainStationsSelection (350 m), then looks up the corresponding NS_Stations_rel from the NS pricing data via the station code. A rail stop without a station within that distance is matched on its name instead (Trainstation_byname_rel): rail platforms carry the name of their station, bus stops do not. In the feed of 25 September 2026, for example, the platforms of Vught lie about 600 m south of the station point, and the name match gives their legs an NS station and so an NS price. The IsNSICStation flag is derived from the distance match only. These attributes are needed for NS pricing and through-pricing.

OV-fiets stations

Find_OVFiets_Stops spatially joins stops against the OV-fiets locations of the reference date (OV_Fiets_Stations, from Impl/OVFiets_File) within DistanceOVFietsStops (200 m), producing the IsOVFietsStation flag. The OV-fiets post-transport type (OVF_Stops2Dest_Cycling) starts only at stops with this flag.


RawStopTimes enriches the raw stop times with the trip’s service selection flags and computes the NextStopId for each stop-time record: the record in the same trip with the next sequential stop sequence number. Each non-final stop-time record thus forms one link from stop N to stop N+1.

Links are built per service day by the template DayLinks_T, in RawStopTimes/Yesterday/Links, RawStopTimes/Today/Links and RawStopTimes/Tomorrow/Links, with the times shifted by −24, 0 and +24 hours. The selection of each day (SelYesterdayNet, SelTodayNet, SelTomorrowNet) leaves out the flex trips that LoadFeeds/FlexOptions thins out. Link length is taken from shape_dist_traveled where it is more than 100 m, falling back to the Euclidean arc length otherwise.

The ScheduledLinks unit in RelevantSelection is the union of the links of the three service days. Each link has:

  • FromStop_rel and ToStop_rel: references into the filtered Stops set.
  • fromTime_rel and toTime_rel: departure and arrival times as time step indices.
  • Duration: travel time in seconds (with midnight-crossing handled via sub_or_null with wrap-around).
  • Length: route distance in kilometres.
  • Mode_rel, Route_rel, Agency_rel, Trip_rel: references to the relevant GTFS entities.
  • FromPickupAllowed, FromDropOffAllowed, ToPickupAllowed and ToDropOffAllowed: whether the GTFS (pickup_type, drop_off_type) allows boarding and alighting at the start and end of the link.
  • c_FromTime_Stop_rel and c_ToTime_Stop_rel: the c_Time_Stops combined keys used throughout the OV model.

The unique set of all departure and arrival time-stop events across all ScheduledLinks is also computed here as uq_TimeStop (aka Scheduled Space-Time Events, SSTE). The unique set of stop locations that appear in the link set forms SL_Places, which is the Places domain of the GTFS container and, together with the origins and destinations, of PublicTransport_Prep.


The scheduled links alone do not yet form a routable network. A traveller sitting in a train at an intermediate stop must wait until the train departs again; this waiting time is not represented by any scheduled link. To make the network routable, waiting-at-stop links are added between consecutive time-stop events on the same trip.

This is done in GTFS/GetWaitingAtStop, using CreateWaitingAtStopSet_T. The construction proceeds as follows:

  1. All scheduled links are doubled into their from-events and to-events, producing a set of (trip, time) pairs called doubledLinks.
  2. The unique set of these pairs, WaitingAtStop, gives one node per moment that a vehicle is present at a stop within a trip.
  3. For each node, NextMoment identifies the next node on the same trip at the same stop, using add_or_null(id(.), 1) where both the trip and stop match.
  4. A waiting link is created from each node to its NextMoment, with Duration equal to the time difference between them. Midnight crossings are handled in the same way as for scheduled links.

Each event takes the stop of its own scheduled link (FromStop_rel or ToStop_rel), not a stop looked up by its point. Several stops can share one point (in the feed of 25 September 2026, 810 of the 49,947 stops in use), and a dwell must join the nodes of its own stop, so that a leg can continue through that stop in the same vehicle.

Waiting links have Length = 0, Mode_rel = Waiting, and no Agency_rel. For Set_L (the NS network subset), waiting links at NS stations are included alongside the scheduled NS links, so that the Dijkstra traversal can wait for a later NS departure.


Step 6: the complete PublicTransportNet

GTFS/PublicTransportNet is the union of RelevantSelection/ScheduledLinks and GetWaitingAtStop/WaitingAtStop. This is the full routable network. All links carry a LinkType_rel attribute (Scheduled or Waiting_at_Stop) from Classifications/LinkTypes.

Two further attributes are computed on this union for NS pricing:

  • NS_StartPoint_rel and NS_EndPoint_rel: the NS station references for the from-stop and to-stop, via Stops/NS_Stations_rel.
  • c_NS_start_end_rel: the combined station-pair key used to look up NS prices and through-prices in TariffUnitsMatrix (see Public transport fares).

This network is the input to Get_Windowed_Agency_Set_T in PublicTransport_Prep, which applies time-window and agency filters to produce the Set_R and Set_L subsets used for the OD matrix calculations. See the Script reference for details on those steps.


Routes and TimeInvariantTypes

The Routes unit in RelevantSelection is not only built from the GTFS routes.txt but also includes a row for each element of Classifications/TimeInvariantTypes. This means that pre- and post-transport connection types (walking, cycling) are represented as routes in the same domain as scheduled public transport routes. This unified representation simplifies attribute lookup by mode and route throughout the model.