12

I am trying to map ~8000 polygons using leaflet and run into performance issues. As I am using the map within a shiny app, I was wondering if its possible to somehow cache or pre-render the map.

Note that in my case, I have different layers of polygons that are swapped following this approach.

A small MWE would be this:

The data can be downloaded from here

library(shiny)
library(leaflet)
library(sf)

## Download Shapefile
file <- "plz-gebiete.shp"

if (!file.exists(file)) {
  url <- "https://www.suche-postleitzahl.org/download_files/public/plz-gebiete.shp.zip"
  zipfile <- paste0(file, ".zip")
  download.file(url, zipfile)
  unzip(zipfile)
}

df <- st_read(file, options = "ENCODING=UTF-8")

# If possible: pre-render the map here!

library(shiny)

ui <- fluidPage(
  leafletOutput("mymap", width = "700px", height = "700px")
)

server <- function(input, output, session) {
  output$mymap <- renderLeaflet({
    leaflet() %>% 
      addTiles() %>% 
      addPolygons(data = df, weight = 1, color = "black")
  })
}

shinyApp(ui, server)

It takes around 16 seconds on my machine to render the map with the polygons.

If possible, I would like to pre-render the map once, save it as an .rds file, and load it on demand. Note that I know the width/height of the map within the app (here set to 700px). But something like

map <- renderLeaflet({leaflet() %>% ...})
saveRDS(map, "renderedmap.rds")

map <- readRDS("renderedmap.rds")

# within server()
output$mymap <- map

does not result in any performance gains.

Alternatively, I have tried to load the leaflet asynchronously so that other parts of the app can be rendered/interacted with but to no avail.

Any ideas how to solve or circumnavigate this problem?

David
  • 9,216
  • 4
  • 45
  • 78
  • You can change the precision. A reduction to 0.001 reduces the file to 34MB and a reduction to 0.01 and saving as json weighs about 6MB. So it's up to you whether you need very accurate maps. – Grzegorz T. Jul 03 '20 at 21:58
  • 1
    You could try out [leafgl](https://github.com/r-spatial/leafgl) or `leafem::addFgb` which uses the performant Flatgeobuf format. – SeGa Jul 07 '20 at 11:49
  • I have looked into leafgl, but found no working solution for recoloring the polygons. `addFgb` looks like a good solution. Do you mind posting a complete example? – David Jul 07 '20 at 18:04

2 Answers2

6

The 2 following approaches dont exactly answer your question, but they are definitly more performant alternatives compared to leaflet::addPolygons.

Using Flatgeobuf Format:

Based on the description from leafem::addFgb:

Flatgeobuf can stream the data chunk by chunk so that rendering of the map is more or less instantaneous. The map is responsive while data is still loading so that popup queries, zooming and panning will work even though not all data has been rendered yet.

I think the dataset are linestrings, that is why fillColor seems to be ignored.

library(leaflet)
library(leafem)
library(shiny)

# via URL (data around 13mb)
url = "https://raw.githubusercontent.com/bjornharrtell/flatgeobuf/3.0.1/test/data/UScounties.fgb"

ui <- fluidPage(
  leafletOutput("mymap", width = "700px", height = "700px")
)

server <- function(input, output, session) {
  output$mymap <- renderLeaflet({
    leaflet() %>%
      addTiles() %>%
      leafem:::addFgb(
        url = url, group = "counties",
        label = "NAME", popup = TRUE,
        fillColor = "blue", fillOpacity = 0.6,
        color = "black", weight = 1) %>%
      addLayersControl(overlayGroups = c("counties")) %>%
      setView(lng = -105.644, lat = 51.618, zoom = 3)
  })
}

shinyApp(ui, server)

Using leafgl (WebGL-Renderer):

library(sf)
library(shiny)
library(leaflet)
library(leafgl)

plz <- st_read("C:/Users/user/Downloads/plz-gebiete.shp", layer = "plz-gebiete")

ui <- fluidPage(
  leafletOutput("mymap", width = "700px", height = "700px")
)

server <- function(input, output, session) {
  output$mymap <- renderLeaflet({
    leaflet() %>%
      addTiles() %>%
      addGlPolygons(data = plz, color = ~plz, popup = "note", group = "plz") %>% 
      addLayersControl(overlayGroups = "plz")
  })
}

shinyApp(ui, server)
SeGa
  • 9,454
  • 3
  • 31
  • 70
  • Have you found a way to fill the polygons in the leafem-case? It does not seem to work for me, [see here](https://gist.github.com/DavZim/414e30fb09b2e1d06e22464b83249cc6). Note that the geometries are multipolygons and not linestrings. – David Jul 08 '20 at 08:14
  • I am just trying to convert the Shapefile to .fgb, but I have to update GDAL, since Flatgeobuf was introduced in 3.1 and I am still on 3.0.4. I'll update my answer as soon as I managed. Also note that `leafgl` works only with polygons and not multipolygons. – SeGa Jul 08 '20 at 08:20
  • 1
    I had to do that yesterday... Version 3.1.2 (came out half-way through my installation attempts) worked best. To spare you the process, the file can be found [here](https://github.com/DavZim/misc/blob/master/local_map.fgb) – David Jul 08 '20 at 08:32
  • This is not the plz-gebiete Shapefile or? I am still getting the US counties with this file. Could you run `ogr2ogr -f FlatGeobuf plz.fgb plz-gebiete.shp` in the directory where the shapefile is and upload the resulting `plz.fgb`? – SeGa Jul 08 '20 at 08:38
  • 1
    Sure, the file is too big for github, so I uploaded it to [dropbox](https://www.dropbox.com/s/qw1k1ge9eyi98nd/plz.fgb?dl=0) – David Jul 08 '20 at 08:53
  • 1
    I didnt manage to install GDAL 3.1 until now. But here is a tweet where Flatgeobuf works with polygons and the fillColor seems to work: https://twitter.com/TimSalabim3/status/1264201631538831360 – SeGa Jul 08 '20 at 11:40
4

Approach 1: minimize polygons

As hinted in the comment by Grzegorz T., you can change the precision of the underlying polygon file. Reducing the file size increased load time by around 3x on my computer.

The Visvalingam and Douglas-Peucker algorithms implemented in the rmapshaper package simplifies polygons by iterating over the points used to define the polyons and removing "extraneous points" while still "maintaining the shape".

library(rmapshaper)

# baseline object size
object.size(df)/1e6  # 61. MB

# simplyfy the spatial object
# `keep_shapes=T` ensures no polygons are dropped
df2 <- ms_simplify(df, keep_shapes = TRUE)
object.size(df2)/1e6 # 11.8 MB

# decreasing the percentage of points to keep from 5% (default) to 1% 
# doesn't result in significantly smaller object size, but still
# improves the loading speed
df3 <- ms_simplify(df, keep = 0.01, keep_shapes = TRUE)
object.size(df3)/1e6 # 9.8 MB

Approach 2: render polygons as points

Points are much smaller than polygons. You might consider taking the centroid of each polygon and rendering those. This renders in about 1-2s on my machine, about a 50-100x speedup.

library(tidyverse)
pts <- st_centroid(df) %>% 
  st_geometry() %>% 
  do.call(rbind, .) %>% 
  as_tibble() %>% 
  setNames(c("lng","lat"))

server <- function(input, output, session) {
  output$mymap <- renderLeaflet({
    leaflet(pts) %>% 
      addTiles() %>% 
      addCircleMarkers(radius = 1)
  })
}

Approach 3: render polygons as clustered points

Similar speed to approach 2, but potentially cleaner in presentation.

server <- function(input, output, session) {
  output$mymap <- renderLeaflet({
    leaflet(pts) %>% 
      addTiles() %>% 
      addMarkers(clusterOptions = markerClusterOptions())
  })
}
Rich Pauloo
  • 7,734
  • 4
  • 37
  • 69
  • 1
    These are some valuable approaches, esp. the first one. I was a bit intimidated by `rmapshaper` and its dependencies, but it seems it pays off. Ill wait a bit if someone has option for actually pre-rendering the map. – David Jul 05 '20 at 11:57