cboettig commited on
Commit
66a02d0
·
1 Parent(s): 8c2dbd8

overture-based location

Browse files
Files changed (3) hide show
  1. app.R +43 -75
  2. inat-ranges.R +63 -29
  3. old-app.R +108 -0
app.R CHANGED
@@ -1,102 +1,70 @@
 
1
  library(shiny)
2
  library(bslib)
3
- library(shinybusy)
4
 
5
- source("utils.R")
6
- source("inat-ranges.R")
7
- public_endpoint <- Sys.getenv("AWS_PUBLIC_ENDPOINT", Sys.getenv("AWS_S3_ENDPOINT"))
 
 
8
 
9
- # intialize data
10
- load_h3()
11
- load_spatial()
12
- duckdbfs::duckdb_secrets()
13
- inat <- open_dataset("s3://public-inat/hex")
14
- taxa <- open_dataset(glue("s3://public-inat/taxonomy/taxa.parquet"),
15
- recursive = FALSE) |> rename(taxon_id = id)
16
- cache <- tempfile(fileext = ".json")
17
 
 
 
18
 
19
- # I/O limited, we can have many more processes than CPU threads
20
- con <- duckdbfs::cached_connection()
21
- DBI::dbExecute(con, "SET threads = 128;")
22
 
23
- ###### User interface ######
24
  ui <- page_sidebar(
25
- title = "iNaturalist Rangemaps",
26
- markdown("Visualize species richness from [iNaturalist Range map datasets](https://www.inaturalist.org/pages/range_maps).
27
- Pan & zoom the map over the desired area and hit 'map richness', or draw the desired area with the draw tool.
28
- Filter by specific taxonomic ranks or view all 100,293 mapped species.
29
- Note that larger areas will be slower to compute. (Area selections that overlap the antimerdian may create visual artefacts).
30
- "),
31
  shinybusy::add_busy_spinner(),
 
 
32
  sidebar = sidebar(
33
- card(
34
- markdown("Filter by taxonomic group or toggle off to see all species."),
35
- input_switch("filter", "filter taxa:", value = TRUE),
36
- varSelectInput("rank", NULL, taxa, selected = "class"),
37
- textInput("taxon", NULL, "Aves")
38
- ),
39
- actionButton("get_features", "Map richness")
40
  ),
41
  card(
42
- maplibreOutput("map"),
 
43
  )
44
  )
45
 
46
- ###### Server ######
47
  server <- function(input, output, session) {
48
- # observeEvent(input$map_bbox, { }) # We can react to any zoom/pan on the map
49
 
50
- output$map <- renderMaplibre({
 
51
 
52
- # Hacky -- we sidecar the metadata here
53
- if(file.exists(cache)) {
 
 
 
 
 
 
 
 
 
 
 
54
  meta <- jsonlite::read_json(cache)
55
  print(meta$url)
56
  } else {
57
- meta <- list(X = -110,
58
- Y = 37,
59
- zoom = 4,
60
- url = paste0("https://",
61
- public_endpoint,
62
- "/public-data/inat-tmp-ranges.h3j"))
 
 
 
 
63
  }
64
-
65
  m <- richness_map(meta)
66
  m
67
-
68
  })
69
-
70
- observeEvent(input$get_features, {
71
- # Use the bbox as the focal area unless user has selected a focal area
72
- drawn_features <- get_drawn_features(mapboxgl_proxy("map"))
73
-
74
- if(nrow(drawn_features) > 0) {
75
- aoi <- drawn_features
76
- } else if (!is.null(input$map_bbox)){
77
- aoi <- sf::st_bbox(unlist(input$map_bbox), crs = 4326)
78
- } else {
79
- aoi <- spData::us_states
80
- }
81
-
82
- rank <- taxon <- NULL
83
- if (input$filter) {
84
- rank <- input$rank
85
- taxon <- input$taxon
86
- }
87
-
88
- message("Computing richness...")
89
- meta <- richness(inat, aoi, rank, taxon, zoom = input$map_zoom)
90
- jsonlite::write_json(meta, cache, auto_unbox = TRUE)
91
- message(paste("rendering", meta$url))
92
- session$reload()
93
-
94
-
95
-
96
- })
97
-
98
-
99
-
100
  }
101
 
102
- shinyApp(ui, server)
 
1
+ library(shinybusy)
2
  library(shiny)
3
  library(bslib)
4
+ library(colourpicker)
5
 
6
+ library(mapgl)
7
+ library(sf)
8
+ library(dplyr)
9
+ library(duckdbfs)
10
+ library(overture)
11
 
 
 
 
 
 
 
 
 
12
 
13
+ source("utils.R")
14
+ source("inat-ranges.R")
15
 
 
 
 
16
 
 
17
  ui <- page_sidebar(
 
 
 
 
 
 
18
  shinybusy::add_busy_spinner(),
19
+
20
+ title = "iNaturalist Species Ranges",
21
  sidebar = sidebar(
22
+ textInput("location", "Location", "California"),
23
+ varSelectInput("rank", NULL, taxa, selected = "scientificName"),
24
+ textInput("taxon", NULL, "Canis lupus"),
 
 
 
 
25
  ),
26
  card(
27
+ full_screen = TRUE,
28
+ maplibreOutput("map")
29
  )
30
  )
31
 
 
32
  server <- function(input, output, session) {
33
+ # output$map <- renderMaplibre({overture:::map(gdf)})
34
 
35
+ observeEvent(input$taxon, {
36
+ aoi <- get_division(input$location)
37
 
38
+ message("Computing richness...")
39
+
40
+ meta <- richness(inat, aoi, input$rank, input$taxon)
41
+ jsonlite::write_json(meta, cache, auto_unbox = TRUE)
42
+
43
+ message(paste("rendering", meta$url))
44
+
45
+ # session$reload()
46
+ }) |>
47
+ debounce(millis = 600)
48
+
49
+ output$map <- renderMaplibre({
50
+ if (file.exists(cache)) {
51
  meta <- jsonlite::read_json(cache)
52
  print(meta$url)
53
  } else {
54
+ meta <- list(
55
+ X = -110,
56
+ Y = 37,
57
+ zoom = 4,
58
+ url = paste0(
59
+ "https://",
60
+ public_endpoint,
61
+ "/public-data/inat-tmp-ranges.h3j"
62
+ )
63
+ )
64
  }
 
65
  m <- richness_map(meta)
66
  m
 
67
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
 
70
+ shinyApp(ui, server)
inat-ranges.R CHANGED
@@ -4,11 +4,44 @@ library(mapgl)
4
  library(glue)
5
 
6
 
7
- # Also requires get_h3_aoi() from utils.R
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
10
 
11
- public_endpoint <- Sys.getenv("AWS_PUBLIC_ENDPOINT", Sys.getenv("AWS_S3_ENDPOINT"))
 
 
 
 
 
12
  hash <- digest::digest(list(aoi, rank, taxon))
13
  s3 <- paste0("s3://public-data/cache/inat/", hash, ".h3j")
14
 
@@ -16,49 +49,49 @@ richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
16
 
17
  # filter
18
  if (!is.null(rank) && !is.null(taxon)) {
19
- taxa <- open_dataset(glue("s3://public-inat/taxonomy/taxa.parquet"),
20
- recursive = FALSE)
21
- inat <- taxa |>
 
 
22
  rename(taxon_id = id) |>
23
- filter(.data[[rank]] == taxon) |>
24
  select(taxon_id) |>
25
  inner_join(inat, by = "taxon_id")
26
  }
27
 
28
- h3_aoi <- get_h3_aoi(aoi, precision = 4) |> select(h3id)
29
 
30
  clock <- bench::bench_time({
31
- inat |>
32
- rename(h3id = h4) |>
33
- inner_join(h3_aoi, by = "h3id") |>
34
- distinct(taxon_id, h3id) |>
35
- group_by(h3id) |>
36
- summarise(n = n()) |>
37
- mutate(height = n / max(n)) |>
38
- duckdbfs::to_h3j(s3)
39
- # write_dataset("s3://public-data/inat-tmp-ranges.parquet")
40
  })
41
 
42
-
43
  center <- c(st_coordinates(st_centroid(st_as_sfc(st_bbox(aoi)))))
44
  url <- gsub("s3://", glue("https://{public_endpoint}/"), s3)
45
 
46
- meta <- list(X = center[1],
47
- Y = center[2],
48
- zoom = zoom,
49
- url = url,
50
- time = clock[[2]])
 
 
51
  return(meta)
52
  }
53
 
54
  richness_map <- function(meta) {
55
-
56
- m <-
57
  maplibre(center = c(meta$X, meta$Y), zoom = meta$zoom) |>
58
  add_draw_control() |>
59
- add_h3j_source("h3j_source",
60
- url = meta$url
61
- ) |>
62
  add_fill_extrusion_layer(
63
  id = "h3j_layer",
64
  source = "h3j_source",
@@ -69,7 +102,8 @@ richness_map <- function(meta) {
69
  list("linear"),
70
  list("zoom"),
71
  0,
72
- 0, 1,
 
73
  list("*", 100000, list("get", "height"))
74
  ),
75
  fill_extrusion_opacity = 0.7
 
4
  library(glue)
5
 
6
 
7
+ public_endpoint <- Sys.getenv(
8
+ "AWS_PUBLIC_ENDPOINT",
9
+ Sys.getenv("AWS_S3_ENDPOINT")
10
+ )
11
+ # intialize data
12
+
13
+ duckdbfs::duckdb_secrets()
14
+ inat <- open_dataset("s3://public-inat/hex")
15
+ taxa <- open_dataset(
16
+ glue("s3://public-inat/taxonomy/taxa.parquet"),
17
+ recursive = FALSE
18
+ )
19
+
20
+ sci <- duckdbfs::open_dataset(
21
+ "s3://public-inat/taxonomy/taxa.parquet",
22
+ recursive = FALSE
23
+ )
24
+ common <- duckdbfs::open_dataset(
25
+ "s3://public-inat/taxonomy/vernacular/VerqnacularNames-english.csv",
26
+ format = "csv",
27
+ recursive = FALSE
28
+ ) |>
29
+ select(id, vernacularName)
30
+
31
+ taxa <- left_join(sci, common, by = "id") |>
32
+ rename(taxon_id = id)
33
+
34
+ common_names <- taxa |> pull(vernacularName)
35
+
36
+ cache <- tempfile(fileext = ".json")
37
 
 
38
 
39
+ # Also requires get_h3_aoi() from utils.R
40
+ richness <- function(inat, aoi, rank = NULL, taxon = NULL, zoom = 3) {
41
+ public_endpoint <- Sys.getenv(
42
+ "AWS_PUBLIC_ENDPOINT",
43
+ Sys.getenv("AWS_S3_ENDPOINT")
44
+ )
45
  hash <- digest::digest(list(aoi, rank, taxon))
46
  s3 <- paste0("s3://public-data/cache/inat/", hash, ".h3j")
47
 
 
49
 
50
  # filter
51
  if (!is.null(rank) && !is.null(taxon)) {
52
+ taxa <- open_dataset(
53
+ glue("s3://public-inat/taxonomy/taxa.parquet"),
54
+ recursive = FALSE
55
+ )
56
+ inat <- taxa |>
57
  rename(taxon_id = id) |>
58
+ filter(.data[[rank]] == taxon) |>
59
  select(taxon_id) |>
60
  inner_join(inat, by = "taxon_id")
61
  }
62
 
63
+ h3_aoi <- get_h3_aoi(aoi, precision = 4) |> select(h3id)
64
 
65
  clock <- bench::bench_time({
66
+ inat |>
67
+ rename(h3id = h4) |>
68
+ inner_join(h3_aoi, by = "h3id") |>
69
+ distinct(taxon_id, h3id) |>
70
+ group_by(h3id) |>
71
+ summarise(n = n()) |>
72
+ mutate(height = n / max(n)) |>
73
+ duckdbfs::to_h3j(s3)
74
+ # write_dataset("s3://public-data/inat-tmp-ranges.parquet")
75
  })
76
 
 
77
  center <- c(st_coordinates(st_centroid(st_as_sfc(st_bbox(aoi)))))
78
  url <- gsub("s3://", glue("https://{public_endpoint}/"), s3)
79
 
80
+ meta <- list(
81
+ X = center[1],
82
+ Y = center[2],
83
+ zoom = zoom,
84
+ url = url,
85
+ time = clock[[2]]
86
+ )
87
  return(meta)
88
  }
89
 
90
  richness_map <- function(meta) {
91
+ m <-
 
92
  maplibre(center = c(meta$X, meta$Y), zoom = meta$zoom) |>
93
  add_draw_control() |>
94
+ add_h3j_source("h3j_source", url = meta$url) |>
 
 
95
  add_fill_extrusion_layer(
96
  id = "h3j_layer",
97
  source = "h3j_source",
 
102
  list("linear"),
103
  list("zoom"),
104
  0,
105
+ 0,
106
+ 1,
107
  list("*", 100000, list("get", "height"))
108
  ),
109
  fill_extrusion_opacity = 0.7
old-app.R ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ library(shiny)
2
+ library(bslib)
3
+ library(shinybusy)
4
+
5
+ source("utils.R")
6
+ source("inat-ranges.R")
7
+ public_endpoint <- Sys.getenv(
8
+ "AWS_PUBLIC_ENDPOINT",
9
+ Sys.getenv("AWS_S3_ENDPOINT")
10
+ )
11
+
12
+ # intialize data
13
+ load_h3()
14
+ load_spatial()
15
+ duckdbfs::duckdb_secrets()
16
+ inat <- open_dataset("s3://public-inat/hex")
17
+ taxa <- open_dataset(
18
+ glue("s3://public-inat/taxonomy/taxa.parquet"),
19
+ recursive = FALSE
20
+ ) |>
21
+ rename(taxon_id = id)
22
+ cache <- tempfile(fileext = ".json")
23
+
24
+
25
+ # I/O limited, we can have many more processes than CPU threads
26
+ con <- duckdbfs::cached_connection()
27
+ DBI::dbExecute(con, "SET threads = 128;")
28
+
29
+ ###### User interface ######
30
+ ui <- page_sidebar(
31
+ title = "iNaturalist Rangemaps",
32
+ markdown(
33
+ "Visualize species richness from [iNaturalist Range map datasets](https://www.inaturalist.org/pages/range_maps).
34
+ Pan & zoom the map over the desired area and hit 'map richness', or draw the desired area with the draw tool.
35
+ Filter by specific taxonomic ranks or view all 100,293 mapped species.
36
+ Note that larger areas will be slower to compute. (Area selections that overlap the antimerdian may create visual artefacts).
37
+ "
38
+ ),
39
+ shinybusy::add_busy_spinner(),
40
+ sidebar = sidebar(
41
+ card(
42
+ markdown("Filter by taxonomic group or toggle off to see all species."),
43
+ input_switch("filter", "filter taxa:", value = TRUE),
44
+ varSelectInput("rank", NULL, taxa, selected = "class"),
45
+ textInput("taxon", NULL, "Aves"),
46
+ textInput("aoi", "Location", "California"),
47
+ ),
48
+ actionButton("get_features", "Map richness")
49
+ ),
50
+ card(
51
+ maplibreOutput("map"),
52
+ )
53
+ )
54
+
55
+ ###### Server ######
56
+ server <- function(input, output, session) {
57
+ # observeEvent(input$map_bbox, { }) # We can react to any zoom/pan on the map
58
+
59
+ output$map <- renderMaplibre({
60
+ # Hacky -- we sidecar the metadata here
61
+ # Cache at the URL level instead?
62
+ if (file.exists(cache)) {
63
+ meta <- jsonlite::read_json(cache)
64
+ print(meta$url)
65
+ } else {
66
+ meta <- list(
67
+ X = -110,
68
+ Y = 37,
69
+ zoom = 4,
70
+ url = paste0(
71
+ "https://",
72
+ public_endpoint,
73
+ "/public-data/inat-tmp-ranges.h3j"
74
+ )
75
+ )
76
+ }
77
+
78
+ m <- richness_map(meta)
79
+ m
80
+ })
81
+
82
+ observeEvent(input$get_features, {
83
+ # Use the bbox as the focal area unless user has selected a focal area
84
+ drawn_features <- get_drawn_features(mapboxgl_proxy("map"))
85
+
86
+ if (nrow(drawn_features) > 0) {
87
+ aoi <- drawn_features
88
+ } else if (!is.null(input$map_bbox)) {
89
+ aoi <- sf::st_bbox(unlist(input$map_bbox), crs = 4326)
90
+ } else {
91
+ aoi <- spData::us_states
92
+ }
93
+
94
+ rank <- taxon <- NULL
95
+ if (input$filter) {
96
+ rank <- input$rank
97
+ taxon <- input$taxon
98
+ }
99
+
100
+ message("Computing richness...")
101
+ meta <- richness(inat, aoi, rank, taxon, zoom = input$map_zoom)
102
+ jsonlite::write_json(meta, cache, auto_unbox = TRUE)
103
+ message(paste("rendering", meta$url))
104
+ session$reload()
105
+ })
106
+ }
107
+
108
+ shinyApp(ui, server)