Hay dos grandes tipos de datos espaciales: grillas regulares y grillas irregulares.

Grillas regulares

En la segunda sección de lectura de datos viste cómo leer archivos NetCDF:

temperatura <- ReadNetCDF("datos/temperatura.nc", vars = "air",
                          subset = list(level = 1000,
                                        lat = c(-55, -20),
                                        lon = c(280, 310))) 
glimpse(temperatura)
## Rows: 195
## Columns: 5
## $ time  <dttm> 2010-07-09, 2010-07-09, 2010-07-09, 2010-07-09, 2010-07-09, 201…
## $ level <dbl> 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000…
## $ lat   <dbl> -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -20.0, -…
## $ lon   <dbl> 280.0, 282.5, 285.0, 287.5, 290.0, 292.5, 295.0, 297.5, 300.0, 3…
## $ air   <dbl> 287.62, 287.70, 289.72, 293.25, 296.32, 297.30, 296.57, 296.15, …

La variable temperatura es una tabla que tiene dato de temperatura del aire para cada tiempo, nivel (altura), latitud y longitud. Como este archivo tiene información sólo para un tiempo y el código filtró sólo datos de un nivel, entonces la variación es sólo en latitud y longitud. ¿Cómo se grafica?

Se puede dibujar un punto por cada dato:

ggplot(temperatura, aes(lon, lat)) +
  geom_point()

Esto no dice nada, aunque podemos confirmar que los datos están en una grilla regular. Para visualizar la temperatura, hay que mapear éste valor a un parámetro estético, por ejemplo, el color:

ggplot(temperatura, aes(lon, lat)) +
  geom_point(aes(color = air))

Puntos redondos y separados no es la forma más informativa de ver estos datos. Una grilla regular en realidad define como una “imagen” donde cada punto es un pixel. Esto se llama un “raster” y con ggplot se puede graficar con geom_raster().

ggplot(temperatura, aes(lon, lat)) +
  geom_raster(aes(fill = air))

geom_raster() dibuja rectángulos de igual ancho y alto en cada x, y. El ancho y el alto se definen a través de la resolución de los datos. Funcionan perfectamente para grillas regulares. Estos rectángulos tienen un color interno, por lo que el parámetro estético se llama “fill”.

Desafío

ggplot2 tiene tres funciones para generar rectángulos: geom_raster(), geom_tile() y geom_rect(). ¿Cuál es la diferencia entre cada uno? Fijate en la documentación.

Mapas

Bien, hasta ahí tenés un gráfico con los datos, pero para entenderlos en su contexto espacial como mínimo hace falta un mapa. Para tener un mapa se necesitan datos de la localización de las costas o las fronteras. Existen varias fuentes de estos datos, pero una muy buena es Natural Earth. El paquete {rnaturalearth} provee una interfaz amigable para usar estos datos directamente en {ggplot2}.

Primero hay que cargar el mapa que queramos. Por ejemplo, para graficar el mapa de Argentina y sus países limítrofes cargamos los datos con ne_countries():

mapa <- rnaturalearth::ne_countries(country = c("argentina", "chile", "uruguay", 
                                                "paraguay", "brazil", "bolivia", 
                                                "falkland islands"), 
                                    returnclass = "sf")

El argumento country es un vector con los países que necesitamos. El argumento returnclass es un poco técnico, pero hace referencia a la estructura que queremos que devuelva. En este caso, returnclass = "sf" hace que devuelva un objeto de clase “Simple Features”. Las “Simple Features” son cualquier cosa menos simple internamente pero con {ggplot2} se pueden graficar con un geom específico:

ggplot(mapa) +
  geom_sf()

Por defecto, el mapa se dibuja con un fondo gris, pero el problema es ese fondo va a tapar los datos! Paro para dibujar sólo los contornos hay que modificarlo un poco:

ggplot(mapa) +
  geom_sf(fill = NA, color = "black", size = 0.2)

Cuando uno usa mucho un mapa, muchas veces termina siendo útil guardar el geom por separado en una variable.

mi_mapa <- geom_sf(data = mapa, inherit.aes = FALSE, fill = NA, color = "black", size = 0.2) 

Además de las modificaciones estéticas, esta llamada a geom_sf tiene el argumento data = mapa, ya que no va a usar los datos “globales” de la llamada a ggplot(). También tiene ìnherit.aes = FALSE porque tampoco tiene que “heredar” los aes globales.

Entonces ahora, para graficar la temperatura y el mapa encima, armamos el gráfico de raster igual que antes y le sumamos el mapa:

ggplot(temperatura, aes(lon, lat)) +
  geom_raster(aes(fill = air)) +
  mi_mapa

Desafío

¿Podés explicar qué salió mal?

Existen dos convenciones básicas para medir la longitud, medida entre 0º y 360º o entre -180º y +180º. Los datos de temperatura usan la primera y los del mapa, la segunda. Para convertir entre una y otra convención, podés usar la función ConvertLongitude() de {metR}:

temperatura <- mutate(temperatura, lon = metR::ConvertLongitude(lon, from = 360))
ggplot(temperatura, aes(lon, lat)) +
  geom_raster(aes(fill = air)) +
  mi_mapa 

Lo único que le falta a este gráfico es lo que le sobra: todo el espacio donde no hay datos. Para recortar el área del gráfico y que tome sólo donde tenemos datos de temperatura, hay que especificar los límites del sistema de coordenadas. Como mi_mapa es un geom_sf, hay que usar coord_sf.

ggplot(temperatura, aes(lon, lat)) +
  geom_raster(aes(fill = air)) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Otra forma de graficar datos espaciales es pensarlos como una superficie tridimensional donde el x y el y son las coordenadas y la altura (la coordenada z) es proporcional al valor de la variable. Imaginándoselo así, este gráfico de temperatura sería una montaña con su cima en Paraguay y su punto más bajo cerca de Chubut.

Precisamente para esta situación están pensadas las líneas de contorno que se pueden graficar en {ggplot2} usando la función geom_contour().

ggplot(temperatura, aes(lon, lat)) +
  geom_contour(aes(z = air)) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Las líneas dibujadas por geom_contour() son líneas de nivel que unen puntos de valor constante. Sin embargo, esas líneas azules no dan ninguna indicación del valor de la variable por lo que es conveniente mapear el color de las líneas ese valor constante.

ggplot(temperatura, aes(lon, lat)) +
  geom_contour(aes(z = air, color = stat(level))) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

¿De dónde salió ese stat(level)? Algunos geoms que realizan transformaciones estadísticas computan variables nuevas que luego se pueden mapear a parámetros geométricos. “level” es una de esas variables computadas de geom_contour. Para decirle a ggplot que al level al que te referís es a la variable computada, se rodea a la variable con la función stat().

Una variedad de las líneas de contorno son las líneas “llenas”. Hasta hace poco, {ggplot2} no tenía una forma de graficar estos contornos llenos, así que está implementado en {metR} con la function geom_contour_fill(). Recientemente {ggplot2} implementó la función geom_contour_filled() pero a nosotros nos gusta más la versión de {metR} 😉

ggplot(temperatura, aes(lon, lat)) +
  metR::geom_contour_fill(aes(z = air)) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Por último, tranquilamente se pueden usar ambos geoms para resaltar los límites y

ggplot(temperatura, aes(lon, lat)) +
  metR::geom_contour_fill(aes(z = air)) +
  geom_contour(aes(z = air), color = "black") +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Grillas irregulares

¿Qué pasa cuando en vez de observaciones organizadas en una bella grilla regular, tenés observaciones puntuales en lugares dispersos? Si te acordás de la sección de uniones, en la carpeta datos hay un archivo con datos de temperatura del servicio meteorológico y metadatos con sus ubicaciones. Esto define datos espaciales de temperatura.

# Para trabajar con menos datos, nos quedamos con la temperatura máxima media 
# de cada estación.
observaciones <- readr::read_csv("datos/observaciones_smn.csv") %>% 
  group_by(station) %>% 
  summarise(tmax_media = mean(tmax, na.rm = TRUE))

estaciones <- read_csv("datos/estaciones_smn.csv") 

observaciones <- left_join(observaciones, estaciones, by = c("station" = "nombre")) %>% 
  filter(provincia != "ANTARTIDA")

Lo primero que se puede hacer es graficar estos datos de estación con puntos, de la misma forma que para los datos regulares.

ggplot(observaciones, aes(lon, lat)) +
  geom_point(aes(color = tmax_media)) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Desafío

Graficá estos puntos usando geom_raster() y geom_contour(). ¿Cuál es el resultado?

Como estos datos no están en una grilla regular, geom_raster() y geom_contour() no los pueden graficar. Para usar contornos hay que interpolar a una grilla regular. Una forma de hacerlo es usando la técnica de kriging. Un paquete de R que la implementa es {kriging} en su función kriging

Si vas a la ayuda de kriging(), vas a ver que requiere un vector de coordenadas x, un vector de coordenadas y y un vector de valores observados. Estos son las columnas lon, lat y tmax_media de la tabla observaciones. Entonces el código sería:

observaciones_regular <- kriging::kriging(x = observaciones$lon, 
                                          y = observaciones$lat,
                                          response = observaciones$tmax_media)
## Error in onedim(response, n): NA/NaN/Inf in foreign function call (arg 1)

Este error indica que hay NAs, y kriging() no funciona si hay valores faltantes. Lo que hay que hacer es omitirlos. La función na.omit() elimina filas donde algún valor sea NA.

na.omit(observaciones)
## # A tibble: 115 × 6
##    station                   tmax_media provincia         lon   lat altua
##    <chr>                          <dbl> <chr>           <dbl> <dbl> <dbl>
##  1 AEROPARQUE AERO                17.4  CAPITAL FEDERAL -58.4 -34.6     6
##  2 AZUL AERO                      16.4  BUENOS AIRES    -59.9 -36.8   147
##  3 BAHIA BLANCA AERO              16.0  BUENOS AIRES    -62.2 -38.7    83
##  4 BARILOCHE AERO                  8.63 RIO NEGRO       -71.2 -41.2   835
##  5 BENITO JUAREZ AERO             15.6  BUENOS AIRES    -59.8 -37.7   207
##  6 BERNARDO DE IRIGOYEN AERO      22.4  MISIONES        -53.6 -26.2   815
##  7 BOLIVAR AERO                   17.4  BUENOS AIRES    -61.1 -36.2    94
##  8 BUENOS AIRES OBSERVATORIO      19.0  CAPITAL FEDERAL -58.5 -34.6    25
##  9 CAMPO DE MAYO AERO             18.8  BUENOS AIRES    -58.7 -34.5    26
## 10 CATAMARCA AERO                 25.1  CATAMARCA       -65.8 -28.6   464
## # … with 105 more rows

Para no escribir na.omit(observaciones) tres veces dentro de la llamada a krigin() se puede usar la función with(). Esta función no es de {dplyr} pero funciona parecido a summarise()/mutate() en que toma como primer argumento una tabla y luego el código que pongamos se entiende que hace referencia a las columnas de esa tabla.

Entonces, poniendo todo eso junto en una cadena:

observaciones_regular <- observaciones %>% 
  na.omit() %>% 
  with(kriging::kriging(lon, lat, response = tmax_media))

Desafío

¿Qué es lo que devolvió la función kriging()? Podés mirar la documentación de la función (en la sección “Value”) o usando glimpse(observaciones_regular).

observaciones_regular es una lista con muchos elementos:

glimpse(observaciones_regular)
## List of 7
##  $ model        : chr "spherical"
##  $ nugget       : num -4.4
##  $ range        : num 11.9
##  $ sill         : num 26.9
##  $ pixel        : num 0.33
##  $ map          :'data.frame':   5544 obs. of  3 variables:
##   ..$ x   : num [1:5544] -72 -72 -72 -72 -72 ...
##   ..$ y   : num [1:5544] -54.8 -54.5 -54.1 -53.8 -53.5 ...
##   ..$ pred: num [1:5544] 8.94 8.74 8.55 8.38 8.24 ...
##  $ semivariogram:'data.frame':   10 obs. of  2 variables:
##   ..$ distance    : num [1:10] 0.685 1.943 3.143 4.41 5.628 ...
##   ..$ semivariance: num [1:10] 0.555 2.139 3.295 5.659 8.877 ...
##  - attr(*, "class")= chr "kriging"

La mayoría de los elementos brindan información detallad sobre los parámetros usados y computados para realizar la interpolación. El elemento útil es el que se llama map, que tiene los datos interpolados. ¿Qué es lo que tiene map?

glimpse(observaciones_regular$map)
## Rows: 5,544
## Columns: 3
## $ x    <dbl> -72.05, -72.05, -72.05, -72.05, -72.05, -72.05, -72.05, -72.05, -…
## $ y    <dbl> -54.80000, -54.46970, -54.13939, -53.80909, -53.47879, -53.14848,…
## $ pred <dbl> 8.938263, 8.740744, 8.554452, 8.379149, 8.240792, 8.154581, 8.111…

Es una tabla con columnas, “x”, “y” y “pred”, que representan las coordenadas horizontales y los valores interpolados. Estos nombres no son muy descriptivos; se pueden cambiar con rename. Entonces este código extrae el elemento map de la lista observaciones_regular, le cambia el nombre a las variables y asigna el resultado a observaciones_regular

observaciones_regular <- observaciones_regular$map %>% 
  rename(lon = x, lat = y, tmax_media = pred)

Y esto, como es una grilla regular, se puede graficar con cualquiera de las técnicas anteriores. Dado que esto es una interpolación, es buena costumbre, además, indicar con puntos las coordenadas de las observaciones usadas para realizarla.

ggplot(observaciones_regular, aes(lon, lat)) +
  geom_contour_fill(aes(z = tmax_media)) +
  geom_point(data = observaciones, size = 0.2) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Este mapa se puede mejorar. En particular, la interpolación en el Mar Argentino, a miles de kilómetros de cualquier observación, no tiene ningún sentido. Para controlar el dominio en el cual se realiza la interpolación hay que usar el argumento polygons de kriging(). Ahí, hay que poner puntos en x e y que definan el polígono dentro del cual se va a hacer la interpolación. ¿De dónde sacás un polígono con el contorno de Argentina? 🤔. ¡Lo estás mirando! Es nuestro mapa.

La función fortify() de {ggplot2} genera polígonos a partir de la salida de {rnaturalearth}. De esos polígonos hay que seleccionar únicamente las coordenadas de longitud y latitud, y al mismo tiempo, renombrarlas como x e y para que kriging() las entienda. Finalmente, hay que meter todo eso en una lista.

poligonos_arg <- rnaturalearth::ne_countries(country = "argentina") %>% 
  fortify() %>% 
  select(x = long, y = lat) %>% 
  list()

Ahora, hay que repetir todo lo anterior, pero usando con polygons = poligonos_arg.

observaciones_regular <- observaciones %>% 
  na.omit() %>% 
  with(kriging::kriging(lon, lat, response = tmax_media, polygons = poligonos_arg)) 

observaciones_regular <- observaciones_regular$map %>% 
  rename(lon = x, lat = y, tmax_media = pred)

ggplot(observaciones_regular, aes(lon, lat)) +
  geom_contour_fill(aes(z = tmax_media)) +
  geom_point(data = observaciones, size = 0.2) +
  mi_mapa +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50))

Desafío

La interpolación de este gráfico queda fea en los bordes porque tiene poca resolución. ¿Qué hay que cambiar en la llamada a kriging() para incrementarla? Fijate en la documentación de la función.

Este gráfico tiene lo básico, pero se le puede poner un poco de amor para que quede mejor. Se le puede agregar líneas de contorno en negro para resaltar los contornos, cambiar la escala de colores para que evoque más la idea de “temperatura máxima”, agregar etiquetas a los contornos y modificar el texto de los ejes y las escalas. Haciendo todo eso, podemos llegar a algo como esto.

ggplot(observaciones_regular, aes(lon, lat)) +
  geom_contour_fill(aes(z = tmax_media)) +
  geom_contour2(aes(z = tmax_media), size = 0.2) +
  geom_text_contour(aes(z = tmax_media), skip = 1,
                    rotate = FALSE, size = 3.5,
                    stroke = 0.1, color ="white", stroke.color = "black") +
  geom_point(data = observaciones, size = 0.2) +
  mi_mapa +
  scale_fill_distiller(palette = "YlOrRd", direction = 1) +
  coord_sf(ylim = c(-55, -20), xlim = c(-80, -50)) +
  labs(fill = "Temperatura (ºC)", 
       x = NULL,
       y = NULL)  +
  theme_minimal()

Vas av er un poco más sobre la apariencia de los gráficos en la siguiente sección.

LS0tCnRpdGxlOiAiR3LDoWZpY29zIGVzcGFjaWFsZXMiCm91dHB1dDogCiAgaHRtbF9kb2N1bWVudDoKICAgIGNvZGVfZG93bmxvYWQ6IHRydWUKICAgIHRvYzogdHJ1ZQogICAgdG9jX2Zsb2F0OiB0cnVlCiAgICBoaWdobGlnaHQ6IHRhbmdvCi0tLQoKYGBge3Igc2V0dXAsIGluY2x1ZGU9RkFMU0V9CmtuaXRyOjpvcHRzX2NodW5rJHNldChlY2hvID0gVFJVRSwKICAgICAgICAgICAgICAgICAgICAgIG1lc3NhZ2UgPSBGQUxTRSkKbGlicmFyeShnZ3Bsb3QyKQpsaWJyYXJ5KGRwbHlyKQpsaWJyYXJ5KHJlYWRyKQpsaWJyYXJ5KG1ldFIpCmBgYAoKCkhheSBkb3MgZ3JhbmRlcyB0aXBvcyBkZSBkYXRvcyBlc3BhY2lhbGVzOiBncmlsbGFzIHJlZ3VsYXJlcyB5IGdyaWxsYXMgaXJyZWd1bGFyZXMuIAoKIyMgR3JpbGxhcyByZWd1bGFyZXMKCkVuIFtsYSBzZWd1bmRhIHNlY2Npw7NuIGRlIGxlY3R1cmEgZGUgZGF0b3NdKDA0LWxlY3R1cmEuaHRtbCNBcmNoaXZvc19OZXRDREYpIHZpc3RlIGPDs21vIGxlZXIgYXJjaGl2b3MgTmV0Q0RGOgoKCmBgYHtyfQp0ZW1wZXJhdHVyYSA8LSBSZWFkTmV0Q0RGKCJkYXRvcy90ZW1wZXJhdHVyYS5uYyIsIHZhcnMgPSAiYWlyIiwKICAgICAgICAgICAgICAgICAgICAgICAgICBzdWJzZXQgPSBsaXN0KGxldmVsID0gMTAwMCwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxhdCA9IGMoLTU1LCAtMjApLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbG9uID0gYygyODAsIDMxMCkpKSAKZ2xpbXBzZSh0ZW1wZXJhdHVyYSkKYGBgCgpMYSB2YXJpYWJsZSBgdGVtcGVyYXR1cmFgIGVzIHVuYSB0YWJsYSBxdWUgdGllbmUgZGF0byBkZSB0ZW1wZXJhdHVyYSBkZWwgYWlyZSBwYXJhIGNhZGEgdGllbXBvLCBuaXZlbCAoYWx0dXJhKSwgbGF0aXR1ZCB5IGxvbmdpdHVkLiBDb21vIGVzdGUgYXJjaGl2byB0aWVuZSBpbmZvcm1hY2nDs24gc8OzbG8gcGFyYSB1biB0aWVtcG8geSBlbCBjw7NkaWdvIGZpbHRyw7Mgc8OzbG8gZGF0b3MgZGUgdW4gbml2ZWwsIGVudG9uY2VzIGxhIHZhcmlhY2nDs24gZXMgc8OzbG8gZW4gbGF0aXR1ZCB5IGxvbmdpdHVkLiDCv0PDs21vIHNlIGdyYWZpY2E/CgpTZSBwdWVkZSBkaWJ1amFyIHVuIHB1bnRvIHBvciBjYWRhIGRhdG86CgpgYGB7cn0KZ2dwbG90KHRlbXBlcmF0dXJhLCBhZXMobG9uLCBsYXQpKSArCiAgZ2VvbV9wb2ludCgpCmBgYAoKRXN0byBubyBkaWNlIG5hZGEsIGF1bnF1ZSBwb2RlbW9zIGNvbmZpcm1hciBxdWUgbG9zIGRhdG9zIGVzdMOhbiBlbiB1bmEgZ3JpbGxhIHJlZ3VsYXIuIFBhcmEgdmlzdWFsaXphciBsYSB0ZW1wZXJhdHVyYSwgaGF5IHF1ZSBtYXBlYXIgw6lzdGUgdmFsb3IgYSB1biBwYXLDoW1ldHJvIGVzdMOpdGljbywgcG9yIGVqZW1wbG8sIGVsIGNvbG9yOgoKYGBge3J9CmdncGxvdCh0ZW1wZXJhdHVyYSwgYWVzKGxvbiwgbGF0KSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gYWlyKSkKYGBgCgpQdW50b3MgcmVkb25kb3MgeSBzZXBhcmFkb3Mgbm8gZXMgbGEgZm9ybWEgbcOhcyBpbmZvcm1hdGl2YSBkZSB2ZXIgZXN0b3MgZGF0b3MuIFVuYSBncmlsbGEgcmVndWxhciBlbiByZWFsaWRhZCBkZWZpbmUgY29tbyB1bmEgImltYWdlbiIgZG9uZGUgY2FkYSBwdW50byBlcyB1biBwaXhlbC4gRXN0byBzZSBsbGFtYSB1biAicmFzdGVyIiB5IGNvbiBnZ3Bsb3Qgc2UgcHVlZGUgZ3JhZmljYXIgY29uIGBnZW9tX3Jhc3RlcigpYC4KCgpgYGB7cn0KZ2dwbG90KHRlbXBlcmF0dXJhLCBhZXMobG9uLCBsYXQpKSArCiAgZ2VvbV9yYXN0ZXIoYWVzKGZpbGwgPSBhaXIpKQpgYGAKCmBnZW9tX3Jhc3RlcigpYCBkaWJ1amEgcmVjdMOhbmd1bG9zIGRlIGlndWFsIGFuY2hvIHkgYWx0byBlbiBjYWRhIHgsIHkuIEVsIGFuY2hvIHkgZWwgYWx0byBzZSBkZWZpbmVuIGEgdHJhdsOpcyBkZSBsYSByZXNvbHVjacOzbiBkZSBsb3MgZGF0b3MuIEZ1bmNpb25hbiBwZXJmZWN0YW1lbnRlIHBhcmEgZ3JpbGxhcyByZWd1bGFyZXMuIEVzdG9zIHJlY3TDoW5ndWxvcyB0aWVuZW4gdW4gY29sb3IgaW50ZXJubywgcG9yIGxvIHF1ZSBlbCBwYXLDoW1ldHJvIGVzdMOpdGljbyBzZSBsbGFtYSAiZmlsbCIuIAoKOjo6IHsuYWxlcnQgLmFsZXJ0LWluZm99IAoqKkRlc2Fmw61vKioKCmdncGxvdDIgdGllbmUgdHJlcyBmdW5jaW9uZXMgcGFyYSBnZW5lcmFyIHJlY3TDoW5ndWxvczogYGdlb21fcmFzdGVyKClgLCBgZ2VvbV90aWxlKClgIHkgYGdlb21fcmVjdCgpYC4gwr9DdcOhbCBlcyBsYSBkaWZlcmVuY2lhIGVudHJlIGNhZGEgdW5vPyBGaWphdGUgZW4gbGEgZG9jdW1lbnRhY2nDs24uIAo6OjoKCiMjIE1hcGFzCgpCaWVuLCBoYXN0YSBhaMOtIHRlbsOpcyB1biBncsOhZmljbyBjb24gbG9zIGRhdG9zLCBwZXJvIHBhcmEgZW50ZW5kZXJsb3MgZW4gc3UgY29udGV4dG8gZXNwYWNpYWwgY29tbyBtw61uaW1vIGhhY2UgZmFsdGEgdW4gbWFwYS4gUGFyYSB0ZW5lciB1biBtYXBhIHNlIG5lY2VzaXRhbiBkYXRvcyBkZSBsYSBsb2NhbGl6YWNpw7NuIGRlIGxhcyBjb3N0YXMgbyBsYXMgZnJvbnRlcmFzLiBFeGlzdGVuIHZhcmlhcyBmdWVudGVzIGRlIGVzdG9zIGRhdG9zLCBwZXJvIHVuYSBtdXkgYnVlbmEgZXMgW05hdHVyYWwgRWFydGhdKGh0dHBzOi8vd3d3Lm5hdHVyYWxlYXJ0aGRhdGEuY29tLykuIEVsIHBhcXVldGUge3JuYXR1cmFsZWFydGh9IHByb3ZlZSB1bmEgaW50ZXJmYXogYW1pZ2FibGUgcGFyYSB1c2FyIGVzdG9zIGRhdG9zIGRpcmVjdGFtZW50ZSBlbiB7Z2dwbG90Mn0uIAoKUHJpbWVybyBoYXkgcXVlIGNhcmdhciBlbCBtYXBhIHF1ZSBxdWVyYW1vcy4gUG9yIGVqZW1wbG8sIHBhcmEgZ3JhZmljYXIgZWwgbWFwYSBkZSBBcmdlbnRpbmEgeSBzdXMgcGHDrXNlcyBsaW3DrXRyb2ZlcyBjYXJnYW1vcyBsb3MgZGF0b3MgY29uIGBuZV9jb3VudHJpZXMoKWA6CgpgYGB7cn0KbWFwYSA8LSBybmF0dXJhbGVhcnRoOjpuZV9jb3VudHJpZXMoY291bnRyeSA9IGMoImFyZ2VudGluYSIsICJjaGlsZSIsICJ1cnVndWF5IiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJwYXJhZ3VheSIsICJicmF6aWwiLCAiYm9saXZpYSIsIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiZmFsa2xhbmQgaXNsYW5kcyIpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcmV0dXJuY2xhc3MgPSAic2YiKQpgYGAKCkVsIGFyZ3VtZW50byBgY291bnRyeWAgZXMgdW4gdmVjdG9yIGNvbiBsb3MgcGHDrXNlcyBxdWUgbmVjZXNpdGFtb3MuIEVsIGFyZ3VtZW50byBgcmV0dXJuY2xhc3NgIGVzIHVuIHBvY28gdMOpY25pY28sIHBlcm8gaGFjZSByZWZlcmVuY2lhIGEgbGEgZXN0cnVjdHVyYSBxdWUgcXVlcmVtb3MgcXVlIGRldnVlbHZhLiBFbiBlc3RlIGNhc28sIGByZXR1cm5jbGFzcyA9ICJzZiJgIGhhY2UgcXVlIGRldnVlbHZhIHVuIG9iamV0byBkZSBjbGFzZSAiU2ltcGxlIEZlYXR1cmVzIi4gTGFzICJTaW1wbGUgRmVhdHVyZXMiIHNvbiBjdWFscXVpZXIgY29zYSBtZW5vcyBzaW1wbGUgaW50ZXJuYW1lbnRlIHBlcm8gY29uIHtnZ3Bsb3QyfSBzZSBwdWVkZW4gZ3JhZmljYXIgY29uIHVuIGdlb20gZXNwZWPDrWZpY286IAoKYGBge3J9CmdncGxvdChtYXBhKSArCiAgZ2VvbV9zZigpCmBgYAoKUG9yIGRlZmVjdG8sIGVsIG1hcGEgc2UgZGlidWphIGNvbiB1biBmb25kbyBncmlzLCBwZXJvIGVsIHByb2JsZW1hIGVzIGVzZSBmb25kbyB2YSBhIHRhcGFyIGxvcyBkYXRvcyEgUGFybyBwYXJhIGRpYnVqYXIgc8OzbG8gbG9zIGNvbnRvcm5vcyBoYXkgcXVlIG1vZGlmaWNhcmxvIHVuIHBvY286CgpgYGB7cn0KZ2dwbG90KG1hcGEpICsKICBnZW9tX3NmKGZpbGwgPSBOQSwgY29sb3IgPSAiYmxhY2siLCBzaXplID0gMC4yKQpgYGAKCkN1YW5kbyB1bm8gdXNhIG11Y2hvIHVuIG1hcGEsIG11Y2hhcyB2ZWNlcyB0ZXJtaW5hIHNpZW5kbyDDunRpbCBndWFyZGFyIGVsIGdlb20gcG9yIHNlcGFyYWRvIGVuIHVuYSB2YXJpYWJsZS4gCgpgYGB7cn0KbWlfbWFwYSA8LSBnZW9tX3NmKGRhdGEgPSBtYXBhLCBpbmhlcml0LmFlcyA9IEZBTFNFLCBmaWxsID0gTkEsIGNvbG9yID0gImJsYWNrIiwgc2l6ZSA9IDAuMikgCmBgYAoKQWRlbcOhcyBkZSBsYXMgbW9kaWZpY2FjaW9uZXMgZXN0w6l0aWNhcywgZXN0YSBsbGFtYWRhIGEgYGdlb21fc2ZgIHRpZW5lIGVsIGFyZ3VtZW50byBgZGF0YSA9IG1hcGFgLCB5YSBxdWUgbm8gdmEgYSB1c2FyIGxvcyBkYXRvcyAiZ2xvYmFsZXMiIGRlIGxhIGxsYW1hZGEgYSBgZ2dwbG90KClgLiBUYW1iacOpbiB0aWVuZSBgw6xuaGVyaXQuYWVzID0gRkFMU0VgIHBvcnF1ZSB0YW1wb2NvIHRpZW5lIHF1ZSAiaGVyZWRhciIgbG9zIGFlcyBnbG9iYWxlcy4gCgpFbnRvbmNlcyBhaG9yYSwgcGFyYSBncmFmaWNhciBsYSB0ZW1wZXJhdHVyYSB5IGVsIG1hcGEgZW5jaW1hLCBhcm1hbW9zIGVsIGdyw6FmaWNvIGRlIHJhc3RlciBpZ3VhbCBxdWUgYW50ZXMgeSBsZSBzdW1hbW9zIGVsIG1hcGE6CgoKYGBge3J9CmdncGxvdCh0ZW1wZXJhdHVyYSwgYWVzKGxvbiwgbGF0KSkgKwogIGdlb21fcmFzdGVyKGFlcyhmaWxsID0gYWlyKSkgKwogIG1pX21hcGEKYGBgCgo6Ojogey5hbGVydCAuYWxlcnQtaW5mb30KKipEZXNhZsOtbyoqCgrCv1BvZMOpcyBleHBsaWNhciBxdcOpIHNhbGnDsyBtYWw/Cjo6OgoKRXhpc3RlbiBkb3MgY29udmVuY2lvbmVzIGLDoXNpY2FzIHBhcmEgbWVkaXIgbGEgbG9uZ2l0dWQsIG1lZGlkYSBlbnRyZSAwwrogeSAzNjDCuiBvIGVudHJlIC0xODDCuiB5ICsxODDCui4gTG9zIGRhdG9zIGRlIHRlbXBlcmF0dXJhIHVzYW4gbGEgcHJpbWVyYSB5IGxvcyBkZWwgbWFwYSwgbGEgc2VndW5kYS4gUGFyYSBjb252ZXJ0aXIgZW50cmUgdW5hIHkgb3RyYSBjb252ZW5jacOzbiwgcG9kw6lzIHVzYXIgbGEgZnVuY2nDs24gYENvbnZlcnRMb25naXR1ZGUoKWAgZGUge21ldFJ9OgoKYGBge3J9CnRlbXBlcmF0dXJhIDwtIG11dGF0ZSh0ZW1wZXJhdHVyYSwgbG9uID0gbWV0Ujo6Q29udmVydExvbmdpdHVkZShsb24sIGZyb20gPSAzNjApKQpgYGAKCgpgYGB7cn0KZ2dwbG90KHRlbXBlcmF0dXJhLCBhZXMobG9uLCBsYXQpKSArCiAgZ2VvbV9yYXN0ZXIoYWVzKGZpbGwgPSBhaXIpKSArCiAgbWlfbWFwYSAKYGBgCgpMbyDDum5pY28gcXVlIGxlIGZhbHRhIGEgZXN0ZSBncsOhZmljbyBlcyBsbyBxdWUgbGUgc29icmE6IHRvZG8gZWwgZXNwYWNpbyBkb25kZSBubyBoYXkgZGF0b3MuIFBhcmEgcmVjb3J0YXIgZWwgw6FyZWEgZGVsIGdyw6FmaWNvIHkgcXVlIHRvbWUgc8OzbG8gZG9uZGUgdGVuZW1vcyBkYXRvcyBkZSB0ZW1wZXJhdHVyYSwgaGF5IHF1ZSBlc3BlY2lmaWNhciBsb3MgbMOtbWl0ZXMgZGVsIHNpc3RlbWEgZGUgY29vcmRlbmFkYXMuIENvbW8gYG1pX21hcGFgIGVzIHVuIGBnZW9tX3NmYCwgaGF5IHF1ZSB1c2FyIGBjb29yZF9zZmAuCgpgYGB7cn0KZ2dwbG90KHRlbXBlcmF0dXJhLCBhZXMobG9uLCBsYXQpKSArCiAgZ2VvbV9yYXN0ZXIoYWVzKGZpbGwgPSBhaXIpKSArCiAgbWlfbWFwYSArCiAgY29vcmRfc2YoeWxpbSA9IGMoLTU1LCAtMjApLCB4bGltID0gYygtODAsIC01MCkpCmBgYAoKCk90cmEgZm9ybWEgZGUgZ3JhZmljYXIgZGF0b3MgZXNwYWNpYWxlcyBlcyBwZW5zYXJsb3MgY29tbyB1bmEgc3VwZXJmaWNpZSB0cmlkaW1lbnNpb25hbCBkb25kZSBlbCB4IHkgZWwgeSBzb24gbGFzIGNvb3JkZW5hZGFzIHkgbGEgYWx0dXJhIChsYSBjb29yZGVuYWRhIHopIGVzIHByb3BvcmNpb25hbCBhbCB2YWxvciBkZSBsYSB2YXJpYWJsZS4gSW1hZ2luw6FuZG9zZWxvIGFzw60sIGVzdGUgZ3LDoWZpY28gZGUgdGVtcGVyYXR1cmEgc2Vyw61hIHVuYSBtb250YcOxYSBjb24gc3UgY2ltYSBlbiBQYXJhZ3VheSB5IHN1IHB1bnRvIG3DoXMgYmFqbyBjZXJjYSBkZSBDaHVidXQuIAoKUHJlY2lzYW1lbnRlIHBhcmEgZXN0YSBzaXR1YWNpw7NuIGVzdMOhbiBwZW5zYWRhcyBsYXMgbMOtbmVhcyBkZSBjb250b3JubyBxdWUgc2UgcHVlZGVuIGdyYWZpY2FyIGVuIHtnZ3Bsb3QyfSB1c2FuZG8gbGEgZnVuY2nDs24gYGdlb21fY29udG91cigpYC4KCmBgYHtyfQpnZ3Bsb3QodGVtcGVyYXR1cmEsIGFlcyhsb24sIGxhdCkpICsKICBnZW9tX2NvbnRvdXIoYWVzKHogPSBhaXIpKSArCiAgbWlfbWFwYSArCiAgY29vcmRfc2YoeWxpbSA9IGMoLTU1LCAtMjApLCB4bGltID0gYygtODAsIC01MCkpCmBgYAoKTGFzIGzDrW5lYXMgZGlidWphZGFzIHBvciBgZ2VvbV9jb250b3VyKClgIHNvbiBsw61uZWFzIGRlIG5pdmVsIHF1ZSB1bmVuIHB1bnRvcyBkZSB2YWxvciBjb25zdGFudGUuIFNpbiBlbWJhcmdvLCBlc2FzIGzDrW5lYXMgYXp1bGVzIG5vIGRhbiBuaW5ndW5hIGluZGljYWNpw7NuIGRlbCB2YWxvciBkZSBsYSB2YXJpYWJsZSBwb3IgbG8gcXVlIGVzIGNvbnZlbmllbnRlIG1hcGVhciBlbCBjb2xvciBkZSBsYXMgbMOtbmVhcyBlc2UgdmFsb3IgY29uc3RhbnRlLgoKYGBge3J9CmdncGxvdCh0ZW1wZXJhdHVyYSwgYWVzKGxvbiwgbGF0KSkgKwogIGdlb21fY29udG91cihhZXMoeiA9IGFpciwgY29sb3IgPSBzdGF0KGxldmVsKSkpICsKICBtaV9tYXBhICsKICBjb29yZF9zZih5bGltID0gYygtNTUsIC0yMCksIHhsaW0gPSBjKC04MCwgLTUwKSkKYGBgCgrCv0RlIGTDs25kZSBzYWxpw7MgZXNlIGBzdGF0KGxldmVsKWA/IEFsZ3Vub3MgZ2VvbXMgcXVlIHJlYWxpemFuIHRyYW5zZm9ybWFjaW9uZXMgZXN0YWTDrXN0aWNhcyBjb21wdXRhbiB2YXJpYWJsZXMgbnVldmFzIHF1ZSBsdWVnbyBzZSBwdWVkZW4gbWFwZWFyIGEgcGFyw6FtZXRyb3MgZ2VvbcOpdHJpY29zLiAibGV2ZWwiIGVzIHVuYSBkZSBlc2FzIHZhcmlhYmxlcyBjb21wdXRhZGFzIGRlIGBnZW9tX2NvbnRvdXJgLiBQYXJhIGRlY2lybGUgYSBnZ3Bsb3QgcXVlIGFsIGxldmVsIGFsIHF1ZSB0ZSByZWZlcsOtcyBlcyBhIGxhIHZhcmlhYmxlIGNvbXB1dGFkYSwgc2Ugcm9kZWEgYSBsYSB2YXJpYWJsZSBjb24gbGEgZnVuY2nDs24gYHN0YXQoKWAuCgpVbmEgdmFyaWVkYWQgZGUgbGFzIGzDrW5lYXMgZGUgY29udG9ybm8gc29uIGxhcyBsw61uZWFzICJsbGVuYXMiLiBIYXN0YSBoYWNlIHBvY28sIHtnZ3Bsb3QyfSBubyB0ZW7DrWEgdW5hIGZvcm1hIGRlIGdyYWZpY2FyIGVzdG9zIGNvbnRvcm5vcyBsbGVub3MsIGFzw60gcXVlIGVzdMOhIGltcGxlbWVudGFkbyBlbiB7bWV0Un0gY29uIGxhIGZ1bmN0aW9uIGBnZW9tX2NvbnRvdXJfZmlsbCgpYC4gUmVjaWVudGVtZW50ZSB7Z2dwbG90Mn0gaW1wbGVtZW50w7MgbGEgZnVuY2nDs24gYGdlb21fY29udG91cl9maWxsZWQoKWAgcGVybyBhIG5vc290cm9zIG5vcyBndXN0YSBtw6FzIGxhIHZlcnNpw7NuIGRlIHttZXRSfSBgciBlbW86OmppKCJ3aW5rIilgCgpgYGB7cn0KZ2dwbG90KHRlbXBlcmF0dXJhLCBhZXMobG9uLCBsYXQpKSArCiAgbWV0Ujo6Z2VvbV9jb250b3VyX2ZpbGwoYWVzKHogPSBhaXIpKSArCiAgbWlfbWFwYSArCiAgY29vcmRfc2YoeWxpbSA9IGMoLTU1LCAtMjApLCB4bGltID0gYygtODAsIC01MCkpCmBgYAoKClBvciDDumx0aW1vLCB0cmFucXVpbGFtZW50ZSBzZSBwdWVkZW4gdXNhciBhbWJvcyBnZW9tcyBwYXJhIHJlc2FsdGFyIGxvcyBsw61taXRlcyB5CgoKYGBge3J9CmdncGxvdCh0ZW1wZXJhdHVyYSwgYWVzKGxvbiwgbGF0KSkgKwogIG1ldFI6Omdlb21fY29udG91cl9maWxsKGFlcyh6ID0gYWlyKSkgKwogIGdlb21fY29udG91cihhZXMoeiA9IGFpciksIGNvbG9yID0gImJsYWNrIikgKwogIG1pX21hcGEgKwogIGNvb3JkX3NmKHlsaW0gPSBjKC01NSwgLTIwKSwgeGxpbSA9IGMoLTgwLCAtNTApKQpgYGAKCiMjIEdyaWxsYXMgaXJyZWd1bGFyZXMKCsK/UXXDqSBwYXNhIGN1YW5kbyBlbiB2ZXogZGUgb2JzZXJ2YWNpb25lcyBvcmdhbml6YWRhcyBlbiB1bmEgYmVsbGEgZ3JpbGxhIHJlZ3VsYXIsIHRlbsOpcyBvYnNlcnZhY2lvbmVzIHB1bnR1YWxlcyBlbiBsdWdhcmVzIGRpc3BlcnNvcz8gU2kgdGUgYWNvcmTDoXMgZGUgbGEgc2VjY2nDs24gZGUgdW5pb25lcywgZW4gbGEgY2FycGV0YSBkYXRvcyBoYXkgdW4gYXJjaGl2byBjb24gZGF0b3MgZGUgdGVtcGVyYXR1cmEgZGVsIHNlcnZpY2lvIG1ldGVvcm9sw7NnaWNvIHkgbWV0YWRhdG9zIGNvbiBzdXMgdWJpY2FjaW9uZXMuIEVzdG8gZGVmaW5lIGRhdG9zIGVzcGFjaWFsZXMgZGUgdGVtcGVyYXR1cmEuIAoKCmBgYHtyfQojIFBhcmEgdHJhYmFqYXIgY29uIG1lbm9zIGRhdG9zLCBub3MgcXVlZGFtb3MgY29uIGxhIHRlbXBlcmF0dXJhIG3DoXhpbWEgbWVkaWEgCiMgZGUgY2FkYSBlc3RhY2nDs24uCm9ic2VydmFjaW9uZXMgPC0gcmVhZHI6OnJlYWRfY3N2KCJkYXRvcy9vYnNlcnZhY2lvbmVzX3Ntbi5jc3YiKSAlPiUgCiAgZ3JvdXBfYnkoc3RhdGlvbikgJT4lIAogIHN1bW1hcmlzZSh0bWF4X21lZGlhID0gbWVhbih0bWF4LCBuYS5ybSA9IFRSVUUpKQoKZXN0YWNpb25lcyA8LSByZWFkX2NzdigiZGF0b3MvZXN0YWNpb25lc19zbW4uY3N2IikgCgpvYnNlcnZhY2lvbmVzIDwtIGxlZnRfam9pbihvYnNlcnZhY2lvbmVzLCBlc3RhY2lvbmVzLCBieSA9IGMoInN0YXRpb24iID0gIm5vbWJyZSIpKSAlPiUgCiAgZmlsdGVyKHByb3ZpbmNpYSAhPSAiQU5UQVJUSURBIikKYGBgCgpMbyBwcmltZXJvIHF1ZSBzZSBwdWVkZSBoYWNlciBlcyBncmFmaWNhciBlc3RvcyBkYXRvcyBkZSBlc3RhY2nDs24gY29uIHB1bnRvcywgZGUgbGEgbWlzbWEgZm9ybWEgcXVlIHBhcmEgbG9zIGRhdG9zIHJlZ3VsYXJlcy4KCmBgYHtyfQpnZ3Bsb3Qob2JzZXJ2YWNpb25lcywgYWVzKGxvbiwgbGF0KSkgKwogIGdlb21fcG9pbnQoYWVzKGNvbG9yID0gdG1heF9tZWRpYSkpICsKICBtaV9tYXBhICsKICBjb29yZF9zZih5bGltID0gYygtNTUsIC0yMCksIHhsaW0gPSBjKC04MCwgLTUwKSkKYGBgCgo6Ojogey5hbGVydCAuYWxlcnQtaW5mb30gCioqRGVzYWbDrW8qKgoKR3JhZmljw6EgZXN0b3MgcHVudG9zIHVzYW5kbyBgZ2VvbV9yYXN0ZXIoKWAgeSBgZ2VvbV9jb250b3VyKClgLiDCv0N1w6FsIGVzIGVsIHJlc3VsdGFkbz8KCjo6OgoKCkNvbW8gZXN0b3MgZGF0b3Mgbm8gZXN0w6FuIGVuIHVuYSBncmlsbGEgcmVndWxhciwgYGdlb21fcmFzdGVyKClgIHkgYGdlb21fY29udG91cigpYCBubyBsb3MgcHVlZGVuIGdyYWZpY2FyLiBQYXJhIHVzYXIgY29udG9ybm9zIGhheSBxdWUgaW50ZXJwb2xhciBhIHVuYSBncmlsbGEgcmVndWxhci4gVW5hIGZvcm1hIGRlIGhhY2VybG8gZXMgdXNhbmRvIGxhIHTDqWNuaWNhIGRlIFtrcmlnaW5nXShodHRwczovL2VzLndpa2lwZWRpYS5vcmcvd2lraS9LcmlnaW5nKS4gVW4gcGFxdWV0ZSBkZSBSIHF1ZSBsYSBpbXBsZW1lbnRhIGVzIHtrcmlnaW5nfSBlbiBzdSBmdW5jacOzbiBga3JpZ2luZ2AKClNpIHZhcyBhIGxhIGF5dWRhIGRlIGBrcmlnaW5nKClgLCB2YXMgYSB2ZXIgcXVlIHJlcXVpZXJlIHVuIHZlY3RvciBkZSBjb29yZGVuYWRhcyB4LCB1biB2ZWN0b3IgZGUgY29vcmRlbmFkYXMgeSB5IHVuIHZlY3RvciBkZSB2YWxvcmVzIG9ic2VydmFkb3MuIEVzdG9zIHNvbiBsYXMgY29sdW1uYXMgYGxvbmAsIGBsYXRgIHkgYHRtYXhfbWVkaWFgIGRlIGxhIHRhYmxhIGBvYnNlcnZhY2lvbmVzYC4gRW50b25jZXMgZWwgY8OzZGlnbyBzZXLDrWE6CgpgYGB7ciwgZXJyb3IgPSBUUlVFfQpvYnNlcnZhY2lvbmVzX3JlZ3VsYXIgPC0ga3JpZ2luZzo6a3JpZ2luZyh4ID0gb2JzZXJ2YWNpb25lcyRsb24sIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICB5ID0gb2JzZXJ2YWNpb25lcyRsYXQsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHJlc3BvbnNlID0gb2JzZXJ2YWNpb25lcyR0bWF4X21lZGlhKQpgYGAKCkVzdGUgZXJyb3IgaW5kaWNhIHF1ZSBoYXkgYE5BYHMsIHkgYGtyaWdpbmcoKWAgbm8gZnVuY2lvbmEgc2kgaGF5IHZhbG9yZXMgZmFsdGFudGVzLiBMbyBxdWUgaGF5IHF1ZSBoYWNlciBlcyBvbWl0aXJsb3MuIExhIGZ1bmNpw7NuIGBuYS5vbWl0KClgIGVsaW1pbmEgZmlsYXMgZG9uZGUgYWxnw7puIHZhbG9yIHNlYSBgTkFgLgoKYGBge3J9Cm5hLm9taXQob2JzZXJ2YWNpb25lcykKYGBgCgpQYXJhIG5vIGVzY3JpYmlyIGBuYS5vbWl0KG9ic2VydmFjaW9uZXMpYCB0cmVzIHZlY2VzIGRlbnRybyBkZSBsYSBsbGFtYWRhIGEgYGtyaWdpbigpYCBzZSBwdWVkZSB1c2FyIGxhIGZ1bmNpw7NuIGB3aXRoKClgLiBFc3RhIGZ1bmNpw7NuIG5vIGVzIGRlIHtkcGx5cn0gcGVybyBmdW5jaW9uYSBwYXJlY2lkbyBhIGBzdW1tYXJpc2UoKWAvYG11dGF0ZSgpYCBlbiBxdWUgdG9tYSBjb21vIHByaW1lciBhcmd1bWVudG8gdW5hIHRhYmxhIHkgbHVlZ28gZWwgY8OzZGlnbyBxdWUgcG9uZ2Ftb3Mgc2UgZW50aWVuZGUgcXVlIGhhY2UgcmVmZXJlbmNpYSBhIGxhcyBjb2x1bW5hcyBkZSBlc2EgdGFibGEuIAoKRW50b25jZXMsIHBvbmllbmRvIHRvZG8gZXNvIGp1bnRvIGVuIHVuYSBjYWRlbmE6IAoKYGBge3J9Cm9ic2VydmFjaW9uZXNfcmVndWxhciA8LSBvYnNlcnZhY2lvbmVzICU+JSAKICBuYS5vbWl0KCkgJT4lIAogIHdpdGgoa3JpZ2luZzo6a3JpZ2luZyhsb24sIGxhdCwgcmVzcG9uc2UgPSB0bWF4X21lZGlhKSkKYGBgCgo6Ojogey5hbGVydCAuYWxlcnQtaW5mb30gCioqRGVzYWbDrW8qKgoKwr9RdcOpIGVzIGxvIHF1ZSBkZXZvbHZpw7MgbGEgZnVuY2nDs24gYGtyaWdpbmcoKWA/IFBvZMOpcyBtaXJhciBsYSBkb2N1bWVudGFjacOzbiBkZSBsYSBmdW5jacOzbiAoZW4gbGEgc2VjY2nDs24gIlZhbHVlIikgbyB1c2FuZG8gYGdsaW1wc2Uob2JzZXJ2YWNpb25lc19yZWd1bGFyKWAuCgo6OjoKCgpgb2JzZXJ2YWNpb25lc19yZWd1bGFyYCBlcyB1bmEgbGlzdGEgY29uIG11Y2hvcyBlbGVtZW50b3M6CgpgYGB7cn0KZ2xpbXBzZShvYnNlcnZhY2lvbmVzX3JlZ3VsYXIpCmBgYApMYSBtYXlvcsOtYSBkZSBsb3MgZWxlbWVudG9zIGJyaW5kYW4gaW5mb3JtYWNpw7NuIGRldGFsbGFkIHNvYnJlIGxvcyBwYXLDoW1ldHJvcyB1c2Fkb3MgeSBjb21wdXRhZG9zIHBhcmEgcmVhbGl6YXIgbGEgaW50ZXJwb2xhY2nDs24uIEVsIGVsZW1lbnRvIMO6dGlsIGVzIGVsIHF1ZSBzZSBsbGFtYSBgbWFwYCwgcXVlIHRpZW5lIGxvcyBkYXRvcyBpbnRlcnBvbGFkb3MuIMK/UXXDqSBlcyBsbyBxdWUgdGllbmUgYG1hcGA/CgpgYGB7cn0KZ2xpbXBzZShvYnNlcnZhY2lvbmVzX3JlZ3VsYXIkbWFwKQpgYGAKCkVzIHVuYSB0YWJsYSBjb24gY29sdW1uYXMsICJ4IiwgInkiIHkgInByZWQiLCBxdWUgcmVwcmVzZW50YW4gbGFzIGNvb3JkZW5hZGFzIGhvcml6b250YWxlcyB5IGxvcyB2YWxvcmVzIGludGVycG9sYWRvcy4gRXN0b3Mgbm9tYnJlcyBubyBzb24gbXV5IGRlc2NyaXB0aXZvczsgc2UgcHVlZGVuIGNhbWJpYXIgY29uIGByZW5hbWVgLiBFbnRvbmNlcyBlc3RlIGPDs2RpZ28gZXh0cmFlIGVsIGVsZW1lbnRvIGBtYXBgIGRlIGxhIGxpc3RhIGBvYnNlcnZhY2lvbmVzX3JlZ3VsYXJgLCBsZSBjYW1iaWEgZWwgbm9tYnJlIGEgbGFzIHZhcmlhYmxlcyB5IGFzaWduYSBlbCByZXN1bHRhZG8gYSBgb2JzZXJ2YWNpb25lc19yZWd1bGFyYAoKCmBgYHtyfQpvYnNlcnZhY2lvbmVzX3JlZ3VsYXIgPC0gb2JzZXJ2YWNpb25lc19yZWd1bGFyJG1hcCAlPiUgCiAgcmVuYW1lKGxvbiA9IHgsIGxhdCA9IHksIHRtYXhfbWVkaWEgPSBwcmVkKQpgYGAKClkgZXN0bywgY29tbyBlcyB1bmEgZ3JpbGxhIHJlZ3VsYXIsIHNlIHB1ZWRlIGdyYWZpY2FyIGNvbiBjdWFscXVpZXJhIGRlIGxhcyBbdMOpY25pY2FzIGFudGVyaW9yZXNdKCNnaWxsYXMtcmVndWxhcmVzKS4gRGFkbyBxdWUgZXN0byBlcyB1bmEgaW50ZXJwb2xhY2nDs24sIGVzIGJ1ZW5hIGNvc3R1bWJyZSwgYWRlbcOhcywgaW5kaWNhciBjb24gcHVudG9zIGxhcyBjb29yZGVuYWRhcyBkZSBsYXMgb2JzZXJ2YWNpb25lcyB1c2FkYXMgcGFyYSByZWFsaXphcmxhLiAKCmBgYHtyfQpnZ3Bsb3Qob2JzZXJ2YWNpb25lc19yZWd1bGFyLCBhZXMobG9uLCBsYXQpKSArCiAgZ2VvbV9jb250b3VyX2ZpbGwoYWVzKHogPSB0bWF4X21lZGlhKSkgKwogIGdlb21fcG9pbnQoZGF0YSA9IG9ic2VydmFjaW9uZXMsIHNpemUgPSAwLjIpICsKICBtaV9tYXBhICsKICBjb29yZF9zZih5bGltID0gYygtNTUsIC0yMCksIHhsaW0gPSBjKC04MCwgLTUwKSkKYGBgCgpFc3RlIG1hcGEgc2UgcHVlZGUgbWVqb3Jhci4gRW4gcGFydGljdWxhciwgbGEgaW50ZXJwb2xhY2nDs24gZW4gZWwgTWFyIEFyZ2VudGlubywgYSBtaWxlcyBkZSBraWzDs21ldHJvcyBkZSBjdWFscXVpZXIgb2JzZXJ2YWNpw7NuLCBubyB0aWVuZSBuaW5nw7puIHNlbnRpZG8uIFBhcmEgY29udHJvbGFyIGVsIGRvbWluaW8gZW4gZWwgY3VhbCBzZSByZWFsaXphIGxhIGludGVycG9sYWNpw7NuIGhheSBxdWUgdXNhciBlbCBhcmd1bWVudG8gYHBvbHlnb25zYCBkZSBga3JpZ2luZygpYC4gQWjDrSwgaGF5IHF1ZSBwb25lciBwdW50b3MgZW4geCBlIHkgcXVlIGRlZmluYW4gZWwgcG9sw61nb25vIGRlbnRybyBkZWwgY3VhbCBzZSB2YSBhIGhhY2VyIGxhIGludGVycG9sYWNpw7NuLiDCv0RlIGTDs25kZSBzYWPDoXMgdW4gcG9sw61nb25vIGNvbiBlbCBjb250b3JubyBkZSBBcmdlbnRpbmE/IGByIGVtbzo6amkoInRoaW5raW5nIilgLiDCoUxvIGVzdMOhcyBtaXJhbmRvISBFcyBudWVzdHJvIG1hcGEuIAoKTGEgZnVuY2nDs24gYGZvcnRpZnkoKWAgZGUge2dncGxvdDJ9IGdlbmVyYSBwb2zDrWdvbm9zIGEgcGFydGlyIGRlIGxhIHNhbGlkYSBkZSB7cm5hdHVyYWxlYXJ0aH0uIERlIGVzb3MgcG9sw61nb25vcyBoYXkgcXVlIHNlbGVjY2lvbmFyIMO6bmljYW1lbnRlIGxhcyBjb29yZGVuYWRhcyBkZSBsb25naXR1ZCB5IGxhdGl0dWQsIHkgYWwgbWlzbW8gdGllbXBvLCByZW5vbWJyYXJsYXMgY29tbyB4IGUgeSBwYXJhIHF1ZSBga3JpZ2luZygpYCBsYXMgZW50aWVuZGEuIEZpbmFsbWVudGUsIGhheSBxdWUgbWV0ZXIgdG9kbyBlc28gZW4gdW5hIGxpc3RhLiAKCmBgYHtyfQpwb2xpZ29ub3NfYXJnIDwtIHJuYXR1cmFsZWFydGg6Om5lX2NvdW50cmllcyhjb3VudHJ5ID0gImFyZ2VudGluYSIpICU+JSAKICBmb3J0aWZ5KCkgJT4lIAogIHNlbGVjdCh4ID0gbG9uZywgeSA9IGxhdCkgJT4lIAogIGxpc3QoKQpgYGAKCgpBaG9yYSwgaGF5IHF1ZSByZXBldGlyIHRvZG8gbG8gYW50ZXJpb3IsIHBlcm8gdXNhbmRvIGNvbiBgcG9seWdvbnMgPSBwb2xpZ29ub3NfYXJnYC4gCgpgYGB7cn0Kb2JzZXJ2YWNpb25lc19yZWd1bGFyIDwtIG9ic2VydmFjaW9uZXMgJT4lIAogIG5hLm9taXQoKSAlPiUgCiAgd2l0aChrcmlnaW5nOjprcmlnaW5nKGxvbiwgbGF0LCByZXNwb25zZSA9IHRtYXhfbWVkaWEsIHBvbHlnb25zID0gcG9saWdvbm9zX2FyZykpIAoKb2JzZXJ2YWNpb25lc19yZWd1bGFyIDwtIG9ic2VydmFjaW9uZXNfcmVndWxhciRtYXAgJT4lIAogIHJlbmFtZShsb24gPSB4LCBsYXQgPSB5LCB0bWF4X21lZGlhID0gcHJlZCkKCmdncGxvdChvYnNlcnZhY2lvbmVzX3JlZ3VsYXIsIGFlcyhsb24sIGxhdCkpICsKICBnZW9tX2NvbnRvdXJfZmlsbChhZXMoeiA9IHRtYXhfbWVkaWEpKSArCiAgZ2VvbV9wb2ludChkYXRhID0gb2JzZXJ2YWNpb25lcywgc2l6ZSA9IDAuMikgKwogIG1pX21hcGEgKwogIGNvb3JkX3NmKHlsaW0gPSBjKC01NSwgLTIwKSwgeGxpbSA9IGMoLTgwLCAtNTApKQpgYGAKCgo6Ojogey5hbGVydCAuYWxlcnQtaW5mb30KKipEZXNhZsOtbyoqCgpMYSBpbnRlcnBvbGFjacOzbiBkZSBlc3RlIGdyw6FmaWNvIHF1ZWRhIGZlYSBlbiBsb3MgYm9yZGVzIHBvcnF1ZSB0aWVuZSBwb2NhIHJlc29sdWNpw7NuLiDCv1F1w6kgaGF5IHF1ZSBjYW1iaWFyIGVuIGxhIGxsYW1hZGEgYSBga3JpZ2luZygpYCBwYXJhIGluY3JlbWVudGFybGE/IEZpamF0ZSBlbiBsYSBkb2N1bWVudGFjacOzbiBkZSBsYSBmdW5jacOzbi4gCgoKOjo6CgoKRXN0ZSBncsOhZmljbyB0aWVuZSBsbyBiw6FzaWNvLCBwZXJvIHNlIGxlIHB1ZWRlIHBvbmVyIHVuIHBvY28gZGUgYW1vciBwYXJhIHF1ZSBxdWVkZSBtZWpvci4gU2UgbGUgcHVlZGUgYWdyZWdhciBsw61uZWFzIGRlIGNvbnRvcm5vIGVuIG5lZ3JvIHBhcmEgcmVzYWx0YXIgbG9zIGNvbnRvcm5vcywgY2FtYmlhciBsYSBlc2NhbGEgZGUgY29sb3JlcyBwYXJhIHF1ZSBldm9xdWUgbcOhcyBsYSBpZGVhIGRlICJ0ZW1wZXJhdHVyYSBtw6F4aW1hIiwgYWdyZWdhciBldGlxdWV0YXMgYSBsb3MgY29udG9ybm9zIHkgbW9kaWZpY2FyIGVsIHRleHRvIGRlIGxvcyBlamVzIHkgbGFzIGVzY2FsYXMuIEhhY2llbmRvIHRvZG8gZXNvLCBwb2RlbW9zIGxsZWdhciBhIGFsZ28gY29tbyBlc3RvLgoKYGBge3J9CmdncGxvdChvYnNlcnZhY2lvbmVzX3JlZ3VsYXIsIGFlcyhsb24sIGxhdCkpICsKICBnZW9tX2NvbnRvdXJfZmlsbChhZXMoeiA9IHRtYXhfbWVkaWEpKSArCiAgZ2VvbV9jb250b3VyMihhZXMoeiA9IHRtYXhfbWVkaWEpLCBzaXplID0gMC4yKSArCiAgZ2VvbV90ZXh0X2NvbnRvdXIoYWVzKHogPSB0bWF4X21lZGlhKSwgc2tpcCA9IDEsCiAgICAgICAgICAgICAgICAgICAgcm90YXRlID0gRkFMU0UsIHNpemUgPSAzLjUsCiAgICAgICAgICAgICAgICAgICAgc3Ryb2tlID0gMC4xLCBjb2xvciA9IndoaXRlIiwgc3Ryb2tlLmNvbG9yID0gImJsYWNrIikgKwogIGdlb21fcG9pbnQoZGF0YSA9IG9ic2VydmFjaW9uZXMsIHNpemUgPSAwLjIpICsKICBtaV9tYXBhICsKICBzY2FsZV9maWxsX2Rpc3RpbGxlcihwYWxldHRlID0gIllsT3JSZCIsIGRpcmVjdGlvbiA9IDEpICsKICBjb29yZF9zZih5bGltID0gYygtNTUsIC0yMCksIHhsaW0gPSBjKC04MCwgLTUwKSkgKwogIGxhYnMoZmlsbCA9ICJUZW1wZXJhdHVyYSAowrpDKSIsIAogICAgICAgeCA9IE5VTEwsCiAgICAgICB5ID0gTlVMTCkgICsKICB0aGVtZV9taW5pbWFsKCkKYGBgCgpWYXMgYXYgZXIgdW4gcG9jbyBtw6FzIHNvYnJlIGxhIGFwYXJpZW5jaWEgZGUgbG9zIGdyw6FmaWNvcyBlbiBsYSBzaWd1aWVudGUgc2VjY2nDs24uIAoKCjxkaXYgY2xhc3M9ImJ0bi1ncm91cCIgcm9sZT0iZ3JvdXAiIGFyaWEtbGFiZWw9Ik5hdmVnYWNpw7NuIj4KICA8YSBocmVmPSAiMDgtbHVicmlkYXRlLmh0bWwiIGNsYXNzID0gImJ0biBidG4tcHJpbWFyeSI+QW50ZXJpb3I8L2E+CiAgPGEgaHJlZj0gIjEwLWdyYWZpY29zLUlJSS5odG1sIiBjbGFzcyA9ICJidG4gYnRuLXByaW1hcnkiPlNpZ3VpZW50ZTwvYT4KPC9kaXY+