Recreating my basic Antarctic Leaflet map in R (tiles, shapes, graticules, labels)

  developmentR

R is a powerful “…free software environment for statistical computing and graphics”. It’s widely used in a variety of fields, and can even run in SQL Server.

I’m an R noob, even so with a little help I was able to put together a quick demo using Leaflet for R to replicate as much functionality as possible from my web-based Antarctic map at https://thomasswilliams.github.io/leaflet-antarctic-demo/.

If you’re following along, before you start you’ll need R from https://www.r-project.org/ and the free RStudio Desktop from https://www.rstudio.com/products/rstudio/.

The first run of the code below will install & reference required packages. The code depends on two TopoJSON files which I’ve put in a new repo at https://github.com/thomasswilliams/leaflet-antarctica-demo-for-r.

What does the R code do?

The R code is in a single “demo” file. Much of it is adapted from my web-based map. In a nutshell, the code:

  • installs and references dependencies like Leaflet, rgdal (used for Antarctic projection), “mustashe” caching (lines 1-27)
  • loads local TopoJSON files, and caches; downloads GeoJSON from my GitHub Pages-hosted map, and caches (lines 29-59)
  • sets up the EPSG:3031 Antarctic projection (lines 61-88)
  • creates a map and layers from the loaded files, styles layers (lines 90-180)
  • displays the map using the in-built R viewer (lines 182-197)

Leaflet map in R viewer

What’s different to the browser-based Leaflet map?

The following is missing from the R version:

  • caching of tiles (though as discussed, I cache everything else)
  • custom SVG icons (though can load icons from file)
  • async loading of GeoJSON (caching helps with this - after the first run, the R code is a lot faster)
  • dependencies not referenced from a CDN, downloaded to computer
  • tile colors using CSS (I just couldn’t figure this out)
  • loading shapefiles - though this is definitely owing to my lack of knowledge, I persisted and eventually got shapefiles to GeoJSON to TopoJSON displaying

Also, you need R to run it (versus only a web browser to view the original), and the version of Leaflet is older (1.3) compared to 1.8 in the web version.

However, I see a ton of advantages using R. It let me be productive, quicker, with less expertise needed. I like to think of this as the “sweet spot” where a lot can be achieved (the 80% as per the 80/20 rule), to get a better idea if more effort should be applied to address the remaining 20%:

Leaflet web and R compared

Since I’m on a list binge, some of the advantages I noticed of the R version were:

  • first-class debugging using RStudio
  • lots of good examples and documentation for Leaflet, rgdal online
  • some required dependencies, such as Proj4, already included
  • code is simpler, complexity is hidden (for instance, referencing dependencies, no need to write javascript)
  • Leaflet popup tooltips and labels easier to use, built-in
  • less than 200 lines of code to get a passable Leaflet Antarctic map in R, the outcome looks very similar to my web-based version which has 300 lines of code
  • under the hood, R generates the same javascript, so javascript skills are transplantable
  • R is super powerful and can do much more than display a map - could combine with any dataset, analyse data, plot, create a dashboard, etc.

The code is below, can also be found at the GitHub repo at https://github.com/thomasswilliams/leaflet-antarctica-demo-for-r:

# install "pacman" package
# will load and install subsequent packages if necessary
if (!require("pacman")) install.packages("pacman")

# ********************* load packages *********************
# load (plus background install if not present) package, requires "pacman"
# Leaflet for R
# https://rstudio.github.io/leaflet/
pacman::p_load(leaflet)
# Bindings for the 'Geospatial' Data Abstraction Library
# https://cran.r-project.org/web/packages/rgdal/index.html
pacman::p_load(rgdal)
# Leaflet extensions
# https://github.com/r-spatial/leafem
pacman::p_load(leafem)
# htmlwidgets for R
# https://www.htmlwidgets.org/
pacman::p_load(htmlwidgets)
# Tools for creating, manipulating, and writing HTML from R
# https://rstudio.github.io/htmltools/index.html
pacman::p_load(htmltools)
# Simple and Robust JSON Parser and Generator for R
# https://cran.r-project.org/web/packages/jsonlite
pacman::p_load(jsonlite)
# mustashe caching
# https://jhrcook.github.io/mustashe/
pacman::p_load(mustashe)

# create a cache key of today's date in YYYY-MM-DD format
cache_key <- Sys.Date()

# for all loaded data, cache for one day using mustashe
# this means subsequent runs on the same day will be faster as
# data is retrieved from cache after the first run
# assumes data does not change very often

mustashe::stash("longitude", depends_on = "cache_key", {
  # load local 30 degree longitude lines originally from Quantarctica
  # converted to GeoJSON then TopoJSON using https://rowanwins.github.io/geojson-editor/
  # if doesn't work, may need to set this directory as working directory
  # load as string as per TopoJSON example at https://rstudio.github.io/leaflet/json.html
  longitude <- readLines("30dg_longitude.topojson", warn = FALSE) %>%
    paste(collapse = "\n") %>%
    jsonlite::fromJSON(simplifyVector = FALSE)
})
mustashe::stash("latitude", depends_on = "cache_key", {
  # local latitude graticules from Quantarctica
  latitude <- readLines("10dg_latitude.topojson", warn = FALSE) %>%
    paste(collapse = "\n") %>%
    jsonlite::fromJSON(simplifyVector = FALSE)
})
mustashe::stash("icebergs", depends_on = "cache_key", {
  # load icebergs GeoJSON from my Github Pages
  icebergs <- rgdal::readOGR("https://thomasswilliams.github.io/leaflet-antarctic-demo/icebergs-apr-2022.json")
})
mustashe::stash("feature_names", depends_on = "cache_key", {
  # load feature names GeoJSON from my GitHub Pages
  feature_names <- rgdal::readOGR("https://thomasswilliams.github.io/leaflet-antarctic-demo/geonames-selected-feature-names.json")
})

# map extent
extent <- 12367396.2185

# hard-code resolutions array
# need to match Leaflet zoom levels (resolutions) to whatever site is used for tiles (in this case, GBIF)
resolutions <- c(
  48310.14147851562, 24155.07073925781, 12077.535369628906, 6038.767684814453, 3019.3838424072264, 1509.6919212036132,
  754.8459606018066
)

# create CRS definition for EPSG:3031
# Proj4Leaflet integrated into Leaflet for R package
# custom bounds as defined by tiles site used (GBIF)
# see https://tile.gbif.org/ui/3031/EPSG3031-leaflet.js
epsg_3031 <- leaflet::leafletCRS(
  crsClass = "L.Proj.CRS",
  # name
  code = "EPSG:3031",
  # proj4 definition of CRS
  # see https://spatialreference.org/ref/epsg/3031/, https://epsg.io/3031
  proj4def = "+proj=stere +lat_0=-90 +lat_ts=-71 +lon_0=0 +k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs",
  # zoom resolutions
  resolutions = resolutions,
  # top-left corner of map
  origin = c(-extent, extent),
  # bounds, adapted from https://github.com/cran/robis/blob/master/R/map.R
  bounds = list(c(-extent, -extent), c(extent, extent))
)

# Leaflet map options
map_options <- leaflet::leafletOptions(
  # set center (South Pole) and zoom level
  center = c(-90, 0),
  zoom = 2,
  # minimum & maximum zoom
  minZoom = 1,
  maxZoom = 6,
  # CRS
  crs = epsg_3031,
  # disable jumping to a copy of the map (plays up on mobile)
  worldCopyJump = FALSE,
  # prefer canvas (over SVG) renderer
  preferCanvas = TRUE
)

# create the map and add layers
map = leaflet::leaflet(options = map_options) %>%
  # base map tiles (from https://tile.gbif.org/ui/)
  # use light style, SRS 3031
  leaflet::addTiles(
    urlTemplate = "https://tile.gbif.org/3031/omt/{z}/{x}/{y}@1x.png?style=gbif-light",
    attribution = 'Map data &copy; <a href="https://www.openstreetmap.org/copyright" rel="noopener noreferrer">OpenStreetMap</a> contributors, &copy; <a href="https://www.openmaptiles.org/copyright" rel="noopener noreferrer">OpenMapTiles</a>',
    # from https://github.com/cran/robis/blob/master/R/map.R
    options = tileOptions(
      tileSize = 512,
      noWrap = TRUE,
      continuousWorld = TRUE,
      # update map after finish of zoom/pan
      updateWhenZooming = FALSE,
      updateWhenIdle = TRUE
    )
  ) %>%
  # add longitude lines
  # could not get working with GeoJSON, works with TopoJSON
  leaflet::addTopoJSON(
    topojson = longitude,
    # light lines
    weight = 0.8,
    color = "#acacad",
    opacity = 0.5,
    # no fill
    fillOpacity = 0,
    fill = FALSE
  ) %>%
  # add latitude lines
  leaflet::addTopoJSON(
    topojson = latitude,
    # light lines
    weight = 0.8,
    color = "#acacad",
    opacity = 0.5,
    # no fill
    fillOpacity = 0,
    fill = FALSE
  ) %>%
  # iceberg positions from GeoJSON
  leaflet::addCircleMarkers(
    data = icebergs,
    radius = 7,
    # purple circle
    fillColor = "#ee82ee",
    fillOpacity = 0.5,
    # no outer line
    stroke = FALSE,
    # add popup from "name" property from GeoJSON
    popup = ~sprintf("<strong>%s</strong>", name),
    popupOptions = popupOptions(closeButton = FALSE)
  ) %>%
  # add feature name labels (text)
  leafem::addStaticLabels(
    feature_names,
    # text will be "name" property
    label = feature_names$name,
    style = list(
      "color" = "#99999a",
      "font-style" = "italic"
    )
  ) %>%
  # configure additional niceties directly on Leaflet map object in javascript
  # in this case, scale control
  # as per https://rstudio.github.io/leaflet/morefeatures.html#custom-javascript-with-htmlwidgetonrender
  htmlwidgets::onRender(jsCode = "
    function(el, x) {
      var map = this;
      // create scale control, see https://www.tutorialspoint.com/leafletjs/leafletjs_controls.htm
      var scale = L.control.scale();
      // add scale control to map
      // by default, bottom-left corner
      scale.addTo(map);
    }")

# add custom CSS to RStudio Viewer HTML
# adapted from https://stackoverflow.com/a/35775262
htmltools::browsable(
  htmltools::tagList(list(
    # add to head tag
    htmltools::tags$head(
      # create style tag and inject basic styles to somewhat match https://thomasswilliams.github.io/leaflet-antarctic-demo/
      # will need lots of "!important" to override inline styles
      # could not get tile styles (filter) to work
      # map element is called "leaflet"
      htmltools::tags$style("body { margin: 0 !important; padding: 0 !important; } html, body, .leaflet { height: 100% !important; width: 100vw !important; } .leaflet-tile-pane .leaflet-layer { filter: sepia(30%) hue-rotate(160deg) contrast(120%) !important; }")
    ),
    # render with Leaflet map
    map
  ))
)