-
-
Save ar-puuk/96904e70306686dcd3a99b83e76b1b9a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # DuckDB Vector Tiles with mapgl Example | |
| # This script demonstrates how to serve vector tiles from DuckDB using ST_AsMVT() | |
| # and display them in a mapgl map using httpuv | |
| library(mapgl) | |
| library(duckdb) # Requires the latest DuckDB version (>= 1.4.0) | |
| library(httpuv) | |
| library(sf) | |
| library(duckspatial) | |
| library(tigris) | |
| # 1. Setup DuckDB connection and load spatial extension | |
| con <- dbConnect(duckdb::duckdb(), dbdir = "tiles.duckdb") | |
| ddbs_install(con) | |
| ddbs_load(con) | |
| # 2. Load your spatial data into DuckDB | |
| # Example: Load an sf object into DuckDB and transform to Web Mercator (EPSG:3857) | |
| # All US block groups: 242,000 polygons | |
| data <- block_groups(cb = TRUE) | |
| # Transform to Web Mercator for tiling | |
| data_mercator <- st_transform(data, 3857) | |
| # Write to DuckDB using duckspatial | |
| ddbs_write_vector(con, data_mercator, "features", overwrite = TRUE) | |
| # Optional: Create a spatial index for better performance | |
| dbExecute(con, "CREATE INDEX idx_geom ON features USING RTREE (geometry)") | |
| # 3. Create a lightweight httpuv server for serving tiles | |
| # Helper function to parse tile coordinates from URL path | |
| parse_tile_path <- function(path) { | |
| # Match pattern: /tiles/{z}/{x}/{y}.pbf | |
| pattern <- "^/tiles/(\\d+)/(\\d+)/(\\d+)\\.pbf$" | |
| matches <- regmatches(path, regexec(pattern, path))[[1]] | |
| if (length(matches) == 4) { | |
| list( | |
| z = as.integer(matches[2]), | |
| x = as.integer(matches[3]), | |
| y = as.integer(matches[4]) | |
| ) | |
| } else { | |
| NULL | |
| } | |
| } | |
| # Create the httpuv application | |
| tile_app <- list( | |
| call = function(req) { | |
| path <- req$PATH_INFO | |
| # Handle CORS preflight requests | |
| if (req$REQUEST_METHOD == "OPTIONS") { | |
| return(list( | |
| status = 200L, | |
| headers = list( | |
| 'Access-Control-Allow-Origin' = '*', | |
| 'Access-Control-Allow-Methods' = 'GET, OPTIONS', | |
| 'Access-Control-Allow-Headers' = '*' | |
| ), | |
| body = "" | |
| )) | |
| } | |
| # Parse tile coordinates from URL | |
| tile_coords <- parse_tile_path(path) | |
| if (!is.null(tile_coords)) { | |
| # Query DuckDB for the tile | |
| tile_query <- " | |
| SELECT ST_AsMVT(mvt_geom, 'layer') | |
| FROM ( | |
| SELECT | |
| GEOID, | |
| NAME, | |
| ST_AsMVTGeom( | |
| geometry, | |
| (SELECT ST_Extent(ST_TileEnvelope(?, ?, ?))) | |
| ) AS geometry | |
| FROM features | |
| WHERE ST_Intersects(geometry, ST_TileEnvelope(?, ?, ?)) | |
| ) AS mvt_geom | |
| " | |
| tryCatch( | |
| { | |
| result <- dbGetQuery( | |
| con, | |
| tile_query, | |
| params = list( | |
| tile_coords$z, | |
| tile_coords$x, | |
| tile_coords$y, | |
| tile_coords$z, | |
| tile_coords$x, | |
| tile_coords$y | |
| ) | |
| ) | |
| # Extract tile blob | |
| tile_blob <- if (!is.null(result[[1]][[1]])) { | |
| result[[1]][[1]] | |
| } else { | |
| raw(0) | |
| } | |
| # Return successful response | |
| list( | |
| status = 200L, | |
| headers = list( | |
| 'Content-Type' = 'application/x-protobuf', | |
| 'Access-Control-Allow-Origin' = '*' # Enable CORS | |
| ), | |
| body = tile_blob | |
| ) | |
| }, | |
| error = function(e) { | |
| # Return error response | |
| list( | |
| status = 500L, | |
| headers = list( | |
| 'Content-Type' = 'text/plain', | |
| 'Access-Control-Allow-Origin' = '*' | |
| ), | |
| body = paste("Error generating tile:", e$message) | |
| ) | |
| } | |
| ) | |
| } else { | |
| # Return 404 for non-tile requests | |
| list( | |
| status = 404L, | |
| headers = list( | |
| 'Content-Type' = 'text/plain', | |
| 'Access-Control-Allow-Origin' = '*' | |
| ), | |
| body = "Not Found" | |
| ) | |
| } | |
| } | |
| ) | |
| # 4. Start the tile server | |
| # Find available port (starting from 8000) | |
| find_available_port <- function(start_port = 8000, max_attempts = 10) { | |
| for (i in 0:(max_attempts - 1)) { | |
| port <- start_port + i | |
| tryCatch( | |
| { | |
| # Try to start server on this port | |
| test_server <- startServer( | |
| "127.0.0.1", | |
| port, | |
| list(call = function(req) { | |
| list(status = 200L, body = "test") | |
| }) | |
| ) | |
| stopServer(test_server) | |
| return(port) | |
| }, | |
| error = function(e) { | |
| # Port is in use, try next one | |
| } | |
| ) | |
| } | |
| stop("Could not find available port") | |
| } | |
| port <- find_available_port() | |
| cat("Starting tile server on port", port, "\n") | |
| # Start the server in the background | |
| server <- startDaemonizedServer("127.0.0.1", port, tile_app) | |
| # Store server info for cleanup | |
| tile_server_info <- list( | |
| server = server, | |
| port = port, | |
| con = con | |
| ) | |
| cat("Tile server running at http://127.0.0.1:", port, "/\n", sep = "") | |
| cat( | |
| "Tiles available at: http://127.0.0.1:", | |
| port, | |
| "/tiles/{z}/{x}/{y}.pbf\n", | |
| sep = "" | |
| ) | |
| # 5. Create the mapgl map with the DuckDB vector tile source | |
| m <- maplibre( | |
| center = c(-79.5, 35.5), | |
| zoom = 6, | |
| style = carto_style("positron") | |
| ) |> | |
| add_vector_source( | |
| id = "duckdb-tiles", | |
| tiles = paste0("http://127.0.0.1:", port, "/tiles/{z}/{x}/{y}.pbf"), | |
| minzoom = 0, | |
| maxzoom = 14, | |
| promote_id = "GEOID" | |
| ) |> | |
| add_fill_layer( | |
| id = "features-fill", | |
| source = "duckdb-tiles", | |
| source_layer = "layer", # ST_AsMVT default layer name | |
| fill_color = "steelblue", | |
| fill_opacity = 0.6, | |
| tooltip = "GEOID", | |
| hover_options = list( | |
| fill_color = "yellow", | |
| fill_opacity = 1 | |
| ) | |
| ) |> | |
| add_line_layer( | |
| id = "features-line", | |
| source = "duckdb-tiles", | |
| source_layer = "layer", | |
| line_color = "white", | |
| line_width = 1 | |
| ) | |
| # Display the map | |
| m | |
| # Cleanup function (run this when done) | |
| cleanup_tile_server <- function() { | |
| if (exists("tile_server_info")) { | |
| stopDaemonizedServer(tile_server_info$server) | |
| dbDisconnect(tile_server_info$con) | |
| cat("Tile server stopped and database disconnected\n") | |
| } | |
| } | |
| # Note: To stop the server and disconnect, run: | |
| # cleanup_tile_server() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment