diff --git a/DESCRIPTION b/DESCRIPTION index 48351c0..d109297 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -31,11 +31,10 @@ License: MIT + file LICENSE Encoding: UTF-8 RoxygenNote: 7.3.2 Imports: - geojsonsf, htmltools, - jsonify, leaflet, sf, + yyjsonr, grDevices Suggests: colourvalues, diff --git a/NAMESPACE b/NAMESPACE index 3af5d6b..75bad7e 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -26,6 +26,7 @@ S3method(makePopup,shiny.tag) export(addGlPoints) export(addGlPolygons) export(addGlPolylines) +export(clearGlGroup) export(clearGlLayers) export(leafglOutput) export(makeColorMatrix) @@ -37,5 +38,6 @@ export(renderLeafgl) importFrom(htmltools,htmlDependencies) importFrom(htmltools,tagList) importFrom(htmltools,tags) +importFrom(leaflet,evalFormula) importFrom(leaflet,leafletOutput) importFrom(leaflet,renderLeaflet) diff --git a/NEWS b/NEWS index d80b363..a73b3f3 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,32 @@ + leafgl development-version + + * Added some @details for Shiny click and mouseover events and their corresponding input. #77 + * Use `@inheritParams leaflet::**` for identical function arguments + +miscellaneous + + * update upstream javascript dependency to 3.3.0. #49 + Note: If you previously used the workaround `L.glify.Shapes.instances.splice(0, 1)`, please remove it with this new version. + * unified / simplified the dependency functions/calls + + leafgl 0.2.2 (2024-11-13) + * Switched from `jsonify` and `geojsonsf` to `yyjsonr` + * New method *clearGlGroup* removes a group from leaflet and the Leaflet.Glify instances. + * The JavaScript methods of the `removeGl**` functions was rewritten to correctly remove an element identified by `layerId` + * `clearGlLayers` now correctly removes all Leaflet.Glify instances + * When showing/hiding Leaflet.Glify layers, they are set to active = TRUE/FALSE to make mouseevents work again. #48 #50 + bug fixes - * src version now works also in shiny. #71 + * Increase precision of points, lines and shapes by translating them closer to the Pixel Origin. Thanks @RayLarone #93 + * src version now works also in shiny. #71 + * added `popupOptions` and `labelOptions`. #83 + * added `stroke` (default=TRUE) in `addGlPolygons` and `addGlPolygonsSrc` for drawing borders. #3 #68 + * Labels work similar to `leaflet`. `leafgl` accepts a single string, a vector of strings or a formula. #78 + * The `...` arguments are now passed to all methods in the underlying library. This allows us to set + additional arguments like `fragmentShaderSource`, `sensitivity` or `sensitivityHover`. #81 documentation etc diff --git a/NEWS.md b/NEWS.md index 193e09d..f83a0e6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,8 +1,31 @@ +# leafgl dev-version + +- Added some @details for Shiny click and mouseover events and their corresponding input. [#77](https://github.com/r-spatial/leafgl/issues/77) +- Use `@inheritParams leaflet::**` for identical function arguments +- unified / simplified the dependency functions/calls + +#### 🍬 miscellaneous + +- update upstream javascript dependency to 3.3.0. [#49](https://github.com/r-spatial/leafgl/issues/49) + ⚠️If you previously used the workaround `L.glify.Shapes.instances.splice(0, 1)`, please remove it with this new version. + + # leafgl 0.2.2 (2024-11-13) +- Switched from `jsonify` and `geojsonsf` to `yyjsonr` +- New method `clearGlGroup` removes a group from leaflet and the Leaflet.Glify instances. +- The JavaScript methods of the `removeGl**` functions was rewritten to correctly remove an element identified by `layerId` +- `clearGlLayers` now correctly removes all Leaflet.Glify instances +- When showing/hiding Leaflet.Glify layers, they are set to active = TRUE/FALSE to make mouseevents work again. [#48](https://github.com/r-spatial/leafgl/issues/48) [#50](https://github.com/r-spatial/leafgl/issues/50) + #### 🐛 bug fixes - * src version now works also in shiny. #71 +- Increase precision of points, lines and shapes by translating them closer to the Pixel Origin. Thanks @RayLarone [#93](https://github.com/r-spatial/leafgl/issues/93) +- src version now works also in shiny. [#71](https://github.com/r-spatial/leafgl/issues/71) +- added `popupOptions` and `labelOptions`. [#83](https://github.com/r-spatial/leafgl/issues/83) +- added `stroke` (default=TRUE) in `addGlPolygons` and `addGlPolygonsSrc` for drawing borders. [#3](https://github.com/r-spatial/leafgl/issues/3) [#68](https://github.com/r-spatial/leafgl/issues/68) +- Labels work similar to `leaflet`. `leafgl` accepts a single string, a vector of strings or a formula. [#78](https://github.com/r-spatial/leafgl/issues/78) +- The `...` arguments are now passed to all methods in the underlying library. This allows us to set additional arguments like `fragmentShaderSource`, `sensitivity` or `sensitivityHover`. [#81](https://github.com/r-spatial/leafgl/issues/81) #### 💬 documentation etc @@ -16,7 +39,7 @@ new features: - * all methods can now have labels/tooltips. Currently only lines and polygons support passing of a column name, points need a predefined label vector. +- all methods can now have labels/tooltips. Currently only lines and polygons support passing of a column name, points need a predefined label vector. miscallaneous: @@ -26,7 +49,7 @@ miscallaneous: miscallaneous: - * update upstream javascript dependency to 3.2.0 +- update upstream javascript dependency to 3.2.0 ## leafgl 0.1.2 diff --git a/R/glify-helpers.R b/R/glify-helpers.R index 5e33ad0..376fef8 100644 --- a/R/glify-helpers.R +++ b/R/glify-helpers.R @@ -1,31 +1,16 @@ -# helpers -glifyDependencies = function() { +# dependencies +glifyDependencies = function(src = FALSE) { + src <- ifelse(src, "Src", "") list( htmltools::htmlDependency( "Leaflet.glify", '3.2.0', system.file("htmlwidgets/Leaflet.glify", package = "leafgl"), script = c( - "addGlifyPoints.js" - , "addGlifyPolygons.js" - , "addGlifyPolylines.js" - , "glify-browser.js" - ) - ) - ) -} - -# helpers -glifyDependenciesSrc = function() { - list( - htmltools::htmlDependency( - "Leaflet.glifySrc", - '3.2.0', - system.file("htmlwidgets/Leaflet.glify", package = "leafgl"), - script = c( - "addGlifyPointsSrc.js" - , "addGlifyPolygonsSrc.js" - , "addGlifyPolylinesSrc.js" + "GlifyUtils.js" + , paste0("addGlifyPoints", src, ".js") + , paste0("addGlifyPolygons", src, ".js") + , paste0("addGlifyPolylines", src, ".js") , "glify-browser.js" ) ) @@ -56,38 +41,16 @@ glifyDataAttachmentSrc = function(fl_data, group, async = FALSE) { } } -glifyColorAttachmentSrc = function(fl_color, group) { - data_dir <- dirname(fl_color) - data_file <- basename(fl_color) - list( - htmltools::htmlDependency( - name = paste0(group, "col"), - version = 1, - src = c(file = data_dir), - script = list(data_file) - ) - ) -} - -glifyPopupAttachmentSrc = function(fl_popup, group) { - data_dir <- dirname(fl_popup) - data_file <- basename(fl_popup) - list( - htmltools::htmlDependency( - name = paste0(group, "pop"), - version = 1, - src = c(file = data_dir), - script = list(data_file) - ) - ) -} - -glifyRadiusAttachmentSrc = function(fl_radius, group) { - data_dir <- dirname(fl_radius) - data_file <- basename(fl_radius) +glifyAttachmentSrc <- function(fl, group, type) { + valid_types <- c("col", "pop", "lab", "rad") + if (!type %in% valid_types) { + stop("Invalid type. Valid types are: col, pop, lab, rad.") + } + data_dir <- dirname(fl) + data_file <- basename(fl) list( htmltools::htmlDependency( - name = paste0(group, "rad"), + name = paste0(group, type), version = 1, src = c(file = data_dir), script = list(data_file) @@ -95,62 +58,70 @@ glifyRadiusAttachmentSrc = function(fl_radius, group) { ) } -glifyDataAttachment = function(fl_data, group) { - data_dir <- dirname(fl_data) - data_file <- basename(fl_data) - list( - htmltools::htmlDependency( - name = paste0(group, "dt"), - version = 1, - src = c(file = data_dir), - attachment = list(data_file) - ) - ) -} - -glifyColorAttachment = function(fl_color, group) { - data_dir <- dirname(fl_color) - data_file <- basename(fl_color) - list( - htmltools::htmlDependency( - name = paste0(group, "cl"), - version = 1, - src = c(file = data_dir), - attachment = list(data_file) - ) - ) +# helpers +yyson_json_str <- function(x, ...) { + dt <- yyjsonr::write_json_str(x, ...) + class(dt) <- "json" + dt } - -glifyPopupAttachment = function(fl_popup, group) { - data_dir <- dirname(fl_popup) - data_file <- basename(fl_popup) - list( - htmltools::htmlDependency( - name = paste0(group, "pop"), - version = 1, - src = c(file = data_dir), - attachment = list(data_file) - ) - ) +yyson_geojson_str <- function(x, ...) { + dt <- yyjsonr::write_geojson_str(x, ...) + class(dt) <- "json" + dt } - - -# helpers -glifyDependenciesFl = function() { - list( - htmltools::htmlDependency( - "Leaflet.glify", - '2.2.0', - system.file("htmlwidgets/Leaflet.glify", package = "leafgl"), - script = c( - "addGlifyPoints.js" - , "addGlifyPolygonsFl.js" - , "addGlifyPolylines.js" - , "glify.js" - ) - ) - ) -} +## Not used ########## +# glifyDependenciesFl = function() { +# list( +# htmltools::htmlDependency( +# "Leaflet.glify", +# '2.2.0', +# system.file("htmlwidgets/Leaflet.glify", package = "leafgl"), +# script = c( +# "GlifyUtils.js" +# , "addGlifyPoints.js" +# , "addGlifyPolygonsFl.js" +# , "addGlifyPolylines.js" +# , "glify-browser.js" +# ) +# ) +# ) +# } +# glifyDataAttachment = function(fl_data, group) { +# data_dir <- dirname(fl_data) +# data_file <- basename(fl_data) +# list( +# htmltools::htmlDependency( +# name = paste0(group, "dt"), +# version = 1, +# src = c(file = data_dir), +# attachment = list(data_file) +# ) +# ) +# } +# glifyColorAttachment = function(fl_color, group) { +# data_dir <- dirname(fl_color) +# data_file <- basename(fl_color) +# list( +# htmltools::htmlDependency( +# name = paste0(group, "cl"), +# version = 1, +# src = c(file = data_dir), +# attachment = list(data_file) +# ) +# ) +# } +# glifyPopupAttachment = function(fl_popup, group) { +# data_dir <- dirname(fl_popup) +# data_file <- basename(fl_popup) +# list( +# htmltools::htmlDependency( +# name = paste0(group, "pop"), +# version = 1, +# src = c(file = data_dir), +# attachment = list(data_file) +# ) +# ) +# } diff --git a/R/glify-lines.R b/R/glify-lines.R index 2daf92d..5d6289c 100644 --- a/R/glify-lines.R +++ b/R/glify-lines.R @@ -1,25 +1,16 @@ -#' add polylines to a leaflet map using Leaflet.glify -#' -#' @details -#' MULTILINESTRINGs are currently not supported! Make sure you cast your data -#' to LINETSRING first (e.g. using \code{sf::st_cast(data, "LINESTRING")}. -#' #' @examples -#' if (interactive()) { #' library(leaflet) #' library(leafgl) #' library(sf) #' #' storms = st_as_sf(atlStorms2005) -#' #' cols = heat.colors(nrow(storms)) #' #' leaflet() %>% #' addProviderTiles(provider = providers$CartoDB.Positron) %>% #' addGlPolylines(data = storms, color = cols, popup = TRUE, opacity = 1) -#' } #' -#' @describeIn addGlPoints add polylines to a leaflet map using Leaflet.glify +#' @describeIn addGlPoints Add Lines to a leaflet map using Leaflet.glify #' @aliases addGlPolylines #' @export addGlPolylines addGlPolylines = function(map, @@ -33,8 +24,28 @@ addGlPolylines = function(map, layerId = NULL, src = FALSE, pane = "overlayPane", + popupOptions = NULL, + labelOptions = NULL, ...) { + # check data ########## + if (missing(labelOptions)) labelOptions <- labelOptions() + if (missing(popupOptions)) popupOptions <- popupOptions() + + if (is.null(group)) group = deparse(substitute(data)) + if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) + stopifnot(inherits(sf::st_geometry(data), c("sfc_LINESTRING", "sfc_MULTILINESTRING"))) + if (inherits(sf::st_geometry(data), "sfc_MULTILINESTRING")) + stop("Can only handle LINESTRINGs, please cast your MULTILINESTRING to LINESTRING using sf::st_cast", + call. = FALSE) + + if (!is.null(layerId) && inherits(layerId, "formula")) + layerId <- evalFormula(layerId, data) + + ## currently leaflet.glify only supports single (fill)opacity! + opacity = opacity[1] + + # call SRC function ############## if (isTRUE(src)) { m = addGlPolylinesSrc( map = map @@ -43,47 +54,40 @@ addGlPolylines = function(map, , opacity = opacity , group = group , popup = popup + , label = label , weight = weight , layerId = layerId , pane = pane + , popupOptions = popupOptions + , labelOptions = labelOptions , ... ) return(m) } - ## currently leaflet.glify only supports single (fill)opacity! - opacity = opacity[1] - - if (is.null(group)) group = deparse(substitute(data)) - if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) - stopifnot(inherits(sf::st_geometry(data), c("sfc_LINESTRING", "sfc_MULTILINESTRING"))) - if (inherits(sf::st_geometry(data), "sfc_MULTILINESTRING")) - stop("Can only handle LINESTRINGs, please cast your MULTILINESTRING to LINESTRING using sf::st_cast", - call. = FALSE) - + # get Bounds and ... ################# + dotopts = list(...) bounds = as.numeric(sf::st_bbox(data)) - # color - args <- list(...) + # color ######## palette = "viridis" - if ("palette" %in% names(args)) { - palette <- args$palette - args$palette = NULL + if ("palette" %in% names(dotopts)) { + palette <- dotopts$palette + dotopts$palette = NULL } color <- makeColorMatrix(color, data, palette = palette) if (ncol(color) != 3) stop("only 3 column color matrix supported so far") color = as.data.frame(color, stringsAsFactors = FALSE) colnames(color) = c("r", "g", "b") + cols = yyson_json_str(color, digits = 3) - cols = jsonify::to_json(color, digits = 3) - - # popup + # label / popup ######## + labels <- leaflet::evalFormula(label, data) if (is.null(popup)) { - # geom = sf::st_transform(sf::st_geometry(data), crs = 4326) geom = sf::st_geometry(data) data = sf::st_sf(id = 1:length(geom), geometry = geom) } else if (isTRUE(popup)) { - data = data[, popup] + ## Don't do anything. Pass all columns to JS } else { htmldeps <- htmltools::htmlDependencies(popup) if (length(htmldeps) != 0) { @@ -92,20 +96,19 @@ addGlPolylines = function(map, htmldeps ) } - popup = makePopup(popup, data) - popup = jsonify::to_json(popup) + popup = yyson_json_str(makePopup(popup, data)) geom = sf::st_geometry(data) data = sf::st_sf(id = 1:length(geom), geometry = geom) } - # data - if (length(args) == 0) { + # data ######## + if (length(dotopts) == 0) { geojsonsf_args = NULL } else { geojsonsf_args = try( match.arg( - names(args) - , names(as.list(args(geojsonsf::sf_geojson))) + names(dotopts) + , names(as.list(args(yyjsonr::opts_write_json))) , several.ok = TRUE ) , silent = TRUE @@ -113,17 +116,12 @@ addGlPolylines = function(map, if (inherits(geojsonsf_args, "try-error")) geojsonsf_args = NULL if (identical(geojsonsf_args, "sf")) geojsonsf_args = NULL } - data = do.call(geojsonsf::sf_geojson, c(list(data), args[geojsonsf_args])) - # data = geojsonsf::sf_geojson(data, ...) + data = do.call(yyson_geojson_str, c(list(data), dotopts[geojsonsf_args])) # dependencies - map$dependencies = c( - map$dependencies - , glifyDependencies() - ) - - # weight is about double the weight of svg, so / 2 + map$dependencies = c(map$dependencies, glifyDependencies()) + # invoke leaflet method and zoom to bounds ######## map = leaflet::invokeMethod( map , leaflet::getMapData(map) @@ -131,12 +129,15 @@ addGlPolylines = function(map, , data , cols , popup - , label + , labels , opacity , group , weight , layerId + , dotopts , pane + , popupOptions + , labelOptions ) leaflet::expandLimits( @@ -154,85 +155,91 @@ addGlPolylinesSrc = function(map, opacity = 0.8, group = "glpolygons", popup = NULL, + label = NULL, weight = 1, layerId = NULL, pane = "overlayPane", + popupOptions = NULL, + labelOptions = NULL, ...) { - if (is.null(group)) group = deparse(substitute(data)) - if (is.null(layerId)) layerId = paste0(group, "-lns") - if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) - stopifnot(inherits(sf::st_geometry(data), c("sfc_LINESTRING", "sfc_MULTILINESTRING"))) - if (inherits(sf::st_geometry(data), "sfc_MULTILINESTRING")) - stop("Can only handle LINESTRINGs, please cast your MULTILINESTRING ", - "to LINESTRING using e.g. sf::st_cast") - + # get Bounds and ... ################# + dotopts = list(...) bounds = as.numeric(sf::st_bbox(data)) - # temp directories + # temp directories ############ dir_data = tempfile(pattern = "glify_polylines_dat") dir.create(dir_data) dir_color = tempfile(pattern = "glify_polylines_col") dir.create(dir_color) - dir_popup = tempfile(pattern = "glify_polylines_pop") - dir.create(dir_popup) dir_weight = tempfile(pattern = "glify_polylines_wgt") dir.create(dir_weight) + dir_popup = tempfile(pattern = "glify_polylines_pop") + dir.create(dir_popup) + dir_labels = tempfile(pattern = "glify_polylines_labl") + dir.create(dir_labels) - # data + # data ############ data_orig <- data geom = sf::st_geometry(data) data = sf::st_sf(id = 1:length(geom), geometry = geom) - ell_args <- list(...) - fl_data = paste0(dir_data, "/", layerId, "_data.js") - pre = paste0('var data = data || {}; data["', layerId, '"] = ') + fl_data = paste0(dir_data, "/", group, "_data.js") + pre = paste0('var data = data || {}; data["', group, '"] = ') writeLines(pre, fl_data) jsonify_args = try( match.arg( - names(ell_args) - , names(as.list(args(geojsonsf::sf_geojson))) + names(dotopts) + , names(as.list(args(yyjsonr::opts_write_json))) , several.ok = TRUE ) , silent = TRUE ) if (inherits(jsonify_args, "try-error")) jsonify_args = NULL if (identical(jsonify_args, "sf")) jsonify_args = NULL - cat('[', do.call(geojsonsf::sf_geojson, c(list(data), ell_args[jsonify_args])), '];', + cat('[', do.call(yyson_geojson_str, c(list(data), dotopts[jsonify_args])), '];', file = fl_data, sep = "", append = TRUE) map$dependencies = c( map$dependencies, - glifyDependenciesSrc(), - glifyDataAttachmentSrc(fl_data, layerId) + glifyDependencies(TRUE), + glifyDataAttachmentSrc(fl_data, group) ) - # color + # color ############ palette = "viridis" - if ("palette" %in% names(ell_args)) { - palette <- ell_args$palette + if ("palette" %in% names(dotopts)) { + palette <- dotopts$palette + dotopts$palette = NULL } color <- makeColorMatrix(color, data_orig, palette = palette) if (ncol(color) != 3) stop("only 3 column color matrix supported so far") color = as.data.frame(color, stringsAsFactors = FALSE) colnames(color) = c("r", "g", "b") - if (nrow(color) > 1) { - fl_color = paste0(dir_color, "/", layerId, "_color.js") - pre = paste0('var col = col || {}; col["', layerId, '"] = ') + fl_color = paste0(dir_color, "/", group, "_color.js") + pre = paste0('var col = col || {}; col["', group, '"] = ') writeLines(pre, fl_color) - cat('[', jsonify::to_json(color), '];', + cat('[', yyson_json_str(color, digits = 3), '];', file = fl_color, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyColorAttachmentSrc(fl_color, layerId) - ) - + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_color, group, "col")) color = NULL } - # popup + # labels ############ + if (!is.null(label)) { + fl_label = paste0(dir_labels, "/", group, "_label.js") + pre = paste0('var labs = labs || {}; labs["', group, '"] = ') + writeLines(pre, fl_label) + cat('[', yyson_json_str(leaflet::evalFormula(label, data_orig)), '];', + file = fl_label, append = TRUE) + + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_label, group, "lab")) + label = NULL + } + + # popup ############ if (!is.null(popup)) { htmldeps <- htmltools::htmlDependencies(popup) if (length(htmldeps) != 0) { @@ -241,46 +248,43 @@ addGlPolylinesSrc = function(map, htmldeps ) } - popup = makePopup(popup, data_orig) - fl_popup = paste0(dir_popup, "/", layerId, "_popup.js") - pre = paste0('var popup = popup || {}; popup["', layerId, '"] = ') + fl_popup = paste0(dir_popup, "/", group, "_popup.js") + pre = paste0('var pops = pops || {}; pops["', group, '"] = ') writeLines(pre, fl_popup) - cat('[', jsonify::to_json(popup), '];', + cat('[', yyson_json_str(makePopup(popup, data_orig)), '];', file = fl_popup, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyPopupAttachmentSrc(fl_popup, layerId) - ) - + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_popup, group, "pop")) + popup = NULL } - # weight + # weight ############ if (length(unique(weight)) > 1) { - fl_weight = paste0(dir_weight, "/", layerId, "_weight.js") - pre = paste0('var wgt = wgt || {}; wgt["', layerId, '"] = ') + fl_weight = paste0(dir_weight, "/", group, "_weight.js") + pre = paste0('var wgt = wgt || {}; wgt["', group, '"] = ') writeLines(pre, fl_weight) - cat('[', jsonify::to_json(weight), '];', + cat('[', yyson_json_str(weight), '];', file = fl_weight, append = TRUE) map$dependencies = c( - map$dependencies, - glifyRadiusAttachmentSrc(fl_weight, layerId) - ) - + map$dependencies, glifyAttachmentSrc(fl_weight, group, "rad")) weight = NULL } + # invoke method ########### map = leaflet::invokeMethod( map , leaflet::getMapData(map) , 'addGlifyPolylinesSrc' , color - , weight , opacity , group + , weight , layerId + , dotopts , pane + , popupOptions + , labelOptions ) leaflet::expandLimits( @@ -288,7 +292,5 @@ addGlPolylinesSrc = function(map, c(bounds[2], bounds[4]), c(bounds[1], bounds[3]) ) - } - diff --git a/R/glify-points.R b/R/glify-points.R index 78aaa44..c51c047 100644 --- a/R/glify-points.R +++ b/R/glify-points.R @@ -1,60 +1,90 @@ -#' add points to a leaflet map using Leaflet.glify +#' @title Add Data to a leaflet map using Leaflet.glify #' #' @description -#' Leaflet.glify is a web gl renderer plugin for leaflet. See +#' Leaflet.glify is a WebGL renderer plugin for leaflet. See #' \url{https://github.com/robertleeplummerjr/Leaflet.glify} for details #' and documentation. #' -#' @param map a leaflet map to add points/polygons to. -#' @param data sf/sp point/polygon data to add to the map. +#' @inheritParams leaflet::addPolylines +#' @param data sf/sp point/polygon/line data to add to the map. #' @param color Object representing the color. Can be of class integer, character with #' color names, HEX codes or random characters, factor, matrix, data.frame, list, json or formula. #' See the examples or \link{makeColorMatrix} for more information. -#' @param fillColor fill color. #' @param opacity feature opacity. Numeric between 0 and 1. #' Note: expect funny results if you set this to < 1. -#' @param fillOpacity fill opacity. #' @param radius point size in pixels. -#' @param group a group name for the feature layer. #' @param popup Object representing the popup. Can be of type character with column names, #' formula, logical, data.frame or matrix, Spatial, list or JSON. If the length does not -#' match the number of rows in the dataset, the popup vector is repeated to match the dimension. -#' @param label either a column name (currently only supported for polygons and polylines) -#' or a character vector to be used as label. -#' @param layerId the layer id -#' @param weight line width/thicknes in pixels for \code{addGlPolylines}. +#' match the number of rows in the data, the popup vector is repeated to match the dimension. +#' @param weight line width/thickness in pixels for \code{addGlPolylines}. #' @param src whether to pass data to the widget via file attachments. #' @param pane A string which defines the pane of the layer. The default is \code{"overlayPane"}. -#' @param ... Used to pass additional named arguments to \code{\link[jsonify]{to_json}} -#' & to pass additional arguments to the underlying JavaScript functions. Typical -#' use-cases include setting 'digits' to round the point coordinates or to pass -#' a different 'fragmentShaderSource' to control the shape of the points. Use -#' 'point' (default) to render circles with a thin black outline, -#' 'simpleCircle' for circles without outline or -#' 'sqaure' for squares (without outline). +#' @param ... Used to pass additional named arguments to \code{\link[yyjsonr]{write_json_str}} or +#' \code{\link[yyjsonr]{write_geojson_str}} & to pass additional arguments to the +#' underlying JavaScript functions. Typical use-cases include setting \code{'digits'} to +#' round the point coordinates or to pass a different \code{'fragmentShaderSource'} to +#' control the shape of the points. Use +#' \itemize{ +#' \item{\code{'point'} (default) to render circles with a thin black outline} +#' \item{\code{'simpleCircle'} for circles without outline} +#' \item{\code{'square'} for squares without outline} +#' } +#' Additional arguments could be \code{'sensitivity'}, \code{'sensitivityHover'} or +#' \code{'vertexShaderSource'}. See a full list at the +#' \href{https://github.com/robertleeplummerjr/Leaflet.glify}{Leaflet.glify} +#' repository. #' -#' @describeIn addGlPoints add points to a leaflet map using Leaflet.glify -#' @examples -#' if (interactive()) { +#' +#' @note +#' MULTILINESTRINGs and MULTIPOLYGONs are currently not supported! +#' Make sure you cast your data to LINESTRING or POLYGON first using: +#' \itemize{ +#' \item{\code{sf::st_cast(data, "LINESTRING")}} +#' \item{\code{sf::st_cast(data, "POLYGON")}} +#' } +#' +#' @section Shiny Inputs: +#' The objects created with \code{leafgl} send input values to Shiny as the +#' user interacts with them. These events follow the pattern +#' \code{input$MAPID_glify_EVENTNAME}. +#' The following events are available: +#' +#' \itemize{ +#' \item \strong{Click Events:} +#' \code{input$MAPID_glify_click} +#' \item \strong{Mouseover Events:} +#' \code{input$MAPID_glify_mouseover} +#' \item \strong{Mouseout Events:} +#' \code{input$MAPID_glify_mouseout} +#' } +#' +#' +#' Each event returns a list containing: +#' \itemize{ +#' \item \code{lat}: Latitude of the object or mouse cursor +#' \item \code{lng}: Longitude of the object or mouse cursor +#' \item \code{id}: The layerId, if any +#' \item \code{group}: The group name of the object +#' \item \code{data}: The properties of the feature +#' } +#' +#' @describeIn addGlPoints Add Points to a leaflet map using Leaflet.glify +#' @examples \donttest{ #' library(leaflet) #' library(leafgl) #' library(sf) #' #' n = 1e5 -#' #' df1 = data.frame(id = 1:n, #' x = rnorm(n, 10, 1), #' y = rnorm(n, 49, 0.8)) #' pts = st_as_sf(df1, coords = c("x", "y"), crs = 4326) -#' #' cols = topo.colors(nrow(pts)) #' #' leaflet() %>% #' addProviderTiles(provider = providers$CartoDB.DarkMatter) %>% #' addGlPoints(data = pts, fillColor = cols, popup = TRUE) -#' #' } -#' #' @export addGlPoints addGlPoints = function(map, data, @@ -67,10 +97,25 @@ addGlPoints = function(map, layerId = NULL, src = FALSE, pane = "overlayPane", + popupOptions = NULL, + labelOptions = NULL, ...) { - dotopts = list(...) + # check data ########## + if (missing(labelOptions)) labelOptions <- labelOptions() + if (missing(popupOptions)) popupOptions <- popupOptions() + + if (is.null(group)) group = deparse(substitute(data)) + if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) + stopifnot(inherits(sf::st_geometry(data), c("sfc_POINT", "sfc_MULTIPOINT"))) + if (!is.null(layerId) && inherits(layerId, "formula")) + layerId <- evalFormula(layerId, data) + + ## currently leaflet.glify only supports single (fill)opacity! + fillOpacity = fillOpacity[1] + + # call SRC function ############## if (isTRUE(src)) { m = addGlPointsSrc( map = map @@ -80,37 +125,34 @@ addGlPoints = function(map, , radius = radius , group = group , popup = popup + , label = label , layerId = layerId , pane = pane + , popupOptions = popupOptions + , labelOptions = labelOptions , ... ) return(m) } - ## currently leaflet.glify only supports single (fill)opacity! - fillOpacity = fillOpacity[1] - - if (is.null(group)) group = deparse(substitute(data)) - if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) - stopifnot(inherits(sf::st_geometry(data), c("sfc_POINT", "sfc_MULTIPOINT"))) - + # get Bounds and ... ################# + dotopts = list(...) bounds = as.numeric(sf::st_bbox(data)) - # fillColor - args <- list(...) + # color ########### palette = "viridis" - if ("palette" %in% names(args)) { - palette <- args$palette - args$palette = NULL + if ("palette" %in% names(dotopts)) { + palette <- dotopts$palette + dotopts$palette = NULL } fillColor <- makeColorMatrix(fillColor, data, palette = palette) if (ncol(fillColor) != 3) stop("only 3 column fillColor matrix supported so far") fillColor = as.data.frame(fillColor, stringsAsFactors = FALSE) colnames(fillColor) = c("r", "g", "b") + fillColor = yyson_json_str(fillColor, digits = 3) - fillColor = jsonify::to_json(fillColor) - - # popup + # label / popup ########### + labels <- leaflet::evalFormula(label, data) if (!is.null(popup)) { htmldeps <- htmltools::htmlDependencies(popup) if (length(htmldeps) != 0) { @@ -119,38 +161,33 @@ addGlPoints = function(map, htmldeps ) } - popup = makePopup(popup, data) - popup = jsonify::to_json(popup) + popup = yyson_json_str(makePopup(popup, data)) } else { popup = NULL } - # data - # data = sf::st_transform(data, 4326) + # data ########### crds = sf::st_coordinates(data)[, c(2, 1)] - # convert data to json - if (length(args) == 0) { + if (length(dotopts) == 0) { jsonify_args = NULL } else { jsonify_args = try( match.arg( - names(args) - , names(as.list(args(jsonify::to_json))) + names(dotopts) + , names(as.list(args(yyjsonr::opts_write_json))) , several.ok = TRUE ) , silent = TRUE ) } if (inherits(jsonify_args, "try-error")) jsonify_args = NULL - data = do.call(jsonify::to_json, c(list(crds), args[jsonify_args])) + data = do.call(yyson_json_str, c(list(crds), dotopts[jsonify_args])) + class(data) <- "json" # dependencies - map$dependencies = c( - map$dependencies - , glifyDependencies() - ) - + map$dependencies = c(map$dependencies, glifyDependencies()) + # invoke leaflet method and zoom to bounds ########### map = leaflet::invokeMethod( map , leaflet::getMapData(map) @@ -158,13 +195,15 @@ addGlPoints = function(map, , data , fillColor , popup - , label + , labels , fillOpacity , radius , group , layerId , dotopts , pane + , popupOptions + , labelOptions ) leaflet::expandLimits( @@ -183,21 +222,18 @@ addGlPointsSrc = function(map, radius = 10, group = "glpoints", popup = NULL, + label = NULL, layerId = NULL, pane = "overlayPane", + popupOptions = NULL, + labelOptions = NULL, ...) { - ## currently leaflet.glify only supports single (fill)opacity! - fillOpacity = fillOpacity[1] - - if (is.null(group)) group = deparse(substitute(data)) - if (is.null(layerId)) layerId = paste0(group, "-pts") - if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) - stopifnot(inherits(sf::st_geometry(data), c("sfc_POINT", "sfc_MULTIPOINT"))) - + # get Bounds and ... ################# + dotopts = list(...) bounds = as.numeric(sf::st_bbox(data)) - # temp directories + # temp directories ############ dir_data = tempfile(pattern = "glify_points_dat") dir.create(dir_data) dir_color = tempfile(pattern = "glify_points_col") @@ -206,38 +242,40 @@ addGlPointsSrc = function(map, dir.create(dir_popup) dir_radius = tempfile(pattern = "glify_points_rad") dir.create(dir_radius) + dir_labels = tempfile(pattern = "glify_polylines_labl") + dir.create(dir_labels) - # data + # data ############ # data = sf::st_transform(data, 4326) crds = sf::st_coordinates(data)[, c(2, 1)] - ell_args <- list(...) - fl_data = paste0(dir_data, "/", layerId, "_data.js") - pre = paste0('var data = data || {}; data["', layerId, '"] = ') + fl_data = paste0(dir_data, "/", group, "_data.js") + pre = paste0('var data = data || {}; data["', group, '"] = ') writeLines(pre, fl_data) jsonify_args = try( match.arg( - names(ell_args) - , names(as.list(args(jsonify::to_json))) + names(dotopts) + , names(as.list(args(yyjsonr::opts_write_json))) , several.ok = TRUE ) , silent = TRUE ) if (inherits(jsonify_args, "try-error")) jsonify_args = NULL if (identical(jsonify_args, "x")) jsonify_args = NULL - cat('[', do.call(jsonify::to_json, c(list(crds), ell_args[jsonify_args])), '];', + cat('[', do.call(yyson_json_str, c(list(crds), dotopts[jsonify_args])), '];', file = fl_data, sep = "", append = TRUE) map$dependencies = c( map$dependencies, - glifyDependenciesSrc(), - glifyDataAttachmentSrc(fl_data, layerId) + glifyDependencies(TRUE), + glifyDataAttachmentSrc(fl_data, group) ) - # color + # color ############ palette = "viridis" - if ("palette" %in% names(ell_args)) { - palette <- ell_args$palette + if ("palette" %in% names(dotopts)) { + palette <- dotopts$palette + dotopts$palette = NULL } fillColor <- makeColorMatrix(fillColor, data, palette = palette) if (ncol(fillColor) != 3) stop("only 3 column fillColor matrix supported so far") @@ -245,21 +283,29 @@ addGlPointsSrc = function(map, colnames(fillColor) = c("r", "g", "b") if (nrow(fillColor) > 1) { - fl_color = paste0(dir_color, "/", layerId, "_color.js") - pre = paste0('var col = col || {}; col["', layerId, '"] = ') + fl_color = paste0(dir_color, "/", group, "_color.js") + pre = paste0('var col = col || {}; col["', group, '"] = ') writeLines(pre, fl_color) - cat('[', jsonify::to_json(fillColor), '];', + cat('[', yyson_json_str(fillColor, digits = 3), '];', file = fl_color, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyColorAttachmentSrc(fl_color, layerId) - ) - + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_color, group, "col")) fillColor = NULL } - # popup + # labels ############ + if (!is.null(label)) { + fl_label = paste0(dir_labels, "/", group, "_label.js") + pre = paste0('var labs = labs || {}; labs["', group, '"] = ') + writeLines(pre, fl_label) + cat('[', yyson_json_str(leaflet::evalFormula(label, data)), '];', + file = fl_label, append = TRUE) + + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_label, group, "lab")) + label = NULL + } + + # popup ############ if (!is.null(popup)) { htmldeps <- htmltools::htmlDependencies(popup) if (length(htmldeps) != 0) { @@ -268,56 +314,42 @@ addGlPointsSrc = function(map, htmldeps ) } - popup = makePopup(popup, data) - fl_popup = paste0(dir_popup, "/", layerId, "_popup.js") - pre = paste0('var popup = popup || {}; popup["', layerId, '"] = ') + fl_popup = paste0(dir_popup, "/", group, "_popup.js") + pre = paste0('var pops = pops || {}; pops["', group, '"] = ') writeLines(pre, fl_popup) - cat('[', jsonify::to_json(popup), '];', + cat('[', yyson_json_str(makePopup(popup, data)), '];', file = fl_popup, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyPopupAttachmentSrc(fl_popup, layerId) - ) - + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_popup, group, "pop")) + popup = NULL } - # radius + # radius ############ if (length(unique(radius)) > 1) { fl_radius = paste0(dir_radius, "/", layerId, "_radius.js") pre = paste0('var rad = rad || {}; rad["', layerId, '"] = ') writeLines(pre, fl_radius) - cat('[', jsonify::to_json(radius), '];', + cat('[', yyson_json_str(radius), '];', file = fl_radius, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyRadiusAttachmentSrc(fl_radius, layerId) - ) - + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_radius, group, "rad")) radius = NULL } - # leaflet::invokeMethod( - # map - # , leaflet::getMapData(map) - # , 'addGlifyPointsSrc' - # , fillOpacity - # , radius - # , group - # , layerId - # ) - + # invoke method ########### map = leaflet::invokeMethod( map , leaflet::getMapData(map) , 'addGlifyPointsSrc' , fillColor - , radius , fillOpacity + , radius , group , layerId + , dotopts , pane + , popupOptions + , labelOptions ) leaflet::expandLimits( @@ -325,11 +357,10 @@ addGlPointsSrc = function(map, c(bounds[2], bounds[4]), c(bounds[1], bounds[3]) ) - } -# ### via src +# ### via src ############## # addGlPointsSrc2 = function(map, # data, # color = cbind(0, 0.2, 1), @@ -363,11 +394,11 @@ addGlPointsSrc = function(map, # fl_data2 = paste0(dir_data, "/", grp2, "_data.json") # pre1 = paste0('var data = data || {}; data["', grp1, '"] = ') # writeLines(pre1, fl_data1) -# cat('[', jsonify::to_json(crds[1:100, ], ...), '];', +# cat('[', yyson_json_str(crds[1:100, ], ...), '];', # file = fl_data1, sep = "", append = TRUE) # pre2 = paste0('var data = data || {}; data["', grp2, '"] = ') # writeLines(pre2, fl_data2) -# cat('[', jsonify::to_json(crds[101:nrow(crds), ], ...), '];', +# cat('[', yyson_json_str(crds[101:nrow(crds), ], ...), '];', # file = fl_data2, sep = "", append = TRUE) # # # color @@ -378,7 +409,7 @@ addGlPointsSrc = function(map, # fl_color = paste0(dir_color, "/", group, "_color.json") # pre = paste0('var col = col || {}; col["', group, '"] = ') # writeLines(pre, fl_color) -# cat('[', jsonify::to_json(color), '];', +# cat('[', yyson_json_str(color), '];', # file = fl_color, append = TRUE) # # # popup @@ -386,7 +417,7 @@ addGlPointsSrc = function(map, # fl_popup = paste0(dir_popup, "/", group, "_popup.json") # pre = paste0('var popup = popup || {}; popup["', group, '"] = ') # writeLines(pre, fl_popup) -# cat('[', jsonify::to_json(data[[popup]]), '];', +# cat('[', yyson_json_str(data[[popup]]), '];', # file = fl_popup, append = TRUE) # } else { # popup = NULL @@ -395,17 +426,14 @@ addGlPointsSrc = function(map, # # dependencies # map$dependencies = c( # map$dependencies, -# glifyDependenciesSrc(), +# glifyDependenciesSrc(TRUE), # glifyDataAttachmentSrc(fl_data1, grp1), # glifyDataAttachmentSrc(fl_data2, grp1, TRUE), -# glifyColorAttachmentSrc(fl_color, group) +# glifyAttachmentSrc(fl_color, group, "col") # ) # # if (!is.null(popup)) { -# map$dependencies = c( -# map$dependencies, -# glifyPopupAttachmentSrc(fl_popup, group) -# ) +# map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_color, group, "col")) # } # # leaflet::invokeMethod(map, leaflet::getMapData(map), 'addGlifyPointsSrc2', @@ -443,7 +471,7 @@ addGlPointsSrc = function(map, # crds = sf::st_coordinates(data)[, c(2, 1)] # # fl_data = paste0(dir_data, "/", group, "_data.json") -# cat(jsonify::to_json(crds, digits = 7), file = fl_data, append = FALSE) +# cat(yyson_json_str(crds, digits = 7), file = fl_data, append = FALSE) # data_var = paste0(group, "dt") # # # color @@ -451,14 +479,14 @@ addGlPointsSrc = function(map, # color = as.data.frame(color, stringsAsFactors = FALSE) # colnames(color) = c("r", "g", "b") # -# jsn = jsonify::to_json(color) +# jsn = yyson_json_str(color) # fl_color = paste0(dir_color, "/", group, "_color.json") # color_var = paste0(group, "cl") # cat(jsn, file = fl_color, append = FALSE) # # # popup # if (!is.null(popup)) { -# pop = jsonify::to_json(data[[popup]]) +# pop = yyson_json_str(data[[popup]]) # fl_popup = paste0(dir_popup, "/", group, "_popup.json") # popup_var = paste0(group, "pop") # cat(pop, file = fl_popup, append = FALSE) diff --git a/R/glify-polygons.R b/R/glify-polygons.R index 7c6d4d8..cddf5f8 100644 --- a/R/glify-polygons.R +++ b/R/glify-polygons.R @@ -1,11 +1,4 @@ -#' add polygons to a leaflet map using Leaflet.glify -#' -#' @details -#' MULTIPOLYGONs are currently not supported! Make sure you cast your data -#' to POLYGON first (e.g. using \code{sf::st_cast(data, "POLYGON")}. -#' #' @examples -#' if (interactive()) { #' library(leaflet) #' library(leafgl) #' library(sf) @@ -17,9 +10,8 @@ #' leaflet() %>% #' addProviderTiles(provider = providers$CartoDB.DarkMatter) %>% #' addGlPolygons(data = gadm, color = cols, popup = TRUE) -#' } #' -#' @describeIn addGlPoints add polygons to a leaflet map using Leaflet.glify +#' @describeIn addGlPoints Add Polygons to a leaflet map using Leaflet.glify #' @aliases addGlPolygons #' @export addGlPolygons addGlPolygons = function(map, @@ -33,8 +25,29 @@ addGlPolygons = function(map, layerId = NULL, src = FALSE, pane = "overlayPane", + stroke = TRUE, + popupOptions = NULL, + labelOptions = NULL, ...) { + # check data ########## + if (missing(labelOptions)) labelOptions <- labelOptions() + if (missing(popupOptions)) popupOptions <- popupOptions() + + if (is.null(group)) group = deparse(substitute(data)) + if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) + stopifnot(inherits(sf::st_geometry(data), c("sfc_POLYGON", "sfc_MULTIPOLYGON"))) + if (inherits(sf::st_geometry(data), "sfc_MULTIPOLYGON")) + stop("Can only handle POLYGONs, please cast your MULTIPOLYGON to POLYGON using sf::st_cast", + call. = FALSE) + + if (!is.null(layerId) && inherits(layerId, "formula")) + layerId <- evalFormula(layerId, data) + + ## currently leaflet.glify only supports single (fill)opacity! + fillOpacity = fillOpacity[1] + + # call SRC function ############## if (isTRUE(src)) { m = addGlPolygonsSrc( map = map @@ -44,46 +57,41 @@ addGlPolygons = function(map, , fillOpacity = fillOpacity , group = group , popup = popup + , label = label , layerId = layerId , pane = pane + , stroke = stroke + , popupOptions = popupOptions + , labelOptions = labelOptions , ... ) return(m) } - ## currently leaflet.glify only supports single (fill)opacity! - fillOpacity = fillOpacity[1] - - if (is.null(group)) group = deparse(substitute(data)) - if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) - stopifnot(inherits(sf::st_geometry(data), c("sfc_POLYGON", "sfc_MULTIPOLYGON"))) - if (inherits(sf::st_geometry(data), "sfc_MULTIPOLYGON")) - stop("Can only handle POLYGONs, please cast your MULTIPOLYGON to POLYGON using sf::st_cast", - call. = FALSE) - + # get Bounds and ... ################# + dotopts = list(...) bounds = as.numeric(sf::st_bbox(data)) - # fillColor - args <- list(...) + # fillColor ########### palette = "viridis" - if ("palette" %in% names(args)) { - palette <- args$palette - args$palette = NULL + if ("palette" %in% names(dotopts)) { + palette <- dotopts$palette + dotopts$palette = NULL } fillColor <- makeColorMatrix(fillColor, data, palette = palette) if (ncol(fillColor) != 3) stop("only 3 column fillColor matrix supported so far") fillColor = as.data.frame(fillColor, stringsAsFactors = FALSE) colnames(fillColor) = c("r", "g", "b") + cols = yyson_json_str(fillColor, digits = 3) - cols = jsonify::to_json(fillColor, digits = 3) - - # popup + # label / popup ########### + labels <- leaflet::evalFormula(label, data) if (is.null(popup)) { # geom = sf::st_transform(sf::st_geometry(data), crs = 4326) geom = sf::st_geometry(data) data = sf::st_sf(id = 1:length(geom), geometry = geom) } else if (isTRUE(popup)) { - data = data[, popup] + ## Don't do anything. Pass all columns to JS } else { htmldeps <- htmltools::htmlDependencies(popup) if (length(htmldeps) != 0) { @@ -92,20 +100,19 @@ addGlPolygons = function(map, htmldeps ) } - popup = makePopup(popup, data) - popup = jsonify::to_json(popup) + popup = yyson_json_str(makePopup(popup, data)) geom = sf::st_geometry(data) data = sf::st_sf(id = 1:length(geom), geometry = geom) } - # data - if (length(args) == 0) { + # data ########### + if (length(dotopts) == 0) { geojsonsf_args = NULL } else { geojsonsf_args = try( match.arg( - names(args) - , names(as.list(args(geojsonsf::sf_geojson))) + names(dotopts) + , names(as.list(args(yyjsonr::opts_write_json))) , several.ok = TRUE ) , silent = TRUE @@ -113,15 +120,12 @@ addGlPolygons = function(map, if (inherits(geojsonsf_args, "try-error")) geojsonsf_args = NULL if (identical(geojsonsf_args, "sf")) geojsonsf_args = NULL } - data = do.call(geojsonsf::sf_geojson, c(list(data), args[geojsonsf_args])) - # data = geojsonsf::sf_geojson(data, ...) + data = do.call(yyson_geojson_str, c(list(data), dotopts[geojsonsf_args])) # dependencies - map$dependencies = c( - map$dependencies - , glifyDependencies() - ) + map$dependencies = c(map$dependencies, glifyDependencies()) + # invoke leaflet method and zoom to bounds ########### map = leaflet::invokeMethod( map , leaflet::getMapData(map) @@ -129,11 +133,15 @@ addGlPolygons = function(map, , data , cols , popup - , label + , labels , fillOpacity , group , layerId + , dotopts , pane + , stroke + , popupOptions + , labelOptions ) leaflet::expandLimits( @@ -153,60 +161,60 @@ addGlPolygonsSrc = function(map, fillOpacity = 0.6, group = "glpolygons", popup = NULL, + label = NULL, layerId = NULL, pane = "overlayPane", + stroke = TRUE, + popupOptions = NULL, + labelOptions = NULL, ...) { - if (is.null(group)) group = deparse(substitute(data)) - if (is.null(layerId)) layerId = paste0(group, "-pls") - if (inherits(data, "Spatial")) data <- sf::st_as_sf(data) - stopifnot(inherits(sf::st_geometry(data), c("sfc_POLYGON", "sfc_MULTIPOLYGON"))) - if (inherits(sf::st_geometry(data), "sfc_MULTIPOLYGON")) - stop("Can only handle POLYGONs, please cast your MULTIPOLYGON ", - "to POLYGON using e.g. sf::st_cast") - + # get Bounds and ... ################# + dotopts = list(...) bounds = as.numeric(sf::st_bbox(data)) - # temp directories + # temp directories ############ dir_data = tempfile(pattern = "glify_polygons_dat") dir.create(dir_data) dir_color = tempfile(pattern = "glify_polygons_col") dir.create(dir_color) dir_popup = tempfile(pattern = "glify_polygons_pop") dir.create(dir_popup) + dir_labels = tempfile(pattern = "glify_polylines_labl") + dir.create(dir_labels) - # data + # data ############ data_orig <- data geom = sf::st_geometry(data) data = sf::st_sf(id = 1:length(geom), geometry = geom) - ell_args <- list(...) - fl_data = paste0(dir_data, "/", layerId, "_data.js") - pre = paste0('var data = data || {}; data["', layerId, '"] = ') + fl_data = paste0(dir_data, "/", group, "_data.js") + pre = paste0('var data = data || {}; data["', group, '"] = ') writeLines(pre, fl_data) jsonify_args = try( match.arg( - names(ell_args) - , names(as.list(args(geojsonsf::sf_geojson))) + names(dotopts) + , names(as.list(args(yyjsonr::opts_write_json))) , several.ok = TRUE ) , silent = TRUE ) if (inherits(jsonify_args, "try-error")) jsonify_args = NULL if (identical(jsonify_args, "sf")) jsonify_args = NULL - cat('[', do.call(geojsonsf::sf_geojson, c(list(data), ell_args[jsonify_args])), '];', + cat('[', do.call(yyson_geojson_str, c(list(data), dotopts[jsonify_args])), '];', file = fl_data, sep = "", append = TRUE) map$dependencies = c( map$dependencies, - glifyDependenciesSrc(), - glifyDataAttachmentSrc(fl_data, layerId) + glifyDependencies(TRUE), + glifyDataAttachmentSrc(fl_data, group) ) - # color + # color ############ palette = "viridis" - if ("palette" %in% names(ell_args)) { - palette <- ell_args$palette + if ("palette" %in% names(dotopts)) { + palette <- dotopts$palette + dotopts$palette = NULL } fillColor <- makeColorMatrix(fillColor, data_orig, palette = palette) if (ncol(fillColor) != 3) stop("only 3 column fillColor matrix supported so far") @@ -214,21 +222,30 @@ addGlPolygonsSrc = function(map, colnames(fillColor) = c("r", "g", "b") if (nrow(fillColor) > 1) { - fl_color = paste0(dir_color, "/", layerId, "_color.js") - pre = paste0('var col = col || {}; col["', layerId, '"] = ') + fl_color = paste0(dir_color, "/", group, "_color.js") + pre = paste0('var col = col || {}; col["', group, '"] = ') writeLines(pre, fl_color) - cat('[', jsonify::to_json(fillColor), '];', + cat('[', yyson_json_str(fillColor, digits = 3), '];', file = fl_color, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyColorAttachmentSrc(fl_color, layerId) - ) + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_color, group, "col")) fillColor = NULL } - # popup + # labels ############ + if (!is.null(label)) { + fl_label = paste0(dir_labels, "/", group, "_label.js") + pre = paste0('var labs = labs || {}; labs["', group, '"] = ') + writeLines(pre, fl_label) + cat('[', yyson_json_str(leaflet::evalFormula(label, data_orig)), '];', + file = fl_label, append = TRUE) + + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_label, group, "lab")) + label = NULL + } + + # popup ############ if (!is.null(popup)) { htmldeps <- htmltools::htmlDependencies(popup) if (length(htmldeps) != 0) { @@ -237,20 +254,17 @@ addGlPolygonsSrc = function(map, htmldeps ) } - popup = makePopup(popup, data_orig) - fl_popup = paste0(dir_popup, "/", layerId, "_popup.js") - pre = paste0('var popup = popup || {}; popup["', layerId, '"] = ') + fl_popup = paste0(dir_popup, "/", group, "_popup.js") + pre = paste0('var pops = pops || {}; pops["', group, '"] = ') writeLines(pre, fl_popup) - cat('[', jsonify::to_json(popup), '];', + cat('[', yyson_json_str(makePopup(popup, data_orig)), '];', file = fl_popup, append = TRUE) - map$dependencies = c( - map$dependencies, - glifyPopupAttachmentSrc(fl_popup, layerId) - ) - + map$dependencies = c(map$dependencies, glifyAttachmentSrc(fl_popup, group, "pop")) + popup = NULL } + # invoke method ########### map = leaflet::invokeMethod( map , leaflet::getMapData(map) @@ -259,7 +273,11 @@ addGlPolygonsSrc = function(map, , fillOpacity , group , layerId + , dotopts , pane + , stroke + , popupOptions + , labelOptions ) leaflet::expandLimits( @@ -271,7 +289,7 @@ addGlPolygonsSrc = function(map, } -# ### via attachments +# ### via attachments ############ # addGlPolygonsFl = function(map, # data, # color = cbind(0, 0.2, 1), diff --git a/R/glify-remove-clear.R b/R/glify-remove-clear.R index 291444f..534009c 100644 --- a/R/glify-remove-clear.R +++ b/R/glify-remove-clear.R @@ -1,35 +1,38 @@ -#' removeGlPoints -#' @description Remove points from a map, identified by layerId; -#' @param map The map widget -#' @param layerId The layerId to remove +#' Remove Leaflet.Glify elements from a map +#' +#' Remove one or more features from a map, identified by `layerId`; +#' or, clear all features of the given group. +#' +#' @inheritParams leaflet::removeShape +#' @return the new `map` object +#' +#' @name remove #' @export removeGlPoints <- function(map, layerId) { leaflet::invokeMethod(map, NULL, "removeGlPoints", layerId) } -#' removeGlPolylines -#' @description Remove lines from a map, identified by layerId; -#' @param map The map widget -#' @param layerId The layerId to remove +#' @rdname remove #' @export removeGlPolylines <- function(map, layerId) { leaflet::invokeMethod(map, NULL, "removeGlPolylines", layerId) } -#' removeGlPolygons -#' @description Remove polygons from a map, identified by layerId; -#' @param map The map widget -#' @param layerId The layerId to remove +#' @rdname remove #' @export removeGlPolygons <- function(map, layerId) { leaflet::invokeMethod(map, NULL, "removeGlPolygons", layerId) } -#' clearGlLayers -#' @description Clear all Glify features -#' @param map The map widget +#' @rdname remove #' @export clearGlLayers <- function(map) { leaflet::invokeMethod(map, NULL, "clearGlLayers") } +#' @rdname remove +#' @export +clearGlGroup <- function(map, group) { + leaflet::invokeMethod(map, NULL, "clearGlGroup", group) +} + diff --git a/R/glify-shiny.R b/R/glify-shiny.R index 90ee686..2f3fdd5 100644 --- a/R/glify-shiny.R +++ b/R/glify-shiny.R @@ -11,8 +11,6 @@ #' #' @return A UI for rendering leafgl #' -#' @importFrom leaflet leafletOutput -#' @importFrom htmltools tagList tags htmlDependencies #' @rdname glify-shiny #' @export #' @@ -56,8 +54,6 @@ leafglOutput <- function(outputId, width = "100%", height = 400){ # Just for consistency # -#' @importFrom leaflet renderLeaflet -#' #' @param expr An expression that generates an HTML widget #' @param env The environment in which to evaluate expr. #' @param quoted Is expr a quoted expression (with quote())? diff --git a/R/leafgl_package.R b/R/leafgl_package.R new file mode 100644 index 0000000..b103af3 --- /dev/null +++ b/R/leafgl_package.R @@ -0,0 +1,9 @@ + +#' @keywords internal +"_PACKAGE" + +## usethis namespace: start +#' @importFrom leaflet leafletOutput renderLeaflet evalFormula +#' @importFrom htmltools tagList tags htmlDependencies +## usethis namespace: end +NULL diff --git a/R/utils-color.R b/R/utils-color.R index 0cec6d3..36b3e44 100644 --- a/R/utils-color.R +++ b/R/utils-color.R @@ -44,8 +44,7 @@ #' makeColorMatrix(~vals1, testdf) #' #' ## For JSON -#' library(jsonify) -#' makeColorMatrix(jsonify::to_json(data.frame(r = 54, g = 186, b = 1)), NULL) +#' makeColorMatrix(leafgl:::yyson_json_str(data.frame(r = 54, g = 186, b = 1)), NULL) #' #' ## For Lists #' makeColorMatrix(list(1,2), data.frame(x=c(1,2))) @@ -140,7 +139,7 @@ makeColorMatrix.list <- function(x, data = NULL, palette = "viridis", ...) { #' @export makeColorMatrix.json <- function(x, data = NULL, palette = "viridis", ...) { - x <- jsonify::from_json(x) + x <- yyjsonr::read_json_str(x) makeColorMatrix(x, data, palette, ...) } diff --git a/R/utils-popup.R b/R/utils-popup.R index c86f1ed..00b2d87 100644 --- a/R/utils-popup.R +++ b/R/utils-popup.R @@ -84,7 +84,7 @@ makePopup.list <- function(x, data) { #' @export makePopup.json <- function(x, data) { - x <- jsonify::from_json(x) + x <- yyjsonr::read_json_str(x) makePopup(x, data) } diff --git a/inst/htmlwidgets/Leaflet.glify/GlifyUtils.js b/inst/htmlwidgets/Leaflet.glify/GlifyUtils.js new file mode 100644 index 0000000..7f7309e --- /dev/null +++ b/inst/htmlwidgets/Leaflet.glify/GlifyUtils.js @@ -0,0 +1,282 @@ +/* global LeafletWidget, Shiny, L */ + + +// Remove elements by `layerId` +LeafletWidget.methods.removeGlPolylines = function(layerId) { + let insts = L.glify.linesInstances; + for(i in insts){ + let layId = insts[i].settings.layerId; + if (layId) { + let idx = layId.findIndex(k => k==layerId); + if (idx !== -1) { + insts[i].remove(idx); + insts[i].settings.layerId.splice(idx, 1); + } + } + } +}; +LeafletWidget.methods.removeGlPolygons = function(layerId) { + let insts = L.glify.shapesInstances; + for (i in insts) { + let layId = insts[i].settings.layerId; + if (layId) { + let idx = layId.findIndex(k => k==layerId); + if (idx !== -1) { + insts[i].remove(idx); + insts[i].settings.layerId.splice(idx, 1); + } + } + } +}; +LeafletWidget.methods.removeGlPoints = function(layerId) { + let insts = L.glify.pointsInstances; + for(i in insts){ + let layId = insts[i].settings.layerId; + if (layId) { + let idx = layId.findIndex(k => k==layerId); + if (idx !== -1) { + insts[i].remove(idx); + insts[i].settings.layerId.splice(idx, 1); + } + } + } +}; + +// Remove all Glify elements or by Group +LeafletWidget.methods.clearGlLayers = function() { + let arr = L.glify.shapesInstances; + for( let i = 0; i < arr.length; i++){ + arr[i].settings.map.off("mousemove"); + arr[i].remove(); + } + + arr = L.glify.linesInstances; + for( let i = 0; i < arr.length; i++){ + arr[i].settings.map.off("mousemove"); + arr[i].remove(); + } + + arr = L.glify.pointsInstances; + for( let i = 0; i < arr.length; i++){ + arr[i].settings.map.off("mousemove"); + arr[i].remove(); + } + + this.layerManager.clearLayers("glify"); +}; +LeafletWidget.methods.clearGlGroup = function(group) { + const formats = ['linesInstances', 'pointsInstances', 'shapesInstances']; + $.each(asArray(group), (j, v) => { + formats.forEach(format => { + let arr = L.glify[format]; + for( let i = 0; i < arr.length; i++){ + if ( arr[i].settings.className === group) { + arr[i].settings.map.off("mousemove"); + arr[i].remove(); + } + } + }); + this.layerManager.clearGroup(v); + }); +}; + + +// Workaround to set 'active' to TRUE / FALSE, when a layer is shown/hidden via the layerControl +function addGlifyEventListeners (map) { + if (!map.hasEventListeners("overlayadd")) { + map.on("overlayadd", function(e) { + let leafid = Object.keys(e.layer._layers)[0]; // L.stamp(e.layer) is not the same; + let glifylayer = this.layerManager._byCategory.glify[leafid] + if (glifylayer) { + let glifyinstance = L.glify.instances.find(e => e.layer._leaflet_id == leafid); + if (glifyinstance) { + glifyinstance.active = true; + } + } + }); + } + if (!map.hasEventListeners("overlayremove")) { + map.on("overlayremove", function(e) { + let leafid = Object.keys(e.layer._layers)[0]; // L.stamp(e.layer) is not the same; + let glifylayer = this.layerManager._byCategory.glify[leafid] + if (glifylayer) { + let glifyinstance = L.glify.instances.find(e => e.layer._leaflet_id == leafid); + if (glifyinstance) { + glifyinstance.active = false; + } + } + }); + } +}; + +// Adapt Leaflet hide/showGroup methods, to set active = TRUE/FALSE for Glify objects. +var origHideFun = LeafletWidget.methods.hideGroup; +LeafletWidget.methods.hideGroup = function(group) { + const map = this; + $.each(asArray(group), (i, g) => { + // Set Glify Instances to false + L.glify.instances.forEach(e => { + if (e.settings.className === g) { + e.active = false; + } + }); + // Remove Layer from Leaflet + origHideFun.call(this, group) + }); +}; + +var origShowFun = LeafletWidget.methods.showGroup; +LeafletWidget.methods.showGroup = function(group) { + const map = this; + $.each(asArray(group), (i, g) => { + // Set Glify Instances to true + L.glify.instances.forEach(e => { + if (e.settings.className === g) { + e.active = true; + } + }); + // Add Layer to Leaflet + origShowFun.call(this, group) + }); +}; + + +// Helper Functions +function click_event_pts(e, point, addpopup, popup, popupOptions, layer, layerId, data, map) { + if (map.hasLayer(layer.layer)) { + var idx = data.findIndex(k => k==point); + var content = popup ? popup[idx].toString() : null; + if (HTMLWidgets.shinyMode) { + Shiny.setInputValue(map.id + "_glify_click", { + id: layerId ? layerId[idx] : idx+1, + group: layer.settings.className, + lat: point[0], + lng: point[1], + data: content, + ".nonce": Math.random() + }); + } + if (addpopup) { + L.popup(popupOptions) + .setLatLng(point) + .setContent(content) + .openOn(map); + } + } +}; +function hover_event_pts(e, point, addlabel, label, layer, tooltip, layerId, data, map) { + if (map.hasLayer(layer.layer)) { + var idx = data.findIndex(k => k==point); + var content = Array.isArray(label) ? (label[idx] ? label[idx].toString() : null) : + typeof label === 'string' ? label : null; + if (HTMLWidgets.shinyMode) { + Shiny.setInputValue(map.id + "_glify_mouseover", { + id: layerId ? layerId[idx] : idx+1, + group: layer.settings.className, + lat: point[0], + lng: point[1], + data: content, + ".nonce": Math.random() + }); + } + if (addlabel) { + tooltip + .setLatLng(point) + .setContent(content) + .addTo(map); + } + } +} +function hoveroff_event_pts(e, point, layer, tooltip, layerId, data, map) { + if (map.hasLayer(layer.layer)) { + tooltip.remove(); + var idx = data.findIndex(k => k==point); + if (HTMLWidgets.shinyMode) { + Shiny.setInputValue(map.id + "_glify_mouseout", { + id: layerId ? layerId[idx] : idx+1, + group: layer.settings.className, + lat: point[0], + lng: point[1], + ".nonce": Math.random() + }); + } + } +} +function click_event(e, feature, addpopup, popup, popupOptions, layer, layerId, data, map) { + if (map.hasLayer(layer.layer)) { + const idx = data.features.findIndex(k => k==feature); + if (HTMLWidgets.shinyMode) { + Shiny.setInputValue(map.id + "_glify_click", { + id: layerId ? layerId[idx] : idx+1, + group: Object.values(layer.layer._eventParents)[0].groupname, + lat: e.latlng.lat, + lng: e.latlng.lng, + data: feature.properties, + ".nonce": Math.random() + }); + } + if (addpopup) { + const content = popup === true ? json2table(feature.properties) : popup[idx].toString(); + L.popup(popupOptions) + .setLatLng(e.latlng) + .setContent(content) + .openOn(map); + } + } +}; +function hover_event(e, feature, addlabel, label, layer, tooltip, layerId, data, map) { + if (map.hasLayer(layer.layer)) { + const idx = data.features.findIndex(k => k==feature); + if (HTMLWidgets.shinyMode) { + Shiny.setInputValue(map.id + "_glify_mouseover", { + id: layerId ? layerId[idx] : idx+1, + group: Object.values(layer.layer._eventParents)[0].groupname, + lat: e.latlng.lat, + lng: e.latlng.lng, + data: feature.properties, + ".nonce": Math.random() + }); + } + if (addlabel) { + const content = Array.isArray(label) ? (label[idx] ? label[idx].toString() : null) : + typeof label === 'string' ? label : null; + tooltip + .setLatLng(e.latlng) + .setContent(content) + .addTo(map); + } + } +} +function hoveroff_event(e, feature, layer, tooltip, layerId, data, map) { + if (map.hasLayer(layer.layer)) { + tooltip.remove(); + const idx = data.features.findIndex(k => k==feature); + if (HTMLWidgets.shinyMode) { + Shiny.setInputValue(map.id + "_glify_mouseout", { + id: layerId ? layerId[idx] : idx+1, + group: Object.values(layer.layer._eventParents)[0].groupname, + lat: e.latlng.lat, + lng: e.latlng.lng, + ".nonce": Math.random() + }); + } + } +} +function json2table(json, cls) { + const cols = Object.keys(json); + const vals = Object.values(json); + + let tab = ""; + for (let i = 0; i < cols.length; i++) { + tab += "
{a=r.settings,r.active&&r.map===n&&(u=r.lookup(t.latlng),null!==u&&(i[u.key]=r,e.push(u)))}),e.length<1)return;if(!a)return;const s=this.closest(t.latlng,e,n);if(!s)return;const c=i[s.key];if(!c)return;const{sensitivity:f}=c,l=s.latLng;return h(n.latLngToLayerPoint(l),t.layerPoint,s.chosenSize*(null!=f?f:1))?(o=c.click(t,s.feature||s.latLng),void 0===o||o):void 0}static tryHover(t,n,r){const e=[];return r.forEach(r=>{if(!r.active)return;if(r.map!==n)return;const i=r.lookup(t.latlng);if(i&&h(n.latLngToLayerPoint(i.latLng),t.layerPoint,i.chosenSize*r.sensitivityHover*30)){const n=r.hover(t,i.feature||i.latLng);void 0!==n&&e.push(n)}}),e}}_.defaults=y,_.maps=[];var m=r(1),x=r.n(m),b=r(2),w=r.n(b);function S(t){switch(t&&t.type||null){case"FeatureCollection":return t.features=t.features.reduce((function(t,n){return t.concat(S(n))}),[]),t;case"Feature":return t.geometry?S(t.geometry).map((function(n){var r={type:"Feature",properties:JSON.parse(JSON.stringify(t.properties)),geometry:n};return void 0!==t.id&&(r.id=t.id),r})):[t];case"MultiPoint":return t.coordinates.map((function(t){return{type:"Point",coordinates:t}}));case"MultiPolygon":return t.coordinates.map((function(t){return{type:"Polygon",coordinates:t}}));case"MultiLineString":return t.coordinates.map((function(t){return{type:"LineString",coordinates:t}}));case"GeometryCollection":return t.geometries.map(S).reduce((function(t,n){return t.concat(n)}),[]);case"Point":case"Polygon":case"LineString":return[t]}}const A={color:c,className:"",opacity:.5,borderOpacity:1,shaderVariables:{vertex:{type:"FLOAT",start:0,size:2},color:{type:"FLOAT",start:2,size:4}},border:!1};class L extends s{constructor(t){if(super(t),this.bytes=6,this.polygonLookup=null,this.settings={...L.defaults,...t},!t.data)throw new Error(o("settings.data"));if(!t.map)throw new Error(o("settings.map"));this.setup().render()}get border(){if("boolean"!=typeof this.settings.border)throw new Error(o("settings.border"));return this.settings.border}get borderOpacity(){if("number"!=typeof this.settings.borderOpacity)throw new Error(o("settings.borderOpacity"));return this.settings.borderOpacity}render(){this.resetVertices();const{canvas:t,gl:n,layer:r,vertices:e,mapMatrix:i}=this,o=this.getBuffer("vertex"),u=new Float32Array(e),a=u.BYTES_PER_ELEMENT,s=this.getAttributeLocation("vertex");return n.bindBuffer(n.ARRAY_BUFFER,o),n.bufferData(n.ARRAY_BUFFER,u,n.STATIC_DRAW),n.vertexAttribPointer(s,2,n.FLOAT,!1,a*this.bytes,0),n.enableVertexAttribArray(s),this.matrix=this.getUniformLocation("matrix"),n.viewport(0,0,t.width,t.height),i.setSize(t.width,t.height),n.uniformMatrix4fv(this.matrix,!1,i.array),this.attachShaderVariables(a),r.redraw(),this}resetVertices(){this.vertices=[],this.vertexLines=[],this.polygonLookup=new w.a;const{vertices:t,vertexLines:n,polygonLookup:r,map:i,border:u,opacity:a,borderOpacity:s,color:c,data:f}=this;let h,v,p,d,g,y,_,m,b,A,L=null,E=0;switch(f.type){case"Feature":r.loadFeatureCollection({type:"FeatureCollection",features:[f]}),p=S(f);break;case"MultiPolygon":{const t={type:"MultiPolygon",coordinates:f.coordinates};r.loadFeatureCollection({type:"FeatureCollection",features:[{type:"Feature",properties:{},geometry:t}]}),p=S(f);break}default:r.loadFeatureCollection(f),p=f.features}const M=p.length;if(!c)throw new Error(o("settings.color"));for("function"==typeof c&&(L=c);E t.layer._leaflet_id===this.layer._leaflet_id));return-1!==t&&k.linesInstances.splice(t,1),this}drawOnCanvas(t){if(!this.gl)return this;const{gl:n,data:r,canvas:e,mapMatrix:i,matrix:o,allVertices:u,vertices:a,weight:s,aPointSize:c,bytes:l,mapCenterPixels:f}=this,{scale:h,offset:p,zoom:v}=t;this.scale=h;const g=Math.max(v-4,4);if(n.clear(n.COLOR_BUFFER_BIT),n.viewport(0,0,e.width,e.height),n.vertexAttrib1f(c,g),i.setSize(e.width,e.height).scaleTo(h),v>18)i.translateTo(-p.x+f.x,-p.y+f.y),n.uniformMatrix4fv(o,!1,i.array),n.drawArrays(n.LINES,0,u.length/l);else if("number"==typeof s)for(let t=-s;t<=s;t+=.5)for(let r=-s;r<=s;r+=.5)i.translateTo(-p.x+f.x+r/h,-p.y+f.y+t/h),n.uniformMatrix4fv(o,!1,i.array),n.drawArrays(n.LINES,0,u.length/l);else if("function"==typeof s){let t=0;const{features:e}=r;for(let r=0;rs?a>c?a:c:s>c?s:c,m=l>f?l>p?l:p:f>p?f:p,x=h(g,y,n,r,e),w=h(_,m,n,r,e),b=t.prevZ,S=t.nextZ;b&&b.z>=x&&S&&S.z<=w;){if(b.x>=g&&b.x<=_&&b.y>=y&&b.y<=m&&b!==i&&b!==u&&v(a,l,s,f,c,p,b.x,b.y)&&d(b.prev,b,b.next)>=0)return!1;if(b=b.prevZ,S.x>=g&&S.x<=_&&S.y>=y&&S.y<=m&&S!==i&&S!==u&&v(a,l,s,f,c,p,S.x,S.y)&&d(S.prev,S,S.next)>=0)return!1;S=S.nextZ}for(;b&&b.z>=x;){if(b.x>=g&&b.x<=_&&b.y>=y&&b.y<=m&&b!==i&&b!==u&&v(a,l,s,f,c,p,b.x,b.y)&&d(b.prev,b,b.next)>=0)return!1;b=b.prevZ}for(;S&&S.z<=w;){if(S.x>=g&&S.x<=_&&S.y>=y&&S.y<=m&&S!==i&&S!==u&&v(a,l,s,f,c,p,S.x,S.y)&&d(S.prev,S,S.next)>=0)return!1;S=S.nextZ}return!0}function a(t,n,r){var i=t;do{var o=i.prev,u=i.next.next;!y(o,u)&&_(o,i,i.next,u)&&w(o,u)&&w(u,o)&&(n.push(o.i/r|0),n.push(i.i/r|0),n.push(u.i/r|0),A(i),A(i.next),i=t=u),i=i.next}while(i!==t);return e(i)}function s(t,n,r,o,u,a){var s=t;do{for(var c=s.next.next;c!==s.prev;){if(s.i!==c.i&&g(s,c)){var l=b(s,c);return s=e(s,s.next),l=e(l,l.next),i(s,n,r,o,u,a,0),void i(l,n,r,o,u,a,0)}c=c.next}s=s.next}while(s!==t)}function c(t,n){return t.x-n.x}function l(t,n){var r=function(t,n){var r,e=n,i=t.x,o=t.y,u=-1/0;do{if(o<=e.y&&o>=e.next.y&&e.next.y!==e.y){var a=e.x+(o-e.y)*(e.next.x-e.x)/(e.next.y-e.y);if(a<=i&&a>u&&(u=a,r=e.x-1&&t%1==0&&t