Taking inspiration from pgWriteRast
in therpostgis
package, I attempted to write a function that can write a stars
object to a PostGIS-enabled database.
pg_write_stars = function(conn, name, layer, tile_size = NULL,bit_depth = NULL) {
# writes raster (stars) to a postgis database
# conn: DBIConnection object
# name: name of table to hold raster
# layer: stars object
# tile_size: size of tiles to split raster into specified as vector with length 1 or 2, expressed as pixel width by height
# bit_depth: bit depth of raster
library(stars)
library(tibble)
library(glue)
library(purrr)
library(DBI)
if (!("postgis_raster" %in% dbGetQuery(conn, "select * FROM pg_available_extensions;")$name))
stop("PostGIS extension not available.")
if (st_is_longlat(layer))
stop("Raster should have projected coordinates.")
if (any(class(layer) != "stars"))
stop("Object should be of class stars.")
crs = st_crs(layer)$epsg
if (is.na(crs)) {crs = 0; warning("SRID set to 0")}
dims = st_dimensions(layer)
res = c(dims$x$delta, dims$y$delta)
bbox = st_bbox(layer)
val = c(dims$x$values,dims$y$values)
aff = attr(dims,"raster")$affine
if (attr(dims, "raster")$curvilinear |
any(!is.null(val)) |
any(is.na(res))) {
stop("Raster must be regular.")
}
# determine bit depth
# from https://github.com/mablab/rpostgis/blob/master/R/pgWriteRast.R
if (is.null(bit_depth)) {
if (is.integer(layer[[1]])) {
if (min(layer[[1]], na.rm = TRUE) >= 0) {
bit_depth = "32BUI"
} else {
bit_depth = "32BSI"
}
} else {
bit_depth = "32BF"
}
}
# determine tile size
if (!is.null(tile_size)) {
if (length(tile_size) == 2) {
bx = tile_size[1]
by = tile_size[2]
} else {
bx = by = tile_size[1]
}
} else {
bx = by = 100
}
tiles = st_as_stars(bbox, dx = res[1] * bx, dy = res[2] * by) %>%
st_as_sfc(as_points = F) %>%
st_intersection(st_as_sfc(bbox))
# create a list with tile dimensions and values
info = map(seq_along(tiles), function(x) {
tile = tiles[x]
inter = layer[tile]
dm = dim(inter)
list(dm, st_dimensions(st_as_stars(
st_bbox(tile), dx = res[1], dy = res[2]
)), as.vector(inter[[1]]))
})
dbExecute(conn, glue("drop table if exists {name}"))
dbExecute(conn, glue("create table {name}(rid serial primary key, rast raster)"))
for (i in seq_along(info)) {
xy = info[[i]][[1]]
# create table with each value assigned to a row
dbWriteTable(conn,
"rst_tile_val",
tibble(val = info[[i]][[3]],
y = sort(rep(
1:xy[2], xy[1]
))),
overwrite = T)
dims = info[[i]][[2]]
# use tile dimensions to create empty raster and populate raster with values from rst_tile_val
dbExecute(conn, glue(
"insert into {name}(rid, rast)
values({i},
st_setvalues(
st_addband(
st_makeemptyraster({xy[1]}, {xy[2]},
{dims$x$offset},{dims$y$offset},
{res[1]},{res[2]},
{aff[1]},{aff[2]},{crs}),
array[row(1,'{bit_depth}',0,-99999)]::addbandarg[]),
1, 1, 1, (select array(select array_agg(val) from rst_tile_val group by y order by y)::double precision[][])
))"))
}
dbExecute(conn,"drop table if exists rst_tile_val")
dbExecute(conn, glue("drop index if exists {name}_rast_idx"))
# create spatial index
dbExecute(conn, glue("create index {name}_rast_idx on {name} using gist(st_convexhull(rast))"))
}
Similar to pg_read_stars
, is a simplified version of a function in rpostigis
. It can only write one band of a raster and it always overwrites existing files. First, information about the raster is collected such as its dimensions and bounding box. The bounding box of the raster is then split into tiles, with the default tile size being 100x100 pixels and each tile’s dimensions and values are put into a list using map
. In a subsequent loop, the raster is written to the PostGIS database tile by tile. In this loop, the values of a tile are first written to a table and then empty raster is created and populated using the values from this table. In order to be usable, the values in the table (rst_tile_val
) need to be made into a nested array. The nested arrays need to be the same length as the width of their corresponding tile, so if a tile had a width of 100 pixels, each array should have 100 values. A spatial index is then created after the raster is written.
library(keyring)
library(mapview)
library(stars)
library(DBI)
conn = DBI::dbConnect(
RPostgres::Postgres(),
dbname = key_list()[1, 1],
user = key_list()[1, 2],
password = key_get(key_list()[1, 1], key_list()[1, 2])
)
initial = read_stars("floodextent200.tif")
pg_write_stars(conn,"pg_stars_test", initial)
[1] 0
postgis = pg_read_stars(conn,"pg_stars_test")
mapview(initial, col.regions = "#EA3546", layer.name = "Initial Raster") +mapview(postgis, col.regions = "#226CE0", layer.name = "PostGIS Raster")
As can be seen in this map, there are still some kinks to work out in pg_write_stars
, specifically the shifting of some pixels to the right, though I believe I have a good foundation to build on in order to fix this. All things considered, this was a fun exercise of integrating SQL in R.
LS0tDQp0aXRsZTogIndyaXRpbmcgc3RhcnMgb2JqZWN0cyB0byBhIHBvc3RnaXMgZGF0YWJhc2UiDQpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sNCi0tLQ0KDQpUYWtpbmcgaW5zcGlyYXRpb24gZnJvbSBbYHBnV3JpdGVSYXN0YF0oaHR0cHM6Ly9naXRodWIuY29tL21hYmxhYi9ycG9zdGdpcy9ibG9iL21hc3Rlci9SL3BnV3JpdGVSYXN0LlIpIGluIHRoZWBycG9zdGdpc2AgcGFja2FnZSwgSSBhdHRlbXB0ZWQgdG8gd3JpdGUgYSBmdW5jdGlvbiB0aGF0IGNhbiB3cml0ZSBhIGBzdGFyc2Agb2JqZWN0IHRvIGEgUG9zdEdJUy1lbmFibGVkIGRhdGFiYXNlLiAgDQpgYGB7ciBwZ193cml0ZV9zdGFyc30NCnBnX3dyaXRlX3N0YXJzID0gIGZ1bmN0aW9uKGNvbm4sIG5hbWUsIGxheWVyLCB0aWxlX3NpemUgPSBOVUxMLGJpdF9kZXB0aCA9IE5VTEwpIHsNCiAgIyB3cml0ZXMgcmFzdGVyIChzdGFycykgdG8gYSBwb3N0Z2lzIGRhdGFiYXNlDQogICMgY29ubjogREJJQ29ubmVjdGlvbiBvYmplY3QNCiAgIyBuYW1lOiBuYW1lIG9mIHRhYmxlIHRvIGhvbGQgcmFzdGVyDQogICMgbGF5ZXI6IHN0YXJzIG9iamVjdCANCiAgIyB0aWxlX3NpemU6IHNpemUgb2YgdGlsZXMgdG8gc3BsaXQgcmFzdGVyIGludG8gc3BlY2lmaWVkIGFzIHZlY3RvciB3aXRoIGxlbmd0aCAxIG9yIDIsIGV4cHJlc3NlZCBhcyBwaXhlbCB3aWR0aCBieSBoZWlnaHQgDQogICMgYml0X2RlcHRoOiBiaXQgZGVwdGggb2YgcmFzdGVyDQogIA0KICBsaWJyYXJ5KHN0YXJzKQ0KICBsaWJyYXJ5KHRpYmJsZSkNCiAgbGlicmFyeShnbHVlKQ0KICBsaWJyYXJ5KHB1cnJyKQ0KICBsaWJyYXJ5KERCSSkNCiAgDQogIGlmICghKCJwb3N0Z2lzX3Jhc3RlciIgJWluJSBkYkdldFF1ZXJ5KGNvbm4sICJzZWxlY3QgKiBGUk9NIHBnX2F2YWlsYWJsZV9leHRlbnNpb25zOyIpJG5hbWUpKQ0KICAgIHN0b3AoIlBvc3RHSVMgZXh0ZW5zaW9uIG5vdCBhdmFpbGFibGUuIikNCiAgDQogIGlmIChzdF9pc19sb25nbGF0KGxheWVyKSkgDQogICAgc3RvcCgiUmFzdGVyIHNob3VsZCBoYXZlIHByb2plY3RlZCBjb29yZGluYXRlcy4iKQ0KICANCiAgaWYgKGFueShjbGFzcyhsYXllcikgIT0gInN0YXJzIikpIA0KICAgIHN0b3AoIk9iamVjdCBzaG91bGQgYmUgb2YgY2xhc3Mgc3RhcnMuIikNCg0KICBjcnMgPSBzdF9jcnMobGF5ZXIpJGVwc2cNCiAgaWYgKGlzLm5hKGNycykpIHtjcnMgPSAwOyB3YXJuaW5nKCJTUklEIHNldCB0byAwIil9DQogIA0KICBkaW1zID0gc3RfZGltZW5zaW9ucyhsYXllcikNCiAgcmVzID0gYyhkaW1zJHgkZGVsdGEsIGRpbXMkeSRkZWx0YSkNCiAgYmJveCA9IHN0X2Jib3gobGF5ZXIpDQogIHZhbCA9IGMoZGltcyR4JHZhbHVlcyxkaW1zJHkkdmFsdWVzKQ0KICBhZmYgPSBhdHRyKGRpbXMsInJhc3RlciIpJGFmZmluZQ0KICANCiAgaWYgKGF0dHIoZGltcywgInJhc3RlciIpJGN1cnZpbGluZWFyIHwNCiAgICAgIGFueSghaXMubnVsbCh2YWwpKSB8DQogICAgICBhbnkoaXMubmEocmVzKSkpIHsNCiAgICBzdG9wKCJSYXN0ZXIgbXVzdCBiZSByZWd1bGFyLiIpDQogIH0NCiAgDQogICMgZGV0ZXJtaW5lIGJpdCBkZXB0aCANCiAgIyBmcm9tIGh0dHBzOi8vZ2l0aHViLmNvbS9tYWJsYWIvcnBvc3RnaXMvYmxvYi9tYXN0ZXIvUi9wZ1dyaXRlUmFzdC5SIA0KICBpZiAoaXMubnVsbChiaXRfZGVwdGgpKSB7DQogICAgaWYgKGlzLmludGVnZXIobGF5ZXJbWzFdXSkpIHsNCiAgICAgIGlmIChtaW4obGF5ZXJbWzFdXSwgbmEucm0gPSBUUlVFKSA+PSAwKSB7DQogICAgICAgIGJpdF9kZXB0aCA9ICIzMkJVSSINCiAgICAgIH0gZWxzZSB7DQogICAgICAgIGJpdF9kZXB0aCA9ICIzMkJTSSINCiAgICAgIH0NCiAgICB9IGVsc2Ugew0KICAgICAgYml0X2RlcHRoID0gIjMyQkYiDQogICAgfQ0KICB9DQogIA0KICAjIGRldGVybWluZSB0aWxlIHNpemUNCiAgaWYgKCFpcy5udWxsKHRpbGVfc2l6ZSkpIHsNCiAgICBpZiAobGVuZ3RoKHRpbGVfc2l6ZSkgPT0gMikgew0KICAgICAgYnggPSB0aWxlX3NpemVbMV0NCiAgICAgIGJ5ID0gdGlsZV9zaXplWzJdDQogICAgfSBlbHNlIHsNCiAgICAgIGJ4ID0gYnkgPSB0aWxlX3NpemVbMV0NCiAgICB9DQogIH0gZWxzZSB7DQogICAgYnggPSBieSA9IDEwMA0KICB9DQogIA0KICB0aWxlcyA9IHN0X2FzX3N0YXJzKGJib3gsIGR4ID0gcmVzWzFdICogYngsIGR5ID0gcmVzWzJdICogYnkpICU+JQ0KICAgIHN0X2FzX3NmYyhhc19wb2ludHMgPSBGKSAlPiUNCiAgICBzdF9pbnRlcnNlY3Rpb24oc3RfYXNfc2ZjKGJib3gpKQ0KICANCiAgIyBjcmVhdGUgYSBsaXN0IHdpdGggdGlsZSBkaW1lbnNpb25zIGFuZCB2YWx1ZXMgDQogIGluZm8gPSBtYXAoc2VxX2Fsb25nKHRpbGVzKSwgZnVuY3Rpb24oeCkgew0KICAgIHRpbGUgPSB0aWxlc1t4XQ0KICAgIGludGVyID0gbGF5ZXJbdGlsZV0NCiAgICBkbSA9IGRpbShpbnRlcikNCiAgICBsaXN0KGRtLCBzdF9kaW1lbnNpb25zKHN0X2FzX3N0YXJzKA0KICAgICAgc3RfYmJveCh0aWxlKSwgZHggPSByZXNbMV0sIGR5ID0gcmVzWzJdDQogICAgKSksIGFzLnZlY3RvcihpbnRlcltbMV1dKSkNCiAgfSkNCiAgDQogIGRiRXhlY3V0ZShjb25uLCBnbHVlKCJkcm9wIHRhYmxlIGlmIGV4aXN0cyB7bmFtZX0iKSkNCiAgZGJFeGVjdXRlKGNvbm4sIGdsdWUoImNyZWF0ZSB0YWJsZSB7bmFtZX0ocmlkIHNlcmlhbCBwcmltYXJ5IGtleSwgcmFzdCByYXN0ZXIpIikpDQogIA0KICBmb3IgKGkgaW4gc2VxX2Fsb25nKGluZm8pKSB7DQogICAgeHkgPSBpbmZvW1tpXV1bWzFdXQ0KICAgICMgY3JlYXRlIHRhYmxlIHdpdGggZWFjaCB2YWx1ZSBhc3NpZ25lZCB0byBhIHJvdyANCiAgICBkYldyaXRlVGFibGUoY29ubiwNCiAgICAgICAgICAgICAgICAgInJzdF90aWxlX3ZhbCIsDQogICAgICAgICAgICAgICAgIHRpYmJsZSh2YWwgPSBpbmZvW1tpXV1bWzNdXSwNCiAgICAgICAgICAgICAgICAgICAgICAgIHkgPSBzb3J0KHJlcCgNCiAgICAgICAgICAgICAgICAgICAgICAgICAgMTp4eVsyXSwgeHlbMV0NCiAgICAgICAgICAgICAgICAgICAgICAgICkpKSwNCiAgICAgICAgICAgICAgICAgb3ZlcndyaXRlID0gVCkNCiAgICANCiAgICBkaW1zID0gaW5mb1tbaV1dW1syXV0NCiAgICANCiAgICAjIHVzZSB0aWxlIGRpbWVuc2lvbnMgdG8gY3JlYXRlIGVtcHR5IHJhc3RlciBhbmQgcG9wdWxhdGUgcmFzdGVyIHdpdGggdmFsdWVzIGZyb20gcnN0X3RpbGVfdmFsDQogICAgZGJFeGVjdXRlKGNvbm4sIGdsdWUoDQogICAgICAiaW5zZXJ0IGludG8ge25hbWV9KHJpZCwgcmFzdCkgDQogICAgIHZhbHVlcyh7aX0sDQogICAgIHN0X3NldHZhbHVlcygNCiAgICAgICAgc3RfYWRkYmFuZCgNCiAgICAgICAgICAgIHN0X21ha2VlbXB0eXJhc3Rlcih7eHlbMV19LCB7eHlbMl19LA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHtkaW1zJHgkb2Zmc2V0fSx7ZGltcyR5JG9mZnNldH0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge3Jlc1sxXX0se3Jlc1syXX0sDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAge2FmZlsxXX0se2FmZlsyXX0se2Nyc30pLA0KICAgICAgICAgICAgYXJyYXlbcm93KDEsJ3tiaXRfZGVwdGh9JywwLC05OTk5OSldOjphZGRiYW5kYXJnW10pLA0KICAgICAgICAxLCAxLCAxLCAoc2VsZWN0IGFycmF5KHNlbGVjdCBhcnJheV9hZ2codmFsKSBmcm9tIHJzdF90aWxlX3ZhbCBncm91cCBieSB5IG9yZGVyIGJ5IHkpOjpkb3VibGUgcHJlY2lzaW9uW11bXSkNCiAgICAgKSkiKSkNCiAgfQ0KICBkYkV4ZWN1dGUoY29ubiwiZHJvcCB0YWJsZSBpZiBleGlzdHMgcnN0X3RpbGVfdmFsIikNCiAgZGJFeGVjdXRlKGNvbm4sIGdsdWUoImRyb3AgaW5kZXggaWYgZXhpc3RzIHtuYW1lfV9yYXN0X2lkeCIpKQ0KICAjIGNyZWF0ZSBzcGF0aWFsIGluZGV4DQogIGRiRXhlY3V0ZShjb25uLCBnbHVlKCJjcmVhdGUgaW5kZXgge25hbWV9X3Jhc3RfaWR4IG9uIHtuYW1lfSB1c2luZyBnaXN0KHN0X2NvbnZleGh1bGwocmFzdCkpIikpIA0KfQ0KYGBgDQpTaW1pbGFyIHRvIFtgcGdfcmVhZF9zdGFyc2BdKGh0dHBzOi8va3VmcmV1LmdpdGh1Yi5pby9tdXNpbmdzL3BnX3JlYWRfc3RhcnMvcGdfcmVhZF9zdGFycy5uYi5odG1sKSwgaXMgYSBzaW1wbGlmaWVkIHZlcnNpb24gb2YgYSBmdW5jdGlvbiBpbiBgcnBvc3RpZ2lzYC4gSXQgY2FuIG9ubHkgd3JpdGUgb25lIGJhbmQgb2YgYSByYXN0ZXIgYW5kIGl0IGFsd2F5cyBvdmVyd3JpdGVzIGV4aXN0aW5nIGZpbGVzLiBGaXJzdCwgaW5mb3JtYXRpb24gYWJvdXQgdGhlIHJhc3RlciBpcyBjb2xsZWN0ZWQgc3VjaCBhcyBpdHMgZGltZW5zaW9ucyBhbmQgYm91bmRpbmcgYm94LiBUaGUgYm91bmRpbmcgYm94IG9mIHRoZSByYXN0ZXIgaXMgdGhlbiBzcGxpdCBpbnRvIHRpbGVzLCB3aXRoIHRoZSBkZWZhdWx0IHRpbGUgc2l6ZSBiZWluZyAxMDB4MTAwIHBpeGVscyBhbmQgZWFjaCB0aWxlJ3MgZGltZW5zaW9ucyBhbmQgdmFsdWVzIGFyZSBwdXQgaW50byBhIGxpc3QgdXNpbmcgYG1hcGAuIEluIGEgc3Vic2VxdWVudCBsb29wLCB0aGUgcmFzdGVyIGlzIHdyaXR0ZW4gdG8gdGhlIFBvc3RHSVMgZGF0YWJhc2UgdGlsZSBieSB0aWxlLiBJbiB0aGlzIGxvb3AsIHRoZSB2YWx1ZXMgb2YgYSB0aWxlIGFyZSBmaXJzdCB3cml0dGVuIHRvIGEgdGFibGUgYW5kIHRoZW4gZW1wdHkgcmFzdGVyIGlzIGNyZWF0ZWQgYW5kIHBvcHVsYXRlZCB1c2luZyB0aGUgdmFsdWVzIGZyb20gdGhpcyB0YWJsZS4gSW4gb3JkZXIgdG8gYmUgdXNhYmxlLCB0aGUgdmFsdWVzIGluIHRoZSB0YWJsZSAoYHJzdF90aWxlX3ZhbGApIG5lZWQgdG8gYmUgbWFkZSBpbnRvIGEgbmVzdGVkIGFycmF5LiBUaGUgbmVzdGVkIGFycmF5cyBuZWVkIHRvIGJlIHRoZSBzYW1lIGxlbmd0aCBhcyB0aGUgd2lkdGggb2YgdGhlaXIgY29ycmVzcG9uZGluZyB0aWxlLCBzbyBpZiBhIHRpbGUgaGFkIGEgd2lkdGggb2YgMTAwIHBpeGVscywgZWFjaCBhcnJheSBzaG91bGQgaGF2ZSAxMDAgdmFsdWVzLiBBIHNwYXRpYWwgaW5kZXggaXMgdGhlbiBjcmVhdGVkIGFmdGVyIHRoZSByYXN0ZXIgaXMgd3JpdHRlbi4gDQpgYGB7ciBwZ19yZWFkX3N0YXJzLCBlY2hvPUZ9DQpwZ19yZWFkX3N0YXJzID0gZnVuY3Rpb24oY29ubiwgbGF5ZXIsIHJhc3QgPSAicmFzdCIpew0KICAjIHJlYWRzIHJhc3RlciBmcm9tIHBvc3RnaXMgZGF0YWJhc2UNCiAgIyBjb25uOiBEQklDb25uZWN0aW9uIG9iamVjdA0KICAjIGxheWVyOiBuYW1lIG9mIHRhYmxlIHdpdGggcmFzdGVyIA0KICAjIHJhc3Q6IG5hbWUgb2YgY29sdW1uIHRoYXQgY29udGFpbnMgcmFzdGVyIA0KICANCiAgbGlicmFyeShzdGFycykNCiAgbGlicmFyeShwdXJycikNCiAgbGlicmFyeShEQkkpDQogIGxpYnJhcnkoZ2x1ZSkNCiAgDQogIHZhbHVlcyA9IGRiR2V0UXVlcnkoDQogICAgY29ubiwNCiAgICBnbHVlKA0KICAgICAgIndpdGggb3JkZXJlZCBhcyAoDQogICAgICAgICAgc2VsZWN0IA0KICAgICAgICAgIHtyYXN0fSBhcyByYXN0LCANCiAgICAgICAgICByaWQgZnJvbSB7bGF5ZXJ9IA0KICAgICAgICAgIG9yZGVyIGJ5IHJpZCBhc2MpDQogICAgICAgc2VsZWN0DQogICAgICAgdW5uZXN0KHN0X2R1bXB2YWx1ZXMocmFzdCwxKSkgYXMgdmFsdWVzLA0KICAgICAgIHJvd19udW1iZXIoKSBvdmVyIChvcmRlciBieSByaWQpIGFzIGlkDQogICAgICAgZnJvbSBvcmRlcmVkIg0KICAgICkNCiAgKQ0KICANCiAgc3JpZCA9IGRiR2V0UXVlcnkoDQogICAgY29ubiwNCiAgICBnbHVlKA0KICAgICAgInNlbGVjdCBkaXN0aW5jdCANCiAgICAgICBzdF9zcmlkKHtyYXN0fSkgYXMgc3JpZCANCiAgICAgICBmcm9tIHtsYXllcn0iKSkkc3JpZCAlPiUgDQogICAgc3RfY3JzDQogIA0KICBib3hlcyA9IGRiR2V0UXVlcnkoDQogICAgY29ubiwgDQogICAgZ2x1ZSgNCiAgICAgICJzZWxlY3QgDQogICAgICAgc3RfYXNld2tiKHN0X2VudmVsb3BlKHtyYXN0fSkpIGFzIGdlb20gDQogICAgICAgZnJvbSB7bGF5ZXJ9DQogICAgICAgb3JkZXIgYnkgcmlkIGFzYyIpKSRnZW9tICU+JSANCiAgICBzdF9hc19zZmMoRVdLQiA9IFQpIA0KICANCiAgZGltcyA9IGRiR2V0UXVlcnkoDQogICAgY29ubiwgDQogICAgZ2x1ZSgNCiAgICAgICJzZWxlY3QgIA0KICAgICAgIHN0X3BpeGVsd2lkdGgoe3Jhc3R9KSBhcyBkeCwgDQogICAgICAgc3RfcGl4ZWxoZWlnaHQoe3Jhc3R9KSBhcyBkeSwNCiAgICAgICBzdF9za2V3eCh7cmFzdH0pIGFzIHN4LA0KICAgICAgIHN0X3NrZXd5KHtyYXN0fSkgYXMgc3kNCiAgICAgICBmcm9tIHtsYXllcn0NCiAgICAgICBsaW1pdCAxIikpDQogIA0KICByZXN1bHQgPSBtYXAyKGJveGVzLCB1bmlxdWUodmFsdWVzJGlkKSwgZnVuY3Rpb24oeCwgeSkNCiAgICBzdF9hc19zdGFycygNCiAgICAgIHN0X2Jib3goeCksDQogICAgICBkeCA9IGRpbXMkZHgsDQogICAgICBkeSA9IGRpbXMkZHksDQogICAgICB2YWx1ZXMgPSB2YWx1ZXNbdmFsdWVzJGlkID09IHksIF0kdmFsdWVzDQogICAgKSAlPiUgc3Rfc2V0X2NycyhzcmlkKSkgJT4lDQogICAgZG8uY2FsbCh3aGF0ID0gc3RfbW9zYWljKQ0KICANCiAgYXR0cihhdHRyKHJlc3VsdCwgImRpbWVuc2lvbnMiKSwgInJhc3RlciIpJGFmZmluZSA9IGMoZGltc1tbM11dLGRpbXNbWzRdXSkNCiAgDQogIHJlc3VsdA0KfQ0KYGBgDQoNCmBgYHtyIHJlYWRpbmcgYW5kIHdyaXRpbmcsIG1lc3NhZ2U9Riwgd2FybmluZz1GLCBmaWcuaGVpZ2h0PTYsIGZpZy53aWR0aD05fQ0KbGlicmFyeShrZXlyaW5nKQ0KbGlicmFyeShtYXB2aWV3KQ0KbGlicmFyeShzdGFycykNCmxpYnJhcnkoREJJKQ0KDQpjb25uID0gREJJOjpkYkNvbm5lY3QoDQogIFJQb3N0Z3Jlczo6UG9zdGdyZXMoKSwNCiAgZGJuYW1lID0ga2V5X2xpc3QoKVsxLCAxXSwNCiAgdXNlciA9IGtleV9saXN0KClbMSwgMl0sDQogIHBhc3N3b3JkID0ga2V5X2dldChrZXlfbGlzdCgpWzEsIDFdLCBrZXlfbGlzdCgpWzEsIDJdKQ0KKQ0KDQppbml0aWFsID0gcmVhZF9zdGFycygiZmxvb2RleHRlbnQyMDAudGlmIikNCg0KcGdfd3JpdGVfc3RhcnMoY29ubiwicGdfc3RhcnNfdGVzdCIsIGluaXRpYWwpDQoNCnBvc3RnaXMgPSBwZ19yZWFkX3N0YXJzKGNvbm4sInBnX3N0YXJzX3Rlc3QiKQ0KDQptYXB2aWV3KGluaXRpYWwsIGNvbC5yZWdpb25zID0gIiNFQTM1NDYiLCBsYXllci5uYW1lID0gIkluaXRpYWwgUmFzdGVyIikgK21hcHZpZXcocG9zdGdpcywgY29sLnJlZ2lvbnMgPSAiIzIyNkNFMCIsIGxheWVyLm5hbWUgPSAiUG9zdEdJUyBSYXN0ZXIiKSANCmBgYA0KDQpBcyBjYW4gYmUgc2VlbiBpbiB0aGlzIG1hcCwgdGhlcmUgYXJlIHN0aWxsIHNvbWUga2lua3MgdG8gd29yayBvdXQgaW4gYHBnX3dyaXRlX3N0YXJzYCwgc3BlY2lmaWNhbGx5IHRoZSBzaGlmdGluZyBvZiBzb21lIHBpeGVscyB0byB0aGUgcmlnaHQsIHRob3VnaCBJIGJlbGlldmUgSSBoYXZlIGEgZ29vZCBmb3VuZGF0aW9uIHRvIGJ1aWxkIG9uIGluIG9yZGVyIHRvIGZpeCB0aGlzLiBBbGwgdGhpbmdzIGNvbnNpZGVyZWQsIHRoaXMgd2FzIGEgZnVuIGV4ZXJjaXNlIG9mIGludGVncmF0aW5nIFNRTCBpbiBSLiAgDQo=