Calculate, decode and plot routes on map using leaflet and R - r

I have raw data which consists of lattitude and longitude of places The sample data is as follows:
EDIT (dput):
structure(list(Lat = c(-33.9409444, -33.9335713, -33.9333906,
-33.9297826), Lon = c(18.5001774, 18.5033218, 18.518719, 18.5209372
)), .Names = c("Lat", "Lon"), row.names = c(NA, 4L), class = "data.frame")
I want to plot routes on the map using this data. This is my R code:
library(RODBC)
library(leaflet)
ui <- fluidPage(
titlePanel("South Africa & Lesotho"),
mainPanel(
leafletOutput("mymap")
)
)
server <- function(input, output, session) {
dbhandle <- odbcDriverConnect('driver={SQL Server};server=localhost\\SQLEXpress;database=OSM;trusted_connection=true')
res <- sqlQuery(dbhandle, 'select Lat, Lon from OSM2 where Street is not null')
output$mymap <- renderLeaflet({
leaflet(res) %>%
addTiles() %>%
addPolylines(lat = ~Lat, lng = ~Lon)
})
}
shinyApp(ui, server)
However, all I get is this:
How can I use leaflet and R to plot the routes using the raw data (lat, long)?

What you have to do:
Import the points
Calculate all routes between the points (I use OSRM)
Extract the route geometry from the routes (Appreciate the reference and have a look there for the speed updates!). Thanks to #SymbolixAU: You can also use googleway::decode_pl() or gepaf::decodePolyline()
Display everything on a map (I use leaflet)
My approach is not optimized for anything, but it should do the job...
(It is script in RStudio, therefore the print() statements after leaflet.)
library(leaflet)
library(stringr)
library(bitops)
df <- structure(list(
lat = c(-33.9409444, -33.9335713, -33.9333906, -33.9297826),
lng = c(18.5001774, 18.5033218, 18.518719, 18.5209372)),
.Names = c("lat", "lng"),
row.names = c(NA, 4L), class = "data.frame")
nn <- nrow(df)
# Functions
# =========
viaroute <- function(lat1, lng1, lat2, lng2) {
R.utils::evalWithTimeout({
repeat {
res <- try(
route <- rjson::fromJSON(
file = paste("http://router.project-osrm.org/route/v1/driving/",
lng1, ",", lat1, ";", lng2, ",", lat2,
"?overview=full", sep = "", NULL)))
if (class(res) != "try-error") {
if (!is.null(res)) {
break
}
}
}
}, timeout = 1, onTimeout = "warning")
return(res)
}
decode_geom <- function(encoded) {
scale <- 1e-5
len = str_length(encoded)
encoded <- strsplit(encoded, NULL)[[1]]
index = 1
N <- 100000
df.index <- 1
array = matrix(nrow = N, ncol = 2)
lat <- dlat <- lng <- dlnt <- b <- shift <- result <- 0
while (index <= len) {
# if (index == 80) browser()
shift <- result <- 0
repeat {
b = as.integer(charToRaw(encoded[index])) - 63
index <- index + 1
result = bitOr(result, bitShiftL(bitAnd(b, 0x1f), shift))
shift = shift + 5
if (b < 0x20) break
}
dlat = ifelse(bitAnd(result, 1),
-(result - (bitShiftR(result, 1))),
bitShiftR(result, 1))
lat = lat + dlat;
shift <- result <- b <- 0
repeat {
b = as.integer(charToRaw(encoded[index])) - 63
index <- index + 1
result = bitOr(result, bitShiftL(bitAnd(b, 0x1f), shift))
shift = shift + 5
if (b < 0x20) break
}
dlng = ifelse(bitAnd(result, 1),
-(result - (bitShiftR(result, 1))),
bitShiftR(result, 1))
lng = lng + dlng
array[df.index,] <- c(lat = lat * scale, lng = lng * scale)
df.index <- df.index + 1
}
geometry <- data.frame(array[1:df.index - 1,])
names(geometry) <- c("lat", "lng")
return(geometry)
}
map <- function() {
m <- leaflet() %>%
addTiles(group = "OSM") %>%
addProviderTiles("Stamen.TonerLite") %>%
addLayersControl(
baseGroups = c("OSM", "Stamen.TonerLite")
)
return(m)
}
map_route <- function(df, my_list) {
m <- map()
m <- addCircleMarkers(map = m,
lat = df$lat,
lng = df$lng,
color = "blue",
stroke = FALSE,
radius = 6,
fillOpacity = 0.8) %>%
addLayersControl(baseGroups = c("OSM", "Stamen.TonerLite")) %>%
{
for (i in 1:length(my_list)) {
. <- addPolylines(., lat = my_list[[i]]$lat, lng = my_list[[i]]$lng, color = "red", weight = 4)
}
return(.)
}
return(m)
}
# Main
# ======
m <- map()
m <- m %>% addCircleMarkers(lat = df$lat,
lng = df$lng,
color = "red",
stroke = FALSE,
radius = 10,
fillOpacity = 0.8)
print(m)
my_list <- list()
r <- 1
for (i in 1:(nn-1)) {
for (j in ((i+1):nn)) {
my_route <- viaroute(df$lat[i], df$lng[i],df$lat[j], df$lng[j])
geom <- decode_geom(my_route$routes[[1]]$geometry)
my_list[[r]] <- geom
r <- r + 1
}
}
print(map_route(df, my_list))
Result:
In the end, you have to put all that in your shiny server...
I hope that helps!

Another more efficient way to calculate routes between points is with the osrm package: Interface Between R and the OpenStreetMap-Based Routing Service OSRM. Look at this example:
library(osrm)
library(leaflet)
df = data.frame(com = c("A", "B", "C"),
lon = c(31.043515, 31.029080, 31.002896),
lat = c(-29.778562, -29.795506, -29.836168),
time = as.POSIXct(c("2020-03-18 07:56:59","2020-03-18 12:28:58","2020-03-18 18:24:52")))
trips <- osrmTrip(df, returnclass="sf")
trip <- trips[[1]]$trip
leaflet(trip) %>%
addProviderTiles("Stamen.TonerLite", group = "OSM") %>%
addPolylines() %>%
addCircleMarkers(lat = df$lat,
lng = df$lon,
popup = paste(df$com,"-",format(df$time,"%H:%M:%S")),
color = "red",
stroke = FALSE,
radius = 8,
fillOpacity = 0.8)

For the viaroute function created.
Use "R.utils::withTimeout" instead of "R.utils::evalWithTimeout" because that is now defunct.
I hope this helps

#Christoph's code is fantastic - although some of the functions no longer work as originally written, presumably due to breaking changes in R over time.
As #user7779697 points out above, the viaroute() function needs be updated to accommodate code changes to R.utils that saw deprecation of evalWithTimeout, replacing it with withTimeout as follows:
R.utils::withTimeout()
I also ran into issues with the map_route() function, corrected by removing the braces from the internal for loop.
I've pasted the full updated code below which works with R Version 4.2.1 - I take no credit for this excellent work, only to get it back up and running with version changes:
library(leaflet)
library(stringr)
library(bitops)
df <- structure(list(
lat = c(-33.9409444, -33.9335713, -33.9333906, -33.9297826),
lng = c(18.5001774, 18.5033218, 18.518719, 18.5209372)),
.Names = c("lat", "lng"),
row.names = c(NA, 4L), class = "data.frame")
nn <- nrow(df)
# Functions
# =========
viaroute <- function(lat1, lng1, lat2, lng2) {
R.utils::withTimeout({
repeat {
res <- try(
route <- rjson::fromJSON(
file = paste("http://router.project-osrm.org/route/v1/driving/",
lng1, ",", lat1, ";", lng2, ",", lat2,
"?overview=full", sep = "", NULL)))
if (class(res) != "try-error") {
if (!is.null(res)) {
break
}
}
}
}, timeout = 1, onTimeout = "warning")
return(res)
}
decode_geom <- function(encoded) {
scale <- 1e-5
len = str_length(encoded)
encoded <- strsplit(encoded, NULL)[[1]]
index = 1
N <- 100000
df.index <- 1
array = matrix(nrow = N, ncol = 2)
lat <- dlat <- lng <- dlnt <- b <- shift <- result <- 0
while (index <= len) {
# if (index == 80) browser()
shift <- result <- 0
repeat {
b = as.integer(charToRaw(encoded[index])) - 63
index <- index + 1
result = bitOr(result, bitShiftL(bitAnd(b, 0x1f), shift))
shift = shift + 5
if (b < 0x20) break
}
dlat = ifelse(bitAnd(result, 1),
-(result - (bitShiftR(result, 1))),
bitShiftR(result, 1))
lat = lat + dlat;
shift <- result <- b <- 0
repeat {
b = as.integer(charToRaw(encoded[index])) - 63
index <- index + 1
result = bitOr(result, bitShiftL(bitAnd(b, 0x1f), shift))
shift = shift + 5
if (b < 0x20) break
}
dlng = ifelse(bitAnd(result, 1),
-(result - (bitShiftR(result, 1))),
bitShiftR(result, 1))
lng = lng + dlng
array[df.index,] <- c(lat = lat * scale, lng = lng * scale)
df.index <- df.index + 1
}
geometry <- data.frame(array[1:df.index - 1,])
names(geometry) <- c("lat", "lng")
return(geometry)
}
map <- function() {
m <- leaflet() %>%
addTiles(group = "OSM") %>%
addProviderTiles("Stamen.TonerLite") %>%
addLayersControl(
baseGroups = c("OSM", "Stamen.TonerLite")
)
return(m)
}
map_route <- function(df, my_list) {
m <- map()
m <- addCircleMarkers(map = m,
lat = df$lat,
lng = df$lng,
color = "blue",
stroke = FALSE,
radius = 6,
fillOpacity = 0.8) %>%
addLayersControl(baseGroups = c("OSM", "Stamen.TonerLite"))
for (i in 1:length(my_list)) {
m <- addPolylines(map = m, lat = my_list[[i]]$lat, lng = my_list[[i]]$lng, color = "red", weight = 4)
}
return(m)
}
# Main
# ======
m <- map()
m <- m %>% addCircleMarkers(lat = df$lat,
lng = df$lng,
color = "red",
stroke = FALSE,
radius = 10,
fillOpacity = 0.8)
print(m)
my_list <- list()
r <- 1
for (i in 1:(nn-1)) {
for (j in ((i+1):nn)) {
my_route <- viaroute(df$lat[i], df$lng[i],df$lat[j], df$lng[j])
geom <- decode_geom(my_route$routes[[1]]$geometry)
my_list[[r]] <- geom
r <- r + 1
}
}
print(map_route(df, my_list))

Related

Incorrect number of probabilities dimsensions when optimizing a multi-season elo model

My data frame looks like this:
and the code to compute the initial/optimized elo
# Elo Rating System
library(eurolig)
library(tidyverse)
library(lubridate)
plldf2 <- read_csv('pll_elor.csv',show_col_types = FALSE)
plldf3 <- plldf2[(plldf2$season == 2019),]
# Helpers -----------------------------------------------------------------
# Expected win probability before a game
getExpectedProb <- function(r_team, r_opp, home_adv, s) {
1 / (1 + 10 ^ ((r_opp - r_team - home_adv) / s))
}
# Get Elo rating for next season
getCarryOver <- function(rating, c) {
c * rating + 1505 * (1 - c)
}
# Get margin of victory multiplier
getMovMultiplier <- function(points_diff, elo_diff) {
((points_diff + 3) ^ 0.8) / (7.5 + 0.006 * elo_diff)
}
getEloSummary <- function(df) {
df %>%
pivot_longer(
cols = ends_with("_new"),
names_to = "type",
values_to = "elo"
) %>%
select(
season,
date,
team,
opp,
type,
elo
) %>%
mutate(
team = ifelse(type == "elo_home_new", team, opp),
order = rank(date),
team_id = paste0(team, "-", season)
) %>%
select(season, date, team, elo, order, team_id)
}
# Algorithm ---------------------------------------------------------------
# For a single season
getSeasonElo <- function(df, k, home_adv, s, initial_elo) {
team_ratings <- initial_elo
# Data frame to store the subsequent values obtained by the algorithm
ratings_df <- df %>%
mutate(
home_adv = NA,
win_points_home = NA,
win_points_away = NA,
expected_prob_home = NA,
expected_prob_away = NA,
mov_home = NA,
mov_away = NA,
elo_home_prev = NA,
elo_away_prev = NA,
elo_home_new = NA,
elo_away_new = NA,
prob_pred = NA
)
for (i in 1:nrow(df)) {
team_home <- df$team[i]
team_away <- df$opp[i]
elo_home <- team_ratings[[team_home]]
elo_away <- team_ratings[[team_away]]
# Home advantage set to 0 for Final 4 games
h <- ifelse(df$phase[i] == "ff", 0, home_adv)
# Assign 1 for wins and 0 for losses
win_points_home <- ifelse(
df$score[i] > df$opp_score[i],
1,
0
)
win_points_away <- ifelse(win_points_home == 0, 1, 0)
# Find pre-game win probabilities
expected_prob_home <- getExpectedProb(
r_team = elo_home,
r_opp = elo_away,
home_adv = h,
s = s)
expected_prob_away <- 1 - expected_prob_home
# Margin of victory multiplier
points_diff_abs <- abs(df$score[i] - df$opp_score[i])
elo_diff_home <- elo_home + h - elo_away
elo_diff_away <- elo_away - elo_home - h
mov_home <- getMovMultiplier(points_diff_abs, elo_diff_home)
mov_away <- getMovMultiplier(points_diff_abs, elo_diff_away)
# Update Elo ratings
elo_home_new <- elo_home + k * (win_points_home - expected_prob_home) * mov_home
elo_away_new <- elo_away + k * (win_points_away - expected_prob_away) * mov_away
team_ratings[[team_home]] <- elo_home_new
team_ratings[[team_away]] <- elo_away_new
prob_pred <- sample(
x = c(team_home, team_away),
size = length(list(expected_prob_home,expected_prob_away)),
prob = c(expected_prob_home, expected_prob_away) #ISSUE HERE
)
ratings_df$home_adv[i] <- h
ratings_df$win_points_home[i] <- win_points_home
ratings_df$win_points_away[i] <- win_points_away
ratings_df$expected_prob_home[i] <- expected_prob_home
ratings_df$expected_prob_away[i] <- expected_prob_away
ratings_df$mov_home[i] <- mov_home
ratings_df$mov_away[i] <- mov_away
ratings_df$elo_home_prev[i] <- elo_home
ratings_df$elo_away_prev[i] <- elo_away
ratings_df$elo_home_new[i] <- elo_home_new
ratings_df$elo_away_new[i] <- elo_away_new
ratings_df$prob_pred[i] <- prob_pred
}
ratings_df <- ratings_df %>%
mutate(
winner = ifelse(score > opp_score, team, opp),
winner_pred = ifelse(elo_home_prev + home_adv >= elo_away_prev,
team, opp),
correct_pred = ifelse(winner == winner_pred, TRUE, FALSE)
)
list(ratings_df = ratings_df, team_elo = team_ratings)
}
# Algorithm for several seasons
getElo <- function(df, k, home_adv, s, carry) {
df <- arrange(df, season)
season_results <- split(df, df$season)
# Start with first season
teams <- sort(unique(season_results[[1]]$team))
# Since it is the first season overall, all teams start with 1300 Elo points
initial_ratings <- as.list(rep(1300, length(teams)))
names(initial_ratings) <- teams
first_season_ratings <- getSeasonElo(
season_results[[1]],
k = k,
home_adv = home_adv,
s = s,
initial_elo = initial_ratings
)
# TODO: Use the last recorded Elo rating, not last season
elo_final <- tibble(
season = unique(season_results[[1]]$season),
team = names(first_season_ratings$team_elo),
elo = unlist(first_season_ratings$team_elo)
)
season_ratings <- vector("list", length(season_results))
season_ratings[[1]] <- first_season_ratings
for (i in 2:length(season_ratings)) {
teams <- sort(unique(season_results[[i]]$team))
teams_new <- teams[!teams %in% elo_final$team]
teams_new_elo <- as.list(rep(1300, length(teams_new)))
names(teams_new_elo) <- teams_new
teams_old <- teams[teams %in% elo_final$team]
teams_old_elo <- vector("list", length(teams_old))
names(teams_old_elo) <- teams_old
for (j in seq_along(teams_old)) {
elo_team <- elo_final %>%
filter(team == teams_old[j])
teams_old_elo[[j]] <- elo_team$elo[which.max(elo_team$season)]
}
teams_old_elo <- lapply(teams_old_elo, getCarryOver, c = carry)
initial_elo <- c(teams_new_elo, teams_old_elo)
season_ratings[[i]] <- getSeasonElo(
season_results[[i]],
k = k,
home_adv = home_adv,
s = s,
initial_elo = initial_elo
)
elo_final_season <- tibble(
season = unique(season_results[[i]]$season),
team = names(season_ratings[[i]]$team_elo),
elo = unlist(season_ratings[[i]]$team_elo)
)
elo_final <- bind_rows(elo_final, elo_final_season)
}
output_df <- map_df(season_ratings, function(x) x$ratings_df)
output_df
}
# Tunning -----------------------------------------------------------------
# Grid optimization
k <- seq(10, 50, by = 5)
h <- seq(0, 150, by = 25)
c <- seq(0.5, 1, by = 0.1)
grid_df <- expand_grid(k, h, c)
checkAccuracy <- function(df, k, h, c) {
df <- getElo(df, k, h, s = 400, c)
sum(df$correct_pred) / nrow(df)
}
n <- nrow(grid_df)
accuracy <- numeric(n)
for (i in 1:n) {
acc <- checkAccuracy(
df = results,
k = grid_df$k[i],
h = grid_df$h[i],
c = grid_df$c[i]
)
accuracy[i] <- acc
}
acc_df <- cbind(grid_df, accuracy) %>%
as_tibble() %>%
arrange(desc(accuracy))
# Ratings -----------------------------------------------------------------
elo_df <- getElo(plldf2, k = 25, home_adv = 100, s = 400, carry = 0.8)
elo_summary <- getEloSummary(elo_df) %>%
left_join(teaminfo, by = c("team" = "team", "season"))
tester <- getSeasonElo(
plldf3,
k = 25,
home_adv = 100,
s = 400,
initial_elo = initial_ratings)
getEloSummary(tester)
elo_summary %>%
ggplot(aes(order, elo, group = team_id)) +
geom_line()
but when I attempt to run the 'getElo' function to optimize the model it says there is a incorrect number of probabilities and I get the error below. However when I subset my data and run it for a single season using the 'getSeasonElo' function it computes it without issue. I assumed the problem was coming from the 'size' parameter being set to 1 by default, which I've fixed to account for the multi-season calculation, but the incorrect number is still happening? Not sure what I missed.
prob_pred <- sample(
x = c(team_home, team_away),
size = length(list(expected_prob_home,expected_prob_away)),
prob = c(expected_prob_home, expected_prob_away) #ISSUE HERE
)

Error when trying to add a custom position adjustment to a package in ggplot2

I am trying to create position adjustments for ggplot2 that explicitly control the way points are spread out along the (x)-axis (rather than just adding a random jitter). I successfully used the examples position_jitter and position_jitterdodge to create simple variants that should for my purposes:
PositionSpread <- ggproto("PositionSpread", Position,
required_aes = c("x", "y"),
setup_params = function(self, data) {
list(
sep = self$sep %||% (resolution(data$x, zero = FALSE) * .05),
max_width = self$max_width %||% (resolution(data$x, zero = FALSE) * 0.4)
)
},
compute_layer = function(self, data, params, layout) {
f <- function(sep, i, n) {
m <- ceiling(max(n) / 2)
ifelse(
as.logical(n %% 2),
sep * c(0, rep(1:m, each = 2) * rep(c(-1, 1), m))[i],
sep * (rep(1:m, each = 2)[i] - 1 / 2) * rep(c(-1, 1), m)
)
}
trans_x <- if(params$max_width > 0) function(df) {
df |>
group_by(x, y) |>
mutate(
i = 1:n(),
n = n(),
sep = pmin(params$sep, 2 * params$max_width / n),
adj = f(sep, i, n)
) |>
magrittr::extract("adj")
}
x_aes <- intersect(ggplot2:::ggplot_global$x_aes, names(data))
y_aes <- intersect(ggplot2:::ggplot_global$y_aes, names(data))
x <- if (length(x_aes) == 0) 0 else data[[x_aes[1]]]
y <- if (length(y_aes) == 0) 0 else data[[y_aes[1]]]
dummy_data <- vctrs::new_data_frame(list(x = x, y = y), nrow(data))
x_adj <- trans_x(dummy_data)
# Apply jitter
transform_position(data, function(x) x + x_adj)
}
)
position_spread <- function(max_width = NULL, sep = NULL) {
ggproto(NULL, PositionSpread,
max_width = max_width,
sep = sep
)
}
and
position_spreaddodge <- function(spread.width = NULL, spread.sep = NULL,
dodge.width = 0.75) {
ggplot2::ggproto(NULL, PositionSpreaddodge,
spread.width = spread.width,
spread.sep = spread.sep,
dodge.width = dodge.width
)
}
PositionSpreaddodge <- ggproto("PositionSpreaddodge", Position,
spread.width = NULL,
spread.sep = NULL,
dodge.width = NULL,
required_aes = c("x", "y"),
setup_params = function(self, data) {
flipped_aes <- has_flipped_aes(data)
data <- flip_data(data, flipped_aes)
spread.width <- self$spread.width %||% (resolution(data$x, zero = FALSE) * 0.4)
# Adjust the x transformation based on the number of 'dodge' variables
dodgecols <- intersect(c("fill", "colour", "linetype", "shape", "size", "alpha"), colnames(data))
if (length(dodgecols) == 0) {
abort("`position_jitterdodge()` requires at least one aesthetic to dodge by")
}
ndodge <- lapply(data[dodgecols], levels) # returns NULL for numeric, i.e. non-dodge layers
ndodge <- length(unique(unlist(ndodge)))
list(
dodge.width = self$dodge.width,
spread.sep = self$spread.sep %||% (resolution(data$x, zero = FALSE) * .05),
spread.width = spread.width / (ndodge + 2),
flipped_aes = flipped_aes
)
},
compute_panel = function(data, params, scales) {
data <- flip_data(data, params$flipped_aes)
data <- ggplot2:::collide(data, params$dodge.width, "position_jitterdodge", ggplot2:::pos_dodge,
check.width = FALSE)
f <- function(sep, i, n) {
m <- ceiling(max(n) / 2)
ifelse(
as.logical(n %% 2),
sep * c(0, rep(1:m, each = 2) * rep(c(-1, 1), m))[i],
sep * (rep(1:m, each = 2)[i] - 1 / 2) * rep(c(-1, 1), m)
)
}
trans_x <- if(params$spread.width > 0) function(df) {
df |>
group_by(x, y) |>
mutate(
i = 1:n(),
n = n(),
sep = pmin(params$spread.sep, 2 * params$spread.width / n),
adj = f(sep, i, n)
) |>
magrittr::extract2("adj")
}
x_aes <- intersect(ggplot2:::ggplot_global$x_aes, names(data))
y_aes <- intersect(ggplot2:::ggplot_global$y_aes, names(data))
x <- if (length(x_aes) == 0) 0 else data[[x_aes[1]]]
y <- if (length(y_aes) == 0) 0 else data[[y_aes[1]]]
dummy_data <- vctrs::new_data_frame(list(x = x, y = y), nrow(data))
x_adj <- trans_x(dummy_data) |> unclass()
# Apply Spread
data <- transform_position(data, function(x) x + x_adj)
flip_data(data, params$flipped_aes)
}
)
These functions both appear to work fine. Now, I'm trying to add them to an internal package so that I have easy access to them across multiple projects, but I'm running into an issue with documentation. I've added Depends: ggplot2 (>= 3.0.0) in the DESCRIPTION file.
Using position_spreaddodge as example, I've added the following comments:
#' Simultaneously dodge and spread points
#'
#' This is primarily used for aligning points generated through
#' `geom_point()` with dodged boxplots (e.g., a `geom_boxplot()` with
#' a fill aesthetic supplied).
#'
#' #family position adjustments
#' #param spread.width degree of spread in x direction. Once points get spread to this amount, the space between points starts being compressed
#' #param spread.sep separation between points before compression.
#' #param dodge.width the amount to dodge in the x direction. Defaults to 0.75,
#' the default `position_dodge()` width.
#' #export
position_spreaddodge <- function(spread.width = NULL, spread.sep = NULL,
dodge.width = 0.75) {
ggplot2::ggproto(NULL, PositionSpreaddodge,
spread.width = spread.width,
spread.sep = spread.sep,
dodge.width = dodge.width
)
}
#' #format NULL
#' #usage NULL
#' #export
PositionSpreaddodge <- ggproto("PositionSpreaddodge", Position,
spread.width = NULL,
spread.sep = NULL,
dodge.width = NULL,
required_aes = c("x", "y"),
setup_params = function(self, data) {
flipped_aes <- has_flipped_aes(data)
data <- flip_data(data, flipped_aes)
spread.width <- self$spread.width %||% (resolution(data$x, zero = FALSE) * 0.4)
# Adjust the x transformation based on the number of 'dodge' variables
dodgecols <- intersect(c("fill", "colour", "linetype", "shape", "size", "alpha"), colnames(data))
if (length(dodgecols) == 0) {
abort("`position_jitterdodge()` requires at least one aesthetic to dodge by")
}
ndodge <- lapply(data[dodgecols], levels) # returns NULL for numeric, i.e. non-dodge layers
ndodge <- length(unique(unlist(ndodge)))
list(
dodge.width = self$dodge.width,
spread.sep = self$spread.sep %||% (resolution(data$x, zero = FALSE) * .05),
spread.width = spread.width / (ndodge + 2),
flipped_aes = flipped_aes
)
},
compute_panel = function(data, params, scales) {
data <- flip_data(data, params$flipped_aes)
data <- ggplot2:::collide(data, params$dodge.width, "position_jitterdodge", ggplot2:::pos_dodge,
check.width = FALSE)
f <- function(sep, i, n) {
m <- ceiling(max(n) / 2)
ifelse(
as.logical(n %% 2),
sep * c(0, rep(1:m, each = 2) * rep(c(-1, 1), m))[i],
sep * (rep(1:m, each = 2)[i] - 1 / 2) * rep(c(-1, 1), m)
)
}
trans_x <- if(params$spread.width > 0) function(df) {
df |>
group_by(x, y) |>
mutate(
i = 1:n(),
n = n(),
sep = pmin(params$spread.sep, 2 * params$spread.width / n),
adj = f(sep, i, n)
) |>
magrittr::extract2("adj")
}
x_aes <- intersect(ggplot2:::ggplot_global$x_aes, names(data))
y_aes <- intersect(ggplot2:::ggplot_global$y_aes, names(data))
x <- if (length(x_aes) == 0) 0 else data[[x_aes[1]]]
y <- if (length(y_aes) == 0) 0 else data[[y_aes[1]]]
dummy_data <- vctrs::new_data_frame(list(x = x, y = y), nrow(data))
x_adj <- trans_x(dummy_data) |> unclass()
# Apply Spread
data <- transform_position(data, function(x) x + x_adj)
flip_data(data, params$flipped_aes)
}
)
When trying to run document() with either of these functions in the package, I get the error "Error: _inherit must be a ggproto object." I cannot figure out what the error is trying to tell me, which makes it impossible to fix. It's clearly an issue with the documentation I've added, but I'm not sure what I'm missing.
Thanks!

Aggregate Weighted Linestrings for Clustered Markers in Leaflet in R

I'm trying to plot locations and weighted connecting linestrings. When I zoom in or out the clustering of the markers adjusts fine. The shown labels of the clusters are the aggregated node_val of the markers.
I would like to do similar with the linestrings, so that
the plot does not show the blue lines connecting the single markers, but instead lines connecting the clusters of markers, and
the new linestrings that connect the clusters of markers are customized in width dependent on the wgt variable.
I hope the code below demonstrates the problem:
library(dplyr)
library(leaflet)
library(sf)
set.seed(123)
N <- 1000
N_conn <- 100
# data frame for points
df_points <- data.frame(id = 1:N,
lng = sample(c(11.579657, 16.370654), N, TRUE) + rnorm(N, 0, 0.5),
lat = sample(c(48.168889, 48.208087), N, TRUE) + rnorm(N, 0, 0.5),
node_val = sample(10, N, TRUE))
# data frame for connections
df_conn <- data.frame(id_from = sample(N_conn, replace = TRUE),
id_to = sample(N_conn, replace = TRUE),
wgt = abs(rnorm(N_conn)))
# drop connections where from and to ids are identical
df_conn <- subset(df_conn, id_from != id_to)
# add the coordinates for the connections (merging is not neccessary due to ordering of synth data)
df_conn$lat_from <- df_points[df_conn$id_from, "lat"]
df_conn$lng_from <- df_points[df_conn$id_from, "lng"]
df_conn$lat_to <- df_points[df_conn$id_to, "lat"]
df_conn$lng_to <- df_points[df_conn$id_to, "lng"]
sf_conn_from <- df_conn %>%
st_as_sf(coords=c("lng_from", "lat_from"))
sf_conn_to <- df_conn %>%
st_as_sf(coords=c("lng_to", "lat_to"))
sf_conn <- st_combine(cbind(sf_conn_from, sf_conn_to)) %>%
st_cast("LINESTRING")
st_crs(sf_conn) <- 4326
leaflet(df_points) %>%
addTiles() %>%
addMarkers(options = markerOptions(node_val = ~node_val),
label = quakes$mag,
clusterOptions = markerClusterOptions(
iconCreateFunction=JS("function (cluster) {
var markers = cluster.getAllChildMarkers();
var sum = 0;
for (i = 0; i < markers.length; i++) {
sum += Number(markers[i].options.node_val);
//sum += 1;
}
sum = Math.round(sum);
return new L.DivIcon({ html: '<div><span>' + sum + '</span></div>',
className: 'marker-cluster marker-cluster-medium',
iconSize: new L.Point(40,40)});
}")
)) %>%
leafem::addFeatures(data = sf_conn,
color = 'blue',#~pal(rel_full$N_scale),#
weight = 1)
Thanks to the contributers of these two questions:
leaflet R, how to make appearance of clustered icon related to statistics of the children?
Shiny leaflet add large amount of separated polylines
This is a partial solution for adjusting the weighting of the lines, I can't help clustering those lines :(
library(dplyr)
library(leaflet)
library(sf)
set.seed(123)
N <- 1000
N_conn <- 100
# data frame for points
df_points <- data.frame(id = 1:N,
lng = sample(c(11.579657, 16.370654), N, TRUE) + rnorm(N, 0, 0.5),
lat = sample(c(48.168889, 48.208087), N, TRUE) + rnorm(N, 0, 0.5),
node_val = sample(10, N, TRUE))
# data frame for connections
df_conn <- data.frame(id_from = sample(N_conn, replace = TRUE),
id_to = sample(N_conn, replace = TRUE),
wgt = abs(rnorm(N_conn)))
# drop connections where from and to ids are identical
df_conn <- subset(df_conn, id_from != id_to)
# add the coordinates for the connections (merging is not neccessary due to ordering of synth data)
df_conn$lat_from <- df_points[df_conn$id_from, "lat"]
df_conn$lng_from <- df_points[df_conn$id_from, "lng"]
df_conn$lat_to <- df_points[df_conn$id_to, "lat"]
df_conn$lng_to <- df_points[df_conn$id_to, "lng"]
geom <- lapply(1:nrow(df_conn),
function(i)
rbind(
as.numeric(df_conn[i, c("lng_from","lat_from")]),
as.numeric(df_conn[i, c("lng_to","lat_to")])
)
) %>%
st_multilinestring() %>%
st_sfc(crs = 4326) %>%
st_cast("LINESTRING")
sf_conn <- st_sf(df_conn,
geometry=geom)
#Modify weighting
sf_conn$cut=exp(sf_conn$wgt-1)
leaflet(df_points) %>%
addTiles() %>%
addMarkers(
options = markerOptions(node_val = ~ node_val),
label = quakes$mag,
clusterOptions = markerClusterOptions(
iconCreateFunction = JS(
"function (cluster) {
var markers = cluster.getAllChildMarkers();
var sum = 0;
for (i = 0; i < markers.length; i++) {
sum += Number(markers[i].options.node_val);
//sum += 1;
}
sum = Math.round(sum);
return new L.DivIcon({ html: '<div><span>' + sum + '</span></div>',
className: 'marker-cluster marker-cluster-medium',
iconSize: new L.Point(40,40)});
}"
)
)
) %>% addPolylines(weight = sf_conn$cut,
data = sf_conn,
col = "blue")

OSRM: Why is the traveltime of route A -> B a factor 2 off route's traveltime B -> A?

As said: it even differs by a factor of 2 in time! How is that possible?
I found this issue but it seems it is still there?
It turns out that the highway is only taken in on direction (See leaflet map from map_route. Do I miss anything?
Here is a reproducible example:
wd <- getwd()
setwd("C:/OSRM_API5")
shell(paste0("osrm-routed ", "switzerland-latest.osrm", " >nul 2>nul"), wait = F)
Sys.sleep(3) # OSRM needs time
setwd(wd)
k1 <- 46.99917
k2 <- 8.610048
k3 <- 47.05398
k4 <- 8.530232
r1 <- viaroute5_2(k1, k2, k3, k4)
r1$routes[[1]]$duration
# [1] 598.2
geometry <- decode_geom(r1$routes[[1]]$geometry, 5)
map_route(geometry)
r2 <- viaroute5_2(k3, k4,k1, k2)
r2$routes[[1]]$duration
# [1] 1302
geometry <- decode_geom(r2$routes[[1]]$geometry, 5)
map_route(geometry)
shell("TaskKill /F /IM osrm-routed.exe >nul 2>nul")
Here are the functions you need:
viaroute5_2 <- function(lat1, lng1, lat2, lng2) {
# address <- "http://localhost:5000" # this should work without a local server
address <- "http://localhost:5000"
request <- paste(address, "/route/v1/driving/",
lng1, ",", lat1, ";", lng2, ",", lat2,
"?overview=full", sep = "", NULL)
R.utils::withTimeout({
repeat {
res <- try(
route <- rjson::fromJSON(
file = request))
if (class(res) != "try-error") {
if (!is.null(res)) {
break
} else {
stop("???")
}
}
}
}, timeout = 1, onTimeout = "warning")
if (res$code == "Ok") {
return(res)
} else {
t_guess <- 16*60
warning("Route not found: ", paste(lat1, lng1, lat2, lng2, collapse = ", "),
". Time set to ", t_guess/60 , " min.")
}
}
decode_geom <- function(encoded, precision = stop("a numeric, either 5 or 6")) {
if (precision == 5) {
scale <- 1e-5
} else if (precision == 6) {
scale <- 1e-6
} else {
stop("precision not set to 5 or 6")
}
len = stringr::str_length(encoded)
encoded <- strsplit(encoded, NULL)[[1]]
index = 1
N <- 100000
df.index <- 1
array = matrix(nrow = N, ncol = 2)
lat <- dlat <- lng <- dlnt <- b <- shift <- result <- 0
while (index <= len) {
shift <- result <- 0
repeat {
b = as.integer(charToRaw(encoded[index])) - 63
index <- index + 1
result = bitops::bitOr(result, bitops::bitShiftL(bitops::bitAnd(b, 0x1f), shift))
shift = shift + 5
if (b < 0x20) break
}
dlat = ifelse(bitops::bitAnd(result, 1),
-(result - (bitops::bitShiftR(result, 1))),
bitops::bitShiftR(result, 1))
lat = lat + dlat;
shift <- result <- b <- 0
repeat {
b = as.integer(charToRaw(encoded[index])) - 63
index <- index + 1
result = bitops::bitOr(result, bitops::bitShiftL(bitops::bitAnd(b, 0x1f), shift))
shift = shift + 5
if (b < 0x20) break
}
dlng = ifelse(bitops::bitAnd(result, 1),
-(result - (bitops::bitShiftR(result, 1))),
bitops::bitShiftR(result, 1))
lng = lng + dlng
array[df.index,] <- c(lat = lat * scale, lng = lng * scale)
df.index <- df.index + 1
}
geometry <- data.frame(array[1:df.index - 1,])
names(geometry) <- c("lat", "lng")
return(geometry)
}
map <- function() {
library(leaflet)
m <- leaflet() %>%
addTiles() %>%
addProviderTiles(providers$OpenStreetMap, group = "OSM") %>%
addProviderTiles(providers$Stamen.TonerLite, group = "Toner Lite") %>%
addLayersControl(baseGroups = c("OSM", "Toner Lite"))
return(m)
}
map_route <- function(geometry) { # Which parameters make sence? osrm inside or outside?
m <- map()
m <- addCircleMarkers(map = m,
lat = geometry$lat[1],
lng = geometry$lng[1],
color = imsbasics::fhs(),
popup = paste("Source"),
stroke = FALSE,
radius = 6,
fillOpacity = 0.8) %>%
addCircleMarkers(lat = geometry$lat[nrow(geometry)],
lng = geometry$lng[nrow(geometry)],
color = imsbasics::fhs(),
popup = paste("Destination"),
stroke = FALSE,
radius = 6,
fillOpacity = 0.8) %>%
addPolylines(lat = geometry$lat, lng = geometry$lng, color = "red", weight = 4) %>%
addLayersControl(baseGroups = c("OSM", "Stamen.TonerLite"))
return(m)
}
The answer is: Because OSRM searches by default a nearest point and searches one route from that point. If your coordinates are slightly north a highway, OSRM will only drive westbound (considering you're drive on the right side as we do in Europe..).
So in your example the point upleft is just a bit north of the highway and therefore when searching from that point OSRM takes quite a bit of a detour.
The following example shows this:
osrmr::run_server("switzerland-latest", "C:/OSRM_API5")
lat1 <- 46.99917
lng1 <- 8.610048
lat2 <- 47.05398
lng2 <- 8.530232
res1 <- osrmr::viaroute(lat1, lng1, lat2, lng2, instructions = TRUE, api_version = 5, localhost = TRUE)
res2 <- osrmr::viaroute(lat2, lng2, lat1, lng1, instructions = TRUE, api_version = 5, localhost = TRUE)
res1$routes[[1]]$duration
# [1] 598.2
res2$routes[[1]]$duration
# [1] 1302
map_route(decode_geom(res1$routes[[1]]$geometry, 5))
map_route(decode_geom(res2$routes[[1]]$geometry, 5))
lat1 <- 46.99917
lng1 <- 8.610048
lat2 <- 47.051 # setting that point a bit more south changes the results to the opposite..
lng2 <- 8.530232
res1 <- osrmr::viaroute(lat1, lng1, lat2, lng2, instructions = TRUE, api_version = 5, localhost = TRUE)
res2 <- osrmr::viaroute(lat2, lng2, lat1, lng1, instructions = TRUE, api_version = 5, localhost = TRUE)
res1$routes[[1]]$duration
# [1] 1307.5
res2$routes[[1]]$duration
# [1] 592.7
map_route(decode_geom(res1$routes[[1]]$geometry, 5))
map_route(decode_geom(res2$routes[[1]]$geometry, 5))
osrmr::quit_server()
As you can see, setting the second point a bit more south inverts the results. Now the other way takes quite a bit longer.
As discussed for example here the radiuses option might provide a solution to that problem. I couldn't however figure out to get that to work on your example..
Or maybe (simpler..) you want to calculate both directions and take the shorter duration?
What's best really depends on your algorithmic problem..

How do I get ggmap route data to follow road path

I am using the ggmap route function to calculate and visualize hundreds of routes using D.C. Capital Bikeshare data. I am successfully able to do this with one minor problem, the route path doesn't follow roads, particularly curved roads (see screenshot below). Is there a way to tweek my code to all for more detailed paths?
library(tidyverse)
library(ggmap)
# Example dataset
feb_14 <- read.csv('https://raw.githubusercontent.com/smitty1788/Personal-Website/master/dl/CaBi_Feb_2017.csv', stringsAsFactors = FALSE)
# Subset first 300 rows, keep start and end Lat/Long strings
start<-c(feb_14[1:300, 14])
dest<-c(feb_14[1:300, 15])
# df of individual routes
routes <- tibble(
start,
dest)
# Function to calculate route
calculationroute <- function(startingpoint, stoppoint) {
route(from = startingpoint,
to = stoppoint,
mode = 'bicycling',
structure = "route")}
# Calculate route path for all individual trips
calculatedroutes <- mapply(calculationroute,
startingpoint = routes$start,
stoppoint = routes$dest,
SIMPLIFY = FALSE)
# Unlist and merge in single dataframe
do.call(rbind.data.frame, lapply(names(calculatedroutes), function(x) {
cbind.data.frame(route=x, calculatedroutes[[x]], stringsAsFactors=FALSE)
})) -> long_routes
# create map with routes
basicmap <- get_map(location = 'washingtondc',
zoom = 13,
maptype = "toner-background",
source = "google",
color = "bw")
basicmap <- ggmap(basicmap)
basicmap + geom_path(data=long_routes,
aes(x=lon, y=lat, group=route), color = "red",
size=1, alpha = .4, lineend = "round")
The answer was to place the decodeLine function into the do.call to create the long routes dataframe
decodeLine <- function(encoded){
require(bitops)
vlen <- nchar(encoded)
vindex <- 0
varray <- NULL
vlat <- 0
vlng <- 0
while(vindex < vlen){
vb <- NULL
vshift <- 0
vresult <- 0
repeat{
if(vindex + 1 <= vlen){
vindex <- vindex + 1
vb <- as.integer(charToRaw(substr(encoded, vindex, vindex))) - 63
}
vresult <- bitOr(vresult, bitShiftL(bitAnd(vb, 31), vshift))
vshift <- vshift + 5
if(vb < 32) break
}
dlat <- ifelse(
bitAnd(vresult, 1)
, -(bitShiftR(vresult, 1)+1)
, bitShiftR(vresult, 1)
)
vlat <- vlat + dlat
vshift <- 0
vresult <- 0
repeat{
if(vindex + 1 <= vlen) {
vindex <- vindex+1
vb <- as.integer(charToRaw(substr(encoded, vindex, vindex))) - 63
}
vresult <- bitOr(vresult, bitShiftL(bitAnd(vb, 31), vshift))
vshift <- vshift + 5
if(vb < 32) break
}
dlng <- ifelse(
bitAnd(vresult, 1)
, -(bitShiftR(vresult, 1)+1)
, bitShiftR(vresult, 1)
)
vlng <- vlng + dlng
varray <- rbind(varray, c(vlat * 1e-5, vlng * 1e-5))
}
coords <- data.frame(varray)
names(coords) <- c("lat", "lon")
coords
}
calculatedroutes <- mapply(calculationroute,
startingpoint = routes$start,
stoppoint = routes$dest,
SIMPLIFY = FALSE)
do.call(rbind.data.frame, lapply(names(calculatedroutes), function(x) {
cbind.data.frame(route = x, decodeLine(calculatedroutes[[x]]$routes[[1]]$overview_polyline$points), stringsAsFactors=FALSE)
})) -> long_routes

Resources