Projects each real-time vehicle position to its corresponding trip geometry, computes cumulative distance along the geometry, and derives segment speed between consecutive updates.
Usage
rt_average_speed(
rt_collection,
trips_geometries,
rt_collection_trips_geometries_match_col = "trip_id",
geometry_sample_meters = 10,
metric_crs = 3857
)Arguments
- rt_collection
sf data.frame with GTFS-RT updates for multiple trips. Must include at least
trip_idandtimestampcolumns.- trips_geometries
sf data.frame with trip geometries. Geometry must be LINESTRING.
- rt_collection_trips_geometries_match_col
Character (Default
"trip_id"). Column name present in bothrt_collectionandtrips_geometriesused to match updates to trip geometry.- geometry_sample_meters
Numeric (Default 10). Sampling step used when projecting points along trip geometry and estimating cumulative distance.
- metric_crs
Integer or character (Default 3857). Projected CRS used to compute distances and speeds.
Value
sf data.frame. Object based on rt_collection, with added columns:
- closest_on_shape
Projected point on trip geometry.
- distance_to_closest_on_geometry
Distance from each update point to its projected location on the shape (meters).
- distance_along_geometry
Cumulative distance along trip geometry (meters).
- distance_along_geometry_reversed
Cumulative distance from shape end to projected location (meters).
- time_since_prev_sec
Elapsed time since previous update (seconds).
- distance_since_prev_meters
Distance increment since previous update (meters).
- speed_kmh
Estimated speed between consecutive updates (km/h).
Details
For each trip (grouped by trip_id), let \(\{(x_i, t_i)\}_{i=1}^n\)
denote the ordered sequence of
real-time observations, where \(x_i\) is the vehicle position and
\(t_i\) the corresponding timestamp, with
\(t_1 \le t_2 \le \dots \le t_n\). Each observation is projected onto the
trip geometry using GTFShift::project_points_along_geometry(), yielding
a projected point \(\hat{x}_i\) and two cumulative distances:
$$d_i = \text{distance\_along\_geometry}(\hat{x}_i)$$
$$d_i^{\mathrm{rev}} = \text{distance\_along\_geometry\_reversed}(\hat{x}_i)$$
For each pair of consecutive observations \((i-1, i)\), the elapsed time is computed as $$\Delta t_i = t_i - t_{i-1}.$$
The distance increment is defined as the minimum of the forward and reversed cumulative-distance differences: $$\Delta d_i^{\mathrm{fwd}} = \left| d_i - d_{i-1} \right|$$ $$\Delta d_i^{\mathrm{rev}} = \left| d_i^{\mathrm{rev}} - d_{i-1}^{\mathrm{rev}} \right|$$ $$\Delta d_i = \min\left(\Delta d_i^{\mathrm{fwd}}, \Delta d_i^{\mathrm{rev}}\right).$$
Trips with fewer than 2 updates are ignored with a warning. The distance increment is defined as the minimum of two alternative cumulative-distance differences: $$\Delta d_i^{\mathrm{fwd}} = \left| d_i - d_{i-1} \right|$$ $$\Delta d_i^{\mathrm{circ}} = \left| d_i - d_{i-1}^{\mathrm{rev}} \right|$$ $$\Delta d_i = \min\left(\Delta d_i^{\mathrm{fwd}}, \Delta d_i^{\mathrm{circ}}\right).$$
The second term is a redundancy designed to avoid overstating movement on circular geometries. In particular, after a vehicle completes a loop, a forward comparison may treat two nearby physical positions as far apart in cumulative distance if the geometry origin has been crossed. Comparing \(d_i\) against \(d_{i-1}^{\mathrm{rev}}\) provides an auxiliary distance candidate that helps avoid overstating movement in that situation.
Average speed is then estimated by $$v_i = \frac{\Delta d_i}{\Delta t_i}$$ and reported in kilometers per hour as $$v_i^{\mathrm{km/h}} = \frac{\Delta d_i}{1000} \cdot \frac{3600}{\Delta t_i}.$$
Trips with fewer than two observations are ignored with a warning because \(\Delta t_i\) and \(\Delta d_i\) are undefined in that case.
Method GTFShift::multiline_to_sorted_linestring() can be used to convert MULTILINESTRING
geometries to LINESTRING if needed.
Examples
# Get GTFS-RT data collection
rt_collect_file <- system.file(
"extdata/samples", "gtfs_rt_sample_tcb_4_4-CS-TERM.csv", package = "GTFShift"
)
rt_collection <- read.csv(rt_collect_file) |>
sf::st_as_sf(coords = c("longitude", "latitude"), crs = 4326) |> dplyr::select(-speed)
head(rt_collection |> dplyr::select(trip_id, timestamp, geometry))
#> Simple feature collection with 6 features and 2 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -9.07846 ymin: 38.63614 xmax: -9.0315 ymax: 38.65217
#> Geodetic CRS: WGS 84
#> trip_id timestamp geometry
#> 1 20260515_DUPE_4-CS-TERM_0_DUPE_18_0650 1778825942 POINT (-9.04843 38.6428)
#> 2 20260514_DUPE_4-CS-TERM_0_DUPE_18_0640 1778738341 POINT (-9.07846 38.65217)
#> 3 20260515_DUPE_4-CS-TERM_0_DUPE_18_0650 1778823962 POINT (-9.03222 38.63645)
#> 4 20260514_DUPE_4-CS-TERM_0_DUPE_18_0640 1778737742 POINT (-9.05829 38.64186)
#> 5 20260515_DUPE_4-CS-TERM_0_DUPE_18_0810 1778832003 POINT (-9.07844 38.65209)
#> 6 20260515_DUPE_4-CS-TERM_0_DUPE_18_0650 1778827442 POINT (-9.0315 38.63614)
nrow(rt_collection)
#> [1] 10
# Get route geometry for data collected
osm_routes <- sf::st_read(
system.file("extdata/samples", "osm_routes_tcb.gpkg", package = "GTFShift"),
quiet = TRUE
) |>
dplyr::filter(route_id %in% rt_collection$route_id) |>
dplyr::mutate(geom = GTFShift::multiline_to_sorted_linestring(geom, metric_crs = 3763))
head(osm_routes)
#> Simple feature collection with 1 feature and 3 fields
#> Geometry type: LINESTRING
#> Dimension: XY
#> Bounding box: xmin: -9.078595 ymin: 38.6307 xmax: -9.03216 ymax: 38.65478
#> Geodetic CRS: WGS 84
#> osm_id shape_id route_id geom
#> 1 18957507 4-CS-TERM 4_4-CS-TERM LINESTRING (-9.032493 38.63...
# Compute average speed (aggregated at route level) based on cumulative distance along the geometry
speed <- GTFShift::rt_average_speed(
rt_collection = rt_collection,
trips_geometries = osm_routes,
rt_collection_trips_geometries_match_col = "route_id",
metric_crs = 3763 # Make sure to addapt to the projection that better suits your location
)
#> Warning: Trip 4_4-CS-TERM has less than 2 updates. Ignoring it.
head(speed |>
dplyr::filter(!is.na(speed_kmh)) |>
dplyr::select(
trip_id, timestamp, speed_kmh,
distance_along_geometry, distance_to_closest_on_geometry
)
)
#> Simple feature collection with 6 features and 5 fields
#> Geometry type: POINT
#> Dimension: XY
#> Bounding box: xmin: -82292.68 ymin: -114578.7 xmax: -78322.61 ymax: -112380.6
#> Projected CRS: ETRS89 / Portugal TM06
#> # A tibble: 6 × 6
#> trip_id timestamp speed_kmh distance_along_geome…¹ distance_to_closest_…²
#> <chr> <int> <dbl> <dbl> <dbl>
#> 1 20260514_DU… 1.78e9 18.1 6465. 8.75
#> 2 20260514_DU… 1.78e9 20.4 2385. 8.70
#> 3 20260514_DU… 1.78e9 61.9 321. 1.16
#> 4 20260515_DU… 1.78e9 14.2 3388. 6.87
#> 5 20260515_DU… 1.78e9 16.8 0 13.1
#> 6 20260515_DU… 1.78e9 17 2546. 0.120
#> # ℹ abbreviated names: ¹distance_along_geometry,
#> # ²distance_to_closest_on_geometry
#> # ℹ 1 more variable: geometry <POINT [m]>
nrow(speed)
#> [1] 9
