Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@ Roxygen: list(markdown = TRUE)
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
Suggests:
chromote,
duckspatial (>= 1.2.0),
knitr,
mapboxapi,
testthat (>= 3.0.0),
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

* 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).

* `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).

# mapgl 0.5.0

* Tooltips and popups now accept `{brace}` templates package-wide. Any layer's `tooltip`/`popup` (and `set_tooltip()`/`set_popup()`) can take a glue-style template such as `"{name}: {population}"`, in addition to the existing column name and `concat()`/`number_format()` expression forms; substituted values are HTML-escaped.
Expand Down
33 changes: 22 additions & 11 deletions R/layers.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -102,6 +102,15 @@ add_layer <- function(
data = geojson,
generateId = TRUE
)
} else if (inherits(source, "duckspatial_df")) {
check_duckspatial()
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)
Expand Down Expand Up @@ -206,7 +215,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.
Expand Down Expand Up @@ -335,9 +344,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".
Expand Down Expand Up @@ -518,7 +527,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.
Expand Down Expand Up @@ -623,7 +632,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.
Expand Down Expand Up @@ -958,7 +967,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.
Expand Down Expand Up @@ -1122,7 +1131,8 @@ 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"))) {
if (inherits(source, "duckspatial_df")) check_duckspatial()
# Native live clustering: inject a clustered GeoJSON source.
map <- add_source(
map,
Expand Down Expand Up @@ -1387,7 +1397,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.
Expand Down Expand Up @@ -1715,7 +1725,8 @@ 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"))) {
if (inherits(source, "duckspatial_df")) check_duckspatial()
map <- add_source(
map,
id = id,
Expand Down
5 changes: 5 additions & 0 deletions R/mapboxgl.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -79,6 +80,10 @@ 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")) {
# Curiously, anyNA(bounds) or any(is.na(bounds)) would return FALSE even
# if one of the four values is NA.
Expand Down
5 changes: 5 additions & 0 deletions R/maplibre.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -62,6 +63,10 @@ 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")) {
# Curiously, anyNA(bounds) or any(is.na(bounds)) would return FALSE even
# if one of the four values is NA.
Expand Down
23 changes: 22 additions & 1 deletion R/markers.R
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -114,6 +114,27 @@ add_markers <- function(map, data, color = "red", rotation = 0, popup = NULL, ma
options = options
)
})
} 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
)
})
}
} else {
stop("Data must be either a length-2 numeric vector, a list of length-2 numeric vectors, or an sf POINT object.")
}
Expand Down
5 changes: 4 additions & 1 deletion R/shiny.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -697,6 +697,9 @@ set_source <- function(map, layer_id = NULL, source, layer = NULL) {
source,
crs = 4326
), simplify = FALSE)
} else if (inherits(source, "duckspatial_df")) {
check_duckspatial()
source <- duckspatial::ddbs_as_geojson(ddbs_ensure_wgs84(source, "source"))
}

if (
Expand Down
8 changes: 6 additions & 2 deletions R/sources.R
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -17,10 +17,14 @@ add_source <- function(map, id, data, ...) {
data <- sf::st_transform(data, crs = 4326)
}
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)) {
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(
Expand Down
30 changes: 30 additions & 0 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,36 @@ 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
}

# 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)

}


# Shared htmlDependency for the layers control (JS IControl + stylesheet),
# used by mapboxgl(), maplibre(), and both compare widgets
layers_control_dependency <- function() {
Expand Down
6 changes: 5 additions & 1 deletion R/views.R
Original file line number Diff line number Diff line change
@@ -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.
#'
Expand All @@ -14,6 +14,10 @@ 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))
}

if (inherits(map, "mapboxgl_proxy") || inherits(map, "maplibre_proxy")) {
Expand Down
2 changes: 1 addition & 1 deletion man/add_circle_layer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/add_fill_extrusion_layer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/add_fill_layer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/add_heatmap_layer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/add_layer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions man/add_line_layer.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion man/add_markers.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading