localgov_bus_data
LocalGov Bus Data is a Drupal 10/11 module that imports UK bus timetable data from bulk GTFS feeds, enriches stop data from NaPTAN, and exposes everything as Drupal entities and Views for use on local council websites.
Alpha release. Core functionality is complete and in use but APIs and configuration schema may change before a stable release.
Requirements
- Drupal 10.2+ or 11
- PHP 8.3+
- MySQL 5.7+ or MariaDB 10.3+ (
GROUP_CONCATis used for stop service aggregation â SQLite and PostgreSQL are not supported) -
drupal/migrate_plus^6.0 -
drupal/migrate_source_csv^3.0 -
drupal/geofield^1.0 (including thegeofieldsubmodule) -
drupal/leaflet^10.0 (including theleaflet_viewsandleaflet_markerclustersubmodules)
All dependencies are declared in localgov_bus_data.info.yml and enabled automatically when the module is installed via Composer.
Install with Composer
composer require localgovdrupal/localgov_bus_data
drush en localgov_bus_data -y
drush cr
Submodules
localgov_bus_data_homepage
An optional submodule that provides a public-facing landing page at /buses with:
- Configurable intro text
- Route number search form
- Stop name search form
- Interchange and bus station quick-links
- Area browse widget (lists localities grouped by configurable area, driven by NaPTAN locality data)
Note: The interchange links and area browse sections rely on
bustimes_localitydata, which is only populated after NaPTAN enrichment runs. Enable NaPTAN in the main settings before enabling this submodule.
drush en localgov_bus_data_homepage -y
Configure at /admin/config/localgov-bus-data/homepage.
Public pages
The module has five visitor-facing page displays:
Path View Description/buses/routes
localgov_bus_routes
Lists all routes â service number and name. Exposed filter to search by route number.
/buses/routes/%/%
localgov_bus_timetable
Timetable grid for a route. The two % segments are agency ID and route short name. Exposed filters for day (MonâFri / Sat / Sun) and direction (Outbound / Inbound). Uses the custom BusTimetableStyle plugin to render a stop à trip matrix. A Leaflet map attachment displays all stops in the filtered result set as markers below the timetable.
/buses/stops
localgov_bus_stop_search
Stop search â type a stop name or ATCO code. When NaPTAN enrichment has run, results include indicator, street and locality (e.g. Church Street (opp) Market Road, Carlisle).
/buses/stops/%
localgov_bus_stop_detail
Scheduled departures for a single stop. The % segment is the ATCO stop code. Exposed filter for day. A Leaflet map attachment displays the stop's location as a marker below the departures table.
/buses/map
localgov_bus_stop_map
All stops with lat/lon coordinates (table format).
Configuration
- All settings live in
localgov_bus_data.settingsconfig and are managed via the settings form at/admin/config/localgov-bus-data/settings. There are no council-specific defaults â all values must be configured after installation.
source.bulk_gtfs_url
(empty)
URL to a regional bulk GTFS ZIP from the BODS timetable download page
source.boundary_geojson
(empty)
GeoJSON Polygon or MultiPolygon used to filter the feed to your area (takes precedence over bounding box)
source.bounding_box
(empty)
N/S/E/W coordinates used to filter the national feed to your area
source.enabled
false
Enable to allow imports to run (cron and manual)
import.schedule
0 3 * * *
Cron expression for automatic daily import (supports * and exact integers only â not ranges or steps)
import.log_retention
30
Days to keep import log entries
naptan.enabled
false
Enable NaPTAN enrichment step
naptan.url
(empty)
NaPTAN access-nodes CSV URL
Data source
This module is designed for use with the UK Department for Transport Bus Open Data Service (BODS).
Select the regional bulk GTFS ZIP for your area and paste the URL into the settings form.
Set the geographic filter to your council's area â either a GeoJSON polygon boundary (most accurate) or a bounding box. Stops outside the area are discarded after download; trips and stop times that no longer serve any in-area stop are also removed. Cross-boundary trips are retained in full.
Importing timetable data
Live import
Run an incremental import (add new records, update changed records, delete removed records) from the national bulk GTFS feed:
drush bus-times:import
Force a full truncate-and-reimport (same as the very first import):
drush bus-times:import --full
Or click Import now on the settings page (/admin/config/localgov-bus-data/settings).
The web UI button always performs a full truncate-and-reimport.
Incremental import pipeline (default Drush/cron path)
- Downloads the bulk GTFS ZIP from the configured URL
- Extracts and filters stops/trips to the configured bounding box
- Stages the filtered CSV files to
public://bus-times/gtfs/ - Runs all five Migrate migrations â only new and changed rows are processed (
track_changes: truein each migration YAML hashes source rows and skips unchanged ones) - Detects and removes deleted records: compares each staged CSV against the migrate map tables and deletes entity rows for source IDs that no longer appear in the feed
- Writes an entry to the import log
Full import pipeline (--full flag or web UI)
Same pipeline but step 4 is preceded by a complete truncation of all entity and migrate map tables, ensuring a clean slate regardless of prior state.
Scheduled imports (cron)
The module hooks into Drupal's standard cron. When cron fires, hook_cron() checks the configured schedule expression (default 0 3 * * * â 3 am daily) and runs the import pipeline if the schedule matches.
Note: The cron expression only supports
*(any value) and exact integers in each field. Ranges (1-5), steps (*/15), and lists (1,3,5) are not supported.
Cron import mode:
- Incremental (default) â runs on every matching cron fire
-
Full â automatically triggered once per week (when more than 6 days have passed since the last full import, tracked in Drupal state as
localgov_bus_data.cron_last_full_import)
This means routine daily cron runs are fast (only changed rows processed), while a weekly full re-import keeps the dataset clean and prevents map-table drift.
To run cron reliably in production, configure a server-level crontab entry:
# Example: run Drupal cron every 15 minutes
*/15 * * * * /path/to/drush --root=/path/to/drupal cron
The module's own import schedule controls when within those cron fires the import actually runs â it will be skipped on cron runs that don't match the expression. An overlapping-run guard (Drupal state key localgov_bus_data.cron_last_run, minimum 3 600 s gap) prevents a second import starting before the first has finished.
NaPTAN stop enrichment
After a timetable import, stop records can be enriched with NaPTAN data (indicator, street, and locality name). Enable this on the settings page under NaPTAN Settings, then click Update stop details now. Enrichment also runs automatically as the final step of each scheduled import when enabled.
The NaPTAN endpoint downloads the full GB access nodes CSV and matches stops by ATCO code.
No API key is required.
Seed data (local development)
For local development with realistic data, use the seed command to insert fixture entities without running a live import:
drush bus-times:seed
This inserts 5 routes, 26 stops, 3 calendars, 40 trips, and ~200 stop times (MonâFri, Saturday, Sunday schedules). Safe to re-run. Useful immediately after enabling the module to try out the public pages and Views without a live download.
Admin paths
Settings and tools:
-
/admin/config/localgov-bus-data/settingsâ GTFS URL, bounding box, import schedule, display defaults, and NaPTAN enrichment settings -
/admin/config/localgov-bus-data/import-logâ recent import run history
Content (data browsing):
-
/admin/content/bus-dataâ landing page, redirects to Bus Stops - Sub-tabs: Bus Routes, Bus Stops, Bus Calendars, Bus Trips, Bus Stop Times
Structure (field management):
/admin/structure/bus-data/stops/admin/structure/bus-data/routes/admin/structure/bus-data/calendars/admin/structure/bus-data/trips/admin/structure/bus-data/stop-times
This module was sponsored by Cumberland Council and contributed as part of the LocalGov Drupal initiative.