5

I am working with the R programming language.

Using the "leaflet" library, I made the following map for these 5 cities:

library(dplyr)
library(leaflet)

map_data <- data.frame("Lat" = c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629), "Long" = c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957 ), type = c(1,2,3,4,5))

map_data$type = as.factor(map_data$type)



leaflet(map_data) %>%
    addTiles() %>% addCircleMarkers(stroke = FALSE, label = ~type,fillOpacity = 0.8, labelOptions = labelOptions(direction = "center",style = list('color' = "white"),noHide = TRUE, offset=c(0,0), fill = TRUE, opacity = 1, weight = 10, textOnly = TRUE))

On this above map that I have created, I would now like to "connect" all these "points" (i.e. cities) on the map (in a route) based on their "number" (e.g. connect 1 with 2, 2 with 3, 3 with 4, 4 with 5, 5 with 1), and output the "total distance" of the route. I found a previous post that shows how to do this: How to show path and distance on map with leaflet, shiny apps?

I tried to adapt the code from this post to suit my question:

library(osrm)

route = osrmRoute(c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957 ), c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629),  overview = 'full')


route_summary = osrmRoute(c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957 ), c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629), overview = FALSE)

leaflet() %>% addTiles() %>% 
    addCircleMarkers(c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957 ), c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629), stroke = FALSE, label = ~type,fillOpacity = 0.8, 
                     labelOptions = labelOptions(direction = "center",style = list('color' = "white"),noHide = TRUE, offset=c(0,0), fill = TRUE, opacity = 1, weight = 10, textOnly = TRUE)) %>% 
    addPolylines(route$lon,route$lat, 
                 label = paste(round(route_summary[1]/60), 'hr - ', round(route_summary[2]), 'km'), 
                 labelOptions = labelOptions(noHide = TRUE))

But this returns the following error:

Error in UseMethod("metaData") : 
  no applicable method for 'metaData' applied to an object of class "NULL"

Can someone please show me how to fix this problem?

I would like to do this using "leaflet" and not using "rshiny". In the end, I would like the final map to look something like this (this is supposed to represent a "single path" from a Travelling Salesman Problem) :

[![enter image description here][2]][2]

Note: I am starting to think that problem might be that the "osrmRoute()" function might not be able to work for more than 2 points?

stats_noob
  • 5,401
  • 4
  • 27
  • 83
  • 1
    For that you don't need OSRM because that's not based on road network + road restrictions + lua profile + ... its just basic point connection. You can do that using `sf` package – det Feb 23 '22 at 21:54
  • @ det: thank you for your reply! I have never used the sf package before, i will start reading about it. Is it compatible with the base leaflet map in this problem? Thank you so much! – stats_noob Feb 23 '22 at 22:00
  • Is it possible to adapt the code from this post over here? https://stackoverflow.com/questions/32275213/how-do-i-connect-two-coordinates-with-a-line-using-leaflet-in-r I will try this! – stats_noob Feb 23 '22 at 22:01
  • see edit in my post – det Feb 23 '22 at 22:31
  • @ Det: thank you so much for your edit! I played around with your code to get the total distance displayed for the trip on each connection - is what I did correct? Thank you so much for all your help! – stats_noob Feb 24 '22 at 01:25
  • yeah it looks OK, that is what you asked for after all. – det Feb 24 '22 at 08:02

2 Answers2

7

One way is for you to make API call:

https://github.com/Project-OSRM/osrm-backend/blob/master/docs/http.md

I'll just outline how can you do it:

data

df <- data.frame(
  lon = c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957), 
  lat = c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629)
)

url call

root <- "http://router.project-osrm.org/route/v1/driving/"

options <- c(
  continue_straight = "true",
  overview = "full",
  annotations = "true",
  steps = "true"
) 
  
coords <- df %>% 
  slice(c(seq_len(n()), 1)) %>%
  pmap_chr(str_c, sep = ",") %>% str_c(collapse = ";")
options <- options %>%
  imap_chr(~str_c(.y, "=", .x)) %>%
  str_c(collapse = "&") %>%
  str_c("?", .)

res <- rjson::fromJSON(file = str_c(root, coords, options))

Note that I've added first point as 6th row to make circle route.

map

res$routes[[1]]$geometry %>%
  googlePolylines::decode() %>%
  .[[1]] %>%
  leaflet() %>%
  addTiles() %>%
  addPolylines(lng = ~lon, lat = ~lat) %>%
  addCircleMarkers(
    data = df,
    stroke = FALSE, 
    label = seq_len(nrow(df)),
    fillOpacity = 0.8, 
    labelOptions = labelOptions(
      direction = "center",
      style = list('color' = "white"),
      noHide = TRUE, 
      offset=c(0,0), 
      fill = TRUE, 
      opacity = 1, 
      weight = 10, 
      textOnly = TRUE
    )
  )   

distance

res$routes[[1]]$distance

This is in meters (documentation)

EDIT

There probably is better way of labeling polyline but I don't have time now:

library(sf)

segment_df <- df %>% rbind(df[1,])

d <- segment_df %>%
  st_as_sf(coords = c("lon", "lat"), crs = 4326) %>%
  {st_distance(.[-6,], .[-1,], by_element = TRUE)} %>%
  as.vector() %>%
  round()

m <- leaflet() %>% addTiles()
for(i in seq_len(nrow(segment_df) - 1))
  m <- m %>% addPolylines(
    data = segment_df[i:(i+1),], 
    lng = ~lon, lat = ~lat, color = "red", label = paste(d[[i]], "m"),
    labelOptions(noHide = TRUE, direction = 'top')
  )

m <- m %>% addCircleMarkers(
    data = df,
    stroke = FALSE, 
    label = seq_len(nrow(df)),
    fillOpacity = 0.8, 
    labelOptions = labelOptions(
      direction = "center",
      style = list('color' = "white"),
      noHide = TRUE, 
      offset=c(0,0), 
      fill = TRUE, 
      opacity = 1, 
      weight = 10, 
      textOnly = TRUE
    )
  ) 

If you want only to show total distance then that is easier and does not require loop, just replace loop with:

segment_df %>%
  leaflet() %>%
  addTiles() %>%
  addPolylines(
    lng = ~lon, lat = ~lat, color = "red", 
    label = paste(sum(d), "m"),
    labelOptions = labelOptions(noHide = TRUE, direction = 'top')
  )

I hope you understand (and see from map) that this is not drivable.

stats_noob
  • 5,401
  • 4
  • 27
  • 83
det
  • 5,013
  • 1
  • 8
  • 16
  • @ det: thank you so much for your answer! Just a few things that I wanted to ask: – stats_noob Feb 20 '22 at 20:36
  • 1) Is it possible to use the same "icons" that I used? Is it possible to keep the numbers on the "circle icons"? So that it looks like this? https://i.stack.imgur.com/Pu2hM.jpg – stats_noob Feb 20 '22 at 20:38
  • 2) Is it possible to show the distance on the map itself? Just like what was shown in the stackoverflow post I linked (https://i.stack.imgur.com/Pu2hM.jpg)? – stats_noob Feb 20 '22 at 20:40
  • 3) Finally, what I meant by "circular route" - I just wanted to make "routes" similar to the Travelling Salesman Problem - something that looks like this: https://physics.aps.org/assets/a38de7c6-00ac-45fb-9bcf-3b3e14a72b41/es32_1.png . What I am trying to do is connect each point to the next point: ( 1,2), (2,3), (3,4), (4,5), (5,1) . The route itself doesnt have to be "circular" - just connect the points in the right order – stats_noob Feb 20 '22 at 20:42
  • 1
    Don't see reason why not just replace `addCircles` with `addCirclesMarkers` like you have in code. `res$routes[[1]]$legs %>% map_dbl("distance")` will give you distances between two successive points. The route is circular, that does not mean it wont cross itself. The route you get will be the fastest route that visits your point in order with the option `continue straight` which you can set to false. – det Feb 20 '22 at 20:44
  • @ det: I have been trying to get this to work for the past few hours but without any luck. can you please take a look at this if you have time? – stats_noob Feb 21 '22 at 01:55
  • I made a change to the data frame to include the "type" variable (i.e. a variable that assigns a number to each city): – stats_noob Feb 21 '22 at 01:55
  • df <- data.frame( lon = c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957), lat = c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629), type = c(1,2,3,4,5) ) ;df$type = as.factor(df$type) – stats_noob Feb 21 '22 at 01:55
  • I then tried to replace addCircles with addCircleMarkers: – stats_noob Feb 21 '22 at 01:56
  • res$routes[[1]]$geometry %>% googlePolylines::decode() %>% .[[1]] %>% leaflet() %>% addTiles() %>% addCircleMarkers(stroke = FALSE, fillOpacity = 0.8, label = ~type, labelOptions = labelOptions(direction = "center",style = list('color' = "white"),noHide = TRUE, offset=c(0,0), fill = TRUE, opacity = 1, weight = 10, textOnly = TRUE)) addPolylines(lng = ~lon, lat = ~lat) %>% addCircles(data = df,radius = 50) – stats_noob Feb 21 '22 at 01:56
  • This returns the following errors: Error in eval(f[[2]], metaData(data), environment(f)) : object 'type' not found AND Assuming "lon" and "lat" are longitude and latitude, respectively Error in getMapData(map) : argument "map" is missing, with no default – stats_noob Feb 21 '22 at 01:57
  • can you please try and take a look at this if you have time? Thank you so much! – stats_noob Feb 21 '22 at 01:57
1

Here is an answer I tried based on @det's answer:

library(sf)
library(geosphere)
library(dplyr)
library(leaflet)
library(data.table)
library(VPF)



#add a 6th row that is equal to the 1st row -  so that the path loops back

   map_data <- data.frame("Lat" = c(43.6426, 43.6424, 43.6544, 43.6452, 43.6629, 43.6426), "Long" = c(-79.3871, -79.3860, -79.3807, -79.3806,-79.3957, -79.3871 ), type = c(1,2,3,4,5,1))

map_data$type = as.factor(map_data$type)


m1 = leaflet(map_data) %>% addTiles() %>% addCircleMarkers(stroke = FALSE, label = ~type,fillOpacity = 0.8, 
color = ~ifelse(type==1,"red","blue"), labelOptions = labelOptions(direction = "center",style = list('color' = "white"),
noHide = TRUE, offset=c(0,0), fill = TRUE, opacity = 1, weight = 10, textOnly = TRUE))


   m1 %>% addTiles() %>%
    addPolylines(data = map_data, lng = ~Long, lat = ~Lat, group = ~type)

Now, I want to calculate the total distance of the trip and have it displayed on the map:

#distances  (https://stackoverflow.com/questions/42119438/calculate-distance-between-two-long-lat-coordinates-in-a-dataframe)

result = rbind(
  cbind(map_data[1:nrow(map_data)-1,c(1,2)], map_data[-1,c(1,2)]),
  cbind(map_data[nrow(map_data), c(1,2)], map_data[1,c(1,2)])
)
colnames(result) <- c("start_lat", "start_long", "end_lat", "end_long")

result$id = as.factor(c(1,2,3,4,5,1))

result = data.frame(result)
 
for (i in 1:nrow(result)) {
    
    a<-result$start_long[i]
    b<-result$start_lat[i]
    c<-result$end_long[i]
    d<-result$end_lat[i]
    
    result$distance[i]<-distm(c(a,b),c(c,d), fun = distHaversine)
}

#total distance of trip in meters
d = result$distance

total_d = signif(sum(d),3)

m1 %>% addPolylines(
    data = map_data, 
    lng = ~Long, lat = ~Lat, color = "blue", label = paste0(total_d, " meters"),
    labelOptions(noHide = TRUE, direction = 'top')
  )

I think I finally got it - thanks so much @ Det!

stats_noob
  • 5,401
  • 4
  • 27
  • 83