6 Stage Route Annotations
As well as reviewing the stage route, we may also want to annotate the route with additional information such as markers showing the location of split points or other points of interest.
We have already seen how we can add stage start and end markers at specific locations using custom markers, but how might we add markers to other locations?
6.1 Load in the Route Data
Let’s load in our route data to give us something to work with:
library(sf)
= 'montecarlo_2021.geojson'
geojson_filename = st_read(geojson_filename) geojson_sf
## Reading layer `montecarlo_2021' from data source `/Users/tonyhirst/Documents/GitHub/visualising-rally-stages/montecarlo_2021.geojson' using driver `GeoJSON'
## Simple feature collection with 9 features and 2 fields
## geometry type: LINESTRING
## dimension: XY
## bbox: xmin: 5.243488 ymin: 43.87633 xmax: 6.951953 ymax: 44.81973
## geographic CRS: WGS 84
and quickly preview a map with start and end markers:
library(leaflet)
# https://fontawesome.com/v4.7.0/icons/
<- awesomeIconList(
start_stop_markers "start" = makeAwesomeIcon(
icon = "play-circle",
markerColor = "blue",
library = "fa"
),"stop" = makeAwesomeIcon(
icon = "stop-circle",
markerColor = "red",
library = "fa"
)
)
# TO DO - we need a better way (dplyr?) to extract this data
# for each stage
= as.data.frame(st_coordinates(geojson_sf[1, ]$geometry))
stage_coords
= stage_coords[1, ]
stage_start_coords = stage_coords[nrow(stage_coords), ]
stage_end_coords
= stage_coords[c(1,nrow(stage_coords)),]
first_and_last $label=c('SS1 stage start', 'SS1 stage end')
first_and_last$typ=c('start', 'stop')
first_and_last$color=c('green', 'red')
first_and_last
= leaflet(geojson_sf[1,]) %>%
m addProviderTiles("OpenTopoMap", group = "OSM") %>%
addPolylines(color = "red", weight = 5) %>%
addAwesomeMarkers(~X, ~Y,
icon = ~ start_stop_markers[typ],
label = ~ label,
data=first_and_last)
m
With a base map in place, what other information might we add?
6.2 Adding Split Point Locations
One of the useful locations to mark on the stage are the split points. Split times are reported by WRC live timing, so it can be useful to see just where are on the stage they are recorded.
A live timing data feed from the WRC live timing service includes the distance into the stage of each split point, as the following JSON fragment taken from the feed demonstrates:
# https://api.wrc.com/results-api/rally-event/1695/split-times/stage-external/1701
{
"stageId": 1701,
"splitPoints": [
{
"splitPointId": 3506,
"stageId": 1701,
"number": 2,
"distance": 10.99
},
{
"splitPointId": 3509,
"stageId": 1701,
"number": 3,
"distance": 17.5
},
{
"splitPointId": 3527,
"stageId": 1701,
"number": 1,
"distance": 6.46
},
{
"splitPointId": 3543,
"stageId": 1701,
"number": 4,
"distance": 19.1
}
]
}
We can grab this JSON from the original URL and parse the data out:
library(jsonlite)
library(dplyr)
= 'https://api.wrc.com/results-api/rally-event/1695/split-times/stage-external/1701'
stage_splits_url = fromJSON( stage_splits_url )
splits_json
# Lets also save a local copy of that JSON data
write(toJSON(splits_json), "split_times_example.json")
= arrange(splits_json$splitPoints, number)
splitPoints
# We also have access to splits_json$entrySplitPointTimes and splits_json$stageId
splitPoints
## splitPointId stageId number distance
## 1 3527 1701 1 6.46
## 2 3506 1701 2 10.99
## 3 3509 1701 3 17.50
## 4 3543 1701 4 19.10
6.2.1 Using geojson to Find the Distance Along a Route
The turf.js is a widely used Javascript package for working with and processing geojson data. The package has been ported to other languages, including R and Python, using similar APIs.
The geoops
R package provides an R port of many of the functions in the turf.js
Javascript package, so let’s see how it works.
We can get the distance into stage (in km) for one of the split points from the split time JSON data:
$splitPoints[2, 'distance'] splits_json
## [1] 17.5
# A distance of 17.5km
We can use this distance to index along the geojson object using the geoops:geo_along()
function. Specifically, geo_along()
take a GeoJSON string (rather than a spatial object) and then returns the location of a point a specific distance along it, again in a GeoJSON string format:
library(geojsonio)
= geojsonio::geojson_json(geojson_sf[1,]$geometry)
route_as_geojson
= geoops::geo_along(route_as_geojson,
gj_point_along 17.5, units='kilometres')
gj_point_along
## [1] "{\"geometry\":{\"coordinates\":[5.908658,44.816138],\"type\":\"Point\"},\"properties\":{},\"type\":\"Feature\"}"
We can parse the geojson point as a simple feature using the geojsonio::geojson_sf()
function and then plot the split time waypoint directly from that object:
%>%
m addCircleMarkers(data=geojson_sf(gj_point_along),
radius =10,
stroke = FALSE, fillOpacity = 0.8)
6.2.2 Using rgeos::gInterpolate
to Find the Distance Along a Route
The following provides a rather clunky workaround to finding the distance along the line from a Spatial object (a more streamlined approach can be found in the next chapter).
I present the recipe here as a useful (useless?!) exercise in how to come up with a recipe from basic principles when you don’t really know what you’re doing!
To begin with, let’s reuse some of the code we used in the chapter on projections that allow us to project from the degree based latlong CRS to the meters based UTM coordinate systems:
# Detect the UTM zone as an EPSG code
= function(lonlat) {
lonlat2UTMzone = (floor((lonlat[1] + 180) / 6) %% 60) + 1
utm if(lonlat[2] > 0) {
+ 32600
utm else{
} + 32700
utm
}
}
# Keep track of the original proj4 string
= st_crs(geojson_sf[1,])$proj4string
old_crs
# Generate a new projection in the appropriate UTM zone
= lonlat2UTMzone(c(st_coordinates(geojson_sf[1,])[1,1],
crs_zone st_coordinates(geojson_sf[1,])[1,2]))
= st_crs(crs_zone)$proj4string
new_proj4_string
# Transform the route to the UTM projection
= st_transform(geojson_sf, crs=new_proj4_string)
utm_routes
st_crs(utm_routes)$proj4string
## [1] "+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs"
Now that we are in a projection system based on meters, we can use the rgeos::gInterpolate()
function to find a point a certain number of meters along the route. Note that the coordinates that are returned are UTM based coordinates:
# Find the distance along the route in meters
# rgeos seems to want a Spatial object?
= rgeos::gInterpolate(as(utm_routes[1,],'Spatial'),
utm_point_along 12500, normalized = FALSE)
utm_point_along
## class : SpatialPoints
## features : 1
## extent : 730351.2, 730351.2, 4963971, 4963971 (xmin, xmax, ymin, ymax)
## crs : +proj=utm +zone=31 +datum=WGS84 +units=m +no_defs
We now need to transform this point back to the latlong CRS:
= st_transform(st_as_sf(utm_point_along),
latlon_point st_crs(geojson_sf)$proj4string)
latlon_point
## Simple feature collection with 1 feature and 0 fields
## geometry type: POINT
## dimension: XY
## bbox: xmin: 5.912144 ymin: 44.792 xmax: 5.912144 ymax: 44.792
## CRS: +proj=longlat +datum=WGS84 +no_defs
## geometry
## 1 POINT (5.912144 44.792)
We can add a marker to the map showing the location of the point the required distance along the line by casting the transformed point location(s) into a dataframe and extracting the desired coordinate columns:
= leaflet(geojson_sf[1,]) %>%
m addProviderTiles("OpenTopoMap", group = "OSM") %>%
addPolylines(color = "red", weight = 5)%>%
addCircleMarkers(~X, ~Y, radius =10,
stroke = FALSE, fillOpacity = 0.9,
data= as.data.frame(st_coordinates(latlon_point)))
m
For convenience, we can cast all the split point distances into stage as actual location points and create a simple feature containing the points:
splitPoints
## splitPointId stageId number distance
## 1 3527 1701 1 6.46
## 2 3506 1701 2 10.99
## 3 3509 1701 3 17.50
## 4 3543 1701 4 19.10
Create a simple function to handle the creation of a point:
library(rgeos)
= function(route, dist){
point_from_dist_along st_as_sf(gInterpolate(as(route,'Spatial'),
normalized = FALSE))
dist,
}
= function(route, dist){
coords_from_dist_along st_coordinates(point_from_dist_along(route, dist))
}
point_from_dist_along(utm_routes[1,], 1200)
## Simple feature collection with 1 feature and 0 fields
## geometry type: POINT
## dimension: XY
## bbox: xmin: 728761.8 ymin: 4958594 xmax: 728761.8 ymax: 4958594
## CRS: +proj=utm +zone=31 +datum=WGS84 +units=m +no_defs
## geometry
## 1 POINT (728761.8 4958594)
coords_from_dist_along(utm_routes[1,], 1200)
## X Y
## 1 728761.8 4958594
And add the point to the dataframe:
= st_sfc(lapply(1:nrow(splitPoints),
geometry function(x) st_point()))
= arrange(splits_json$splitPoints, number)
splitPoints = st_sf(splitPoints, geometry = geometry)
splitPoints
= splitPoints %>% mutate(geometry = st_geometry(point_from_dist_along(utm_routes[1,], distance)))
splitPoints
splitPoints
## Simple feature collection with 4 features and 4 fields
## geometry type: POINT
## dimension: XY
## bbox: xmin: 729180.3 ymin: 4957663 xmax: 729185.7 ymax: 4957675
## CRS: +proj=utm +zone=31 +datum=WGS84 +units=m +no_defs
## splitPointId stageId number distance geometry
## 1 3527 1701 1 6.46 POINT (729180.3 4957663)
## 2 3506 1701 2 10.99 POINT (729182.2 4957667)
## 3 3509 1701 3 17.50 POINT (729185 4957673)
## 4 3543 1701 4 19.10 POINT (729185.7 4957675)
st_coordinates(splitPoints )
## X Y
## 1 729180.3 4957663
## 2 729182.2 4957667
## 3 729185.0 4957673
## 4 729185.7 4957675
We could also cast all the points to a multipoint:
st_multipoint(st_coordinates(splitPoints))
## MULTIPOINT ((729180.3 4957663), (729182.2 4957667), (729185 4957673), (729185.7 4957675))
6.3 Parallel Routes
If we have access to multiple datasets along route, such as telemetry data sets for different cars, it may be useful to be able to plot each set on its own route line translated from the provided route.
The following example of how we might be able to achieve this is taken from One sided line buffers in R using rgeos and represents a minimal proof of concept.
library(sp)
<- function(line,width,minEx){
TwoBuf <- gBuffer(line, width=minEx, capStyle="SQUARE")
Buf0 <- gBuffer(line, width=width, capStyle="FLAT")
Buf1 return(disaggregate(gDifference(Buf1,Buf0)))
}
# Sample the first few points from a route
= st_coordinates(geojson_sf[1,])[1:10,]
cc
# Create a linestring from the first few coordinates
# st_linestring expects a matrix,
# so create one with cbind on the long and lat columns
= st_linestring(cbind(x=cc[,1], y=cc[,2]))
partial_route
<- TwoBuf(line=as(partial_route,'Spatial'),
TortBuf width=0.0001, minEx=0.00001)
plot(TortBuf, col=c('red', 'blue')) #First object on left, second on right
To make this approach more useful, it would probably make sense to project into UTM coordinates and set the parameters in units of meters.