In this lab, we are going to work with vector and raster data, spatially joining point data to vector data. We will learn how to visualize Census data, we will talk about how to wrangle spatial data, and we will get into some really cool ways to create choropleth maps (maps color coded by attributes). Finally, we will take a deep dive into raster data.

The objectives of this guide are to teach you how to:

  1. Wrangle vector data
  2. Create Publication-Ready Maps with vector data
  3. Introduce raster data
  4. Import our dataset with simulated geocoded addresses and mortality data from an ovarian cancer cohort
  5. Import a dataset with greenspace across the Bay Area
  6. Compare projections of datasets and re-project if needed
  7. Make maps
  8. Spatially join the two datasets
  9. Run a quick statistical analysis on the two datasets combined

Let’s get cracking!

First, let’s install our packages.


Load packages

library(sf)
library(MapGAM)
library(tidyverse)
library(tidycensus)
library(flextable)
library(RColorBrewer)
library(tmap)
library(terra)


In Lab 3, we worked with the tidycensus package and the Census API to bring in Census data into R. We can use the same commands to bring in Census geography. If you haven’t already, make sure to sign up for and install your Census API key. If you could not install your API key, you’ll need to use census_api_key() to activate it with the following code:

census_api_key("YOUR API KEY GOES HERE", install = TRUE)


Use the get_acs() command to bring in California tract-level race/ethnicity counts, total population, and total number of households. How did I find the variable IDs? Check Lab 3. Since we want tracts, we’ll use the geography = "tract" argument.

ca.tracts <- get_acs(geography = "tract", 
              year = 2023,
              variables = c(tpopr = "B03002_001", 
                            nhwhite = "B03002_003", nhblk = "B03002_004", 
                            nhasn = "B03002_006", hisp = "B03002_012"), 
              state = "CA",
              output = "wide",
              survey = "acs5",
              geometry = TRUE,
              cb = FALSE)
## Getting data from the 2019-2023 5-year ACS
## Downloading feature geometry from the Census website.  To cache shapefiles for use in future sessions, set `options(tigris_use_cache = TRUE)`.


The only difference between the code above and what we used in Lab 3 is we have one additional argument added to the get_acs() command: geometry = TRUE. This tells R to bring in the spatial features associated with the geography you specified in the command, in the above case California tracts. You can set cache_table = TRUE so that you don’t have to re-download after you’ve downloaded successfully the first time. This is important because you might be downloading a really large file, or may encounter Census FTP issues when trying to collect data.


Note: We can also download the data another way. We can go to the Census Shapefiles website and navigate to 2023, Census Tracts, then California. We can then download a .zip file that contains an ESRI shapefile of the Census tracts for California. When we unzip the file, we see a series of files. Thankfully, the sf package has an st_read() function that can tackle this! For more detailed data downloads, you can use National Historical Geographic Information System (NHGIS). The code below is example of how we might bring in a shapefile, just for future reference!

ca.tracts <- st_read("/Users/pjames1/Downloads/tl_2024_06_tract/tl_2024_06_tract.shp")

OK, let’s go back to the data we got from tidycensus. Lets take a look at our data.


ca.tracts
## Simple feature collection with 9129 features and 12 fields
## Geometry type: MULTIPOLYGON
## Dimension:     XY
## Bounding box:  xmin: -124.482 ymin: 32.52951 xmax: -114.1312 ymax: 42.0095
## Geodetic CRS:  NAD83
## First 10 features:
##          GEOID                                                 NAME tpoprE
## 1  06001442700        Census Tract 4427; Alameda County; California   2998
## 2  06001442800        Census Tract 4428; Alameda County; California   3087
## 3  06037204920 Census Tract 2049.20; Los Angeles County; California   2459
## 4  06037205110 Census Tract 2051.10; Los Angeles County; California   3690
## 5  06037320101 Census Tract 3201.01; Los Angeles County; California   3402
## 6  06037205120 Census Tract 2051.20; Los Angeles County; California   3409
## 7  06037206010 Census Tract 2060.10; Los Angeles County; California   3490
## 8  06037206020 Census Tract 2060.20; Los Angeles County; California   8179
## 9  06037206050 Census Tract 2060.50; Los Angeles County; California   2290
## 10 06037111402 Census Tract 1114.02; Los Angeles County; California   5998
##    tpoprM nhwhiteE nhwhiteM nhblkE nhblkM nhasnE nhasnM hispE hispM
## 1     475      921      168    119    115   1577    469   308    81
## 2     386      779      183     12     49   1671    356   561   181
## 3     387       68       33     49     51      0     14  2331   387
## 4     446       14       14      8     12      0     14  3642   446
## 5     438      236      131     65     93    123    164  2978   481
## 6     624       15       24      8     10     70    104  3303   628
## 7     605      500      264     17     75   1030    235  1919   519
## 8     350     1379      165   2633    344    238     76  3541   285
## 9     509      138       49     18     22    151     82  1932   519
## 10   1024     2685      831     18     19    920    563  2322   433
##                          geometry
## 1  MULTIPOLYGON (((-122.0172 3...
## 2  MULTIPOLYGON (((-122.0023 3...
## 3  MULTIPOLYGON (((-118.2028 3...
## 4  MULTIPOLYGON (((-118.2196 3...
## 5  MULTIPOLYGON (((-118.4388 3...
## 6  MULTIPOLYGON (((-118.2202 3...
## 7  MULTIPOLYGON (((-118.2392 3...
## 8  MULTIPOLYGON (((-118.2379 3...
## 9  MULTIPOLYGON (((-118.2287 3...
## 10 MULTIPOLYGON (((-118.5023 3...


The object looks much like a basic tibble, but with a few differences.

  • You’ll find that the description of the object now indicates that it is a simple feature collection with 9,129 features (tracts in California) with 12 fields (attributes or columns of data).
  • The Geometry Type indicates that the spatial data are in MULTIPOLYGON form (as opposed to points or lines, the other basic vector data forms).
  • Bounding box indicates the spatial extent of the features (from left to right, for example, California tracts go from a longitude of -124.482 to -114.1312).
  • Geodetic CRS tells us the coordinate reference system.
  • The final difference is that the data frame contains the column geometry. This column (a list-column) contains the geometry for each observation. This looks familiar!

At its most basic, an sf object is a collection of simple features that includes attributes and geometries in the form of a data frame. In other words, it is a data frame (or tibble) with rows of features, columns of attributes, and a special column always named geometry that contains the spatial aspects of the features.

If you want to peek behind the curtain and learn more about the nitty gritty details about simple features, check out the official sf vignette.


Data Wrangling

There is a lot of stuff behind the curtain of how R handles spatial data as simple features, but the main takeaway is that sf objects are data frames. This means you can use many of the tidyverse functions we’ve learned in the past couple labs to manipulate sf objects, including the pipe %>% operator. For example, let’s break up the column NAME into separate tract, county and state variables using the separate() function

We do all of this in one line of continuous code using the pipe operator %>%

ca.tracts <- ca.tracts %>%
              separate(NAME, c("Tract", "County", "State"), sep = "; ")
glimpse(ca.tracts)
## Rows: 9,129
## Columns: 15
## $ GEOID    <chr> "06001442700", "06001442800", "06037204920", "06037205110", "…
## $ Tract    <chr> "Census Tract 4427", "Census Tract 4428", "Census Tract 2049.…
## $ County   <chr> "Alameda County", "Alameda County", "Los Angeles County", "Lo…
## $ State    <chr> "California", "California", "California", "California", "Cali…
## $ tpoprE   <dbl> 2998, 3087, 2459, 3690, 3402, 3409, 3490, 8179, 2290, 5998, 4…
## $ tpoprM   <dbl> 475, 386, 387, 446, 438, 624, 605, 350, 509, 1024, 578, 621, …
## $ nhwhiteE <dbl> 921, 779, 68, 14, 236, 15, 500, 1379, 138, 2685, 2306, 11, 81…
## $ nhwhiteM <dbl> 168, 183, 33, 14, 131, 24, 264, 165, 49, 831, 535, 27, 73, 18…
## $ nhblkE   <dbl> 119, 12, 49, 8, 65, 8, 17, 2633, 18, 18, 93, 26, 2, 352, 1086…
## $ nhblkM   <dbl> 115, 49, 51, 12, 93, 10, 75, 344, 22, 19, 97, 41, 4, 264, 679…
## $ nhasnE   <dbl> 1577, 1671, 0, 0, 123, 70, 1030, 238, 151, 920, 665, 13, 29, …
## $ nhasnM   <dbl> 469, 356, 14, 14, 164, 104, 235, 76, 82, 563, 176, 21, 45, 26…
## $ hispE    <dbl> 308, 561, 2331, 3642, 2978, 3303, 1919, 3541, 1932, 2322, 134…
## $ hispM    <dbl> 81, 181, 387, 446, 481, 628, 519, 285, 519, 433, 265, 618, 18…
## $ geometry <MULTIPOLYGON [°]> MULTIPOLYGON (((-122.0172 3..., MULTIPOLYGON (((…


Another important data wrangling operation is to join attribute data to an sf object. For example, let’s say you wanted to add tract level median household income, which we can get using get_acs(). We could have searched online for the variable name, but it’s B19013_001. Read the file in and save it as an object call ca.inc.

ca.inc <- get_acs(geography = "tract", 
              year = 2023,
              variables = c(medinc = "B19013_001"), 
              state = "CA",
              survey = "acs5",
              output = "wide")
## Getting data from the 2019-2023 5-year ACS


Unlike before, we brought these data in without the geometry = TRUE option. So this is just a table. But remember, an sf object is a data frame, so we can use left_join(), which we covered in Lab 2, to join the files ca.inc and ca.tracts.

ca.tracts <- ca.tracts %>%
  left_join(ca.inc, by = "GEOID")

#take a look to make sure the join worked
glimpse(ca.tracts)
## Rows: 9,129
## Columns: 18
## $ GEOID    <chr> "06001442700", "06001442800", "06037204920", "06037205110", "…
## $ Tract    <chr> "Census Tract 4427", "Census Tract 4428", "Census Tract 2049.…
## $ County   <chr> "Alameda County", "Alameda County", "Los Angeles County", "Lo…
## $ State    <chr> "California", "California", "California", "California", "Cali…
## $ tpoprE   <dbl> 2998, 3087, 2459, 3690, 3402, 3409, 3490, 8179, 2290, 5998, 4…
## $ tpoprM   <dbl> 475, 386, 387, 446, 438, 624, 605, 350, 509, 1024, 578, 621, …
## $ nhwhiteE <dbl> 921, 779, 68, 14, 236, 15, 500, 1379, 138, 2685, 2306, 11, 81…
## $ nhwhiteM <dbl> 168, 183, 33, 14, 131, 24, 264, 165, 49, 831, 535, 27, 73, 18…
## $ nhblkE   <dbl> 119, 12, 49, 8, 65, 8, 17, 2633, 18, 18, 93, 26, 2, 352, 1086…
## $ nhblkM   <dbl> 115, 49, 51, 12, 93, 10, 75, 344, 22, 19, 97, 41, 4, 264, 679…
## $ nhasnE   <dbl> 1577, 1671, 0, 0, 123, 70, 1030, 238, 151, 920, 665, 13, 29, …
## $ nhasnM   <dbl> 469, 356, 14, 14, 164, 104, 235, 76, 82, 563, 176, 21, 45, 26…
## $ hispE    <dbl> 308, 561, 2331, 3642, 2978, 3303, 1919, 3541, 1932, 2322, 134…
## $ hispM    <dbl> 81, 181, 387, 446, 481, 628, 519, 285, 519, 433, 265, 618, 18…
## $ NAME     <chr> "Census Tract 4427; Alameda County; California", "Census Trac…
## $ medincE  <dbl> 199154, 180800, 70500, 52262, 110967, 28516, 60703, 148661, 4…
## $ medincM  <dbl> 30525, 28293, 15698, 5659, 22149, 3246, 28578, 39223, 19295, …
## $ geometry <MULTIPOLYGON [°]> MULTIPOLYGON (((-122.0172 3..., MULTIPOLYGON (((…


Note that we can’t use left_join() to join the attribute tables of two sf files. You will need to either make one of them not spatial by using the st_drop_geometry() function or use the st_join() function to spatially join them.

We use the function tm_shape() from the tmap package to map the data.

tmap_mode("plot")
## ℹ tmap modes "plot" - "view"
## ℹ toggle with `tmap::ttm()`
tract_map <- tm_shape(ca.tracts) +   tm_polygons()
tract_map


Spatial Data Wrangling

There is Data Wrangling and then there is Spatial Data Wrangling. Cue dangerous sounding music. Well, it’s not that dangerous or scary. Spatial Data Wrangling involves cleaning or altering your data set based on the geographic location of features. The sf package offers a suite of functions unique to wrangling spatial data. Most of these functions start out with the prefix st_. To see all of the functions, type in

methods(class = "sf")


We won’t go through all of these functions as the list is quite extensive. But, we’ll go through a few relevant spatial operations for this class below. The function we will be primarily using is st_join().


Intersect

A common spatial data wrangling issue is to subset a set of spatial objects based on their location relative to another spatial object. In our case, we want to keep California tracts that are in the Sacramento metro area. We can do this using the st_join() function. We’ll need to specify a type of join. Let’s first try join = st_intersects. First, let’s bring in a polygon of the Sacramento metro area from Github.

url <- "https://github.com/pjames-ucdavis/SPH215/raw/main/sac.metro.rds"
download.file(url, destfile = "sac.metro.rds", mode = "wb")
sac.metro <- readRDS("sac.metro.rds")


Let’s take a look at sac.metro and understand what file it is.

glimpse(sac.metro)
## Rows: 1
## Columns: 14
## $ CSAFP    <chr> "472"
## $ CBSAFP   <chr> "40900"
## $ GEOID    <chr> "40900"
## $ GEOIDFQ  <chr> "310M700US40900"
## $ NAME     <chr> "Sacramento-Roseville-Folsom, CA"
## $ NAMELSAD <chr> "Sacramento-Roseville-Folsom, CA Metro Area"
## $ LSAD     <chr> "M1"
## $ MEMI     <chr> "1"
## $ MTFCC    <chr> "G3110"
## $ ALAND    <dbl> 13191309279
## $ AWATER   <dbl> 548018355
## $ INTPTLAT <chr> "+38.7902715"
## $ INTPTLON <chr> "-121.0056427"
## $ geometry <MULTIPOLYGON [°]> MULTIPOLYGON (((-120.0064 3...


OK, that geometry looks good. And it’s a polygon, so that’s good. Let’s now try to intersect our sac.metro dataset with our ca.tracts dataset.

sac.metro.tracts.int <- st_join(ca.tracts, sac.metro, 
                                join = st_intersects, left=FALSE)


The above code tells R to identify the polygons in ca.tracts that intersect with the polygon sac.metro. We indicate we want a polygon intersection by specifying join = st_intersects. The option left=FALSE tells R to remove the polygons from ca.tracts that do not intersect (make it TRUE and see what happens) with sac.metro. Plotting our tracts, we get:

tm_shape(sac.metro.tracts.int) +
  tm_polygons(col = "blue") +
tm_shape(sac.metro) +  
  tm_borders(col = "red")


Within

We have one small issue. Using join = st_intersects returns all tracts that intersect sac.metro, which include those that touch the metro’s boundary. No bueno. We can instead use the argument join = st_within to return tracts that are completely within the metro area.

sac.metro.tracts.w <- st_join(ca.tracts, sac.metro, join = st_within, left=FALSE)

tm_shape(sac.metro.tracts.w) +
  tm_polygons(col = "blue") +
tm_shape(sac.metro) +
  tm_borders(col = "red")


Looking much better! Now, if we look at sac.metro.tracts.w’s attribute table, you’ll see it includes all the variables from both ca.tracts and sac.metro. We don’t need these variables, so use select() to eliminate them. You’ll also notice that if variables from two data sets share the same name, R will keep both and attach a .x and .y to the end. For example, GEOID was found in both ca.tracts and sac.metro, so R named one GEOID.x and the other that was merged in was named GEOID.y.


Mapping in R

ggplot

OK, so now we’ve talked a little about how to bring in and manipulate vector polygon data, let’s do some mapping and create some choropleth maps. We can do this with the ggplot package, the tmap package, and the leaflet package (which we won’t cover now, but it’s very cool for interactive maps). Let’s start with ggplot.


ggplot

Because sf is tidy friendly, it is no surprise we can use the tidyverse plotting function ggplot() to make maps. We already received an introduction to ggplot() in Lab 3. Recall its basic structure:

ggplot(data = <DATA>) +
      <GEOM_FUNCTION>(mapping = aes(x, y)) +
      <OPTIONS>()


In mapping, geom_sf() is <GEOM_FUNCTION>(). Unlike with functions like geom_histogram() and geom_boxplot(), we don’t specify an x and y axis. Instead you use fill if you want to map a variable or color to just map boundaries.

Let’s use ggplot() to make a choropleth map. We need to specify a numeric variable in the fill = argument within geom_sf(). Here we map tract-level median household income in the Sacramento metro area.

ggplot(data = sac.metro.tracts.w) +
  geom_sf(aes(fill = medincE))


We can also specify a title (as well as subtitles and captions) using the labs() function.

ggplot(data = sac.metro.tracts.w) +
  geom_sf(aes(fill = medincE)) +
    labs(title = "Median Income Sacramento MSA Tracts") 


We can make further layout adjustments to the map. Don’t like a blue scale on the legend? You can change it using the scale_file_gradient() function. Let’s change it to a white to red gradient. We can also eliminate the gray tract border colors to make the fill color distinction clearer. We do this by specifying color = NA inside geom_sf(). We can also get rid of the gray background by specifying a basic black and white theme using theme_bw(). We also added a caption indicating the source of the data using the captions = parameter within labs(). We then changed the color to red using labels for low= and high=, and we added a name to our legend with `name=’.

ggplot(data = sac.metro.tracts.w) +
  geom_sf(aes(fill = medincE), color = NA) +
    scale_fill_gradient(low= "white", high = "red", na.value ="gray", name = "Median Income") +  
    labs(title = "Median Income Sacramento MSA Tracts",
         caption = "Source: American Community Survey") +  
  theme_bw()


Dare I say, we are ready for the New York Times with this map!


Points on top of polygons

OK, now that we have mapped points and mapped polygons, let’s put them both together! First, we are going to bring in our old friend CAdata from last week’s lab.

data(CAdata)
ca_pts <- CAdata
summary(ca_pts)
##       time               event              X                 Y          
##  Min.   : 0.004068   Min.   :0.0000   Min.   :1811375   Min.   :-241999  
##  1st Qu.: 1.931247   1st Qu.:0.0000   1st Qu.:2018363   1st Qu.: -94700  
##  Median : 4.749980   Median :1.0000   Median :2325084   Median : -60386  
##  Mean   : 6.496130   Mean   :0.6062   Mean   :2230219   Mean   :  87591  
##  3rd Qu.: 9.609031   3rd Qu.:1.0000   3rd Qu.:2380230   3rd Qu.: 318280  
##  Max.   :24.997764   Max.   :1.0000   Max.   :2705633   Max.   : 770658  
##       AGE         INS      
##  Min.   :25.00   Mcd: 431  
##  1st Qu.:53.00   Mcr:1419  
##  Median :62.00   Mng:2304  
##  Mean   :61.28   Oth: 526  
##  3rd Qu.:71.00   Uni: 168  
##  Max.   :80.00   Unk: 152
ca_pts <- st_as_sf(CAdata, coords=c("X","Y"))
ca_proj <- "+proj=lcc +lat_1=40 +lat_2=41.66666666666666 
             +lat_0=39.33333333333334 +lon_0=-122 +x_0=2000000 
             +y_0=500000.0000000002 +ellps=GRS80 
             +datum=NAD83 +units=m +no_defs"

#Set CRS
ca_pts_crs <- st_set_crs(ca_pts, ca_proj)


This time, we will map those points with ggplot.

ggplot(data = ca_pts_crs) +
  geom_sf(fill = "black") +
  labs(title = "Study Participants",
       caption = "Source: Ovarian Cancer Cases") +  
  theme_bw()


We can overlay the points over Sacramento tracts to give the locations some perspective. Here, you add two geom_sf() arguments for the tracts and the cancer cases.

ggplot() +
  geom_sf(data = sac.metro.tracts.w) +
  geom_sf(data = ca_pts_crs, fill = "black") +
  labs(title = "Study Participants",
       caption = "Source: Ovarian Cancer Cases") +  
  theme_bw()


Hmmm. That doesn’t look great. We have lots of cases outside of Sacramento. Let’s filter out to just pick cases within the Sacramento area.

ca_pts_crs.w <- st_join(ca_pts_crs, sac.metro.tracts.w, join = st_within, left=FALSE)


Ooof. That doesn’t work. It says our st_crs(x) == st_crs(y) is not TRUE. That means our Coordinate Reference Systems are not matching! Let’s transform our cancer dataset ca_pts_crs.w to match the CRS for sac.metro.tracts.w with one easy step using st_transform:

#check crs of each dataset
st_crs(ca_pts_crs)
## Coordinate Reference System:
##   User input: +proj=lcc +lat_1=40 +lat_2=41.66666666666666 
##              +lat_0=39.33333333333334 +lon_0=-122 +x_0=2000000 
##              +y_0=500000.0000000002 +ellps=GRS80 
##              +datum=NAD83 +units=m +no_defs 
##   wkt:
## PROJCRS["unknown",
##     BASEGEOGCRS["unknown",
##         DATUM["North American Datum 1983",
##             ELLIPSOID["GRS 1980",6378137,298.257222101,
##                 LENGTHUNIT["metre",1]],
##             ID["EPSG",6269]],
##         PRIMEM["Greenwich",0,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8901]]],
##     CONVERSION["unknown",
##         METHOD["Lambert Conic Conformal (2SP)",
##             ID["EPSG",9802]],
##         PARAMETER["Latitude of false origin",39.3333333333333,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8821]],
##         PARAMETER["Longitude of false origin",-122,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8822]],
##         PARAMETER["Latitude of 1st standard parallel",40,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8823]],
##         PARAMETER["Latitude of 2nd standard parallel",41.6666666666667,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8824]],
##         PARAMETER["Easting at false origin",2000000,
##             LENGTHUNIT["metre",1],
##             ID["EPSG",8826]],
##         PARAMETER["Northing at false origin",500000,
##             LENGTHUNIT["metre",1],
##             ID["EPSG",8827]]],
##     CS[Cartesian,2],
##         AXIS["(E)",east,
##             ORDER[1],
##             LENGTHUNIT["metre",1,
##                 ID["EPSG",9001]]],
##         AXIS["(N)",north,
##             ORDER[2],
##             LENGTHUNIT["metre",1,
##                 ID["EPSG",9001]]]]
st_crs(sac.metro.tracts.w)
## Coordinate Reference System:
##   User input: NAD83 
##   wkt:
## GEOGCRS["NAD83",
##     DATUM["North American Datum 1983",
##         ELLIPSOID["GRS 1980",6378137,298.257222101,
##             LENGTHUNIT["metre",1]]],
##     PRIMEM["Greenwich",0,
##         ANGLEUNIT["degree",0.0174532925199433]],
##     CS[ellipsoidal,2],
##         AXIS["latitude",north,
##             ORDER[1],
##             ANGLEUNIT["degree",0.0174532925199433]],
##         AXIS["longitude",east,
##             ORDER[2],
##             ANGLEUNIT["degree",0.0174532925199433]],
##     ID["EPSG",4269]]
#create new dataset with transformed CRS
ca_pts_crs.transformed <- st_transform(ca_pts_crs,st_crs(sac.metro.tracts.w))

st_crs(ca_pts_crs.transformed )
## Coordinate Reference System:
##   User input: NAD83 
##   wkt:
## GEOGCRS["NAD83",
##     DATUM["North American Datum 1983",
##         ELLIPSOID["GRS 1980",6378137,298.257222101,
##             LENGTHUNIT["metre",1]]],
##     PRIMEM["Greenwich",0,
##         ANGLEUNIT["degree",0.0174532925199433]],
##     CS[ellipsoidal,2],
##         AXIS["latitude",north,
##             ORDER[1],
##             ANGLEUNIT["degree",0.0174532925199433]],
##         AXIS["longitude",east,
##             ORDER[2],
##             ANGLEUNIT["degree",0.0174532925199433]],
##     ID["EPSG",4269]]


OK, let’s try this again!

ca_pts_crs.w <- st_join(ca_pts_crs.transformed, sac.metro.tracts.w, join = st_within, left=FALSE)

It worked! OK, now let’s try our map again.

ggplot() +
  geom_sf(data = sac.metro.tracts.w) +
  geom_sf(data = ca_pts_crs.w, fill = "black") +
  labs(title = "Study Participants",
       caption = "Source: Ovarian Cancer Cases") +  
  theme_bw()


Alright, who’s ready for a challenge? Let’s put it all together in one nice map.

ggplot() + 
  geom_sf(data = sac.metro.tracts.w, aes(fill = medincE), color = NA) +
    scale_fill_gradient(low= "white", high = "red", na.value ="gray", name = "Median Income") +  
  geom_sf(data = ca_pts_crs.w, fill = "black") +
  labs(title = "Study Participants Overlaid with Median Income of Sacramento Tracts",
       caption = "Source: American Community Survey and Ovarian Cancer Cases") +
  theme_bw()


Can I just say, you’re very impressive. Well done!


tmap

Whether you prefer tmap or ggplot is up to you, but I find that tmap has some benefits, so let’s focus on its mapping functions next.

tmap uses the same layered logic as ggplot. As we saw last week, the initial command is tm_shape(), which specifies the geography to which the mapping is applied. You then build on tm_shape() by adding one or more elements such as tm_polygons() for polygons, tm_borders() for lines, and tm_dots() for points. All additional functions take on the form of tm_. Check the full list of tm_ elements here.

Choropleth maps in tmap

Let’s make a static choropleth map of median household income in Sacramento MSA just like we did above, but this time in tmap.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile"))


We first put the dataset sac.metro.tracts.w inside tm_shape(). Because you are plotting polygons, you use tm_polygons() next. The argument fill = "medincE" tells R to shade (or color) the tracts by the variable medincE. The argument fill.scale = tm_scale_intervals(style = "quantile")" tells R to break up the shading into quantiles, or equal groups of 5 as a default. I find that this is where tmap offers a distinct advantage over ggplot in that users have greater control over the legend and bin breaks. tmap allows users to specify algorithms to automatically create breaks with the style argument. You can also change the number of breaks by setting n=. The default is n=5. Rather than quintiles, you can show quartiles using n=4. I’m feeling crazy. Let’s do it.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile", n=4))


Check out this link for more on available classification styles in tmap.


The tm_polygons() command is a wrapper around two other functions, tm_fill() and tm_borders(). tm_fill() controls the contents of the polygons (color, classification, etc.), while tm_borders() does the same for the polygon outlines.

For example, using the same shape (but no variable), we obtain the outlines of the neighborhoods from the tm_borders() command.

tm_shape(sac.metro.tracts.w) +
  tm_borders()


Similarly, we obtain a choropleth map without the polygon outlines when we just use the tm_fill() command.

tm_shape(sac.metro.tracts.w) + 
  tm_fill("medincE")

When we combine the two commands, we obtain the same map as with tm_polygons() (this illustrates how in R one can often obtain the same result in a number of different ways). Try this on your own.


Color scheme

The argument values = defines the color ranges associated with the bins and determined by the style arguments. Several built-in palettes are contained in tmap. For example, using values = "Reds" would yield the following map for our example.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile", 
                                              values = "Reds")) 

Under the hood, “Reds” refers to one of the color schemes supported by the RColorBrewer package (see below).


In addition to the built-in palettes, customized color ranges can be created by specifying a vector with the desired colors as anchors. This will create a spectrum of colors in the map that range between the colors specified in the vector. For instance, if we used c(“red”, “blue”), the color spectrum would move from red to purple, then to blue, with in between shades. In our example:

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile", 
                                              values = c("red","blue"))) 


Not exactly a pretty picture. In order to capture a diverging scale, we insert “white” in between red and blue.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile", 
                                              values = c("red","white", "blue"))) 


A preferred approach to select a color palette is to chose one of the schemes contained in the RColorBrewer package. These are based on the research of cartographer Cynthia Brewer (see the colorbrewer2 website for details). ColorBrewer makes a distinction between sequential scales (for a scale that goes from low to high), diverging scales (to highlight how values differ from a central tendency), and qualitative scales (for categorical variables). For each scale, a series of single hue and multi-hue scales are suggested. In the RColorBrewer package, these are referred to by a name (e.g., the “Reds” palette we used above is an example). The full list is contained in the RColorBrewer documentation.

There are two very useful commands in this package. One sets a color palette by specifying its name and the number of desired categories. The result is a character vector with the hex codes of the corresponding colors.

For example, we select a sequential color scheme going from blue to green, as BuGn, by means of the command brewer.pal, with the number of categories (6) and the scheme as arguments. The resulting vector contains the HEX codes for the colors.

brewer.pal(6,"BuGn")
## [1] "#EDF8FB" "#CCECE6" "#99D8C9" "#66C2A4" "#2CA25F" "#006D2C"


Using this palette in our map yields the following result.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values="brewer.bu_gn")) 


The command display.brewer.pal() allows us to explore different color schemes before applying them to a map. For example:

display.brewer.pal(6,"BuGn")


Legend

There are many options to change the formatting of the legend. The automatic title for the legend is not that attractive, since it is simply the variable name. This can be customized by setting the title argument in tm_legend().

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"), 
              fill.legend = tm_legend(title = "Median Income")) 


Another important aspect of the legend is its positioning. This is handled through the tm_layout() function. This function has a vast number of options, as detailed in the documentation. There are also specialized subsets of layout functions, focused on specific aspects of the map, such as tm_legend(), tm_style() and tm_format(). We illustrate the positioning of the legend.

Often, the default location of the legend is appropriate, but sometimes further control is needed. The legend.position argument to the tm_layout function moves the legend around the map, and it takes a vector of two string variables that determine both the horizontal position (“left”, “right”, or “center”) and the vertical position (“top”, “bottom”, or “center”).

For example, if we would want to move the legend to the bottom-right position, we would use the following set of commands.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) +
  tm_layout(legend.position = c("right", "bottom"))


There is also the option to position the legend outside the frame of the map. This is accomplished by setting legend.outside to TRUE, and optionally also specify its position by means of legend.outside.position(). The latter can take the values “top”, “bottom”, “right”, and “left”.

For example, to position the legend outside and on the right, would be accomplished by the following commands.

tm_shape(sac.metro.tracts.w) +
 tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) +
  tm_layout(legend.outside = TRUE, legend.outside.position = "right")


We can also customize the size of the legend, its alignment, font, etc. Check out the documentation for more!


Title

The tm_title() function can be used to set a title for the map, and specify its position, size, etc. For example, we can set the text, the size and the position as in the example below. We made the font size a bit smaller (0.8) in order not to overwhelm the map, and positioned it in the top left-hand corner.

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) + 
  tm_layout(legend.outside = TRUE, legend.outside.position = "right") +
  tm_title(text = "Median Income of Sacramento Tracts",
           size = 0.8, 
           position = c("left","top"))


To have a title appear on top (or on the bottom) of the map, we need to set the position argument of the tm_title() function with the associated tm_pos_out(), as illustrated below (with title.size set to 1.25 to have a larger font).

tm_shape(sac.metro.tracts.w) +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) +
  tm_layout(legend.outside = TRUE, legend.outside.position = "right") +
  tm_title(text = "Median Income of Sacramento Tracts",
           size = 1.25,
           position = tm_pos_out("center", "top"))

A shortcut to achieve this title position is to use tm_title_out() instead of tm_title().


Scale bar and arrow

OK this really wouldn’t be a GIS class without talking about one of the core elements of a map–the good ole scale bar and arrow. Let’s add these to our map. First, we add the scale bar with tm_scalebar().

The argument breaks tells R the distances to break up and end the bar. The argument position places the scale bar on the bottom left part of the map. Note that the scale is in miles (we’re in Amurica!). The default is in kilometers (the rest of the world!), but you can specify the units within tm_shape() using the argument unit. text.size scales the size of the bar smaller (below 1) or larger (above 1).

tm_shape(sac.metro.tracts.w, unit = "mi") +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) +
  tm_layout(legend.outside = TRUE, legend.outside.position = "right") +
  tm_title_out(text = "Median Income of Sacramento Tracts",
               size = 1.25) +
  tm_scalebar(breaks = c(0, 5, 10, 20), text.size = 0.75, position = c("left", "bottom")) 


Next let’s spice things up by adding a north arrow, which we can do using the function tm_compass(). You can control for the type, size and location of the arrow within this function. I place a 4-star arrow on the bottom right of the map.

tm_shape(sac.metro.tracts.w, unit = "mi") +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) +
  tm_layout(legend.outside = TRUE, legend.outside.position = "right") +
  tm_title_out(text = "Median Income of Sacramento Tracts",
               size = 1.25) +
  tm_scalebar(breaks = c(0, 5, 10, 20), text.size = 0.75, 
               position = c("left", "bottom")) +
  tm_compass(type = "4star", position = c("right", "bottom"))


We can also eliminate the frame around the map using the argument frame = FALSE in tm_layout().

sac.map <- tm_shape(sac.metro.tracts.w, unit = "mi") +
  tm_polygons(fill = "medincE", 
              fill.scale = tm_scale_intervals(style = "quantile",
                                              values = "brewer.reds"),
              fill.legend = tm_legend(title = "Median Income")) +
  tm_layout(frame = FALSE,
            legend.outside = TRUE, legend.outside.position = "right") +
  tm_title(text = "Median Income of Sacramento Tracts",
           size = 1.25) +
  tm_scalebar(breaks = c(0, 5, 10, 20), text.size = 0.75, position = c("left", "bottom")) +
  tm_compass(type = "4star", position = c("right", "bottom")) 
  
sac.map


Note that I saved the map into an object called sac.map. R is an object-oriented program, so everything you make in R are objects that can be saved for future manipulation. This includes maps. And future manipulations of a saved map includes adding more tm_* functions to the saved object, such as sac.map + tm_layout(your changes here). Check the help documentation for tm_layout() to see the complete list of settings.


Saving maps

You can save your maps a few ways. 1. On the plotting screen where the map is shown, click on Export and save it as either an image or pdf file. 2. Use the function tmap_save()

For option 2, we can save the map object sac.map as such:

tmap_save(sac.map, "sac_city_inc.jpg")

Specify the tmap object and a filename with an extension. It supports .pdf, .eps, .svg, .wmf, .png, .jpg, .bmp and .tiff. The default is .png. Also make sure you’ve set your working directory to the folder that you want your map to be saved in.


Making a map with CDC Places data

OK, do we have energy for one more example? Let’s bring in data from the CDC Places dataset. This is an incredible resource to access data on the CDC’s Behavioral Risk Factor and Surveillance System (BRFSS), as well as social determinants of health data from American Community Survey. I’ve already downloaded California Census tract data on the “% of adults reporting no leisure-time physical activity”. Let’s bring this in and take a look at it!

places_ca_lpa <- read_csv("https://raw.githubusercontent.com/pjames-ucdavis/SPH215/refs/heads/main/places_ca_lpa.csv")
## Rows: 9070 Columns: 24
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (16): StateAbbr, StateDesc, CountyName, CountyFIPS, LocationName, DataSo...
## dbl  (6): Year, Data_Value, Low_Confidence_Limit, High_Confidence_Limit, Tot...
## lgl  (2): Data_Value_Footnote_Symbol, Data_Value_Footnote
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
glimpse(places_ca_lpa)
## Rows: 9,070
## Columns: 24
## $ Year                       <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2…
## $ StateAbbr                  <chr> "CA", "CA", "CA", "CA", "CA", "CA", "CA", "…
## $ StateDesc                  <chr> "California", "California", "California", "…
## $ CountyName                 <chr> "Orange", "Riverside", "Riverside", "Sacram…
## $ CountyFIPS                 <chr> "06059", "06065", "06065", "06067", "06073"…
## $ LocationName               <chr> "06059063010", "06065042509", "06065042519"…
## $ DataSource                 <chr> "BRFSS", "BRFSS", "BRFSS", "BRFSS", "BRFSS"…
## $ Category                   <chr> "Health Risk Behaviors", "Health Risk Behav…
## $ Measure                    <chr> "No leisure-time physical activity among ad…
## $ Data_Value_Unit            <chr> "%", "%", "%", "%", "%", "%", "%", "%", "%"…
## $ Data_Value_Type            <chr> "Crude prevalence", "Crude prevalence", "Cr…
## $ Data_Value                 <dbl> 13.1, 33.6, 40.2, 26.6, 32.9, 12.9, 18.7, 2…
## $ Data_Value_Footnote_Symbol <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Data_Value_Footnote        <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Low_Confidence_Limit       <dbl> 11.3, 30.0, 36.3, 23.4, 29.6, 11.2, 16.4, 1…
## $ High_Confidence_Limit      <dbl> 15.0, 37.4, 44.2, 29.9, 36.3, 14.7, 21.1, 2…
## $ TotalPopulation            <dbl> 6698, 3460, 1887, 7420, 3601, 4831, 4778, 3…
## $ TotalPop18plus             <dbl> 5354, 2491, 1297, 5680, 2534, 4017, 3684, 2…
## $ Geolocation                <chr> "POINT (-117.8983938 33.6281163)", "POINT (…
## $ LocationID                 <chr> "06059063010", "06065042509", "06065042519"…
## $ CategoryID                 <chr> "RISKBEH", "RISKBEH", "RISKBEH", "RISKBEH",…
## $ MeasureId                  <chr> "LPA", "LPA", "LPA", "LPA", "LPA", "LPA", "…
## $ DataValueTypeID            <chr> "CrdPrv", "CrdPrv", "CrdPrv", "CrdPrv", "Cr…
## $ Short_Question_Text        <chr> "Physical Inactivity", "Physical Inactivity…


Interesting stuff. Looks like the values we care about are stores in a column called Data_Value and the FIPS code seems to be in LocationName. Let’s go ahead and rename LocationName and then see if we can join this data with our Census tract data ca.tracts.

places_ca_lpa<-rename(places_ca_lpa, GEOID = LocationName)
glimpse(places_ca_lpa)
## Rows: 9,070
## Columns: 24
## $ Year                       <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2…
## $ StateAbbr                  <chr> "CA", "CA", "CA", "CA", "CA", "CA", "CA", "…
## $ StateDesc                  <chr> "California", "California", "California", "…
## $ CountyName                 <chr> "Orange", "Riverside", "Riverside", "Sacram…
## $ CountyFIPS                 <chr> "06059", "06065", "06065", "06067", "06073"…
## $ GEOID                      <chr> "06059063010", "06065042509", "06065042519"…
## $ DataSource                 <chr> "BRFSS", "BRFSS", "BRFSS", "BRFSS", "BRFSS"…
## $ Category                   <chr> "Health Risk Behaviors", "Health Risk Behav…
## $ Measure                    <chr> "No leisure-time physical activity among ad…
## $ Data_Value_Unit            <chr> "%", "%", "%", "%", "%", "%", "%", "%", "%"…
## $ Data_Value_Type            <chr> "Crude prevalence", "Crude prevalence", "Cr…
## $ Data_Value                 <dbl> 13.1, 33.6, 40.2, 26.6, 32.9, 12.9, 18.7, 2…
## $ Data_Value_Footnote_Symbol <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Data_Value_Footnote        <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Low_Confidence_Limit       <dbl> 11.3, 30.0, 36.3, 23.4, 29.6, 11.2, 16.4, 1…
## $ High_Confidence_Limit      <dbl> 15.0, 37.4, 44.2, 29.9, 36.3, 14.7, 21.1, 2…
## $ TotalPopulation            <dbl> 6698, 3460, 1887, 7420, 3601, 4831, 4778, 3…
## $ TotalPop18plus             <dbl> 5354, 2491, 1297, 5680, 2534, 4017, 3684, 2…
## $ Geolocation                <chr> "POINT (-117.8983938 33.6281163)", "POINT (…
## $ LocationID                 <chr> "06059063010", "06065042509", "06065042519"…
## $ CategoryID                 <chr> "RISKBEH", "RISKBEH", "RISKBEH", "RISKBEH",…
## $ MeasureId                  <chr> "LPA", "LPA", "LPA", "LPA", "LPA", "LPA", "…
## $ DataValueTypeID            <chr> "CrdPrv", "CrdPrv", "CrdPrv", "CrdPrv", "Cr…
## $ Short_Question_Text        <chr> "Physical Inactivity", "Physical Inactivity…
ca.tracts.lpa <- ca.tracts %>%
  left_join(places_ca_lpa, by = "GEOID")
glimpse(ca.tracts.lpa)
## Rows: 9,129
## Columns: 41
## $ GEOID                      <chr> "06001442700", "06001442800", "06037204920"…
## $ Tract                      <chr> "Census Tract 4427", "Census Tract 4428", "…
## $ County                     <chr> "Alameda County", "Alameda County", "Los An…
## $ State                      <chr> "California", "California", "California", "…
## $ tpoprE                     <dbl> 2998, 3087, 2459, 3690, 3402, 3409, 3490, 8…
## $ tpoprM                     <dbl> 475, 386, 387, 446, 438, 624, 605, 350, 509…
## $ nhwhiteE                   <dbl> 921, 779, 68, 14, 236, 15, 500, 1379, 138, …
## $ nhwhiteM                   <dbl> 168, 183, 33, 14, 131, 24, 264, 165, 49, 83…
## $ nhblkE                     <dbl> 119, 12, 49, 8, 65, 8, 17, 2633, 18, 18, 93…
## $ nhblkM                     <dbl> 115, 49, 51, 12, 93, 10, 75, 344, 22, 19, 9…
## $ nhasnE                     <dbl> 1577, 1671, 0, 0, 123, 70, 1030, 238, 151, …
## $ nhasnM                     <dbl> 469, 356, 14, 14, 164, 104, 235, 76, 82, 56…
## $ hispE                      <dbl> 308, 561, 2331, 3642, 2978, 3303, 1919, 354…
## $ hispM                      <dbl> 81, 181, 387, 446, 481, 628, 519, 285, 519,…
## $ NAME                       <chr> "Census Tract 4427; Alameda County; Califor…
## $ medincE                    <dbl> 199154, 180800, 70500, 52262, 110967, 28516…
## $ medincM                    <dbl> 30525, 28293, 15698, 5659, 22149, 3246, 285…
## $ Year                       <dbl> 2022, 2022, 2022, 2022, 2022, 2022, 2022, 2…
## $ StateAbbr                  <chr> "CA", "CA", "CA", "CA", "CA", "CA", "CA", "…
## $ StateDesc                  <chr> "California", "California", "California", "…
## $ CountyName                 <chr> "Alameda", "Alameda", "Los Angeles", "Los A…
## $ CountyFIPS                 <chr> "06001", "06001", "06037", "06037", "06037"…
## $ DataSource                 <chr> "BRFSS", "BRFSS", "BRFSS", "BRFSS", "BRFSS"…
## $ Category                   <chr> "Health Risk Behaviors", "Health Risk Behav…
## $ Measure                    <chr> "No leisure-time physical activity among ad…
## $ Data_Value_Unit            <chr> "%", "%", "%", "%", "%", "%", "%", "%", "%"…
## $ Data_Value_Type            <chr> "Crude prevalence", "Crude prevalence", "Cr…
## $ Data_Value                 <dbl> 14.2, 18.8, 38.2, 38.7, 25.3, 40.2, 30.7, 2…
## $ Data_Value_Footnote_Symbol <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Data_Value_Footnote        <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA,…
## $ Low_Confidence_Limit       <dbl> 12.2, 16.3, 35.2, 35.7, 22.9, 37.3, 28.1, 2…
## $ High_Confidence_Limit      <dbl> 16.6, 21.6, 41.1, 41.7, 27.5, 43.1, 33.2, 2…
## $ TotalPopulation            <dbl> 3141, 2959, 2537, 3850, 3632, 3858, 3335, 5…
## $ TotalPop18plus             <dbl> 2432, 2359, 1903, 2599, 2822, 2783, 2695, 5…
## $ Geolocation                <chr> "POINT (-122.0081094 37.5371514)", "POINT (…
## $ LocationID                 <chr> "06001442700", "06001442800", "06037204920"…
## $ CategoryID                 <chr> "RISKBEH", "RISKBEH", "RISKBEH", "RISKBEH",…
## $ MeasureId                  <chr> "LPA", "LPA", "LPA", "LPA", "LPA", "LPA", "…
## $ DataValueTypeID            <chr> "CrdPrv", "CrdPrv", "CrdPrv", "CrdPrv", "Cr…
## $ Short_Question_Text        <chr> "Physical Inactivity", "Physical Inactivity…
## $ geometry                   <MULTIPOLYGON [°]> MULTIPOLYGON (((-122.0172 3...…


It worked! OK, now let’s make a map of % of adults reporting no leisure-time physical activity. We will build on what we’ve learned above! I’ve also used tm_fill() instead of tm_polygons() to make it so there are no borders and we can easily see polygon values in denser concentrations of Census tracts (e.g., around Sacramento, San Francisco, LA, etc.)

tm_shape(ca.tracts.lpa, unit = "mi") +
  tm_fill(fill = "Data_Value", 
          fill.scale = tm_scale_intervals(style = "quantile",
                                          values = "brewer.reds"),
          fill.legend = tm_legend(title = "% Adults No Physical Activity")) +
  tm_title_out(text = "% of Adults Reporting No Leisure Time Physical Activity")
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.


Oooooooo that is one goooood looking map!


You’ve completed your introduction to sf. Whew! Badge? Yes, please, you earned it! Time to celebrate!


sf Badge
sf Badge


Rasters

Raster datasets are simply an array of pixels/cells organized into rows and columns (or a grid) where each cell contains a value representing information, such as temperature, vegetation, land use, air pollution, etc. Raster maps usually represent continuous phenomena such as elevation, temperature, or population density. Discrete features such as soil type or land-cover classes can also be represented in the raster data model. Rasters are aerial photographs, imagery from satellites, Google Street View images, etc. A few things to note.

  • Raster datasets are always rectangular (rows x col) similar to matrices. Irregular boundaries are created by using NAs.
  • Rasters have to contain values of the same type (int, float, boolean) throughout the raster, just like matrices and unlike data frames.
  • The size of the raster depends on the resolution and the extent of the raster. As such many rasters are large and often cannot be held in memory completely.

The workhorse package for working with rasters in R is the terra package by Robert Hijmans. terra has functions for creating, reading, manipulating, and writing raster data. The package also implements raster algebra and many other functions for raster data manipulation. The package works with SpatRaster objects. The rast() function is used to create these objects.

Typically you will bring in a raster dataset directly from a file. These files come in many different forms, typically .tif, .img, and .grd.


We’ll bring in the file NDVI_rast.tif. The file contains normalized difference vegetation index (NDVI) data for the Bay Area. These data are taken from Landsat satellite data that I downloaded from Google Earth Engine. We use the function rast() to bring in data in raster form, then take a look at the dataset.

url <- "https://github.com/pjames-ucdavis/SPH215/raw/main/NDVI_rast2.tif"
download.file(url, destfile = "NDVI_rast2.tif", mode = "wb")
NDVI_raster = rast("NDVI_rast2.tif")

## Get summary of raster data
NDVI_raster
## class       : SpatRaster 
## size        : 1856, 1485, 1  (nrow, ncol, nlyr)
## resolution  : 0.0002694946, 0.0002694946  (x, y)
## extent      : -122.6001, -122.1999, 37.59988, 38.10007  (xmin, xmax, ymin, ymax)
## coord. ref. : lon/lat WGS 84 (EPSG:4326) 
## source      : NDVI_rast2.tif 
## name        : NDVI_BayArea 
## min value   :   -1.0000000 
## max value   :    0.8533574


Does it have a CRS?

## Check CRS
st_crs(NDVI_raster)
## Coordinate Reference System:
##   User input: WGS 84 
##   wkt:
## GEOGCRS["WGS 84",
##     ENSEMBLE["World Geodetic System 1984 ensemble",
##         MEMBER["World Geodetic System 1984 (Transit)"],
##         MEMBER["World Geodetic System 1984 (G730)"],
##         MEMBER["World Geodetic System 1984 (G873)"],
##         MEMBER["World Geodetic System 1984 (G1150)"],
##         MEMBER["World Geodetic System 1984 (G1674)"],
##         MEMBER["World Geodetic System 1984 (G1762)"],
##         MEMBER["World Geodetic System 1984 (G2139)"],
##         MEMBER["World Geodetic System 1984 (G2296)"],
##         ELLIPSOID["WGS 84",6378137,298.257223563,
##             LENGTHUNIT["metre",1]],
##         ENSEMBLEACCURACY[2.0]],
##     PRIMEM["Greenwich",0,
##         ANGLEUNIT["degree",0.0174532925199433]],
##     CS[ellipsoidal,2],
##         AXIS["geodetic latitude (Lat)",north,
##             ORDER[1],
##             ANGLEUNIT["degree",0.0174532925199433]],
##         AXIS["geodetic longitude (Lon)",east,
##             ORDER[2],
##             ANGLEUNIT["degree",0.0174532925199433]],
##     USAGE[
##         SCOPE["Horizontal component of 3D system."],
##         AREA["World."],
##         BBOX[-90,-180,90,180]],
##     ID["EPSG",4326]]


OK we have what looks like a raster. We see our resolution and our extent, and we have a CRS. Nice! Shall we plot this?

## Plot the raster on a map
tmap_mode("plot")
## ℹ tmap modes "plot" - "view"
NDVI_map <-  tm_shape(NDVI_raster) +
  tm_raster(col.scale = tm_scale_continuous(),
            col.legend = tm_legend(outside = TRUE))
NDVI_map
## Variable(s) "col" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full range of visual values.


Looks pretty cool! Seems to be the Bay Area, and we have some nice variability. But we let’s see if we can make this fancier.

# palette for plotting
breaks_ndvi <- c(-1,-0.2,-0.1,0,0.025 ,0.05,0.075,0.1,0.125,0.15,0.175,0.2 ,0.25 ,0.3 ,0.35,0.4,0.45,0.5,0.55,0.6,1)
palette_ndvi <- c("#BFBFBF","#DBDBDB","#FFFFE0","#FFFACC","#EDE8B5","#DED99C","#CCC782","#BDB86B","#B0C261","#A3CC59","#91BF52","#80B347","#70A340","#619636","#4F8A2E","#407D24","#306E1C","#216112","#0F540A","#004500")

NDVI_map <-  tm_shape(NDVI_raster) +
  tm_raster(col.scale = tm_scale_continuous(values = palette_ndvi),
            col.legend = tm_legend(title = "NDVI", outside = TRUE))
NDVI_map


Crop

OK, let’s see if we can crop this to focus on San Francisco. I’ve googled the lat and long for the area around San Francisco, and I’ll put these right into my crop() function. I can use the tmap_mode("view") now because the raster is small enough for R to make interactive.

sf_rast<-crop(NDVI_raster, ext(-122.55, -122.35, 37.7, 37.83))

tmap_mode("plot")
## ℹ tmap modes "plot" - "view"
NDVI_sf_map <- tm_shape(sf_rast) +
  tm_raster(col.scale = tm_scale_continuous(),
            col.legend = tm_legend(title = "NDVI", outside = TRUE))

NDVI_sf_map
## Variable(s) "col" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full range of visual values.


Classify

So negative values of NDVI represent water. Let’s set all negative values to -1, and that will help us to distinguish water from land easier.

sf_rast_neg <- app(sf_rast, fun=function(x){ x[x <= 0] <- -1; return(x)} )

NDVI_sf_map_neg <- tm_shape(sf_rast_neg) +
  tm_raster(col.scale = tm_scale_continuous(),
            col.legend = tm_legend(title = "NDVI", outside = TRUE))

NDVI_sf_map_neg
## Variable(s) "col" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full range of visual values.


Nice. What do we notice? The coast of San Francisco might have some cloud cover errors! Satellite data isn’t perfect! But the good news is, we’ve learned how to bring in raster data! I think we need a badge!!!!


terra Badge
terra Badge


One last step. Let’s put all our knowledge together and map our old friend CAdata on top of the NDVI data in San Francisco. First, let’s bring in the CAdata dataset on ovarian cancer cases again.

data(CAdata)
ca_pts <- CAdata
ca_proj <- "+proj=lcc +lat_1=40 +lat_2=41.66666666666666 
             +lat_0=39.33333333333334 +lon_0=-122 +x_0=2000000 
             +y_0=500000.0000000002 +ellps=GRS80 
             +datum=NAD83 +units=m +no_defs"

ca_pts <- st_as_sf(CAdata, coords=c("X","Y"), crs=ca_proj)


Let’s check the CRS and compare it to our NDVI dataset.

st_crs(ca_pts)
## Coordinate Reference System:
##   User input: +proj=lcc +lat_1=40 +lat_2=41.66666666666666 
##              +lat_0=39.33333333333334 +lon_0=-122 +x_0=2000000 
##              +y_0=500000.0000000002 +ellps=GRS80 
##              +datum=NAD83 +units=m +no_defs 
##   wkt:
## PROJCRS["unknown",
##     BASEGEOGCRS["unknown",
##         DATUM["North American Datum 1983",
##             ELLIPSOID["GRS 1980",6378137,298.257222101,
##                 LENGTHUNIT["metre",1]],
##             ID["EPSG",6269]],
##         PRIMEM["Greenwich",0,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8901]]],
##     CONVERSION["unknown",
##         METHOD["Lambert Conic Conformal (2SP)",
##             ID["EPSG",9802]],
##         PARAMETER["Latitude of false origin",39.3333333333333,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8821]],
##         PARAMETER["Longitude of false origin",-122,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8822]],
##         PARAMETER["Latitude of 1st standard parallel",40,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8823]],
##         PARAMETER["Latitude of 2nd standard parallel",41.6666666666667,
##             ANGLEUNIT["degree",0.0174532925199433],
##             ID["EPSG",8824]],
##         PARAMETER["Easting at false origin",2000000,
##             LENGTHUNIT["metre",1],
##             ID["EPSG",8826]],
##         PARAMETER["Northing at false origin",500000,
##             LENGTHUNIT["metre",1],
##             ID["EPSG",8827]]],
##     CS[Cartesian,2],
##         AXIS["(E)",east,
##             ORDER[1],
##             LENGTHUNIT["metre",1,
##                 ID["EPSG",9001]]],
##         AXIS["(N)",north,
##             ORDER[2],
##             LENGTHUNIT["metre",1,
##                 ID["EPSG",9001]]]]
st_crs(sf_rast_neg)
## Coordinate Reference System:
##   User input: WGS 84 
##   wkt:
## GEOGCRS["WGS 84",
##     ENSEMBLE["World Geodetic System 1984 ensemble",
##         MEMBER["World Geodetic System 1984 (Transit)"],
##         MEMBER["World Geodetic System 1984 (G730)"],
##         MEMBER["World Geodetic System 1984 (G873)"],
##         MEMBER["World Geodetic System 1984 (G1150)"],
##         MEMBER["World Geodetic System 1984 (G1674)"],
##         MEMBER["World Geodetic System 1984 (G1762)"],
##         MEMBER["World Geodetic System 1984 (G2139)"],
##         MEMBER["World Geodetic System 1984 (G2296)"],
##         ELLIPSOID["WGS 84",6378137,298.257223563,
##             LENGTHUNIT["metre",1]],
##         ENSEMBLEACCURACY[2.0]],
##     PRIMEM["Greenwich",0,
##         ANGLEUNIT["degree",0.0174532925199433]],
##     CS[ellipsoidal,2],
##         AXIS["geodetic latitude (Lat)",north,
##             ORDER[1],
##             ANGLEUNIT["degree",0.0174532925199433]],
##         AXIS["geodetic longitude (Lon)",east,
##             ORDER[2],
##             ANGLEUNIT["degree",0.0174532925199433]],
##     USAGE[
##         SCOPE["Horizontal component of 3D system."],
##         AREA["World."],
##         BBOX[-90,-180,90,180]],
##     ID["EPSG",4326]]


Hmmm, let’s make sure they are the same projection.

ca_pts_proj <- st_transform(ca_pts,st_crs(sf_rast_neg))
st_crs(ca_pts_proj)
## Coordinate Reference System:
##   User input: WGS 84 
##   wkt:
## GEOGCRS["WGS 84",
##     ENSEMBLE["World Geodetic System 1984 ensemble",
##         MEMBER["World Geodetic System 1984 (Transit)"],
##         MEMBER["World Geodetic System 1984 (G730)"],
##         MEMBER["World Geodetic System 1984 (G873)"],
##         MEMBER["World Geodetic System 1984 (G1150)"],
##         MEMBER["World Geodetic System 1984 (G1674)"],
##         MEMBER["World Geodetic System 1984 (G1762)"],
##         MEMBER["World Geodetic System 1984 (G2139)"],
##         MEMBER["World Geodetic System 1984 (G2296)"],
##         ELLIPSOID["WGS 84",6378137,298.257223563,
##             LENGTHUNIT["metre",1]],
##         ENSEMBLEACCURACY[2.0]],
##     PRIMEM["Greenwich",0,
##         ANGLEUNIT["degree",0.0174532925199433]],
##     CS[ellipsoidal,2],
##         AXIS["geodetic latitude (Lat)",north,
##             ORDER[1],
##             ANGLEUNIT["degree",0.0174532925199433]],
##         AXIS["geodetic longitude (Lon)",east,
##             ORDER[2],
##             ANGLEUNIT["degree",0.0174532925199433]],
##     USAGE[
##         SCOPE["Horizontal component of 3D system."],
##         AREA["World."],
##         BBOX[-90,-180,90,180]],
##     ID["EPSG",4326]]


They should be good to go now. Let’s map these addresses on top of the raster data!

tmap_mode("plot")
## ℹ tmap modes "plot" - "view"
NDVI_cancer_map <-  tm_shape(sf_rast_neg) +
  tm_raster(col.scale = tm_scale_continuous(),
            col.legend = tm_legend(title = "NDVI", outside = TRUE)) +
  tm_shape(ca_pts_proj) +
  tm_dots(fill = "blue", fill_alpha = 0.5, size = 0.3)

NDVI_cancer_map
## Variable(s) "col" contains positive and negative values, so midpoint is set to 0. Set midpoint = NA to show the full range of visual values.


That is one fine looking map. You deserve a break–get outside and enjoy some greenspace.

LS0tDQp0aXRsZTogJ0xhYiA0OiBNb3JlIHdpdGggVmVjdG9yIERhdGEgYW5kIFZpc3VhbGl6aW5nIFJhc3RlciBEYXRhJw0KLS0tDQoNCkluIHRoaXMgbGFiLCB3ZSBhcmUgZ29pbmcgdG8gd29yayB3aXRoIHZlY3RvciBhbmQgcmFzdGVyIGRhdGEsIHNwYXRpYWxseSBqb2luaW5nIHBvaW50IGRhdGEgdG8gdmVjdG9yIGRhdGEuIFdlIHdpbGwgbGVhcm4gaG93IHRvIHZpc3VhbGl6ZSBDZW5zdXMgZGF0YSwgd2Ugd2lsbCB0YWxrIGFib3V0IGhvdyB0byB3cmFuZ2xlIHNwYXRpYWwgZGF0YSwgYW5kIHdlIHdpbGwgZ2V0IGludG8gc29tZSByZWFsbHkgY29vbCB3YXlzIHRvIGNyZWF0ZSBjaG9yb3BsZXRoIG1hcHMgKG1hcHMgY29sb3IgY29kZWQgYnkgYXR0cmlidXRlcykuIEZpbmFsbHksIHdlIHdpbGwgdGFrZSBhIGRlZXAgZGl2ZSBpbnRvIHJhc3RlciBkYXRhLg0KDQpUaGUgb2JqZWN0aXZlcyBvZiB0aGlzIGd1aWRlIGFyZSB0byB0ZWFjaCB5b3UgaG93IHRvOg0KDQogICAxLiBXcmFuZ2xlIHZlY3RvciBkYXRhDQogICAxLiBDcmVhdGUgUHVibGljYXRpb24tUmVhZHkgTWFwcyB3aXRoIHZlY3RvciBkYXRhDQogICAxLiBJbnRyb2R1Y2UgcmFzdGVyIGRhdGEgDQogICAyLiBJbXBvcnQgb3VyIGRhdGFzZXQgd2l0aCBzaW11bGF0ZWQgZ2VvY29kZWQgYWRkcmVzc2VzIGFuZCBtb3J0YWxpdHkgZGF0YSBmcm9tIGFuIG92YXJpYW4gY2FuY2VyIGNvaG9ydA0KICAgMy4gSW1wb3J0IGEgZGF0YXNldCB3aXRoIGdyZWVuc3BhY2UgYWNyb3NzIHRoZSBCYXkgQXJlYQ0KICAgNC4gQ29tcGFyZSBwcm9qZWN0aW9ucyBvZiBkYXRhc2V0cyBhbmQgcmUtcHJvamVjdCBpZiBuZWVkZWQNCiAgIDUuIE1ha2UgbWFwcw0KICAgNi4gU3BhdGlhbGx5IGpvaW4gdGhlIHR3byBkYXRhc2V0cw0KICAgNy4gUnVuIGEgcXVpY2sgc3RhdGlzdGljYWwgYW5hbHlzaXMgb24gdGhlIHR3byBkYXRhc2V0cyBjb21iaW5lZA0KDQpMZXQncyBnZXQgY3JhY2tpbmchDQoNCkZpcnN0LCBsZXQncyBpbnN0YWxsIG91ciBwYWNrYWdlcy4NCg0KXA0KDQojIExvYWQgcGFja2FnZXMNCmBgYHtyLCBtZXNzYWdlID0gRkFMU0V9DQpsaWJyYXJ5KHNmKQ0KbGlicmFyeShNYXBHQU0pDQpsaWJyYXJ5KHRpZHl2ZXJzZSkNCmxpYnJhcnkodGlkeWNlbnN1cykNCmxpYnJhcnkoZmxleHRhYmxlKQ0KbGlicmFyeShSQ29sb3JCcmV3ZXIpDQpsaWJyYXJ5KHRtYXApDQpsaWJyYXJ5KHRlcnJhKQ0KYGBgDQoNClwNCg0KSW4gW0xhYiAzXShMYWIzXzIwMjYuaHRtbCksIHdlIHdvcmtlZCB3aXRoIHRoZSB0aWR5Y2Vuc3VzIHBhY2thZ2UgYW5kIHRoZSBDZW5zdXMgQVBJIHRvIGJyaW5nIGluIENlbnN1cyBkYXRhIGludG8gUi4gV2UgY2FuIHVzZSB0aGUgc2FtZSBjb21tYW5kcyB0byBicmluZyBpbiBDZW5zdXMgZ2VvZ3JhcGh5LiBJZiB5b3UgaGF2ZW7igJl0IGFscmVhZHksIG1ha2Ugc3VyZSB0byBbc2lnbiB1cCBmb3IgYW5kIGluc3RhbGwgeW91ciBDZW5zdXMgQVBJIGtleV0oaHR0cHM6Ly9hcGkuY2Vuc3VzLmdvdi9kYXRhL2tleV9zaWdudXAuaHRtbCkuIElmIHlvdSBjb3VsZCBub3QgaW5zdGFsbCB5b3VyIEFQSSBrZXksIHlvdeKAmWxsIG5lZWQgdG8gdXNlIGBjZW5zdXNfYXBpX2tleSgpYCB0byBhY3RpdmF0ZSBpdCB3aXRoIHRoZSBmb2xsb3dpbmcgY29kZToNCg0KYGBge3IsIGV2YWwgPSBGQUxTRX0NCmNlbnN1c19hcGlfa2V5KCJZT1VSIEFQSSBLRVkgR09FUyBIRVJFIiwgaW5zdGFsbCA9IFRSVUUpDQpgYGANCg0KYGBge3IsIGVjaG89RkFMU0UsIGV2YWw9RkFMU0V9DQpjZW5zdXNfYXBpX2tleSgiNWQ2ODkzNWM5NmMyNmVlNjdjYTUyZWI5NzNkNzFlNGE3Yjg0OTBhZCIsIGluc3RhbGwgPSBUUlVFLCBvdmVyd3JpdGU9VFJVRSkNCmBgYA0KXA0KDQpVc2UgdGhlIGBnZXRfYWNzKClgIGNvbW1hbmQgdG8gYnJpbmcgaW4gQ2FsaWZvcm5pYSB0cmFjdC1sZXZlbCByYWNlL2V0aG5pY2l0eSBjb3VudHMsIHRvdGFsIHBvcHVsYXRpb24sIGFuZCB0b3RhbCBudW1iZXIgb2YgaG91c2Vob2xkcy4gSG93IGRpZCBJIGZpbmQgdGhlIHZhcmlhYmxlIElEcz8gQ2hlY2sgW0xhYiAzXShMYWIzXzIwMjYuaHRtbCkuIFNpbmNlIHdlIHdhbnQgdHJhY3RzLCB3ZeKAmWxsIHVzZSB0aGUgYGdlb2dyYXBoeSA9ICJ0cmFjdCJgIGFyZ3VtZW50Lg0KYGBge3IsIHJlc3VsdHM9RkFMU0V9DQpjYS50cmFjdHMgPC0gZ2V0X2FjcyhnZW9ncmFwaHkgPSAidHJhY3QiLCANCiAgICAgICAgICAgICAgeWVhciA9IDIwMjMsDQogICAgICAgICAgICAgIHZhcmlhYmxlcyA9IGModHBvcHIgPSAiQjAzMDAyXzAwMSIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgIG5od2hpdGUgPSAiQjAzMDAyXzAwMyIsIG5oYmxrID0gIkIwMzAwMl8wMDQiLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICBuaGFzbiA9ICJCMDMwMDJfMDA2IiwgaGlzcCA9ICJCMDMwMDJfMDEyIiksIA0KICAgICAgICAgICAgICBzdGF0ZSA9ICJDQSIsDQogICAgICAgICAgICAgIG91dHB1dCA9ICJ3aWRlIiwNCiAgICAgICAgICAgICAgc3VydmV5ID0gImFjczUiLA0KICAgICAgICAgICAgICBnZW9tZXRyeSA9IFRSVUUsDQogICAgICAgICAgICAgIGNiID0gRkFMU0UpDQpgYGANCg0KXA0KDQpUaGUgb25seSBkaWZmZXJlbmNlIGJldHdlZW4gdGhlIGNvZGUgYWJvdmUgYW5kIHdoYXQgd2UgdXNlZCBpbiBbTGFiIDNdKExhYjNfMjAyNi5odG1sKSBpcyB3ZSBoYXZlIG9uZSBhZGRpdGlvbmFsIGFyZ3VtZW50IGFkZGVkIHRvIHRoZSBgZ2V0X2FjcygpYCBjb21tYW5kOiBgZ2VvbWV0cnkgPSBUUlVFYC4gVGhpcyB0ZWxscyBSIHRvIGJyaW5nIGluIHRoZSBzcGF0aWFsIGZlYXR1cmVzIGFzc29jaWF0ZWQgd2l0aCB0aGUgZ2VvZ3JhcGh5IHlvdSBzcGVjaWZpZWQgaW4gdGhlIGNvbW1hbmQsIGluIHRoZSBhYm92ZSBjYXNlIENhbGlmb3JuaWEgdHJhY3RzLiBZb3UgY2FuIHNldCBgY2FjaGVfdGFibGUgPSBUUlVFYCBzbyB0aGF0IHlvdSBkb27igJl0IGhhdmUgdG8gcmUtZG93bmxvYWQgYWZ0ZXIgeW914oCZdmUgZG93bmxvYWRlZCBzdWNjZXNzZnVsbHkgdGhlIGZpcnN0IHRpbWUuIFRoaXMgaXMgaW1wb3J0YW50IGJlY2F1c2UgeW91IG1pZ2h0IGJlIGRvd25sb2FkaW5nIGEgcmVhbGx5IGxhcmdlIGZpbGUsIG9yIG1heSBlbmNvdW50ZXIgQ2Vuc3VzIEZUUCBpc3N1ZXMgd2hlbiB0cnlpbmcgdG8gY29sbGVjdCBkYXRhLiANCg0KXA0KDQpOb3RlOiBXZSBjYW4gYWxzbyBkb3dubG9hZCB0aGUgZGF0YSBhbm90aGVyIHdheS4gV2UgY2FuIGdvIHRvIHRoZSBbQ2Vuc3VzIFNoYXBlZmlsZXMgd2Vic2l0ZV0oaHR0cHM6Ly93d3cuY2Vuc3VzLmdvdi9jZ2ktYmluL2dlby9zaGFwZWZpbGVzL2luZGV4LnBocCkgYW5kIG5hdmlnYXRlIHRvIDIwMjMsIENlbnN1cyBUcmFjdHMsIHRoZW4gQ2FsaWZvcm5pYS4gV2UgY2FuIHRoZW4gZG93bmxvYWQgYSAuemlwIGZpbGUgdGhhdCBjb250YWlucyBhbiBFU1JJIHNoYXBlZmlsZSBvZiB0aGUgQ2Vuc3VzIHRyYWN0cyBmb3IgQ2FsaWZvcm5pYS4gV2hlbiB3ZSB1bnppcCB0aGUgZmlsZSwgd2Ugc2VlIGEgc2VyaWVzIG9mIGZpbGVzLiBUaGFua2Z1bGx5LCB0aGUgKipzZioqIHBhY2thZ2UgaGFzIGFuIGBzdF9yZWFkKClgIGZ1bmN0aW9uIHRoYXQgY2FuIHRhY2tsZSB0aGlzISBGb3IgbW9yZSBkZXRhaWxlZCBkYXRhIGRvd25sb2FkcywgeW91IGNhbiB1c2UgW05hdGlvbmFsIEhpc3RvcmljYWwgR2VvZ3JhcGhpYyBJbmZvcm1hdGlvbiBTeXN0ZW0gKE5IR0lTKV0oaHR0cHM6Ly93d3cubmhnaXMub3JnLykuIFRoZSBjb2RlIGJlbG93IGlzIGV4YW1wbGUgb2YgaG93IHdlIG1pZ2h0IGJyaW5nIGluIGEgc2hhcGVmaWxlLCBqdXN0IGZvciBmdXR1cmUgcmVmZXJlbmNlIQ0KDQpgYGB7ciBldmFsPUZBTFNFfQ0KY2EudHJhY3RzIDwtIHN0X3JlYWQoIi9Vc2Vycy9wamFtZXMxL0Rvd25sb2Fkcy90bF8yMDI0XzA2X3RyYWN0L3RsXzIwMjRfMDZfdHJhY3Quc2hwIikNCmBgYA0KDQoNCk9LLCBsZXQncyBnbyBiYWNrIHRvIHRoZSBkYXRhIHdlIGdvdCBmcm9tICoqdGlkeWNlbnN1cyoqLiBMZXRzIHRha2UgYSBsb29rIGF0IG91ciBkYXRhLg0KDQpcDQoNCmBgYHtyfQ0KY2EudHJhY3RzDQpgYGANCg0KXA0KDQpUaGUgb2JqZWN0IGxvb2tzIG11Y2ggbGlrZSBhIGJhc2ljIHRpYmJsZSwgYnV0IHdpdGggYSBmZXcgZGlmZmVyZW5jZXMuDQoNCiAgLSBZb3XigJlsbCBmaW5kIHRoYXQgdGhlIGRlc2NyaXB0aW9uIG9mIHRoZSBvYmplY3Qgbm93IGluZGljYXRlcyB0aGF0IGl0IGlzIGEgc2ltcGxlIGZlYXR1cmUgY29sbGVjdGlvbiB3aXRoIDksMTI5IGZlYXR1cmVzICh0cmFjdHMgaW4gQ2FsaWZvcm5pYSkgd2l0aCAxMiBmaWVsZHMgKGF0dHJpYnV0ZXMgb3IgY29sdW1ucyBvZiBkYXRhKS4NCiAgLSBUaGUgYEdlb21ldHJ5IFR5cGVgIGluZGljYXRlcyB0aGF0IHRoZSBzcGF0aWFsIGRhdGEgYXJlIGluIGBNVUxUSVBPTFlHT05gIGZvcm0gKGFzIG9wcG9zZWQgdG8gcG9pbnRzIG9yIGxpbmVzLCB0aGUgb3RoZXIgYmFzaWMgdmVjdG9yIGRhdGEgZm9ybXMpLg0KICAtIGBCb3VuZGluZyBib3hgIGluZGljYXRlcyB0aGUgc3BhdGlhbCBleHRlbnQgb2YgdGhlIGZlYXR1cmVzIChmcm9tIGxlZnQgdG8gcmlnaHQsIGZvciBleGFtcGxlLCBDYWxpZm9ybmlhIHRyYWN0cyBnbyBmcm9tIGEgbG9uZ2l0dWRlIG9mIC0xMjQuNDgyIHRvIC0xMTQuMTMxMikuDQogIC0gYEdlb2RldGljIENSU2AgdGVsbHMgdXMgdGhlIGNvb3JkaW5hdGUgcmVmZXJlbmNlIHN5c3RlbS4NCiAgLSBUaGUgZmluYWwgZGlmZmVyZW5jZSBpcyB0aGF0IHRoZSBkYXRhIGZyYW1lIGNvbnRhaW5zIHRoZSBjb2x1bW4gZ2VvbWV0cnkuIFRoaXMgY29sdW1uIChhIGxpc3QtY29sdW1uKSBjb250YWlucyB0aGUgZ2VvbWV0cnkgZm9yIGVhY2ggb2JzZXJ2YXRpb24uIFRoaXMgbG9va3MgZmFtaWxpYXIhDQogIA0KQXQgaXRzIG1vc3QgYmFzaWMsIGFuICoqc2YqKiBvYmplY3QgaXMgYSBjb2xsZWN0aW9uIG9mIHNpbXBsZSBmZWF0dXJlcyB0aGF0IGluY2x1ZGVzIGF0dHJpYnV0ZXMgYW5kIGdlb21ldHJpZXMgaW4gdGhlIGZvcm0gb2YgYSBkYXRhIGZyYW1lLiBJbiBvdGhlciB3b3JkcywgaXQgaXMgYSBkYXRhIGZyYW1lIChvciB0aWJibGUpIHdpdGggcm93cyBvZiBmZWF0dXJlcywgY29sdW1ucyBvZiBhdHRyaWJ1dGVzLCBhbmQgYSBzcGVjaWFsIGNvbHVtbiBhbHdheXMgbmFtZWQgZ2VvbWV0cnkgdGhhdCBjb250YWlucyB0aGUgc3BhdGlhbCBhc3BlY3RzIG9mIHRoZSBmZWF0dXJlcy4NCg0KSWYgeW91IHdhbnQgdG8gcGVlayBiZWhpbmQgdGhlIGN1cnRhaW4gYW5kIGxlYXJuIG1vcmUgYWJvdXQgdGhlIG5pdHR5IGdyaXR0eSBkZXRhaWxzIGFib3V0IHNpbXBsZSBmZWF0dXJlcywgY2hlY2sgb3V0IHRoZSBvZmZpY2lhbCAqKnNmKiogW3ZpZ25ldHRlLl0oaHR0cHM6Ly9yLXNwYXRpYWwuZ2l0aHViLmlvL3NmL2FydGljbGVzL3NmMS5odG1sKQ0KDQpcDQoNCiMgRGF0YSBXcmFuZ2xpbmcNCg0KVGhlcmUgaXMgYSBsb3Qgb2Ygc3R1ZmYgW2JlaGluZCB0aGUgY3VydGFpbl0oaHR0cHM6Ly93d3cuamVzc2VzYWRsZXIuY29tL3Bvc3Qvc2ltcGxlLWZlYXR1cmUtb2JqZWN0cy8pIG9mIGhvdyBSIGhhbmRsZXMgc3BhdGlhbCBkYXRhIGFzIHNpbXBsZSBmZWF0dXJlcywgYnV0IHRoZSBtYWluIHRha2Vhd2F5IGlzIHRoYXQgKipzZioqIG9iamVjdHMgYXJlIGRhdGEgZnJhbWVzLiBUaGlzIG1lYW5zIHlvdSBjYW4gdXNlIG1hbnkgb2YgdGhlICoqdGlkeXZlcnNlKiogZnVuY3Rpb25zIHdl4oCZdmUgbGVhcm5lZCBpbiB0aGUgcGFzdCBjb3VwbGUgbGFicyB0byBtYW5pcHVsYXRlICoqc2YqKiBvYmplY3RzLCBpbmNsdWRpbmcgdGhlIHBpcGUgYCU+JWAgb3BlcmF0b3IuIEZvciBleGFtcGxlLCBsZXTigJlzIGJyZWFrIHVwIHRoZSBjb2x1bW4gKk5BTUUqIGludG8gc2VwYXJhdGUgdHJhY3QsIGNvdW50eSBhbmQgc3RhdGUgdmFyaWFibGVzIHVzaW5nIHRoZSBgc2VwYXJhdGUoKWAgZnVuY3Rpb24NCg0KV2UgZG8gYWxsIG9mIHRoaXMgaW4gb25lIGxpbmUgb2YgY29udGludW91cyBjb2RlIHVzaW5nIHRoZSBwaXBlIG9wZXJhdG9yIGAlPiVgDQoNCmBgYHtyfQ0KY2EudHJhY3RzIDwtIGNhLnRyYWN0cyAlPiUNCiAgICAgICAgICAgICAgc2VwYXJhdGUoTkFNRSwgYygiVHJhY3QiLCAiQ291bnR5IiwgIlN0YXRlIiksIHNlcCA9ICI7ICIpDQpnbGltcHNlKGNhLnRyYWN0cykNCmBgYA0KDQpcDQoNCkFub3RoZXIgaW1wb3J0YW50IGRhdGEgd3JhbmdsaW5nIG9wZXJhdGlvbiBpcyB0byBqb2luIGF0dHJpYnV0ZSBkYXRhIHRvIGFuIHNmIG9iamVjdC4gRm9yIGV4YW1wbGUsIGxldOKAmXMgc2F5IHlvdSB3YW50ZWQgdG8gYWRkIHRyYWN0IGxldmVsIG1lZGlhbiBob3VzZWhvbGQgaW5jb21lLCB3aGljaCB3ZSBjYW4gZ2V0IHVzaW5nIGBnZXRfYWNzKClgLiBXZSBjb3VsZCBoYXZlIHNlYXJjaGVkIG9ubGluZSBmb3IgdGhlIHZhcmlhYmxlIG5hbWUsIGJ1dCBpdCdzICpCMTkwMTNfMDAxKi4gUmVhZCB0aGUgZmlsZSBpbiBhbmQgc2F2ZSBpdCBhcyBhbiBvYmplY3QgY2FsbCAqY2EuaW5jKi4NCg0KYGBge3J9DQpjYS5pbmMgPC0gZ2V0X2FjcyhnZW9ncmFwaHkgPSAidHJhY3QiLCANCiAgICAgICAgICAgICAgeWVhciA9IDIwMjMsDQogICAgICAgICAgICAgIHZhcmlhYmxlcyA9IGMobWVkaW5jID0gIkIxOTAxM18wMDEiKSwgDQogICAgICAgICAgICAgIHN0YXRlID0gIkNBIiwNCiAgICAgICAgICAgICAgc3VydmV5ID0gImFjczUiLA0KICAgICAgICAgICAgICBvdXRwdXQgPSAid2lkZSIpDQpgYGANCg0KXA0KDQpVbmxpa2UgYmVmb3JlLCB3ZSBicm91Z2h0IHRoZXNlIGRhdGEgaW4gd2l0aG91dCB0aGUgYGdlb21ldHJ5ID0gVFJVRWAgb3B0aW9uLiBTbyB0aGlzIGlzIGp1c3QgYSB0YWJsZS4gQnV0IHJlbWVtYmVyLCBhbiAqKnNmKiogb2JqZWN0IGlzIGEgZGF0YSBmcmFtZSwgc28gd2UgY2FuIHVzZSBgbGVmdF9qb2luKClgLCB3aGljaCB3ZSBjb3ZlcmVkIGluIFtMYWIgMl0oTGFiMl8yMDI2Lmh0bWwpLCB0byBqb2luIHRoZSBmaWxlcyAqY2EuaW5jKiBhbmQgKmNhLnRyYWN0cyouDQoNCmBgYHtyfQ0KY2EudHJhY3RzIDwtIGNhLnRyYWN0cyAlPiUNCiAgbGVmdF9qb2luKGNhLmluYywgYnkgPSAiR0VPSUQiKQ0KDQojdGFrZSBhIGxvb2sgdG8gbWFrZSBzdXJlIHRoZSBqb2luIHdvcmtlZA0KZ2xpbXBzZShjYS50cmFjdHMpDQpgYGANCg0KXA0KDQpOb3RlIHRoYXQgd2UgY2Fu4oCZdCB1c2UgYGxlZnRfam9pbigpYCB0byBqb2luIHRoZSBhdHRyaWJ1dGUgdGFibGVzIG9mIHR3byAqKnNmKiogZmlsZXMuIFlvdSB3aWxsIG5lZWQgdG8gZWl0aGVyIG1ha2Ugb25lIG9mIHRoZW0gbm90IHNwYXRpYWwgYnkgdXNpbmcgdGhlIGBzdF9kcm9wX2dlb21ldHJ5KClgIGZ1bmN0aW9uIG9yIHVzZSB0aGUgYHN0X2pvaW4oKWAgZnVuY3Rpb24gdG8gc3BhdGlhbGx5IGpvaW4gdGhlbS4NCg0KV2UgdXNlIHRoZSBmdW5jdGlvbiBgdG1fc2hhcGUoKWAgZnJvbSB0aGUgKip0bWFwKiogcGFja2FnZSB0byBtYXAgdGhlIGRhdGEuIA0KDQpgYGB7cn0NCnRtYXBfbW9kZSgicGxvdCIpDQp0cmFjdF9tYXAgPC0gdG1fc2hhcGUoY2EudHJhY3RzKSArICAgdG1fcG9seWdvbnMoKQ0KdHJhY3RfbWFwDQpgYGANCg0KXA0KDQojIFNwYXRpYWwgRGF0YSBXcmFuZ2xpbmcNCg0KVGhlcmUgaXMgRGF0YSBXcmFuZ2xpbmcgYW5kIHRoZW4gdGhlcmUgaXMgU3BhdGlhbCBEYXRhIFdyYW5nbGluZy4gQ3VlIGRhbmdlcm91cyBzb3VuZGluZyBtdXNpYy4gV2VsbCwgaXTigJlzIG5vdCB0aGF0IGRhbmdlcm91cyBvciBzY2FyeS4gU3BhdGlhbCBEYXRhIFdyYW5nbGluZyBpbnZvbHZlcyBjbGVhbmluZyBvciBhbHRlcmluZyB5b3VyIGRhdGEgc2V0IGJhc2VkIG9uIHRoZSBnZW9ncmFwaGljIGxvY2F0aW9uIG9mIGZlYXR1cmVzLiBUaGUgKipzZioqIHBhY2thZ2Ugb2ZmZXJzIGEgc3VpdGUgb2YgZnVuY3Rpb25zIHVuaXF1ZSB0byB3cmFuZ2xpbmcgc3BhdGlhbCBkYXRhLiBNb3N0IG9mIHRoZXNlIGZ1bmN0aW9ucyBzdGFydCBvdXQgd2l0aCB0aGUgcHJlZml4IGBzdF9gLiBUbyBzZWUgYWxsIG9mIHRoZSBmdW5jdGlvbnMsIHR5cGUgaW4NCg0KYGBge3IsIGV2YWw9RkFMU0V9DQptZXRob2RzKGNsYXNzID0gInNmIikNCmBgYA0KDQpcDQoNCldlIHdvbuKAmXQgZ28gdGhyb3VnaCBhbGwgb2YgdGhlc2UgZnVuY3Rpb25zIGFzIHRoZSBsaXN0IGlzIHF1aXRlIGV4dGVuc2l2ZS4gQnV0LCB3ZeKAmWxsIGdvIHRocm91Z2ggYSBmZXcgcmVsZXZhbnQgc3BhdGlhbCBvcGVyYXRpb25zIGZvciB0aGlzIGNsYXNzIGJlbG93LiBUaGUgZnVuY3Rpb24gd2Ugd2lsbCBiZSBwcmltYXJpbHkgdXNpbmcgaXMgYHN0X2pvaW4oKWAuDQoNClwNCg0KIyMgSW50ZXJzZWN0DQoNCkEgY29tbW9uIHNwYXRpYWwgZGF0YSB3cmFuZ2xpbmcgaXNzdWUgaXMgdG8gc3Vic2V0IGEgc2V0IG9mIHNwYXRpYWwgb2JqZWN0cyBiYXNlZCBvbiB0aGVpciBsb2NhdGlvbiByZWxhdGl2ZSB0byBhbm90aGVyIHNwYXRpYWwgb2JqZWN0LiBJbiBvdXIgY2FzZSwgd2Ugd2FudCB0byBrZWVwIENhbGlmb3JuaWEgdHJhY3RzIHRoYXQgYXJlIGluIHRoZSBTYWNyYW1lbnRvIG1ldHJvIGFyZWEuIFdlIGNhbiBkbyB0aGlzIHVzaW5nIHRoZSBgc3Rfam9pbigpYCBmdW5jdGlvbi4gV2XigJlsbCBuZWVkIHRvIHNwZWNpZnkgYSB0eXBlIG9mIGpvaW4uIExldOKAmXMgZmlyc3QgdHJ5IGBqb2luID0gc3RfaW50ZXJzZWN0c2AuIEZpcnN0LCBsZXQncyBicmluZyBpbiBhIHBvbHlnb24gb2YgdGhlIFNhY3JhbWVudG8gbWV0cm8gYXJlYSBmcm9tIEdpdGh1Yi4NCg0KYGBge3J9DQp1cmwgPC0gImh0dHBzOi8vZ2l0aHViLmNvbS9wamFtZXMtdWNkYXZpcy9TUEgyMTUvcmF3L21haW4vc2FjLm1ldHJvLnJkcyINCmRvd25sb2FkLmZpbGUodXJsLCBkZXN0ZmlsZSA9ICJzYWMubWV0cm8ucmRzIiwgbW9kZSA9ICJ3YiIpDQpzYWMubWV0cm8gPC0gcmVhZFJEUygic2FjLm1ldHJvLnJkcyIpDQpgYGANCg0KXA0KDQpMZXQncyB0YWtlIGEgbG9vayBhdCAqc2FjLm1ldHJvKiBhbmQgdW5kZXJzdGFuZCB3aGF0IGZpbGUgaXQgaXMuDQoNCmBgYHtyfQ0KZ2xpbXBzZShzYWMubWV0cm8pDQpgYGANCg0KXA0KDQpPSywgdGhhdCBnZW9tZXRyeSBsb29rcyBnb29kLiBBbmQgaXQncyBhIHBvbHlnb24sIHNvIHRoYXQncyBnb29kLiBMZXQncyBub3cgdHJ5IHRvIGludGVyc2VjdCBvdXIgKnNhYy5tZXRybyogZGF0YXNldCB3aXRoIG91ciAqY2EudHJhY3RzKiBkYXRhc2V0LiANCg0KYGBge3J9DQpzYWMubWV0cm8udHJhY3RzLmludCA8LSBzdF9qb2luKGNhLnRyYWN0cywgc2FjLm1ldHJvLCANCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgam9pbiA9IHN0X2ludGVyc2VjdHMsIGxlZnQ9RkFMU0UpDQpgYGANCg0KXA0KDQpUaGUgYWJvdmUgY29kZSB0ZWxscyBSIHRvIGlkZW50aWZ5IHRoZSBwb2x5Z29ucyBpbiAqY2EudHJhY3RzKiB0aGF0IGludGVyc2VjdCB3aXRoIHRoZSBwb2x5Z29uICpzYWMubWV0cm8qLiBXZSBpbmRpY2F0ZSB3ZSB3YW50IGEgcG9seWdvbiBpbnRlcnNlY3Rpb24gYnkgc3BlY2lmeWluZyBgam9pbiA9IHN0X2ludGVyc2VjdHNgLiBUaGUgb3B0aW9uIGBsZWZ0PUZBTFNFYCB0ZWxscyBSIHRvIHJlbW92ZSB0aGUgcG9seWdvbnMgZnJvbSAqY2EudHJhY3RzKiB0aGF0IGRvIG5vdCBpbnRlcnNlY3QgKG1ha2UgaXQgVFJVRSBhbmQgc2VlIHdoYXQgaGFwcGVucykgd2l0aCAqc2FjLm1ldHJvKi4gUGxvdHRpbmcgb3VyIHRyYWN0cywgd2UgZ2V0Og0KYGBge3J9DQp0bV9zaGFwZShzYWMubWV0cm8udHJhY3RzLmludCkgKw0KICB0bV9wb2x5Z29ucyhjb2wgPSAiYmx1ZSIpICsNCnRtX3NoYXBlKHNhYy5tZXRybykgKyAgDQogIHRtX2JvcmRlcnMoY29sID0gInJlZCIpDQpgYGANCg0KXA0KDQojIyBXaXRoaW4NCg0KV2UgaGF2ZSBvbmUgc21hbGwgaXNzdWUuIFVzaW5nIGBqb2luID0gc3RfaW50ZXJzZWN0c2AgcmV0dXJucyBhbGwgdHJhY3RzIHRoYXQgaW50ZXJzZWN0ICpzYWMubWV0cm8qLCB3aGljaCBpbmNsdWRlIHRob3NlIHRoYXQgdG91Y2ggdGhlIG1ldHJv4oCZcyBib3VuZGFyeS4gTm8gYnVlbm8uIFdlIGNhbiBpbnN0ZWFkIHVzZSB0aGUgYXJndW1lbnQgYGpvaW4gPSBzdF93aXRoaW5gIHRvIHJldHVybiB0cmFjdHMgdGhhdCBhcmUgKmNvbXBsZXRlbHkgd2l0aGluKiB0aGUgbWV0cm8gYXJlYS4NCg0KYGBge3J9DQpzYWMubWV0cm8udHJhY3RzLncgPC0gc3Rfam9pbihjYS50cmFjdHMsIHNhYy5tZXRybywgam9pbiA9IHN0X3dpdGhpbiwgbGVmdD1GQUxTRSkNCg0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIHRtX3BvbHlnb25zKGNvbCA9ICJibHVlIikgKw0KdG1fc2hhcGUoc2FjLm1ldHJvKSArDQogIHRtX2JvcmRlcnMoY29sID0gInJlZCIpDQpgYGANCg0KXA0KDQpMb29raW5nIG11Y2ggYmV0dGVyISBOb3csIGlmIHdlIGxvb2sgYXQgKnNhYy5tZXRyby50cmFjdHMudyrigJlzIGF0dHJpYnV0ZSB0YWJsZSwgeW914oCZbGwgc2VlIGl0IGluY2x1ZGVzIGFsbCB0aGUgdmFyaWFibGVzIGZyb20gYm90aCAqY2EudHJhY3RzKiBhbmQgKnNhYy5tZXRybyouIFdlIGRvbuKAmXQgbmVlZCB0aGVzZSB2YXJpYWJsZXMsIHNvIHVzZSBgc2VsZWN0KClgIHRvIGVsaW1pbmF0ZSB0aGVtLiBZb3XigJlsbCBhbHNvIG5vdGljZSB0aGF0IGlmIHZhcmlhYmxlcyBmcm9tIHR3byBkYXRhIHNldHMgc2hhcmUgdGhlIHNhbWUgbmFtZSwgUiB3aWxsIGtlZXAgYm90aCBhbmQgYXR0YWNoIGEgKi54KiBhbmQgKi55KiB0byB0aGUgZW5kLiBGb3IgZXhhbXBsZSwgKkdFT0lEKiB3YXMgZm91bmQgaW4gYm90aCAqY2EudHJhY3RzKiBhbmQgKnNhYy5tZXRybyosIHNvIFIgbmFtZWQgb25lICpHRU9JRC54KiBhbmQgdGhlIG90aGVyIHRoYXQgd2FzIG1lcmdlZCBpbiB3YXMgbmFtZWQgKkdFT0lELnkqLg0KDQpcDQoNCiMgTWFwcGluZyBpbiBSDQoNCiMjIGdncGxvdA0KDQpPSywgc28gbm93IHdlJ3ZlIHRhbGtlZCBhIGxpdHRsZSBhYm91dCBob3cgdG8gYnJpbmcgaW4gYW5kIG1hbmlwdWxhdGUgdmVjdG9yIHBvbHlnb24gZGF0YSwgbGV0J3MgZG8gc29tZSBtYXBwaW5nIGFuZCBjcmVhdGUgc29tZSBjaG9yb3BsZXRoIG1hcHMuIFdlIGNhbiBkbyB0aGlzIHdpdGggdGhlICoqZ2dwbG90KiogcGFja2FnZSwgdGhlICoqdG1hcCoqIHBhY2thZ2UsIGFuZCB0aGUgKipsZWFmbGV0KiogcGFja2FnZSAod2hpY2ggd2Ugd29uJ3QgY292ZXIgbm93LCBidXQgaXQncyB2ZXJ5IGNvb2wgZm9yIGludGVyYWN0aXZlIG1hcHMpLiBMZXQncyBzdGFydCB3aXRoICoqZ2dwbG90KiouDQoNClwNCg0KIyMjIGdncGxvdA0KDQpCZWNhdXNlICoqc2YqKiBpcyB0aWR5IGZyaWVuZGx5LCBpdCBpcyBubyBzdXJwcmlzZSB3ZSBjYW4gdXNlIHRoZSAqKnRpZHl2ZXJzZSoqIHBsb3R0aW5nIGZ1bmN0aW9uIGBnZ3Bsb3QoKWAgdG8gbWFrZSBtYXBzLiBXZSBhbHJlYWR5IHJlY2VpdmVkIGFuIGludHJvZHVjdGlvbiB0byBgZ2dwbG90KClgIGluIFtMYWIgM10oTGFiM18yMDI2Lmh0bWwpLiBSZWNhbGwgaXRzIGJhc2ljIHN0cnVjdHVyZToNCg0KYGBge3IsIGV2YWw9RkFMU0V9DQpnZ3Bsb3QoZGF0YSA9IDxEQVRBPikgKw0KICAgICAgPEdFT01fRlVOQ1RJT04+KG1hcHBpbmcgPSBhZXMoeCwgeSkpICsNCiAgICAgIDxPUFRJT05TPigpDQpgYGANCg0KXA0KDQpJbiBtYXBwaW5nLCBgZ2VvbV9zZigpYCBpcyBgPEdFT01fRlVOQ1RJT04+KClgLiBVbmxpa2Ugd2l0aCBmdW5jdGlvbnMgbGlrZSBgZ2VvbV9oaXN0b2dyYW0oKWAgYW5kIGBnZW9tX2JveHBsb3QoKWAsIHdlIGRvbuKAmXQgc3BlY2lmeSBhbiB4IGFuZCB5IGF4aXMuIEluc3RlYWQgeW91IHVzZSBgZmlsbGAgaWYgeW91IHdhbnQgdG8gbWFwIGEgdmFyaWFibGUgb3IgY29sb3IgdG8ganVzdCBtYXAgYm91bmRhcmllcy4NCg0KTGV04oCZcyB1c2UgYGdncGxvdCgpYCB0byBtYWtlIGEgY2hvcm9wbGV0aCBtYXAuIFdlIG5lZWQgdG8gc3BlY2lmeSBhIG51bWVyaWMgdmFyaWFibGUgaW4gdGhlIGBmaWxsID1gIGFyZ3VtZW50IHdpdGhpbiBgZ2VvbV9zZigpYC4gSGVyZSB3ZSBtYXAgdHJhY3QtbGV2ZWwgbWVkaWFuIGhvdXNlaG9sZCBpbmNvbWUgaW4gdGhlIFNhY3JhbWVudG8gbWV0cm8gYXJlYS4NCg0KYGBge3J9DQpnZ3Bsb3QoZGF0YSA9IHNhYy5tZXRyby50cmFjdHMudykgKw0KICBnZW9tX3NmKGFlcyhmaWxsID0gbWVkaW5jRSkpDQpgYGANCg0KXA0KDQpXZSBjYW4gYWxzbyBzcGVjaWZ5IGEgdGl0bGUgKGFzIHdlbGwgYXMgc3VidGl0bGVzIGFuZCBjYXB0aW9ucykgdXNpbmcgdGhlIGBsYWJzKClgIGZ1bmN0aW9uLg0KDQoNCmBgYHtyfQ0KZ2dwbG90KGRhdGEgPSBzYWMubWV0cm8udHJhY3RzLncpICsNCiAgZ2VvbV9zZihhZXMoZmlsbCA9IG1lZGluY0UpKSArDQogICAgbGFicyh0aXRsZSA9ICJNZWRpYW4gSW5jb21lIFNhY3JhbWVudG8gTVNBIFRyYWN0cyIpIA0KYGBgDQoNClwNCg0KV2UgY2FuIG1ha2UgZnVydGhlciBsYXlvdXQgYWRqdXN0bWVudHMgdG8gdGhlIG1hcC4gRG9u4oCZdCBsaWtlIGEgYmx1ZSBzY2FsZSBvbiB0aGUgbGVnZW5kPyBZb3UgY2FuIGNoYW5nZSBpdCB1c2luZyB0aGUgYHNjYWxlX2ZpbGVfZ3JhZGllbnQoKWAgZnVuY3Rpb24uIExldOKAmXMgY2hhbmdlIGl0IHRvIGEgd2hpdGUgdG8gcmVkIGdyYWRpZW50LiBXZSBjYW4gYWxzbyBlbGltaW5hdGUgdGhlIGdyYXkgdHJhY3QgYm9yZGVyIGNvbG9ycyB0byBtYWtlIHRoZSBmaWxsIGNvbG9yIGRpc3RpbmN0aW9uIGNsZWFyZXIuIFdlIGRvIHRoaXMgYnkgc3BlY2lmeWluZyBgY29sb3IgPSBOQWAgaW5zaWRlIGBnZW9tX3NmKClgLiBXZSBjYW4gYWxzbyBnZXQgcmlkIG9mIHRoZSBncmF5IGJhY2tncm91bmQgYnkgc3BlY2lmeWluZyBhIGJhc2ljIGJsYWNrIGFuZCB3aGl0ZSB0aGVtZSB1c2luZyBgdGhlbWVfYncoKWAuIFdlIGFsc28gYWRkZWQgYSBjYXB0aW9uIGluZGljYXRpbmcgdGhlIHNvdXJjZSBvZiB0aGUgZGF0YSB1c2luZyB0aGUgYGNhcHRpb25zID1gIHBhcmFtZXRlciB3aXRoaW4gYGxhYnMoKWAuIFdlIHRoZW4gY2hhbmdlZCB0aGUgY29sb3IgdG8gcmVkIHVzaW5nIGxhYmVscyBmb3IgYGxvdz1gIGFuZCBgaGlnaD1gLCBhbmQgd2UgYWRkZWQgYSBuYW1lIHRvIG91ciBsZWdlbmQgd2l0aCBgbmFtZT0nLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhID0gc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIGdlb21fc2YoYWVzKGZpbGwgPSBtZWRpbmNFKSwgY29sb3IgPSBOQSkgKw0KICAgIHNjYWxlX2ZpbGxfZ3JhZGllbnQobG93PSAid2hpdGUiLCBoaWdoID0gInJlZCIsIG5hLnZhbHVlID0iZ3JheSIsIG5hbWUgPSAiTWVkaWFuIEluY29tZSIpICsgIA0KICAgIGxhYnModGl0bGUgPSAiTWVkaWFuIEluY29tZSBTYWNyYW1lbnRvIE1TQSBUcmFjdHMiLA0KICAgICAgICAgY2FwdGlvbiA9ICJTb3VyY2U6IEFtZXJpY2FuIENvbW11bml0eSBTdXJ2ZXkiKSArICANCiAgdGhlbWVfYncoKQ0KYGBgDQoNClwNCg0KRGFyZSBJIHNheSwgd2UgYXJlIHJlYWR5IGZvciB0aGUgTmV3IFlvcmsgVGltZXMgd2l0aCB0aGlzIG1hcCENCg0KXA0KDQojIyMgUG9pbnRzIG9uIHRvcCBvZiBwb2x5Z29ucw0KDQpPSywgbm93IHRoYXQgd2UgaGF2ZSBtYXBwZWQgcG9pbnRzIGFuZCBtYXBwZWQgcG9seWdvbnMsIGxldCdzIHB1dCB0aGVtIGJvdGggdG9nZXRoZXIhIEZpcnN0LCB3ZSBhcmUgZ29pbmcgdG8gYnJpbmcgaW4gb3VyIG9sZCBmcmllbmQgKkNBZGF0YSogZnJvbSBsYXN0IHdlZWsncyBsYWIuDQoNCmBgYHtyfQ0KZGF0YShDQWRhdGEpDQpjYV9wdHMgPC0gQ0FkYXRhDQpzdW1tYXJ5KGNhX3B0cykNCmNhX3B0cyA8LSBzdF9hc19zZihDQWRhdGEsIGNvb3Jkcz1jKCJYIiwiWSIpKQ0KY2FfcHJvaiA8LSAiK3Byb2o9bGNjICtsYXRfMT00MCArbGF0XzI9NDEuNjY2NjY2NjY2NjY2NjYgDQogICAgICAgICAgICAgK2xhdF8wPTM5LjMzMzMzMzMzMzMzMzM0ICtsb25fMD0tMTIyICt4XzA9MjAwMDAwMCANCiAgICAgICAgICAgICAreV8wPTUwMDAwMC4wMDAwMDAwMDAyICtlbGxwcz1HUlM4MCANCiAgICAgICAgICAgICArZGF0dW09TkFEODMgK3VuaXRzPW0gK25vX2RlZnMiDQoNCiNTZXQgQ1JTDQpjYV9wdHNfY3JzIDwtIHN0X3NldF9jcnMoY2FfcHRzLCBjYV9wcm9qKQ0KYGBgDQoNClwNCg0KVGhpcyB0aW1lLCB3ZSB3aWxsIG1hcCB0aG9zZSBwb2ludHMgd2l0aCAqKmdncGxvdCoqLg0KDQpgYGB7cn0NCmdncGxvdChkYXRhID0gY2FfcHRzX2NycykgKw0KICBnZW9tX3NmKGZpbGwgPSAiYmxhY2siKSArDQogIGxhYnModGl0bGUgPSAiU3R1ZHkgUGFydGljaXBhbnRzIiwNCiAgICAgICBjYXB0aW9uID0gIlNvdXJjZTogT3ZhcmlhbiBDYW5jZXIgQ2FzZXMiKSArICANCiAgdGhlbWVfYncoKQ0KDQpgYGANCg0KXA0KDQpXZSBjYW4gb3ZlcmxheSB0aGUgcG9pbnRzIG92ZXIgU2FjcmFtZW50byB0cmFjdHMgdG8gZ2l2ZSB0aGUgbG9jYXRpb25zIHNvbWUgcGVyc3BlY3RpdmUuIEhlcmUsIHlvdSBhZGQgdHdvIGBnZW9tX3NmKClgIGFyZ3VtZW50cyBmb3IgdGhlIHRyYWN0cyBhbmQgdGhlIGNhbmNlciBjYXNlcy4NCg0KYGBge3J9DQpnZ3Bsb3QoKSArDQogIGdlb21fc2YoZGF0YSA9IHNhYy5tZXRyby50cmFjdHMudykgKw0KICBnZW9tX3NmKGRhdGEgPSBjYV9wdHNfY3JzLCBmaWxsID0gImJsYWNrIikgKw0KICBsYWJzKHRpdGxlID0gIlN0dWR5IFBhcnRpY2lwYW50cyIsDQogICAgICAgY2FwdGlvbiA9ICJTb3VyY2U6IE92YXJpYW4gQ2FuY2VyIENhc2VzIikgKyAgDQogIHRoZW1lX2J3KCkNCmBgYA0KDQpcDQoNCkhtbW0uIFRoYXQgZG9lc24ndCBsb29rIGdyZWF0LiBXZSBoYXZlIGxvdHMgb2YgY2FzZXMgb3V0c2lkZSBvZiBTYWNyYW1lbnRvLiBMZXQncyBmaWx0ZXIgb3V0IHRvIGp1c3QgcGljayBjYXNlcyB3aXRoaW4gdGhlIFNhY3JhbWVudG8gYXJlYS4gDQoNCmBgYHtyLCBldmFsPUZBTFNFfQ0KY2FfcHRzX2Nycy53IDwtIHN0X2pvaW4oY2FfcHRzX2Nycywgc2FjLm1ldHJvLnRyYWN0cy53LCBqb2luID0gc3Rfd2l0aGluLCBsZWZ0PUZBTFNFKQ0KYGBgDQoNClwNCg0KT29vZi4gVGhhdCBkb2Vzbid0IHdvcmsuIEl0IHNheXMgb3VyICBgc3RfY3JzKHgpID09IHN0X2Nycyh5KSBpcyBub3QgVFJVRWAuIFRoYXQgbWVhbnMgb3VyIENvb3JkaW5hdGUgUmVmZXJlbmNlIFN5c3RlbXMgYXJlIG5vdCBtYXRjaGluZyEgTGV0J3MgdHJhbnNmb3JtIG91ciBjYW5jZXIgZGF0YXNldCAqY2FfcHRzX2Nycy53KiB0byBtYXRjaCB0aGUgQ1JTIGZvciAqc2FjLm1ldHJvLnRyYWN0cy53KiB3aXRoIG9uZSBlYXN5IHN0ZXAgdXNpbmcgYHN0X3RyYW5zZm9ybWA6DQpgYGB7cn0NCiNjaGVjayBjcnMgb2YgZWFjaCBkYXRhc2V0DQpzdF9jcnMoY2FfcHRzX2NycykNCnN0X2NycyhzYWMubWV0cm8udHJhY3RzLncpDQoNCiNjcmVhdGUgbmV3IGRhdGFzZXQgd2l0aCB0cmFuc2Zvcm1lZCBDUlMNCmNhX3B0c19jcnMudHJhbnNmb3JtZWQgPC0gc3RfdHJhbnNmb3JtKGNhX3B0c19jcnMsc3RfY3JzKHNhYy5tZXRyby50cmFjdHMudykpDQoNCnN0X2NycyhjYV9wdHNfY3JzLnRyYW5zZm9ybWVkICkNCmBgYA0KDQpcDQoNCk9LLCBsZXQncyB0cnkgdGhpcyBhZ2FpbiENCg0KYGBge3J9DQpjYV9wdHNfY3JzLncgPC0gc3Rfam9pbihjYV9wdHNfY3JzLnRyYW5zZm9ybWVkLCBzYWMubWV0cm8udHJhY3RzLncsIGpvaW4gPSBzdF93aXRoaW4sIGxlZnQ9RkFMU0UpDQpgYGANCg0KSXQgd29ya2VkISBPSywgbm93IGxldCdzIHRyeSBvdXIgbWFwIGFnYWluLg0KDQpgYGB7cn0NCmdncGxvdCgpICsNCiAgZ2VvbV9zZihkYXRhID0gc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIGdlb21fc2YoZGF0YSA9IGNhX3B0c19jcnMudywgZmlsbCA9ICJibGFjayIpICsNCiAgbGFicyh0aXRsZSA9ICJTdHVkeSBQYXJ0aWNpcGFudHMiLA0KICAgICAgIGNhcHRpb24gPSAiU291cmNlOiBPdmFyaWFuIENhbmNlciBDYXNlcyIpICsgIA0KICB0aGVtZV9idygpDQpgYGANCg0KXA0KDQpBbHJpZ2h0LCB3aG8ncyByZWFkeSBmb3IgYSBjaGFsbGVuZ2U/IExldCdzIHB1dCBpdCBhbGwgdG9nZXRoZXIgaW4gb25lIG5pY2UgbWFwLg0KDQpgYGB7cn0NCmdncGxvdCgpICsgDQogIGdlb21fc2YoZGF0YSA9IHNhYy5tZXRyby50cmFjdHMudywgYWVzKGZpbGwgPSBtZWRpbmNFKSwgY29sb3IgPSBOQSkgKw0KICAgIHNjYWxlX2ZpbGxfZ3JhZGllbnQobG93PSAid2hpdGUiLCBoaWdoID0gInJlZCIsIG5hLnZhbHVlID0iZ3JheSIsIG5hbWUgPSAiTWVkaWFuIEluY29tZSIpICsgIA0KICBnZW9tX3NmKGRhdGEgPSBjYV9wdHNfY3JzLncsIGZpbGwgPSAiYmxhY2siKSArDQogIGxhYnModGl0bGUgPSAiU3R1ZHkgUGFydGljaXBhbnRzIE92ZXJsYWlkIHdpdGggTWVkaWFuIEluY29tZSBvZiBTYWNyYW1lbnRvIFRyYWN0cyIsDQogICAgICAgY2FwdGlvbiA9ICJTb3VyY2U6IEFtZXJpY2FuIENvbW11bml0eSBTdXJ2ZXkgYW5kIE92YXJpYW4gQ2FuY2VyIENhc2VzIikgKw0KICB0aGVtZV9idygpDQogIA0KYGBgDQoNClwNCg0KQ2FuIEkganVzdCBzYXksIHlvdSdyZSB2ZXJ5IGltcHJlc3NpdmUuIFdlbGwgZG9uZSENCg0KXA0KDQojIyB0bWFwDQoNCldoZXRoZXIgeW91IHByZWZlciAqKnRtYXAqKiBvciAqKmdncGxvdCoqIGlzIHVwIHRvIHlvdSwgYnV0IEkgZmluZCB0aGF0ICoqdG1hcCoqIGhhcyBzb21lIGJlbmVmaXRzLCBzbyBsZXTigJlzIGZvY3VzIG9uIGl0cyBtYXBwaW5nIGZ1bmN0aW9ucyBuZXh0Lg0KDQoqKnRtYXAqKiB1c2VzIHRoZSBzYW1lIGxheWVyZWQgbG9naWMgYXMgKipnZ3Bsb3QqKi4gQXMgd2Ugc2F3IGxhc3Qgd2VlaywgdGhlIGluaXRpYWwgY29tbWFuZCBpcyBgdG1fc2hhcGUoKWAsIHdoaWNoIHNwZWNpZmllcyB0aGUgZ2VvZ3JhcGh5IHRvIHdoaWNoIHRoZSBtYXBwaW5nIGlzIGFwcGxpZWQuIFlvdSB0aGVuIGJ1aWxkIG9uIGB0bV9zaGFwZSgpYCBieSBhZGRpbmcgb25lIG9yIG1vcmUgZWxlbWVudHMgc3VjaCBhcyBgdG1fcG9seWdvbnMoKWAgZm9yIHBvbHlnb25zLCBgdG1fYm9yZGVycygpYCBmb3IgbGluZXMsIGFuZCBgdG1fZG90cygpYCBmb3IgcG9pbnRzLiBBbGwgYWRkaXRpb25hbCBmdW5jdGlvbnMgdGFrZSBvbiB0aGUgZm9ybSBvZiBgdG1fYC4gQ2hlY2sgdGhlIGZ1bGwgbGlzdCBvZiBgdG1fYCBlbGVtZW50cyBbaGVyZV0oaHR0cHM6Ly93d3cucmRvY3VtZW50YXRpb24ub3JnL3BhY2thZ2VzL3RtYXAvdmVyc2lvbnMvMi4wL3RvcGljcy90bWFwLWVsZW1lbnQpLg0KDQojIyMgQ2hvcm9wbGV0aCBtYXBzIGluIHRtYXANCg0KTGV04oCZcyBtYWtlIGEgc3RhdGljIGNob3JvcGxldGggbWFwIG9mIG1lZGlhbiBob3VzZWhvbGQgaW5jb21lIGluIFNhY3JhbWVudG8gTVNBIGp1c3QgbGlrZSB3ZSBkaWQgYWJvdmUsIGJ1dCB0aGlzIHRpbWUgaW4gKip0bWFwKiouDQoNCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIHRtX3BvbHlnb25zKGZpbGwgPSAibWVkaW5jRSIsIA0KICAgICAgICAgICAgICBmaWxsLnNjYWxlID0gdG1fc2NhbGVfaW50ZXJ2YWxzKHN0eWxlID0gInF1YW50aWxlIikpDQpgYGANCg0KXA0KDQpXZSBmaXJzdCBwdXQgdGhlIGRhdGFzZXQgKnNhYy5tZXRyby50cmFjdHMudyogaW5zaWRlIGB0bV9zaGFwZSgpYC4gQmVjYXVzZSB5b3UgYXJlIHBsb3R0aW5nIHBvbHlnb25zLCB5b3UgdXNlIGB0bV9wb2x5Z29ucygpYCBuZXh0LiBUaGUgYXJndW1lbnQgYGZpbGwgPSAibWVkaW5jRSJgIHRlbGxzIFIgdG8gc2hhZGUgKG9yIGNvbG9yKSB0aGUgdHJhY3RzIGJ5IHRoZSB2YXJpYWJsZSAqbWVkaW5jRS4qIFRoZSBhcmd1bWVudCBgZmlsbC5zY2FsZSA9IHRtX3NjYWxlX2ludGVydmFscyhzdHlsZSA9ICJxdWFudGlsZSIpImAgdGVsbHMgUiB0byBicmVhayB1cCB0aGUgc2hhZGluZyBpbnRvIHF1YW50aWxlcywgb3IgZXF1YWwgZ3JvdXBzIG9mIDUgYXMgYSBkZWZhdWx0LiBJIGZpbmQgdGhhdCB0aGlzIGlzIHdoZXJlICoqdG1hcCoqIG9mZmVycyBhIGRpc3RpbmN0IGFkdmFudGFnZSBvdmVyICoqZ2dwbG90KiogaW4gdGhhdCB1c2VycyBoYXZlIGdyZWF0ZXIgY29udHJvbCBvdmVyIHRoZSBsZWdlbmQgYW5kIGJpbiBicmVha3MuICoqdG1hcCoqIGFsbG93cyB1c2VycyB0byBzcGVjaWZ5IGFsZ29yaXRobXMgdG8gYXV0b21hdGljYWxseSBjcmVhdGUgYnJlYWtzIHdpdGggdGhlIHN0eWxlIGFyZ3VtZW50LiBZb3UgY2FuIGFsc28gY2hhbmdlIHRoZSBudW1iZXIgb2YgYnJlYWtzIGJ5IHNldHRpbmcgYG49YC4gVGhlIGRlZmF1bHQgaXMgYG49NWAuIFJhdGhlciB0aGFuIHF1aW50aWxlcywgeW91IGNhbiBzaG93IHF1YXJ0aWxlcyB1c2luZyBgbj00YC4gSSdtIGZlZWxpbmcgY3JhenkuIExldCdzIGRvIGl0Lg0KDQpgYGB7cn0NCnRtX3NoYXBlKHNhYy5tZXRyby50cmFjdHMudykgKw0KICB0bV9wb2x5Z29ucyhmaWxsID0gIm1lZGluY0UiLCANCiAgICAgICAgICAgICAgZmlsbC5zY2FsZSA9IHRtX3NjYWxlX2ludGVydmFscyhzdHlsZSA9ICJxdWFudGlsZSIsIG49NCkpDQpgYGANCg0KXA0KDQpDaGVjayBvdXQgW3RoaXMgbGlua10oaHR0cHM6Ly9nZW9jb21wci5yb2JpbmxvdmVsYWNlLm5ldC9hZHYtbWFwLmh0bWwjY29sb3Itc2V0dGluZ3MpIGZvciBtb3JlIG9uICBhdmFpbGFibGUgY2xhc3NpZmljYXRpb24gc3R5bGVzIGluICoqdG1hcCoqLg0KDQpcDQoNClRoZSBgdG1fcG9seWdvbnMoKWAgY29tbWFuZCBpcyBhIHdyYXBwZXIgYXJvdW5kIHR3byBvdGhlciBmdW5jdGlvbnMsIGB0bV9maWxsKClgIGFuZCBgdG1fYm9yZGVycygpYC4gYHRtX2ZpbGwoKWAgY29udHJvbHMgdGhlIGNvbnRlbnRzIG9mIHRoZSBwb2x5Z29ucyAoY29sb3IsIGNsYXNzaWZpY2F0aW9uLCBldGMuKSwgd2hpbGUgYHRtX2JvcmRlcnMoKWAgZG9lcyB0aGUgc2FtZSBmb3IgdGhlIHBvbHlnb24gb3V0bGluZXMuDQoNCkZvciBleGFtcGxlLCB1c2luZyB0aGUgc2FtZSBzaGFwZSAoYnV0IG5vIHZhcmlhYmxlKSwgd2Ugb2J0YWluIHRoZSBvdXRsaW5lcyBvZiB0aGUgbmVpZ2hib3Job29kcyBmcm9tIHRoZSBgdG1fYm9yZGVycygpYCBjb21tYW5kLg0KYGBge3J9DQp0bV9zaGFwZShzYWMubWV0cm8udHJhY3RzLncpICsNCiAgdG1fYm9yZGVycygpDQpgYGANCg0KXA0KDQpTaW1pbGFybHksIHdlIG9idGFpbiBhIGNob3JvcGxldGggbWFwIHdpdGhvdXQgdGhlIHBvbHlnb24gb3V0bGluZXMgd2hlbiB3ZSBqdXN0IHVzZSB0aGUgYHRtX2ZpbGwoKWAgY29tbWFuZC4NCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArIA0KICB0bV9maWxsKCJtZWRpbmNFIikNCmBgYA0KDQpXaGVuIHdlIGNvbWJpbmUgdGhlIHR3byBjb21tYW5kcywgd2Ugb2J0YWluIHRoZSBzYW1lIG1hcCBhcyB3aXRoIHRtX3BvbHlnb25zKCkgKHRoaXMgaWxsdXN0cmF0ZXMgaG93IGluIFIgb25lIGNhbiBvZnRlbiBvYnRhaW4gdGhlIHNhbWUgcmVzdWx0IGluIGEgbnVtYmVyIG9mIGRpZmZlcmVudCB3YXlzKS4gVHJ5IHRoaXMgb24geW91ciBvd24uDQoNClwNCg0KIyMjIENvbG9yIHNjaGVtZQ0KDQpUaGUgYXJndW1lbnQgYHZhbHVlcyA9YCBkZWZpbmVzIHRoZSBjb2xvciByYW5nZXMgYXNzb2NpYXRlZCB3aXRoIHRoZSBiaW5zIGFuZCBkZXRlcm1pbmVkIGJ5IHRoZSBgc3R5bGVgIGFyZ3VtZW50cy4gU2V2ZXJhbCBidWlsdC1pbiBwYWxldHRlcyBhcmUgY29udGFpbmVkIGluICoqdG1hcCoqLiBGb3IgZXhhbXBsZSwgdXNpbmcgYHZhbHVlcyA9ICJSZWRzImAgd291bGQgeWllbGQgdGhlIGZvbGxvd2luZyBtYXAgZm9yIG91ciBleGFtcGxlLg0KYGBge3IsIG1lc3NhZ2UgPSBGQUxTRX0NCnRtX3NoYXBlKHNhYy5tZXRyby50cmFjdHMudykgKw0KICB0bV9wb2x5Z29ucyhmaWxsID0gIm1lZGluY0UiLCANCiAgICAgICAgICAgICAgZmlsbC5zY2FsZSA9IHRtX3NjYWxlX2ludGVydmFscyhzdHlsZSA9ICJxdWFudGlsZSIsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlcyA9ICJSZWRzIikpIA0KYGBgDQoNClVuZGVyIHRoZSBob29kLCBg4oCcUmVkc+KAnWAgcmVmZXJzIHRvIG9uZSBvZiB0aGUgY29sb3Igc2NoZW1lcyBzdXBwb3J0ZWQgYnkgdGhlICoqUkNvbG9yQnJld2VyKiogcGFja2FnZSAoc2VlIGJlbG93KS4NCg0KXA0KDQpJbiBhZGRpdGlvbiB0byB0aGUgYnVpbHQtaW4gcGFsZXR0ZXMsIGN1c3RvbWl6ZWQgY29sb3IgcmFuZ2VzIGNhbiBiZSBjcmVhdGVkIGJ5IHNwZWNpZnlpbmcgYSB2ZWN0b3Igd2l0aCB0aGUgZGVzaXJlZCBjb2xvcnMgYXMgYW5jaG9ycy4gVGhpcyB3aWxsIGNyZWF0ZSBhIHNwZWN0cnVtIG9mIGNvbG9ycyBpbiB0aGUgbWFwIHRoYXQgcmFuZ2UgYmV0d2VlbiB0aGUgY29sb3JzIHNwZWNpZmllZCBpbiB0aGUgdmVjdG9yLiBGb3IgaW5zdGFuY2UsIGlmIHdlIHVzZWQgYGMo4oCccmVk4oCdLCDigJxibHVl4oCdKWAsIHRoZSBjb2xvciBzcGVjdHJ1bSB3b3VsZCBtb3ZlIGZyb20gcmVkIHRvIHB1cnBsZSwgdGhlbiB0byBibHVlLCB3aXRoIGluIGJldHdlZW4gc2hhZGVzLiBJbiBvdXIgZXhhbXBsZToNCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIHRtX3BvbHlnb25zKGZpbGwgPSAibWVkaW5jRSIsIA0KICAgICAgICAgICAgICBmaWxsLnNjYWxlID0gdG1fc2NhbGVfaW50ZXJ2YWxzKHN0eWxlID0gInF1YW50aWxlIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzID0gYygicmVkIiwiYmx1ZSIpKSkgDQpgYGANCg0KXA0KDQpOb3QgZXhhY3RseSBhIHByZXR0eSBwaWN0dXJlLiBJbiBvcmRlciB0byBjYXB0dXJlIGEgZGl2ZXJnaW5nIHNjYWxlLCB3ZSBpbnNlcnQgYOKAnHdoaXRl4oCdYCBpbiBiZXR3ZWVuIHJlZCBhbmQgYmx1ZS4NCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIHRtX3BvbHlnb25zKGZpbGwgPSAibWVkaW5jRSIsIA0KICAgICAgICAgICAgICBmaWxsLnNjYWxlID0gdG1fc2NhbGVfaW50ZXJ2YWxzKHN0eWxlID0gInF1YW50aWxlIiwgDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzID0gYygicmVkIiwid2hpdGUiLCAiYmx1ZSIpKSkgDQpgYGANCg0KXA0KDQpBIHByZWZlcnJlZCBhcHByb2FjaCB0byBzZWxlY3QgYSBjb2xvciBwYWxldHRlIGlzIHRvIGNob3NlIG9uZSBvZiB0aGUgc2NoZW1lcyBjb250YWluZWQgaW4gdGhlICoqUkNvbG9yQnJld2VyKiogcGFja2FnZS4gVGhlc2UgYXJlIGJhc2VkIG9uIHRoZSByZXNlYXJjaCBvZiBjYXJ0b2dyYXBoZXIgQ3ludGhpYSBCcmV3ZXIgKHNlZSB0aGUgY29sb3JicmV3ZXIyIFt3ZWJzaXRlXShodHRwczovL2NvbG9yYnJld2VyMi5vcmcvI3R5cGU9c2VxdWVudGlhbCZzY2hlbWU9QnVHbiZuPTMpIGZvciBkZXRhaWxzKS4gQ29sb3JCcmV3ZXIgbWFrZXMgYSBkaXN0aW5jdGlvbiBiZXR3ZWVuIHNlcXVlbnRpYWwgc2NhbGVzIChmb3IgYSBzY2FsZSB0aGF0IGdvZXMgZnJvbSBsb3cgdG8gaGlnaCksIGRpdmVyZ2luZyBzY2FsZXMgKHRvIGhpZ2hsaWdodCBob3cgdmFsdWVzIGRpZmZlciBmcm9tIGEgY2VudHJhbCB0ZW5kZW5jeSksIGFuZCBxdWFsaXRhdGl2ZSBzY2FsZXMgKGZvciBjYXRlZ29yaWNhbCB2YXJpYWJsZXMpLiBGb3IgZWFjaCBzY2FsZSwgYSBzZXJpZXMgb2Ygc2luZ2xlIGh1ZSBhbmQgbXVsdGktaHVlIHNjYWxlcyBhcmUgc3VnZ2VzdGVkLiBJbiB0aGUgKipSQ29sb3JCcmV3ZXIqKiBwYWNrYWdlLCB0aGVzZSBhcmUgcmVmZXJyZWQgdG8gYnkgYSBuYW1lIChlLmcuLCB0aGUg4oCcUmVkc+KAnSBwYWxldHRlIHdlIHVzZWQgYWJvdmUgaXMgYW4gZXhhbXBsZSkuIFRoZSBmdWxsIGxpc3QgaXMgY29udGFpbmVkIGluIHRoZSAqKlJDb2xvckJyZXdlcioqIGRvY3VtZW50YXRpb24uDQoNClRoZXJlIGFyZSB0d28gdmVyeSB1c2VmdWwgY29tbWFuZHMgaW4gdGhpcyBwYWNrYWdlLiBPbmUgc2V0cyBhIGNvbG9yIHBhbGV0dGUgYnkgc3BlY2lmeWluZyBpdHMgbmFtZSBhbmQgdGhlIG51bWJlciBvZiBkZXNpcmVkIGNhdGVnb3JpZXMuIFRoZSByZXN1bHQgaXMgYSBjaGFyYWN0ZXIgdmVjdG9yIHdpdGggdGhlIGhleCBjb2RlcyBvZiB0aGUgY29ycmVzcG9uZGluZyBjb2xvcnMuDQoNCkZvciBleGFtcGxlLCB3ZSBzZWxlY3QgYSBzZXF1ZW50aWFsIGNvbG9yIHNjaGVtZSBnb2luZyBmcm9tIGJsdWUgdG8gZ3JlZW4sIGFzICpCdUduKiwgYnkgbWVhbnMgb2YgdGhlIGNvbW1hbmQgYGJyZXdlci5wYWxgLCB3aXRoIHRoZSBudW1iZXIgb2YgY2F0ZWdvcmllcyAoNikgYW5kIHRoZSBzY2hlbWUgYXMgYXJndW1lbnRzLiBUaGUgcmVzdWx0aW5nIHZlY3RvciBjb250YWlucyB0aGUgSEVYIGNvZGVzIGZvciB0aGUgY29sb3JzLg0KDQpgYGB7cn0NCmJyZXdlci5wYWwoNiwiQnVHbiIpDQpgYGANCg0KXA0KDQpVc2luZyB0aGlzIHBhbGV0dGUgaW4gb3VyIG1hcCB5aWVsZHMgdGhlIGZvbGxvd2luZyByZXN1bHQuDQpgYGB7cn0NCnRtX3NoYXBlKHNhYy5tZXRyby50cmFjdHMudykgKw0KICB0bV9wb2x5Z29ucyhmaWxsID0gIm1lZGluY0UiLCANCiAgICAgICAgICAgICAgZmlsbC5zY2FsZSA9IHRtX3NjYWxlX2ludGVydmFscyhzdHlsZSA9ICJxdWFudGlsZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzPSJicmV3ZXIuYnVfZ24iKSkgDQpgYGANCg0KXA0KDQpUaGUgY29tbWFuZCBgZGlzcGxheS5icmV3ZXIucGFsKClgIGFsbG93cyB1cyB0byBleHBsb3JlIGRpZmZlcmVudCBjb2xvciBzY2hlbWVzIGJlZm9yZSBhcHBseWluZyB0aGVtIHRvIGEgbWFwLiBGb3IgZXhhbXBsZToNCmBgYHtyfQ0KZGlzcGxheS5icmV3ZXIucGFsKDYsIkJ1R24iKQ0KYGBgDQoNClwNCg0KIyMjIExlZ2VuZA0KDQpUaGVyZSBhcmUgbWFueSBvcHRpb25zIHRvIGNoYW5nZSB0aGUgZm9ybWF0dGluZyBvZiB0aGUgbGVnZW5kLiBUaGUgYXV0b21hdGljIHRpdGxlIGZvciB0aGUgbGVnZW5kIGlzIG5vdCB0aGF0IGF0dHJhY3RpdmUsIHNpbmNlIGl0IGlzIHNpbXBseSB0aGUgdmFyaWFibGUgbmFtZS4gVGhpcyBjYW4gYmUgY3VzdG9taXplZCBieSBzZXR0aW5nIHRoZSBgdGl0bGVgIGFyZ3VtZW50IGluIGB0bV9sZWdlbmQoKWAuDQoNCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIHRtX3BvbHlnb25zKGZpbGwgPSAibWVkaW5jRSIsIA0KICAgICAgICAgICAgICBmaWxsLnNjYWxlID0gdG1fc2NhbGVfaW50ZXJ2YWxzKHN0eWxlID0gInF1YW50aWxlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXMgPSAiYnJld2VyLnJlZHMiKSwgDQogICAgICAgICAgICAgIGZpbGwubGVnZW5kID0gdG1fbGVnZW5kKHRpdGxlID0gIk1lZGlhbiBJbmNvbWUiKSkgDQpgYGANCg0KXA0KDQpBbm90aGVyIGltcG9ydGFudCBhc3BlY3Qgb2YgdGhlIGxlZ2VuZCBpcyBpdHMgcG9zaXRpb25pbmcuIFRoaXMgaXMgaGFuZGxlZCB0aHJvdWdoIHRoZSBgdG1fbGF5b3V0KClgIGZ1bmN0aW9uLiBUaGlzIGZ1bmN0aW9uIGhhcyBhIHZhc3QgbnVtYmVyIG9mIG9wdGlvbnMsIGFzIGRldGFpbGVkIGluIHRoZSBbZG9jdW1lbnRhdGlvbl0oaHR0cHM6Ly93d3cucmRvY3VtZW50YXRpb24ub3JnL3BhY2thZ2VzL3RtYXAvdmVyc2lvbnMvMi4xLTEvdG9waWNzL3RtX2xheW91dCkuIFRoZXJlIGFyZSBhbHNvIHNwZWNpYWxpemVkIHN1YnNldHMgb2YgbGF5b3V0IGZ1bmN0aW9ucywgZm9jdXNlZCBvbiBzcGVjaWZpYyBhc3BlY3RzIG9mIHRoZSBtYXAsIHN1Y2ggYXMgYHRtX2xlZ2VuZCgpYCwgYHRtX3N0eWxlKClgIGFuZCBgdG1fZm9ybWF0KClgLiBXZSBpbGx1c3RyYXRlIHRoZSBwb3NpdGlvbmluZyBvZiB0aGUgbGVnZW5kLg0KDQpPZnRlbiwgdGhlIGRlZmF1bHQgbG9jYXRpb24gb2YgdGhlIGxlZ2VuZCBpcyBhcHByb3ByaWF0ZSwgYnV0IHNvbWV0aW1lcyBmdXJ0aGVyIGNvbnRyb2wgaXMgbmVlZGVkLiBUaGUgYGxlZ2VuZC5wb3NpdGlvbmAgYXJndW1lbnQgdG8gdGhlIGB0bV9sYXlvdXRgIGZ1bmN0aW9uIG1vdmVzIHRoZSBsZWdlbmQgYXJvdW5kIHRoZSBtYXAsIGFuZCBpdCB0YWtlcyBhIHZlY3RvciBvZiB0d28gc3RyaW5nIHZhcmlhYmxlcyB0aGF0IGRldGVybWluZSBib3RoIHRoZSBob3Jpem9udGFsIHBvc2l0aW9uICjigJxsZWZ04oCdLCDigJxyaWdodOKAnSwgb3Ig4oCcY2VudGVy4oCdKSBhbmQgdGhlIHZlcnRpY2FsIHBvc2l0aW9uICjigJx0b3DigJ0sIOKAnGJvdHRvbeKAnSwgb3Ig4oCcY2VudGVy4oCdKS4NCg0KRm9yIGV4YW1wbGUsIGlmIHdlIHdvdWxkIHdhbnQgdG8gbW92ZSB0aGUgbGVnZW5kIHRvIHRoZSBib3R0b20tcmlnaHQgcG9zaXRpb24sIHdlIHdvdWxkIHVzZSB0aGUgZm9sbG93aW5nIHNldCBvZiBjb21tYW5kcy4NCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogIHRtX3BvbHlnb25zKGZpbGwgPSAibWVkaW5jRSIsIA0KICAgICAgICAgICAgICBmaWxsLnNjYWxlID0gdG1fc2NhbGVfaW50ZXJ2YWxzKHN0eWxlID0gInF1YW50aWxlIiwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB2YWx1ZXMgPSAiYnJld2VyLnJlZHMiKSwNCiAgICAgICAgICAgICAgZmlsbC5sZWdlbmQgPSB0bV9sZWdlbmQodGl0bGUgPSAiTWVkaWFuIEluY29tZSIpKSArDQogIHRtX2xheW91dChsZWdlbmQucG9zaXRpb24gPSBjKCJyaWdodCIsICJib3R0b20iKSkNCmBgYA0KDQpcDQoNClRoZXJlIGlzIGFsc28gdGhlIG9wdGlvbiB0byBwb3NpdGlvbiB0aGUgbGVnZW5kIG91dHNpZGUgdGhlIGZyYW1lIG9mIHRoZSBtYXAuIFRoaXMgaXMgYWNjb21wbGlzaGVkIGJ5IHNldHRpbmcgYGxlZ2VuZC5vdXRzaWRlYCB0byBUUlVFLCBhbmQgb3B0aW9uYWxseSBhbHNvIHNwZWNpZnkgaXRzIHBvc2l0aW9uIGJ5IG1lYW5zIG9mIGBsZWdlbmQub3V0c2lkZS5wb3NpdGlvbigpYC4gVGhlIGxhdHRlciBjYW4gdGFrZSB0aGUgdmFsdWVzIOKAnHRvcOKAnSwg4oCcYm90dG9t4oCdLCDigJxyaWdodOKAnSwgYW5kIOKAnGxlZnTigJ0uDQoNCkZvciBleGFtcGxlLCB0byBwb3NpdGlvbiB0aGUgbGVnZW5kIG91dHNpZGUgYW5kIG9uIHRoZSByaWdodCwgd291bGQgYmUgYWNjb21wbGlzaGVkIGJ5IHRoZSBmb2xsb3dpbmcgY29tbWFuZHMuDQoNCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53KSArDQogdG1fcG9seWdvbnMoZmlsbCA9ICJtZWRpbmNFIiwgDQogICAgICAgICAgICAgIGZpbGwuc2NhbGUgPSB0bV9zY2FsZV9pbnRlcnZhbHMoc3R5bGUgPSAicXVhbnRpbGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlcyA9ICJicmV3ZXIucmVkcyIpLA0KICAgICAgICAgICAgICBmaWxsLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJNZWRpYW4gSW5jb21lIikpICsNCiAgdG1fbGF5b3V0KGxlZ2VuZC5vdXRzaWRlID0gVFJVRSwgbGVnZW5kLm91dHNpZGUucG9zaXRpb24gPSAicmlnaHQiKQ0KYGBgDQoNClwNCg0KV2UgY2FuIGFsc28gY3VzdG9taXplIHRoZSBzaXplIG9mIHRoZSBsZWdlbmQsIGl0cyBhbGlnbm1lbnQsIGZvbnQsIGV0Yy4gQ2hlY2sgb3V0IHRoZSBbZG9jdW1lbnRhdGlvbl0oaHR0cHM6Ly9yLXRtYXAuZ2l0aHViLmlvL3RtYXAvYXJ0aWNsZXMvYmFzaWNzX2xlZ2VuZHMpIGZvciBtb3JlIQ0KDQpcDQoNCiMjIyBUaXRsZQ0KDQpUaGUgYHRtX3RpdGxlKClgIGZ1bmN0aW9uIGNhbiBiZSB1c2VkIHRvIHNldCBhIHRpdGxlIGZvciB0aGUgbWFwLCBhbmQgc3BlY2lmeSBpdHMgcG9zaXRpb24sIHNpemUsIGV0Yy4gRm9yIGV4YW1wbGUsIHdlIGNhbiBzZXQgdGhlIGB0ZXh0YCwgdGhlIGBzaXplYCBhbmQgdGhlIGBwb3NpdGlvbmAgYXMgaW4gdGhlIGV4YW1wbGUgYmVsb3cuIFdlIG1hZGUgdGhlIGZvbnQgc2l6ZSBhIGJpdCBzbWFsbGVyICgwLjgpIGluIG9yZGVyIG5vdCB0byBvdmVyd2hlbG0gdGhlIG1hcCwgYW5kIHBvc2l0aW9uZWQgaXQgaW4gdGhlIHRvcCBsZWZ0LWhhbmQgY29ybmVyLg0KDQpgYGB7cn0NCnRtX3NoYXBlKHNhYy5tZXRyby50cmFjdHMudykgKw0KICB0bV9wb2x5Z29ucyhmaWxsID0gIm1lZGluY0UiLCANCiAgICAgICAgICAgICAgZmlsbC5zY2FsZSA9IHRtX3NjYWxlX2ludGVydmFscyhzdHlsZSA9ICJxdWFudGlsZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzID0gImJyZXdlci5yZWRzIiksDQogICAgICAgICAgICAgIGZpbGwubGVnZW5kID0gdG1fbGVnZW5kKHRpdGxlID0gIk1lZGlhbiBJbmNvbWUiKSkgKyANCiAgdG1fbGF5b3V0KGxlZ2VuZC5vdXRzaWRlID0gVFJVRSwgbGVnZW5kLm91dHNpZGUucG9zaXRpb24gPSAicmlnaHQiKSArDQogIHRtX3RpdGxlKHRleHQgPSAiTWVkaWFuIEluY29tZSBvZiBTYWNyYW1lbnRvIFRyYWN0cyIsDQogICAgICAgICAgIHNpemUgPSAwLjgsIA0KICAgICAgICAgICBwb3NpdGlvbiA9IGMoImxlZnQiLCJ0b3AiKSkNCmBgYA0KDQpcDQoNClRvIGhhdmUgYSB0aXRsZSBhcHBlYXIgb24gdG9wIChvciBvbiB0aGUgYm90dG9tKSBvZiB0aGUgbWFwLCB3ZSBuZWVkIHRvIHNldCB0aGUgYHBvc2l0aW9uYCBhcmd1bWVudCBvZiB0aGUgYHRtX3RpdGxlKClgIGZ1bmN0aW9uIHdpdGggdGhlIGFzc29jaWF0ZWQgYHRtX3Bvc19vdXQoKWAsIGFzIGlsbHVzdHJhdGVkIGJlbG93ICh3aXRoIHRpdGxlLnNpemUgc2V0IHRvIDEuMjUgdG8gaGF2ZSBhIGxhcmdlciBmb250KS4NCg0KYGBge3J9DQp0bV9zaGFwZShzYWMubWV0cm8udHJhY3RzLncpICsNCiAgdG1fcG9seWdvbnMoZmlsbCA9ICJtZWRpbmNFIiwgDQogICAgICAgICAgICAgIGZpbGwuc2NhbGUgPSB0bV9zY2FsZV9pbnRlcnZhbHMoc3R5bGUgPSAicXVhbnRpbGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlcyA9ICJicmV3ZXIucmVkcyIpLA0KICAgICAgICAgICAgICBmaWxsLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJNZWRpYW4gSW5jb21lIikpICsNCiAgdG1fbGF5b3V0KGxlZ2VuZC5vdXRzaWRlID0gVFJVRSwgbGVnZW5kLm91dHNpZGUucG9zaXRpb24gPSAicmlnaHQiKSArDQogIHRtX3RpdGxlKHRleHQgPSAiTWVkaWFuIEluY29tZSBvZiBTYWNyYW1lbnRvIFRyYWN0cyIsDQogICAgICAgICAgIHNpemUgPSAxLjI1LA0KICAgICAgICAgICBwb3NpdGlvbiA9IHRtX3Bvc19vdXQoImNlbnRlciIsICJ0b3AiKSkNCmBgYA0KDQpBIHNob3J0Y3V0IHRvIGFjaGlldmUgdGhpcyB0aXRsZSBwb3NpdGlvbiBpcyB0byB1c2UgYHRtX3RpdGxlX291dCgpYCBpbnN0ZWFkIG9mIGB0bV90aXRsZSgpYC4NCg0KXA0KDQojIyMgU2NhbGUgYmFyIGFuZCBhcnJvdw0KDQpPSyB0aGlzIHJlYWxseSB3b3VsZG4ndCBiZSBhIEdJUyBjbGFzcyB3aXRob3V0IHRhbGtpbmcgYWJvdXQgb25lIG9mIHRoZSBjb3JlIGVsZW1lbnRzIG9mIGEgbWFwLS10aGUgZ29vZCBvbGUgc2NhbGUgYmFyIGFuZCBhcnJvdy4gTGV0J3MgYWRkIHRoZXNlIHRvIG91ciBtYXAuIEZpcnN0LCB3ZSBhZGQgdGhlIHNjYWxlIGJhciB3aXRoIGB0bV9zY2FsZWJhcigpYC4NCg0KVGhlIGFyZ3VtZW50IGBicmVha3NgIHRlbGxzIFIgdGhlIGRpc3RhbmNlcyB0byBicmVhayB1cCBhbmQgZW5kIHRoZSBiYXIuIFRoZSBhcmd1bWVudCBgcG9zaXRpb25gIHBsYWNlcyB0aGUgc2NhbGUgYmFyIG9uIHRoZSBib3R0b20gbGVmdCBwYXJ0IG9mIHRoZSBtYXAuIE5vdGUgdGhhdCB0aGUgYHNjYWxlYCBpcyBpbiBtaWxlcyAod2UncmUgaW4gQW11cmljYSEpLiBUaGUgZGVmYXVsdCBpcyBpbiBraWxvbWV0ZXJzICh0aGUgcmVzdCBvZiB0aGUgd29ybGQhKSwgYnV0IHlvdSBjYW4gc3BlY2lmeSB0aGUgdW5pdHMgd2l0aGluIGB0bV9zaGFwZSgpYCB1c2luZyB0aGUgYXJndW1lbnQgYHVuaXRgLiBgdGV4dC5zaXplYCBzY2FsZXMgdGhlIHNpemUgb2YgdGhlIGJhciBzbWFsbGVyIChiZWxvdyAxKSBvciBsYXJnZXIgKGFib3ZlIDEpLg0KDQpgYGB7cn0NCnRtX3NoYXBlKHNhYy5tZXRyby50cmFjdHMudywgdW5pdCA9ICJtaSIpICsNCiAgdG1fcG9seWdvbnMoZmlsbCA9ICJtZWRpbmNFIiwgDQogICAgICAgICAgICAgIGZpbGwuc2NhbGUgPSB0bV9zY2FsZV9pbnRlcnZhbHMoc3R5bGUgPSAicXVhbnRpbGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlcyA9ICJicmV3ZXIucmVkcyIpLA0KICAgICAgICAgICAgICBmaWxsLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJNZWRpYW4gSW5jb21lIikpICsNCiAgdG1fbGF5b3V0KGxlZ2VuZC5vdXRzaWRlID0gVFJVRSwgbGVnZW5kLm91dHNpZGUucG9zaXRpb24gPSAicmlnaHQiKSArDQogIHRtX3RpdGxlX291dCh0ZXh0ID0gIk1lZGlhbiBJbmNvbWUgb2YgU2FjcmFtZW50byBUcmFjdHMiLA0KICAgICAgICAgICAgICAgc2l6ZSA9IDEuMjUpICsNCiAgdG1fc2NhbGViYXIoYnJlYWtzID0gYygwLCA1LCAxMCwgMjApLCB0ZXh0LnNpemUgPSAwLjc1LCBwb3NpdGlvbiA9IGMoImxlZnQiLCAiYm90dG9tIikpIA0KYGBgDQoNClwNCg0KTmV4dCBsZXQncyBzcGljZSB0aGluZ3MgdXAgYnkgYWRkaW5nIGEgbm9ydGggYXJyb3csIHdoaWNoIHdlIGNhbiBkbyB1c2luZyB0aGUgZnVuY3Rpb24gYHRtX2NvbXBhc3MoKWAuIFlvdSBjYW4gY29udHJvbCBmb3IgdGhlIHR5cGUsIHNpemUgYW5kIGxvY2F0aW9uIG9mIHRoZSBhcnJvdyB3aXRoaW4gdGhpcyBmdW5jdGlvbi4gSSBwbGFjZSBhIDQtc3RhciBhcnJvdyBvbiB0aGUgYm90dG9tIHJpZ2h0IG9mIHRoZSBtYXAuDQoNCmBgYHtyfQ0KdG1fc2hhcGUoc2FjLm1ldHJvLnRyYWN0cy53LCB1bml0ID0gIm1pIikgKw0KICB0bV9wb2x5Z29ucyhmaWxsID0gIm1lZGluY0UiLCANCiAgICAgICAgICAgICAgZmlsbC5zY2FsZSA9IHRtX3NjYWxlX2ludGVydmFscyhzdHlsZSA9ICJxdWFudGlsZSIsDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzID0gImJyZXdlci5yZWRzIiksDQogICAgICAgICAgICAgIGZpbGwubGVnZW5kID0gdG1fbGVnZW5kKHRpdGxlID0gIk1lZGlhbiBJbmNvbWUiKSkgKw0KICB0bV9sYXlvdXQobGVnZW5kLm91dHNpZGUgPSBUUlVFLCBsZWdlbmQub3V0c2lkZS5wb3NpdGlvbiA9ICJyaWdodCIpICsNCiAgdG1fdGl0bGVfb3V0KHRleHQgPSAiTWVkaWFuIEluY29tZSBvZiBTYWNyYW1lbnRvIFRyYWN0cyIsDQogICAgICAgICAgICAgICBzaXplID0gMS4yNSkgKw0KICB0bV9zY2FsZWJhcihicmVha3MgPSBjKDAsIDUsIDEwLCAyMCksIHRleHQuc2l6ZSA9IDAuNzUsIA0KICAgICAgICAgICAgICAgcG9zaXRpb24gPSBjKCJsZWZ0IiwgImJvdHRvbSIpKSArDQogIHRtX2NvbXBhc3ModHlwZSA9ICI0c3RhciIsIHBvc2l0aW9uID0gYygicmlnaHQiLCAiYm90dG9tIikpDQpgYGANCg0KXA0KDQpXZSBjYW4gYWxzbyBlbGltaW5hdGUgdGhlIGZyYW1lIGFyb3VuZCB0aGUgbWFwIHVzaW5nIHRoZSBhcmd1bWVudCBgZnJhbWUgPSBGQUxTRWAgaW4gYHRtX2xheW91dCgpYC4NCg0KYGBge3J9DQpzYWMubWFwIDwtIHRtX3NoYXBlKHNhYy5tZXRyby50cmFjdHMudywgdW5pdCA9ICJtaSIpICsNCiAgdG1fcG9seWdvbnMoZmlsbCA9ICJtZWRpbmNFIiwgDQogICAgICAgICAgICAgIGZpbGwuc2NhbGUgPSB0bV9zY2FsZV9pbnRlcnZhbHMoc3R5bGUgPSAicXVhbnRpbGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlcyA9ICJicmV3ZXIucmVkcyIpLA0KICAgICAgICAgICAgICBmaWxsLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJNZWRpYW4gSW5jb21lIikpICsNCiAgdG1fbGF5b3V0KGZyYW1lID0gRkFMU0UsDQogICAgICAgICAgICBsZWdlbmQub3V0c2lkZSA9IFRSVUUsIGxlZ2VuZC5vdXRzaWRlLnBvc2l0aW9uID0gInJpZ2h0IikgKw0KICB0bV90aXRsZSh0ZXh0ID0gIk1lZGlhbiBJbmNvbWUgb2YgU2FjcmFtZW50byBUcmFjdHMiLA0KICAgICAgICAgICBzaXplID0gMS4yNSkgKw0KICB0bV9zY2FsZWJhcihicmVha3MgPSBjKDAsIDUsIDEwLCAyMCksIHRleHQuc2l6ZSA9IDAuNzUsIHBvc2l0aW9uID0gYygibGVmdCIsICJib3R0b20iKSkgKw0KICB0bV9jb21wYXNzKHR5cGUgPSAiNHN0YXIiLCBwb3NpdGlvbiA9IGMoInJpZ2h0IiwgImJvdHRvbSIpKSANCiAgDQpzYWMubWFwDQpgYGANCg0KXA0KDQpOb3RlIHRoYXQgSSBzYXZlZCB0aGUgbWFwIGludG8gYW4gb2JqZWN0IGNhbGxlZCAqc2FjLm1hcCouIFIgaXMgYW4gb2JqZWN0LW9yaWVudGVkIHByb2dyYW0sIHNvIGV2ZXJ5dGhpbmcgeW91IG1ha2UgaW4gUiBhcmUgb2JqZWN0cyB0aGF0IGNhbiBiZSBzYXZlZCBmb3IgZnV0dXJlIG1hbmlwdWxhdGlvbi4gVGhpcyBpbmNsdWRlcyBtYXBzLiBBbmQgZnV0dXJlIG1hbmlwdWxhdGlvbnMgb2YgYSBzYXZlZCBtYXAgaW5jbHVkZXMgYWRkaW5nIG1vcmUgYHRtXypgIGZ1bmN0aW9ucyB0byB0aGUgc2F2ZWQgb2JqZWN0LCBzdWNoIGFzIGBzYWMubWFwICsgdG1fbGF5b3V0KHlvdXIgY2hhbmdlcyBoZXJlKWAuIENoZWNrIHRoZSBoZWxwIGRvY3VtZW50YXRpb24gZm9yIGB0bV9sYXlvdXQoKWAgdG8gc2VlIHRoZSBjb21wbGV0ZSBsaXN0IG9mIHNldHRpbmdzLiANCg0KXA0KDQojIyBTYXZpbmcgbWFwcw0KDQpZb3UgY2FuIHNhdmUgeW91ciBtYXBzIGEgZmV3IHdheXMuIA0KMS4gT24gdGhlIHBsb3R0aW5nIHNjcmVlbiB3aGVyZSB0aGUgbWFwIGlzIHNob3duLCBjbGljayBvbiBFeHBvcnQgYW5kIHNhdmUgaXQgYXMgZWl0aGVyIGFuIGltYWdlIG9yIHBkZiBmaWxlLg0KMi4gVXNlIHRoZSBmdW5jdGlvbiBgdG1hcF9zYXZlKClgDQoNCkZvciBvcHRpb24gMiwgd2UgY2FuIHNhdmUgdGhlIG1hcCBvYmplY3QgKnNhYy5tYXAqIGFzIHN1Y2g6DQpgYGB7ciwgbWVzc2FnZSA9IEZBTFNFfQ0KdG1hcF9zYXZlKHNhYy5tYXAsICJzYWNfY2l0eV9pbmMuanBnIikNCmBgYA0KU3BlY2lmeSB0aGUgKip0bWFwKiogb2JqZWN0IGFuZCBhIGZpbGVuYW1lIHdpdGggYW4gZXh0ZW5zaW9uLiBJdCBzdXBwb3J0cyAucGRmLCAuZXBzLCAuc3ZnLCAud21mLCAucG5nLCAuanBnLCAuYm1wIGFuZCAudGlmZi4gVGhlIGRlZmF1bHQgaXMgLnBuZy4gQWxzbyBtYWtlIHN1cmUgeW914oCZdmUgc2V0IHlvdXIgd29ya2luZyBkaXJlY3RvcnkgdG8gdGhlIGZvbGRlciB0aGF0IHlvdSB3YW50IHlvdXIgbWFwIHRvIGJlIHNhdmVkIGluLg0KDQpcDQoNCiMjIE1ha2luZyBhIG1hcCB3aXRoIENEQyBQbGFjZXMgZGF0YSANCg0KT0ssIGRvIHdlIGhhdmUgZW5lcmd5IGZvciBvbmUgbW9yZSBleGFtcGxlPyBMZXQncyBicmluZyBpbiBkYXRhIGZyb20gdGhlIFtDREMgUGxhY2VzIGRhdGFzZXRdKGh0dHBzOi8vd3d3LmNkYy5nb3YvcGxhY2VzL3Rvb2xzL2RhdGEtcG9ydGFsLmh0bWwpLiBUaGlzIGlzIGFuIGluY3JlZGlibGUgcmVzb3VyY2UgdG8gYWNjZXNzIGRhdGEgb24gdGhlIENEQydzIFtCZWhhdmlvcmFsIFJpc2sgRmFjdG9yIGFuZCBTdXJ2ZWlsbGFuY2UgU3lzdGVtIChCUkZTUyldKGh0dHBzOi8vd3d3LmNkYy5nb3YvYnJmc3MvaW5kZXguaHRtbCksIGFzIHdlbGwgYXMgc29jaWFsIGRldGVybWluYW50cyBvZiBoZWFsdGggZGF0YSBmcm9tIEFtZXJpY2FuIENvbW11bml0eSBTdXJ2ZXkuIEkndmUgYWxyZWFkeSBkb3dubG9hZGVkIENhbGlmb3JuaWEgQ2Vuc3VzIHRyYWN0IGRhdGEgb24gdGhlICIlIG9mIGFkdWx0cyByZXBvcnRpbmcgbm8gbGVpc3VyZS10aW1lIHBoeXNpY2FsIGFjdGl2aXR5Ii4gTGV0J3MgYnJpbmcgdGhpcyBpbiBhbmQgdGFrZSBhIGxvb2sgYXQgaXQhDQoNCmBgYHtyfQ0KcGxhY2VzX2NhX2xwYSA8LSByZWFkX2NzdigiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3BqYW1lcy11Y2RhdmlzL1NQSDIxNS9yZWZzL2hlYWRzL21haW4vcGxhY2VzX2NhX2xwYS5jc3YiKQ0KZ2xpbXBzZShwbGFjZXNfY2FfbHBhKQ0KYGBgDQoNClwNCg0KSW50ZXJlc3Rpbmcgc3R1ZmYuIExvb2tzIGxpa2UgdGhlIHZhbHVlcyB3ZSBjYXJlIGFib3V0IGFyZSBzdG9yZXMgaW4gYSBjb2x1bW4gY2FsbGVkICpEYXRhX1ZhbHVlKiBhbmQgdGhlIEZJUFMgY29kZSBzZWVtcyB0byBiZSBpbiAqTG9jYXRpb25OYW1lKi4gTGV0J3MgZ28gYWhlYWQgYW5kIHJlbmFtZSAqTG9jYXRpb25OYW1lKiBhbmQgdGhlbiBzZWUgaWYgd2UgY2FuIGpvaW4gdGhpcyBkYXRhIHdpdGggb3VyIENlbnN1cyB0cmFjdCBkYXRhICpjYS50cmFjdHMqLg0KDQpgYGB7cn0NCnBsYWNlc19jYV9scGE8LXJlbmFtZShwbGFjZXNfY2FfbHBhLCBHRU9JRCA9IExvY2F0aW9uTmFtZSkNCmdsaW1wc2UocGxhY2VzX2NhX2xwYSkNCg0KY2EudHJhY3RzLmxwYSA8LSBjYS50cmFjdHMgJT4lDQogIGxlZnRfam9pbihwbGFjZXNfY2FfbHBhLCBieSA9ICJHRU9JRCIpDQpnbGltcHNlKGNhLnRyYWN0cy5scGEpDQpgYGANCg0KXA0KDQpJdCB3b3JrZWQhIE9LLCBub3cgbGV0J3MgbWFrZSBhIG1hcCBvZiAlIG9mIGFkdWx0cyByZXBvcnRpbmcgbm8gbGVpc3VyZS10aW1lIHBoeXNpY2FsIGFjdGl2aXR5LiBXZSB3aWxsIGJ1aWxkIG9uIHdoYXQgd2UndmUgbGVhcm5lZCBhYm92ZSEgSSd2ZSBhbHNvIHVzZWQgYHRtX2ZpbGwoKWAgaW5zdGVhZCBvZiBgdG1fcG9seWdvbnMoKWAgdG8gbWFrZSBpdCBzbyB0aGVyZSBhcmUgbm8gYm9yZGVycyBhbmQgd2UgY2FuIGVhc2lseSBzZWUgcG9seWdvbiB2YWx1ZXMgaW4gZGVuc2VyIGNvbmNlbnRyYXRpb25zIG9mIENlbnN1cyB0cmFjdHMgKGUuZy4sIGFyb3VuZCBTYWNyYW1lbnRvLCBTYW4gRnJhbmNpc2NvLCBMQSwgZXRjLikNCg0KYGBge3J9DQp0bV9zaGFwZShjYS50cmFjdHMubHBhLCB1bml0ID0gIm1pIikgKw0KICB0bV9maWxsKGZpbGwgPSAiRGF0YV9WYWx1ZSIsIA0KICAgICAgICAgIGZpbGwuc2NhbGUgPSB0bV9zY2FsZV9pbnRlcnZhbHMoc3R5bGUgPSAicXVhbnRpbGUiLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWVzID0gImJyZXdlci5yZWRzIiksDQogICAgICAgICAgZmlsbC5sZWdlbmQgPSB0bV9sZWdlbmQodGl0bGUgPSAiJSBBZHVsdHMgTm8gUGh5c2ljYWwgQWN0aXZpdHkiKSkgKw0KICB0bV90aXRsZV9vdXQodGV4dCA9ICIlIG9mIEFkdWx0cyBSZXBvcnRpbmcgTm8gTGVpc3VyZSBUaW1lIFBoeXNpY2FsIEFjdGl2aXR5IikNCmBgYA0KXA0KDQpPb29vb29vbyB0aGF0IGlzIG9uZSBnb29vb29kIGxvb2tpbmcgbWFwIQ0KDQpcDQoNCllvdeKAmXZlIGNvbXBsZXRlZCB5b3VyIGludHJvZHVjdGlvbiB0byAqKnNmKiouIFdoZXchIEJhZGdlPyBZZXMsIHBsZWFzZSwgeW91IGVhcm5lZCBpdCEgVGltZSB0byBbY2VsZWJyYXRlXShodHRwczovL3d3dy55b3V0dWJlLmNvbS93YXRjaD92PTNHd2pmVUZ5WTZNKSENCg0KXA0KDQohW3NmIEJhZGdlXShzZi5naWYpDQoNClwNCg0KIyBSYXN0ZXJzDQoNClJhc3RlciBkYXRhc2V0cyBhcmUgc2ltcGx5IGFuIGFycmF5IG9mIHBpeGVscy9jZWxscyBvcmdhbml6ZWQgaW50byByb3dzIGFuZCBjb2x1bW5zIChvciBhIGdyaWQpIHdoZXJlIGVhY2ggY2VsbCBjb250YWlucyBhIHZhbHVlIHJlcHJlc2VudGluZyBpbmZvcm1hdGlvbiwgc3VjaCBhcyB0ZW1wZXJhdHVyZSwgdmVnZXRhdGlvbiwgbGFuZCB1c2UsIGFpciBwb2xsdXRpb24sIGV0Yy4gUmFzdGVyIG1hcHMgdXN1YWxseSByZXByZXNlbnQgY29udGludW91cyBwaGVub21lbmEgc3VjaCBhcyBlbGV2YXRpb24sIHRlbXBlcmF0dXJlLCBvciBwb3B1bGF0aW9uIGRlbnNpdHkuIERpc2NyZXRlIGZlYXR1cmVzIHN1Y2ggYXMgc29pbCB0eXBlIG9yIGxhbmQtY292ZXIgY2xhc3NlcyBjYW4gYWxzbyBiZSByZXByZXNlbnRlZCBpbiB0aGUgcmFzdGVyIGRhdGEgbW9kZWwuIFJhc3RlcnMgYXJlIGFlcmlhbCBwaG90b2dyYXBocywgaW1hZ2VyeSBmcm9tIHNhdGVsbGl0ZXMsIEdvb2dsZSBTdHJlZXQgVmlldyBpbWFnZXMsIGV0Yy4gQSBmZXcgdGhpbmdzIHRvIG5vdGUuDQoNCiAtIFJhc3RlciBkYXRhc2V0cyBhcmUgYWx3YXlzIHJlY3Rhbmd1bGFyIChyb3dzIHggY29sKSBzaW1pbGFyIHRvIG1hdHJpY2VzLiBJcnJlZ3VsYXIgYm91bmRhcmllcyBhcmUgY3JlYXRlZCBieSB1c2luZyBgTkFzYC4NCiAtIFJhc3RlcnMgaGF2ZSB0byBjb250YWluIHZhbHVlcyBvZiB0aGUgc2FtZSB0eXBlIChpbnQsIGZsb2F0LCBib29sZWFuKSB0aHJvdWdob3V0IHRoZSByYXN0ZXIsIGp1c3QgbGlrZSBtYXRyaWNlcyBhbmQgdW5saWtlIGRhdGEgZnJhbWVzLg0KIC0gVGhlIHNpemUgb2YgdGhlIHJhc3RlciBkZXBlbmRzIG9uIHRoZSAqKnJlc29sdXRpb24qKiBhbmQgdGhlICoqZXh0ZW50Kiogb2YgdGhlIHJhc3Rlci4gQXMgc3VjaCBtYW55IHJhc3RlcnMgYXJlIGxhcmdlIGFuZCBvZnRlbiBjYW5ub3QgYmUgaGVsZCBpbiBtZW1vcnkgY29tcGxldGVseS4NCiANCiANClRoZSB3b3JraG9yc2UgcGFja2FnZSBmb3Igd29ya2luZyB3aXRoIHJhc3RlcnMgaW4gUiBpcyB0aGUgKip0ZXJyYSoqIHBhY2thZ2UgYnkgUm9iZXJ0IEhpam1hbnMuICoqdGVycmEqKiBoYXMgZnVuY3Rpb25zIGZvciBjcmVhdGluZywgcmVhZGluZywgbWFuaXB1bGF0aW5nLCBhbmQgd3JpdGluZyByYXN0ZXIgZGF0YS4gVGhlIHBhY2thZ2UgYWxzbyBpbXBsZW1lbnRzIHJhc3RlciBhbGdlYnJhIGFuZCBtYW55IG90aGVyIGZ1bmN0aW9ucyBmb3IgcmFzdGVyIGRhdGEgbWFuaXB1bGF0aW9uLiBUaGUgcGFja2FnZSB3b3JrcyB3aXRoIGBTcGF0UmFzdGVyYCBvYmplY3RzLiBUaGUgYHJhc3QoKWAgZnVuY3Rpb24gaXMgdXNlZCB0byBjcmVhdGUgdGhlc2Ugb2JqZWN0cy4gDQoNClR5cGljYWxseSB5b3Ugd2lsbCBicmluZyBpbiBhIHJhc3RlciBkYXRhc2V0IGRpcmVjdGx5IGZyb20gYSBmaWxlLiBUaGVzZSBmaWxlcyBjb21lIGluIG1hbnkgZGlmZmVyZW50IGZvcm1zLCB0eXBpY2FsbHkgLnRpZiwgLmltZywgYW5kIC5ncmQuDQoNClwNCg0KV2XigJlsbCBicmluZyBpbiB0aGUgZmlsZSBORFZJX3Jhc3QudGlmLiBUaGUgZmlsZSBjb250YWlucyBub3JtYWxpemVkIGRpZmZlcmVuY2UgdmVnZXRhdGlvbiBpbmRleCAoTkRWSSkgZGF0YSBmb3IgdGhlIEJheSBBcmVhLiBUaGVzZSBkYXRhIGFyZSB0YWtlbiBmcm9tIExhbmRzYXQgc2F0ZWxsaXRlIGRhdGEgdGhhdCBJIGRvd25sb2FkZWQgZnJvbSBHb29nbGUgRWFydGggRW5naW5lLiBXZSB1c2UgdGhlIGZ1bmN0aW9uIGByYXN0KClgIHRvIGJyaW5nIGluIGRhdGEgaW4gcmFzdGVyIGZvcm0sIHRoZW4gdGFrZSBhIGxvb2sgYXQgdGhlIGRhdGFzZXQuDQoNCmBgYHtyfQ0KDQoNCnVybCA8LSAiaHR0cHM6Ly9naXRodWIuY29tL3BqYW1lcy11Y2RhdmlzL1NQSDIxNS9yYXcvbWFpbi9ORFZJX3Jhc3QyLnRpZiINCmRvd25sb2FkLmZpbGUodXJsLCBkZXN0ZmlsZSA9ICJORFZJX3Jhc3QyLnRpZiIsIG1vZGUgPSAid2IiKQ0KTkRWSV9yYXN0ZXIgPSByYXN0KCJORFZJX3Jhc3QyLnRpZiIpDQoNCiMjIEdldCBzdW1tYXJ5IG9mIHJhc3RlciBkYXRhDQpORFZJX3Jhc3Rlcg0KYGBgDQoNClwNCg0KRG9lcyBpdCBoYXZlIGEgQ1JTPw0KYGBge3J9DQoNCiMjIENoZWNrIENSUw0Kc3RfY3JzKE5EVklfcmFzdGVyKQ0KDQpgYGANCg0KXA0KDQpPSyB3ZSBoYXZlIHdoYXQgbG9va3MgbGlrZSBhIHJhc3Rlci4gV2Ugc2VlIG91ciByZXNvbHV0aW9uIGFuZCBvdXIgZXh0ZW50LCBhbmQgd2UgaGF2ZSBhIENSUy4gTmljZSEgU2hhbGwgd2UgcGxvdCB0aGlzPw0KDQpgYGB7cn0NCiMjIFBsb3QgdGhlIHJhc3RlciBvbiBhIG1hcA0KdG1hcF9tb2RlKCJwbG90IikNCk5EVklfbWFwIDwtICB0bV9zaGFwZShORFZJX3Jhc3RlcikgKw0KICB0bV9yYXN0ZXIoY29sLnNjYWxlID0gdG1fc2NhbGVfY29udGludW91cygpLA0KICAgICAgICAgICAgY29sLmxlZ2VuZCA9IHRtX2xlZ2VuZChvdXRzaWRlID0gVFJVRSkpDQpORFZJX21hcA0KYGBgDQoNClwNCg0KTG9va3MgcHJldHR5IGNvb2whIFNlZW1zIHRvIGJlIHRoZSBCYXkgQXJlYSwgYW5kIHdlIGhhdmUgc29tZSBuaWNlIHZhcmlhYmlsaXR5LiBCdXQgd2UgbGV0J3Mgc2VlIGlmIHdlIGNhbiBtYWtlIHRoaXMgZmFuY2llci4NCg0KYGBge3J9DQojIHBhbGV0dGUgZm9yIHBsb3R0aW5nDQpicmVha3NfbmR2aSA8LSBjKC0xLC0wLjIsLTAuMSwwLDAuMDI1ICwwLjA1LDAuMDc1LDAuMSwwLjEyNSwwLjE1LDAuMTc1LDAuMiAsMC4yNSAsMC4zICwwLjM1LDAuNCwwLjQ1LDAuNSwwLjU1LDAuNiwxKQ0KcGFsZXR0ZV9uZHZpIDwtIGMoIiNCRkJGQkYiLCIjREJEQkRCIiwiI0ZGRkZFMCIsIiNGRkZBQ0MiLCIjRURFOEI1IiwiI0RFRDk5QyIsIiNDQ0M3ODIiLCIjQkRCODZCIiwiI0IwQzI2MSIsIiNBM0NDNTkiLCIjOTFCRjUyIiwiIzgwQjM0NyIsIiM3MEEzNDAiLCIjNjE5NjM2IiwiIzRGOEEyRSIsIiM0MDdEMjQiLCIjMzA2RTFDIiwiIzIxNjExMiIsIiMwRjU0MEEiLCIjMDA0NTAwIikNCg0KTkRWSV9tYXAgPC0gIHRtX3NoYXBlKE5EVklfcmFzdGVyKSArDQogIHRtX3Jhc3Rlcihjb2wuc2NhbGUgPSB0bV9zY2FsZV9jb250aW51b3VzKHZhbHVlcyA9IHBhbGV0dGVfbmR2aSksDQogICAgICAgICAgICBjb2wubGVnZW5kID0gdG1fbGVnZW5kKHRpdGxlID0gIk5EVkkiLCBvdXRzaWRlID0gVFJVRSkpDQpORFZJX21hcA0KYGBgDQoNClwNCg0KIyMgQ3JvcA0KDQpPSywgbGV0J3Mgc2VlIGlmIHdlIGNhbiBjcm9wIHRoaXMgdG8gZm9jdXMgb24gU2FuIEZyYW5jaXNjby4gSSd2ZSBnb29nbGVkIHRoZSBsYXQgYW5kIGxvbmcgZm9yIHRoZSBhcmVhIGFyb3VuZCBTYW4gRnJhbmNpc2NvLCBhbmQgSSdsbCBwdXQgdGhlc2UgcmlnaHQgaW50byBteSBgY3JvcCgpYCBmdW5jdGlvbi4gSSBjYW4gdXNlIHRoZSBgdG1hcF9tb2RlKCJ2aWV3IilgIG5vdyBiZWNhdXNlIHRoZSByYXN0ZXIgaXMgc21hbGwgZW5vdWdoIGZvciBSIHRvIG1ha2UgaW50ZXJhY3RpdmUuDQoNCmBgYHtyfQ0Kc2ZfcmFzdDwtY3JvcChORFZJX3Jhc3RlciwgZXh0KC0xMjIuNTUsIC0xMjIuMzUsIDM3LjcsIDM3LjgzKSkNCg0KdG1hcF9tb2RlKCJwbG90IikNCk5EVklfc2ZfbWFwIDwtIHRtX3NoYXBlKHNmX3Jhc3QpICsNCiAgdG1fcmFzdGVyKGNvbC5zY2FsZSA9IHRtX3NjYWxlX2NvbnRpbnVvdXMoKSwNCiAgICAgICAgICAgIGNvbC5sZWdlbmQgPSB0bV9sZWdlbmQodGl0bGUgPSAiTkRWSSIsIG91dHNpZGUgPSBUUlVFKSkNCg0KTkRWSV9zZl9tYXANCmBgYA0KDQpcDQoNCiMjIENsYXNzaWZ5DQoNClNvIG5lZ2F0aXZlIHZhbHVlcyBvZiBORFZJIHJlcHJlc2VudCB3YXRlci4gTGV0J3Mgc2V0IGFsbCBuZWdhdGl2ZSB2YWx1ZXMgdG8gLTEsIGFuZCB0aGF0IHdpbGwgaGVscCB1cyB0byBkaXN0aW5ndWlzaCB3YXRlciBmcm9tIGxhbmQgZWFzaWVyLg0KDQpgYGB7cn0NCnNmX3Jhc3RfbmVnIDwtIGFwcChzZl9yYXN0LCBmdW49ZnVuY3Rpb24oeCl7IHhbeCA8PSAwXSA8LSAtMTsgcmV0dXJuKHgpfSApDQoNCk5EVklfc2ZfbWFwX25lZyA8LSB0bV9zaGFwZShzZl9yYXN0X25lZykgKw0KICB0bV9yYXN0ZXIoY29sLnNjYWxlID0gdG1fc2NhbGVfY29udGludW91cygpLA0KICAgICAgICAgICAgY29sLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJORFZJIiwgb3V0c2lkZSA9IFRSVUUpKQ0KDQpORFZJX3NmX21hcF9uZWcNCmBgYA0KDQpcDQoNCk5pY2UuIFdoYXQgZG8gd2Ugbm90aWNlPyBUaGUgY29hc3Qgb2YgU2FuIEZyYW5jaXNjbyBtaWdodCBoYXZlIHNvbWUgY2xvdWQgY292ZXIgZXJyb3JzISBTYXRlbGxpdGUgZGF0YSBpc24ndCBwZXJmZWN0ISBCdXQgdGhlIGdvb2QgbmV3cyBpcywgd2UndmUgbGVhcm5lZCBob3cgdG8gYnJpbmcgaW4gcmFzdGVyIGRhdGEhIEkgdGhpbmsgd2UgbmVlZCBhIGJhZGdlISEhIQ0KDQpcDQoNCiFbdGVycmEgQmFkZ2VdKHRlcnJhLnBuZykNCg0KXA0KDQpPbmUgbGFzdCBzdGVwLiBMZXQncyBwdXQgYWxsIG91ciBrbm93bGVkZ2UgdG9nZXRoZXIgYW5kIG1hcCBvdXIgb2xkIGZyaWVuZCBDQWRhdGEgb24gdG9wIG9mIHRoZSBORFZJIGRhdGEgaW4gU2FuIEZyYW5jaXNjby4gRmlyc3QsIGxldCdzIGJyaW5nIGluIHRoZSBDQWRhdGEgZGF0YXNldCBvbiBvdmFyaWFuIGNhbmNlciBjYXNlcyBhZ2Fpbi4gDQoNCmBgYHtyfQ0KDQpkYXRhKENBZGF0YSkNCmNhX3B0cyA8LSBDQWRhdGENCmNhX3Byb2ogPC0gIitwcm9qPWxjYyArbGF0XzE9NDAgK2xhdF8yPTQxLjY2NjY2NjY2NjY2NjY2IA0KICAgICAgICAgICAgICtsYXRfMD0zOS4zMzMzMzMzMzMzMzMzNCArbG9uXzA9LTEyMiAreF8wPTIwMDAwMDAgDQogICAgICAgICAgICAgK3lfMD01MDAwMDAuMDAwMDAwMDAwMiArZWxscHM9R1JTODAgDQogICAgICAgICAgICAgK2RhdHVtPU5BRDgzICt1bml0cz1tICtub19kZWZzIg0KDQpjYV9wdHMgPC0gc3RfYXNfc2YoQ0FkYXRhLCBjb29yZHM9YygiWCIsIlkiKSwgY3JzPWNhX3Byb2opDQpgYGANCg0KXA0KDQpMZXQncyBjaGVjayB0aGUgQ1JTIGFuZCBjb21wYXJlIGl0IHRvIG91ciBORFZJIGRhdGFzZXQuDQoNCmBgYHtyfQ0Kc3RfY3JzKGNhX3B0cykNCnN0X2NycyhzZl9yYXN0X25lZykNCmBgYA0KDQpcDQoNCkhtbW0sIGxldCdzIG1ha2Ugc3VyZSB0aGV5IGFyZSB0aGUgc2FtZSBwcm9qZWN0aW9uLg0KDQpgYGB7cn0NCmNhX3B0c19wcm9qIDwtIHN0X3RyYW5zZm9ybShjYV9wdHMsc3RfY3JzKHNmX3Jhc3RfbmVnKSkNCnN0X2NycyhjYV9wdHNfcHJvaikNCmBgYA0KDQpcDQoNClRoZXkgc2hvdWxkIGJlIGdvb2QgdG8gZ28gbm93LiBMZXQncyBtYXAgdGhlc2UgYWRkcmVzc2VzIG9uIHRvcCBvZiB0aGUgcmFzdGVyIGRhdGEhDQoNCmBgYHtyfQ0KdG1hcF9tb2RlKCJwbG90IikNCk5EVklfY2FuY2VyX21hcCA8LSAgdG1fc2hhcGUoc2ZfcmFzdF9uZWcpICsNCiAgdG1fcmFzdGVyKGNvbC5zY2FsZSA9IHRtX3NjYWxlX2NvbnRpbnVvdXMoKSwNCiAgICAgICAgICAgIGNvbC5sZWdlbmQgPSB0bV9sZWdlbmQodGl0bGUgPSAiTkRWSSIsIG91dHNpZGUgPSBUUlVFKSkgKw0KICB0bV9zaGFwZShjYV9wdHNfcHJvaikgKw0KICB0bV9kb3RzKGZpbGwgPSAiYmx1ZSIsIGZpbGxfYWxwaGEgPSAwLjUsIHNpemUgPSAwLjMpDQoNCk5EVklfY2FuY2VyX21hcA0KYGBgDQoNClwNCg0KVGhhdCBpcyBvbmUgZmluZSBsb29raW5nIG1hcC4gWW91IGRlc2VydmUgYSBicmVhay0tZ2V0IG91dHNpZGUgYW5kIGVuam95IHNvbWUgZ3JlZW5zcGFjZS4gDQoNCg0KDQo=