Introduction

This notebook covers interactive map production in R using the leflet library. We’ll review some of what we discussed during the first course meeting and then introduce some additional complexity with leaflet.

Dependencies

This notebook requires a variety of packages for working with spatial data:

# tidyverse packages
library(dplyr)        # data wrangling

# spatial packages
library(leaflet)      # interactive maps
library(mapview)      # preview spatial data
library(sf)           # spatial data tools

# other packages
library(here)         # file path management
library(measurements) # convert units
library(RColorBrewer) # color palettes

Load Data

This notebook requires the data stored in data/example-data/. We’ll automatically convert these to WGS84 for mapping, since this is the coordinate system they require:

city <- st_read(here("data", "example-data", "STL_BOUNDARY_City", "STL_BOUNDARY_City.shp"), stringsAsFactors = FALSE) %>%
  st_transform(crs = 4326)
Reading layer `STL_BOUNDARY_City' from data source `/Users/chris/GitHub/slu-soc5650/content/module-5-leaflet/data/example-data/STL_BOUNDARY_City/STL_BOUNDARY_City.shp' using driver `ESRI Shapefile'
Simple feature collection with 1 feature and 17 fields
Geometry type: POLYGON
Dimension:     XY
Bounding box:  xmin: -90.32052 ymin: 38.53185 xmax: -90.16657 ymax: 38.77443
Geodetic CRS:  GRS 1980(IUGG, 1980)
nhoods <- st_read(here("data", "example-data", "STL_DEMOS_Nhoods", "STL_DEMOS_Nhoods.shp"), stringsAsFactors = FALSE) %>%
  st_transform(crs = 4326)
Reading layer `STL_DEMOS_Nhoods' from data source `/Users/chris/GitHub/slu-soc5650/content/module-5-leaflet/data/example-data/STL_DEMOS_Nhoods/STL_DEMOS_Nhoods.shp' using driver `ESRI Shapefile'
Simple feature collection with 79 features and 6 fields
Geometry type: MULTIPOLYGON
Dimension:     XY
Bounding box:  xmin: 733361.8 ymin: 4268512 xmax: 745417.9 ymax: 4295501
Projected CRS: UTM_Zone_15_Northern_Hemisphere

Interactive Mapping with leaflet (Module 1 review)

Basic Mapping of Geometric Objects

During our first course meeting, we discussed the creation of interactive maps using the leaflet package. Leaflet is a JavaScript library for creating interactive maps. It is primarily focused on web and mobile mapping. The leaflet package for R provides access to the JavaScript library.

Leaflet provides a number of basemaps for mapping. If you add map tiles using addTiles(), you’ll get the open street map basemap. Other basemaps can be added using addProviderTiles(). The names of available options can be found using:

names(providers)
  [1] "OpenStreetMap"                       "OpenStreetMap.Mapnik"                "OpenStreetMap.DE"                   
  [4] "OpenStreetMap.CH"                    "OpenStreetMap.France"                "OpenStreetMap.HOT"                  
  [7] "OpenStreetMap.BZH"                   "OpenSeaMap"                          "OpenPtMap"                          
 [10] "OpenTopoMap"                         "OpenRailwayMap"                      "OpenFireMap"                        
 [13] "SafeCast"                            "Thunderforest"                       "Thunderforest.OpenCycleMap"         
 [16] "Thunderforest.Transport"             "Thunderforest.TransportDark"         "Thunderforest.SpinalMap"            
 [19] "Thunderforest.Landscape"             "Thunderforest.Outdoors"              "Thunderforest.Pioneer"              
 [22] "Thunderforest.MobileAtlas"           "Thunderforest.Neighbourhood"         "OpenMapSurfer"                      
 [25] "OpenMapSurfer.Roads"                 "OpenMapSurfer.Hybrid"                "OpenMapSurfer.AdminBounds"          
 [28] "OpenMapSurfer.ContourLines"          "OpenMapSurfer.Hillshade"             "OpenMapSurfer.ElementsAtRisk"       
 [31] "Hydda"                               "Hydda.Full"                          "Hydda.Base"                         
 [34] "Hydda.RoadsAndLabels"                "MapBox"                              "Stamen"                             
 [37] "Stamen.Toner"                        "Stamen.TonerBackground"              "Stamen.TonerHybrid"                 
 [40] "Stamen.TonerLines"                   "Stamen.TonerLabels"                  "Stamen.TonerLite"                   
 [43] "Stamen.Watercolor"                   "Stamen.Terrain"                      "Stamen.TerrainBackground"           
 [46] "Stamen.TerrainLabels"                "Stamen.TopOSMRelief"                 "Stamen.TopOSMFeatures"              
 [49] "TomTom"                              "TomTom.Basic"                        "TomTom.Hybrid"                      
 [52] "TomTom.Labels"                       "Esri"                                "Esri.WorldStreetMap"                
 [55] "Esri.DeLorme"                        "Esri.WorldTopoMap"                   "Esri.WorldImagery"                  
 [58] "Esri.WorldTerrain"                   "Esri.WorldShadedRelief"              "Esri.WorldPhysical"                 
 [61] "Esri.OceanBasemap"                   "Esri.NatGeoWorldMap"                 "Esri.WorldGrayCanvas"               
 [64] "OpenWeatherMap"                      "OpenWeatherMap.Clouds"               "OpenWeatherMap.CloudsClassic"       
 [67] "OpenWeatherMap.Precipitation"        "OpenWeatherMap.PrecipitationClassic" "OpenWeatherMap.Rain"                
 [70] "OpenWeatherMap.RainClassic"          "OpenWeatherMap.Pressure"             "OpenWeatherMap.PressureContour"     
 [73] "OpenWeatherMap.Wind"                 "OpenWeatherMap.Temperature"          "OpenWeatherMap.Snow"                
 [76] "HERE"                                "HERE.normalDay"                      "HERE.normalDayCustom"               
 [79] "HERE.normalDayGrey"                  "HERE.normalDayMobile"                "HERE.normalDayGreyMobile"           
 [82] "HERE.normalDayTransit"               "HERE.normalDayTransitMobile"         "HERE.normalDayTraffic"              
 [85] "HERE.normalNight"                    "HERE.normalNightMobile"              "HERE.normalNightGrey"               
 [88] "HERE.normalNightGreyMobile"          "HERE.normalNightTransit"             "HERE.normalNightTransitMobile"      
 [91] "HERE.reducedDay"                     "HERE.reducedNight"                   "HERE.basicMap"                      
 [94] "HERE.mapLabels"                      "HERE.trafficFlow"                    "HERE.carnavDayGrey"                 
 [97] "HERE.hybridDay"                      "HERE.hybridDayMobile"                "HERE.hybridDayTransit"              
[100] "HERE.hybridDayGrey"                  "HERE.hybridDayTraffic"               "HERE.pedestrianDay"                 
[103] "HERE.pedestrianNight"                "HERE.satelliteDay"                   "HERE.terrainDay"                    
[106] "HERE.terrainDayMobile"               "FreeMapSK"                           "MtbMap"                             
[109] "CartoDB"                             "CartoDB.Positron"                    "CartoDB.PositronNoLabels"           
[112] "CartoDB.PositronOnlyLabels"          "CartoDB.DarkMatter"                  "CartoDB.DarkMatterNoLabels"         
[115] "CartoDB.DarkMatterOnlyLabels"        "CartoDB.Voyager"                     "CartoDB.VoyagerNoLabels"            
[118] "CartoDB.VoyagerOnlyLabels"           "CartoDB.VoyagerLabelsUnder"          "HikeBike"                           
[121] "HikeBike.HikeBike"                   "HikeBike.HillShading"                "BasemapAT"                          
[124] "BasemapAT.basemap"                   "BasemapAT.grau"                      "BasemapAT.overlay"                  
[127] "BasemapAT.highdpi"                   "BasemapAT.orthofoto"                 "nlmaps"                             
[130] "nlmaps.standaard"                    "nlmaps.pastel"                       "nlmaps.grijs"                       
[133] "nlmaps.luchtfoto"                    "NASAGIBS"                            "NASAGIBS.ModisTerraTrueColorCR"     
[136] "NASAGIBS.ModisTerraBands367CR"       "NASAGIBS.ViirsEarthAtNight2012"      "NASAGIBS.ModisTerraLSTDay"          
[139] "NASAGIBS.ModisTerraSnowCover"        "NASAGIBS.ModisTerraAOD"              "NASAGIBS.ModisTerraChlorophyll"     
[142] "NLS"                                 "JusticeMap"                          "JusticeMap.income"                  
[145] "JusticeMap.americanIndian"           "JusticeMap.asian"                    "JusticeMap.black"                   
[148] "JusticeMap.hispanic"                 "JusticeMap.multi"                    "JusticeMap.nonWhite"                
[151] "JusticeMap.white"                    "JusticeMap.plurality"                "Wikimedia"                          
[154] "GeoportailFrance"                    "GeoportailFrance.parcels"            "GeoportailFrance.ignMaps"           
[157] "GeoportailFrance.maps"               "GeoportailFrance.orthos"             "OneMapSG"                           
[160] "OneMapSG.Default"                    "OneMapSG.Night"                      "OneMapSG.Original"                  
[163] "OneMapSG.Grey"                       "OneMapSG.LandLot"                   

As you can see, there are a ton of choices! We’ll use CartoDB.Positron here, but feel free to pick one that you like for assignments if we don’t specify what you should use. Make sure that your other cartographic selections, such as color, do not clash with your basemap.

The basic leaflet workflow involves piping functions together (the %>% operator). Each time to see the pipe, think of the word “then.” For example, the following code chunk would read:

1.Take the city object, then 2. use it as the basis for creating a leaflet object with leaflet(), then 3. add a basemap using the CartoDB.Positron tiles, then 4. add polygons and create a pop-up.

city %>%
  leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(popup = ~NAME)

Adding Pop-ups

We can get more into the weeds with the neighborhood data since they have additional features. We can create more detailed pop-ups using the base::paste() function and some html tags. The most important html tags to know are:

  • <b>text</b> - bold text
  • <em>text</em> - italicized text
  • <br> - line break
nhoods %>%  
  leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(popup = paste("<b>Name:</b> ", nhoods$NHD_NAME, "<br>",
                            "<b>2017 Population:</b> ", round(nhoods$pop17, digits = 0)))

Mapping Quantities with leaflet

If we want to turn this into a thematic choropleth map, we can add some additional parameters to the addPolygons() function. Before we do that, however, we should create normalized versions of our two population variables, pop50 and pop17. We can use the AREA column, which represents the area of each neighborhood in square meters, as a basis for this.

nhoods %>%
  mutate(sq_km = conv_unit(AREA, from = "m2", to = "km2"), .after = "AREA") %>%
  mutate(pop50_den = pop50/sq_km, .after = "pop50") %>%
  mutate(pop17_den = pop17/sq_km, .after = "pop17") -> nhoods

Now that we have these normalized, we can get mapping! The additional cartographic options we’ll mention are:

  • color - outline (“stroke”) color for each polygon
  • weight - stroke width
  • opacity - stroke opacity
  • smoothFactor - allows leaflet to simplify polygons depending on zoom
  • fillOpacity - fill opacity
  • fillColor - creates the fill itself
  • highlightOptions - creates effect when mouse drags over specific polygons

What I have here are good default settings for most of these options, but feel free to experiment!

When we created our pop-up, we want to round our values so that we don’t see the very long real number associated with our data. By using base::round(var, digits = 0), we round to the nearest integer. digits = 2 would give us two decimal places in contrast.

# create color palette
npal <- colorNumeric("YlOrRd", nhoods$pop17_den)

# create leaflet object
nhoods %>%
  leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(
    color = "#444444", 
    weight = 1, 
    opacity = 1.0, 
    smoothFactor = 0.5,
    fillOpacity = 0.5,
    fillColor = ~npal(pop17_den),
    highlightOptions = highlightOptions(color = "white", weight = 2, bringToFront = TRUE),
    popup = paste("<b>Name:</b> ", nhoods$NHD_NAME, "<br>",
                  "<b>2017 Population:</b> ", round(nhoods$pop17, digits = 0), "<br>",
                  "<b>2017 Population per Square Kilometer:</b> ", round(nhoods$pop17_den, digits = 2))) 

Next, we should add a legend to make the map easier to interpret. This is done with the addLegend() argument. The opacity argument in addLegend() should match the fillOpacity argument in addPolygons()!

# create color palette
npal <- colorNumeric("YlOrRd", nhoods$pop17_den)

# create leaflet object
nhoods %>%
  leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(
    color = "#444444", 
    weight = 1, 
    opacity = 1.0, 
    smoothFactor = 0.5,
    fillOpacity = 0.5,
    fillColor = ~npal(pop17_den),
    highlightOptions = highlightOptions(color = "white", weight = 2, bringToFront = TRUE),
    popup = paste("<b>Name:</b> ", nhoods$NHD_NAME, "<br>",
                  "<b>2017 Population:</b> ", round(nhoods$pop17, digits = 0), "<br>",
                  "<b>2017 Population per Square Kilometer:</b> ", round(nhoods$pop17_den, digits = 2))) %>%
    addLegend(pal = npal, values = ~pop17_den, opacity = .5, title = "Population Density (2017)")

As a review, the color palette we’re using comes from the RColorBrewer package. We can use RColorBrewer::display.brewer.all() to identify other color ramps:

display.brewer.all(type = "seq")

One final complication we’ll discuss is adding the city’s outline on top of the addPolygons() function. We can’t use a second instance of addPolygons() in our call, and so we’ll use the addPolylines() function instead. This allows us to display the boundary as a line instead of a polygon feature.

# create color palette
npal <- colorNumeric("Blues", nhoods$pop17_den)

# create leaflet object
nhoods %>%
  leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(
    color = "#444444", 
    weight = 1, 
    opacity = 1.0, 
    smoothFactor = 0.5,
    fillOpacity = 0.5,
    fillColor = ~npal(pop17_den),
    highlightOptions = highlightOptions(color = "white", weight = 2, bringToFront = TRUE),
    popup = paste("<b>Name:</b> ", nhoods$NHD_NAME, "<br>",
                  "<b>2017 Population:</b> ", round(nhoods$pop17, digits = 0), "<br>",
                  "<b>2017 Population per Square Kilometer:</b> ", round(nhoods$pop17_den, digits = 2))) %>%
    addPolylines(
      data = city,
      color = "#000000",
      weight = 3
    ) %>%
    addLegend(pal = npal, values = ~pop17_den, opacity = .5, title = "Population Density (2017)")

For our final leaflet map, write your own code to map the 1950 population density of neighborhoods:

# create color palette
npal <- colorNumeric("RdPu", nhoods$pop50_den)

# create map
nhoods %>%
  leaflet() %>%
  addProviderTiles(providers$CartoDB.Positron) %>%
  addPolygons(
    color = "#444444", 
    weight = 1, 
    smoothFactor = 0.5,
    opacity = 1.0, 
    fillOpacity = 0.5,
    fillColor = ~npal(pop50_den),
    highlightOptions = highlightOptions(color = "white", weight = 2, bringToFront = TRUE),
    popup = paste("<b>Name:</b> ", nhoods$NHD_NAME, "<br>",
                  "<b>1950 Population:</b> ", round(nhoods$pop50, digits = 0), "<br>",
                  "<b>1950 Population per Square Kilometer:</b> ", 
                      round(nhoods$pop50_den, digits = 2))) %>%
    addPolylines(
      data = city,
      color = "#000000",
      weight = 3
    ) %>%
    addLegend(pal = npal, values = ~pop50_den, opacity = .5, 
              title = "Population Density (1950)")
LS0tCnRpdGxlOiAiTGVjdHVyZS0wNSBFeGFtcGxlcyIKYXV0aG9yOiAiQ2hyaXN0b3BoZXIgUHJlbmVyLCBQaC5ELiIKZGF0ZTogJyhgciBmb3JtYXQoU3lzLnRpbWUoKSwgIiVCICVkLCAlWSIpYCknCm91dHB1dDogCiAgZ2l0aHViX2RvY3VtZW50OiBkZWZhdWx0CiAgaHRtbF9ub3RlYm9vazogZGVmYXVsdAotLS0KCiMjIEludHJvZHVjdGlvbgpUaGlzIG5vdGVib29rIGNvdmVycyBpbnRlcmFjdGl2ZSBtYXAgcHJvZHVjdGlvbiBpbiBgUmAgdXNpbmcgdGhlIGBsZWZsZXRgIGxpYnJhcnkuIFdlJ2xsIHJldmlldyBzb21lIG9mIHdoYXQgd2UgZGlzY3Vzc2VkIGR1cmluZyBbdGhlIGZpcnN0IGNvdXJzZSBtZWV0aW5nXShodHRwczovL3NsdS1zb2M1NjUwLmdpdGh1Yi5pby9kb2NzL21lZXRpbmdfMDEvKSBhbmQgdGhlbiBpbnRyb2R1Y2Ugc29tZSBhZGRpdGlvbmFsIGNvbXBsZXhpdHkgd2l0aCBgbGVhZmxldGAuCgojIyBEZXBlbmRlbmNpZXMKVGhpcyBub3RlYm9vayByZXF1aXJlcyBhIHZhcmlldHkgb2YgcGFja2FnZXMgZm9yIHdvcmtpbmcgd2l0aCBzcGF0aWFsIGRhdGE6CgpgYGB7ciBsb2FkLXBhY2thZ2VzfQojIHRpZHl2ZXJzZSBwYWNrYWdlcwpsaWJyYXJ5KGRwbHlyKSAgICAgICAgIyBkYXRhIHdyYW5nbGluZwoKIyBzcGF0aWFsIHBhY2thZ2VzCmxpYnJhcnkobGVhZmxldCkgICAgICAjIGludGVyYWN0aXZlIG1hcHMKbGlicmFyeShtYXB2aWV3KSAgICAgICMgcHJldmlldyBzcGF0aWFsIGRhdGEKbGlicmFyeShzZikgICAgICAgICAgICMgc3BhdGlhbCBkYXRhIHRvb2xzCgojIG90aGVyIHBhY2thZ2VzCmxpYnJhcnkoaGVyZSkgICAgICAgICAjIGZpbGUgcGF0aCBtYW5hZ2VtZW50CmxpYnJhcnkobWVhc3VyZW1lbnRzKSAjIGNvbnZlcnQgdW5pdHMKbGlicmFyeShSQ29sb3JCcmV3ZXIpICMgY29sb3IgcGFsZXR0ZXMKYGBgCgojIyBMb2FkIERhdGEKVGhpcyBub3RlYm9vayByZXF1aXJlcyB0aGUgZGF0YSBzdG9yZWQgaW4gYGRhdGEvZXhhbXBsZS1kYXRhL2AuIFdlJ2xsIGF1dG9tYXRpY2FsbHkgY29udmVydCB0aGVzZSB0byBXR1M4NCBmb3IgbWFwcGluZywgc2luY2UgdGhpcyBpcyB0aGUgY29vcmRpbmF0ZSBzeXN0ZW0gdGhleSByZXF1aXJlOgoKYGBge3IgbG9hZC1kYXRhfQpjaXR5IDwtIHN0X3JlYWQoaGVyZSgiZGF0YSIsICJleGFtcGxlLWRhdGEiLCAiU1RMX0JPVU5EQVJZX0NpdHkiLCAiU1RMX0JPVU5EQVJZX0NpdHkuc2hwIiksIHN0cmluZ3NBc0ZhY3RvcnMgPSBGQUxTRSkgJT4lCiAgc3RfdHJhbnNmb3JtKGNycyA9IDQzMjYpCm5ob29kcyA8LSBzdF9yZWFkKGhlcmUoImRhdGEiLCAiZXhhbXBsZS1kYXRhIiwgIlNUTF9ERU1PU19OaG9vZHMiLCAiU1RMX0RFTU9TX05ob29kcy5zaHAiKSwgc3RyaW5nc0FzRmFjdG9ycyA9IEZBTFNFKSAlPiUKICBzdF90cmFuc2Zvcm0oY3JzID0gNDMyNikKYGBgCgojIyBJbnRlcmFjdGl2ZSBNYXBwaW5nIHdpdGggYGxlYWZsZXRgIChNb2R1bGUgMSByZXZpZXcpCiMjIyBCYXNpYyBNYXBwaW5nIG9mIEdlb21ldHJpYyBPYmplY3RzCkR1cmluZyBvdXIgZmlyc3QgY291cnNlIG1lZXRpbmcsIHdlIGRpc2N1c3NlZCB0aGUgY3JlYXRpb24gb2YgaW50ZXJhY3RpdmUgbWFwcyB1c2luZyB0aGUgYGxlYWZsZXRgIHBhY2thZ2UuIFtMZWFmbGV0XShodHRwczovL2xlYWZsZXRqcy5jb20pIGlzIGEgSmF2YVNjcmlwdCBsaWJyYXJ5IGZvciBjcmVhdGluZyBpbnRlcmFjdGl2ZSBtYXBzLiBJdCBpcyBwcmltYXJpbHkgZm9jdXNlZCBvbiB3ZWIgYW5kIG1vYmlsZSBtYXBwaW5nLiBUaGUgYGxlYWZsZXRgIHBhY2thZ2UgZm9yIGBSYCBwcm92aWRlcyBhY2Nlc3MgdG8gdGhlIEphdmFTY3JpcHQgbGlicmFyeS4gCgpMZWFmbGV0IHByb3ZpZGVzIGEgbnVtYmVyIG9mIGJhc2VtYXBzIGZvciBtYXBwaW5nLiBJZiB5b3UgYWRkIG1hcCB0aWxlcyB1c2luZyBgYWRkVGlsZXMoKWAsIHlvdSdsbCBnZXQgdGhlIG9wZW4gc3RyZWV0IG1hcCBiYXNlbWFwLiBPdGhlciBiYXNlbWFwcyBjYW4gYmUgYWRkZWQgdXNpbmcgYGFkZFByb3ZpZGVyVGlsZXMoKWAuIFRoZSBuYW1lcyBvZiBhdmFpbGFibGUgb3B0aW9ucyBjYW4gYmUgZm91bmQgdXNpbmc6CgpgYGB7ciBsZWFmbGV0LW5hbWVzfQpuYW1lcyhwcm92aWRlcnMpCmBgYAoKQXMgeW91IGNhbiBzZWUsIHRoZXJlIGFyZSBhIHRvbiBvZiBjaG9pY2VzISBXZSdsbCB1c2UgYENhcnRvREIuUG9zaXRyb25gIGhlcmUsIGJ1dCBmZWVsIGZyZWUgdG8gcGljayBvbmUgdGhhdCB5b3UgbGlrZSBmb3IgYXNzaWdubWVudHMgaWYgd2UgZG9uJ3Qgc3BlY2lmeSB3aGF0IHlvdSBzaG91bGQgdXNlLiBNYWtlIHN1cmUgdGhhdCB5b3VyIG90aGVyIGNhcnRvZ3JhcGhpYyBzZWxlY3Rpb25zLCBzdWNoIGFzIGNvbG9yLCBkbyBub3QgY2xhc2ggd2l0aCB5b3VyIGJhc2VtYXAuIAoKVGhlIGJhc2ljIGBsZWFmbGV0YCB3b3JrZmxvdyBpbnZvbHZlcyBwaXBpbmcgZnVuY3Rpb25zIHRvZ2V0aGVyICh0aGUgYCU+JWAgb3BlcmF0b3IpLiBFYWNoIHRpbWUgdG8gc2VlIHRoZSBwaXBlLCB0aGluayBvZiB0aGUgd29yZCAidGhlbi4iIEZvciBleGFtcGxlLCB0aGUgZm9sbG93aW5nIGNvZGUgY2h1bmsgd291bGQgcmVhZDoKCjEuVGFrZSB0aGUgYGNpdHlgIG9iamVjdCwgKip0aGVuKioKMi4gdXNlIGl0IGFzIHRoZSBiYXNpcyBmb3IgY3JlYXRpbmcgYSBgbGVhZmxldGAgb2JqZWN0IHdpdGggYGxlYWZsZXQoKWAsICoqdGhlbioqCjMuIGFkZCBhIGJhc2VtYXAgdXNpbmcgdGhlIGBDYXJ0b0RCLlBvc2l0cm9uYCB0aWxlcywgKip0aGVuKioKNC4gYWRkIHBvbHlnb25zIGFuZCBjcmVhdGUgYSBwb3AtdXAuCgpgYGB7ciBsZWFmbGV0LWNpdHl9CmNpdHkgJT4lCiAgbGVhZmxldCgpICU+JQogIGFkZFByb3ZpZGVyVGlsZXMocHJvdmlkZXJzJENhcnRvREIuUG9zaXRyb24pICU+JQogIGFkZFBvbHlnb25zKHBvcHVwID0gfk5BTUUpCmBgYAoKIyMjIEFkZGluZyBQb3AtdXBzCgpXZSBjYW4gZ2V0IG1vcmUgaW50byB0aGUgd2VlZHMgd2l0aCB0aGUgbmVpZ2hib3Job29kIGRhdGEgc2luY2UgdGhleSBoYXZlIGFkZGl0aW9uYWwgZmVhdHVyZXMuIFdlIGNhbiBjcmVhdGUgbW9yZSBkZXRhaWxlZCBwb3AtdXBzIHVzaW5nIHRoZSBgYmFzZTo6cGFzdGUoKWAgZnVuY3Rpb24gYW5kIHNvbWUgaHRtbCB0YWdzLiBUaGUgbW9zdCBpbXBvcnRhbnQgaHRtbCB0YWdzIHRvIGtub3cgYXJlOgoKKiBgPGI+dGV4dDwvYj5gIC0gYm9sZCB0ZXh0CiogYDxlbT50ZXh0PC9lbT5gIC0gaXRhbGljaXplZCB0ZXh0CiogYDxicj5gIC0gbGluZSBicmVhawoKYGBge3IgbGVhZmxldC1uaG9vZHN9Cm5ob29kcyAlPiUgIAogIGxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBhZGRQb2x5Z29ucyhwb3B1cCA9IHBhc3RlKCI8Yj5OYW1lOjwvYj4gIiwgbmhvb2RzJE5IRF9OQU1FLCAiPGJyPiIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAiPGI+MjAxNyBQb3B1bGF0aW9uOjwvYj4gIiwgcm91bmQobmhvb2RzJHBvcDE3LCBkaWdpdHMgPSAwKSkpCmBgYAoKIyMjIE1hcHBpbmcgUXVhbnRpdGllcyB3aXRoIGBsZWFmbGV0YAoKSWYgd2Ugd2FudCB0byB0dXJuIHRoaXMgaW50byBhIHRoZW1hdGljIGNob3JvcGxldGggbWFwLCB3ZSBjYW4gYWRkIHNvbWUgYWRkaXRpb25hbCBwYXJhbWV0ZXJzIHRvIHRoZSBgYWRkUG9seWdvbnMoKWAgZnVuY3Rpb24uIEJlZm9yZSB3ZSBkbyB0aGF0LCBob3dldmVyLCB3ZSBzaG91bGQgY3JlYXRlIG5vcm1hbGl6ZWQgdmVyc2lvbnMgb2Ygb3VyIHR3byBwb3B1bGF0aW9uIHZhcmlhYmxlcywgYHBvcDUwYCBhbmQgYHBvcDE3YC4gV2UgY2FuIHVzZSB0aGUgYEFSRUFgIGNvbHVtbiwgd2hpY2ggcmVwcmVzZW50cyB0aGUgYXJlYSBvZiBlYWNoIG5laWdoYm9yaG9vZCBpbiBzcXVhcmUgbWV0ZXJzLCBhcyBhIGJhc2lzIGZvciB0aGlzLgoKYGBge3Igbm9ybWFsaXplfQpuaG9vZHMgJT4lCiAgbXV0YXRlKHNxX2ttID0gY29udl91bml0KEFSRUEsIGZyb20gPSAibTIiLCB0byA9ICJrbTIiKSwgLmFmdGVyID0gIkFSRUEiKSAlPiUKICBtdXRhdGUocG9wNTBfZGVuID0gcG9wNTAvc3Ffa20sIC5hZnRlciA9ICJwb3A1MCIpICU+JQogIG11dGF0ZShwb3AxN19kZW4gPSBwb3AxNy9zcV9rbSwgLmFmdGVyID0gInBvcDE3IikgLT4gbmhvb2RzCmBgYAoKTm93IHRoYXQgd2UgaGF2ZSB0aGVzZSBub3JtYWxpemVkLCB3ZSBjYW4gZ2V0IG1hcHBpbmchIFRoZSBhZGRpdGlvbmFsIGNhcnRvZ3JhcGhpYyBvcHRpb25zIHdlJ2xsIG1lbnRpb24gYXJlOgoKKiBgY29sb3JgIC0gb3V0bGluZSAoInN0cm9rZSIpIGNvbG9yIGZvciBlYWNoIHBvbHlnb24KKiBgd2VpZ2h0YCAtIHN0cm9rZSB3aWR0aAoqIGBvcGFjaXR5YCAtIHN0cm9rZSBvcGFjaXR5CiogYHNtb290aEZhY3RvcmAgLSBhbGxvd3MgYGxlYWZsZXRgIHRvIHNpbXBsaWZ5IHBvbHlnb25zIGRlcGVuZGluZyBvbiB6b29tCiogYGZpbGxPcGFjaXR5YCAtIGZpbGwgb3BhY2l0eQoqIGBmaWxsQ29sb3JgIC0gY3JlYXRlcyB0aGUgZmlsbCBpdHNlbGYKKiBgaGlnaGxpZ2h0T3B0aW9uc2AgLSBjcmVhdGVzIGVmZmVjdCB3aGVuIG1vdXNlIGRyYWdzIG92ZXIgc3BlY2lmaWMgcG9seWdvbnMKCldoYXQgSSBoYXZlIGhlcmUgYXJlIGdvb2QgZGVmYXVsdCBzZXR0aW5ncyBmb3IgbW9zdCBvZiB0aGVzZSBvcHRpb25zLCBidXQgZmVlbCBmcmVlIHRvIGV4cGVyaW1lbnQhCgpXaGVuIHdlIGNyZWF0ZWQgb3VyIHBvcC11cCwgd2Ugd2FudCB0byByb3VuZCBvdXIgdmFsdWVzIHNvIHRoYXQgd2UgZG9uJ3Qgc2VlIHRoZSB2ZXJ5IGxvbmcgcmVhbCBudW1iZXIgYXNzb2NpYXRlZCB3aXRoIG91ciBkYXRhLiBCeSB1c2luZyBgYmFzZTo6cm91bmQodmFyLCBkaWdpdHMgPSAwKWAsIHdlIHJvdW5kIHRvIHRoZSBuZWFyZXN0IGludGVnZXIuIGBkaWdpdHMgPSAyYCB3b3VsZCBnaXZlIHVzIHR3byBkZWNpbWFsIHBsYWNlcyBpbiBjb250cmFzdC4KCmBgYHtyIGxlYWZsZXQtbmhvb2RzM30KIyBjcmVhdGUgY29sb3IgcGFsZXR0ZQpucGFsIDwtIGNvbG9yTnVtZXJpYygiWWxPclJkIiwgbmhvb2RzJHBvcDE3X2RlbikKCiMgY3JlYXRlIGxlYWZsZXQgb2JqZWN0Cm5ob29kcyAlPiUKICBsZWFmbGV0KCkgJT4lCiAgYWRkUHJvdmlkZXJUaWxlcyhwcm92aWRlcnMkQ2FydG9EQi5Qb3NpdHJvbikgJT4lCiAgYWRkUG9seWdvbnMoCiAgICBjb2xvciA9ICIjNDQ0NDQ0IiwgCiAgICB3ZWlnaHQgPSAxLCAKICAgIG9wYWNpdHkgPSAxLjAsIAogICAgc21vb3RoRmFjdG9yID0gMC41LAogICAgZmlsbE9wYWNpdHkgPSAwLjUsCiAgICBmaWxsQ29sb3IgPSB+bnBhbChwb3AxN19kZW4pLAogICAgaGlnaGxpZ2h0T3B0aW9ucyA9IGhpZ2hsaWdodE9wdGlvbnMoY29sb3IgPSAid2hpdGUiLCB3ZWlnaHQgPSAyLCBicmluZ1RvRnJvbnQgPSBUUlVFKSwKICAgIHBvcHVwID0gcGFzdGUoIjxiPk5hbWU6PC9iPiAiLCBuaG9vZHMkTkhEX05BTUUsICI8YnI+IiwKICAgICAgICAgICAgICAgICAgIjxiPjIwMTcgUG9wdWxhdGlvbjo8L2I+ICIsIHJvdW5kKG5ob29kcyRwb3AxNywgZGlnaXRzID0gMCksICI8YnI+IiwKICAgICAgICAgICAgICAgICAgIjxiPjIwMTcgUG9wdWxhdGlvbiBwZXIgU3F1YXJlIEtpbG9tZXRlcjo8L2I+ICIsIHJvdW5kKG5ob29kcyRwb3AxN19kZW4sIGRpZ2l0cyA9IDIpKSkgCmBgYAoKTmV4dCwgd2Ugc2hvdWxkIGFkZCBhIGxlZ2VuZCB0byBtYWtlIHRoZSBtYXAgZWFzaWVyIHRvIGludGVycHJldC4gVGhpcyBpcyBkb25lIHdpdGggdGhlIGBhZGRMZWdlbmQoKWAgYXJndW1lbnQuIFRoZSBgb3BhY2l0eWAgYXJndW1lbnQgaW4gYGFkZExlZ2VuZCgpYCBzaG91bGQgbWF0Y2ggdGhlIGBmaWxsT3BhY2l0eWAgYXJndW1lbnQgaW4gYGFkZFBvbHlnb25zKClgIQoKYGBge3IgbGVhZmxldC1uaG9vZHM0fQojIGNyZWF0ZSBjb2xvciBwYWxldHRlCm5wYWwgPC0gY29sb3JOdW1lcmljKCJZbE9yUmQiLCBuaG9vZHMkcG9wMTdfZGVuKQoKIyBjcmVhdGUgbGVhZmxldCBvYmplY3QKbmhvb2RzICU+JQogIGxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBhZGRQb2x5Z29ucygKICAgIGNvbG9yID0gIiM0NDQ0NDQiLCAKICAgIHdlaWdodCA9IDEsIAogICAgb3BhY2l0eSA9IDEuMCwgCiAgICBzbW9vdGhGYWN0b3IgPSAwLjUsCiAgICBmaWxsT3BhY2l0eSA9IDAuNSwKICAgIGZpbGxDb2xvciA9IH5ucGFsKHBvcDE3X2RlbiksCiAgICBoaWdobGlnaHRPcHRpb25zID0gaGlnaGxpZ2h0T3B0aW9ucyhjb2xvciA9ICJ3aGl0ZSIsIHdlaWdodCA9IDIsIGJyaW5nVG9Gcm9udCA9IFRSVUUpLAogICAgcG9wdXAgPSBwYXN0ZSgiPGI+TmFtZTo8L2I+ICIsIG5ob29kcyROSERfTkFNRSwgIjxicj4iLAogICAgICAgICAgICAgICAgICAiPGI+MjAxNyBQb3B1bGF0aW9uOjwvYj4gIiwgcm91bmQobmhvb2RzJHBvcDE3LCBkaWdpdHMgPSAwKSwgIjxicj4iLAogICAgICAgICAgICAgICAgICAiPGI+MjAxNyBQb3B1bGF0aW9uIHBlciBTcXVhcmUgS2lsb21ldGVyOjwvYj4gIiwgcm91bmQobmhvb2RzJHBvcDE3X2RlbiwgZGlnaXRzID0gMikpKSAlPiUKICAgIGFkZExlZ2VuZChwYWwgPSBucGFsLCB2YWx1ZXMgPSB+cG9wMTdfZGVuLCBvcGFjaXR5ID0gLjUsIHRpdGxlID0gIlBvcHVsYXRpb24gRGVuc2l0eSAoMjAxNykiKQpgYGAKCkFzIGEgcmV2aWV3LCB0aGUgY29sb3IgcGFsZXR0ZSB3ZSdyZSB1c2luZyBjb21lcyBmcm9tIHRoZSBgUkNvbG9yQnJld2VyYCBwYWNrYWdlLiBXZSBjYW4gdXNlIGBSQ29sb3JCcmV3ZXI6OmRpc3BsYXkuYnJld2VyLmFsbCgpYCB0byBpZGVudGlmeSBvdGhlciBjb2xvciByYW1wczoKCmBgYHtyIGRpc3BsYXktYnJld2VyfQpkaXNwbGF5LmJyZXdlci5hbGwodHlwZSA9ICJzZXEiKQpgYGAKCk9uZSBmaW5hbCBjb21wbGljYXRpb24gd2UnbGwgZGlzY3VzcyBpcyBhZGRpbmcgdGhlIGNpdHkncyBvdXRsaW5lIG9uIHRvcCBvZiB0aGUgYGFkZFBvbHlnb25zKClgIGZ1bmN0aW9uLiBXZSBjYW4ndCB1c2UgYSBzZWNvbmQgaW5zdGFuY2Ugb2YgYGFkZFBvbHlnb25zKClgIGluIG91ciBjYWxsLCBhbmQgc28gd2UnbGwgdXNlIHRoZSBgYWRkUG9seWxpbmVzKClgIGZ1bmN0aW9uIGluc3RlYWQuIFRoaXMgYWxsb3dzIHVzIHRvIGRpc3BsYXkgdGhlIGJvdW5kYXJ5IGFzIGEgbGluZSBpbnN0ZWFkIG9mIGEgcG9seWdvbiBmZWF0dXJlLiAKCmBgYHtyIGxlYWZsZXQtbmhvb2RzNX0KIyBjcmVhdGUgY29sb3IgcGFsZXR0ZQpucGFsIDwtIGNvbG9yTnVtZXJpYygiQmx1ZXMiLCBuaG9vZHMkcG9wMTdfZGVuKQoKIyBjcmVhdGUgbGVhZmxldCBvYmplY3QKbmhvb2RzICU+JQogIGxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBhZGRQb2x5Z29ucygKICAgIGNvbG9yID0gIiM0NDQ0NDQiLCAKICAgIHdlaWdodCA9IDEsIAogICAgb3BhY2l0eSA9IDEuMCwgCiAgICBzbW9vdGhGYWN0b3IgPSAwLjUsCiAgICBmaWxsT3BhY2l0eSA9IDAuNSwKICAgIGZpbGxDb2xvciA9IH5ucGFsKHBvcDE3X2RlbiksCiAgICBoaWdobGlnaHRPcHRpb25zID0gaGlnaGxpZ2h0T3B0aW9ucyhjb2xvciA9ICJ3aGl0ZSIsIHdlaWdodCA9IDIsIGJyaW5nVG9Gcm9udCA9IFRSVUUpLAogICAgcG9wdXAgPSBwYXN0ZSgiPGI+TmFtZTo8L2I+ICIsIG5ob29kcyROSERfTkFNRSwgIjxicj4iLAogICAgICAgICAgICAgICAgICAiPGI+MjAxNyBQb3B1bGF0aW9uOjwvYj4gIiwgcm91bmQobmhvb2RzJHBvcDE3LCBkaWdpdHMgPSAwKSwgIjxicj4iLAogICAgICAgICAgICAgICAgICAiPGI+MjAxNyBQb3B1bGF0aW9uIHBlciBTcXVhcmUgS2lsb21ldGVyOjwvYj4gIiwgcm91bmQobmhvb2RzJHBvcDE3X2RlbiwgZGlnaXRzID0gMikpKSAlPiUKICAgIGFkZFBvbHlsaW5lcygKICAgICAgZGF0YSA9IGNpdHksCiAgICAgIGNvbG9yID0gIiMwMDAwMDAiLAogICAgICB3ZWlnaHQgPSAzCiAgICApICU+JQogICAgYWRkTGVnZW5kKHBhbCA9IG5wYWwsIHZhbHVlcyA9IH5wb3AxN19kZW4sIG9wYWNpdHkgPSAuNSwgdGl0bGUgPSAiUG9wdWxhdGlvbiBEZW5zaXR5ICgyMDE3KSIpCmBgYAoKRm9yIG91ciBmaW5hbCBgbGVhZmxldGAgbWFwLCB3cml0ZSB5b3VyIG93biBjb2RlIHRvIG1hcCB0aGUgMTk1MCBwb3B1bGF0aW9uIGRlbnNpdHkgb2YgbmVpZ2hib3Job29kczoKCmBgYHtyIGxlYWZsZXQtbmhvb2RzNn0KIyBjcmVhdGUgY29sb3IgcGFsZXR0ZQpucGFsIDwtIGNvbG9yTnVtZXJpYygiUmRQdSIsIG5ob29kcyRwb3A1MF9kZW4pCgojIGNyZWF0ZSBtYXAKbmhvb2RzICU+JQogIGxlYWZsZXQoKSAlPiUKICBhZGRQcm92aWRlclRpbGVzKHByb3ZpZGVycyRDYXJ0b0RCLlBvc2l0cm9uKSAlPiUKICBhZGRQb2x5Z29ucygKICAgIGNvbG9yID0gIiM0NDQ0NDQiLCAKICAgIHdlaWdodCA9IDEsIAogICAgc21vb3RoRmFjdG9yID0gMC41LAogICAgb3BhY2l0eSA9IDEuMCwgCiAgICBmaWxsT3BhY2l0eSA9IDAuNSwKICAgIGZpbGxDb2xvciA9IH5ucGFsKHBvcDUwX2RlbiksCiAgICBoaWdobGlnaHRPcHRpb25zID0gaGlnaGxpZ2h0T3B0aW9ucyhjb2xvciA9ICJ3aGl0ZSIsIHdlaWdodCA9IDIsIGJyaW5nVG9Gcm9udCA9IFRSVUUpLAogICAgcG9wdXAgPSBwYXN0ZSgiPGI+TmFtZTo8L2I+ICIsIG5ob29kcyROSERfTkFNRSwgIjxicj4iLAogICAgICAgICAgICAgICAgICAiPGI+MTk1MCBQb3B1bGF0aW9uOjwvYj4gIiwgcm91bmQobmhvb2RzJHBvcDUwLCBkaWdpdHMgPSAwKSwgIjxicj4iLAogICAgICAgICAgICAgICAgICAiPGI+MTk1MCBQb3B1bGF0aW9uIHBlciBTcXVhcmUgS2lsb21ldGVyOjwvYj4gIiwgCiAgICAgICAgICAgICAgICAgICAgICByb3VuZChuaG9vZHMkcG9wNTBfZGVuLCBkaWdpdHMgPSAyKSkpICU+JQogICAgYWRkUG9seWxpbmVzKAogICAgICBkYXRhID0gY2l0eSwKICAgICAgY29sb3IgPSAiIzAwMDAwMCIsCiAgICAgIHdlaWdodCA9IDMKICAgICkgJT4lCiAgICBhZGRMZWdlbmQocGFsID0gbnBhbCwgdmFsdWVzID0gfnBvcDUwX2Rlbiwgb3BhY2l0eSA9IC41LCAKICAgICAgICAgICAgICB0aXRsZSA9ICJQb3B1bGF0aW9uIERlbnNpdHkgKDE5NTApIikKYGBgCgoKCmBgYHtyIG1vdmUtdG8tZG9jcywgaW5jbHVkZT1GQUxTRX0KIyB5b3UgZG8gbmVlZCB0byBpbmNsdWRlIHRoaXMgaW4gYW55IG5vdGVib29rIHlvdSBjcmVhdGUgZm9yIHRoaXMgY2xhc3MKZnM6OmZpbGVfY29weShoZXJlOjpoZXJlKCJleGFtcGxlcyIsICJtZWV0aW5nLWV4YW1wbGVzLm5iLmh0bWwiKSwgCiAgICAgICAgICAgICAgaGVyZTo6aGVyZSgiZG9jcyIsICJpbmRleC5uYi5odG1sIiksIAogICAgICAgICAgICAgIG92ZXJ3cml0ZSA9IFRSVUUpCmBgYAo=