“Remember: every GPS point is a step toward understanding the world a little better.”

In this final lab, we bring together everything we’ve learned this quarter:
vector data, raster data, coordinate systems, interactive maps, and exposure science. In this lab, we’re diving into the wonderful world of GPS data, our old friend NDVI rasters, and some fun interactive mapping – with a light touch and an emphasis on public health applications. This will be our last interactive session, so we will lean on old tools we’ve been using, and we will also apply some new packages for the first time!

The objectives of this guide are to teach you to:

  1. Understand how to load and preprocess GPS tracking data in R
  2. Calculate movement metrics (distance + speed)
  3. Create interactive mobility maps with leaflet
  4. Compare spatial patterns across time of day using tmap
  5. Integrate NDVI rasters as environmental exposure
  6. Extract momentary exposure values
  7. Visualize exposure over time

Let’s do this, one last time!



Load our packages

library(dplyr)        # data wrangling, piping, and general awesomeness
library(readr)        # reading in data like a pro
library(terra)        # handling raster data
library(sf)           # spatial vector data with all the bells and whistles
library(tmap)         # beautiful thematic maps, both static and interactive

#New Packages this week!
library(lubridate)    # working with dates and times like it's no big deal
library(leaflet)      # making interactive maps that aim to impress
library(geosphere)    # helps us to colculate the great-circle distance between two points


Import and Prepare Our GPS Data

Here we read in our GPS dataset, parse the timestamp, and create friendly time labels. This is where readr, dplyr, and lubridate come together to easily tackle this task.

download.file(
  "https://raw.githubusercontent.com/pjames-ucdavis/SPH215/refs/heads/main/gps_apr25_30.csv",
  destfile = "gps_apr25_30.csv",
  mode = "wb"
)

gps_data_raw <- read_csv("gps_apr25_30.csv")

gps_data <- gps_data_raw %>%
  mutate(
    time = ymd_hms(`UTC time`),
    time_label = format(time, "%Y-%m-%d %H:%M:%S")
  ) %>%
  arrange(time)

glimpse(gps_data)
## Rows: 21,886
## Columns: 8
## $ timestamp  <dbl> 1.61931e+12, 1.61931e+12, 1.61931e+12, 1.61931e+12, 1.61931…
## $ `UTC time` <dttm> 2021-04-25 00:15:10, 2021-04-25 00:15:10, 2021-04-25 00:16…
## $ latitude   <dbl> 42.37178, 42.37177, 42.37180, 42.37183, 42.37186, 42.37183,…
## $ longitude  <dbl> -71.08780, -71.08793, -71.08796, -71.08791, -71.08791, -71.…
## $ altitude   <dbl> 16.142262, 3.940258, 3.940258, 3.940258, 3.940258, 3.940258…
## $ accuracy   <dbl> 25.62929, 65.00000, 1030.74153, 65.00000, 65.00000, 65.0000…
## $ time       <dttm> 2021-04-25 00:15:10, 2021-04-25 00:15:10, 2021-04-25 00:16…
## $ time_label <chr> "2021-04-25 00:15:10", "2021-04-25 00:15:10", "2021-04-25 0…


We now have minute-level GPS points over multiple days.


Movement Analytics

Now we will calculate:

  • Distance between consecutive points
  • Time difference
  • Speed

This helps us to have an idea of how fast we are moving at any point!

gps_data <- gps_data %>%
  mutate(
    lon2 = lead(longitude),
    lat2 = lead(latitude),
    t2   = lead(time),
    dt_s = as.numeric(difftime(t2, time, units = "secs")),
    step_m = distHaversine(
      cbind(longitude, latitude),
      cbind(lon2, lat2)
    ),
    speed_mps = step_m / dt_s
  ) %>%
  filter(dt_s > 0) %>%                       # remove zero/negative time gaps
  mutate(speed_mps = ifelse(is.infinite(speed_mps), NA, speed_mps))

summary(gps_data$speed_mps)
##     Min.  1st Qu.   Median     Mean  3rd Qu.     Max. 
##   0.0000   0.0000   0.0000   1.2290   0.8059 884.9334


That is a dynamic view of human mobility in meters per second. Some of those numbers are a little high though!


Mapping with Leaflet: Interactive Map Colored by Speed (mph)

Now let’s use the leaflet package to make an interactive map of our GPS data. This lets us explore movement patterns dynamically, and it’s a great way to get familiar with spatial data in a hands-on way. Let’s start off by converting speed to miles per hour and removing unrealistic values (>90 mph).

gps_speed_clean <- gps_data %>%
  mutate(speed_mph = speed_mps * 2.23694) %>%
  filter(
    !is.na(speed_mph),
    speed_mph > 0,
    speed_mph < 90
  )

Now let’s map points by speed!

pal_speed <- colorNumeric(
  palette = "viridis",
  domain = range(gps_speed_clean$speed_mph, na.rm = TRUE)
)

leaflet(gps_speed_clean) %>%
  addProviderTiles("CartoDB.Positron") %>%
  addCircleMarkers(
    lng = ~longitude,
    lat = ~latitude,
    radius = 3,
    color = ~pal_speed(speed_mph),
    stroke = FALSE,
    fillOpacity = 0.85,
    popup = ~paste(
      "Time:", time_label,
      "<br>Speed (mph):", round(speed_mph, 1)
    )
  ) %>%
  addLegend(
    "bottomright",
    pal = pal_speed,
    values = ~speed_mph,
    title = "Speed (mph)"
  )


This map shows where movement is slow (likely stationary or walking) versus fast (driving). What do we see?


Time-of-Day Maps

Let’s compare spatial behavior across time of day. Where did I spend time during the morning? What about afternoon and evening? What about night?

gps_data <- gps_data %>%
  mutate(
    hour = hour(time),
    tod = cut(
      hour,
      breaks = c(-1, 5, 11, 17, 23),
      labels = c("Night", "Morning", "Afternoon", "Evening")
    )
  )

gps_sf <- st_as_sf(gps_data, coords = c("longitude", "latitude"), crs = 4326)

tmap_mode("plot")
## ℹ tmap modes "plot" - "view"
## ℹ toggle with `tmap::ttm()`
tm_shape(gps_sf) +
  tm_symbols(
    fill = "steelblue",
    col = NA,
    size = 0.2,
    fill_alpha = 0.6
  ) +
  tm_facets(by = "tod", ncol = 2, free.coords = FALSE) +
  tm_title(  
    text = "Activity Space by Time of Day",
    size = 1.2,
    ) +
  tm_basemap()


Interesting. I like the nightlife. I like to boogie.


🌿 Add Nature: Load up an NDVI Raster

Let’s make this a little richer by adding an environmental exposure that we could link to our data. Our old friend NDVI (Normalized Difference Vegetation Index) from Landsat (30m resolution) helps us understand greenness and vegetation density.

download.file("https://raw.githubusercontent.com/pjames-ucdavis/SPH215/refs/heads/main/NDVI_rast_boston.tif", destfile = "NDVI_rast_boston.tif", mode = "wb")
ndvi_raster <- rast("NDVI_rast_boston.tif")
ndvi_raster
## class       : SpatRaster 
## size        : 210, 583, 1  (nrow, ncol, nlyr)
## resolution  : 0.0002694946, 0.0002694946  (x, y)
## extent      : -71.19885, -71.04174, 42.33032, 42.38692  (xmin, xmax, ymin, ymax)
## coord. ref. : lon/lat WGS 84 (EPSG:4326) 
## source      : NDVI_rast_boston.tif 
## name        :  NDVI_mean 
## min value   : -0.1340106 
## max value   :  0.7256226
tm_shape(ndvi_raster) +
  tm_raster(col.scale = tm_scale_continuous(values = "brewer.yl_gn"),
            col.legend = tm_legend(title = "NDVI"))

Looks quite green!


✨ Make It Beautiful with tmap

Time to show off with tmap, our old reliable thematic mapping package. We reproject the GPS points to match the raster and then we layer it all together.

tmap_mode("view")  # switch to interactive map mode
## ℹ tmap modes "plot" - "view"
# Reproject GPS CRS to match raster
gps_sf <- st_as_sf(gps_data, coords = c("longitude", "latitude"), crs = 4326)

# Transform our GPS CRS to match the raster!
gps_proj <- st_transform(gps_sf, crs = crs(ndvi_raster))


# Map it!
tm_shape(ndvi_raster) +
  tm_raster(col.scale = tm_scale_continuous(values = "brewer.yl_gn"),
            col_alpha = 0.55, 
            col.legend = tm_legend(title = "NDVI")) +
  tm_shape(gps_proj) +
  tm_symbols(
    fill = "blue", 
    size = 0.5, 
    col = NA,
    fill_alpha = 0.1
    ) +
  tm_title_out(title = "Where We've Been with a Little Green)")
## Registered S3 method overwritten by 'jsonify':
##   method     from    
##   print.json jsonlite
## Warning: tm_scale_intervals `label.style = "continuous"` implementation in view mode
## work in progress

Now we can visually inspect where I’ve been relative to greenness. This gives an idea of my actual exposure to greenness as I move through life. Slightly better than just residential address alone!


Extract NDVI values to GPS data

Finally, let’s extract NDVI values to GPS data to give us a quantitative estimate of where exactly we are exposed to greenspace. We will use the extract() function from the terra package to do this.

ndvi_values <- data.frame(gps_data,terra::extract(ndvi_raster, terra::vect(gps_proj)))
glimpse(ndvi_values)
## Rows: 21,264
## Columns: 18
## $ timestamp  <dbl> 1.61931e+12, 1.61931e+12, 1.61931e+12, 1.61931e+12, 1.61931…
## $ UTC.time   <dttm> 2021-04-25 00:15:10, 2021-04-25 00:16:58, 2021-04-25 00:16…
## $ latitude   <dbl> 42.37177, 42.37180, 42.37183, 42.37186, 42.37187, 42.37188,…
## $ longitude  <dbl> -71.08793, -71.08796, -71.08791, -71.08791, -71.08792, -71.…
## $ altitude   <dbl> 3.940258, 3.940258, 3.940258, 3.940258, 3.929049, 3.938418,…
## $ accuracy   <dbl> 65.00000, 1030.74153, 65.00000, 65.00000, 54.44491, 54.7162…
## $ time       <dttm> 2021-04-25 00:15:10, 2021-04-25 00:16:58, 2021-04-25 00:16…
## $ time_label <chr> "2021-04-25 00:15:10", "2021-04-25 00:16:58", "2021-04-25 0…
## $ lon2       <dbl> -71.08796, -71.08791, -71.08791, -71.08796, -71.08792, -71.…
## $ lat2       <dbl> 42.37180, 42.37183, 42.37186, 42.37183, 42.37188, 42.37188,…
## $ t2         <dttm> 2021-04-25 00:16:58, 2021-04-25 00:16:59, 2021-04-25 00:17…
## $ dt_s       <dbl> 108, 1, 1, 6, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, …
## $ step_m     <dbl> 3.730937e+00, 5.242994e+00, 3.571994e+00, 5.394825e+00, 8.0…
## $ speed_mps  <dbl> 3.454572e-02, 5.242994e+00, 3.571994e+00, 8.991376e-01, 8.0…
## $ hour       <int> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
## $ tod        <fct> Night, Night, Night, Night, Night, Night, Night, Night, Nig…
## $ ID         <dbl> 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, …
## $ NDVI_mean  <dbl> 0.2657485, 0.2657485, 0.2624005, 0.2624005, 0.2624005, 0.26…


Look at the data

OK, and now we can take a glimpse at the data. This is one participant’s (me) minute-to-minute exposure to greenspace as they move throughout their lives. This is the full distribution of a personal exposure to NDVI for five days! If we had, for instance, physical activity data, we could see if momentary exposure to greenspace is associated with higher physical activity.

summary(ndvi_values$NDVI_mean)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
## 0.08799 0.33104 0.33498 0.33002 0.33498 0.64671
hist(
  ndvi_values$NDVI_mean,
  main = "Distribution of NDVI Exposure",
  xlab = "NDVI"
)


Exposure over time

Next, let’s take a look at how my exposure varies from minute to minute over the course of a few days. We can do that by simply plotting time and our GPS-based NDVI measure!

plot(
  ndvi_values$time,
  ndvi_values$NDVI_mean,
  type = "l",
  xlab = "Time",
  ylab = "NDVI",
  main = "Momentary Greenness Exposure"
)

Kinda like an EKG of greenness! Interesting!


🎉 You Did It!

You loaded real-world GPS data, estimated movement speed from raw GPS data, mapped it, extracted data from a vegetation raster, made it interactive, and created estimates of personal exposure. You now have the tools to link where people go to what they are exposed to. Pretty cool.


Over this quarter, we’ve taken our GIS data from the macro to the micro-scale. We’ve covered so much in so little time! You’ve learned a TON of R. It has been a mapping whirlwind, and I’m so proud of all of you!

LS0tDQp0aXRsZTogJ0xhYiA5OiBGdXR1cmUgRGlyZWN0aW9ucyBpbiBHSVMgYW5kIFB1YmxpYyBIZWFsdGgnDQotLS0NCg0KPiAiUmVtZW1iZXI6IGV2ZXJ5IEdQUyBwb2ludCBpcyBhIHN0ZXAgdG93YXJkIHVuZGVyc3RhbmRpbmcgdGhlIHdvcmxkIGEgbGl0dGxlIGJldHRlci4iDQoNCkluIHRoaXMgZmluYWwgbGFiLCB3ZSBicmluZyB0b2dldGhlciBldmVyeXRoaW5nIHdlJ3ZlIGxlYXJuZWQgdGhpcyBxdWFydGVyOiAgDQp2ZWN0b3IgZGF0YSwgcmFzdGVyIGRhdGEsIGNvb3JkaW5hdGUgc3lzdGVtcywgaW50ZXJhY3RpdmUgbWFwcywgYW5kIGV4cG9zdXJlIHNjaWVuY2UuIEluIHRoaXMgbGFiLCB3ZSdyZSBkaXZpbmcgaW50byB0aGUgd29uZGVyZnVsIHdvcmxkIG9mICoqR1BTIGRhdGEqKiwgb3VyIG9sZCBmcmllbmQgKipORFZJIHJhc3RlcnMqKiwgYW5kIHNvbWUgZnVuICoqaW50ZXJhY3RpdmUgbWFwcGluZyoqIOKAkyB3aXRoIGEgbGlnaHQgdG91Y2ggYW5kIGFuIGVtcGhhc2lzIG9uIHB1YmxpYyBoZWFsdGggYXBwbGljYXRpb25zLiBUaGlzIHdpbGwgYmUgb3VyIGxhc3QgaW50ZXJhY3RpdmUgc2Vzc2lvbiwgc28gd2Ugd2lsbCBsZWFuIG9uIG9sZCB0b29scyB3ZSd2ZSBiZWVuIHVzaW5nLCBhbmQgd2Ugd2lsbCBhbHNvIGFwcGx5IHNvbWUgbmV3IHBhY2thZ2VzIGZvciB0aGUgZmlyc3QgdGltZSEgDQoNClRoZSBvYmplY3RpdmVzIG9mIHRoaXMgZ3VpZGUgYXJlIHRvIHRlYWNoIHlvdSB0bzoNCg0KMS4gVW5kZXJzdGFuZCBob3cgdG8gbG9hZCBhbmQgcHJlcHJvY2VzcyBHUFMgdHJhY2tpbmcgZGF0YSBpbiBSDQoyLiBDYWxjdWxhdGUgbW92ZW1lbnQgbWV0cmljcyAoZGlzdGFuY2UgKyBzcGVlZCkgIA0KMy4gQ3JlYXRlIGludGVyYWN0aXZlIG1vYmlsaXR5IG1hcHMgd2l0aCAqKmxlYWZsZXQqKiAgDQo0LiBDb21wYXJlIHNwYXRpYWwgcGF0dGVybnMgYWNyb3NzIHRpbWUgb2YgZGF5IHVzaW5nICoqdG1hcCoqICANCjUuIEludGVncmF0ZSBORFZJIHJhc3RlcnMgYXMgZW52aXJvbm1lbnRhbCBleHBvc3VyZSAgDQo2LiBFeHRyYWN0IG1vbWVudGFyeSBleHBvc3VyZSB2YWx1ZXMgIA0KNy4gVmlzdWFsaXplIGV4cG9zdXJlIG92ZXIgdGltZSAgDQoNCkxldCdzIGRvIHRoaXMsIG9uZSBsYXN0IHRpbWUhDQoNClwNCg0KLS0tDQoNCiMgTG9hZCBvdXIgcGFja2FnZXMNCg0KYGBge3IgbWVzc2FnZT1GQUxTRSwgd2FybmluZz1GQUxTRX0NCmxpYnJhcnkoZHBseXIpICAgICAgICAjIGRhdGEgd3JhbmdsaW5nLCBwaXBpbmcsIGFuZCBnZW5lcmFsIGF3ZXNvbWVuZXNzDQpsaWJyYXJ5KHJlYWRyKSAgICAgICAgIyByZWFkaW5nIGluIGRhdGEgbGlrZSBhIHBybw0KbGlicmFyeSh0ZXJyYSkgICAgICAgICMgaGFuZGxpbmcgcmFzdGVyIGRhdGENCmxpYnJhcnkoc2YpICAgICAgICAgICAjIHNwYXRpYWwgdmVjdG9yIGRhdGEgd2l0aCBhbGwgdGhlIGJlbGxzIGFuZCB3aGlzdGxlcw0KbGlicmFyeSh0bWFwKSAgICAgICAgICMgYmVhdXRpZnVsIHRoZW1hdGljIG1hcHMsIGJvdGggc3RhdGljIGFuZCBpbnRlcmFjdGl2ZQ0KDQojTmV3IFBhY2thZ2VzIHRoaXMgd2VlayENCmxpYnJhcnkobHVicmlkYXRlKSAgICAjIHdvcmtpbmcgd2l0aCBkYXRlcyBhbmQgdGltZXMgbGlrZSBpdCdzIG5vIGJpZyBkZWFsDQpsaWJyYXJ5KGxlYWZsZXQpICAgICAgIyBtYWtpbmcgaW50ZXJhY3RpdmUgbWFwcyB0aGF0IGFpbSB0byBpbXByZXNzDQpsaWJyYXJ5KGdlb3NwaGVyZSkgICAgIyBoZWxwcyB1cyB0byBjb2xjdWxhdGUgdGhlIGdyZWF0LWNpcmNsZSBkaXN0YW5jZSBiZXR3ZWVuIHR3byBwb2ludHMNCg0KYGBgDQoNClwNCg0KIyBJbXBvcnQgYW5kIFByZXBhcmUgT3VyIEdQUyBEYXRhDQoNCkhlcmUgd2UgcmVhZCBpbiBvdXIgR1BTIGRhdGFzZXQsIHBhcnNlIHRoZSB0aW1lc3RhbXAsIGFuZCBjcmVhdGUgZnJpZW5kbHkgdGltZSBsYWJlbHMuIFRoaXMgaXMgd2hlcmUgKipyZWFkcioqLCAqKmRwbHlyKiosIGFuZCAqKmx1YnJpZGF0ZSoqIGNvbWUgdG9nZXRoZXIgdG8gZWFzaWx5IHRhY2tsZSB0aGlzIHRhc2suDQpgYGB7ciwgbWVzc2FnZT1GQUxTRX0NCmRvd25sb2FkLmZpbGUoDQogICJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGphbWVzLXVjZGF2aXMvU1BIMjE1L3JlZnMvaGVhZHMvbWFpbi9ncHNfYXByMjVfMzAuY3N2IiwNCiAgZGVzdGZpbGUgPSAiZ3BzX2FwcjI1XzMwLmNzdiIsDQogIG1vZGUgPSAid2IiDQopDQoNCmdwc19kYXRhX3JhdyA8LSByZWFkX2NzdigiZ3BzX2FwcjI1XzMwLmNzdiIpDQoNCmdwc19kYXRhIDwtIGdwc19kYXRhX3JhdyAlPiUNCiAgbXV0YXRlKA0KICAgIHRpbWUgPSB5bWRfaG1zKGBVVEMgdGltZWApLA0KICAgIHRpbWVfbGFiZWwgPSBmb3JtYXQodGltZSwgIiVZLSVtLSVkICVIOiVNOiVTIikNCiAgKSAlPiUNCiAgYXJyYW5nZSh0aW1lKQ0KDQpnbGltcHNlKGdwc19kYXRhKQ0KYGBgDQoNClwNCg0KV2Ugbm93IGhhdmUgbWludXRlLWxldmVsIEdQUyBwb2ludHMgb3ZlciBtdWx0aXBsZSBkYXlzLg0KDQpcDQoNCiMgTW92ZW1lbnQgQW5hbHl0aWNzDQoNCk5vdyB3ZSB3aWxsIGNhbGN1bGF0ZToNCg0KLSBEaXN0YW5jZSBiZXR3ZWVuIGNvbnNlY3V0aXZlIHBvaW50cyAgDQotIFRpbWUgZGlmZmVyZW5jZSAgDQotIFNwZWVkIA0KDQpUaGlzIGhlbHBzIHVzIHRvIGhhdmUgYW4gaWRlYSBvZiBob3cgZmFzdCB3ZSBhcmUgbW92aW5nIGF0IGFueSBwb2ludCENCg0KYGBge3J9DQpncHNfZGF0YSA8LSBncHNfZGF0YSAlPiUNCiAgbXV0YXRlKA0KICAgIGxvbjIgPSBsZWFkKGxvbmdpdHVkZSksDQogICAgbGF0MiA9IGxlYWQobGF0aXR1ZGUpLA0KICAgIHQyICAgPSBsZWFkKHRpbWUpLA0KICAgIGR0X3MgPSBhcy5udW1lcmljKGRpZmZ0aW1lKHQyLCB0aW1lLCB1bml0cyA9ICJzZWNzIikpLA0KICAgIHN0ZXBfbSA9IGRpc3RIYXZlcnNpbmUoDQogICAgICBjYmluZChsb25naXR1ZGUsIGxhdGl0dWRlKSwNCiAgICAgIGNiaW5kKGxvbjIsIGxhdDIpDQogICAgKSwNCiAgICBzcGVlZF9tcHMgPSBzdGVwX20gLyBkdF9zDQogICkgJT4lDQogIGZpbHRlcihkdF9zID4gMCkgJT4lICAgICAgICAgICAgICAgICAgICAgICAjIHJlbW92ZSB6ZXJvL25lZ2F0aXZlIHRpbWUgZ2Fwcw0KICBtdXRhdGUoc3BlZWRfbXBzID0gaWZlbHNlKGlzLmluZmluaXRlKHNwZWVkX21wcyksIE5BLCBzcGVlZF9tcHMpKQ0KDQpzdW1tYXJ5KGdwc19kYXRhJHNwZWVkX21wcykNCg0KYGBgDQoNClwNCg0KVGhhdCBpcyBhIGR5bmFtaWMgdmlldyBvZiBodW1hbiBtb2JpbGl0eSBpbiBtZXRlcnMgcGVyIHNlY29uZC4gU29tZSBvZiB0aG9zZSBudW1iZXJzIGFyZSBhIGxpdHRsZSBoaWdoIHRob3VnaCENCg0KXA0KDQojIE1hcHBpbmcgd2l0aCBMZWFmbGV0OiBJbnRlcmFjdGl2ZSBNYXAgQ29sb3JlZCBieSBTcGVlZCAobXBoKQ0KDQoNCg0KTm93IGxldOKAmXMgdXNlIHRoZSAqKmxlYWZsZXQqKiBwYWNrYWdlIHRvIG1ha2UgYW4gaW50ZXJhY3RpdmUgbWFwIG9mIG91ciBHUFMgZGF0YS4gVGhpcyBsZXRzIHVzIGV4cGxvcmUgbW92ZW1lbnQgcGF0dGVybnMgZHluYW1pY2FsbHksIGFuZCBpdOKAmXMgYSBncmVhdCB3YXkgdG8gZ2V0IGZhbWlsaWFyIHdpdGggc3BhdGlhbCBkYXRhIGluIGEgaGFuZHMtb24gd2F5LiBMZXQncyBzdGFydCBvZmYgYnkgY29udmVydGluZyBzcGVlZCB0byBtaWxlcyBwZXIgaG91ciBhbmQgcmVtb3ZpbmcgdW5yZWFsaXN0aWMgdmFsdWVzICg+OTAgbXBoKS4NCmBgYHtyfQ0KZ3BzX3NwZWVkX2NsZWFuIDwtIGdwc19kYXRhICU+JQ0KICBtdXRhdGUoc3BlZWRfbXBoID0gc3BlZWRfbXBzICogMi4yMzY5NCkgJT4lDQogIGZpbHRlcigNCiAgICAhaXMubmEoc3BlZWRfbXBoKSwNCiAgICBzcGVlZF9tcGggPiAwLA0KICAgIHNwZWVkX21waCA8IDkwDQogICkNCmBgYA0KDQpOb3cgbGV0J3MgbWFwIHBvaW50cyBieSBzcGVlZCENCmBgYHtyfQ0KcGFsX3NwZWVkIDwtIGNvbG9yTnVtZXJpYygNCiAgcGFsZXR0ZSA9ICJ2aXJpZGlzIiwNCiAgZG9tYWluID0gcmFuZ2UoZ3BzX3NwZWVkX2NsZWFuJHNwZWVkX21waCwgbmEucm0gPSBUUlVFKQ0KKQ0KDQpsZWFmbGV0KGdwc19zcGVlZF9jbGVhbikgJT4lDQogIGFkZFByb3ZpZGVyVGlsZXMoIkNhcnRvREIuUG9zaXRyb24iKSAlPiUNCiAgYWRkQ2lyY2xlTWFya2VycygNCiAgICBsbmcgPSB+bG9uZ2l0dWRlLA0KICAgIGxhdCA9IH5sYXRpdHVkZSwNCiAgICByYWRpdXMgPSAzLA0KICAgIGNvbG9yID0gfnBhbF9zcGVlZChzcGVlZF9tcGgpLA0KICAgIHN0cm9rZSA9IEZBTFNFLA0KICAgIGZpbGxPcGFjaXR5ID0gMC44NSwNCiAgICBwb3B1cCA9IH5wYXN0ZSgNCiAgICAgICJUaW1lOiIsIHRpbWVfbGFiZWwsDQogICAgICAiPGJyPlNwZWVkIChtcGgpOiIsIHJvdW5kKHNwZWVkX21waCwgMSkNCiAgICApDQogICkgJT4lDQogIGFkZExlZ2VuZCgNCiAgICAiYm90dG9tcmlnaHQiLA0KICAgIHBhbCA9IHBhbF9zcGVlZCwNCiAgICB2YWx1ZXMgPSB+c3BlZWRfbXBoLA0KICAgIHRpdGxlID0gIlNwZWVkIChtcGgpIg0KICApDQpgYGANCg0KXA0KDQpUaGlzIG1hcCBzaG93cyB3aGVyZSBtb3ZlbWVudCBpcyBzbG93IChsaWtlbHkgc3RhdGlvbmFyeSBvciB3YWxraW5nKSB2ZXJzdXMgZmFzdCAoZHJpdmluZykuIFdoYXQgZG8gd2Ugc2VlPw0KDQotLS0NCg0KIyBUaW1lLW9mLURheSBNYXBzDQoNCkxldCdzIGNvbXBhcmUgc3BhdGlhbCBiZWhhdmlvciBhY3Jvc3MgdGltZSBvZiBkYXkuIFdoZXJlIGRpZCBJIHNwZW5kIHRpbWUgZHVyaW5nIHRoZSBtb3JuaW5nPyBXaGF0IGFib3V0IGFmdGVybm9vbiBhbmQgZXZlbmluZz8gV2hhdCBhYm91dCBuaWdodD8gDQoNCmBgYHtyfQ0KZ3BzX2RhdGEgPC0gZ3BzX2RhdGEgJT4lDQogIG11dGF0ZSgNCiAgICBob3VyID0gaG91cih0aW1lKSwNCiAgICB0b2QgPSBjdXQoDQogICAgICBob3VyLA0KICAgICAgYnJlYWtzID0gYygtMSwgNSwgMTEsIDE3LCAyMyksDQogICAgICBsYWJlbHMgPSBjKCJOaWdodCIsICJNb3JuaW5nIiwgIkFmdGVybm9vbiIsICJFdmVuaW5nIikNCiAgICApDQogICkNCg0KZ3BzX3NmIDwtIHN0X2FzX3NmKGdwc19kYXRhLCBjb29yZHMgPSBjKCJsb25naXR1ZGUiLCAibGF0aXR1ZGUiKSwgY3JzID0gNDMyNikNCg0KdG1hcF9tb2RlKCJwbG90IikNCg0KdG1fc2hhcGUoZ3BzX3NmKSArDQogIHRtX3N5bWJvbHMoDQogICAgZmlsbCA9ICJzdGVlbGJsdWUiLA0KICAgIGNvbCA9IE5BLA0KICAgIHNpemUgPSAwLjIsDQogICAgZmlsbF9hbHBoYSA9IDAuNg0KICApICsNCiAgdG1fZmFjZXRzKGJ5ID0gInRvZCIsIG5jb2wgPSAyLCBmcmVlLmNvb3JkcyA9IEZBTFNFKSArDQogIHRtX3RpdGxlKCAgDQogICAgdGV4dCA9ICJBY3Rpdml0eSBTcGFjZSBieSBUaW1lIG9mIERheSIsDQogICAgc2l6ZSA9IDEuMiwNCiAgICApICsNCiAgdG1fYmFzZW1hcCgpDQpgYGANCg0KXA0KDQpJbnRlcmVzdGluZy4gSSBsaWtlIHRoZSBuaWdodGxpZmUuIEkgbGlrZSB0byBib29naWUuDQoNClwNCg0KIyDwn4y/IEFkZCBOYXR1cmU6IExvYWQgdXAgYW4gTkRWSSBSYXN0ZXINCg0KTGV0J3MgbWFrZSB0aGlzIGEgbGl0dGxlIHJpY2hlciBieSBhZGRpbmcgYW4gZW52aXJvbm1lbnRhbCBleHBvc3VyZSB0aGF0IHdlIGNvdWxkIGxpbmsgdG8gb3VyIGRhdGEuIE91ciBvbGQgZnJpZW5kIE5EVkkgKE5vcm1hbGl6ZWQgRGlmZmVyZW5jZSBWZWdldGF0aW9uIEluZGV4KSBmcm9tIExhbmRzYXQgKDMwbSByZXNvbHV0aW9uKSBoZWxwcyB1cyB1bmRlcnN0YW5kIGdyZWVubmVzcyBhbmQgdmVnZXRhdGlvbiBkZW5zaXR5LiANCg0KYGBge3J9DQpkb3dubG9hZC5maWxlKCJodHRwczovL3Jhdy5naXRodWJ1c2VyY29udGVudC5jb20vcGphbWVzLXVjZGF2aXMvU1BIMjE1L3JlZnMvaGVhZHMvbWFpbi9ORFZJX3Jhc3RfYm9zdG9uLnRpZiIsIGRlc3RmaWxlID0gIk5EVklfcmFzdF9ib3N0b24udGlmIiwgbW9kZSA9ICJ3YiIpDQpuZHZpX3Jhc3RlciA8LSByYXN0KCJORFZJX3Jhc3RfYm9zdG9uLnRpZiIpDQpuZHZpX3Jhc3Rlcg0KdG1fc2hhcGUobmR2aV9yYXN0ZXIpICsNCiAgdG1fcmFzdGVyKGNvbC5zY2FsZSA9IHRtX3NjYWxlX2NvbnRpbnVvdXModmFsdWVzID0gImJyZXdlci55bF9nbiIpLA0KICAgICAgICAgICAgY29sLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJORFZJIikpDQpgYGANCg0KTG9va3MgcXVpdGUgZ3JlZW4hDQoNClwNCg0KIyDinKggTWFrZSBJdCBCZWF1dGlmdWwgd2l0aCB0bWFwDQoNClRpbWUgdG8gc2hvdyBvZmYgd2l0aCAqKnRtYXAqKiwgb3VyIG9sZCByZWxpYWJsZSB0aGVtYXRpYyBtYXBwaW5nIHBhY2thZ2UuIFdlIHJlcHJvamVjdCB0aGUgR1BTIHBvaW50cyB0byBtYXRjaCB0aGUgcmFzdGVyIGFuZCB0aGVuIHdlIGxheWVyIGl0IGFsbCB0b2dldGhlci4NCmBgYHtyfQ0KdG1hcF9tb2RlKCJ2aWV3IikgICMgc3dpdGNoIHRvIGludGVyYWN0aXZlIG1hcCBtb2RlDQoNCiMgUmVwcm9qZWN0IEdQUyBDUlMgdG8gbWF0Y2ggcmFzdGVyDQpncHNfc2YgPC0gc3RfYXNfc2YoZ3BzX2RhdGEsIGNvb3JkcyA9IGMoImxvbmdpdHVkZSIsICJsYXRpdHVkZSIpLCBjcnMgPSA0MzI2KQ0KDQojIFRyYW5zZm9ybSBvdXIgR1BTIENSUyB0byBtYXRjaCB0aGUgcmFzdGVyIQ0KZ3BzX3Byb2ogPC0gc3RfdHJhbnNmb3JtKGdwc19zZiwgY3JzID0gY3JzKG5kdmlfcmFzdGVyKSkNCg0KDQojIE1hcCBpdCENCnRtX3NoYXBlKG5kdmlfcmFzdGVyKSArDQogIHRtX3Jhc3Rlcihjb2wuc2NhbGUgPSB0bV9zY2FsZV9jb250aW51b3VzKHZhbHVlcyA9ICJicmV3ZXIueWxfZ24iKSwNCiAgICAgICAgICAgIGNvbF9hbHBoYSA9IDAuNTUsIA0KICAgICAgICAgICAgY29sLmxlZ2VuZCA9IHRtX2xlZ2VuZCh0aXRsZSA9ICJORFZJIikpICsNCiAgdG1fc2hhcGUoZ3BzX3Byb2opICsNCiAgdG1fc3ltYm9scygNCiAgICBmaWxsID0gImJsdWUiLCANCiAgICBzaXplID0gMC41LCANCiAgICBjb2wgPSBOQSwNCiAgICBmaWxsX2FscGhhID0gMC4xDQogICAgKSArDQogIHRtX3RpdGxlX291dCh0aXRsZSA9ICJXaGVyZSBXZSd2ZSBCZWVuIHdpdGggYSBMaXR0bGUgR3JlZW4pIikNCmBgYA0KTm93IHdlIGNhbiB2aXN1YWxseSBpbnNwZWN0IHdoZXJlIEkndmUgYmVlbiByZWxhdGl2ZSB0byBncmVlbm5lc3MuIFRoaXMgZ2l2ZXMgYW4gaWRlYSBvZiBteSBhY3R1YWwgZXhwb3N1cmUgdG8gZ3JlZW5uZXNzIGFzIEkgbW92ZSB0aHJvdWdoIGxpZmUuIFNsaWdodGx5IGJldHRlciB0aGFuIGp1c3QgcmVzaWRlbnRpYWwgYWRkcmVzcyBhbG9uZSENCg0KXA0KDQojIEV4dHJhY3QgTkRWSSB2YWx1ZXMgdG8gR1BTIGRhdGENCg0KRmluYWxseSwgbGV0J3MgZXh0cmFjdCBORFZJIHZhbHVlcyB0byBHUFMgZGF0YSB0byBnaXZlIHVzIGEgcXVhbnRpdGF0aXZlIGVzdGltYXRlIG9mIHdoZXJlIGV4YWN0bHkgd2UgYXJlIGV4cG9zZWQgdG8gZ3JlZW5zcGFjZS4gV2Ugd2lsbCB1c2UgdGhlIGBleHRyYWN0KClgIGZ1bmN0aW9uIGZyb20gdGhlICoqdGVycmEqKiBwYWNrYWdlIHRvIGRvIHRoaXMuDQpgYGB7cn0NCm5kdmlfdmFsdWVzIDwtIGRhdGEuZnJhbWUoZ3BzX2RhdGEsdGVycmE6OmV4dHJhY3QobmR2aV9yYXN0ZXIsIHRlcnJhOjp2ZWN0KGdwc19wcm9qKSkpDQpnbGltcHNlKG5kdmlfdmFsdWVzKQ0KYGBgDQoNClwNCg0KIyBMb29rIGF0IHRoZSBkYXRhDQoNCk9LLCBhbmQgbm93IHdlIGNhbiB0YWtlIGEgZ2xpbXBzZSBhdCB0aGUgZGF0YS4gVGhpcyBpcyBvbmUgcGFydGljaXBhbnQncyAobWUpIG1pbnV0ZS10by1taW51dGUgZXhwb3N1cmUgdG8gZ3JlZW5zcGFjZSBhcyB0aGV5IG1vdmUgdGhyb3VnaG91dCB0aGVpciBsaXZlcy4gVGhpcyBpcyB0aGUgZnVsbCBkaXN0cmlidXRpb24gb2YgYSBwZXJzb25hbCBleHBvc3VyZSB0byBORFZJIGZvciBmaXZlIGRheXMhIElmIHdlIGhhZCwgZm9yIGluc3RhbmNlLCBwaHlzaWNhbCBhY3Rpdml0eSBkYXRhLCB3ZSBjb3VsZCBzZWUgaWYgbW9tZW50YXJ5IGV4cG9zdXJlIHRvIGdyZWVuc3BhY2UgaXMgYXNzb2NpYXRlZCB3aXRoIGhpZ2hlciBwaHlzaWNhbCBhY3Rpdml0eS4NCmBgYHtyfQ0Kc3VtbWFyeShuZHZpX3ZhbHVlcyRORFZJX21lYW4pDQpoaXN0KA0KICBuZHZpX3ZhbHVlcyRORFZJX21lYW4sDQogIG1haW4gPSAiRGlzdHJpYnV0aW9uIG9mIE5EVkkgRXhwb3N1cmUiLA0KICB4bGFiID0gIk5EVkkiDQopDQpgYGANCg0KXA0KDQojIEV4cG9zdXJlIG92ZXIgdGltZQ0KDQpOZXh0LCBsZXQncyB0YWtlIGEgbG9vayBhdCBob3cgbXkgZXhwb3N1cmUgdmFyaWVzIGZyb20gbWludXRlIHRvIG1pbnV0ZSBvdmVyIHRoZSBjb3Vyc2Ugb2YgYSBmZXcgZGF5cy4gV2UgY2FuIGRvIHRoYXQgYnkgc2ltcGx5IHBsb3R0aW5nIHRpbWUgYW5kIG91ciBHUFMtYmFzZWQgTkRWSSBtZWFzdXJlISANCmBgYHtyfQ0KcGxvdCgNCiAgbmR2aV92YWx1ZXMkdGltZSwNCiAgbmR2aV92YWx1ZXMkTkRWSV9tZWFuLA0KICB0eXBlID0gImwiLA0KICB4bGFiID0gIlRpbWUiLA0KICB5bGFiID0gIk5EVkkiLA0KICBtYWluID0gIk1vbWVudGFyeSBHcmVlbm5lc3MgRXhwb3N1cmUiDQopDQoNCmBgYA0KDQpLaW5kYSBsaWtlIGFuIEVLRyBvZiBncmVlbm5lc3MhIEludGVyZXN0aW5nIQ0KDQpcDQoNCiMjIPCfjokgWW91IERpZCBJdCENCg0KWW91IGxvYWRlZCByZWFsLXdvcmxkIEdQUyBkYXRhLCBlc3RpbWF0ZWQgbW92ZW1lbnQgc3BlZWQgZnJvbSByYXcgR1BTIGRhdGEsIG1hcHBlZCBpdCwgZXh0cmFjdGVkIGRhdGEgZnJvbSBhIHZlZ2V0YXRpb24gcmFzdGVyLCBtYWRlIGl0IGludGVyYWN0aXZlLCBhbmQgY3JlYXRlZCBlc3RpbWF0ZXMgb2YgcGVyc29uYWwgZXhwb3N1cmUuIFlvdSBub3cgaGF2ZSB0aGUgdG9vbHMgdG8gbGluayAqKndoZXJlIHBlb3BsZSBnbyoqIHRvICoqd2hhdCB0aGV5IGFyZSBleHBvc2VkIHRvKiouIFByZXR0eSBjb29sLg0KDQpcDQoNCk92ZXIgdGhpcyBxdWFydGVyLCB3ZSd2ZSB0YWtlbiBvdXIgR0lTIGRhdGEgZnJvbSB0aGUgbWFjcm8gdG8gdGhlIG1pY3JvLXNjYWxlLiBXZSd2ZSBjb3ZlcmVkIHNvIG11Y2ggaW4gc28gbGl0dGxlIHRpbWUhIFlvdSd2ZSBsZWFybmVkIGEgKipUT04qKiBvZiBSLiBJdCBoYXMgYmVlbiBhIG1hcHBpbmcgd2hpcmx3aW5kLCBhbmQgSSdtIHNvIHByb3VkIG9mIGFsbCBvZiB5b3UhDQo=