Introduction

In the Czech Republic, only a fraction of psychotherapeutic care is covered by health insurance companies. So, it was a welcomed step when VZP, one of the largest such companies, offered its clients an allowance of 7 000 CZK (or circa 280 EUR) for psychotherapy.

However, the way of finding yourself a psychotherapist was not the most convenient one as you had to scroll through a .pdf file. Luckily, Dominika Čechová, a board member of the Czech Association for Psychotherapy, negotiated additional information from VZP like which of the psychotherapists can take new clients.

My part was to combine these inputs into a more user-friendly way of finding yourself a therapist and thus make the service more accessible. The focus was the speed of delivery and ease of deployment, so I decided to follow through R with flexdashboard, a package allowing for creating interactive yet serverless dashboards.

I had most of the building blocks covered from previous projects - R implementations of Leaflet for maps and DataTables for tables. Tidyverse for loading and wrangling the data.

However, the input table did not contain latitude and longitude, two elements needed for displaying the therapists’ location on a map. Previously, I had adjusted data using ggmap. However, the changes in the Google API requirements have made this somewhat inconvenient.

So, I decided to look for an alternative. Then I came across the article Geocode with Python by Abdishakur, a great introduction to Python’s package GeoPy.

Now, although R is my special data friend, I also like to integrate other languages and tools like Python and pandas in my workflow where beneficial.

You may find the final dashboard here. In this post, I would like to review how I proceed when creating the desired outcome.

R packages

Let us begin by loading the required packages:

# Load required packages
library(flexdashboard) # dashboard wrapping
library(tidyverse) # data wrangling
library(crosstalk) # interactivity
library(broom) # output of built-in functions cleanup
library(DT) # Table formatting
library(htmltools) # widgets
library(reshape2) # data taransformations
library(leaflet) # interactive maps
library(leaflet.extras) # interactive features

Input data

You may find the input dataset on GitHub.

# Initial dataset
vzp_data = read.csv("vzp_data_geo.csv") %>%
    select(name, surname, alias, website, address,
           city, region, psc, phone, email, Kapacita, remote_therapy)    

Python

Activation

First, you need to turn on the Python interface in R. I work with reticulate.

# Python interoperability in R
library(reticulate)

# Specifying which version of python to use.
use_python("/home/vg/anaconda3/bin/python3.7",
           required=T)  # Locate and run Python

Packages and data wrangling

Even though it is possible to run all of the following chunks of code at once, let us follow the do one thing principle and separate these according to their functions.

Python packages first:

from geopy.geocoders import Nominatim
from geopy.extra.rate_limiter import RateLimiter
import pandas as pd

Activate the geocoder:

locator = Nominatim(user_agent="myGeocoder")

Link parts of an address to one variable - will be used for geocoding:

df = r['vzp_data']

df['state'] = "CZ"

df["ADDRESS"] = df["address"] + "," + df["city"] + "," + df["state"]

df["Adresa"] = df["address"] + ", " + df["city"]

Geocoding using GeoPy

To mitigate the Too Many Requests error, use RateLimiter. Adding a delay “between geocoding calls to reduce the load on the Geocoding service”, as the documentation of GeoPy puts it.

# 1 - conveneint function to delay between geocoding calls
geocode = RateLimiter(locator.geocode, min_delay_seconds=1)

# 2- create location column
df['location'] = df['ADDRESS'].apply(geocode)
# 3 - create longitude, laatitude and altitude from location column (returns tuple)
df['point'] = df['location'].apply(lambda loc: tuple(loc.point) if loc else None)

# 4 - split point column into latitude, longitude and altitude columns
df[['latitude', 'longitude', 'altitude']] = pd.DataFrame(df['point'].tolist(), index=df.index)

Export the geocoded data

Now, you might be asking - why I am exporting the table? Am I not going to proceed with the data further as a Python/R object?

Unfortunately, flexdashboard cannot yet be knitted while containing Python code. So, when working on the dashboard, I had to create two separate workflows - one for the data geocoding and the second for compiling the dashboard. However, I wanted to make the dashboard coding more reproducible while keeping all of the important components in one workflow.

Fortunately, other R Markdown outputs like R notebook are happy wrapping Python code. That is why you see this step in code. Surely, it is not the most efficient workflow, but you know what they say - done (and working) is better than perfect.

Of course, if the goal would be an R Markdown document containing some Python chunks, you can simply add vzp_data = py[df] in an R chunk to convert a pandas DataFrame into R Data Frame and proceed smoothly to the next part.

# Convert pandas DataFrame into R Data Frame (tibble)
vzp_data_geocoded = py['df']

vzp_data_geo <- vzp_data_geocoded %>%
    select(name, surname, alias, website, address,
           city, region, psc, phone, email, `Kapacita`, remote_therapy,
           state, Adresa, latitude, longitude)

vzp_data_geo <- apply(vzp_data_geo,2,as.character)
write.csv(vzp_data_geo, file = "vzp_data_geo.csv")

R

Data wrangling in tidyverse

Select only the columns that will be used in the output and format them:

# Data import and wrangling
vzp_data = read.csv("vzp_data_geo.csv") %>%
  select(name,
         surname,
         alias,
         website,
         address,
         city,
         phone,
         email,
         Kapacita,
         remote_therapy,
         latitude,
         longitude) %>%
  mutate_all(list(~str_trim(.,side = "both"))) %>%
  mutate(`Jméno a příjmení` = paste(name,surname),
         latitude = as.numeric(latitude),
         longitude = as.numeric(longitude)) %>%
  rename("Email" = email,
         "Telefon" = phone,
         "Web" = website,
         "Online nebo telefonické konzultace?" = remote_therapy,
         "Kapacita?" = Kapacita,
         "Město" = city,
         "Ulice" = address) %>%
  mutate(`Kapacita?` = case_when(`Kapacita?` == "volno" ~ "Volno",
                              `Kapacita?` == "naplněno" ~ "Naplněno",
                              `Kapacita?` == "Naplněno" ~ "Naplněno",
                              TRUE ~ `Kapacita?`),
         `Online nebo telefonické konzultace?` = case_when(`Online nebo telefonické konzultace?` == "Ano" ~ "Ano",
                                                           `Online nebo telefonické konzultace?` == "Ano/ možnost konzultací v anglickém jazyce" ~ "Ano",
                                                           `Online nebo telefonické konzultace?` == "Ne" ~ "Ne",
                                                           `Online nebo telefonické konzultace?` == "ne" ~ "Ne",
                                                           TRUE ~ `Online nebo telefonické konzultace?`)
         )

# Replace "NaN" with "Neuvedeno"
vzp_data[vzp_data=="NaN"] <- "Neuvedeno"

Outputs

List of available psychotherapists

The following table is a snapshot of the final look-up table, done using DataTable package. This package means a number of interactive elements at your disposal. You can add search and filter across columns, sorting, pagination, and so forth. In addition, it could be very easily linked with filters using crosstalk.

As the output is in Czech, I suggest the Translate to English feature of your browser to get more value.

vzp_data_table <- vzp_data %>%
  select(`Kapacita?`, `Online nebo telefonické konzultace?`,
        Město,
        Ulice,
         `Jméno a příjmení`, 
         Email, Telefon, Web, Ulice,
         `Online nebo telefonické konzultace?`) 

test_shared <- crosstalk::SharedData$new(vzp_data_table)

DT::datatable(test_shared,
    extensions = c(
      "Responsive"
    ),
    rownames = FALSE,  # remove rownames
    style = "bootstrap",
    class = 'cell-border display',
    options = list(
      pageLength = 10,
      dom = 't',
      deferRender = TRUE,
      scroller = TRUE,
      columnDefs = list(list(className = 'dt-center', targets = "_all"))
        )
      ) %>% formatStyle(
  "Kapacita?",
  target = 'row',
  backgroundColor = styleEqual(c("Naplněno", "Volno"), c('#ffcccb', '#d2e9af')))

Map of available psychotherapists

As the search might be more convenient via a map, I decided to add one using leaflet. Similar to DataTable, leaflet allows for multiple interactive elements. Including search based on a string or different map layers.

Again, as the output is in Czech, I suggest the Translate to English feature of your browser to get more value.

# prepare a palette - manual colors according to branch column
pal <- leaflet::colorFactor(palette = c("Naplněno" = "#8b0000",
                                        "Neuvedeno" = "#A9A9A9",
                                        "Volno" = "#006400"
                                        ), 
                               domain = vzp_data$`Kapacita?`)

points_fin <- SharedData$new(vzp_data)

map1 <- leaflet(data = points_fin, width = '100%', height = 800) %>%
          addProviderTiles("CartoDB.Positron", group = 'Základní') %>%
          addProviderTiles("Esri.WorldImagery", group = 'Letecká') %>%
          addProviderTiles("OpenStreetMap.Mapnik", group = 'Uliční') %>%
          addProviderTiles("OpenTopoMap", group = 'Zeměpisná') %>%
          addScaleBar('bottomright') %>%
          setView(15.4129318, 49.7559455, zoom = 8.2) %>%
          addCircleMarkers(
                   group = 'Obor', 
                   stroke = FALSE, 
                   opacity = 0.9,
                   fillOpacity = 0.9,
                   fillColor = ~sapply(`Kapacita?`, switch, USE.NAMES = FALSE,
                                        "Volno" = "#006400", 
                                        "Naplněno" = "#8b0000",
                                        "Neuvedeno" = "#A9A9A9"
                                     ),
                   popup = ~paste0('<h2>Detail</h2> <br>',
                                   '<b>Město</b>: ', Město, '<br>',
                                   '<b>Ulice</b>: ', Ulice, '<br>',
                                   '<b>Jméno a příjmení</b>: ', `Jméno a příjmení`, '<br>',
                                   '<b>Online nebo telefonické konzultace</b>: ', `Online nebo telefonické konzultace?`, '<br>',
                                   '<b>Telefon</b>: ',`Telefon`, "<br>",
                                   '<b>Email</b>: ', Email, '<br>',
                                   '<b>Web</b>: ', Web, '<br>',
                                   '<b>Kapacita</b>: ', `Kapacita?`, '<br>')
                   ,
                   clusterOptions = markerClusterOptions(showCoverageOnHover = FALSE,
                    iconCreateFunction=JS("function (cluster) {    
    var childCount = cluster.getChildCount(); 
    var c = ' marker-cluster-';  
    if (childCount < 100) {  
      c += 'small';  
    } else if (childCount < 1000) {  
      c += 'medium';  
    } else { 
      c += 'large';  
    }    
    return new L.DivIcon({ html: '<div><span>' + childCount + '</span></div>', className: 'marker-cluster' + c, iconSize: new L.Point(40, 40) });

  }"))) %>%
  addLegend(position = "topright",
            values = ~`Kapacita?`,
            opacity = .7,
            pal = pal,
            title = "Kapacita?") %>%
  leaflet.extras::addResetMapButton() %>%
   addLayersControl(
                baseGroups = c("Základní", "Letecká", "Uliční", "Zeměpisná"),
                options = layersControlOptions(collapsed = TRUE)
              ) %>%
      addSearchOSM()

map1

Closing remarks

I believe that some will roll their eyebrow when integrating R and Python the way presented above. “Why not choose just one tool and use it?” you may ask yourself. In my case, I needed to add a piece to my template. Although its parts had been written in R, I did not want to limit myself to one tool only.

At the same time, by looking at the current limits, we can promote future ease of integration. Here, I have to acknowledge that RStudio, in my view one of the best tools for working with data, has taken several significant steps in integrating Python. However, in particular use-cases, it is still a somewhat bumpy ride.

Ultimately, I wanted to experiment and test the limits. I believe that both Python and R have their pros and cons when working with data, so why not explore where you can go if you combine them?

   

Go back to Blog

LS0tCnRpdGxlOiAiQ29tYmluaW5nICoqUioqIHsqdGlkeXZlcnNlLCBsZWFmbGV0LCBEVCwgZmxleGRhc2hib2FyZCp9IGFuZCAqKlB5dGhvbioqIHsqcGFuZGFzLCBHZW9QeSp9IHRvIGNyZWF0ZSBhIHNlcnZlcmxlc3MgcHN5Y2hvdGhlcmFweSBkYXNoYm9hcmQiCm91dHB1dDoKICBodG1sX25vdGVib29rOgogICAgYWxsaWduOiByaWdodAogICAgdGhlbWU6IGZsYXRseQogICAgaGlnaGxpZ2h0OiBtb25vY2hyb21lCiAgICBjc3M6IH4vRGVza3RvcC9HaXQvUGVyc29uYWwvZGF0YW11c3RmbG93L3B1YmxpYy9jc3MvY29kZXIubWluLmE0ZjMzMjIxM2EyMWNlOGViNTIxNjcwYzYxNDQ3MGM1ODkyM2FhYWYzODVlMmE3Mzk4MmMzMWRkNzY0MmRlY2IuY3NzCiAgICB0b2M6IHllcwogICAgdG9jX2RlcHRoOiA0CiAgICB0b2NfZmxvYXQ6IHllcwotLS0KCiMjIyBJbnRyb2R1Y3Rpb24KCkluIHRoZSBDemVjaCBSZXB1YmxpYywgb25seSBhICoqZnJhY3Rpb24gb2YgcHN5Y2hvdGhlcmFwZXV0aWMgY2FyZSoqIGlzICoqY292ZXJlZCoqIGJ5ICoqaGVhbHRoIGluc3VyYW5jZSoqIGNvbXBhbmllcy4gU28sIGl0IHdhcyBhIHdlbGNvbWVkIHN0ZXAgd2hlbiBWWlAsIG9uZSBvZiB0aGUgbGFyZ2VzdCBzdWNoIGNvbXBhbmllcywgb2ZmZXJlZCBpdHMgY2xpZW50cyBhbiBhbGxvd2FuY2Ugb2YgNyAwMDAgQ1pLIChvciBjaXJjYSAyODAgRVVSKSBmb3IgW3BzeWNob3RoZXJhcHldKGh0dHBzOi8vd3d3LnZ6cC5jei9wb2ppc3RlbmNpL3Z5aG9keS1hLXByaXNwZXZreS9kb3NwZWxpL3ByaXNwZXZlay1uYS1wb2Rwb3J1LXp2eXNlbmktZG9zdHVwbm9zdGktcHN5Y2hvc29jaWFsbmktcG9kcG9yeSkuCgpIb3dldmVyLCB0aGUgd2F5IG9mIGZpbmRpbmcgeW91cnNlbGYgYSBwc3ljaG90aGVyYXBpc3Qgd2FzIG5vdCB0aGUgbW9zdCBjb252ZW5pZW50IG9uZSBhcyB5b3UgaGFkIHRvIHNjcm9sbCB0aHJvdWdoIGEgLnBkZiBbZmlsZV0oaHR0cHM6Ly9tZWRpYS52enBzdGF0aWMuY3ovbWVkaWEvRGVmYXVsdC9kb2t1bWVudHkvc2V6bmFtLXRlcmFwZXV0dS5wZGY/dmVyPTAzMDMpLiBMdWNraWx5LCBbRG9taW5pa2EgxIxlY2hvdsOhXShodHRwczovL3d3dy5saW5rZWRpbi5jb20vaW4vZG9taW5pa2EtJUM0JThEZWNob3YlQzMlQTEtbS1hLWI2Yjk1NzI0LyksIGEgYm9hcmQgbWVtYmVyIG9mIHRoZSBbKkN6ZWNoIEFzc29jaWF0aW9uIGZvciBQc3ljaG90aGVyYXB5Kl0oaHR0cHM6Ly9jemFwLmN6L2VuZyksIG5lZ290aWF0ZWQgYWRkaXRpb25hbCBpbmZvcm1hdGlvbiBmcm9tIFZaUCBsaWtlIHdoaWNoIG9mIHRoZSBwc3ljaG90aGVyYXBpc3RzIGNhbiB0YWtlIG5ldyBjbGllbnRzLgoKTXkgcGFydCB3YXMgdG8gY29tYmluZSB0aGVzZSBpbnB1dHMgaW50byBhIG1vcmUgdXNlci1mcmllbmRseSB3YXkgb2YgZmluZGluZyB5b3Vyc2VsZiBhIHRoZXJhcGlzdCBhbmQgdGh1cyBtYWtlIHRoZSBzZXJ2aWNlIG1vcmUgKiphY2Nlc3NpYmxlKiouIFRoZSBmb2N1cyB3YXMgdGhlIHNwZWVkIG9mIGRlbGl2ZXJ5IGFuZCBlYXNlIG9mIGRlcGxveW1lbnQsIHNvIEkgZGVjaWRlZCB0byBmb2xsb3cgdGhyb3VnaCAqKlIqKiB3aXRoIFsqKmZsZXhkYXNoYm9hcmQqKl0oaHR0cHM6Ly9ybWFya2Rvd24ucnN0dWRpby5jb20vZmxleGRhc2hib2FyZC8pLCBhIHBhY2thZ2UgYWxsb3dpbmcgZm9yIGNyZWF0aW5nICoqaW50ZXJhY3RpdmUqKiB5ZXQgKipzZXJ2ZXJsZXNzKiogZGFzaGJvYXJkcy4KCkkgaGFkIG1vc3Qgb2YgdGhlIGJ1aWxkaW5nIGJsb2NrcyBjb3ZlcmVkIGZyb20gcHJldmlvdXMgW3Byb2plY3RzXShodHRwczovL3d3dy5kYXRhLW11c3QtZmxvdy5jb20vcG9zdC8pIC0gUiBpbXBsZW1lbnRhdGlvbnMgb2YgWyoqTGVhZmxldCoqXShodHRwczovL3JzdHVkaW8uZ2l0aHViLmlvL2xlYWZsZXQvKSBmb3IgbWFwcyBhbmQgWyoqRGF0YVRhYmxlcyoqXShodHRwczovL3JzdHVkaW8uZ2l0aHViLmlvL0RULykgZm9yIHRhYmxlcy4gWyoqVGlkeXZlcnNlKipdKGh0dHBzOi8vd3d3LnRpZHl2ZXJzZS5vcmcvKSBmb3IgbG9hZGluZyBhbmQgd3JhbmdsaW5nIHRoZSBkYXRhLgoKSG93ZXZlciwgdGhlIGlucHV0IHRhYmxlIGRpZCBub3QgY29udGFpbiBsYXRpdHVkZSBhbmQgbG9uZ2l0dWRlLCB0d28gZWxlbWVudHMgbmVlZGVkIGZvciBkaXNwbGF5aW5nIHRoZSB0aGVyYXBpc3RzJyBsb2NhdGlvbiBvbiBhIG1hcC4gUHJldmlvdXNseSwgSSBoYWQgYWRqdXN0ZWQgZGF0YSB1c2luZyBbKipnZ21hcCoqXShodHRwczovL2dpdGh1Yi5jb20vZGthaGxlL2dnbWFwKS4gSG93ZXZlciwgdGhlIGNoYW5nZXMgaW4gdGhlIEdvb2dsZSBBUEkgcmVxdWlyZW1lbnRzIGhhdmUgbWFkZSB0aGlzIHNvbWV3aGF0IGluY29udmVuaWVudC4gCgpTbywgSSBkZWNpZGVkIHRvIGxvb2sgZm9yIGFuIGFsdGVybmF0aXZlLiBUaGVuIEkgY2FtZSBhY3Jvc3MgdGhlIGFydGljbGUgWypHZW9jb2RlIHdpdGggUHl0aG9uKl0oaHR0cHM6Ly90b3dhcmRzZGF0YXNjaWVuY2UuY29tL2dlb2NvZGUtd2l0aC1weXRob24tMTYxZWMxZTYyYjg5KSBieSBbQWJkaXNoYWt1cl0oaHR0cHM6Ly9zaGFrYXNvbS5tZWRpdW0uY29tLyksIGEgZ3JlYXQgaW50cm9kdWN0aW9uIHRvIFB5dGhvbidzIHBhY2thZ2UgWyoqR2VvUHkqKl0oaHR0cHM6Ly9nZW9weS5yZWFkdGhlZG9jcy5pby9lbi9zdGFibGUvKS4KCk5vdywgYWx0aG91Z2ggUiBpcyBteSBzcGVjaWFsIGRhdGEgZnJpZW5kLCBJIGFsc28gbGlrZSB0byAqKmludGVncmF0ZSoqIG90aGVyIGxhbmd1YWdlcyBhbmQgdG9vbHMgbGlrZSAqKlB5dGhvbioqIGFuZCBbKipwYW5kYXMqKl0oaHR0cHM6Ly9wYW5kYXMucHlkYXRhLm9yZy8pIGluIG15IHdvcmtmbG93IHdoZXJlIGJlbmVmaWNpYWwuIAoKWW91IG1heSBmaW5kIHRoZSBmaW5hbCBkYXNoYm9hcmQgW2hlcmVdKGh0dHBzOi8vd3d3LmRhdGEtbXVzdC1mbG93LmNvbS9wb3N0cy9wc3ljaG90aGVyYXB5X3Z6cC8pLiBJbiB0aGlzIHBvc3QsIEkgd291bGQgbGlrZSB0byByZXZpZXcgaG93IEkgcHJvY2VlZCB3aGVuIGNyZWF0aW5nIHRoZSBkZXNpcmVkIG91dGNvbWUuCgojIyMgUiBwYWNrYWdlcwoKTGV0IHVzIGJlZ2luIGJ5IGxvYWRpbmcgdGhlIHJlcXVpcmVkIHBhY2thZ2VzOgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGVycm9yPUZBTFNFfQojIExvYWQgcmVxdWlyZWQgcGFja2FnZXMKbGlicmFyeShmbGV4ZGFzaGJvYXJkKSAjIGRhc2hib2FyZCB3cmFwcGluZwpsaWJyYXJ5KHRpZHl2ZXJzZSkgIyBkYXRhIHdyYW5nbGluZwpsaWJyYXJ5KGNyb3NzdGFsaykgIyBpbnRlcmFjdGl2aXR5CmxpYnJhcnkoYnJvb20pICMgb3V0cHV0IG9mIGJ1aWx0LWluIGZ1bmN0aW9ucyBjbGVhbnVwCmxpYnJhcnkoRFQpICMgVGFibGUgZm9ybWF0dGluZwpsaWJyYXJ5KGh0bWx0b29scykgIyB3aWRnZXRzCmxpYnJhcnkocmVzaGFwZTIpICMgZGF0YSB0YXJhbnNmb3JtYXRpb25zCmxpYnJhcnkobGVhZmxldCkgIyBpbnRlcmFjdGl2ZSBtYXBzCmxpYnJhcnkobGVhZmxldC5leHRyYXMpICMgaW50ZXJhY3RpdmUgZmVhdHVyZXMKYGBgCgojIyMgSW5wdXQgZGF0YQoKWW91IG1heSBmaW5kIHRoZSBpbnB1dCBkYXRhc2V0IG9uIFtHaXRIdWJdKCkuCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXJyb3I9RkFMU0V9CiMgSW5pdGlhbCBkYXRhc2V0CnZ6cF9kYXRhID0gcmVhZC5jc3YoInZ6cF9kYXRhX2dlby5jc3YiKSAlPiUKICAgIHNlbGVjdChuYW1lLCBzdXJuYW1lLCBhbGlhcywgd2Vic2l0ZSwgYWRkcmVzcywKICAgICAgICAgICBjaXR5LCByZWdpb24sIHBzYywgcGhvbmUsIGVtYWlsLCBLYXBhY2l0YSwgcmVtb3RlX3RoZXJhcHkpICAgIApgYGAKCiMjIyBQeXRob24KCiMjIyMgKkFjdGl2YXRpb24qCgpGaXJzdCwgeW91IG5lZWQgdG8gdHVybiBvbiB0aGUgUHl0aG9uIGludGVyZmFjZSBpbiBSLiBJIHdvcmsgd2l0aCBbKipyZXRpY3VsYXRlKipdKGh0dHBzOi8vcnN0dWRpby5naXRodWIuaW8vcmV0aWN1bGF0ZS9pbmRleC5odG1sKS4KCmBgYHtyLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCBlcnJvcj1GQUxTRX0KIyBQeXRob24gaW50ZXJvcGVyYWJpbGl0eSBpbiBSCmxpYnJhcnkocmV0aWN1bGF0ZSkKCiMgU3BlY2lmeWluZyB3aGljaCB2ZXJzaW9uIG9mIHB5dGhvbiB0byB1c2UuCnVzZV9weXRob24oIi9ob21lL3ZnL2FuYWNvbmRhMy9iaW4vcHl0aG9uMy43IiwKICAgICAgICAgICByZXF1aXJlZD1UKSAgIyBMb2NhdGUgYW5kIHJ1biBQeXRob24KYGBgCgojIyMjICpQYWNrYWdlcyBhbmQgZGF0YSB3cmFuZ2xpbmcqCgpFdmVuIHRob3VnaCBpdCBpcyBwb3NzaWJsZSB0byBydW4gYWxsIG9mIHRoZSBmb2xsb3dpbmcgY2h1bmtzIG9mIGNvZGUgYXQgb25jZSwgbGV0IHVzIGZvbGxvdyB0aGUgW2RvIG9uZSB0aGluZ10oaHR0cHM6Ly9ibG9nLmNvZGluZ2hvcnJvci5jb20vY3VybHlzLWxhdy1kby1vbmUtdGhpbmcvKSBwcmluY2lwbGUgYW5kIHNlcGFyYXRlIHRoZXNlIGFjY29yZGluZyB0byB0aGVpciBmdW5jdGlvbnMuCgpQeXRob24gcGFja2FnZXMgZmlyc3Q6CgpgYGB7cHl0aG9uLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCBlcnJvcj1GQUxTRX0KZnJvbSBnZW9weS5nZW9jb2RlcnMgaW1wb3J0IE5vbWluYXRpbQpmcm9tIGdlb3B5LmV4dHJhLnJhdGVfbGltaXRlciBpbXBvcnQgUmF0ZUxpbWl0ZXIKaW1wb3J0IHBhbmRhcyBhcyBwZApgYGAKCkFjdGl2YXRlIHRoZSBnZW9jb2RlcjoKCmBgYHtweXRob24sIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGVycm9yPUZBTFNFfQpsb2NhdG9yID0gTm9taW5hdGltKHVzZXJfYWdlbnQ9Im15R2VvY29kZXIiKQpgYGAKCkxpbmsgcGFydHMgb2YgYW4gYWRkcmVzcyB0byBvbmUgdmFyaWFibGUgLSB3aWxsIGJlIHVzZWQgZm9yIGdlb2NvZGluZzoKCmBgYHtweXRob24sIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGVycm9yPUZBTFNFfQpkZiA9IHJbJ3Z6cF9kYXRhJ10KCmRmWydzdGF0ZSddID0gIkNaIgoKZGZbIkFERFJFU1MiXSA9IGRmWyJhZGRyZXNzIl0gKyAiLCIgKyBkZlsiY2l0eSJdICsgIiwiICsgZGZbInN0YXRlIl0KCmRmWyJBZHJlc2EiXSA9IGRmWyJhZGRyZXNzIl0gKyAiLCAiICsgZGZbImNpdHkiXQpgYGAKCiMjIyMgKkdlb2NvZGluZyB1c2luZyBHZW9QeSoKClRvIG1pdGlnYXRlIHRoZSBgVG9vIE1hbnkgUmVxdWVzdHNgIGVycm9yLCB1c2UgYFJhdGVMaW1pdGVyYC4gQWRkaW5nIGEgZGVsYXkgIipiZXR3ZWVuIGdlb2NvZGluZyBjYWxscyB0byByZWR1Y2UgdGhlIGxvYWQgb24gdGhlIEdlb2NvZGluZyBzZXJ2aWNlKiIsIGFzIHRoZSBbZG9jdW1lbnRhdGlvbl0oaHR0cHM6Ly9nZW9weS5yZWFkdGhlZG9jcy5pby9lbi9zdGFibGUvKSBvZiBHZW9QeSBwdXRzIGl0LgoKYGBge3B5dGhvbiwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXJyb3I9RkFMU0V9CiMgMSAtIGNvbnZlbmVpbnQgZnVuY3Rpb24gdG8gZGVsYXkgYmV0d2VlbiBnZW9jb2RpbmcgY2FsbHMKZ2VvY29kZSA9IFJhdGVMaW1pdGVyKGxvY2F0b3IuZ2VvY29kZSwgbWluX2RlbGF5X3NlY29uZHM9MSkKCiMgMi0gY3JlYXRlIGxvY2F0aW9uIGNvbHVtbgpkZlsnbG9jYXRpb24nXSA9IGRmWydBRERSRVNTJ10uYXBwbHkoZ2VvY29kZSkKYGBgCgpgYGB7cHl0aG9uLCB3YXJuaW5nPUZBTFNFLCBtZXNzYWdlPUZBTFNFLCBlcnJvcj1GQUxTRX0KIyAzIC0gY3JlYXRlIGxvbmdpdHVkZSwgbGFhdGl0dWRlIGFuZCBhbHRpdHVkZSBmcm9tIGxvY2F0aW9uIGNvbHVtbiAocmV0dXJucyB0dXBsZSkKZGZbJ3BvaW50J10gPSBkZlsnbG9jYXRpb24nXS5hcHBseShsYW1iZGEgbG9jOiB0dXBsZShsb2MucG9pbnQpIGlmIGxvYyBlbHNlIE5vbmUpCgojIDQgLSBzcGxpdCBwb2ludCBjb2x1bW4gaW50byBsYXRpdHVkZSwgbG9uZ2l0dWRlIGFuZCBhbHRpdHVkZSBjb2x1bW5zCmRmW1snbGF0aXR1ZGUnLCAnbG9uZ2l0dWRlJywgJ2FsdGl0dWRlJ11dID0gcGQuRGF0YUZyYW1lKGRmWydwb2ludCddLnRvbGlzdCgpLCBpbmRleD1kZi5pbmRleCkKYGBgCgojIyMjICpFeHBvcnQgdGhlIGdlb2NvZGVkIGRhdGEqCgpOb3csIHlvdSBtaWdodCBiZSBhc2tpbmcgLSB3aHkgSSBhbSBleHBvcnRpbmcgdGhlIHRhYmxlPyBBbSBJIG5vdCBnb2luZyB0byBwcm9jZWVkIHdpdGggdGhlIGRhdGEgZnVydGhlciBhcyBhIFB5dGhvbi9SIG9iamVjdD8gCgpVbmZvcnR1bmF0ZWx5LCBmbGV4ZGFzaGJvYXJkIFtjYW5ub3QgeWV0IGJlIGtuaXR0ZWRdKGh0dHBzOi8vY29tbXVuaXR5LnJzdHVkaW8uY29tL3QvcHl0aG9uLWNvZGUtaW5zaWRlLWZsZXhkYXNoYm9hcmQtZnJhbWV3b3JrLzQ0ODczLzIpIHdoaWxlIGNvbnRhaW5pbmcgUHl0aG9uIGNvZGUuIFNvLCB3aGVuIHdvcmtpbmcgb24gdGhlIGRhc2hib2FyZCwgSSBoYWQgdG8gY3JlYXRlIHR3byBzZXBhcmF0ZSB3b3JrZmxvd3MgLSBvbmUgZm9yIHRoZSBkYXRhIGdlb2NvZGluZyBhbmQgdGhlIHNlY29uZCBmb3IgY29tcGlsaW5nIHRoZSBkYXNoYm9hcmQuIEhvd2V2ZXIsIEkgd2FudGVkIHRvIG1ha2UgdGhlIGRhc2hib2FyZCBjb2RpbmcgbW9yZSByZXByb2R1Y2libGUgd2hpbGUga2VlcGluZyBhbGwgb2YgdGhlIGltcG9ydGFudCBjb21wb25lbnRzIGluIG9uZSB3b3JrZmxvdy4gCgpGb3J0dW5hdGVseSwgb3RoZXIgUiBNYXJrZG93biBvdXRwdXRzIGxpa2UgUiBub3RlYm9vayBhcmUgaGFwcHkgd3JhcHBpbmcgUHl0aG9uIGNvZGUuIFRoYXQgaXMgd2h5IHlvdSBzZWUgdGhpcyBzdGVwIGluIGNvZGUuIFN1cmVseSwgaXQgaXMgbm90IHRoZSBtb3N0IGVmZmljaWVudCB3b3JrZmxvdywgYnV0IHlvdSBrbm93IHdoYXQgdGhleSBzYXkgLSBkb25lIChhbmQgd29ya2luZykgaXMgYmV0dGVyIHRoYW4gcGVyZmVjdC4KCk9mIGNvdXJzZSwgaWYgdGhlIGdvYWwgd291bGQgYmUgYW4gUiBNYXJrZG93biBkb2N1bWVudCBjb250YWluaW5nIHNvbWUgUHl0aG9uIGNodW5rcywgeW91IGNhbiBzaW1wbHkgYWRkIGB2enBfZGF0YSA9IHB5W2RmXWAgaW4gYW4gUiBjaHVuayB0byBjb252ZXJ0IGEgcGFuZGFzIERhdGFGcmFtZSBpbnRvIFIgRGF0YSBGcmFtZSBhbmQgcHJvY2VlZCBzbW9vdGhseSB0byB0aGUgbmV4dCBwYXJ0LgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGVycm9yPUZBTFNFfQojIENvbnZlcnQgcGFuZGFzIERhdGFGcmFtZSBpbnRvIFIgRGF0YSBGcmFtZSAodGliYmxlKQp2enBfZGF0YV9nZW9jb2RlZCA9IHB5WydkZiddCgp2enBfZGF0YV9nZW8gPC0gdnpwX2RhdGFfZ2VvY29kZWQgJT4lCiAgICBzZWxlY3QobmFtZSwgc3VybmFtZSwgYWxpYXMsIHdlYnNpdGUsIGFkZHJlc3MsCiAgICAgICAgICAgY2l0eSwgcmVnaW9uLCBwc2MsIHBob25lLCBlbWFpbCwgYEthcGFjaXRhYCwgcmVtb3RlX3RoZXJhcHksCiAgICAgICAgICAgc3RhdGUsIEFkcmVzYSwgbGF0aXR1ZGUsIGxvbmdpdHVkZSkKCnZ6cF9kYXRhX2dlbyA8LSBhcHBseSh2enBfZGF0YV9nZW8sMixhcy5jaGFyYWN0ZXIpCndyaXRlLmNzdih2enBfZGF0YV9nZW8sIGZpbGUgPSAidnpwX2RhdGFfZ2VvLmNzdiIpCmBgYAoKIyMjIFIKCiMjIyMgKkRhdGEgd3JhbmdsaW5nIGluIHRpZHl2ZXJzZSoKClNlbGVjdCBvbmx5IHRoZSBjb2x1bW5zIHRoYXQgd2lsbCBiZSB1c2VkIGluIHRoZSBvdXRwdXQgYW5kIGZvcm1hdCB0aGVtOgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGVycm9yPUZBTFNFfQojIERhdGEgaW1wb3J0IGFuZCB3cmFuZ2xpbmcKdnpwX2RhdGEgPSByZWFkLmNzdigidnpwX2RhdGFfZ2VvLmNzdiIpICU+JQogIHNlbGVjdChuYW1lLAogICAgICAgICBzdXJuYW1lLAogICAgICAgICBhbGlhcywKICAgICAgICAgd2Vic2l0ZSwKICAgICAgICAgYWRkcmVzcywKICAgICAgICAgY2l0eSwKICAgICAgICAgcGhvbmUsCiAgICAgICAgIGVtYWlsLAogICAgICAgICBLYXBhY2l0YSwKICAgICAgICAgcmVtb3RlX3RoZXJhcHksCiAgICAgICAgIGxhdGl0dWRlLAogICAgICAgICBsb25naXR1ZGUpICU+JQogIG11dGF0ZV9hbGwobGlzdCh+c3RyX3RyaW0oLixzaWRlID0gImJvdGgiKSkpICU+JQogIG11dGF0ZShgSm3DqW5vIGEgcMWZw61qbWVuw61gID0gcGFzdGUobmFtZSxzdXJuYW1lKSwKICAgICAgICAgbGF0aXR1ZGUgPSBhcy5udW1lcmljKGxhdGl0dWRlKSwKICAgICAgICAgbG9uZ2l0dWRlID0gYXMubnVtZXJpYyhsb25naXR1ZGUpKSAlPiUKICByZW5hbWUoIkVtYWlsIiA9IGVtYWlsLAogICAgICAgICAiVGVsZWZvbiIgPSBwaG9uZSwKICAgICAgICAgIldlYiIgPSB3ZWJzaXRlLAogICAgICAgICAiT25saW5lIG5lYm8gdGVsZWZvbmlja8OpIGtvbnp1bHRhY2U/IiA9IHJlbW90ZV90aGVyYXB5LAogICAgICAgICAiS2FwYWNpdGE/IiA9IEthcGFjaXRhLAogICAgICAgICAiTcSbc3RvIiA9IGNpdHksCiAgICAgICAgICJVbGljZSIgPSBhZGRyZXNzKSAlPiUKICBtdXRhdGUoYEthcGFjaXRhP2AgPSBjYXNlX3doZW4oYEthcGFjaXRhP2AgPT0gInZvbG5vIiB+ICJWb2xubyIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGBLYXBhY2l0YT9gID09ICJuYXBsbmXMjG5vIiB+ICJOYXBsbsSbbm8iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICBgS2FwYWNpdGE/YCA9PSAiTmFwbG5lzIxubyIgfiAiTmFwbG7Em25vIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgVFJVRSB+IGBLYXBhY2l0YT9gKSwKICAgICAgICAgYE9ubGluZSBuZWJvIHRlbGVmb25pY2vDqSBrb256dWx0YWNlP2AgPSBjYXNlX3doZW4oYE9ubGluZSBuZWJvIHRlbGVmb25pY2vDqSBrb256dWx0YWNlP2AgPT0gIkFubyIgfiAiQW5vIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBgT25saW5lIG5lYm8gdGVsZWZvbmlja8OpIGtvbnp1bHRhY2U/YCA9PSAiQW5vLyBtb8W+bm9zdCBrb256dWx0YWPDrSB2IGFuZ2xpY2vDqW0gamF6eWNlIiB+ICJBbm8iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGBPbmxpbmUgbmVibyB0ZWxlZm9uaWNrw6kga29uenVsdGFjZT9gID09ICJOZSIgfiAiTmUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGBPbmxpbmUgbmVibyB0ZWxlZm9uaWNrw6kga29uenVsdGFjZT9gID09ICJuZSIgfiAiTmUiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIFRSVUUgfiBgT25saW5lIG5lYm8gdGVsZWZvbmlja8OpIGtvbnp1bHRhY2U/YCkKICAgICAgICAgKQoKIyBSZXBsYWNlICJOYU4iIHdpdGggIk5ldXZlZGVubyIKdnpwX2RhdGFbdnpwX2RhdGE9PSJOYU4iXSA8LSAiTmV1dmVkZW5vIgpgYGAKCiMjIyBPdXRwdXRzCgojIyMjICpMaXN0IG9mIGF2YWlsYWJsZSBwc3ljaG90aGVyYXBpc3RzKgoKVGhlIGZvbGxvd2luZyB0YWJsZSBpcyBhIHNuYXBzaG90IG9mIHRoZSBmaW5hbCBbbG9vay11cCB0YWJsZV0oaHR0cHM6Ly93d3cuZGF0YS1tdXN0LWZsb3cuY29tL3Bvc3RzL3BzeWNob3RoZXJhcHlfdnpwLyksIGRvbmUgdXNpbmcgYERhdGFUYWJsZWAgcGFja2FnZS4gVGhpcyBwYWNrYWdlIG1lYW5zIGEgbnVtYmVyIG9mIGludGVyYWN0aXZlIGVsZW1lbnRzIGF0IHlvdXIgZGlzcG9zYWwuIFlvdSBjYW4gYWRkIHNlYXJjaCBhbmQgZmlsdGVyIGFjcm9zcyBjb2x1bW5zLCBzb3J0aW5nLCBwYWdpbmF0aW9uLCBhbmQgc28gZm9ydGguIEluIGFkZGl0aW9uLCBpdCBjb3VsZCBiZSB2ZXJ5IGVhc2lseSBsaW5rZWQgd2l0aCBmaWx0ZXJzIHVzaW5nIGBjcm9zc3RhbGtgLgoKQXMgdGhlIG91dHB1dCBpcyBpbiBDemVjaCwgSSBzdWdnZXN0IHRoZSBgVHJhbnNsYXRlIHRvIEVuZ2xpc2hgIGZlYXR1cmUgb2YgeW91ciBicm93c2VyIHRvIGdldCBtb3JlIHZhbHVlLgoKYGBge3IsIHdhcm5pbmc9RkFMU0UsIG1lc3NhZ2U9RkFMU0UsIGVycm9yPUZBTFNFfQp2enBfZGF0YV90YWJsZSA8LSB2enBfZGF0YSAlPiUKICBzZWxlY3QoYEthcGFjaXRhP2AsIGBPbmxpbmUgbmVibyB0ZWxlZm9uaWNrw6kga29uenVsdGFjZT9gLAogICAgICAgIE3Em3N0bywKICAgICAgICBVbGljZSwKICAgICAgICAgYEptw6lubyBhIHDFmcOtam1lbsOtYCwgCiAgICAgICAgIEVtYWlsLCBUZWxlZm9uLCBXZWIsIFVsaWNlLAogICAgICAgICBgT25saW5lIG5lYm8gdGVsZWZvbmlja8OpIGtvbnp1bHRhY2U/YCkgCgp0ZXN0X3NoYXJlZCA8LSBjcm9zc3RhbGs6OlNoYXJlZERhdGEkbmV3KHZ6cF9kYXRhX3RhYmxlKQoKRFQ6OmRhdGF0YWJsZSh0ZXN0X3NoYXJlZCwKICAgIGV4dGVuc2lvbnMgPSBjKAogICAgICAiUmVzcG9uc2l2ZSIKICAgICksCiAgICByb3duYW1lcyA9IEZBTFNFLCAgIyByZW1vdmUgcm93bmFtZXMKICAgIHN0eWxlID0gImJvb3RzdHJhcCIsCiAgICBjbGFzcyA9ICdjZWxsLWJvcmRlciBkaXNwbGF5JywKICAgIG9wdGlvbnMgPSBsaXN0KAogICAgICBwYWdlTGVuZ3RoID0gMTAsCiAgICAgIGRvbSA9ICd0JywKICAgICAgZGVmZXJSZW5kZXIgPSBUUlVFLAogICAgICBzY3JvbGxlciA9IFRSVUUsCiAgICAgIGNvbHVtbkRlZnMgPSBsaXN0KGxpc3QoY2xhc3NOYW1lID0gJ2R0LWNlbnRlcicsIHRhcmdldHMgPSAiX2FsbCIpKQogICAgICAgICkKICAgICAgKSAlPiUgZm9ybWF0U3R5bGUoCiAgIkthcGFjaXRhPyIsCiAgdGFyZ2V0ID0gJ3JvdycsCiAgYmFja2dyb3VuZENvbG9yID0gc3R5bGVFcXVhbChjKCJOYXBsbsSbbm8iLCAiVm9sbm8iKSwgYygnI2ZmY2NjYicsICcjZDJlOWFmJykpKQpgYGAKCiMjIyMgKk1hcCBvZiBhdmFpbGFibGUgcHN5Y2hvdGhlcmFwaXN0cyoKCkFzIHRoZSBzZWFyY2ggbWlnaHQgYmUgbW9yZSBjb252ZW5pZW50IHZpYSBhIG1hcCwgSSBkZWNpZGVkIHRvIGFkZCBvbmUgdXNpbmcgbGVhZmxldC4gU2ltaWxhciB0byBEYXRhVGFibGUsIGxlYWZsZXQgYWxsb3dzIGZvciBtdWx0aXBsZSBpbnRlcmFjdGl2ZSBlbGVtZW50cy4gSW5jbHVkaW5nIHNlYXJjaCBiYXNlZCBvbiBhIHN0cmluZyBvciBkaWZmZXJlbnQgbWFwIGxheWVycy4KCkFnYWluLCBhcyB0aGUgb3V0cHV0IGlzIGluIEN6ZWNoLCBJIHN1Z2dlc3QgdGhlIGBUcmFuc2xhdGUgdG8gRW5nbGlzaGAgZmVhdHVyZSBvZiB5b3VyIGJyb3dzZXIgdG8gZ2V0IG1vcmUgdmFsdWUuCgpgYGB7ciwgd2FybmluZz1GQUxTRSwgbWVzc2FnZT1GQUxTRSwgZXJyb3I9RkFMU0V9CiMgcHJlcGFyZSBhIHBhbGV0dGUgLSBtYW51YWwgY29sb3JzIGFjY29yZGluZyB0byBicmFuY2ggY29sdW1uCnBhbCA8LSBsZWFmbGV0Ojpjb2xvckZhY3RvcihwYWxldHRlID0gYygiTmFwbG7Em25vIiA9ICIjOGIwMDAwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJOZXV2ZWRlbm8iID0gIiNBOUE5QTkiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlZvbG5vIiA9ICIjMDA2NDAwIgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBkb21haW4gPSB2enBfZGF0YSRgS2FwYWNpdGE/YCkKCnBvaW50c19maW4gPC0gU2hhcmVkRGF0YSRuZXcodnpwX2RhdGEpCgptYXAxIDwtIGxlYWZsZXQoZGF0YSA9IHBvaW50c19maW4sIHdpZHRoID0gJzEwMCUnLCBoZWlnaHQgPSA4MDApICU+JQogICAgICAgICAgYWRkUHJvdmlkZXJUaWxlcygiQ2FydG9EQi5Qb3NpdHJvbiIsIGdyb3VwID0gJ1rDoWtsYWRuw60nKSAlPiUKICAgICAgICAgIGFkZFByb3ZpZGVyVGlsZXMoIkVzcmkuV29ybGRJbWFnZXJ5IiwgZ3JvdXAgPSAnTGV0ZWNrw6EnKSAlPiUKICAgICAgICAgIGFkZFByb3ZpZGVyVGlsZXMoIk9wZW5TdHJlZXRNYXAuTWFwbmlrIiwgZ3JvdXAgPSAnVWxpxI1uw60nKSAlPiUKICAgICAgICAgIGFkZFByb3ZpZGVyVGlsZXMoIk9wZW5Ub3BvTWFwIiwgZ3JvdXAgPSAnWmVtxJtwaXNuw6EnKSAlPiUKICAgICAgICAgIGFkZFNjYWxlQmFyKCdib3R0b21yaWdodCcpICU+JQogICAgICAgICAgc2V0VmlldygxNS40MTI5MzE4LCA0OS43NTU5NDU1LCB6b29tID0gOC4yKSAlPiUKICAgICAgICAgIGFkZENpcmNsZU1hcmtlcnMoCiAgICAgICAgICAgICAgICAgICBncm91cCA9ICdPYm9yJywgCiAgICAgICAgICAgICAgICAgICBzdHJva2UgPSBGQUxTRSwgCiAgICAgICAgICAgICAgICAgICBvcGFjaXR5ID0gMC45LAogICAgICAgICAgICAgICAgICAgZmlsbE9wYWNpdHkgPSAwLjksCiAgICAgICAgICAgICAgICAgICBmaWxsQ29sb3IgPSB+c2FwcGx5KGBLYXBhY2l0YT9gLCBzd2l0Y2gsIFVTRS5OQU1FUyA9IEZBTFNFLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIlZvbG5vIiA9ICIjMDA2NDAwIiwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAiTmFwbG7Em25vIiA9ICIjOGIwMDAwIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJOZXV2ZWRlbm8iID0gIiNBOUE5QTkiCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICApLAogICAgICAgICAgICAgICAgICAgcG9wdXAgPSB+cGFzdGUwKCc8aDI+RGV0YWlsPC9oMj4gPGJyPicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzxiPk3Em3N0bzwvYj46ICcsIE3Em3N0bywgJzxicj4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc8Yj5VbGljZTwvYj46ICcsIFVsaWNlLCAnPGJyPicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzxiPkptw6lubyBhIHDFmcOtam1lbsOtPC9iPjogJywgYEptw6lubyBhIHDFmcOtam1lbsOtYCwgJzxicj4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc8Yj5PbmxpbmUgbmVibyB0ZWxlZm9uaWNrw6kga29uenVsdGFjZTwvYj46ICcsIGBPbmxpbmUgbmVibyB0ZWxlZm9uaWNrw6kga29uenVsdGFjZT9gLCAnPGJyPicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzxiPlRlbGVmb248L2I+OiAnLGBUZWxlZm9uYCwgIjxicj4iLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc8Yj5FbWFpbDwvYj46ICcsIEVtYWlsLCAnPGJyPicsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgJzxiPldlYjwvYj46ICcsIFdlYiwgJzxicj4nLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICc8Yj5LYXBhY2l0YTwvYj46ICcsIGBLYXBhY2l0YT9gLCAnPGJyPicpCiAgICAgICAgICAgICAgICAgICAsCiAgICAgICAgICAgICAgICAgICBjbHVzdGVyT3B0aW9ucyA9IG1hcmtlckNsdXN0ZXJPcHRpb25zKHNob3dDb3ZlcmFnZU9uSG92ZXIgPSBGQUxTRSwKICAgICAgICAgICAgICAgICAgICBpY29uQ3JlYXRlRnVuY3Rpb249SlMoImZ1bmN0aW9uIChjbHVzdGVyKSB7ICAgIAogICAgdmFyIGNoaWxkQ291bnQgPSBjbHVzdGVyLmdldENoaWxkQ291bnQoKTsgCiAgICB2YXIgYyA9ICcgbWFya2VyLWNsdXN0ZXItJzsgIAogICAgaWYgKGNoaWxkQ291bnQgPCAxMDApIHsgIAogICAgICBjICs9ICdzbWFsbCc7ICAKICAgIH0gZWxzZSBpZiAoY2hpbGRDb3VudCA8IDEwMDApIHsgIAogICAgICBjICs9ICdtZWRpdW0nOyAgCiAgICB9IGVsc2UgeyAKICAgICAgYyArPSAnbGFyZ2UnOyAgCiAgICB9ICAgIAogICAgcmV0dXJuIG5ldyBMLkRpdkljb24oeyBodG1sOiAnPGRpdj48c3Bhbj4nICsgY2hpbGRDb3VudCArICc8L3NwYW4+PC9kaXY+JywgY2xhc3NOYW1lOiAnbWFya2VyLWNsdXN0ZXInICsgYywgaWNvblNpemU6IG5ldyBMLlBvaW50KDQwLCA0MCkgfSk7CgogIH0iKSkpICU+JQogIGFkZExlZ2VuZChwb3NpdGlvbiA9ICJ0b3ByaWdodCIsCiAgICAgICAgICAgIHZhbHVlcyA9IH5gS2FwYWNpdGE/YCwKICAgICAgICAgICAgb3BhY2l0eSA9IC43LAogICAgICAgICAgICBwYWwgPSBwYWwsCiAgICAgICAgICAgIHRpdGxlID0gIkthcGFjaXRhPyIpICU+JQogIGxlYWZsZXQuZXh0cmFzOjphZGRSZXNldE1hcEJ1dHRvbigpICU+JQogICBhZGRMYXllcnNDb250cm9sKAogICAgICAgICAgICAgICAgYmFzZUdyb3VwcyA9IGMoIlrDoWtsYWRuw60iLCAiTGV0ZWNrw6EiLCAiVWxpxI1uw60iLCAiWmVtxJtwaXNuw6EiKSwKICAgICAgICAgICAgICAgIG9wdGlvbnMgPSBsYXllcnNDb250cm9sT3B0aW9ucyhjb2xsYXBzZWQgPSBUUlVFKQogICAgICAgICAgICAgICkgJT4lCiAgICAgIGFkZFNlYXJjaE9TTSgpCgptYXAxCmBgYAoKIyMjIENsb3NpbmcgcmVtYXJrcwoKSSBiZWxpZXZlIHRoYXQgc29tZSB3aWxsIHJvbGwgdGhlaXIgZXllYnJvdyB3aGVuIGludGVncmF0aW5nIFIgYW5kIFB5dGhvbiB0aGUgd2F5IHByZXNlbnRlZCBhYm92ZS4gIipXaHkgbm90IGNob29zZSBqdXN0IG9uZSB0b29sIGFuZCB1c2UgaXQ/KiIgeW91IG1heSBhc2sgeW91cnNlbGYuIEluIG15IGNhc2UsIEkgbmVlZGVkIHRvIGFkZCBhIHBpZWNlIHRvIG15IHRlbXBsYXRlLiBBbHRob3VnaCBpdHMgcGFydHMgaGFkIGJlZW4gd3JpdHRlbiBpbiBSLCBJIGRpZCBub3Qgd2FudCB0byBsaW1pdCBteXNlbGYgdG8gb25lIHRvb2wgb25seS4gIAoKQXQgdGhlIHNhbWUgdGltZSwgYnkgbG9va2luZyBhdCB0aGUgY3VycmVudCBsaW1pdHMsIHdlIGNhbiBwcm9tb3RlIGZ1dHVyZSBlYXNlIG9mIGludGVncmF0aW9uLiBIZXJlLCBJIGhhdmUgdG8gYWNrbm93bGVkZ2UgdGhhdCBbUlN0dWRpb10oaHR0cHM6Ly93d3cucnN0dWRpby5jb20vc29sdXRpb25zL3ItYW5kLXB5dGhvbi8pLCBpbiBteSB2aWV3IG9uZSBvZiB0aGUgYmVzdCB0b29scyBmb3Igd29ya2luZyB3aXRoIGRhdGEsIGhhcyB0YWtlbiBzZXZlcmFsIHNpZ25pZmljYW50IHN0ZXBzIGluIGludGVncmF0aW5nIFB5dGhvbi4gSG93ZXZlciwgaW4gcGFydGljdWxhciB1c2UtY2FzZXMsIGl0IGlzIHN0aWxsIGEgc29tZXdoYXQgYnVtcHkgcmlkZS4KClVsdGltYXRlbHksIEkgd2FudGVkIHRvIGV4cGVyaW1lbnQgYW5kIHRlc3QgdGhlIGxpbWl0cy4gSSBiZWxpZXZlIHRoYXQgYm90aCBQeXRob24gYW5kIFIgaGF2ZSB0aGVpciBwcm9zIGFuZCBjb25zIHdoZW4gd29ya2luZyB3aXRoIGRhdGEsIHNvIHdoeSBub3QgZXhwbG9yZSB3aGVyZSB5b3UgY2FuIGlmIHlvdSBjb21iaW5lIHRoZW0/ICAKCiZuYnNwOwombmJzcDsKCjxwIHN0eWxlPSJ0ZXh0LWFsaWduOiBjZW50ZXI7Ij48Yj5HbyBiYWNrIHRvIDxhIGhyZWY9Ii9wb3N0Ij5CbG9nPC9iPjwvYT48L3A+