Skip to main content
Data Connectors · Public transport

GTFS

The node downloads a static GTFS feed (a ZIP archive with text files such as stops.txt, routes.txt, trips.txt and stop_times.txt), filters the stops with turf to the configured administrative areas, reduces all remaining files to the trips serving those stops and emits the results as GtfsAgency, GtfsStop, GtfsRoute, GtfsTrip and GtfsStopTime entities through five outputs. It is designed for very large feeds such as the Germany-wide feed from gtfs.de.

  • Data source
  • Trigger-based
  • Static GTFS
Node type
gtfs-data
Package
@htag/node-red-contrib-hypertegrity-urbanmobility
Inputs / outputs
1 / 5
Data source
Any static GTFS feed as ZIP via HTTPS (default: gtfs.de Germany)

When to use this node​

The General Transit Feed Specification (GTFS) is the standard format for public transport schedule data. A static GTFS feed is a ZIP archive of CSV text files describing agencies, stops, routes, trips, stop times and service days. It only changes with timetable updates. This is distinct from GTFS Realtime, a Protobuf data stream with delays, vehicle positions and service alerts, which is handled by the GTFS-RT node.

Germany-wide feeds contain several million trips and stop times. The GTFS node processes such volumes in several passes on disk and keeps only the data relevant to your areas in memory.

Typical use cases are:

  • Stop directory of a municipality with accessibility, served routes and hub classification for maps and dashboards.
  • Route network and trips as a basis for accessibility analyses or service frequency evaluations.
  • Filter basis for real-time data: the Stops, Routes and Trips outputs can be wired directly to the GTFS-RT node, which learns its filter lists from them.

How the node works​

1

Trigger and check cache

An incoming message starts the run. If a valid cache exists, the node skips the download.

2

Download and unzip

The ZIP is downloaded via HTTPS into a temporary directory and extracted with the system tool unzip.

3

Filter stops

Every stop from stops.txt is checked with turf against the polygons of the configured areas.

4

Reduce the feed

Using awk and sort, stop_times.txt, trips.txt and shapes.txt are reduced to the trips that call at the filtered stops.

5

Write cache and emit

The entities are written line by line (JSONL) to a cache and then sent through the five outputs.

Result: Five outputs with agencies, stops, routes, trips and stop times for your areas.

The node is trigger-based. On the first trigger it downloads and processes the feed, which can take several minutes for large feeds. Afterwards it answers triggers from the cache until the cache duration has expired, the areas have changed or an update is forced. The cache lives in the temporary directory of the operating system under node-red-gtfs-data/<node id>, survives a deploy and is deleted when the node is removed from the flow.

Configuration​

The edit dialog has three tabs.

Tab "General"​

FieldTypeDefaultMeaning
NameTextemptyDisplay name of the node in the flow.

Tab "Source"​

FieldTypeDefaultMeaning
GTFS URLTextsee belowAddress of the ZIP archive. Only https:// addresses are downloaded; redirects (HTTP 301/302) are followed.
Refresh (h) (refreshInterval)Number24Refresh interval of the cache in hours.
StopTimes Batch (stopTimesBatchSize)Number50000Number of stop time entities per message on output 5. The messages form a sequence with msg.parts.

Default values of the URL fields:

GTFS URL: https://download.gtfs.de/germany/free/latest.zip

Tab "Areas"​

FieldTypeDefaultMeaning
SearchSearch field with suggestionsemptySearches Nominatim (restricted to Germany) for cities, municipalities and districts. A selected hit is added to the list with its boundary polygon.
Area list and map (areas)List with remove button, Leaflet mapemptyThe selected areas as GeoJSON features with display_name, osm_id, osm_type and place_id. The map shows the polygons. The list is stored as JSON text.

At least one area is mandatory. Without an area the node refuses to process so that the complete feed for all of Germany is not generated by accident. Changing the areas invalidates the cache.

Input​

Every message triggers the run. Two properties control the behaviour.

PropertyTypeMeaning
msg.payloadanyTriggers the run. The value "update" forces download and reprocessing even if the cache is still valid.
msg.actionstring"update" forces an update like payload and then emits all outputs. "agency", "stops", "routes", "trips" or "stoptimes" emit only the respective output. Without action all five outputs are served.

Properties of the incoming message are carried over to the output messages.

Output​

All entities carry an id of the form urn:ngsi-ld:Gtfs<Type>:<ID>; characters other than letters, digits, _ and - in the GTFS id are replaced by _. Text values are sanitised: German umlauts and ß are rewritten, non-ASCII characters are encoded as \u sequences, the characters ()<>"'=; are removed. Attributes have the form {"value": …}, numbers, booleans and timestamps additionally a type, relationships the form {"type": "Relationship", "object": "urn:…"}. Missing values get the text unknown.

Output 1: Agency​

An array of GtfsAgency entities in one message; only agencies whose routes operate in the area.

[
{
"id": "urn:ngsi-ld:GtfsAgency:example-transit",
"type": "GtfsAgency",
"name": {"value": "Example Transit Ltd"},
"page": {"value": "https://www.example-transit.org"},
"timezone": {"value": "Europe/Berlin"},
"source": {"value": "https://download.gtfs.de/germany/free/latest.zip"},
"language": {"value": "de"}
}
]
FieldMeaning
name, page, timezone, sourceName, website, time zone of the agency and URL of the feed.
language, phone, fareUrl, emailOnly if present in the feed: language, phone number, fare website and e-mail address of the agency.

Output 2: Stops​

An array of GtfsStop entities in one message: all stops within the areas plus their parent stations.

[
{
"id": "urn:ngsi-ld:GtfsStop:de_07312_1001",
"type": "GtfsStop",
"code": {"value": "unknown"},
"name": {"value": "Example Town Central Station"},
"location": {"value": {"type": "Point", "coordinates": [7.7640, 49.4360]}},
"wheelchairBoarding": {"value": true},
"stopType": {"value": "Stop"},
"parentStationName": {"value": "Example Town Central"},
"routeCount": {"value": 12, "type": "Number"},
"hubCategory": {"value": "Major Interchange"},
"servedByRoutes": {"value": [{"id": "de:vrn:12-345", "shortName": "105", "type": "Bus"}]}
}
]
FieldMeaning
code, name, description, pageStop code, name, optional description and website.
locationGeoJSON point [longitude, latitude], only for valid coordinates.
wheelchairBoardingtrue, false or unknown from wheelchair_boarding.
stopTypeStop, Station or Entrance from location_type.
parentStationNameName of the parent station, if any.
routeCount, hubCategoryNumber of serving routes and the derived classification: from 10 routes "Major Interchange", from 3 "Hub", otherwise "Local Stop".
servedByRoutesList of serving routes with id, short name and mode (Tram, Subway, Rail, Bus, Ferry, …).

Output 3: Routes​

An array of GtfsRoute entities in one message.

[
{
"id": "urn:ngsi-ld:GtfsRoute:de_vrn_12-345",
"type": "GtfsRoute",
"shortName": {"value": "105"},
"name": {"value": "Central Station - University"},
"routeType": {"value": "3"},
"routeColor": {"value": "#E2001A"},
"operatedBy": {"type": "Relationship", "object": "urn:ngsi-ld:GtfsAgency:example-transit"}
}
]
FieldMeaning
shortName, name, description, pageRoute number, route name, optional description and website.
routeTypeGTFS mode code as text (0 tram, 1 subway, 2 rail, 3 bus, 4 ferry, …).
routeColor, routeTextColorRoute colours as hex value with #, if present in the feed.
operatedByRelationship to the agency.

Output 4: Trips​

GtfsTrip entities as a sequence of messages with msg.parts (id, index, type: "array", plus count on the last message), by default 50000 trips per message.

[
{
"id": "urn:ngsi-ld:GtfsTrip:de_vrn_12-345_67",
"type": "GtfsTrip",
"headSign": {"value": "University"},
"shortName": {"value": "unknown"},
"direction": {"value": 0, "type": "Number"},
"block": {"value": "unknown"},
"wheelchairAccessible": {"value": true},
"hasRoute": {"type": "Relationship", "object": "urn:ngsi-ld:GtfsRoute:de_vrn_12-345"},
"hasService": {"type": "Relationship", "object": "urn:ngsi-ld:GtfsService:T0-1"},
"hasShape": {"type": "Relationship", "object": "urn:ngsi-ld:GtfsShape:12-345-H"},
"totalDuration": {"value": 24, "type": "Number"},
"startStopName": {"value": "Example Town Central Station"},
"endStopName": {"value": "University"},
"routeShortName": {"value": "105"},
"routeLongName": {"value": "Central Station - University"},
"routeType": {"value": "Bus"},
"agencyName": {"value": "Example Transit Ltd"},
"serviceDays": {"value": "MoTuWeThFr...."},
"validFrom": {"value": "2026-06-15T00:00:00Z", "type": "DateTime"},
"validTo": {"value": "2026-12-12T00:00:00Z", "type": "DateTime"}
}
]
FieldMeaning
headSign, shortName, direction, block, wheelchairAccessibleDestination sign, short name, direction (0 or 1), block id and accessibility from trips.txt.
hasRoute, hasService, hasShapeRelationships to route, service days and shape.
totalDurationTrip duration in minutes from the first to the last stop within the area. With only one stop in the area or a negative duration, unknown plus isPartial: true.
startStopName, endStopNameNames of the first and last stop within the areas.
routeShortName, routeLongName, routeType, agencyNameDetails of the associated route and agency; routeType here as a name (Bus, Tram, …).
serviceDays, validFrom, validToService days as an abbreviation string (Mo, Tu, … or ..) and validity period from calendar.txt.

Output 5: StopTimes​

GtfsStopTime entities as a sequence of messages with msg.parts, batch size as configured.

[
{
"id": "urn:ngsi-ld:GtfsStopTime:de_vrn_12-345_67_5",
"type": "GtfsStopTime",
"hasTrip": {"type": "Relationship", "object": "urn:ngsi-ld:GtfsTrip:de_vrn_12-345_67"},
"hasStop": {"type": "Relationship", "object": "urn:ngsi-ld:GtfsStop:de_07312_1001"},
"arrivalTime": {"value": "08:17:00"},
"departureTime": {"value": "08:18:00"},
"stopSequence": {"value": 5, "type": "Number"}
}
]
FieldMeaning
hasTrip, hasStopRelationships to trip and stop; for flexible services also hasLocationGroup, hasLocation, hasPickupBookingRule, hasDropOffBookingRule.
arrivalTime, departureTimeArrival and departure time as text HH:MM:SS; values beyond 24:00:00 denote the following day.
stopSequenceOrder of the stop within the trip.
stopHeadsign, pickupType, dropOffType, shapeDistTraveled, timepoint, continuousPickup, continuousDropOff, startPickupDropOffWindow, endPickupDropOffWindowOnly if present and different from the GTFS default value.

Example flow​

The package ships with an example flow. Import it in Node-RED via Menu → Import → Examples → @htag/node-red-contrib-hypertegrity-urbanmobility → gtfs-data. Alternatively download gtfs-data.json and import it via Menu → Import. The example flow contains the complete boundary polygon of the city of Kaiserslautern; in the abridged version below it is replaced by a placeholder.

Step 1

Inject

An Inject node triggers the run daily at 01:00 (cron 00 01 * * *).

Step 2

GTFS

The node downloads the Germany feed from gtfs.de and filters it to the city area of Kaiserslautern.

Step 3

Five Debug nodes

One Debug node each shows Agency, Stops, Routes, Trips and StopTimes in the sidebar.

Show example flow as JSON
[
{"id": "tab1", "type": "tab", "label": "Gtfs-data", "disabled": false, "info": ""},
{"id": "inject1", "type": "inject", "z": "tab1", "name": "01:00 daily", "props": [{"p": "payload"}], "repeat": "", "crontab": "00 01 * * *", "once": false, "onceDelay": "30", "topic": "", "payload": "", "payloadType": "date", "x": 130, "y": 200, "wires": [["gtfs1"]]},
{"id": "gtfs1", "type": "gtfs-data", "z": "tab1", "name": "gtfs.de", "url": "https://download.gtfs.de/germany/free/latest.zip", "refreshInterval": "23", "stopTimesBatchSize": 50000, "areas": "[{\"type\":\"Feature\",\"geometry\":{\"type\":\"Polygon\",\"coordinates\":[[\"<POLYGON-COORDINATES>\"]]},\"properties\":{\"display_name\":\"Kaiserslautern, Rheinland-Pfalz, Deutschland\",\"osm_id\":62652,\"osm_type\":\"relation\"}}]", "x": 350, "y": 200, "wires": [["debug1"], ["debug2"], ["debug3"], ["debug4"], ["debug5"]]},
{"id": "debug1", "type": "debug", "z": "tab1", "name": "Agency", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 540, "y": 120, "wires": []},
{"id": "debug2", "type": "debug", "z": "tab1", "name": "Stops", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 530, "y": 160, "wires": []},
{"id": "debug3", "type": "debug", "z": "tab1", "name": "Routes", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 540, "y": 200, "wires": []},
{"id": "debug4", "type": "debug", "z": "tab1", "name": "Trips", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 530, "y": 240, "wires": []},
{"id": "debug5", "type": "debug", "z": "tab1", "name": "StopTimes", "active": true, "tosidebar": true, "console": false, "tostatus": false, "complete": "payload", "targetType": "msg", "x": 550, "y": 280, "wires": []}
]

Status and errors​

IndicatorMeaning
Blue dot "Downloading..."The ZIP archive is being downloaded.
Yellow ring "Unzipping..."The archive is being extracted with unzip.
Yellow dot "Pass 1: Geographic Stop Filtering..." to "Pass 5: Writing Cache..."The five processing passes: filter stops, pre-filter with awk, load metadata, finalise routes, write cache.
Green dot "Ready: 412 stops"Processing is complete; the number names the stops within the area.
Red ring "Error"Download, extraction or processing failed, for example "Status 404", "Unzip failed: …" or "Missing required column (stop_id/trip_id/stop_sequence) in stop_times.txt or trips.txt".
Red ring "No areas configured"No area is configured. Error message: "No areas configured. Please configure at least one geographic area to prevent downloading data for entire Germany."
Warning "Updating GTFS data..." or "Cache expired (older than 24 hours). Updating GTFS data..."A new download starts because there is no cache, it has expired or an update was forced.
Warning "Areas configuration has changed. Cache will be invalidated."The areas were changed; the cache is rebuilt.
Warning "GTFS data is already processing, please wait..." or "Still processing..."A run is still active; the trigger is discarded.
Error "No data available." or "Cache read failed: …"There is no cache or it cannot be read.

Errors are reported via node.error and can be handled with a Catch node.

Notes​

  • System requirements: The node calls the command line tools unzip, awk, sort, head, tail and rm. It therefore runs only on Linux or Unix systems (including containers) where these tools are installed.
  • Disk space and runtime: The Germany-wide feed from gtfs.de is several hundred megabytes compressed; extracted and during processing, several gigabytes are needed in the temporary directory. Schedule updates for quiet hours, for example at night.
  • Large messages: Trips and stop times are sent in batches with msg.parts. With Debug nodes use the message counter option or limit the output; displaying large arrays in full can overwhelm the browser.
  • Data licence: The gtfs.de feed is subject to the terms stated there (DELFI data, Creative Commons with attribution). Other feeds have their own licences.
  • Personal data: GTFS schedule data contains no personal data. agency.txt may contain the phone number and e-mail address of the agency; these are usually corporate contacts.
  • Times: Stop times are local times in the time zone of the agency; validity dates are emitted as YYYY-MM-DDT00:00:00Z.

References​