This document introduces various techniques for making interactive maps from spatial data.

Using ggiraph

suppressPackageStartupMessages(library(sp))
suppressPackageStartupMessages(library(sf))
suppressPackageStartupMessages(library(ggplot2))
suppressPackageStartupMessages(library(ggiraph))

world <- sf::st_as_sf(rnaturalearth::countries110)
internet_usage <- suppressMessages(readr::read_csv(
  system.file(
    'extdata','africa-internet_usage-2015.csv',
    package='user2017.geodataviz')))

africa <- dplyr::filter(world, region_un=='Africa') %>%
  dplyr::left_join(internet_usage %>% dplyr::select(
    `Country Code`, `2015 [YR2015]`
  ) %>% dplyr::rename(iso_a3=`Country Code`, internet.usage.2015=`2015 [YR2015]`),
  by = 'iso_a3') %>%
  st_transform(crs="+proj=laea +lon_0=18.984375")

africa.centers <- st_centroid(africa)

africa.spdf <- methods::as(africa, 'Spatial')
africa.spdf@data$id <- row.names(africa.spdf@data)

africa.tidy <- broom::tidy(africa.spdf)
Regions defined for each Polygons
africa.tidy <- dplyr::left_join(africa.tidy, africa.spdf@data, by='id')

g <- ggplot(africa.tidy) +
  geom_polygon_interactive(
    color='black',
    aes(long, lat, group=group, fill=internet.usage.2015,
        tooltip=sprintf("%s<br/>%s",iso_a3,internet.usage.2015))) +
 hrbrthemes::theme_ipsum() +
  colormap::scale_fill_colormap(
    colormap=colormap::colormaps$copper, reverse = T) +
  labs(title='Internet Usage in Africa in 2015', subtitle='As Percent of Population',
       caption='Source: World Bank Open Data.')

widgetframe::frameWidget(ggiraph(code=print(g)))

Using plotly

suppressPackageStartupMessages(library(plotly))
usa <- albersusa::usa_sf("laea")
usd <- crosstalk::SharedData$new(usa)
p <- ggplot(usd) + geom_sf(aes(fill = pop_2010))

ggplotly(p) %>%
  highlight(
    "plotly_hover",
    selected = attrs_selected(line = list(color = "black"))
) %>%
  widgetframe::frameWidget()
Setting the `off` event (i.e., 'plotly_doubleclick') to match the `on` event (i.e., 'plotly_hover'). You can change this default via the `highlight()` function.

Using tmap

Using rbokeh

library(maps)
data(world.cities)
library(rbokeh)
caps <- dplyr::filter(world.cities, capital == 1)
caps$population <- prettyNum(caps$pop, big.mark = ",")
plot <- suppressWarnings(figure(width = 800, height = 450, padding_factor = 0) %>%
  ly_map("world", col = "gray") %>%
  ly_points(long, lat, data = caps, size = 5,
            hover = c(name, country.etc, population)))

widgetframe::frameWidget(plot,width=600,height=400)

Using highcharter

library(magrittr)

download_map_data <- highcharter::download_map_data
get_data_from_map <- highcharter::get_data_from_map
hcmap <- highcharter::hcmap

mapdata <- get_data_from_map(download_map_data("countries/au/au-all"))

data_fake <- mapdata %>%
  dplyr::select(code = `hc-a2`) %>%
  dplyr::mutate(value = 1e5 * abs(rt(nrow(.), df = 10)))

hcmap("countries/au/au-all", data = data_fake, value = "value",
        joinBy = c("hc-a2", "code"), name = "Fake data",
        dataLabels = list(enabled = TRUE, format = '{point.name}'),
        borderColor = "#FAFAFA", borderWidth = 0.1,
        tooltip = list(valueDecimals = 2, valuePrefix = "$", valueSuffix = " AUD")) %>%
  highcharter::hc_title(text="Economy Down Under") %>%
  widgetframe::frameWidget()

Using leaflet

Base Maps

l <- leaflet() %>%
  setView(lat = 50.85045, lng = 4.34878, zoom=13) %>%
  addTiles(group="OSM") %>%
  addProviderTiles(providers$CartoDB.DarkMatter, group="Dark") %>%
  addProviderTiles(providers$CartoDB.Positron, group="Light") %>%
  addLayersControl(baseGroups=c('OSM','Dark','Light'))
  
frameWidget(l)

Markers

quakes.df <- quakes %>% dplyr::mutate(
    mag.level = cut(mag,c(3.5,4.5,5.5,6.5),
    labels = c('> 3.5 & <=4.5', '>4.5 & <=5.5', '>5.5 & <=6.5'))) %>%
  split(.$mag.level)

l <- leaflet() %>%
  addProviderTiles(providers$Esri.OceanBasemap)

names(quakes.df) %>%
  purrr::walk( function(df) {
    l <<- l %>%
      addMarkers(data=quakes.df[[df]], lng=~long, lat=~lat,
                 label=~as.character(mag), popup=~as.character(mag),
                 group = df,
                 clusterOptions = markerClusterOptions())
  })

l <- l %>%
  addLayersControl(
    overlayGroups = names(quakes.df),
    options = layersControlOptions(collapsed = FALSE)) %>%
  addMiniMap(tiles = providers$Esri.OceanBasemap, width = 120, height=80)

Polygons

# spdf is a sp::SpatialPolygonsDataFrame
qpal <- colorQuantile(rev(viridis::viridis(5)),
                      spdf$POPDENSITY, n=5)

l <- leaflet(spdf, options =
               leafletOptions(attributionControl = FALSE, minzoom=1.5)) %>%
  addPolygons(
    label=~stringr::str_c(
      NAME, ' ',
      formatC(POPDENSITY, big.mark = ',', format='d')),
    labelOptions= labelOptions(direction = 'auto'),
    weight=1,color='#333333', opacity=1,
    fillColor = ~qpal(POPDENSITY), fillOpacity = 1,
    highlightOptions = highlightOptions(
      color='#000000', weight = 2,
      bringToFront = TRUE, sendToBack = TRUE)
    ) %>%
  addLegend(
    "topright", pal = qpal, values = ~POPDENSITY,
    title = htmltools::HTML("Population Density<br/>(2005)"),
    opacity = 1 )

Projections 01

spdf <- spdf.world
crs.molvidde <- leafletCRS(
  crsClass="L.Proj.CRS", code='ESRI:53009',
  proj4def= '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +a=6371000 +b=6371000 +units=m +no_defs',
  resolutions = c(65536, 32768, 16384, 8192, 4096, 2048))

l <- leaflet(
  spdf,
  options = leafletOptions(
    maxZoom = 5, crs= crs.molvidde, attributionControl = FALSE)) %>%
  addGraticule(style= list(color= '#999', weight= 0.5, opacity= 1)) %>%
  addGraticule(sphere = TRUE,
               style= list(color= '#777', weight= 1, opacity= 0.25)) %>%
  addPolygons(
    label=~stringr::str_c(
      NAME, ' ', formatC(POPDENSITY, big.mark = ',', format='d')),
    labelOptions= labelOptions(direction = 'auto'),
    weight=1,color='#ffffff', opacity=1,
    fillColor = ~qpal(POPDENSITY), fillOpacity = 1,
    highlightOptions = highlightOptions(
      color='#000000', weight = 2,
      bringToFront = TRUE, sendToBack = TRUE)) 

Projections 02

leaflet.extras

leaflet + crosstalk

LS0tCnRpdGxlOiAiSW50ZXJhY3RpdmUgTWFwcyBpbiBSIgpvdXRwdXQ6CiAgaHRtbF9ub3RlYm9vazoKICAgIHRvYzogVFJVRQogICAgdG9jX2Zsb2F0OiBUUlVFCmVkaXRvcl9vcHRpb25zOiAKICBjaHVua19vdXRwdXRfdHlwZTogY29uc29sZQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQpvcHRpb25zKGh0bWx0b29scy5kaXIudmVyc2lvbiA9IEZBTFNFKQprbml0cjo6b3B0c19jaHVuayRzZXQoY2FjaGUgPSBUUlVFKQprbml0cjo6b3B0c19jaHVuayRzZXQoZGV2ID0gJ3N2ZycpCm9wdGlvbnMoZGV2aWNlID0gZnVuY3Rpb24oZmlsZSwgd2lkdGgsIGhlaWdodCkgewogIHN2Zyh0ZW1wZmlsZSgpLCB3aWR0aCA9IHdpZHRoLCBoZWlnaHQgPSBoZWlnaHQpCn0pCmxpYnJhcnkobWFncml0dHIpCmBgYAoKVGhpcyBkb2N1bWVudCBpbnRyb2R1Y2VzIHZhcmlvdXMgdGVjaG5pcXVlcyBmb3IgbWFraW5nIGludGVyYWN0aXZlIG1hcHMgZnJvbSBzcGF0aWFsIGRhdGEuCgojIFVzaW5nIGBnZ2lyYXBoYAoKYGBge3IgMDEsIGZpZy53aWR0aD02LCBmaWcuaGVpZ2h0PTUsIGZpZy5hbGlnbj0nY2VudGVyJ30Kc3VwcHJlc3NQYWNrYWdlU3RhcnR1cE1lc3NhZ2VzKGxpYnJhcnkoc3ApKQpzdXBwcmVzc1BhY2thZ2VTdGFydHVwTWVzc2FnZXMobGlicmFyeShzZikpCnN1cHByZXNzUGFja2FnZVN0YXJ0dXBNZXNzYWdlcyhsaWJyYXJ5KGdncGxvdDIpKQpzdXBwcmVzc1BhY2thZ2VTdGFydHVwTWVzc2FnZXMobGlicmFyeShnZ2lyYXBoKSkKCndvcmxkIDwtIHNmOjpzdF9hc19zZihybmF0dXJhbGVhcnRoOjpjb3VudHJpZXMxMTApCmludGVybmV0X3VzYWdlIDwtIHN1cHByZXNzTWVzc2FnZXMocmVhZHI6OnJlYWRfY3N2KAogIHN5c3RlbS5maWxlKAogICAgJ2V4dGRhdGEnLCdhZnJpY2EtaW50ZXJuZXRfdXNhZ2UtMjAxNS5jc3YnLAogICAgcGFja2FnZT0ndXNlcjIwMTcuZ2VvZGF0YXZpeicpKSkKCmFmcmljYSA8LSBkcGx5cjo6ZmlsdGVyKHdvcmxkLCByZWdpb25fdW49PSdBZnJpY2EnKSAlPiUKICBkcGx5cjo6bGVmdF9qb2luKGludGVybmV0X3VzYWdlICU+JSBkcGx5cjo6c2VsZWN0KAogICAgYENvdW50cnkgQ29kZWAsIGAyMDE1IFtZUjIwMTVdYAogICkgJT4lIGRwbHlyOjpyZW5hbWUoaXNvX2EzPWBDb3VudHJ5IENvZGVgLCBpbnRlcm5ldC51c2FnZS4yMDE1PWAyMDE1IFtZUjIwMTVdYCksCiAgYnkgPSAnaXNvX2EzJykgJT4lCiAgc3RfdHJhbnNmb3JtKGNycz0iK3Byb2o9bGFlYSArbG9uXzA9MTguOTg0Mzc1IikKCmFmcmljYS5jZW50ZXJzIDwtIHN0X2NlbnRyb2lkKGFmcmljYSkKCmFmcmljYS5zcGRmIDwtIG1ldGhvZHM6OmFzKGFmcmljYSwgJ1NwYXRpYWwnKQphZnJpY2Euc3BkZkBkYXRhJGlkIDwtIHJvdy5uYW1lcyhhZnJpY2Euc3BkZkBkYXRhKQoKYWZyaWNhLnRpZHkgPC0gYnJvb206OnRpZHkoYWZyaWNhLnNwZGYpCmFmcmljYS50aWR5IDwtIGRwbHlyOjpsZWZ0X2pvaW4oYWZyaWNhLnRpZHksIGFmcmljYS5zcGRmQGRhdGEsIGJ5PSdpZCcpCgpnIDwtIGdncGxvdChhZnJpY2EudGlkeSkgKwogIGdlb21fcG9seWdvbl9pbnRlcmFjdGl2ZSgKICAgIGNvbG9yPSdibGFjaycsCiAgICBhZXMobG9uZywgbGF0LCBncm91cD1ncm91cCwgZmlsbD1pbnRlcm5ldC51c2FnZS4yMDE1LAogICAgICAgIHRvb2x0aXA9c3ByaW50ZigiJXM8YnIvPiVzIixpc29fYTMsaW50ZXJuZXQudXNhZ2UuMjAxNSkpKSArCiBocmJydGhlbWVzOjp0aGVtZV9pcHN1bSgpICsKICBjb2xvcm1hcDo6c2NhbGVfZmlsbF9jb2xvcm1hcCgKICAgIGNvbG9ybWFwPWNvbG9ybWFwOjpjb2xvcm1hcHMkY29wcGVyLCByZXZlcnNlID0gVCkgKwogIGxhYnModGl0bGU9J0ludGVybmV0IFVzYWdlIGluIEFmcmljYSBpbiAyMDE1Jywgc3VidGl0bGU9J0FzIFBlcmNlbnQgb2YgUG9wdWxhdGlvbicsCiAgICAgICBjYXB0aW9uPSdTb3VyY2U6IFdvcmxkIEJhbmsgT3BlbiBEYXRhLicpCgp3aWRnZXRmcmFtZTo6ZnJhbWVXaWRnZXQoZ2dpcmFwaChjb2RlPXByaW50KGcpKSkKYGBgCgojIFVzaW5nIGBwbG90bHlgCgoKYGBge3IgMDIsIGZpZy53aWR0aD02LCBmaWcuaGVpZ2h0PTUsIGZpZy5hbGlnbj0nY2VudGVyJ30Kc3VwcHJlc3NQYWNrYWdlU3RhcnR1cE1lc3NhZ2VzKGxpYnJhcnkocGxvdGx5KSkKdXNhIDwtIGFsYmVyc3VzYTo6dXNhX3NmKCJsYWVhIikKdXNkIDwtIGNyb3NzdGFsazo6U2hhcmVkRGF0YSRuZXcodXNhKQpwIDwtIGdncGxvdCh1c2QpICsgZ2VvbV9zZihhZXMoZmlsbCA9IHBvcF8yMDEwKSkKCmdncGxvdGx5KHApICU+JQogIGhpZ2hsaWdodCgKICAgICJwbG90bHlfaG92ZXIiLAogICAgc2VsZWN0ZWQgPSBhdHRyc19zZWxlY3RlZChsaW5lID0gbGlzdChjb2xvciA9ICJibGFjayIpKQopICU+JQogIHdpZGdldGZyYW1lOjpmcmFtZVdpZGdldCgpCgpgYGAKCgojIFVzaW5nIGB0bWFwYAoKIyBVc2luZyBgcmJva2VoYAoKYGBge3IgMDMsIGZpZy53aWR0aD02LCBmaWcuaGVpZ2h0PTUsIGZpZy5hbGlnbj0nY2VudGVyJ30KbGlicmFyeShtYXBzKQpkYXRhKHdvcmxkLmNpdGllcykKbGlicmFyeShyYm9rZWgpCmNhcHMgPC0gZHBseXI6OmZpbHRlcih3b3JsZC5jaXRpZXMsIGNhcGl0YWwgPT0gMSkKY2FwcyRwb3B1bGF0aW9uIDwtIHByZXR0eU51bShjYXBzJHBvcCwgYmlnLm1hcmsgPSAiLCIpCnBsb3QgPC0gc3VwcHJlc3NXYXJuaW5ncyhmaWd1cmUod2lkdGggPSA4MDAsIGhlaWdodCA9IDQ1MCwgcGFkZGluZ19mYWN0b3IgPSAwKSAlPiUKICBseV9tYXAoIndvcmxkIiwgY29sID0gImdyYXkiKSAlPiUKICBseV9wb2ludHMobG9uZywgbGF0LCBkYXRhID0gY2Fwcywgc2l6ZSA9IDUsCiAgICAgICAgICAgIGhvdmVyID0gYyhuYW1lLCBjb3VudHJ5LmV0YywgcG9wdWxhdGlvbikpKQoKd2lkZ2V0ZnJhbWU6OmZyYW1lV2lkZ2V0KHBsb3Qsd2lkdGg9NjAwLGhlaWdodD00MDApCgpgYGAKCgojIFVzaW5nIGBoaWdoY2hhcnRlcmAKCgpgYGB7ciAwNCwgZXZhbD1GQUxTRSwgZmlnLndpZHRoPTYsIGZpZy5oZWlnaHQ9NSwgZmlnLmFsaWduPSdjZW50ZXInfQpsaWJyYXJ5KG1hZ3JpdHRyKQoKZG93bmxvYWRfbWFwX2RhdGEgPC0gaGlnaGNoYXJ0ZXI6OmRvd25sb2FkX21hcF9kYXRhCmdldF9kYXRhX2Zyb21fbWFwIDwtIGhpZ2hjaGFydGVyOjpnZXRfZGF0YV9mcm9tX21hcApoY21hcCA8LSBoaWdoY2hhcnRlcjo6aGNtYXAKCm1hcGRhdGEgPC0gZ2V0X2RhdGFfZnJvbV9tYXAoZG93bmxvYWRfbWFwX2RhdGEoImNvdW50cmllcy9hdS9hdS1hbGwiKSkKCmRhdGFfZmFrZSA8LSBtYXBkYXRhICU+JQogIGRwbHlyOjpzZWxlY3QoY29kZSA9IGBoYy1hMmApICU+JQogIGRwbHlyOjptdXRhdGUodmFsdWUgPSAxZTUgKiBhYnMocnQobnJvdyguKSwgZGYgPSAxMCkpKQoKaGNtYXAoImNvdW50cmllcy9hdS9hdS1hbGwiLCBkYXRhID0gZGF0YV9mYWtlLCB2YWx1ZSA9ICJ2YWx1ZSIsCiAgICAgICAgam9pbkJ5ID0gYygiaGMtYTIiLCAiY29kZSIpLCBuYW1lID0gIkZha2UgZGF0YSIsCiAgICAgICAgZGF0YUxhYmVscyA9IGxpc3QoZW5hYmxlZCA9IFRSVUUsIGZvcm1hdCA9ICd7cG9pbnQubmFtZX0nKSwKICAgICAgICBib3JkZXJDb2xvciA9ICIjRkFGQUZBIiwgYm9yZGVyV2lkdGggPSAwLjEsCiAgICAgICAgdG9vbHRpcCA9IGxpc3QodmFsdWVEZWNpbWFscyA9IDIsIHZhbHVlUHJlZml4ID0gIiQiLCB2YWx1ZVN1ZmZpeCA9ICIgQVVEIikpICU+JQogIGhpZ2hjaGFydGVyOjpoY190aXRsZSh0ZXh0PSJFY29ub215IERvd24gVW5kZXIiKSAlPiUKICB3aWRnZXRmcmFtZTo6ZnJhbWVXaWRnZXQoKQpgYGAKCiMgVXNpbmcgYGxlYWZsZXRgCgoKYGBge3IgbGlicywgZWNobz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0Kc3VwcHJlc3NQYWNrYWdlU3RhcnR1cE1lc3NhZ2VzKGxpYnJhcnkod2lkZ2V0ZnJhbWUpKQpzdXBwcmVzc1BhY2thZ2VTdGFydHVwTWVzc2FnZXMobGlicmFyeShsZWFmbGV0KSkKc3VwcHJlc3NQYWNrYWdlU3RhcnR1cE1lc3NhZ2VzKGxpYnJhcnkobGVhZmxldC5leHRyYXMpKQpgYGAKCgojIyBCYXNlIE1hcHMKCmBgYHtyIDA1LCBmaWcud2lkdGg9NiwgZmlnLmhlaWdodD00LCBmaWcuYWxpZ249J2NlbnRlcid9CmwgPC0gbGVhZmxldCgpICU+JQogIHNldFZpZXcobGF0ID0gNTAuODUwNDUsIGxuZyA9IDQuMzQ4NzgsIHpvb209MTMpICU+JQogIGFkZFRpbGVzKGdyb3VwPSJPU00iKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLkRhcmtNYXR0ZXIsIGdyb3VwPSJEYXJrIikgJT4lCiAgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbiwgZ3JvdXA9IkxpZ2h0IikgJT4lCiAgYWRkTGF5ZXJzQ29udHJvbChiYXNlR3JvdXBzPWMoJ09TTScsJ0RhcmsnLCdMaWdodCcpKQogIApmcmFtZVdpZGdldChsKQpgYGAKCiMjIE1hcmtlcnMKCmBgYHtyIDA3LWxlYWZsZXQtMDQsIHdhcm5pbmc9RkFMU0V9CnF1YWtlcy5kZiA8LSBxdWFrZXMgJT4lIGRwbHlyOjptdXRhdGUoCiAgICBtYWcubGV2ZWwgPSBjdXQobWFnLGMoMy41LDQuNSw1LjUsNi41KSwKICAgIGxhYmVscyA9IGMoJz4gMy41ICYgPD00LjUnLCAnPjQuNSAmIDw9NS41JywgJz41LjUgJiA8PTYuNScpKSkgJT4lCiAgc3BsaXQoLiRtYWcubGV2ZWwpCgpsIDwtIGxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRFc3JpLk9jZWFuQmFzZW1hcCkKCm5hbWVzKHF1YWtlcy5kZikgJT4lCiAgcHVycnI6OndhbGsoIGZ1bmN0aW9uKGRmKSB7CiAgICBsIDw8LSBsICU+JQogICAgICBhZGRNYXJrZXJzKGRhdGE9cXVha2VzLmRmW1tkZl1dLCBsbmc9fmxvbmcsIGxhdD1+bGF0LAogICAgICAgICAgICAgICAgIGxhYmVsPX5hcy5jaGFyYWN0ZXIobWFnKSwgcG9wdXA9fmFzLmNoYXJhY3RlcihtYWcpLAogICAgICAgICAgICAgICAgIGdyb3VwID0gZGYsCiAgICAgICAgICAgICAgICAgY2x1c3Rlck9wdGlvbnMgPSBtYXJrZXJDbHVzdGVyT3B0aW9ucygpKQogIH0pCgpsIDwtIGwgJT4lCiAgYWRkTGF5ZXJzQ29udHJvbCgKICAgIG92ZXJsYXlHcm91cHMgPSBuYW1lcyhxdWFrZXMuZGYpLAogICAgb3B0aW9ucyA9IGxheWVyc0NvbnRyb2xPcHRpb25zKGNvbGxhcHNlZCA9IEZBTFNFKSkgJT4lCiAgYWRkTWluaU1hcCh0aWxlcyA9IHByb3ZpZGVycyRFc3JpLk9jZWFuQmFzZW1hcCwgd2lkdGggPSAxMjAsIGhlaWdodD04MCkKYGBgCgpgYGB7ciAwNy1sZWFmbGV0LTA1LCBlY2hvPUZBTFNFLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD02LCBmaWcuYWxpZ249J2NlbnRlcid9CmwgJT4lCmZyYW1lV2lkZ2V0KCkKYGBgCgojIyBQb2x5Z29ucwoKYGBge3IgZWcgbGVhZmxldC0wNywgZWNobz1GQUxTRSwgd2FybmluZz1GQUxTRX0KbGlicmFyeShtYWdyaXR0cikKZk5hbWUgPC0gc3lzdGVtLmZpbGUoJ2V4dGRhdGEnLCd3b3JsZC1wb3B1bGF0aW9uLmdlby5qc29uJyxwYWNrYWdlID0gJ3VzZXIyMDE3Lmdlb2RhdGF2aXonKQpzcGRmIDwtIGdlb2pzb25pbzo6Z2VvanNvbl9zcChybWFwc2hhcGVyOjptc19zaW1wbGlmeShyZWFkcjo6cmVhZF9maWxlKGZOYW1lKSkpCnNwZGZAZGF0YSAlPD4lIGRwbHlyOjptdXRhdGUoCiAgQVJFQSA9IGFzLm51bWVyaWMoYXMuY2hhcmFjdGVyKEFSRUEpKSwKICBQT1AyMDA1ID0gYXMubnVtZXJpYyhhcy5jaGFyYWN0ZXIoUE9QMjAwNSkpCikKCnNwZGYgPC0gc3Vic2V0KAogIHNwZGYsCiAgIShpcy5uYShBUkVBKSB8IEFSRUEgPDEgfCBpcy5uYShQT1AyMDA1KSB8IFBPUDIwMDU8MSkKKQoKc3BkZkBkYXRhICU8PiUKICBkcGx5cjo6bXV0YXRlKAogIFBPUERFTlNJVFkgPSBQT1AyMDA1L0FSRUEKKQoKc3BkZi53b3JsZCA8LSBzcGRmCmBgYAoKYGBge3IgMDctbGVhZmxldC0wOH0KIyBzcGRmIGlzIGEgc3A6OlNwYXRpYWxQb2x5Z29uc0RhdGFGcmFtZQpxcGFsIDwtIGNvbG9yUXVhbnRpbGUocmV2KHZpcmlkaXM6OnZpcmlkaXMoNSkpLAogICAgICAgICAgICAgICAgICAgICAgc3BkZiRQT1BERU5TSVRZLCBuPTUpCgpsIDwtIGxlYWZsZXQoc3BkZiwgb3B0aW9ucyA9CiAgICAgICAgICAgICAgIGxlYWZsZXRPcHRpb25zKGF0dHJpYnV0aW9uQ29udHJvbCA9IEZBTFNFLCBtaW56b29tPTEuNSkpICU+JQogIGFkZFBvbHlnb25zKAogICAgbGFiZWw9fnN0cmluZ3I6OnN0cl9jKAogICAgICBOQU1FLCAnICcsCiAgICAgIGZvcm1hdEMoUE9QREVOU0lUWSwgYmlnLm1hcmsgPSAnLCcsIGZvcm1hdD0nZCcpKSwKICAgIGxhYmVsT3B0aW9ucz0gbGFiZWxPcHRpb25zKGRpcmVjdGlvbiA9ICdhdXRvJyksCiAgICB3ZWlnaHQ9MSxjb2xvcj0nIzMzMzMzMycsIG9wYWNpdHk9MSwKICAgIGZpbGxDb2xvciA9IH5xcGFsKFBPUERFTlNJVFkpLCBmaWxsT3BhY2l0eSA9IDEsCiAgICBoaWdobGlnaHRPcHRpb25zID0gaGlnaGxpZ2h0T3B0aW9ucygKICAgICAgY29sb3I9JyMwMDAwMDAnLCB3ZWlnaHQgPSAyLAogICAgICBicmluZ1RvRnJvbnQgPSBUUlVFLCBzZW5kVG9CYWNrID0gVFJVRSkKICAgICkgJT4lCiAgYWRkTGVnZW5kKAogICAgInRvcHJpZ2h0IiwgcGFsID0gcXBhbCwgdmFsdWVzID0gflBPUERFTlNJVFksCiAgICB0aXRsZSA9IGh0bWx0b29sczo6SFRNTCgiUG9wdWxhdGlvbiBEZW5zaXR5PGJyLz4oMjAwNSkiKSwKICAgIG9wYWNpdHkgPSAxICkKCmBgYAoKYGBge3IgMDctbGVhZmxldC0wOSwgZWNobz1GQUxTRSwgZmlnLndpZHRoPTgsIGZpZy5oZWlnaHQ9NiwgZmlnLmFsaWduPSdjZW50ZXInfQpsICU+JSBzZXRNYXBXaWRnZXRTdHlsZSgpICU+JQogIGZyYW1lV2lkZ2V0KCkKYGBgCgoKIyMgUHJvamVjdGlvbnMgMDEKCmBgYHtyIDA3LWxlYWZsZXQtMTJ9CnNwZGYgPC0gc3BkZi53b3JsZApjcnMubW9sdmlkZGUgPC0gbGVhZmxldENSUygKICBjcnNDbGFzcz0iTC5Qcm9qLkNSUyIsIGNvZGU9J0VTUkk6NTMwMDknLAogIHByb2o0ZGVmPSAnK3Byb2o9bW9sbCArbG9uXzA9MCAreF8wPTAgK3lfMD0wICthPTYzNzEwMDAgK2I9NjM3MTAwMCArdW5pdHM9bSArbm9fZGVmcycsCiAgcmVzb2x1dGlvbnMgPSBjKDY1NTM2LCAzMjc2OCwgMTYzODQsIDgxOTIsIDQwOTYsIDIwNDgpKQoKbCA8LSBsZWFmbGV0KAogIHNwZGYsCiAgb3B0aW9ucyA9IGxlYWZsZXRPcHRpb25zKAogICAgbWF4Wm9vbSA9IDUsIGNycz0gY3JzLm1vbHZpZGRlLCBhdHRyaWJ1dGlvbkNvbnRyb2wgPSBGQUxTRSkpICU+JQogIGFkZEdyYXRpY3VsZShzdHlsZT0gbGlzdChjb2xvcj0gJyM5OTknLCB3ZWlnaHQ9IDAuNSwgb3BhY2l0eT0gMSkpICU+JQogIGFkZEdyYXRpY3VsZShzcGhlcmUgPSBUUlVFLAogICAgICAgICAgICAgICBzdHlsZT0gbGlzdChjb2xvcj0gJyM3NzcnLCB3ZWlnaHQ9IDEsIG9wYWNpdHk9IDAuMjUpKSAlPiUKICBhZGRQb2x5Z29ucygKICAgIGxhYmVsPX5zdHJpbmdyOjpzdHJfYygKICAgICAgTkFNRSwgJyAnLCBmb3JtYXRDKFBPUERFTlNJVFksIGJpZy5tYXJrID0gJywnLCBmb3JtYXQ9J2QnKSksCiAgICBsYWJlbE9wdGlvbnM9IGxhYmVsT3B0aW9ucyhkaXJlY3Rpb24gPSAnYXV0bycpLAogICAgd2VpZ2h0PTEsY29sb3I9JyNmZmZmZmYnLCBvcGFjaXR5PTEsCiAgICBmaWxsQ29sb3IgPSB+cXBhbChQT1BERU5TSVRZKSwgZmlsbE9wYWNpdHkgPSAxLAogICAgaGlnaGxpZ2h0T3B0aW9ucyA9IGhpZ2hsaWdodE9wdGlvbnMoCiAgICAgIGNvbG9yPScjMDAwMDAwJywgd2VpZ2h0ID0gMiwKICAgICAgYnJpbmdUb0Zyb250ID0gVFJVRSwgc2VuZFRvQmFjayA9IFRSVUUpKSAKYGBgCgpgYGB7ciAwNy1sZWFmbGV0LTEzLCBlY2hvPUZBTFNFLCBmaWcud2lkdGg9OCwgZmlnLmhlaWdodD02LCBmaWcuYWxpZ249J2NlbnRlcid9CmwgJT4lCiAgc2V0VmlldygxMCwwLDAuNSkgJT4lIHNldE1hcFdpZGdldFN0eWxlKCkgJT4lCiAgZnJhbWVXaWRnZXQoKQpgYGAKCgoKIyMgUHJvamVjdGlvbnMgMDIKCiMjIGBsZWFmbGV0LmV4dHJhc2AKCiMjIGBsZWFmbGV0YCArIGBjcm9zc3RhbGtgCg==