| Title: | Processing of GNSS Streams from Sports Watches |
| Version: | 1.0.0 |
| Description: | Tools to read, clean and standardise GNSS (Global Navigation Satellite System) streams recorded by sports watches and similar devices, with a focus on trail and mountain running. The pipeline reads GPX files, applies a naming convention, sanitises coordinates (Kalman, LOESS, Gaussian kernel or Savitzky-Golay smoothing, and red flags for implausible readings), resamples the trajectory to a uniform spatial resolution derived from the Nyquist criterion, and replaces elevation with values from a Digital Elevation Model. Derived measures include speed, slope, elevation gain and kilometre-effort, Tobler hiking function parameters fitted by quantile regression, comparison between streams, detection of passes along a route and map matching to a known reference trail by dynamic programming. Methods follow Sanchez and Villena (2020) <doi:10.1177/1754337120918975>, Sanchez et al. (2024) <doi:10.1109/ICECET61485.2024.10698606>, Sanchez et al. (2025) <doi:10.1177/17543371251341660> and Martinez, Sanchez and Diaz (2026) <doi:10.1109/ICECET65726.2026.11632531>. |
| URL: | https://github.com/raimun2/metrictrails |
| BugReports: | https://github.com/raimun2/metrictrails/issues |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| LazyData: | true |
| Depends: | R (≥ 4.1) |
| Imports: | magrittr, sf, stats, terra, tibble, utils |
| Suggests: | curl, knitr, rmarkdown, testthat (≥ 3.0.0) |
| VignetteBuilder: | knitr |
| Config/testthat/edition: | 3 |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | no |
| Packaged: | 2026-10-11 10:28:13 UTC; raimu |
| Author: | Raimundo Sanchez |
| Maintainer: | Raimundo Sanchez <raimundosanchezu@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-21 15:20:02 UTC |
metrictrails: Processing of GNSS Streams from Sports Watches
Description
Tools to read, clean and standardise GNSS streams recorded by sports watches and similar devices, with a focus on trail and mountain running.
A stream is a sequence of consecutive readings with lon and lat and,
optionally, ele, timestamp, time and other variables (heart rate,
cadence, ...). The recommended pipeline is:
-
read_stream(): read a GPX file. -
uniform_stream(): apply the naming convention. -
sanitize_stream(): smooth the coordinates and flag implausible readings. -
resample_stream(): resample to a uniform spatial resolution derived from the Nyquist criterion (resolution_stream()). -
ele_correction(): replace elevation with values from a Digital Elevation Model (get_dem(),resample_dem()).
Once the stream is tidy:
-
differential_stream(),summarise_stream(),fractal_stream()andtobler_stream()compute derived measures; -
project_stream()adds UTM coordinates andlocate_stream()projects readings on a reference (linear referencing); -
compare_stream()anddetect_stream()compare a stream with another stream or route; -
match_stream()aligns a stream along a reference trail it is known to follow; -
write_stream()writes the result to GPX.
Author(s)
Maintainer: Raimundo Sanchez raimundosanchezu@gmail.com (ORCID)
Authors:
Raimundo Sanchez raimundosanchezu@gmail.com (ORCID)
See Also
Useful links:
Report bugs at https://github.com/raimun2/metrictrails/issues
Pipe operator
Description
See magrittr::%>% for details.
Usage
lhs %>% rhs
Arguments
lhs |
A value or the magrittr placeholder. |
rhs |
A function call using the magrittr semantics. |
Value
The result of calling rhs(lhs).
Compare two streams
Description
Spatial agreement between two streams, based on the distance from every
reading of one stream to the nearest reading of the other (in the UTM zone
of a). Directed measures are reported in both directions (_ab: from a
to b; _ba: from b to a):
-
hausdorff: maximum nearest-neighbour distance (m); the symmetric value is the larger of both directions. -
separation: median nearest-neighbour distance (m). -
metric_score: share of readings whose nearest-neighbour distance is at mostthreshold(%).
Separation follows the implementation used in Sanchez, Nieto-Jimenez &
Riveros-Matthey (2025): the median of the directed nearest-neighbour
distances. Streams should be resampled to the same resolution before
comparing them (see resample_stream()).
Usage
compare_stream(a, b, threshold = 1, dims = 2)
Arguments
a, b |
Streams with |
threshold |
Distance (m) used by the metric score. |
dims |
|
Value
A one-row tibble.
References
Sanchez, R., Nieto-Jimenez, C., & Riveros-Matthey, C. (2025). High-precision mapping of mountain terrain for human locomotion research: A multimodal geospatial framework. 2025 15th IEEE International Conference on Pattern Recognition Systems (ICPRS). doi:10.1109/ICPRS66293.2025.11302827
Examples
act = read_stream(system.file("extdata", "gpx_activity.gpx", package = "metrictrails"))
route = read_stream(system.file("extdata", "gpx_route.gpx", package = "metrictrails"))
compare_stream(resample_stream(act, res = 2), resample_stream(route, res = 2))
Detect the passes of a stream along a route
Description
Predicate and locator: does a stream follow a route, and where? The stream
is not transformed (compare with match_stream(), which aligns a stream
known to follow a route).
The procedure follows the former GPStream match_stream(), in metres:
the stream must come within
toleranceof the route;at least
min_completionof the route readings must lie withintoleranceof the stream;every time the stream comes within
toleranceof the start (or of the end) of the route a candidate pass begins; it ends at the first arrival withintoleranceof the opposite end of the route after covering half of the route length (or, if there is none, after the route length plustolerance);the coverage of a pass is the share of route readings within
toleranceof the pass; passes with coverage of at leastmin_coverageare"match", the others"partial";the direction of a pass is
"forward"when the order of the readings agrees with the order of their nearest route readings (positive Spearman correlation), otherwise"reverse".
Distances are measured between readings, so both streams should have a
resolution finer than tolerance (see resample_stream()).
Usage
detect_stream(
data,
route,
tolerance = 50,
min_completion = 1,
min_coverage = 0.97,
min_partial = 0.25
)
Arguments
data |
A stream with |
route |
The route, with |
tolerance |
Distance (m) within which the stream is on the route. |
min_completion |
Share of the route that must be covered by the whole stream. |
min_coverage |
Share of the route that a pass must cover to be a
|
min_partial |
Passes covering less than this share of the route are not reported. Overlapping passes are reduced to the one with the highest coverage. |
Value
A tibble with one row per pass: pass, start_row, end_row
(rows of data), direction, coverage and status. It has no rows
when the stream does not follow the route; the reason is given in
attr(x, "diagnosis").
See Also
match_stream(), compare_stream()
Examples
act = read_stream(system.file("extdata", "gpx_activity.gpx", package = "metrictrails"))
route = read_stream(system.file("extdata", "gpx_route.gpx", package = "metrictrails"))
detect_stream(act, route)
Spatial and temporal derivatives of a stream
Description
Adds, for every reading, the differences with the previous reading of the same segment and the quantities derived from them.
Always computed:
-
delta_d: horizontal (2D) distance (m), anddistance, its cumulative sum along the stream (distance between segments is not counted). -
turn_angle: absolute change of heading between the previous and the current step (degrees, 0 to 180).
With ele: delta_ele, dplus and dminus (elevation gained and lost,
both positive), delta_d3d (3D distance) and slope (delta_ele / delta_d,
rise over run; NA when the reading is idle).
With time: delta_time, speed (horizontal, m/s), accel (change of
speed over time, m/s^2), pace (min/km); with ele also vert_speed,
vert_accel and speed_3d.
The first reading of every segment gets 0 for differences and NA for
rates.
Usage
differential_stream(data, distance = c("utm", "haversine"), idle = 0.1)
Arguments
data |
A stream with |
distance |
How horizontal distance is computed: |
idle |
Horizontal distance (m) below which slope and heading are not defined. |
Value
The stream with the new columns.
References
Sanchez, R., Nieto-Jimenez, C., & Riveros-Matthey, C. (2025). High-precision mapping of mountain terrain for human locomotion research: A multimodal geospatial framework. 2025 15th IEEE International Conference on Pattern Recognition Systems (ICPRS). doi:10.1109/ICPRS66293.2025.11302827
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
stream = read_stream(f) %>% uniform_stream() %>% differential_stream()
summary(stream$speed)
Replace the elevation of a stream with values from a DEM
Description
Extracts the elevation of every reading from a Digital Elevation Model and
replaces the raw elevation (or adds it as ele_dem). Elevation from an open
DEM gives consistent elevation gain across devices, independently of the
altitude sensor (Sanchez & Villena, 2020). This is the last step of the
pipeline, after resample_stream().
Usage
ele_correction(
data,
dem = NULL,
z = 14,
extract = c("bilinear", "simple"),
replace = TRUE
)
Arguments
data |
A stream with |
dem |
A |
z |
Zoom level used when |
extract |
|
replace |
If |
Value
The stream with the new elevation.
References
Sanchez, R., & Villena, M. (2020). Comparative evaluation of wearable devices for measuring elevation gain in mountain physical activities. Proceedings of the Institution of Mechanical Engineers, Part P: Journal of Sports Engineering and Technology, 234(4), 312-319. doi:10.1177/1754337120918975
Examples
f = system.file("extdata", "gpx_route.gpx", package = "metrictrails")
dem = system.file("extdata", "test_dem.tif", package = "metrictrails")
stream = ele_correction(read_stream(f), dem = dem, replace = FALSE)
summary(stream$ele - stream$ele_dem)
Fractal dimension of a stream
Description
Estimates the fractal dimension of the horizontal path of a stream with the
divider (Richardson) method. A divider of size r is walked along the path:
from the current point, the next point is the first point of the path at a
straight-line distance r. The measured length is
L(r) = N(r) \cdot r + \textrm{remainder}, and for a fractal curve
L(r) \propto r^{1 - D}, so the dimension is
D = 1 - b, where b is the slope of the least-squares regression of
\log L(r) on \log r.
A straight line has D = 1; values above 1 measure how much length is
gained when the path is measured with finer steps, i.e. the geometric
complexity that makes distance and elevation gain depend on the spatial
resolution (Sanchez et al., 2025). Only horizontal 2D distance (UTM) is
used. Each segment is walked separately and their lengths are added.
By default the divider sizes are n_scales values evenly spaced in log
scale between twice the median distance between moving readings (finer
dividers would only measure the straight segments between readings) and one
tenth of the path length.
Usage
fractal_stream(data, scales = NULL, n_scales = 10)
Arguments
data |
A stream with |
scales |
Optional vector of divider sizes in metres. |
n_scales |
Number of divider sizes when |
Value
A list with dimension (the estimate), r_squared of the
log-log regression, and scaling, a tibble with the divider size r,
the number of steps n and the length length (m).
References
Mandelbrot, B. (1967). How long is the coast of Britain? Statistical self-similarity and fractional dimension. Science, 156(3775), 636-638. doi:10.1126/science.156.3775.636
Sanchez, R., Egli, P. E., Jornet, K., Duggan, M., & Besomi, M. (2025). How fractal complexity distorts distance and elevation gain in trail and mountain running: The case for course measurement standardization. Proceedings of the Institution of Mechanical Engineers, Part P: Journal of Sports Engineering and Technology. doi:10.1177/17543371251341660
See Also
summarise_stream(), resample_stream()
Examples
f = system.file("extdata", "gpx_route.gpx", package = "metrictrails")
fd = fractal_stream(read_stream(f))
fd$dimension
fd$scaling
Download a Digital Elevation Model for a stream
Description
Downloads the Terrain Tiles (GeoTIFF) hosted on Amazon Web Services that
intersect a corridor of buffer metres around the stream, mosaics them and
projects the result to the UTM zone of the stream. Only the tiles along the
path are downloaded, not every tile of its bounding box; cells of the
bounding box outside those tiles are NA. The tiles combine SRTM and other sources; their
resolution depends on the zoom level z. The default, z = 14, is the
finest level (about 4 m at the latitude of Santiago, Chile, and 4.8 m at the
equator) and is used for every stream regardless of its extent, so that
elevations are always comparable; large areas simply need more tiles.
When res is given, the DEM is refined (or coarsened) with
resample_dem(). Refining a public DEM with bilinear interpolation to
sub-metre resolution reduces elevation gain errors to levels comparable to
a LiDAR model (Sanchez et al., 2024).
Usage
get_dem(
data,
z = 14,
res = NULL,
method = "bilinear",
buffer = 500,
max_tiles = Inf
)
Arguments
data |
A stream with |
z |
Zoom level of the tiles, from 1 to 14 (default 14). |
res |
Optional target resolution in metres, passed to |
method |
Resampling method passed to |
buffer |
Width (m) of the corridor around the stream. |
max_tiles |
Optional maximum number of tiles to download ( |
Value
A terra::SpatRaster in the UTM zone of the stream.
References
Sanchez, R., Egli, P., Besomi, M., & Truffello, R. (2024). Assessing the impact of Digital Elevation Model resolution on elevation gain estimations in trail running. 2024 International Conference on Electrical, Computer and Energy Technologies (ICECET). doi:10.1109/ICECET61485.2024.10698606
Terrain Tiles on AWS: https://registry.opendata.aws/terrain-tiles/
Examples
f = system.file("extdata", "gpx_route.gpx", package = "metrictrails")
dem = get_dem(read_stream(f))
dem
Locate a stream along a reference
Description
Linear referencing: every reading of data is projected orthogonally onto
the polyline of reference, giving its position along the reference and its
signed distance to it. Unlike match_stream(), each reading is located
independently, without monotonicity or speed constraints, so noisy readings
can jump back and forth along the reference.
Each reading is projected on its nearest segment of the reference. Distances are measured in the UTM zone of the reference.
Usage
locate_stream(data, reference)
Arguments
data |
A stream with |
reference |
The reference, with |
Value
The stream with:
-
s_ref: distance along the reference of the projected point (m); -
offset_cross: signed distance to the reference (m, positive to the left of the reference direction); -
lon_ref,lat_ref: coordinates of the projected point.
See Also
match_stream(), detect_stream()
Examples
act = read_stream(system.file("extdata", "gpx_activity.gpx", package = "metrictrails"))
route = read_stream(system.file("extdata", "gpx_route.gpx", package = "metrictrails"))
located = locate_stream(act, route)
summary(abs(located$offset_cross))
Match a stream to a known reference trail
Description
Temporal alignment of a stream along a reference trail that the athlete is known to have followed (for example, rounds on a trail measured with a high-precision protocol). For every reading the function estimates where the athlete was along the reference, under the constraints that the athlete never moves backwards and never exceeds a physiologically plausible speed for the local slope.
The unknown is the sequence of reference vertices S = (j_1, \dots, j_n),
non-decreasing along the direction of travel, that minimises
J(S) = \sum_i d^2(obs_i, ref_{j_i}) + \lambda^{-2} \sum_i pen(j_{i-1}, j_i, \Delta t_i)
where d is the horizontal (or 3D) distance and pen the squared
excess of the horizontal, vertical and total speeds along the reference over
the Tobler ceiling (tobler_speed() * tol_factor). Transitions whose total
speed exceeds the ceiling (with slope clipped to slope_clip) are not
allowed; staying on the same vertex is always allowed, so a solution always
exists. Because J is a sum of terms that depend on one reading or on
two consecutive readings, the global minimum is found exactly with dynamic
programming (Viterbi algorithm).
The reference should come out of the full pipeline (sanitized, resampled at a fine resolution, with DEM elevation), because the matched stream takes its coordinates and elevation from it.
Usage
match_stream(
data,
reference,
params,
direction = c("auto", "forward", "reverse"),
by = NULL,
dims = 2,
lambda = 1,
tol_factor = 1.1,
slope_clip = 0.6,
max_cells = 5e+07
)
Arguments
data |
A stream with |
reference |
The reference trail, with |
params |
Tobler parameters, output of |
direction |
Direction of travel along the reference: |
by |
Optional name of a column splitting |
dims |
|
lambda |
Weight of the speed penalty (larger values weaken it). |
tol_factor |
Tolerance factor applied to the Tobler ceiling. |
slope_clip |
Slope clipping used by the hard speed constraint. |
max_cells |
Maximum size of the dynamic programming table (readings x vertices) for one round. |
Value
The stream with:
-
lon,lat,ele: position of the matched reference vertex (the original values are kept inlon_obs,lat_obs,ele_obs); -
ref_index,s_ref: matched vertex and its distance along the reference; -
offset_d: horizontal distance between the original reading and the matched vertex (m); -
offset_along,offset_cross: the same offset decomposed along the local direction of travel of the reference (positive when the reading is ahead) and across it (positive to the left), so thatoffset_d^2 = offset_along^2 + offset_cross^2; -
speed_h,speed_v,speed_total,slope_ref: kinematics along the reference after matching.
A quality-control report is stored in attr(x, "match_report"): readings
per reference vertex (maximum and number of vertices above
max(2, ceiling(n / m))), share of transitions at the speed ceiling,
objective value and offset summaries, per round.
References
Sanchez, R. (2025). Human trajectory mapping in ecological contexts: a framework for reliable space-time localization. ANZSCPB Conference 2025, Sippy Downs, Australia.
Forney, G. D. (1973). The Viterbi algorithm. Proceedings of the IEEE, 61(3), 268-278. doi:10.1109/PROC.1973.9030
See Also
tobler_stream(), detect_stream()
Examples
act = read_stream(system.file("extdata", "gpx_activity.gpx", package = "metrictrails"))
route = read_stream(system.file("extdata", "gpx_route.gpx", package = "metrictrails"))
dem = system.file("extdata", "test_dem.tif", package = "metrictrails")
ref = route %>% resample_stream(res = 5) %>% ele_correction(dem = dem)
act = act[1:300, ] %>% uniform_stream() %>% sanitize_stream() %>%
ele_correction(dem = dem)
params = c(v1 = 3.5, m1 = 0.1, c_plus = 3.2, c_minus = -0.5)
matched = match_stream(act, ref, params)
summary(matched$offset_d)
Project a stream to metric coordinates
Description
Adds the coordinates of every reading in a metric CRS, by default the WGS 84 / UTM zone that contains the centre of the stream. This is the projection used internally by every function of the package that measures distances.
Usage
project_stream(data, epsg = NULL)
Arguments
data |
A stream with |
epsg |
Optional EPSG code of the target CRS. When |
Value
The stream with columns x and y (metres); the EPSG code is
stored in attr(x, "epsg").
See Also
Examples
f = system.file("extdata", "gpx_route.gpx", package = "metrictrails")
route = project_stream(read_stream(f))
attr(route, "epsg")
head(route[, c("lon", "lat", "x", "y")])
Read a GNSS stream from a GPX file
Description
Reads the track points (or route points) of a GPX file with sf::st_read()
and returns them as a tibble with one row per reading.
Coordinates are returned as lon and lat (WGS 84 degrees), elevation as
ele and the time signature as timestamp (POSIXct, UTC). Each GPX track
segment (trkseg) becomes a value of the column segment, so that later
processing never interpolates across a recording gap. Point extensions such
as the Garmin TrackPointExtension (heart rate, cadence, temperature, power)
are returned as numeric columns with their original tag names (e.g. hr,
cad, atemp).
Only GPX is supported. Other formats can be converted to GPX beforehand.
Usage
read_stream(file, type = c("auto", "track", "route"))
Arguments
file |
Path to a |
type |
Which GPX geometry to read: |
Value
A tibble with columns lon, lat, and, when available, ele,
timestamp, segment and extension variables.
See Also
uniform_stream(), write_stream()
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
stream = read_stream(f)
head(stream)
Change the resolution of a DEM
Description
Refines or coarsens a Digital Elevation Model.
-
"bilinear"and"near"resample the DEM to a grid ofresmetres. Bilinear interpolation is recommended to refine a coarse public DEM; nearest neighbour performs poorly at fine resolutions (Sanchez et al., 2024). -
"mean","median","min"and"max"aggregate blocks of cells to a coarser resolution (factorround(res / current resolution)).
Usage
resample_dem(
dem,
res,
method = c("bilinear", "near", "mean", "median", "min", "max")
)
Arguments
dem |
A |
res |
Target resolution in metres. |
method |
One of |
Value
A terra::SpatRaster.
References
Sanchez, R., Egli, P., Besomi, M., & Truffello, R. (2024). Assessing the impact of Digital Elevation Model resolution on elevation gain estimations in trail running. 2024 International Conference on Electrical, Computer and Energy Technologies (ICECET). doi:10.1109/ICECET61485.2024.10698606
Examples
dem = terra::rast(system.file("extdata", "test_dem.tif", package = "metrictrails"))
dem_utm = terra::project(dem, "EPSG:32719")
resample_dem(dem_utm, res = 5)
Resample a stream to a uniform spatial resolution
Description
Resamples each segment of a stream along its horizontal 2D path, so that the result does not depend on how fast the athlete moved or on the recording rate of the device (the "defractalizer").
By default the resolution is derived from the stream itself with
resolution_stream() (res = "nyquist"): the q-quantile of the distance
between consecutive moving readings. Streams resampled at different
resolutions are not directly comparable; to compare several streams, pass
the same numeric res to all of them (at least the largest of their Nyquist
resolutions).
Methods:
-
"arclength"(default): points are placed everyL / floor(L / res)metres along the 2D path of lengthL(a spacing equal to or slightly larger thanres, keeping both ends), as indefractalize()of Sanchez, Nieto-Jimenez & Riveros-Matthey (2025). -
"simplify": the path is densified to 0.2 m and then simplified with the Douglas-Peucker algorithm usingresas tolerance, as in Sanchez et al. (2025). The spacing of the result is not uniform.
Resampling decimates the stream wherever the original readings are closer
than res; the stream should be low-pass filtered before (see
sanitize_stream()) to avoid aliasing. time, timestamp, ele and
every other numeric column are interpolated linearly along the 2D distance;
other columns take the value of the preceding original reading. Elevation is
expected to be replaced from a DEM afterwards (ele_correction()).
Usage
resample_stream(
data,
res = "nyquist",
q = 0.9,
method = c("arclength", "simplify"),
idle = 0.1
)
Arguments
data |
A stream with |
res |
Target resolution in metres, or |
q |
Quantile used when |
method |
|
idle |
Distance (m) below which a reading is idle (see
|
Value
The resampled stream; the resolution used is stored in
attr(x, "res").
References
Sanchez, R., Egli, P. E., Jornet, K., Duggan, M., & Besomi, M. (2025). How fractal complexity distorts distance and elevation gain in trail and mountain running: The case for course measurement standardization. Proceedings of the Institution of Mechanical Engineers, Part P: Journal of Sports Engineering and Technology. doi:10.1177/17543371251341660
Sanchez, R., Nieto-Jimenez, C., & Riveros-Matthey, C. (2025). High-precision mapping of mountain terrain for human locomotion research: A multimodal geospatial framework. 2025 15th IEEE International Conference on Pattern Recognition Systems (ICPRS). doi:10.1109/ICPRS66293.2025.11302827
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
stream = read_stream(f) %>% uniform_stream() %>% sanitize_stream()
resampled = resample_stream(stream)
attr(resampled, "res")
Spatial resolution of a stream
Description
Describes the distribution of the horizontal distance between consecutive
readings (delta_d, 2D, in metres) and derives the Nyquist resolution used
by resample_stream().
A GNSS device that records at a fixed rate (e.g. 1 Hz) produces a spatial
resolution that is a random variable: delta_d is the speed times the
sampling interval. Readings with delta_d < idle (stops) are excluded.
The spacing res is a high quantile (q) of delta_d. Spatial features
with wavelength of at least 2 * res are resolved almost everywhere along
the track, and a uniform grid with spacing res represents exactly that
band (its Nyquist wavelength is 2 * res). A finer grid would add
interpolated readings between sparse fixes; a coarser grid would discard
resolved information.
Only horizontal geodesic distance is used: raw elevations come from sensor fusion or are rounded to the metre, and are replaced from a DEM at the end of the pipeline.
Usage
resolution_stream(data, q = 0.9, idle = 0.1)
Arguments
data |
A stream with |
q |
Quantile of |
idle |
Distance (m) below which a reading is considered idle. |
Value
A tibble with one row per stream: number of readings, share of idle
readings, mean, standard deviation and quantiles of delta_d, and res,
the q-quantile. When data is a list, use max(result$res) as the
common resolution to make the streams comparable.
References
Sanchez, R., Egli, P. E., Jornet, K., Duggan, M., & Besomi, M. (2025). How fractal complexity distorts distance and elevation gain in trail and mountain running: The case for course measurement standardization. Proceedings of the Institution of Mechanical Engineers, Part P: Journal of Sports Engineering and Technology. doi:10.1177/17543371251341660
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
resolution_stream(read_stream(f))
Sanitize the coordinates of a stream and flag implausible readings
Description
Cleans the horizontal position (lon, lat) of a GNSS stream and adds red
flags for readings that are physically implausible. This is the third step
of the pipeline, after read_stream() and uniform_stream() and before
resample_stream().
Red flags are computed on the raw values, before smoothing, and returned as logical columns:
| flag | rule | acts on |
flag_coord | missing coordinate, |lat| > 90, |lon| > 180 or (0, 0) | position |
flag_time | time does not increase (delta_t <= 0) | position |
flag_speed | horizontal speed from the previous reading > v_max, or above the Tobler ceiling when tobler is given | position |
flag_accel | |acceleration| > accel_max | position |
flag_jump | displacement > edge_jump among the first or last 3 readings | position |
flag_slope | |slope| > slope_max | elevation |
flag_vert_speed | |vertical speed| > vert_speed_max | elevation |
flag_idle_ele | elevation changes while the horizontal position does not | elevation |
flag_gap | delta_t > gap | information only |
flag_low_res | distance from the previous reading > low_res | information only |
Rules that need time are skipped when the stream has no time column, or
when more than half of the readings exceed v_max (time is then not
physical, e.g. routes exported with fake timestamps; a warning is given).
Elevation flags are diagnostics of the device: elevation is replaced from a
DEM at the end of the pipeline (see ele_correction()).
action decides what happens to flagged readings before smoothing:
"flag" only adds the columns; "drop" removes readings with a position
flag and sets ele to NA where an elevation flag is raised; "interpolate"
replaces flagged positions (and flagged elevations) by linear interpolation
between the nearest valid readings of the same segment. Readings with a
flag_coord cannot be smoothed and are always removed.
Smoothing methods (per segment, in metric UTM coordinates):
-
"kalman"(default): constant-velocity Kalman filter followed by a Rauch-Tung-Striebel smoother (rts = TRUE). Process noise is a white acceleration with standard deviationsigma_acc(m/s^2); measurement noise has standard deviationsigma_obs(m). The filter restarts after time gaps longer thangapand where time is missing or decreases. Without time, every method except"gaussian"runs along the cumulative 2D distance instead of time (arc-length parametrisation;sigma_accthen bounds the curvature, in 1/m). -
"loess": local polynomial regression of each coordinate against time (or cumulative distance when there is no time), with spanalpha. This is the method of the former GPStreamsmooth_stream(). -
"gaussian": Gaussian kernel regression against time with standard deviationbandwidthseconds (Sanchez & Villena, 2020). -
"savgol": Savitzky-Golay filter with windowsg_windowand polynomial ordersg_order. -
"pipeline": the post-processing sequence of Martinez, Sanchez & Diaz (2026): Kalman filter, outlier detection on derived speed, acceleration and slope, linear interpolation of outliers, Savitzky-Golay filter and edge-jump correction. See Details. -
"none": flags only.
Usage
sanitize_stream(
data,
method = c("kalman", "loess", "gaussian", "savgol", "pipeline", "none"),
action = c("flag", "drop", "interpolate"),
smooth_ele = FALSE,
v_max = 10,
accel_max = 6,
vert_speed_max = 1,
slope_max = 1.19,
gap = 30,
low_res = 20,
edge_jump = 30,
idle = 0.1,
tobler = NULL,
tol_factor = 1.1,
sigma_obs = 5,
sigma_acc = 1,
rts = TRUE,
alpha = 0.05,
bandwidth = 10,
sg_window = 9,
sg_order = 2,
window = 25,
jump_k = 1.5,
local_k = 2
)
Arguments
data |
A stream with |
method |
Smoothing method, see Description. |
action |
What to do with flagged readings: |
smooth_ele |
If |
v_max |
Maximum plausible horizontal speed (m/s). |
accel_max |
Maximum plausible absolute acceleration (m/s^2). |
vert_speed_max |
Maximum plausible absolute vertical speed (m/s). |
slope_max |
Maximum plausible absolute slope (rise over run); 1.19 is about 50 degrees. |
gap |
Time gap (s) reported by |
low_res |
Distance (m) between readings reported by |
edge_jump |
Displacement (m) that defines a jump at the edges. |
idle |
Horizontal displacement (m) below which a reading is idle. |
tobler |
Optional Tobler parameters (output of |
tol_factor |
Tolerance factor applied to the Tobler ceiling. |
sigma_obs, sigma_acc, rts |
Kalman filter parameters. |
alpha |
Span of |
bandwidth |
Standard deviation (s) of the |
sg_window, sg_order |
Savitzky-Golay window (odd) and polynomial order. |
window, jump_k, local_k |
Outlier detection parameters of |
Details
In "pipeline", a reading is an outlier when at least two of three criteria
hold for any of the derived variables: (1) extreme value, outside
[Q1 - 1.5 IQR, Q3 + 1.5 IQR]; (2) abrupt jump, absolute change from the
previous reading larger than jump_k standard deviations of the changes;
(3) local deviation, distance to the moving mean (window window) larger than
local_k moving standard deviations. The paper reports the thresholds
(window 25, jump 1.5, local deviation 2.0, Savitzky-Golay 9/2, edge jump
30 m) but not their exact formulation; the formulation above is the one
implemented here. The uniform temporal interpolation of the paper is not
applied, because spatial resampling (resample_stream()) follows in the
metrictrails pipeline.
Value
The stream with smoothed lon, lat (and ele if smooth_ele),
the flag_* columns, and a summary in attr(x, "sanitize_report").
References
Martinez, C., Sanchez, R., & Diaz, C. (2026). A post-processing methodology for improving kinematic consistency in GNSS-based running trajectories. 2026 6th International Conference on Electrical, Computer and Energy Technologies (ICECET). doi:10.1109/ICECET65726.2026.11632531
Sanchez, R., & Villena, M. (2020). Comparative evaluation of wearable devices for measuring elevation gain in mountain physical activities. Proceedings of the Institution of Mechanical Engineers, Part P: Journal of Sports Engineering and Technology, 234(4), 312-319. doi:10.1177/1754337120918975
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
stream = read_stream(f) %>% uniform_stream() %>% sanitize_stream()
attr(stream, "sanitize_report")
Metadata of the activities in strava_streams
Description
Summary of the two activities in strava_streams as returned by the Strava API activities endpoint, one row per activity.
Usage
strava_metadata
Format
A data frame with 2 rows and 61 variables.
Details
Main variables: id (activity identifier, links to
strava_streams), name, type, start_date and start_date_local
(ISO 8601), distance (km), elapsed_time and moving_time (s),
total_elevation_gain, elev_high and elev_low (m), average_speed
and max_speed, and map.summary_polyline (encoded polyline). See the
Strava API reference for the remaining variables.
Source
https://developers.strava.com/docs/reference/
GNSS streams of two activities from the Strava API
Description
Readings of two activities recorded in Santiago, Chile (a trail run on Manquehue hill and a cross-country ride), as returned by the Strava API streams endpoint, already following the metrictrails naming convention.
Usage
strava_streams
Format
A data frame with 4148 rows and 13 variables:
- lon
longitude, WGS 84 degrees
- lat
latitude, WGS 84 degrees
- ele
elevation reported by Strava, metres
- time
seconds since the start of the activity
- cadence
cadence, steps or revolutions per minute
- distance
cumulative distance computed by Strava, kilometres
- grade_smooth
terrain grade smoothed by Strava, percent
- heartrate
heart rate, beats per minute
- moving
whether Strava considers the athlete moving
- temp
temperature, degrees Celsius
- velocity_smooth
speed smoothed by Strava, km/h
- id
activity identifier, links to strava_metadata
- type
activity type (
"Run"or"Ride")
Source
https://developers.strava.com/docs/reference/
Summary measures of a stream
Description
Computes course-level measures of a stream, or of each group of a stream.
-
distance_2d,distance_3d: horizontal and 3D length (m). -
elevation_gain,elevation_loss: cumulative positive and negative elevation change (m, both positive). -
ele_min,ele_max: elevation range (m). -
mean_slope,iqr_slope: mean and inter-quartile range of the slope of moving readings (%). -
sinuosity: path length over straight-line distance between the first and last readings (Inffor closed loops). -
mean_turn: mean heading change (degrees). -
resolution: mean horizontal distance between consecutive readings (m). -
km_effort: kilometre-effort, horizontal km plus elevation gain / 100. -
pct_idle: share of readings without horizontal displacement (%), andidle_gain,idle_loss: elevation gained and lost while idle. -
duration: elapsed time (s), whentimeis available. -
fractal_dim: fractal dimension of the horizontal path (divider method, seefractal_stream()), whenfractal = TRUE.
Usage
summarise_stream(data, by = NULL, idle = 0.1, fractal = FALSE)
Arguments
data |
A stream with |
by |
Optional name(s) of grouping columns (e.g. |
idle |
Horizontal distance (m) below which a reading is idle. |
fractal |
If |
Value
A tibble with one row per group.
References
Sanchez, R., Egli, P. E., Jornet, K., Duggan, M., & Besomi, M. (2025). How fractal complexity distorts distance and elevation gain in trail and mountain running: The case for course measurement standardization. Proceedings of the Institution of Mechanical Engineers, Part P: Journal of Sports Engineering and Technology. doi:10.1177/17543371251341660
Sanchez, R., Nieto-Jimenez, C., & Riveros-Matthey, C. (2025). High-precision mapping of mountain terrain for human locomotion research: A multimodal geospatial framework. 2025 15th IEEE International Conference on Pattern Recognition Systems (ICPRS). doi:10.1109/ICPRS66293.2025.11302827
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
summarise_stream(read_stream(f) %>% uniform_stream())
Speed predicted by the Tobler function
Description
Evaluates the asymmetric Tobler function fitted by tobler_stream().
Usage
tobler_speed(slope, params)
Arguments
slope |
Slope (rise over run). |
params |
Output of |
Value
Horizontal speed (m/s).
Examples
tobler_speed(c(-0.2, 0, 0.2), c(v1 = 3, m1 = -0.05, c_plus = 3.5, c_minus = -3.5))
Tobler parameters of a stream
Description
Fits an asymmetric Tobler-type hiking function to the relation between horizontal speed and slope of a stream:
v(m) = v_1 \exp(-c_+ (m - m_1)) \quad \textrm{if } m \ge m_1
v(m) = v_1 \exp(-c_- (m - m_1)) \quad \textrm{if } m < m_1
where m is the slope (rise over run), m1 the slope of maximum speed,
v1 the speed at m1 (m/s), c_plus > 0 the decay uphill and
c_minus < 0 the decay downhill.
The model describes the frontier of human performance, so it is fitted to
the tau quantile of speed, not to its mean. Two estimators are available,
both using only base R:
-
"pinball"(default): minimises the quantile (pinball) loss\sum \rho_\tau(v_i - v(m_i))withstats::optim()(Nelder-Mead), starting from the"binned"solution and a few perturbations of it. This is the same estimator as nonlinear quantile regression (quantreg::nlrq). -
"binned": groups the readings in slope bins of widthbin_width, takes thetauquantile of speed in each bin with at leastmin_binreadings, and fitslog(speed)with a weighted piecewise-linearstats::lm();m1is chosen withstats::optimize().
The stream should have gone through the pipeline (sanitized, resampled and with DEM elevation) so that slope and speed are reliable.
Usage
tobler_stream(
data,
tau = 0.9,
method = c("pinball", "binned"),
slope_range = c(-0.6, 0.6),
min_speed = 0.1,
bin_width = 0.02,
min_bin = 10
)
Arguments
data |
A stream with |
tau |
Quantile of speed to model. |
method |
|
slope_range |
Slopes outside this range are ignored. |
min_speed |
Readings slower than this (m/s) are ignored (stops). |
bin_width |
Width of the slope bins of |
min_bin |
Minimum readings per bin. |
Value
A one-row tibble with v1, m1, c_plus, c_minus, tau, n
(readings used), r1 (pseudo R-squared for quantile regression,
1 - loss / loss of the intercept-only model) and method.
References
Tobler, W. (1993). Three presentations on geographical analysis and modeling. National Center for Geographic Information and Analysis, Technical Report 93-1.
Koenker, R., & Machado, J. A. F. (1999). Goodness of fit and related inference processes for quantile regression. Journal of the American Statistical Association, 94(448), 1296-1310.
See Also
Examples
f = system.file("extdata", "gpx_activity.gpx", package = "metrictrails")
dem = system.file("extdata", "test_dem.tif", package = "metrictrails")
stream = read_stream(f) %>% uniform_stream() %>% sanitize_stream() %>%
resample_stream(res = 5) %>% ele_correction(dem = dem)
tobler_stream(stream)
Uniform naming convention for a stream
Description
Renames the columns of a stream to the convention used by every function of the package:
-
lon: longitude (WGS 84 degrees) -
lat: latitude (WGS 84 degrees) -
ele: elevation or altitude (metres) -
timestamp: time signature of the reading (POSIXct, UTC) -
time: seconds since the first reading
Columns are matched against a dictionary of exact names (case-insensitive),
e.g. lng, longitude or position_long for longitude and altitude or
enhanced_altitude for elevation. Partial matches are not used, so columns
such as moving_time or platform are never renamed. Other variables
(heart rate, cadence, power, ...) are kept unchanged.
A column called time holding date-times (or ISO 8601 text) is treated as a
timestamp. A numeric time column (e.g. Strava API streams) is kept as
seconds. When time increases in less than half of the readings (e.g. routes
exported by planners with a constant time), time and timestamp are
removed with a warning.
Usage
uniform_stream(data)
Arguments
data |
A data frame or tibble with a GNSS stream. |
Value
A tibble with lon, lat (and ele, timestamp, time when
available) as first columns.
Examples
data(strava_streams)
head(uniform_stream(strava_streams))
df = data.frame(Latitude = c(-33.40, -33.41), Longitude = c(-70.50, -70.51),
Altitude = c(800, 805),
Time = c("2024-01-01T10:00:00Z", "2024-01-01T10:00:05Z"))
uniform_stream(df)
Write a GNSS stream to a GPX file
Description
Writes a stream as a GPX 1.1 track. Each value of segment becomes a
<trkseg>. Elevation (ele) and time (timestamp) are written when
present.
Usage
write_stream(data, file, name = NULL)
Arguments
data |
A stream with at least |
file |
Path of the GPX file to create. |
name |
Optional track name. |
Value
file, invisibly.
See Also
Examples
f = system.file("extdata", "gpx_route.gpx", package = "metrictrails")
route = read_stream(f)
out = tempfile(fileext = ".gpx")
write_stream(route, out)
nrow(read_stream(out)) == nrow(route)