Skip to contents

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").