Module 16 Spatial data and maps
Spatial data, also known as geospatial data, is a term used to describe any data related to or containing information about a specific location on a surface (often a map). We will consider distance matrix calculations for finding the shortest distance/travel time between a set of origins and destinations, geocoding which is the process of converting an address to geographic coordinates (latitude, longitude) and reverse geocoding which is the opposite process of converting a location as described by geographic coordinates (latitude, longitude) to a human-readable address or place name. Finally, we consider how to display spatial data on a map.
16.1 Learning outcomes
By the end of this module, you are expected to be able to:
- Calculate euclidean, manhattan, etc. distances.
- Calculate a distance matrix, i.e. shortest paths between places using the Google and Bing API.
- Geocode an address using the Google and Bing API.
- Add markers and routes (lines) on a map.
16.2 Services for obtatining spatial data
Often you need to connect to a service using an API for obtaining spatial data. The most common is Google and Bing (Microsoft) and to use the services you need an API key. Another service is also Here.
If you use Google Maps you can obtain an API key here. Modest to light use is free; however, you need a valid credit card. Note you must enable the APIs you intend to use. Google in fact has several services for geo-related solutions. For example, the Maps Static API provides map images, while the Geocoding API provides geocoding and reverse geocoding services. You need to enable the APIs before you use them. You will only need to do that once, and then they will be ready for you to use. Enabling the APIs just means clicking a few radio buttons on the Google Maps Platform web interface.
We will be using the ggmap package for Google services. You can add the API key using:
Note the key is stored in the environment object GGMAP_GOOGLE_API_KEY
. Moreover, it is saved in the file .Renviron
so that it is automatically reloaded when you restart R.
If you use Bing Maps you can obtain an API key here. No credit card is needed. You can add the API key using:
usethis::edit_r_environ() # opens the .Renviron file
Sys.setenv(BING_MAPS_API_KEY=[your key]) # so you don't have to restart R
Add the line BING_MAPS_API_KEY=[your key]
and save the file.
Note your API keys is private and unique to you, so be careful not to share it online!
16.3 Calculating distances
If you need to calculate euclidean, manhattan, etc. distances, you can use the dist
R function:
coord <- matrix(c(0,0, 0,1, 1,0, 1,1), ncol = 2, byrow = TRUE)
coord
#> [,1] [,2]
#> [1,] 0 0
#> [2,] 0 1
#> [3,] 1 0
#> [4,] 1 1
dist(coord)
#> 1 2 3
#> 2 1.00
#> 3 1.00 1.41
#> 4 1.41 1.00 1.00
as.matrix(dist(coord))
#> 1 2 3 4
#> 1 0.00 1.00 1.00 1.41
#> 2 1.00 0.00 1.41 1.00
#> 3 1.00 1.41 0.00 1.00
#> 4 1.41 1.00 1.00 0.00
However, euclidean distances are often a poor approximation of shortest path lengths.
16.3.1 Using Google Maps
Remember to have set your API key and activate the Distance Matrix API service on the Google Cloud Platform. We consider the following places:
library(ggmap)
#> ℹ Google's Terms of Service: <https://mapsplatform.google.com>
#> Stadia Maps' Terms of Service: <https://stadiamaps.com/terms-of-service/>
#> OpenStreetMap's Tile Usage Policy: <https://operations.osmfoundation.org/policies/tiles/>
#> ℹ Please cite ggmap if you use it! Use `citation("ggmap")` for details.
library(tidyverse)
dat <- tibble::tribble(
~Id, ~Shop, ~Address,
1L, "Bilka Esbjerg", "Stormgade 157, 6715 Esbjerg, Denmark",
2L, "Bilka Herning", "Golfvej 5, 7400 Herning, Denmark",
3L, "Bilka Hillerød", "Slotsarkaderne 26, 3400 Hillerød, Denmark",
4L, "Bilka Hjørring", "A.F Heidemannsvej 20, 9800 Hjørring, Denmark",
5L, "Bilka Holstebro", "Nyholmvej 20, 7500 Holstebro, Denmark",
)
dat
#> # A tibble: 5 × 3
#> Id Shop Address
#> <int> <chr> <chr>
#> 1 1 Bilka Esbjerg Stormgade 157, 6715 Esbjerg, Denmark
#> 2 2 Bilka Herning Golfvej 5, 7400 Herning, Denmark
#> 3 3 Bilka Hillerød Slotsarkaderne 26, 3400 Hillerød, Denmark
#> 4 4 Bilka Hjørring A.F Heidemannsvej 20, 9800 Hjørring, Denmark
#> 5 5 Bilka Holstebro Nyholmvej 20, 7500 Holstebro, Denmark
To calculate a distance matrix we use the mapdist
function:
mapdist(from = "Stormgade 157, 6715 Esbjerg, Denmark",
to = "Golfvej 5, 7400 Herning, Denmark")
#> ℹ <https://maps.googleapis.com/maps/api/distancematrix/json?origins=Stormgade+157,+6715+Esbjerg,+Denmark&destinations=Golfvej+5,+7400+Herning,+Denmark&key=xxx-RC0wa23c&mode=driving>
#> # A tibble: 1 × 9
#> from to m km miles seconds minutes hours mode
#> <chr> <chr> <int> <dbl> <dbl> <int> <dbl> <dbl> <chr>
#> 1 Stormgade 157, 6715 Esbjerg, Denmark Golfvej 5, 740… 87780 87.8 54.5 4336 72.3 1.20 driv…
Note Google returns results for the fastest path between the two points. Let us try to define a function which calculate all the distances:
#' Calculate the distance matrix in long format.
#'
#' @param address A vector of addresses.
#' @param mode Driving, bicycling, walking, or transit.
#' @param symmetric Use symmetric distances (half the number of queries).
#' @return A data frame with the results
#' @note The API returns results for the fastest route.
goo_calc_distances <- function(address, mode = "driving", symmetric = TRUE) {
datDist <- expand_grid(id_from = 1:length(address), id_to = 1:length(address))
datDist <- datDist |> filter(id_from != id_to)
if (symmetric) datDist <- datDist |> filter(id_from < id_to)
datDist <- datDist |> mutate(from = address[id_from], to = address[id_to])
res <- mapdist(from = datDist |> pull(from), to = datDist |> pull(to), mode = mode)
return(left_join(datDist, res, by = c("from", "to")))
}
datDistGoo <- goo_calc_distances(dat$Address)
#> ℹ <https://maps.googleapis.com/maps/api/distancematrix/json?origins=A.F+Heidemannsvej+20,+9800+Hj%C3%B8rring,+Denmark&destinations=Nyholmvej+20,+7500+Holstebro,+Denmark&key=xxx-RC0wa23c&mode=driving>
#> ℹ <https://maps.googleapis.com/maps/api/distancematrix/json?origins=Golfvej+5,+7400+Herning,+Denmark&destinations=Slotsarkaderne+26,+3400+Hiller%C3%B8d,+Denmark%7CA.F+Heidemannsvej+20,+9800+Hj%C3%B8rring,+Denmark%7CNyholmvej+20,+7500+Holstebro,+Denmark&key=xxx-RC0wa23c&mode=driving>
#> ℹ <https://maps.googleapis.com/maps/api/distancematrix/json?origins=Slotsarkaderne+26,+3400+Hiller%C3%B8d,+Denmark&destinations=A.F+Heidemannsvej+20,+9800+Hj%C3%B8rring,+Denmark%7CNyholmvej+20,+7500+Holstebro,+Denmark&key=xxx-RC0wa23c&mode=driving>
#> ℹ <https://maps.googleapis.com/maps/api/distancematrix/json?origins=Stormgade+157,+6715+Esbjerg,+Denmark&destinations=Golfvej+5,+7400+Herning,+Denmark%7CSlotsarkaderne+26,+3400+Hiller%C3%B8d,+Denmark%7CA.F+Heidemannsvej+20,+9800+Hj%C3%B8rring,+Denmark%7CNyholmvej+20,+7500+Holstebro,+Denmark&key=xxx-RC0wa23c&mode=driving>
datDistGoo
#> # A tibble: 10 × 11
#> id_from id_to from to m km miles seconds minutes hours mode
#> <int> <int> <chr> <chr> <int> <dbl> <dbl> <int> <dbl> <dbl> <chr>
#> 1 1 2 Stormgade 157, 6715 Esbjerg, … Golf… 87780 87.8 54.5 4336 72.3 1.20 driv…
#> 2 1 3 Stormgade 157, 6715 Esbjerg, … Slot… 321064 321. 200. 11806 197. 3.28 driv…
#> 3 1 4 Stormgade 157, 6715 Esbjerg, … A.F … 322081 322. 200. 11980 200. 3.33 driv…
#> 4 1 5 Stormgade 157, 6715 Esbjerg, … Nyho… 103748 104. 64.5 5298 88.3 1.47 driv…
#> 5 2 3 Golfvej 5, 7400 Herning, Denm… Slot… 332529 333. 207. 12137 202. 3.37 driv…
#> 6 2 4 Golfvej 5, 7400 Herning, Denm… A.F … 175960 176. 109. 7880 131. 2.19 driv…
#> 7 2 5 Golfvej 5, 7400 Herning, Denm… Nyho… 43980 44.0 27.3 1791 29.8 0.498 driv…
#> 8 3 4 Slotsarkaderne 26, 3400 Hille… A.F … 491647 492. 306. 18399 307. 5.11 driv…
#> 9 3 5 Slotsarkaderne 26, 3400 Hille… Nyho… 374969 375. 233. 13724 229. 3.81 driv…
#> 10 4 5 A.F Heidemannsvej 20, 9800 Hj… Nyho… 181192 181. 113. 7919 132. 2.20 driv…
Note that only the calculated distances are returned. If you want to have the whole distance matrix we define function:
#' Convert the data frame returned from calling a `___calc_distances` function to a distance matrix.
#'
#' @param dat The data frame returned from calling `___calc_distances`.
#' @param value_col The column containing the distances.
#' @return The distance matrix
as_dist_matrix <- function(dat, value_col) {
lgt <- max(dat$id_from, dat$id_to)
distanceMat<-matrix(NA, nrow=lgt, ncol = lgt)
diag(distanceMat) <- 0
map(1:nrow(dat), function(r) {
distanceMat[dat$id_from[r], dat$id_to[r]] <<- dat[[value_col]][r]
})
idx <- which(is.na(distanceMat), arr.ind = TRUE)
map(1:nrow(idx), function(r) {
distanceMat[idx[r, "row"], idx[r, "col"]] <<- distanceMat[idx[r, "col"], idx[r, "row"]]
})
return(distanceMat)
}
as_dist_matrix(datDistGoo, "km") # distances in km
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.0 87.8 321 322 104
#> [2,] 87.8 0.0 333 176 44
#> [3,] 321.1 332.5 0 492 375
#> [4,] 322.1 176.0 492 0 181
#> [5,] 103.7 44.0 375 181 0
as_dist_matrix(datDistGoo, "seconds") # travel time in seconds
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0 4336 11806 11980 5298
#> [2,] 4336 0 12137 7880 1791
#> [3,] 11806 12137 0 18399 13724
#> [4,] 11980 7880 18399 0 7919
#> [5,] 5298 1791 13724 7919 0
16.3.2 Using Bing Maps
Remember to have set your API key. We first define a bing_mapdist
function:
library(jsonlite)
#' Distance between two locations.
#'
#' @param from From address.
#' @param to To address.
#' @param mode Walking, driving or transit.
#' @param optimize Optimize either `distance` or `time`.
#' @return
bing_mapdist <- function(from, to, mode = "driving", optimize = "time") {
if (is.data.frame(from)) {
stopifnot(all(c("from", "to") %in% names(from)))
from_to_df <- from |> select("from", "to") |> as_tibble()
}
else {
from_to_df <- tibble(from = from, to = to)
}
dat <- map_dfr(1:nrow(from_to_df), function(r) {
url <-
str_c("http://dev.virtualearth.net/REST/V1/Routes/", mode, "?wp.0=", from_to_df$from[r], "&wp.1=", from_to_df$to[r],
"&optimize=", optimize, "&ra=excludeItinerary&rpo=None&ig=false&du=km",
"&avoid=minimizeTolls&key=", Sys.getenv("BING_MAPS_API_KEY"))
url <- URLencode(url)
lst <- fromJSON(url)
if (lst$statusCode != 200) return(tibble(km = NA, seconds = NA, mode = NA))
df <- lst$resourceSets$resources[[1]]
return(tibble(km = df$travelDistance, seconds = df$travelDuration, mode = df$travelMode))
})
return(bind_cols(from_to_df, dat))
}
bing_mapdist(from = "Stormgade 157, 6715 Esbjerg, Denmark",
to = "A.F Heidemannsvej 20, 9800 Hjørring, Denmark")
#> # A tibble: 1 × 5
#> from to km seconds mode
#> <chr> <chr> <dbl> <int> <chr>
#> 1 Stormgade 157, 6715 Esbjerg, Denmark A.F Heidemannsvej 20, 9800 Hjørring, Den… 322. 10442 Driv…
bing_mapdist(from = "Stormgade 157, 6715 Esbjerg, Denmark",
to = "A.F Heidemannsvej 20, 9800 Hjørring, Denmark",
optimize = "distance")
#> # A tibble: 1 × 5
#> from to km seconds mode
#> <chr> <chr> <dbl> <int> <chr>
#> 1 Stormgade 157, 6715 Esbjerg, Denmark A.F Heidemannsvej 20, 9800 Hjørring, Den… 259. 13429 Driv…
Note we here can get both the shortest and fastest path between two points. Next, we use the bing_mapdist
function for calculating distances:
#' Calculate the distance matrix in long format.
#'
#' @param address A vector of addresses.
#' @param symmetric Use symmetric distances (only half the number of queries).
#' @param ... Further parameters passed to `bing_mapdist`.
#' @return A data frame with the results
#' @note The API returns results for the fastest route.
bing_calc_distances <- function(address, symmetric = TRUE, ...) {
datDist <- expand_grid(id_from = 1:length(address), id_to = 1:length(address))
datDist <- datDist |> filter(id_from != id_to)
if (symmetric) datDist <- datDist |> filter(id_from < id_to)
datDist <- datDist |> mutate(from = address[id_from], to = address[id_to])
res <- bing_mapdist(datDist |> select(from, to), ...)
return(bind_cols(datDist |> select(id_from, id_to), res))
}
datDistBing <- bing_calc_distances(dat$Address)
datDistBing
#> # A tibble: 10 × 7
#> id_from id_to from to km seconds mode
#> <int> <int> <chr> <chr> <dbl> <int> <chr>
#> 1 1 2 Stormgade 157, 6715 Esbjerg, Denmark Golfvej 5, 7400 H… 89.9 4266 Driv…
#> 2 1 3 Stormgade 157, 6715 Esbjerg, Denmark Slotsarkaderne 26… 322. 10572 Driv…
#> 3 1 4 Stormgade 157, 6715 Esbjerg, Denmark A.F Heidemannsvej… 322. 10442 Driv…
#> 4 1 5 Stormgade 157, 6715 Esbjerg, Denmark Nyholmvej 20, 750… 104. 5252 Driv…
#> 5 2 3 Golfvej 5, 7400 Herning, Denmark Slotsarkaderne 26… 334. 10838 Driv…
#> 6 2 4 Golfvej 5, 7400 Herning, Denmark A.F Heidemannsvej… 178. 7659 Driv…
#> 7 2 5 Golfvej 5, 7400 Herning, Denmark Nyholmvej 20, 750… 44.7 1857 Driv…
#> 8 3 4 Slotsarkaderne 26, 3400 Hillerød, Denmark A.F Heidemannsvej… 374. 16885 Driv…
#> 9 3 5 Slotsarkaderne 26, 3400 Hillerød, Denmark Nyholmvej 20, 750… 376. 12266 Driv…
#> 10 4 5 A.F Heidemannsvej 20, 9800 Hjørring, Denmark Nyholmvej 20, 750… 186. 7869 Driv…
Note in general results from the API services are based on a set of assumptions and hence the results from different services may not be the same. You may use the different services to check if the distances are correct, by checking the results from different services:
# Difference in minutes
abs(as_dist_matrix(datDistGoo, "seconds") - as_dist_matrix(datDistBing, "seconds"))/60
#> [,1] [,2] [,3] [,4] [,5]
#> [1,] 0.000 1.17 20.6 25.633 0.767
#> [2,] 1.167 0.00 21.6 3.683 1.100
#> [3,] 20.567 21.65 0.0 25.233 24.300
#> [4,] 25.633 3.68 25.2 0.000 0.833
#> [5,] 0.767 1.10 24.3 0.833 0.000
# Km from 1 to 3
mapdist(from = "Stormgade 157, 6715 Esbjerg, Denmark",
to = "Slotsarkaderne 26, 3400 Hillerød, Denmark")
#> ℹ <https://maps.googleapis.com/maps/api/distancematrix/json?origins=Stormgade+157,+6715+Esbjerg,+Denmark&destinations=Slotsarkaderne+26,+3400+Hiller%C3%B8d,+Denmark&key=xxx-RC0wa23c&mode=driving>
#> # A tibble: 1 × 9
#> from to m km miles seconds minutes hours mode
#> <chr> <chr> <int> <dbl> <dbl> <int> <dbl> <dbl> <chr>
#> 1 Stormgade 157, 6715 Esbjerg, Denmark Slotsarkadern… 321064 321. 200. 11806 197. 3.28 driv…
bing_mapdist(from = "Stormgade 157, 6715 Esbjerg, Denmark",
to = "Slotsarkaderne 26, 3400 Hillerød, Denmark")
#> # A tibble: 1 × 5
#> from to km seconds mode
#> <chr> <chr> <dbl> <int> <chr>
#> 1 Stormgade 157, 6715 Esbjerg, Denmark Slotsarkaderne 26, 3400 Hillerød, Denmark 322. 10572 Driv…
16.4 Geocoding and reverse geocoding
16.4.1 Using Google maps
Remember to enable the Geocoding API. To get the coordinates of an address we use the geocode
function:
geocode("Stormgade 157, 6715 Esbjerg, Denmark")
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Stormgade+157,+6715+Esbjerg,+Denmark&key=xxx-RC0wa23c>
#> # A tibble: 1 × 2
#> lon lat
#> <dbl> <dbl>
#> 1 8.46 55.5
We may use mutate_geocode
to add the coordinates to a dataset:
dat <- dat |>
mutate_geocode(Address)
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Stormgade+157,+6715+Esbjerg,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Golfvej+5,+7400+Herning,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Slotsarkaderne+26,+3400+Hiller%C3%B8d,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=A.F+Heidemannsvej+20,+9800+Hj%C3%B8rring,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Nyholmvej+20,+7500+Holstebro,+Denmark&key=xxx-RC0wa23c>
dat
#> # A tibble: 5 × 5
#> Id Shop Address lon lat
#> <int> <chr> <chr> <dbl> <dbl>
#> 1 1 Bilka Esbjerg Stormgade 157, 6715 Esbjerg, Denmark 8.46 55.5
#> 2 2 Bilka Herning Golfvej 5, 7400 Herning, Denmark 9.01 56.1
#> 3 3 Bilka Hillerød Slotsarkaderne 26, 3400 Hillerød, Denmark 12.3 55.9
#> 4 4 Bilka Hjørring A.F Heidemannsvej 20, 9800 Hjørring, Denmark 10.0 57.4
#> 5 5 Bilka Holstebro Nyholmvej 20, 7500 Holstebro, Denmark 8.62 56.4
To reverse geocode use the revgeocode
function:
revgeocode(c(dat$lon[1], dat$lat[1]))
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?latlng=55.5107577,8.4559613&key=xxx-RC0wa23c>
#> Warning: Multiple addresses found, the first will be returned:
#> ! Stormgade 157, 6715 Esbjerg Kommune, Denmark
#> ! Stormgade 157, 6715 Esbjerg N, Denmark
#> ! GF64+89 Esbjerg, Denmark
#> ! Stormgade 156, 6715 Esbjerg Kommune, Denmark
#> ! Stormgade 158-155, 6715 Esbjerg Kommune, Denmark
#> ! 6715 Esbjerg N, Denmark
#> ! Esbjerg N, 6715 Esbjerg N, Denmark
#> ! Esbjerg N, 6715 Esbjerg, Denmark
#> ! Esbjerg, Denmark
#> ! Esbjerg Municipality, Denmark
#> ! Region of Southern Denmark, Denmark
#> ! Denmark
#> [1] "Stormgade 157, 6715 Esbjerg Kommune, Denmark"
To apply it to the dataset use:
dat <- dat |>
mutate(AddressGeoGoo =
map_chr(1:n(), function(i) {
revgeocode(c(lon = lon[i], lat = lat[i]))
}))
#> ! Stormgade 157, 6715 Esbjerg Kommune, Denmark
#> ! Stormgade 157, 6715 Esbjerg N, Denmark
#> ! GF64+89 Esbjerg, Denmark
#> ! Stormgade 156, 6715 Esbjerg Kommune, Denmark
#> ! Stormgade 158-155, 6715 Esbjerg Kommune, Denmark
#> ! 6715 Esbjerg N, Denmark
#> ! Esbjerg N, 6715 Esbjerg N, Denmark
#> ! Esbjerg N, 6715 Esbjerg, Denmark
#> ! Esbjerg, Denmark
#> ! Esbjerg Municipality, Denmark
#> ! Region of Southern Denmark, Denmark
#> ! Denmark
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?latlng=56.1357174,9.0053714&key=xxx-RC0wa23c>
#> ! Golfvej 5, 7400 Herning, Denmark
#> ! 42P4+74 Herning, Denmark
#> ! Engdahlsvej, 7400 Herning, Denmark
#> ! 7400 Herning, Denmark
#> ! Herning, 7400 Herning, Denmark
#> ! Herning Municipality, Denmark
#> ! Central Denmark Region, Denmark
#> ! Denmark
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?latlng=55.9288332,12.2978145&key=xxx-RC0wa23c>
#> ! Slotsarkaderne 30, 3400 Hillerød, Denmark
#> ! Slotsarkaderne 26, 3400 Hillerød, Denmark
#> ! W7HX+G4 Hillerød, Denmark
#> ! Hillerød, 3400 Hillerød, Denmark
#> ! 3400 Hillerød, Denmark
#> ! Hillerød Municipality, Denmark
#> ! Capital Region of Denmark, Denmark
#> ! Denmark
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?latlng=57.4441555,10.0034928&key=xxx-RC0wa23c>
#> ! A F Heidemanns Vej 20, 9800 Hjørring, Denmark
#> ! A F Heidemanns Vej 21, 9800 Hjørring, Denmark
#> ! C2V3+M9 Hjørring, Denmark
#> ! 9800 Hjørring, Denmark
#> ! Hjørring, 9800 Hjørring, Denmark
#> ! Hjørring Municipality, Denmark
#> ! North Denmark Region, Denmark
#> ! Denmark
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?latlng=56.3779116,8.6222409&key=xxx-RC0wa23c>
#> ! Nyholmvej 20, 7500 Holstebro, Denmark
#> ! Nyholmvej 20c, 7500 Holstebro, Denmark
#> ! 9JHC+5V Holstebro, Denmark
#> ! Nyholmvej 15, 7500 Holstebro, Denmark
#> ! Nyholmvej 19-17, 7500 Holstebro, Denmark
#> ! 7500 Holstebro, Denmark
#> ! Holstebro, 7500 Holstebro, Denmark
#> ! Holstebro Municipality, Denmark
#> ! Central Denmark Region, Denmark
#> ! Denmark
#> Warning: There were 5 warnings in `mutate()`.
#> The first warning was:
#> ℹ In argument: `AddressGeoGoo = map_chr(...)`.
#> Caused by warning:
#> ! Multiple addresses found, the first will be returned:
#> ℹ Run `dplyr::last_dplyr_warnings()` to see the 4 remaining warnings.
dat
#> # A tibble: 5 × 6
#> Id Shop Address lon lat AddressGeoGoo
#> <int> <chr> <chr> <dbl> <dbl> <chr>
#> 1 1 Bilka Esbjerg Stormgade 157, 6715 Esbjerg, Denmark 8.46 55.5 Stormgade 157, 671…
#> 2 2 Bilka Herning Golfvej 5, 7400 Herning, Denmark 9.01 56.1 Golfvej 5, 7400 He…
#> 3 3 Bilka Hillerød Slotsarkaderne 26, 3400 Hillerød, Denmark 12.3 55.9 Slotsarkaderne 30,…
#> 4 4 Bilka Hjørring A.F Heidemannsvej 20, 9800 Hjørring, Denmark 10.0 57.4 A F Heidemanns Vej…
#> 5 5 Bilka Holstebro Nyholmvej 20, 7500 Holstebro, Denmark 8.62 56.4 Nyholmvej 20, 7500…
16.4.2 Use Bing Maps
To get the coordinates of an address we define the bing_geocode
function:
#' Geocode addresses
#'
#' @param address Address(es) to geocode.
#' @return The coordinates as a data frame.
bing_geocode <- function(address) {
dat <- map_dfr(address, function(s) {
url <- str_c("http://dev.virtualearth.net/REST/v1/Locations?q=",
s, "&key=", Sys.getenv("BING_MAPS_API_KEY"))
url <- URLencode(url)
lst <- fromJSON(url)
if (lst$statusCode != 200) return(tibble(lon = NA, lat = NA))
v <- lst$resourceSets$resources[[1]]$point$coordinates[[1]]
if (is.null(v)) return(tibble(lon = NA, lat = NA))
return(tibble(lon = v[2], lat = v[1]))
})
return(dat)
}
bing_geocode("A.F. Heidemannsvej 20, 9800 Hjørring, Denmark")
#> # A tibble: 1 × 2
#> lon lat
#> <dbl> <dbl>
#> 1 10.0 57.4
bing_geocode(dat$Address)
#> # A tibble: 5 × 2
#> lon lat
#> <dbl> <dbl>
#> 1 8.46 55.5
#> 2 9.00 56.1
#> 3 12.3 55.9
#> 4 10.0 57.4
#> 5 8.62 56.4
We may use bing_mutate_geocode
to add the coordinates to a dataset:
dat <- dat |> select(-lat, -lon)
mutate_bing_geocode <- function (data, address, ...) {
adr <- data[[deparse(substitute(Address))]]
gcdf <- bing_geocode(adr)
bind_cols(data, gcdf)
}
dat <- dat |>
mutate_bing_geocode(Address)
dat
#> # A tibble: 5 × 6
#> Id Shop Address AddressGeoGoo lon lat
#> <int> <chr> <chr> <chr> <dbl> <dbl>
#> 1 1 Bilka Esbjerg Stormgade 157, 6715 Esbjerg, Denmark Stormgade 157, 671… 8.46 55.5
#> 2 2 Bilka Herning Golfvej 5, 7400 Herning, Denmark Golfvej 5, 7400 He… 9.00 56.1
#> 3 3 Bilka Hillerød Slotsarkaderne 26, 3400 Hillerød, Denmark Slotsarkaderne 30,… 12.3 55.9
#> 4 4 Bilka Hjørring A.F Heidemannsvej 20, 9800 Hjørring, Denmark A F Heidemanns Vej… 10.0 57.4
#> 5 5 Bilka Holstebro Nyholmvej 20, 7500 Holstebro, Denmark Nyholmvej 20, 7500… 8.62 56.4
To reverse geocode use the bing_revgeocode
function:
#' Reverse geocode coordinates
#'
#' @param coordinates A vector with two elements (lon, lat).
#' @return The address as a data frame.
bing_revgeocode <- function(coordinates) {
url <- str_c("http://dev.virtualearth.net/REST/v1/Locations/",
coordinates[2], ",", coordinates[1],
"?key=", Sys.getenv("BING_MAPS_API_KEY"))
url <- URLencode(url)
lst <- fromJSON(url)
if (lst$statusCode != 200) return(NA)
v <- lst$resourceSets$resources[[1]]$address$formattedAddress
if (is.null(v)) return(NA)
return(v)
}
bing_revgeocode(c(dat$lon[1], dat$lat[1]))
#> [1] "Stormgade 258, 6715 Esbjerg, Denmark"
To apply it to the dataset use:
dat <- dat |>
mutate(AddressGeoBing =
map_chr(1:n(), function(i) {
bing_revgeocode(c(lon = lon[i], lat = lat[i]))
}))
dat
#> # A tibble: 5 × 7
#> Id Shop Address AddressGeoGoo lon lat AddressGeoBing
#> <int> <chr> <chr> <chr> <dbl> <dbl> <chr>
#> 1 1 Bilka Esbjerg Stormgade 157, 6715 Esbjerg, Denma… Stormgade 15… 8.46 55.5 Stormgade 258…
#> 2 2 Bilka Herning Golfvej 5, 7400 Herning, Denmark Golfvej 5, 7… 9.00 56.1 Golfvej 5, 74…
#> 3 3 Bilka Hillerød Slotsarkaderne 26, 3400 Hillerød, … Slotsarkader… 12.3 55.9 Slotsarkadern…
#> 4 4 Bilka Hjørring A.F Heidemannsvej 20, 9800 Hjørrin… A F Heideman… 10.0 57.4 A F Heidemann…
#> 5 5 Bilka Holstebro Nyholmvej 20, 7500 Holstebro, Denm… Nyholmvej 20… 8.62 56.4 Nyholmvej 20,…
16.5 Adding markers and routes to a map
Leaflet is an open-source JavaScript library for interactive maps. It’s used by websites ranging from The New York Times and The Washington Post to GitHub and Flickr, as well as GIS specialists like OpenStreetMap, Mapbox, and CartoDB. The leaflet package makes it easy to create Leaflet maps from R.
Note Leaflet is open-source and free so you do not need an API key for making maps. If you would like to use Google maps instead then have a look at the googleway package instead.
First let us create a map with two base layers
library(leaflet)
library(htmlwidgets)
m <- leaflet() |>
# Base maps
addTiles(group = "Map") |>
addProviderTiles('Esri.WorldImagery', group = "Satelite") |>
addProviderTiles("CartoDB.PositronOnlyLabels", group = "Map") |>
# Center and zoom
setView(10.2, 56.2, zoom = 7) |>
# Layer control
addLayersControl(
baseGroups = c("Map", "Satelite"),
options = layersControlOptions(collapsed = TRUE)
)
m
Next, let us add the places:
dat <- tibble::tribble(
~Id, ~Shop, ~Address,
1L, "Bilka Esbjerg", "Stormgade 157, 6715 Esbjerg, Denmark",
2L, "Bilka Herning", "Golfvej 5, 7400 Herning, Denmark",
3L, "Bilka Hillerød", "Slotsarkaderne 26, 3400 Hillerød, Denmark",
4L, "Bilka Hjørring", "A.F Heidemannsvej 20, 9800 Hjørring, Denmark",
5L, "Bilka Holstebro", "Nyholmvej 20, 7500 Holstebro, Denmark",
)
dat <- dat |> mutate_geocode(Address)
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Stormgade+157,+6715+Esbjerg,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Golfvej+5,+7400+Herning,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Slotsarkaderne+26,+3400+Hiller%C3%B8d,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=A.F+Heidemannsvej+20,+9800+Hj%C3%B8rring,+Denmark&key=xxx-RC0wa23c>
#> ℹ <https://maps.googleapis.com/maps/api/geocode/json?address=Nyholmvej+20,+7500+Holstebro,+Denmark&key=xxx-RC0wa23c>
m <- m |>
addMarkers(~lon, ~lat, popup = ~Address, label = ~str_c(Id, " - ", Shop), data = dat)
m
To add a line between a set of points use the addPolylines
function:
routeIds <- c(2, 5, 1, 2)
route <- dat[routeIds,]
m |> addPolylines(lng = ~lon, lat = ~lat, data = route, weight = 2, label = "Route 1")
A more advanced setup is to use the addFlows
function from the leaflet.minicharts package. First let us define some lines:
datLines <- tibble(FromId = c(2, 5, 1, 2, 4, 3), ToId = c(5, 1, 2, 4, 3, 2))
datLines <- left_join(datLines, dat |> select(-Shop, -Address), by = c("FromId" = "Id")) |>
rename("FromLat" = "lat", "FromLon" = "lon") |>
left_join(dat |> select(-Shop, -Address), by = c("ToId" = "Id")) |>
rename("ToLat" = "lat", "ToLon" = "lon")
datLines
#> # A tibble: 6 × 6
#> FromId ToId FromLon FromLat ToLon ToLat
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2 5 9.01 56.1 8.62 56.4
#> 2 5 1 8.62 56.4 8.46 55.5
#> 3 1 2 8.46 55.5 9.01 56.1
#> 4 2 4 9.01 56.1 10.0 57.4
#> 5 4 3 10.0 57.4 12.3 55.9
#> 6 3 2 12.3 55.9 9.01 56.1
Note we have a from/to pair in each row. We add the lines to the map:
library(leaflet.minicharts)
m |> addFlows(datLines$FromLon, datLines$FromLat, datLines$ToLon, datLines$ToLat,
flow = 1, maxFlow = 20, opacity = 0.5, popup = popupArgs(noPopup = TRUE))
Let us define a function for adding routes:
add_route <- function(dat = NULL, route, solution = 1) {
route_id = if_else(is.null(dat), 1, max(dat$RouteId) + 1)
tmp <- tibble(From = route[1:(length(route)-1)], To = route[2:length(route)])
tmp <- tmp |> mutate(Sol = solution, RouteId = route_id)
dat <- bind_rows(dat, tmp)
}
datLines <- add_route(route = c(2, 5, 1, 2)) |>
add_route(route = c(2, 4, 3, 2)) |>
add_route(route = c(2, 3, 1, 2), solution = 2) |>
add_route(route = c(2, 5, 4, 2), solution = 2)
#> Warning in max(dat$RouteId): no non-missing arguments to max; returning -Inf
datLines <- left_join(datLines, dat |> select(-Shop, -Address), by = c("From" = "Id")) |>
rename("FromLat" = "lat", "FromLon" = "lon") |>
left_join(dat |> select(-Shop, -Address), by = c("To" = "Id")) |>
rename("ToLat" = "lat", "ToLon" = "lon") |>
mutate(group = str_c("Solution ", Sol))
datLines
#> # A tibble: 12 × 9
#> From To Sol RouteId FromLon FromLat ToLon ToLat group
#> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 2 5 1 1 9.01 56.1 8.62 56.4 Solution 1
#> 2 5 1 1 1 8.62 56.4 8.46 55.5 Solution 1
#> 3 1 2 1 1 8.46 55.5 9.01 56.1 Solution 1
#> 4 2 4 1 2 9.01 56.1 10.0 57.4 Solution 1
#> 5 4 3 1 2 10.0 57.4 12.3 55.9 Solution 1
#> 6 3 2 1 2 12.3 55.9 9.01 56.1 Solution 1
#> 7 2 3 2 3 9.01 56.1 12.3 55.9 Solution 2
#> 8 3 1 2 3 12.3 55.9 8.46 55.5 Solution 2
#> 9 1 2 2 3 8.46 55.5 9.01 56.1 Solution 2
#> 10 2 5 2 4 9.01 56.1 8.62 56.4 Solution 2
#> 11 5 4 2 4 8.62 56.4 10.0 57.4 Solution 2
#> 12 4 2 2 4 10.0 57.4 9.01 56.1 Solution 2
A map with Solution 1:
col_pal <- rainbow(max(datLines$RouteId))
datP <- datLines |> filter(Sol == 1)
m |> addFlows(datP$FromLon, datP$FromLat, datP$ToLon, datP$ToLat,
flow = datP$RouteId, maxThickness = 2, color = col_pal[datP$RouteId],
opacity = 0.5, popup = popupArgs(labels = "Route"))
A map with Solution 2:
col_pal <- rainbow(max(datLines$RouteId))
datP <- datLines |> filter(Sol == 2)
m |> addFlows(datP$FromLon, datP$FromLat, datP$ToLon, datP$ToLat,
flow = datP$RouteId, maxThickness = 2, color = col_pal[datP$RouteId],
opacity = 0.5, popup = popupArgs(labels = "Route"))
An interactive map with both solutions:
mm <- m
res <- map(1:nrow(datLines), function(i) {
tmp <- tibble(lat = c(datLines$FromLat[i], datLines$ToLat[i]), lon = c(datLines$FromLon[i], datLines$ToLon[i]))
mm <<- mm |> addPolylines(lng = ~lon, lat = ~lat,
color = col_pal[datLines$Sol[i]], group = datLines$group[i],
data = tmp, weight = 2, label = datLines$group[i])
return(invisible(NULL))
})
mm <- mm |>
# Layer control
addLayersControl(
baseGroups = c("Map", "Satelite"),
overlayGroups = unique(datLines$group),
options = layersControlOptions(collapsed = FALSE)
) |>
hideGroup(unique(datLines$group)) |>
showGroup("Solution 1")
mm
Note unfortunately arrows cannot be shown in this case.