Introduction
OpenStreetMaps (OSM) is an important data source for transit analysis, due to its rich, open, and detailed geographic data. GTFShift includes some methods that allow to access its information directly.
Download bus lanes
Dedicated bus lanes can improve bus transit operation. Understanding
their spatial distribution is important to study operation dynamics.
osm_bus_lanes
allows to obtain the bus lanes network on
OpenStreetMaps for a given area.
aml = sf::st_read("https://github.com/U-Shift/MQAT/raw/refs/heads/main/geo/MUNICIPIOSgeo.gpkg", quiet = TRUE)
lisboa = aml |> dplyr::filter(Concelho == "Lisboa") |> sf::st_bbox()
bus_lanes = GTFShift::osm_bus_lanes(lisboa) |> select(osm_id)
mapview::mapview(bus_lanes, layer.name = "Bus lanes")
Get centerlines for OSM road network
Performing an aggregated analysis for the spatial distribution of the route frequencies might be easier if the routes are projected over a simplified road network (refer to vignette(“analyse”) for more details).
GTFShift::osm_centerlines()
allows to generate this
simplification by creating the centerlines for the road network exported
from OpenStreetMaps, using Python neatnet package.
Original network
library(osmdata)
road_osm = opq("Arroios, Lisboa, Portugal") |>
add_osm_feature(key = "highway", value = c("motorway", "trunk", "primary", "secondary", "tertiary", "residential", "unclassified", "living_street")) |>
add_osm_feature(key = "area", value = "!yes") |>
osmdata_sf() |>
osm_poly2line()
road_osm = road_osm$osm_lines
mapview::mapview(road_osm)
Simplified network
centerlines = GTFShift::osm_centerlines(place="Arroios, Lisboa, Portugal")
#> Using Python: /usr/bin/python3.12
#> Creating virtual environment '~/.virtualenvs/r-reticulate' ...
#> Done!
#> Installing packages: pip, wheel, setuptools
#> Installing packages: numpy
#> Virtual environment '~/.virtualenvs/r-reticulate' successfully created.
#> Using virtual environment '/home/runner/.virtualenvs/r-reticulate' ...
mapview::mapview(centerlines)
Get OSM bus routes
OpenStreetMaps defines bus routes as a relation of ways (usually roads) and nodes (stops and platforms). GTFShift provides methods to use them in the GTFS analysis.
For demonstration purposes, the next snippets will use the Lisbon urban bus network.
# Get GTFS from library GTFS database for Portugal
data = read.csv(system.file("extdata", "gtfs_sources_pt.csv", package = "GTFShift"))
gtfs_id = "lisboa"
gtfs = GTFShift::load_feed(data$URL[data$ID == gtfs_id], create_transfers=FALSE)
# Build OSM query
library(osmdata)
q = opq("Lisbon") |>
add_osm_feature(key = "route", value = c("bus", "tram")) |>
add_osm_feature(key = "network", value = "Carris", key_exact = TRUE)
Matching GTFS ids
GTFS routes shapes and OSM bus routes are linked through OSM
gtfs:*
keys. GTFShift::osm_shapes_to_routes()
and GTFShift::osm_trips_to_routes()
allow to query OSM for
the routes matching the feed trips, given, respectively, their shape or
trip id.
# Subset feed for some routes only, for demonstration purposes
gtfs_794 = GTFShift::filter_by_route_name(gtfs, list("794"))
# Match shapes geometry
shapes_geometry_osm = GTFShift::osm_shapes_to_routes(gtfs_794, q)
shapes_geometry_osm
#> Simple feature collection with 2 features and 2 fields
#> Geometry type: MULTILINESTRING
#> Dimension: XY
#> Bounding box: xmin: -9.133896 ymin: 38.70714 xmax: -9.099827 ymax: 38.76858
#> Geodetic CRS: WGS 84
#> # A tibble: 2 × 3
#> shape_id osm_id geometry
#> <chr> <chr> <MULTILINESTRING [°]>
#> 1 115_0_ASC_shp 15470713 ((-9.13309 38.70745, -9.133067 38.70747), (-9.133067 …
#> 2 115_0_DESC_shp 15470712 ((-9.099841 38.76819, -9.09986 38.76797, -9.099882 38…
GTFS shapes
shapes_sf = tidytransit::shapes_as_sf(gtfs_794$shapes)
mapview::mapview(shapes_sf, zcol = "shape_id", legend = TRUE, layer.name="GTFS shapes")
OSM routes
mapview::mapview(shapes_geometry_osm, zcol = "shape_id", legend = TRUE, layer.name="OSM routes")
Matching shapes geometry
Despite existing, OSM gtfs:*
keys are not widely used.
In July 2025, only 3.1% of relations tagged as route=bus
had the gtfs:shape_id
key set (9 784 of 312 049).
To overcome this issue,
GTFShift::osm_shapes_match_routes()
performs the
association between the OSM bus routes and the GTFS shapes considering,
for each route, the start and end points and total length.
# Subset feed for some routes only, for demonstration purposes
gtfs_subset = GTFShift::filter_by_route_name(gtfs, list("736", "750", "15E", "65B"))
# Match shapes geometry
shapes_match_routes = GTFShift::osm_shapes_match_routes(gtfs_subset, q)
summary(shapes_match_routes)
#> shape_id osm_id distance_diff points_diff
#> Length:14 Length:14 Min. : 5.099 Min. : 7.965
#> Class :character Class :character 1st Qu.:21.147 1st Qu.:13.605
#> Mode :character Mode :character Median :30.634 Median :19.372
#> Mean :33.967 Mean :24.330
#> 3rd Qu.:40.034 3rd Qu.:29.309
#> Max. :72.797 Max. :49.602
#> route_short_name route_long_name geometry
#> Length:14 Length:14 MULTILINESTRING:14
#> Class :character Class :character epsg:4326 : 0
#> Mode :character Mode :character +proj=long... : 0
#>
#>
#>
# Visualize results
shapes_match_routes$map_name = paste(
shapes_match_routes$route_short_name,
" | ",
shapes_match_routes$shape_id,
" | ",
shapes_match_routes$osm_id
)
mapview::mapview(shapes_match_routes, zcol = "map_name", legend = TRUE, layer.name="route_short_name | shape_id | osm_id")
Updating OSM routes with shape_id
The association between OSM route relations id and the GTFS shapes_id
returned by GTFShift::osm_shapes_match_routes()
can be used
to update OpenStreetMaps dataset.
Python library OsmApi enables
to perform this batch process using OpenStreetMaps API. GTFShift
provides a script to perform this operation using OsmApi. Check it at
the repository, at inst/python/OSM_routes_add_shapes.py
, or
find its location on your computer, using
system.file("python", "osm_routes_add_shapes.py", package = "GTFShift")
.