Skip to content

Instantly share code, notes, and snippets.

@markusloecher
Created November 26, 2017 20:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save markusloecher/93fc27d57bd07235770c1f659db93fe1 to your computer and use it in GitHub Desktop.
Save markusloecher/93fc27d57bd07235770c1f659db93fe1 to your computer and use it in GitHub Desktop.
---
title: "Offline Maps with RgoogleMaps and leaflets"
author: "M Loecher"
output:
html_document:
toc: true
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(echo = TRUE, message=FALSE, warning=FALSE)
library(leaflet)
library(ggmap)
library(RgoogleMaps)
library(tidyverse)
```
## Fetching map tiles
Until version 1.3.0 *RgoogleMaps* only
downloaded static maps as provided by the static maps APIs from e.g. Google, bing and OSM. While there are numerous advantages to this strategy such as full access to the extensive feature list provided by those APIs, the limimtations are also clear:
1. unlikely reusability of previously stored static maps,
2. limits on the maximum size of the map (640,640),
3. and the requirement to be online.
Beginning with [version 1.4.1](https://r-forge.r-project.org/R/?group_id=2178)
, we added the functions *GetMapTiles* and *PlotOnMapTiles* which fetch individual map tiles and store them locally.
For example, if we wanted to fetch 20 tiles (in each direction) at zoom level 16 around Washington Square Park in Manhattan, we would simply run
```{r, eval = FALSE}
library(RgoogleMaps)
(center=getGeoCode("Washington Square Park;NY"))
GetMapTiles(center, zoom=16,nTiles = c(20,20))
```
Note that the default server is taken to be *openstreetmap* and the default local directory $tileDir= "~/mapTiles/OSM/"$.
We could have also passed the location string directly and saved several zoom levels at once (note the constant radius adaptation of the number of tiles):
```{r, eval = FALSE}
for (zoom in 13:15)
GetMapTiles("Washington Square Park;NY", zoom=zoom,nTiles = round(c(20,20)/(17-zoom)))
```
Before requesting new tiles, the function checks if that map tile exists already which avoids redundant downloads.
We can repeat the process with Google map tiles and plot them:
```{r, eval = FALSE}
for (zoom in 13:16)
GetMapTiles("Washington Square Park;NY", zoom=zoom,nTiles = round(c(20,20)/(17-zoom)),
urlBase = "http://mt1.google.com/vt/lyrs=m", tileDir= "~/mapTiles/Google/")
```
```{r, fig.width=8,fig.height=8}
#just get 3x3 tiles:
mt= GetMapTiles("Washington Square Park;NY", zoom=16,nTiles = c(3,3),
urlBase = "http://mt1.google.com/vt/lyrs=m", tileDir= "~/mapTiles/Google/", returnTiles = TRUE)
par(pty="s")
PlotOnMapTiles(mt)
```
## Leaflet
#### Interactive Web Maps with the JavaScript 'Leaflet' Library
As a reminder, the syntax for the leaflet is straightforward once the "pipe" operator %>% is understood. For example, centering a map at Aalborg university:
```{r, echo=TRUE,fig.width=8, fig.height=7}
m = leaflet::leaflet() %>% addTiles()
m = m %>% leaflet::setView(8.638431, 49.866438, zoom = 14)
m
```
While the original motivation of *GetMapTiles* was to enable offline creation of static maps within the package *RgoogleMaps*, combining this feature with the interactivity of the *leaflet* library leads to an effective offline maps version of leaflet!
## Tile Server
We only need to replace the default server specified by the parameter *urlTemplate* by a local server obliging with the file naming scheme zoom_X_Y.png set by *GetMapTiles*
Any simple local Web service will suffice, but a useful recommendation from http://stackoverflow.com/questions/5050851/best-lightweight-web-server-only-static-content-for-windows suggests:
> To use Python as a simple web server just change your working
> directory to the folder with your static content and type
> python -m SimpleHTTPServer 8000, everything in the directory
> will be available at http:/localhost:8000/
So assuming (i) successful execution of the map tileabove and (ii) the correct launch of the server (in the parent dirtectory of *mapTiles/*), the following code will have leaflet dynamically load them (from the local repository) for zooming and panning abilities:
```{r, eval=FALSE }
m = leaflet::leaflet() %>%
addTiles( urlTemplate = "http:/localhost:8000/mapTiles/OSM/{z}_{x}_{y}.png")
m = m %>% leaflet::setView(-73.99733, 40.73082 , zoom = 16)
m = m %>% leaflet::addMarkers(-73.99733, 40.73082 )
m
```
```{r, echo=FALSE }
m = leaflet::leaflet() %>% addTiles()
m = m %>% leaflet::setView(-73.99733, 40.73082 , zoom = 16)
m = m %>% leaflet::addMarkers(-73.99733, 40.73082 )
m
```
And for google map tiles:
```{r, eval=FALSE }
m = leaflet::leaflet() %>%
addTiles( urlTemplate = "http:/localhost:8000/mapTiles/Google/{z}_{x}_{y}.png")
m = m %>% leaflet::setView(-73.99733, 40.73082 , zoom = 16)
m = m %>% leaflet::addMarkers(-73.99733, 40.73082 )
m
```
```{r, echo=FALSE }
m = leaflet::leaflet() %>%
addTiles( urlTemplate = "http://mt1.google.com/vt/lyrs=m&x={x}&y={y}&z={z}")
m = m %>% leaflet::setView(-73.99733, 40.73082 , zoom = 16)
m = m %>% leaflet::addMarkers(-73.99733, 40.73082 )
m
```
## Synchronized Views
```{r}
library(sp)
demo(meuse, ask = FALSE, echo = FALSE) # loads the meuse data sets
library(mapview)
m1 <- mapview(meuse, zcol = "soil", burst = TRUE, legend = TRUE)
m2 <- mapview(meuse, zcol = "lead", legend = TRUE)
m3 <- mapview(meuse, zcol = "landuse", map.types = "Esri.WorldImagery", legend = TRUE)
m4 <- mapview(meuse, zcol = "dist.m", legend = TRUE)
sync(m1, m2, m3, m4) # 4 panels synchronised
```
## Routing
```{r, echo=FALSE}
library(tidyverse)
source('decodeUtils.R')
```
```{r}
p=vector()
p[1] = "Frankfurt train station, Germany"
p[2]= "Frankfurt airport, Germany"
p[3]= "Schoefferstr. 3, 64295 Darmstadt, Germany"
```
```{r, echo=FALSE}
getGeoCode("HWR Berlin, Germany")
# lat lon
#52.48544 13.33785
getGeoCode("Frankfurt, Germany")
# lat lon
# 50.110922 8.682127
getGeoCode("Schoefferstr. 3, 64295 Darmstadt, Germany")
# lat lon
# 49.866438 8.638431
#this function creates a tibble with starting to destinations relationships one per row from your original route p. So this is a wrapper from your original loop, but returns a dataframe instead of a list
connections.from.array <- function(p){
routes <- tibble(start=p,dest=c(p[-1],NA))[1:nrow(cbind(p,c(p[-1],NA)))-1,]
return(routes)
}
routes <- connections.from.array(p) #tibble with a connection per row.
#this function applies route() to start/destinations, output="all" is the most important change
calculationroute <- function(startingpoint, stoppoint) {
route(from = startingpoint,
to = stoppoint,
structure = "route", output="all")}
#mapply the new "route"-replacer function to all the connections in the tibble
calculatedroutes <- mapply(calculationroute,
startingpoint = routes$start,
stoppoint = routes$dest,
SIMPLIFY = FALSE)
# in order to keep the markers on the map working, we still need the list of connections,
#since the structure of the route changes when using output="all", the original code isn`t working anymore
route_df = list()
for (i in 1:(length(p)-1)) {
route_df[[i]] <- route(p[i], p[i+1], structure = "route")
}
myRoute = 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)
}))
```
```{r}
head(myRoute)
```
```{r, fig.width=8, fig.height = 8}
m = leaflet::leaflet() %>% addTiles()
m = m %>% leaflet::setView(myRoute[1,"lon"], myRoute[1,"lat"], zoom = 13)
for (i in 1:(length(p)-1)) {
m = m %>% leaflet::addMarkers(route_df[[i]][1,"lon"], route_df[[i]][1,"lat"])
}
m = m %>% addPolylines(myRoute[,"lon"], myRoute[,"lat"], col="red")
m
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment