Skip to content

Commit d675dfd

Browse files
committed
carbon works! 🌱
1 parent 3e1983a commit d675dfd

5 files changed

Lines changed: 247 additions & 112 deletions

File tree

app/app.R

Lines changed: 66 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ Smaller areas will be faster to compute! Zoom in further to show richness with
8181
card(
8282
card_header("Biodiversity"),
8383
# chat_ui("chat", placeholder = "hummingbirds"),
84-
actionLink("get_richness", "🐦 richness hexes"),
85-
actionLink("get_mean_richness", "🐦 zonal richness")
84+
actionLink("get_richness", "🐦 GBIF species richness"),
85+
actionLink("get_carbon", "🌱 vulnerable carbon"),
8686
),
8787
card(
8888
card_header("Resolution"),
@@ -93,7 +93,8 @@ Smaller areas will be faster to compute! Zoom in further to show richness with
9393
max = MAXZOOM,
9494
value = 2,
9595
step = 1
96-
)
96+
),
97+
input_switch("show_hexes", "show hexes", value = FALSE)
9798
),
9899
br(),
99100
input_switch("toggle_natgeo", "natgeo", value = TRUE),
@@ -128,7 +129,7 @@ server <- function(input, output, session) {
128129

129130
taxa_selections <- taxonomicSelectorServer("taxa_selector")
130131

131-
# React to changes in taxonomic selections
132+
# Update taxa selector tool to drill down on taxonomic selections
132133
observe({
133134
selections <- taxa_selections$selections()
134135
print(paste(
@@ -137,34 +138,9 @@ server <- function(input, output, session) {
137138
))
138139
taxa_filter(selections)
139140
})
140-
# Waterfall strategy to determine feature selection:
141-
get_active_feature <- function(input) {
142-
gdf <- active_feature()
143-
144-
if (is_empty(gdf)) {
145-
print("No feature selected, checking for drawing")
146-
gdf <- get_drawn_features(maplibre_proxy("map"))
147-
}
148-
if (is_empty(gdf)) {
149-
print("No drawing found, checking geocoder")
150-
gdf <- geocoder_to_gdf(input$map_geocoder)
151-
}
152-
if (is_empty(gdf)) {
153-
print("No geocoder, getting current bbox")
154-
bbox <- input$map_bbox
155-
print(bbox)
156-
if (!is.null(bbox)) {
157-
gdf <- get_polygon_bbox(bbox)
158-
}
159-
}
160-
161-
if (is_empty(gdf)) {
162-
warning("No selection found, using default!")
163-
return(spData::us_states)
164-
}
165141

166-
gdf
167-
}
142+
# Waterfall strategy to determine feature selection:
143+
# FIXME select all (visible) protected areas? select protected areas by filtering them?
168144

169145
# Set up the map:
170146
output$map <- renderMaplibre({
@@ -195,6 +171,7 @@ server <- function(input, output, session) {
195171
m <- m |> add_hillshade(visibility = "none")
196172

197173
m <- m |> richness_layer() # default richness layer
174+
m <- m |> carbon_layer() # default carbon layer
198175

199176
m |> add_countries()
200177
})
@@ -239,30 +216,6 @@ server <- function(input, output, session) {
239216
proxy |> mapgl::set_filter(input$layer_selection, layer_filter())
240217
})
241218

242-
# Observe chat input
243-
observeEvent(input$chat_user_input, {
244-
taxa_selected <- txt_to_taxa(input$chat_user_input)
245-
246-
resp <- bot_response(taxa_selected, input$resolution)
247-
print(resp)
248-
chat_append("chat", resp)
249-
250-
# optionally - store the selection as global variable for future reactions
251-
taxa_filter(taxa_selected)
252-
253-
# we can react right away, computing richness and updating map
254-
gdf <- get_richness(
255-
poly = get_active_feature(input),
256-
zoom = as.integer(input$resolution),
257-
taxa_selections = taxa_selected
258-
)
259-
260-
chat_clear("chat")
261-
262-
maplibre_proxy("map") |>
263-
set_source("richness", gdf)
264-
})
265-
266219
# Zoom into selected feature, move down a layer, show resulting child features
267220
observeEvent(input$map_feature_click, {
268221
x <- input$map_feature_click
@@ -316,35 +269,53 @@ server <- function(input, output, session) {
316269
})
317270

318271
observeEvent(input$get_richness, {
319-
gdf <- get_richness(
320-
poly = get_active_feature(input),
321-
zoom = as.integer(input$resolution),
322-
taxa_selections = taxa_filter()
323-
)
272+
poly <- get_active_feature(active_feature(), input)
273+
274+
if (input$show_hexes) {
275+
gdf <- get_richness(
276+
poly = poly,
277+
zoom = as.integer(input$resolution),
278+
taxa_selections = taxa_filter()
279+
)
280+
} else {
281+
layer <- layer_config[[input$layer_selection]]$parent_layer
282+
child_poly <- child_polygons(poly, layer, layer_config)
283+
gdf <- get_zonal_richness(
284+
child_poly,
285+
zoom = as.integer(input$resolution),
286+
taxa_selections = taxa_filter()
287+
)
288+
}
324289

325290
print(gdf)
326291
maplibre_proxy("map") |>
327292
set_source("richness", gdf)
328293
})
329294

330-
observeEvent(input$get_mean_richness, {
331-
poly <- get_active_feature(input)
332-
layer <- layer_config[[input$layer_selection]]$parent_layer
333-
child_poly <- child_polygons(poly, layer, layer_config)
334-
gdf <- get_zonal_richness(child_poly, zoom = as.integer(input$resolution))
295+
observeEvent(input$get_carbon, {
296+
poly <- get_active_feature(active_feature(), input)
297+
298+
if (input$show_hexes) {
299+
gdf <- get_carbon(
300+
poly = poly,
301+
zoom = as.integer(input$resolution)
302+
)
303+
} else {
304+
layer <- layer_config[[input$layer_selection]]$parent_layer
305+
child_poly <- child_polygons(poly, layer, layer_config)
306+
gdf <- get_mean_carbon(child_poly, zoom = as.integer(input$resolution))
307+
}
335308

336-
print("mapping zonal richness...")
337-
maplibre_proxy("map") |> set_source("richness", gdf)
309+
print(gdf)
310+
maplibre_proxy("map") |>
311+
set_source("carbon", gdf)
338312
})
339313

314+
# Layer selection tools
340315
observeEvent(input$clear_filters, {
341316
maplibre_proxy("map") |> set_filter(input$layer_selection, NULL)
342317
})
343318

344-
observeEvent(input$clear_richness, {
345-
maplibre_proxy("map") |> set_source("richness", "https://example.com")
346-
})
347-
348319
observeEvent(input$clear_area, {
349320
active_feature(NULL)
350321
layer_filter(NULL)
@@ -405,6 +376,30 @@ server <- function(input, output, session) {
405376
set_style(input$basemap)
406377
}
407378
})
379+
380+
# Observe chat input
381+
observeEvent(input$chat_user_input, {
382+
taxa_selected <- txt_to_taxa(input$chat_user_input)
383+
384+
resp <- bot_response(taxa_selected, input$resolution)
385+
print(resp)
386+
chat_append("chat", resp)
387+
388+
# optionally - store the selection as global variable for future reactions
389+
taxa_filter(taxa_selected)
390+
391+
# we can react right away, computing richness and updating map
392+
gdf <- get_richness(
393+
poly = get_active_feature(active_feature(), input),
394+
zoom = as.integer(input$resolution),
395+
taxa_selections = taxa_selected
396+
)
397+
398+
chat_clear("chat")
399+
400+
maplibre_proxy("map") |>
401+
set_source("richness", gdf)
402+
})
408403
}
409404

410405
shinyApp(ui, server)

app/data-layers.R

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,27 @@ richness_layer <- function(
193193
)
194194
}
195195

196+
carbon_layer <- function(
197+
map,
198+
gdf = "https://minio.carlboettiger.info",
199+
n_stops = 7
200+
) {
201+
map |>
202+
mapgl::add_source("carbon_source", gdf) |>
203+
mapgl::add_fill_extrusion_layer(
204+
id = "carbon",
205+
source = "carbon_source",
206+
tooltip = concat("carbon:", mapgl::get_column("carbon")),
207+
fill_extrusion_color = mapgl::interpolate(
208+
column = "value",
209+
values = seq(0, 1, length.out = n_stops),
210+
stops = viridisLite::viridis(n_stops, option = "viridis")
211+
),
212+
fill_extrusion_height = list("*", 10000, list("get", "value")),
213+
fill_extrusion_opacity = 0.7
214+
)
215+
}
216+
196217

197218
add_richness_2d <- function(map, gdf) {
198219
map |>

app/tools-carbon.R

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ get_carbon <- function(
4141
poly,
4242
zoom = 8L,
4343
id_column = "id",
44-
max_features = getOption("shiny_max_features", 20000L),
4544
warning = TRUE,
4645
verbose = TRUE,
47-
server = Sys.getenv("AWS_S3_ENDPOINT", "minio.carlboettiger.info")
46+
server = Sys.getenv("AWS_S3_ENDPOINT", "minio.carlboettiger.info"),
47+
bucket = "public-data/cache/gbif-app"
4848
) {
4949
duckdbfs::load_h3()
5050

@@ -55,25 +55,46 @@ get_carbon <- function(
5555
## This operation is maybe always fast enough not to cache?
5656
carbon <- open_carbon_region(poly_hexed, server) |>
5757
dplyr::group_by(h3id) |>
58-
dplyr::summarise(carbon = mean(carbon))
59-
60-
# in-memory gdf will crash above a certain number of hexes
61-
if (warning) {
62-
n_features <- carbon |> count() |> pull(n)
63-
print(paste("computed", n_features, "hexes"))
64-
if (n_features > max_features) {
65-
warning(paste("returning only first", max_features, "of", n_features))
66-
}
67-
}
58+
dplyr::summarise(carbon = mean(carbon)) |>
59+
dplyr::mutate(value = carbon / max(carbon)) # normalize for color scale
6860

6961
carbon <- carbon |>
70-
head(max_features) |> # max number of features
71-
dplyr::mutate(geom = h3_cell_to_boundary_wkt(h3id))
62+
dplyr::mutate(geom = ST_GeomFromText(h3_cell_to_boundary_wkt(h3id)))
7263

73-
hash <- digest::digest(list(gdf, zoom, id_column, label))
64+
## this part should be separate? Or be included in cache logic.
65+
label <- "carbon"
66+
hash <- digest::digest(list(carbon, zoom, id_column, label))
7467
s3 <- glue::glue("s3://{bucket}/{label}/{hash}.geojson")
75-
duckdbfs::to_geojson(gdf, s3, as_http = TRUE)
68+
duckdbfs::to_geojson(carbon, s3, as_http = TRUE)
7669
}
7770

7871

79-
get_mean_carbon <- function() {}
72+
get_mean_carbon <- function(
73+
poly,
74+
zoom = 8L,
75+
id_column = "id",
76+
warning = TRUE,
77+
verbose = TRUE,
78+
server = Sys.getenv("AWS_S3_ENDPOINT", "minio.carlboettiger.info"),
79+
bucket = "public-data/cache/gbif-app"
80+
) {
81+
# get_h3_aoi is self-caching, shared across metrics
82+
poly_hexed_url <- get_h3_aoi(poly, precision = zoom, keep_cols = id_column)
83+
poly_hexed <- duckdbfs::open_dataset(poly_hexed_url, recursive = FALSE)
84+
85+
## This operation is maybe always fast enough not to cache?
86+
carbon <- open_carbon_region(poly_hexed, server) |>
87+
dplyr::group_by(.data[[id_column]]) |>
88+
dplyr::summarise(carbon = mean(carbon)) |>
89+
dplyr::mutate(value = carbon / max(carbon)) # normalize for color scale
90+
91+
gdf <- poly |>
92+
dplyr::select(dplyr::all_of(id_column), geometry) |>
93+
dplyr::inner_join(carbon, by = id_column) |>
94+
rename(geom = "geometry")
95+
96+
label <- "carbon"
97+
hash <- digest::digest(list(gdf, zoom, id_column, label))
98+
s3 <- glue::glue("s3://{bucket}/{label}/{hash}.geojson")
99+
duckdbfs::to_geojson(gdf, s3, as_http = TRUE)
100+
}

0 commit comments

Comments
 (0)