In this tutorial, I will introduce R codes to map Twitter users with coordinates obtained from Google Map API. This tutorial is built on: http://lucaspuente.github.io/notes/2016/04/05/Mapping-Twitter-Followers.
Let’s fire up necessary R libraries
require(twitteR)
require(data.table)
require(RJSONIO)
require(leaflet)
A quick example of Twitter location information.
#Get the location of @UMassPoll.
user<-getUser("UMassPoll")
user$location
[1] "Amherst, MA"
Now we know where @UMassPoll is located. But a more interesting question is: where are @UMassPoll’s followers? To answer the question, we will first create a R function called get_followers. The function can download follower information from API, remove users whose location information is blank or contains special characters. Notice that Twitter API has rate limit, that is why retryOnRateLimit is set to 180.
We can now apply the function to download a list of @UMassPoll’s followers.
followers_df <- get_followers("UMassPoll")
The location information is stored in the column named location. We can match the cities and states with exact coordinates through Google Map API. To do that, obtain a key from Google Maps Geocoding API. (https://developers.google.com/maps/documentation/geocoding/get-api-key). There is a limit of 2,500 coordinates per day if you are a standard Google Map API user.
#create a function for getting coordinates from Google Map API.We use the code published by Lucas Puente (http://lucaspuente.github.io/notes/2016/04/05/Mapping-Twitter-Followers)
source("https://raw.githubusercontent.com/LucasPuente/geocoding/master/geocode_helpers.R")
source("https://raw.githubusercontent.com/LucasPuente/geocoding/master/modified_geocode.R")
geocode_apply<-function(x){
geocode(x, source = "google", output = "all", api_key="xxx")
}
Let’s apply the function called geocode_apply to get coordinates.
geocode_results<-sapply(followers_df$location, geocode_apply, simplify = F)
View(geocode_results_hashtag_clean)
Use the following code to clean the coordinate data.
condition_a <- sapply(geocode_results, function(x) x["status"]=="OK")
geocode_results<-geocode_results[condition_a]
condition_b <- lapply(geocode_results, lapply, length)
condition_b2<-sapply(condition_b, function(x) x["results"]=="1")
geocode_results<-geocode_results[condition_b2]
source("https://raw.githubusercontent.com/LucasPuente/geocoding/master/cleaning_geocoded_results.R")
results_b<-lapply(geocode_results, as.data.frame)
results_c<-lapply(results_b,function(x) subset(x, select=c("results.formatted_address",
"results.geometry.location")))
results_d<-lapply(results_c,function(x) data.frame(Location=x[1,"results.formatted_address"],
lat=x[1,"results.geometry.location"],
lng=x[2,"results.geometry.location"]))
results_e<-rbindlist(results_d)
Now, we have a dataframe of Twitter followers with the coordinates matching their self-reported location on Twitter bio. Let’s use the data to create an interactive map.
map1 <- leaflet(data = results_e) %>%
addTiles() %>%
setView(lng = -98.35, lat = 39.50, zoom = 4) %>%
addMarkers(lng = ~lng, lat = ~lat, popup = ~ as.character(Location)) %>%
addProviderTiles("CartoDB.Positron") %>%
addCircleMarkers(
stroke = FALSE, fillOpacity = 0.5
)
Assuming 'lng' and 'lat' are longitude and latitude, respectively
map1
Try a different style.
Or try this.
Next, we will map Twitter users who have tweeted a given hashtag. To begin with, we use Google Map API to get each Twitter user’s coordinate. After running the following code, you will get a dataframe named user_info that contains Twitter user profiles.
#Load the tweets you want to visualize.
tweets <- read.csv("hashtagtweets.csv")
#Let's mine the profile of the first 50 users in the data.
users <- tweets$screenName[1:50]
user_info<-as.data.frame(getUser(users[1]))
for (user in users[1:length(users)]){
print(c("mining the profile for:",user))
Sys.sleep(5)
#because of the Twitter API limit, we let R rest for 5 sec after each request.
a<-getUser(user)
a<- as.data.frame(a)
user_info<-rbind(user_info,a)
}
In the beginning, we’ve created a function called geocode_apply. We can apply the function to our dataframe named user_info.
geocode_results<-sapply(user_info$location, geocode_apply, simplify = F)
We can repeat the following code for cleaning coordinate data.
condition_a <- sapply(geocode_results, function(x) x["status"]=="OK")
geocode_results<-geocode_results[condition_a]
condition_b <- lapply(geocode_results, lapply, length)
condition_b2<-sapply(condition_b, function(x) x["results"]=="1")
geocode_results<-geocode_results[condition_b2]
source("https://raw.githubusercontent.com/LucasPuente/geocoding/master/cleaning_geocoded_results.R")
results_b<-lapply(geocode_results, as.data.frame)
results_c<-lapply(results_b,function(x) subset(x, select=c("results.formatted_address",
"results.geometry.location")))
results_d<-lapply(results_c,function(x) data.frame(Location=x[1,"results.formatted_address"],
lat=x[1,"results.geometry.location"],
lng=x[2,"results.geometry.location"]))
results_e<-rbindlist(results_d)
Now, visualize the Twitter users.
map2 <- leaflet() %>% setView(lng = -98.35, lat = 39.50, zoom = 3)
map2 <- leaflet(data = results_e) %>%
addTiles() %>%
setView(lng = -98.35, lat = 39.50, zoom = 4) %>%
addMarkers(lng = ~lng, lat = ~lat, popup = ~ as.character(Location)) %>%
addProviderTiles("CartoDB.Positron") %>%
addCircleMarkers(
stroke = FALSE, fillOpacity = 0.5
)
Assuming 'lng' and 'lat' are longitude and latitude, respectively
map2
LS0tCnRpdGxlOiAiR2VvLU1hcHBpbmcgVHdpdHRlciBVc2VycyIKb3V0cHV0OgogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQKICBodG1sX2RvY3VtZW50OiBkZWZhdWx0Ci0tLQpJbiB0aGlzIHR1dG9yaWFsLCBJIHdpbGwgaW50cm9kdWNlIFIgY29kZXMgdG8gbWFwIFR3aXR0ZXIgdXNlcnMgd2l0aCBjb29yZGluYXRlcyBvYnRhaW5lZCBmcm9tIEdvb2dsZSBNYXAgQVBJLiBUaGlzIHR1dG9yaWFsIGlzIGJ1aWx0IG9uOiBodHRwOi8vbHVjYXNwdWVudGUuZ2l0aHViLmlvL25vdGVzLzIwMTYvMDQvMDUvTWFwcGluZy1Ud2l0dGVyLUZvbGxvd2Vycy4gIAoKTGV0J3MgZmlyZSB1cCBuZWNlc3NhcnkgUiBsaWJyYXJpZXMKYGBge3Igd2FybmluZyA9IEZBTFNFLCByZXN1bHRzPSJoaWRlIn0KcmVxdWlyZSh0d2l0dGVSKQpyZXF1aXJlKGRhdGEudGFibGUpCnJlcXVpcmUoUkpTT05JTykKcmVxdWlyZShsZWFmbGV0KQpgYGAKCkEgcXVpY2sgZXhhbXBsZSBvZiBUd2l0dGVyIGxvY2F0aW9uIGluZm9ybWF0aW9uLiAKYGBge3J9CiNHZXQgdGhlIGxvY2F0aW9uIG9mIEBVTWFzc1BvbGwuIFRvIHJ1biB0aGUgZm9sbG93aW5nIGNvZGUsIHlvdSBtdXN0IGZpcnN0IHNldCB1cCBUd2l0dGVyIEFQSS4gCnVzZXI8LWdldFVzZXIoIlVNYXNzUG9sbCIpCnVzZXIkbG9jYXRpb24KYGBgCgpOb3cgd2Uga25vdyB3aGVyZSBAVU1hc3NQb2xsIGlzIGxvY2F0ZWQuIEJ1dCBhIG1vcmUgaW50ZXJlc3RpbmcgcXVlc3Rpb24gaXM6IHdoZXJlIGFyZSAgQFVNYXNzUG9sbCdzIGZvbGxvd2Vycz8gVG8gYW5zd2VyIHRoZSBxdWVzdGlvbiwgd2Ugd2lsbCBmaXJzdCBjcmVhdGUgYSBSIGZ1bmN0aW9uIGNhbGxlZCBnZXRfZm9sbG93ZXJzLiBUaGUgZnVuY3Rpb24gY2FuIGRvd25sb2FkIGZvbGxvd2VyIGluZm9ybWF0aW9uIGZyb20gQVBJLCByZW1vdmUgdXNlcnMgd2hvc2UgbG9jYXRpb24gaW5mb3JtYXRpb24gaXMgYmxhbmsgb3IgY29udGFpbnMgc3BlY2lhbCBjaGFyYWN0ZXJzLiBOb3RpY2UgdGhhdCBUd2l0dGVyIEFQSSBoYXMgcmF0ZSBsaW1pdCwgdGhhdCBpcyB3aHkgcmV0cnlPblJhdGVMaW1pdCBpcyBzZXQgdG8gMTgwLiAKYGBge3IsIGVjaG89VFJVRX0KI0NyZWF0ZSBhIGZ1bmN0aW9uIGNhbGxlZCBnZXRfZm9sbG93ZXJzLiBXZSBjYW4gInJlY3ljbGUiIHRoaXMgZnVuY3Rpb24gZm9yIG90aGVyIGRhdGFzZXQuCmdldF9mb2xsb3dlcnMgPC0gZnVuY3Rpb24odXNlcm5hbWUpewogIHVzZXI8LWdldFVzZXIodXNlcm5hbWUpCiAgcHJpbnQoYygiZG93bmxvYWRpbmcgdGhlIGZvbGxvd2VycyBvZjoiLCB1c2VybmFtZSkpCiAgZm9sbG93ZXJfSURzIDwtIHVzZXIkZ2V0Rm9sbG93ZXJzKHJldHJ5T25SYXRlTGltaXQ9MTgwKQogIGZvbGxvd2Vyc19kZiA9IHJiaW5kbGlzdChsYXBwbHkoZm9sbG93ZXJfSURzLGFzLmRhdGEuZnJhbWUpKQogIGZvbGxvd2Vyc19kZjwtc3Vic2V0KGZvbGxvd2Vyc19kZiwgbG9jYXRpb24hPSIiKQogIGZvbGxvd2Vyc19kZiRsb2NhdGlvbjwtZ3N1YigiJSIsICIgIixmb2xsb3dlcnNfZGYkbG9jYXRpb24pCiAgcmV0dXJuKGZvbGxvd2Vyc19kZikKfQpgYGAKCldlIGNhbiBub3cgYXBwbHkgdGhlIGZ1bmN0aW9uIHRvIGRvd25sb2FkIGEgbGlzdCBvZiBAVU1hc3NQb2xsJ3MgZm9sbG93ZXJzLiAKYGBge3IgcmVzdWx0cz0iaGlkZSJ9CmZvbGxvd2Vyc19kZiA8LSBnZXRfZm9sbG93ZXJzKCJVTWFzc1BvbGwiKQpgYGAKClRoZSBsb2NhdGlvbiBpbmZvcm1hdGlvbiBpcyBzdG9yZWQgaW4gdGhlIGNvbHVtbiBuYW1lZCBsb2NhdGlvbi4gV2UgY2FuIG1hdGNoIHRoZSBjaXRpZXMgYW5kIHN0YXRlcyB3aXRoIGV4YWN0IGNvb3JkaW5hdGVzIHRocm91Z2ggR29vZ2xlIE1hcCBBUEkuIFRvIGRvIHRoYXQsIG9idGFpbiBhIGtleSBmcm9tIEdvb2dsZSBNYXBzIEdlb2NvZGluZyBBUEkuIChodHRwczovL2RldmVsb3BlcnMuZ29vZ2xlLmNvbS9tYXBzL2RvY3VtZW50YXRpb24vZ2VvY29kaW5nL2dldC1hcGkta2V5KS4gVGhlcmUgaXMgYSBsaW1pdCBvZiAyLDUwMCBjb29yZGluYXRlcyBwZXIgZGF5IGlmIHlvdSBhcmUgYSBzdGFuZGFyZCBHb29nbGUgTWFwIEFQSSB1c2VyLiAKYGBge3IsIGVjaG89VFJVRX0KI2NyZWF0ZSBhIGZ1bmN0aW9uIGZvciBnZXR0aW5nIGNvb3JkaW5hdGVzIGZyb20gR29vZ2xlIE1hcCBBUEkuV2UgdXNlIHRoZSBjb2RlIHB1Ymxpc2hlZCBieSBMdWNhcyBQdWVudGUgKGh0dHA6Ly9sdWNhc3B1ZW50ZS5naXRodWIuaW8vbm90ZXMvMjAxNi8wNC8wNS9NYXBwaW5nLVR3aXR0ZXItRm9sbG93ZXJzKQpzb3VyY2UoImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9MdWNhc1B1ZW50ZS9nZW9jb2RpbmcvbWFzdGVyL2dlb2NvZGVfaGVscGVycy5SIikKc291cmNlKCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vTHVjYXNQdWVudGUvZ2VvY29kaW5nL21hc3Rlci9tb2RpZmllZF9nZW9jb2RlLlIiKQoKZ2VvY29kZV9hcHBseTwtZnVuY3Rpb24oeCl7CiAgZ2VvY29kZSh4LCBzb3VyY2UgPSAiZ29vZ2xlIiwgb3V0cHV0ID0gImFsbCIsIGFwaV9rZXk9Inh4eCIpCn0KYGBgCgpMZXQncyBhcHBseSB0aGUgZnVuY3Rpb24gY2FsbGVkIGdlb2NvZGVfYXBwbHkgdG8gZ2V0IGNvb3JkaW5hdGVzLiAKYGBge3IsIG1lc3NhZ2U9RkFMU0V9Cmdlb2NvZGVfcmVzdWx0czwtc2FwcGx5KGZvbGxvd2Vyc19kZiRsb2NhdGlvbiwgZ2VvY29kZV9hcHBseSwgc2ltcGxpZnkgPSBGKQpgYGAKClVzZSB0aGUgZm9sbG93aW5nIGNvZGUgdG8gY2xlYW4gdGhlIGNvb3JkaW5hdGUgZGF0YS4gCmBgYHtyfQpjb25kaXRpb25fYSA8LSBzYXBwbHkoZ2VvY29kZV9yZXN1bHRzLCBmdW5jdGlvbih4KSB4WyJzdGF0dXMiXT09Ik9LIikKZ2VvY29kZV9yZXN1bHRzPC1nZW9jb2RlX3Jlc3VsdHNbY29uZGl0aW9uX2FdCmNvbmRpdGlvbl9iIDwtIGxhcHBseShnZW9jb2RlX3Jlc3VsdHMsIGxhcHBseSwgbGVuZ3RoKQpjb25kaXRpb25fYjI8LXNhcHBseShjb25kaXRpb25fYiwgZnVuY3Rpb24oeCkgeFsicmVzdWx0cyJdPT0iMSIpCmdlb2NvZGVfcmVzdWx0czwtZ2VvY29kZV9yZXN1bHRzW2NvbmRpdGlvbl9iMl0Kc291cmNlKCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vTHVjYXNQdWVudGUvZ2VvY29kaW5nL21hc3Rlci9jbGVhbmluZ19nZW9jb2RlZF9yZXN1bHRzLlIiKQpyZXN1bHRzX2I8LWxhcHBseShnZW9jb2RlX3Jlc3VsdHMsIGFzLmRhdGEuZnJhbWUpCnJlc3VsdHNfYzwtbGFwcGx5KHJlc3VsdHNfYixmdW5jdGlvbih4KSBzdWJzZXQoeCwgc2VsZWN0PWMoInJlc3VsdHMuZm9ybWF0dGVkX2FkZHJlc3MiLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICJyZXN1bHRzLmdlb21ldHJ5LmxvY2F0aW9uIikpKQpyZXN1bHRzX2Q8LWxhcHBseShyZXN1bHRzX2MsZnVuY3Rpb24oeCkgZGF0YS5mcmFtZShMb2NhdGlvbj14WzEsInJlc3VsdHMuZm9ybWF0dGVkX2FkZHJlc3MiXSwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsYXQ9eFsxLCJyZXN1bHRzLmdlb21ldHJ5LmxvY2F0aW9uIl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGxuZz14WzIsInJlc3VsdHMuZ2VvbWV0cnkubG9jYXRpb24iXSkpCnJlc3VsdHNfZTwtcmJpbmRsaXN0KHJlc3VsdHNfZCkKYGBgCgpOb3csIHdlIGhhdmUgYSBkYXRhZnJhbWUgb2YgVHdpdHRlciBmb2xsb3dlcnMgd2l0aCB0aGUgY29vcmRpbmF0ZXMgbWF0Y2hpbmcgdGhlaXIgc2VsZi1yZXBvcnRlZCBsb2NhdGlvbiBvbiBUd2l0dGVyIGJpby4gTGV0J3MgdXNlIHRoZSBkYXRhIHRvIGNyZWF0ZSBhbiBpbnRlcmFjdGl2ZSBtYXAuCmBgYHtyfQptYXAxIDwtIGxlYWZsZXQoKSAlPiUgc2V0VmlldyhsbmcgPSAtOTguMzUsIGxhdCA9IDM5LjUwLCB6b29tID0gMykKbWFwMSA8LSBsZWFmbGV0KGRhdGEgPSByZXN1bHRzX2UpICU+JSAKICBhZGRUaWxlcygpICU+JQogIHNldFZpZXcobG5nID0gLTk4LjM1LCBsYXQgPSAzOS41MCwgem9vbSA9IDQpICU+JSAKICBhZGRNYXJrZXJzKGxuZyA9IH5sbmcsIGxhdCA9IH5sYXQsIHBvcHVwID0gfiBhcy5jaGFyYWN0ZXIoTG9jYXRpb24pKSAlPiUgCiAgYWRkUHJvdmlkZXJUaWxlcygiQ2FydG9EQi5Qb3NpdHJvbiIpICU+JQogIGFkZENpcmNsZU1hcmtlcnMoCiAgICBzdHJva2UgPSBGQUxTRSwgZmlsbE9wYWNpdHkgPSAwLjUKICApIAptYXAxCmBgYAoKVHJ5IGEgZGlmZmVyZW50IHN0eWxlLgpgYGB7cn0KbWFwMSAlPiUgYWRkUHJvdmlkZXJUaWxlcygiU3RhbWVuLlRvbmVyIikKYGBgCgpPciB0cnkgdGhpcy4KYGBge3J9Cm1hcDEgJT4lIGFkZFRpbGVzKCkgCmBgYAoKTmV4dCwgd2Ugd2lsbCBtYXAgVHdpdHRlciB1c2VycyB3aG8gaGF2ZSB0d2VldGVkIGEgZ2l2ZW4gaGFzaHRhZy4gVG8gYmVnaW4gd2l0aCwgd2UgdXNlIEdvb2dsZSBNYXAgQVBJIHRvIGdldCBlYWNoIFR3aXR0ZXIgdXNlcidzIGNvb3JkaW5hdGUuIEFmdGVyIHJ1bm5pbmcgdGhlIGZvbGxvd2luZyBjb2RlLCB5b3Ugd2lsbCBnZXQgYSBkYXRhZnJhbWUgbmFtZWQgdXNlcl9pbmZvIHRoYXQgY29udGFpbnMgVHdpdHRlciB1c2VyIHByb2ZpbGVzLgoKYGBge3J9CiNMb2FkIHRoZSB0d2VldHMgeW91IHdhbnQgdG8gdmlzdWFsaXplLiAKdHdlZXRzIDwtIHJlYWQuY3N2KCJoYXNodGFndHdlZXRzLmNzdiIpCgojTGV0J3MgbWluZSB0aGUgcHJvZmlsZSBvZiB0aGUgZmlyc3QgNTAgdXNlcnMgaW4gdGhlIGRhdGEuIAp1c2VycyA8LSB0d2VldHMkc2NyZWVuTmFtZVsxOjUwXQp1c2VyX2luZm88LWFzLmRhdGEuZnJhbWUoZ2V0VXNlcih1c2Vyc1sxXSkpCmZvciAodXNlciBpbiB1c2Vyc1sxOmxlbmd0aCh1c2VycyldKXsKICBwcmludChjKCJtaW5pbmcgdGhlIHByb2ZpbGUgZm9yOiIsdXNlcikpCiAgU3lzLnNsZWVwKDUpIAogICNiZWNhdXNlIG9mIHRoZSBUd2l0dGVyIEFQSSBsaW1pdCwgd2UgbGV0IFIgcmVzdCBmb3IgNSBzZWMgYWZ0ZXIgZWFjaCByZXF1ZXN0LiAKICBhPC1nZXRVc2VyKHVzZXIpCiAgYTwtIGFzLmRhdGEuZnJhbWUoYSkKICB1c2VyX2luZm88LXJiaW5kKHVzZXJfaW5mbyxhKQp9CmBgYAoKSW4gdGhlIGJlZ2lubmluZywgd2UndmUgY3JlYXRlZCBhIGZ1bmN0aW9uIGNhbGxlZCBnZW9jb2RlX2FwcGx5LiBXZSBjYW4gYXBwbHkgdGhlIGZ1bmN0aW9uIHRvIG91ciBkYXRhZnJhbWUgbmFtZWQgdXNlcl9pbmZvLiAgCmBgYHtyLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFfQpnZW9jb2RlX3Jlc3VsdHM8LXNhcHBseSh1c2VyX2luZm8kbG9jYXRpb24sIGdlb2NvZGVfYXBwbHksIHNpbXBsaWZ5ID0gRikKYGBgCgpXZSBjYW4gcmVwZWF0IHRoZSBmb2xsb3dpbmcgY29kZSBmb3IgY2xlYW5pbmcgY29vcmRpbmF0ZSBkYXRhLgpgYGB7ciwgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0KY29uZGl0aW9uX2EgPC0gc2FwcGx5KGdlb2NvZGVfcmVzdWx0cywgZnVuY3Rpb24oeCkgeFsic3RhdHVzIl09PSJPSyIpCmdlb2NvZGVfcmVzdWx0czwtZ2VvY29kZV9yZXN1bHRzW2NvbmRpdGlvbl9hXQpjb25kaXRpb25fYiA8LSBsYXBwbHkoZ2VvY29kZV9yZXN1bHRzLCBsYXBwbHksIGxlbmd0aCkKY29uZGl0aW9uX2IyPC1zYXBwbHkoY29uZGl0aW9uX2IsIGZ1bmN0aW9uKHgpIHhbInJlc3VsdHMiXT09IjEiKQpnZW9jb2RlX3Jlc3VsdHM8LWdlb2NvZGVfcmVzdWx0c1tjb25kaXRpb25fYjJdCnNvdXJjZSgiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL0x1Y2FzUHVlbnRlL2dlb2NvZGluZy9tYXN0ZXIvY2xlYW5pbmdfZ2VvY29kZWRfcmVzdWx0cy5SIikKcmVzdWx0c19iPC1sYXBwbHkoZ2VvY29kZV9yZXN1bHRzLCBhcy5kYXRhLmZyYW1lKQpyZXN1bHRzX2M8LWxhcHBseShyZXN1bHRzX2IsZnVuY3Rpb24oeCkgc3Vic2V0KHgsIHNlbGVjdD1jKCJyZXN1bHRzLmZvcm1hdHRlZF9hZGRyZXNzIiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAicmVzdWx0cy5nZW9tZXRyeS5sb2NhdGlvbiIpKSkKcmVzdWx0c19kPC1sYXBwbHkocmVzdWx0c19jLGZ1bmN0aW9uKHgpIGRhdGEuZnJhbWUoTG9jYXRpb249eFsxLCJyZXN1bHRzLmZvcm1hdHRlZF9hZGRyZXNzIl0sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbGF0PXhbMSwicmVzdWx0cy5nZW9tZXRyeS5sb2NhdGlvbiJdLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICBsbmc9eFsyLCJyZXN1bHRzLmdlb21ldHJ5LmxvY2F0aW9uIl0pKQpyZXN1bHRzX2U8LXJiaW5kbGlzdChyZXN1bHRzX2QpCmBgYAoKTm93LCB2aXN1YWxpemUgdGhlIFR3aXR0ZXIgdXNlcnMuCmBgYHtyfQptYXAyIDwtIGxlYWZsZXQoKSAlPiUgc2V0VmlldyhsbmcgPSAtOTguMzUsIGxhdCA9IDM5LjUwLCB6b29tID0gMykKbWFwMiA8LSBsZWFmbGV0KGRhdGEgPSByZXN1bHRzX2UpICU+JSAKICBhZGRUaWxlcygpICU+JQogIHNldFZpZXcobG5nID0gLTk4LjM1LCBsYXQgPSAzOS41MCwgem9vbSA9IDQpICU+JSAKICBhZGRNYXJrZXJzKGxuZyA9IH5sbmcsIGxhdCA9IH5sYXQsIHBvcHVwID0gfiBhcy5jaGFyYWN0ZXIoTG9jYXRpb24pKSAlPiUgCiAgYWRkUHJvdmlkZXJUaWxlcygiQ2FydG9EQi5Qb3NpdHJvbiIpICU+JQogIGFkZENpcmNsZU1hcmtlcnMoCiAgICBzdHJva2UgPSBGQUxTRSwgZmlsbE9wYWNpdHkgPSAwLjUKICApIAptYXAyCmBgYAoK