From 1b468fb4860ed0c34a638fef886ea495fcd5f069 Mon Sep 17 00:00:00 2001 From: Cidree Date: Fri, 26 Jun 2026 19:57:55 +0200 Subject: [PATCH 1/4] feat: add duckspatial_df support across map functions Add support for `duckspatial_df` objects in `add_source()`, `add_layer()`, all specific layer functions, `fit_bounds()`, `maplibre()`, `mapboxgl()`, `set_source()`, and `add_markers()`. Introduces a `ddbs_ensure_wgs84()` helper in utils.R to centralise CRS validation and reprojection. Update docs and DESCRIPTION imports accordingly. Co-Authored-By: Claude Sonnet 4.6 --- DESCRIPTION | 31 ++++++++++++++++--------------- R/layers.R | 30 +++++++++++++++++++----------- R/mapboxgl.R | 4 ++++ R/maplibre.R | 4 ++++ R/markers.R | 21 ++++++++++++++++++++- R/shiny.R | 4 +++- R/sources.R | 7 +++++-- R/utils.R | 14 ++++++++++++++ R/views.R | 5 ++++- man/add_circle_layer.Rd | 2 +- man/add_fill_extrusion_layer.Rd | 2 +- man/add_fill_layer.Rd | 2 +- man/add_heatmap_layer.Rd | 2 +- man/add_layer.Rd | 2 +- man/add_line_layer.Rd | 6 +++--- man/add_markers.Rd | 2 +- man/add_source.Rd | 2 +- man/add_symbol_layer.Rd | 2 +- man/fit_bounds.Rd | 2 +- man/mapboxgl.Rd | 1 + man/mapgl-package.Rd | 5 +++++ man/maplibre.Rd | 1 + man/set_source.Rd | 2 +- 23 files changed, 109 insertions(+), 44 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 863b4061..0ba1d87d 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,24 +16,24 @@ License: MIT + file LICENSE Encoding: UTF-8 LazyData: true Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 Depends: R (>= 4.1.0) Imports: - htmlwidgets, - geojsonsf, - sf, - rlang, - htmltools, - grDevices, - base64enc, - terra, - classInt, - shiny, - viridisLite, - png, - jsonlite, - httpuv + htmlwidgets, + geojsonsf, + sf, + rlang, + htmltools, + grDevices, + base64enc, + terra, + classInt, + shiny, + viridisLite, + png, + jsonlite, + httpuv, + duckspatial Suggests: chromote, knitr, @@ -42,3 +42,4 @@ Suggests: usethis, leaflet Config/testthat/edition: 3 +Config/roxygen2/version: 8.0.0 diff --git a/R/layers.R b/R/layers.R index 29552868..5e2d2659 100644 --- a/R/layers.R +++ b/R/layers.R @@ -5,7 +5,7 @@ #' @param map A map object created by the `mapboxgl()` or `maplibre()` functions. #' @param id A unique ID for the layer. #' @param type The type of the layer (e.g., "fill", "line", "circle"). -#' @param source The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies `type` and `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a `duckspatial_df` object (both converted to a GeoJSON source), or a named list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param paint A list of paint properties for the layer. #' @param layout A list of layout properties for the layer. @@ -102,6 +102,14 @@ add_layer <- function( data = geojson, generateId = TRUE ) + } else if (inherits(source, "duckspatial_df")) { + source <- ddbs_ensure_wgs84(source, "source") + geojson <- duckspatial::ddbs_as_geojson(source) + source <- list( + type = "geojson", + data = geojson, + generateId = TRUE + ) } map <- mapgl_resolve_pending_flowmaps(map, before_id = id) @@ -206,7 +214,7 @@ add_layer <- function( #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. #' @param id A unique ID for the layer. -#' @param source The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies `type` and `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a `duckspatial_df` object (both converted to a GeoJSON source), or a named list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param fill_antialias Whether or not the fill should be antialiased. #' @param fill_color The color of the filled part of this layer. @@ -335,9 +343,9 @@ add_fill_layer <- function( #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. #' @param id A unique ID for the layer. -#' @param source The ID of the source, alternatively an sf object (which will be -#' converted to a GeoJSON source) or a named list that specifies `type` and -#' `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a +#' `duckspatial_df` object (both converted to a GeoJSON source), or a named +#' list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param line_blur Amount to blur the line, in pixels. #' @param line_cap The display of line endings. One of "butt", "round", "square". @@ -518,7 +526,7 @@ add_line_layer <- function( #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. #' @param id A unique ID for the layer. -#' @param source The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies `type` and `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a `duckspatial_df` object (both converted to a GeoJSON source), or a named list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param heatmap_color The color of the heatmap points. #' @param heatmap_intensity The intensity of the heatmap points. @@ -623,7 +631,7 @@ add_heatmap_layer <- function( #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. #' @param id A unique ID for the layer. -#' @param source The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies `type` and `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a `duckspatial_df` object (both converted to a GeoJSON source), or a named list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param fill_extrusion_ambient_occlusion_intensity Controls the intensity of ambient occlusion shading. Value between 0 and 1; around 0.3 provides the most plausible results for buildings. #' @param fill_extrusion_ambient_occlusion_radius Shades area near ground and concave angles between walls. Default 3.0 corresponds to one floor height. @@ -958,7 +966,7 @@ cluster_options <- function( #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. #' @param id A unique ID for the layer. -#' @param source The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies `type` and `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a `duckspatial_df` object (both converted to a GeoJSON source), or a named list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param circle_blur Amount to blur the circle. #' @param circle_color The color of the circle. @@ -1122,7 +1130,7 @@ add_circle_layer <- function( # Dispatch on source shape. sf/sfc takes precedence so existing # calls that incidentally pass `source_layer` alongside sf data # (silently ignored today) don't regress. - if (inherits(source, c("sf", "sfc"))) { + if (inherits(source, c("sf", "sfc", "duckspatial_df"))) { # Native live clustering: inject a clustered GeoJSON source. map <- add_source( map, @@ -1387,7 +1395,7 @@ add_raster_layer <- function( #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. #' @param id A unique ID for the layer. -#' @param source The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies `type` and `url` for a remote source. +#' @param source The ID of the source, alternatively an sf object or a `duckspatial_df` object (both converted to a GeoJSON source), or a named list that specifies `type` and `url` for a remote source. #' @param source_layer The source layer (for vector sources). #' @param icon_allow_overlap If TRUE, the icon will be visible even if it collides with other previously drawn symbols. #' @param icon_anchor Part of the icon placed closest to the anchor. @@ -1715,7 +1723,7 @@ add_symbol_layer <- function( if (!is.null(cluster_options)) { # Dispatch on source shape. See add_circle_layer() for notes. - if (inherits(source, c("sf", "sfc"))) { + if (inherits(source, c("sf", "sfc", "duckspatial_df"))) { map <- add_source( map, id = id, diff --git a/R/mapboxgl.R b/R/mapboxgl.R index 9f227610..8ad58cce 100644 --- a/R/mapboxgl.R +++ b/R/mapboxgl.R @@ -10,6 +10,7 @@ #' @param access_token Your Mapbox access token. #' @param bounds The bounding box to fit the map to. Accepts one of the following: #' * `sf` object; +#' * `duckspatial_df` object; #' * output of `st_bbox()`; #' * unnamed numeric vector of the form `c(xmin, ymin, xmax, ymax)`. #' @param width The width of the output htmlwidget. @@ -79,6 +80,9 @@ mapboxgl <- function( } if (inherits(bounds, "sf")) { bounds <- as.vector(sf::st_bbox(sf::st_transform(bounds, 4326))) + } else if (inherits(bounds, "duckspatial_df")) { + bounds <- ddbs_ensure_wgs84(bounds, "bounds") + bounds <- as.vector(duckspatial::ddbs_bbox(bounds)) } else if (inherits(bounds, "bbox")) { # Curiously, anyNA(bounds) or any(is.na(bounds)) would return FALSE even # if one of the four values is NA. diff --git a/R/maplibre.R b/R/maplibre.R index 7d41ea45..748fc7a2 100644 --- a/R/maplibre.R +++ b/R/maplibre.R @@ -8,6 +8,7 @@ #' @param projection The map projection to use (e.g., "mercator", "globe"). #' @param bounds The bounding box to fit the map to. Accepts one of the following: #' * `sf` object; +#' * `duckspatial_df` object; #' * output of `st_bbox()`; #' * unnamed numeric vector of the form `c(xmin, ymin, xmax, ymax)`. #' @param width The width of the output htmlwidget. @@ -62,6 +63,9 @@ maplibre <- function( } if (inherits(bounds, "sf")) { bounds <- as.vector(sf::st_bbox(sf::st_transform(bounds, 4326))) + } else if (inherits(bounds, "duckspatial_df")) { + bounds <- ddbs_ensure_wgs84(bounds, "bounds") + bounds <- as.vector(duckspatial::ddbs_bbox(bounds)) } else if (inherits(bounds, "bbox")) { # Curiously, anyNA(bounds) or any(is.na(bounds)) would return FALSE even # if one of the four values is NA. diff --git a/R/markers.R b/R/markers.R index a24d2fe4..93434ccb 100644 --- a/R/markers.R +++ b/R/markers.R @@ -1,7 +1,7 @@ #' Add markers to a Mapbox GL or Maplibre GL map #' #' @param map A map object created by the `mapboxgl` or `maplibre` functions. -#' @param data A length-2 numeric vector of coordinates, a list of length-2 numeric vectors, or an `sf` POINT object. +#' @param data A length-2 numeric vector of coordinates, a list of length-2 numeric vectors, an `sf` POINT object, or a `duckspatial_df` POINT object. #' @param color The color of the marker (default is "red"). #' @param rotation The rotation of the marker (default is 0). #' @param popup A column name for popups (if data is an `sf` object) or a string for a single popup (if data is a numeric vector or list of vectors). @@ -114,6 +114,25 @@ add_markers <- function(map, data, color = "red", rotation = 0, popup = NULL, ma options = options ) }) + } else if (inherits(data, "duckspatial_df") && + duckspatial::ddbs_geometry_type(data, FALSE)[1] == "POINT") { + lng <- duckspatial::ddbs_x(data, mode = "sf") + lat <- duckspatial::ddbs_y(data, mode = "sf") + properties <- duckspatial::ddbs_drop_geometry(data) |> dplyr::collect() + if (!is.null(marker_id) && !marker_id %in% colnames(properties)) { + stop("When providing an sf object, marker_id must be a column name in the object.") + } + markers <- lapply(seq_len(length(lng)), function(i) { + list( + id = if (!is.null(marker_id)) properties[[marker_id]][i] else paste0("marker_", i), + lng = lng[i], + lat = lat[i], + color = color, + rotation = rotation, + popup = if (!is.null(popup)) as.character(properties[i, popup]) else NULL, + options = options + ) + }) } else { stop("Data must be either a length-2 numeric vector, a list of length-2 numeric vectors, or an sf POINT object.") } diff --git a/R/shiny.R b/R/shiny.R index ba628395..56509fc4 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -668,7 +668,7 @@ set_popup <- function(map, layer_id = NULL, popup, layer = NULL, style = NULL) { #' #' @param map A map object created by the `mapboxgl` or `maplibre` function, or a proxy object. #' @param layer_id The ID of the layer to update. -#' @param source An sf object (which will be converted to a GeoJSON source). +#' @param source An sf object or a `duckspatial_df` object (both converted to a GeoJSON source). #' @param layer Deprecated. Use `layer_id` instead. #' #' @return The updated map object. @@ -697,6 +697,8 @@ set_source <- function(map, layer_id = NULL, source, layer = NULL) { source, crs = 4326 )) + } else if (inherits(source, "duckspatial_df")) { + source <- duckspatial::ddbs_as_geojson(ddbs_ensure_wgs84(source, "source")) } if ( diff --git a/R/sources.R b/R/sources.R index 8e7e5a56..50123b4a 100644 --- a/R/sources.R +++ b/R/sources.R @@ -2,7 +2,7 @@ #' #' @param map A map object created by the `mapboxgl` or `maplibre` function. #' @param id A unique ID for the source. -#' @param data An sf object or a URL pointing to a remote GeoJSON file. +#' @param data An sf object, a `duckspatial_df` object, or a URL pointing to a remote GeoJSON file. #' @param ... Additional arguments to be passed to the JavaScript addSource method. #' #' @return The modified map object with the new source added. @@ -17,10 +17,13 @@ add_source <- function(map, id, data, ...) { data <- sf::st_transform(data, crs = 4326) } geojson <- geojsonsf::sf_geojson(data) + } else if (inherits(data, "duckspatial_df")) { + data <- ddbs_ensure_wgs84(data, "data") + geojson <- duckspatial::ddbs_as_geojson(data) } else if (is.character(data) && grepl("^http", data)) { geojson <- data } else { - stop("Data must be an sf object or a URL to a remote GeoJSON file.") + stop("Data must be an sf object, a duckspatial_df, or a URL to a remote GeoJSON file.") } source <- list( diff --git a/R/utils.R b/R/utils.R index 8191e296..af726712 100644 --- a/R/utils.R +++ b/R/utils.R @@ -604,3 +604,17 @@ mapgl_invoke_method <- function(map, type, ...) { } return(map) } + +# Ensures a duckspatial_df is in EPSG:4326, transforming if needed. +# Stops if the object has no CRS set. +ddbs_ensure_wgs84 <- function(x, arg = deparse(substitute(x))) { + crs <- duckspatial::ddbs_crs(x) + if (is.na(crs)) { + stop(sprintf("`%s` doesn't have a CRS set.", arg)) + } + if (!crs$input %in% c("EPSG:4326", "WGS 84", "4326")) { + x <- duckspatial::ddbs_transform(x, "EPSG:4326") + } + x +} + diff --git a/R/views.R b/R/views.R index 8c87575a..5c1a6d9c 100644 --- a/R/views.R +++ b/R/views.R @@ -1,7 +1,7 @@ #' Fit the map to a bounding box #' #' @param map A map object created by the `mapboxgl` or `maplibre` function or a proxy object. -#' @param bbox A bounding box specified as a numeric vector of length 4 (minLng, minLat, maxLng, maxLat), or an sf object from which a bounding box will be calculated. +#' @param bbox A bounding box specified as a numeric vector of length 4 (minLng, minLat, maxLng, maxLat), an sf object, or a `duckspatial_df` object, from which a bounding box will be calculated. #' @param animate A logical value indicating whether to animate the transition to the new bounds. Defaults to FALSE. #' @param ... Additional named arguments for fitting the bounds. #' @@ -14,6 +14,9 @@ fit_bounds <- function(map, bbox, animate = FALSE, ...) { if (inherits(bbox, "sf")) { bbox <- as.vector(sf::st_bbox(sf::st_transform(bbox, 4326))) + } else if (inherits(bbox, "duckspatial_df")) { + bbox <- ddbs_ensure_wgs84(bbox, "bbox") + bbox <- as.vector(duckspatial::ddbs_bbox(bbox)) } if (inherits(map, "mapboxgl_proxy") || inherits(map, "maplibre_proxy")) { diff --git a/man/add_circle_layer.Rd b/man/add_circle_layer.Rd index 5b3cc00e..116f93e0 100644 --- a/man/add_circle_layer.Rd +++ b/man/add_circle_layer.Rd @@ -41,7 +41,7 @@ add_circle_layer( \item{id}{A unique ID for the layer.} -\item{source}{The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies \code{type} and \code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source), or a named list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/add_fill_extrusion_layer.Rd b/man/add_fill_extrusion_layer.Rd index 5902ce78..db01bb6c 100644 --- a/man/add_fill_extrusion_layer.Rd +++ b/man/add_fill_extrusion_layer.Rd @@ -40,7 +40,7 @@ add_fill_extrusion_layer( \item{id}{A unique ID for the layer.} -\item{source}{The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies \code{type} and \code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source), or a named list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/add_fill_layer.Rd b/man/add_fill_layer.Rd index 68cfaab9..0068d361 100644 --- a/man/add_fill_layer.Rd +++ b/man/add_fill_layer.Rd @@ -38,7 +38,7 @@ add_fill_layer( \item{id}{A unique ID for the layer.} -\item{source}{The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies \code{type} and \code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source), or a named list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/add_heatmap_layer.Rd b/man/add_heatmap_layer.Rd index f46c0117..d077c20f 100644 --- a/man/add_heatmap_layer.Rd +++ b/man/add_heatmap_layer.Rd @@ -27,7 +27,7 @@ add_heatmap_layer( \item{id}{A unique ID for the layer.} -\item{source}{The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies \code{type} and \code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source), or a named list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/add_layer.Rd b/man/add_layer.Rd index b6842e36..94ac2104 100644 --- a/man/add_layer.Rd +++ b/man/add_layer.Rd @@ -31,7 +31,7 @@ add_layer( \item{type}{The type of the layer (e.g., "fill", "line", "circle").} -\item{source}{The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies \code{type} and \code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source), or a named list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/add_line_layer.Rd b/man/add_line_layer.Rd index d154e047..75fc4686 100644 --- a/man/add_line_layer.Rd +++ b/man/add_line_layer.Rd @@ -52,9 +52,9 @@ add_line_layer( \item{id}{A unique ID for the layer.} -\item{source}{The ID of the source, alternatively an sf object (which will be -converted to a GeoJSON source) or a named list that specifies \code{type} and -\code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a +\code{duckspatial_df} object (both converted to a GeoJSON source), or a named +list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/add_markers.Rd b/man/add_markers.Rd index 07fd5fb0..2312f0b0 100644 --- a/man/add_markers.Rd +++ b/man/add_markers.Rd @@ -18,7 +18,7 @@ add_markers( \arguments{ \item{map}{A map object created by the \code{mapboxgl} or \code{maplibre} functions.} -\item{data}{A length-2 numeric vector of coordinates, a list of length-2 numeric vectors, or an \code{sf} POINT object.} +\item{data}{A length-2 numeric vector of coordinates, a list of length-2 numeric vectors, an \code{sf} POINT object, or a \code{duckspatial_df} POINT object.} \item{color}{The color of the marker (default is "red").} diff --git a/man/add_source.Rd b/man/add_source.Rd index bcc971b6..d6c0e8c3 100644 --- a/man/add_source.Rd +++ b/man/add_source.Rd @@ -11,7 +11,7 @@ add_source(map, id, data, ...) \item{id}{A unique ID for the source.} -\item{data}{An sf object or a URL pointing to a remote GeoJSON file.} +\item{data}{An sf object, a \code{duckspatial_df} object, or a URL pointing to a remote GeoJSON file.} \item{...}{Additional arguments to be passed to the JavaScript addSource method.} } diff --git a/man/add_symbol_layer.Rd b/man/add_symbol_layer.Rd index 7a33aa5d..b2f1192c 100644 --- a/man/add_symbol_layer.Rd +++ b/man/add_symbol_layer.Rd @@ -94,7 +94,7 @@ add_symbol_layer( \item{id}{A unique ID for the layer.} -\item{source}{The ID of the source, alternatively an sf object (which will be converted to a GeoJSON source) or a named list that specifies \code{type} and \code{url} for a remote source.} +\item{source}{The ID of the source, alternatively an sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source), or a named list that specifies \code{type} and \code{url} for a remote source.} \item{source_layer}{The source layer (for vector sources).} diff --git a/man/fit_bounds.Rd b/man/fit_bounds.Rd index 77a61b29..cb3bbace 100644 --- a/man/fit_bounds.Rd +++ b/man/fit_bounds.Rd @@ -9,7 +9,7 @@ fit_bounds(map, bbox, animate = FALSE, ...) \arguments{ \item{map}{A map object created by the \code{mapboxgl} or \code{maplibre} function or a proxy object.} -\item{bbox}{A bounding box specified as a numeric vector of length 4 (minLng, minLat, maxLng, maxLat), or an sf object from which a bounding box will be calculated.} +\item{bbox}{A bounding box specified as a numeric vector of length 4 (minLng, minLat, maxLng, maxLat), an sf object, or a \code{duckspatial_df} object, from which a bounding box will be calculated.} \item{animate}{A logical value indicating whether to animate the transition to the new bounds. Defaults to FALSE.} diff --git a/man/mapboxgl.Rd b/man/mapboxgl.Rd index a3095347..35f7180b 100644 --- a/man/mapboxgl.Rd +++ b/man/mapboxgl.Rd @@ -39,6 +39,7 @@ mapboxgl( \item{bounds}{The bounding box to fit the map to. Accepts one of the following: \itemize{ \item \code{sf} object; +\item \code{duckspatial_df} object; \item output of \code{st_bbox()}; \item unnamed numeric vector of the form \code{c(xmin, ymin, xmax, ymax)}. }} diff --git a/man/mapgl-package.Rd b/man/mapgl-package.Rd index 57ec637d..dce4f115 100644 --- a/man/mapgl-package.Rd +++ b/man/mapgl-package.Rd @@ -21,6 +21,11 @@ Useful links: \author{ \strong{Maintainer}: Kyle Walker \email{kyle@walker-data.com} +Authors: +\itemize{ + \item Kyle Walker \email{kyle@walker-data.com} +} + Other contributors: \itemize{ \item Egor Kotov \email{kotov.egor@gmail.com} [contributor] diff --git a/man/maplibre.Rd b/man/maplibre.Rd index 8339e285..5638970a 100644 --- a/man/maplibre.Rd +++ b/man/maplibre.Rd @@ -33,6 +33,7 @@ maplibre( \item{bounds}{The bounding box to fit the map to. Accepts one of the following: \itemize{ \item \code{sf} object; +\item \code{duckspatial_df} object; \item output of \code{st_bbox()}; \item unnamed numeric vector of the form \code{c(xmin, ymin, xmax, ymax)}. }} diff --git a/man/set_source.Rd b/man/set_source.Rd index e3ca929e..4a525baf 100644 --- a/man/set_source.Rd +++ b/man/set_source.Rd @@ -11,7 +11,7 @@ set_source(map, layer_id = NULL, source, layer = NULL) \item{layer_id}{The ID of the layer to update.} -\item{source}{An sf object (which will be converted to a GeoJSON source).} +\item{source}{An sf object or a \code{duckspatial_df} object (both converted to a GeoJSON source).} \item{layer}{Deprecated. Use \code{layer_id} instead.} } From 05569d303268982cf3d0c913e1186f25b14f1f22 Mon Sep 17 00:00:00 2001 From: Cidree Date: Sat, 27 Jun 2026 08:43:02 +0200 Subject: [PATCH 2/4] docs: Update news with duckspatial PR --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 9a20923c..8054f6a4 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,7 @@ # mapgl (development version) +* `duckspatial_df` objects from the `duckspatial` package are now accepted wherever `sf` objects are: `add_source()`, `add_layer()` and all specific layer functions, `fit_bounds()`, the `bounds` argument of `maplibre()` and `mapboxgl()`, `set_source()`, and `add_markers()`. CRS is validated and reprojected to EPSG:4326 automatically (#213). + * Fixed a bug where passing a geometry-only `sf` object (no non-geometry columns) as a layer source caused a serialization error. `geojsonsf::sf_geojson()` simplifies property-less sf objects into a vector of individual geometry strings rather than a FeatureCollection, which `htmlwidgets` cannot serialize. All internal calls now use `simplify = FALSE` to consistently return a FeatureCollection (#212). # mapgl 0.5.0 From 0e5d1745eaa8ffb6cb4121fdeb7b67225137404d Mon Sep 17 00:00:00 2001 From: Cidree Date: Sat, 26 Sep 2026 16:17:30 +0200 Subject: [PATCH 3/4] refactor: Move duckspatial to suggests --- DESCRIPTION | 4 ++-- R/layers.R | 3 +++ R/mapboxgl.R | 1 + R/maplibre.R | 1 + R/markers.R | 38 ++++++++++++++++++++------------------ R/shiny.R | 1 + R/sources.R | 1 + R/utils.R | 14 ++++++++++++++ R/views.R | 1 + 9 files changed, 44 insertions(+), 20 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 0ba1d87d..39f62311 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -32,10 +32,10 @@ Imports: viridisLite, png, jsonlite, - httpuv, - duckspatial + httpuv Suggests: chromote, + duckspatial (>= 1.2.0), knitr, mapboxapi, testthat (>= 3.0.0), diff --git a/R/layers.R b/R/layers.R index 14f82828..054123e8 100644 --- a/R/layers.R +++ b/R/layers.R @@ -103,6 +103,7 @@ add_layer <- function( generateId = TRUE ) } else if (inherits(source, "duckspatial_df")) { + check_duckspatial() source <- ddbs_ensure_wgs84(source, "source") geojson <- duckspatial::ddbs_as_geojson(source) source <- list( @@ -1131,6 +1132,7 @@ add_circle_layer <- function( # calls that incidentally pass `source_layer` alongside sf data # (silently ignored today) don't regress. if (inherits(source, c("sf", "sfc", "duckspatial_df"))) { + if (inherits(source, "duckspatial_df")) check_duckspatial() # Native live clustering: inject a clustered GeoJSON source. map <- add_source( map, @@ -1724,6 +1726,7 @@ add_symbol_layer <- function( if (!is.null(cluster_options)) { # Dispatch on source shape. See add_circle_layer() for notes. if (inherits(source, c("sf", "sfc", "duckspatial_df"))) { + if (inherits(source, "duckspatial_df")) check_duckspatial() map <- add_source( map, id = id, diff --git a/R/mapboxgl.R b/R/mapboxgl.R index 8ad58cce..e37a22d1 100644 --- a/R/mapboxgl.R +++ b/R/mapboxgl.R @@ -81,6 +81,7 @@ mapboxgl <- function( if (inherits(bounds, "sf")) { bounds <- as.vector(sf::st_bbox(sf::st_transform(bounds, 4326))) } else if (inherits(bounds, "duckspatial_df")) { + check_duckspatial() bounds <- ddbs_ensure_wgs84(bounds, "bounds") bounds <- as.vector(duckspatial::ddbs_bbox(bounds)) } else if (inherits(bounds, "bbox")) { diff --git a/R/maplibre.R b/R/maplibre.R index 748fc7a2..ddf2dd00 100644 --- a/R/maplibre.R +++ b/R/maplibre.R @@ -64,6 +64,7 @@ maplibre <- function( if (inherits(bounds, "sf")) { bounds <- as.vector(sf::st_bbox(sf::st_transform(bounds, 4326))) } else if (inherits(bounds, "duckspatial_df")) { + check_duckspatial() bounds <- ddbs_ensure_wgs84(bounds, "bounds") bounds <- as.vector(duckspatial::ddbs_bbox(bounds)) } else if (inherits(bounds, "bbox")) { diff --git a/R/markers.R b/R/markers.R index 93434ccb..1f814a03 100644 --- a/R/markers.R +++ b/R/markers.R @@ -114,25 +114,27 @@ add_markers <- function(map, data, color = "red", rotation = 0, popup = NULL, ma options = options ) }) - } else if (inherits(data, "duckspatial_df") && - duckspatial::ddbs_geometry_type(data, FALSE)[1] == "POINT") { - lng <- duckspatial::ddbs_x(data, mode = "sf") - lat <- duckspatial::ddbs_y(data, mode = "sf") - properties <- duckspatial::ddbs_drop_geometry(data) |> dplyr::collect() - if (!is.null(marker_id) && !marker_id %in% colnames(properties)) { - stop("When providing an sf object, marker_id must be a column name in the object.") + } else if (inherits(data, "duckspatial_df")) { + check_duckspatial() + if (duckspatial::ddbs_geometry_type(data, FALSE)[1] == "POINT") { + lng <- duckspatial::ddbs_x(data, mode = "sf") + lat <- duckspatial::ddbs_y(data, mode = "sf") + properties <- duckspatial::ddbs_drop_geometry(data) |> dplyr::collect() + if (!is.null(marker_id) && !marker_id %in% colnames(properties)) { + stop("When providing an sf object, marker_id must be a column name in the object.") + } + markers <- lapply(seq_len(length(lng)), function(i) { + list( + id = if (!is.null(marker_id)) properties[[marker_id]][i] else paste0("marker_", i), + lng = lng[i], + lat = lat[i], + color = color, + rotation = rotation, + popup = if (!is.null(popup)) as.character(properties[i, popup]) else NULL, + options = options + ) + }) } - markers <- lapply(seq_len(length(lng)), function(i) { - list( - id = if (!is.null(marker_id)) properties[[marker_id]][i] else paste0("marker_", i), - lng = lng[i], - lat = lat[i], - color = color, - rotation = rotation, - popup = if (!is.null(popup)) as.character(properties[i, popup]) else NULL, - options = options - ) - }) } else { stop("Data must be either a length-2 numeric vector, a list of length-2 numeric vectors, or an sf POINT object.") } diff --git a/R/shiny.R b/R/shiny.R index 2a6c7812..b465dd42 100644 --- a/R/shiny.R +++ b/R/shiny.R @@ -698,6 +698,7 @@ set_source <- function(map, layer_id = NULL, source, layer = NULL) { crs = 4326 ), simplify = FALSE) } else if (inherits(source, "duckspatial_df")) { + check_duckspatial() source <- duckspatial::ddbs_as_geojson(ddbs_ensure_wgs84(source, "source")) } diff --git a/R/sources.R b/R/sources.R index efe2858f..4beb8bcb 100644 --- a/R/sources.R +++ b/R/sources.R @@ -18,6 +18,7 @@ add_source <- function(map, id, data, ...) { } geojson <- geojsonsf::sf_geojson(data, simplify = FALSE) } else if (inherits(data, "duckspatial_df")) { + check_duckspatial() data <- ddbs_ensure_wgs84(data, "data") geojson <- duckspatial::ddbs_as_geojson(data) } else if (is.character(data) && grepl("^http", data)) { diff --git a/R/utils.R b/R/utils.R index af726712..981530f5 100644 --- a/R/utils.R +++ b/R/utils.R @@ -618,3 +618,17 @@ ddbs_ensure_wgs84 <- function(x, arg = deparse(substitute(x))) { x } +# Checks if duckspatial is installed when needed, and triggers its installation +check_duckspatial <- function( + min_version = "1.2.0", + call = rlang::caller_env() +) { + + rlang::check_installed( + "duckspatial", + version = min_version, + call = call + ) + + invisible(TRUE) +} diff --git a/R/views.R b/R/views.R index 5c1a6d9c..da0165a8 100644 --- a/R/views.R +++ b/R/views.R @@ -15,6 +15,7 @@ fit_bounds <- function(map, bbox, animate = FALSE, ...) { if (inherits(bbox, "sf")) { bbox <- as.vector(sf::st_bbox(sf::st_transform(bbox, 4326))) } else if (inherits(bbox, "duckspatial_df")) { + check_duckspatial() bbox <- ddbs_ensure_wgs84(bbox, "bbox") bbox <- as.vector(duckspatial::ddbs_bbox(bbox)) } From 3e575906df464fcc830eb5867d83d3a183d69d7c Mon Sep 17 00:00:00 2001 From: Cidree Date: Sat, 26 Sep 2026 16:34:18 +0200 Subject: [PATCH 4/4] fix: Add missing bracket --- R/utils.R | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/R/utils.R b/R/utils.R index b172d644..e781d822 100644 --- a/R/utils.R +++ b/R/utils.R @@ -631,6 +631,10 @@ check_duckspatial <- function( ) invisible(TRUE) + +} + + # Shared htmlDependency for the layers control (JS IControl + stylesheet), # used by mapboxgl(), maplibre(), and both compare widgets layers_control_dependency <- function() {