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:
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 NA
s, 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
.
## # 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+