From 828d220f8a69e2cb2e7c522570ed9f384d63fdd5 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 21 Jun 2018 13:44:10 +0100 Subject: [PATCH 1/7] Add setStyle() --- NAMESPACE | 1 + R/methods.R | 20 ++++++++++++++++++++ inst/htmlwidgets/leaflet.js | 9 +++++++++ javascript/src/methods.js | 8 ++++++++ man/setStyle.Rd | 23 +++++++++++++++++++++++ 5 files changed, 61 insertions(+) create mode 100644 man/setStyle.Rd diff --git a/NAMESPACE b/NAMESPACE index b84fb841f..a63c3474c 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -155,6 +155,7 @@ export(renderLeaflet) export(safeLabel) export(scaleBarOptions) export(setMaxBounds) +export(setStyle) export(setView) export(showGroup) export(tileOptions) diff --git a/R/methods.R b/R/methods.R index d394ae1aa..8a1db08d1 100644 --- a/R/methods.R +++ b/R/methods.R @@ -32,6 +32,26 @@ setView <- function(map, lng, lat, zoom, options = list()) { ) } +#' Efficiently style a group that has already been added to the map +#' +#' Call with a group and a vector of style lists of length N. The first N +#' features of the group will be restyled. +#' +#' @examples +#' \donttest{ +#' renderLeaflet("map", { +#' leaflet() %>% addPolygons(data = zones, group = "zones", color = "red") +#' }) +#' colour = "blue" +#' styles = lapply(pal(values), function(colour) {list(fillColor=colour, color=colour)}) +#' leafletProxy("map") %>% +#' setStyle("zones", styles) +#' } +#' @export +setStyle = function(map, group, styles) { + invokeMethod(map, NULL, "setStyle", group, styles) +} + #' @describeIn map-methods Flys to a given location/zoom-level using smooth pan-zoom. #' @export flyTo <- function(map, lng, lat, zoom, options = list()) { diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 3d0104a49..78f34b506 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -1282,6 +1282,15 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de var methods = {}; exports.default = methods; +/** Much more performant way to style loaded geometry */ + +methods.setStyle = function (group, styles) { + window.map = this; + var layers = this.layerManager.getLayerGroup(group).getLayers(); + for (var i = 0; i < styles.length; i++) { + layers[i].setStyle(styles[i]); + } +}; function mouseHandler(mapId, layerId, group, eventName, extraInfo) { return function (e) { diff --git a/javascript/src/methods.js b/javascript/src/methods.js index 5cbc7c3e6..c61ba707b 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -13,6 +13,14 @@ import Mipmapper from "./mipmapper"; let methods = {}; export default methods; +/** Much more performant way to style loaded geometry */ +methods.setStyle = function(group, styles) { + window.map = this; + let layers = this.layerManager.getLayerGroup(group).getLayers(); + for (let i = 0; i < styles.length; i++) { + layers[i].setStyle(styles[i]); + } +}; function mouseHandler(mapId, layerId, group, eventName, extraInfo) { return function(e) { diff --git a/man/setStyle.Rd b/man/setStyle.Rd new file mode 100644 index 000000000..11693ba41 --- /dev/null +++ b/man/setStyle.Rd @@ -0,0 +1,23 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/methods.R +\name{setStyle} +\alias{setStyle} +\title{Efficiently style a group that has already been added to the map} +\usage{ +setStyle(map, group, styles) +} +\description{ +Call with a group and a vector of style lists of length N. The first N +features of the group will be restyled. +} +\examples{ +\donttest{ +renderLeaflet("map", { + leaflet() \%>\% addPolygons(data = zones, group = "zones", color = "red") +}) +colour = "blue" +styles = lapply(pal(values), function(colour) {list(fillColor=colour, color=colour)}) +leafletProxy("map") \%>\% + setStyle("zones", styles) +} +} From dd9844052caaf9ff6a6b143c333ee8eddad8f518 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 21 Jun 2018 18:42:07 +0100 Subject: [PATCH 2/7] Add label argument to setStyle --- R/methods.R | 4 ++-- inst/htmlwidgets/leaflet.js | 14 +++++++++++--- javascript/src/methods.js | 14 +++++++++++--- man/setStyle.Rd | 2 +- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/R/methods.R b/R/methods.R index 8a1db08d1..e1499e593 100644 --- a/R/methods.R +++ b/R/methods.R @@ -48,8 +48,8 @@ setView <- function(map, lng, lat, zoom, options = list()) { #' setStyle("zones", styles) #' } #' @export -setStyle = function(map, group, styles) { - invokeMethod(map, NULL, "setStyle", group, styles) +setStyle = function(map, group, styles, label = NULL) { + invokeMethod(map, NULL, "setStyle", group, styles, label) } #' @describeIn map-methods Flys to a given location/zoom-level using smooth pan-zoom. diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 78f34b506..3f28338e6 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -1284,11 +1284,19 @@ exports.default = methods; /** Much more performant way to style loaded geometry */ -methods.setStyle = function (group, styles) { +methods.setStyle = function (group, styles, labels) { window.map = this; var layers = this.layerManager.getLayerGroup(group).getLayers(); - for (var i = 0; i < styles.length; i++) { - layers[i].setStyle(styles[i]); + + if (styles) { + for (var i = 0; i < styles.length; i++) { + layers[i].setStyle(styles[i]); + } + } + if (labels) { + for (var _i = 0; _i < styles.length; _i++) { + layers[_i].bindTooltip(labels[_i]); + } } }; diff --git a/javascript/src/methods.js b/javascript/src/methods.js index c61ba707b..cdd3751e4 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -14,11 +14,19 @@ let methods = {}; export default methods; /** Much more performant way to style loaded geometry */ -methods.setStyle = function(group, styles) { +methods.setStyle = function(group, styles, labels) { window.map = this; let layers = this.layerManager.getLayerGroup(group).getLayers(); - for (let i = 0; i < styles.length; i++) { - layers[i].setStyle(styles[i]); + + if (styles) { + for (let i = 0; i < styles.length; i++) { + layers[i].setStyle(styles[i]); + } + } + if (labels) { + for (let i = 0; i < styles.length; i++) { + layers[i].bindTooltip(labels[i]); + } } }; diff --git a/man/setStyle.Rd b/man/setStyle.Rd index 11693ba41..142735314 100644 --- a/man/setStyle.Rd +++ b/man/setStyle.Rd @@ -4,7 +4,7 @@ \alias{setStyle} \title{Efficiently style a group that has already been added to the map} \usage{ -setStyle(map, group, styles) +setStyle(map, group, styles, label = NULL) } \description{ Call with a group and a vector of style lists of length N. The first N From 0e3f51362a879fa3c5a8d6b0863b17bc8076f43a Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 23 Jun 2018 09:37:04 +0100 Subject: [PATCH 3/7] Provide faster setStyle implementation This work sponsored by [Integrated Transport Planning](https://www.itpworld.net) and the World Bank. Performance still wasn't good enough when styling 20000 features at once. Turns out it's something to do with how shiny sends different data types between R and JS. Shiny passes arrays much faster than objects. I don't know if the delay is in encoding or transmission. You can then reassemble the required objects in JS basically for free. The same optimisation will probably be possible for addPolylines, etc. At the moment shiny sends an array of arrays of arrays of objects of arrays. Which is unnecessarily convoluted and requires the creation of N small objects and at least 4N small arrays. I suspect that it will be faster to send a smaller number of larger arrays -- something like the format provided by st_coordinates -- or possibly to use a space efficient encoding scheme like Google's encoded polyline (plugin available for leaflet). --- NAMESPACE | 1 + R/methods.R | 8 ++++++-- inst/htmlwidgets/leaflet.js | 17 +++++++++++++++-- javascript/src/methods.js | 17 ++++++++++++++--- man/setStyle.Rd | 2 +- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/NAMESPACE b/NAMESPACE index a63c3474c..9ffcdf950 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -156,6 +156,7 @@ export(safeLabel) export(scaleBarOptions) export(setMaxBounds) export(setStyle) +export(setStyleFast) export(setView) export(showGroup) export(tileOptions) diff --git a/R/methods.R b/R/methods.R index e1499e593..b0a901c11 100644 --- a/R/methods.R +++ b/R/methods.R @@ -48,8 +48,12 @@ setView <- function(map, lng, lat, zoom, options = list()) { #' setStyle("zones", styles) #' } #' @export -setStyle = function(map, group, styles, label = NULL) { - invokeMethod(map, NULL, "setStyle", group, styles, label) +setStyle = function(map, group, styles, label = NULL, offset = 0) { + invokeMethod(map, NULL, "setStyle", group, styles, label, offset) +} + +setStyleFast = function(map, group, color, label = NULL) { + invokeMethod(map, NULL, "setStyleFast", group, color, label) } #' @describeIn map-methods Flys to a given location/zoom-level using smooth pan-zoom. diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 3f28338e6..cd2604f8e 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -1285,21 +1285,34 @@ exports.default = methods; /** Much more performant way to style loaded geometry */ methods.setStyle = function (group, styles, labels) { + var offset = arguments.length <= 3 || arguments[3] === undefined ? 0 : arguments[3]; + window.map = this; var layers = this.layerManager.getLayerGroup(group).getLayers(); if (styles) { for (var i = 0; i < styles.length; i++) { - layers[i].setStyle(styles[i]); + layers[i + offset].setStyle(styles[i]); } } if (labels) { for (var _i = 0; _i < styles.length; _i++) { - layers[_i].bindTooltip(labels[_i]); + layers[_i + offset].bindTooltip(labels[_i]); } } }; +/** Much more performant way to style loaded geometry */ +methods.setStyleFast = function (group, colors, labels) { + window.map = this; + var layers = this.layerManager.getLayerGroup(group).getLayers(); + + for (var i = 0; i < colors.length; i++) { + layers[i].setStyle({ color: colors[i], fillColor: colors[i] }); + layers[i].bindTooltip(labels[i]); + } +}; + function mouseHandler(mapId, layerId, group, eventName, extraInfo) { return function (e) { if (!_htmlwidgets2.default.shinyMode) return; diff --git a/javascript/src/methods.js b/javascript/src/methods.js index cdd3751e4..5a1589a2d 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -14,22 +14,33 @@ let methods = {}; export default methods; /** Much more performant way to style loaded geometry */ -methods.setStyle = function(group, styles, labels) { +methods.setStyle = function(group, styles, labels, offset = 0) { window.map = this; let layers = this.layerManager.getLayerGroup(group).getLayers(); if (styles) { for (let i = 0; i < styles.length; i++) { - layers[i].setStyle(styles[i]); + layers[i + offset].setStyle(styles[i]); } } if (labels) { for (let i = 0; i < styles.length; i++) { - layers[i].bindTooltip(labels[i]); + layers[i + offset].bindTooltip(labels[i]); } } }; +/** Much more performant way to style loaded geometry */ +methods.setStyleFast = function(group, colors, labels) { + window.map = this; + let layers = this.layerManager.getLayerGroup(group).getLayers(); + + for (let i = 0; i < colors.length; i++) { + layers[i].setStyle({color: colors[i], fillColor: colors[i]}) + layers[i].bindTooltip(labels[i]); + } +}; + function mouseHandler(mapId, layerId, group, eventName, extraInfo) { return function(e) { if (!HTMLWidgets.shinyMode) return; diff --git a/man/setStyle.Rd b/man/setStyle.Rd index 142735314..24af1cd15 100644 --- a/man/setStyle.Rd +++ b/man/setStyle.Rd @@ -4,7 +4,7 @@ \alias{setStyle} \title{Efficiently style a group that has already been added to the map} \usage{ -setStyle(map, group, styles, label = NULL) +setStyle(map, group, styles, label = NULL, offset = 0) } \description{ Call with a group and a vector of style lists of length N. The first N From 3dbf5bfbc0f8bd81f7c27bfc0d795b5250ac1aae Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Mon, 25 Jun 2018 19:29:13 +0100 Subject: [PATCH 4/7] Add weights argument to setStyleFast --- R/methods.R | 4 ++-- inst/htmlwidgets/leaflet.js | 21 +++++++++++++++++---- javascript/src/methods.js | 21 +++++++++++++++++---- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/R/methods.R b/R/methods.R index b0a901c11..e6525ea00 100644 --- a/R/methods.R +++ b/R/methods.R @@ -52,8 +52,8 @@ setStyle = function(map, group, styles, label = NULL, offset = 0) { invokeMethod(map, NULL, "setStyle", group, styles, label, offset) } -setStyleFast = function(map, group, color, label = NULL) { - invokeMethod(map, NULL, "setStyleFast", group, color, label) +setStyleFast = function(map, group, color = NULL, weight = NULL, label = NULL) { + invokeMethod(map, NULL, "setStyleFast", group, color, weight, label) } #' @describeIn map-methods Flys to a given location/zoom-level using smooth pan-zoom. diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index cd2604f8e..c5a3bd245 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -1303,13 +1303,26 @@ methods.setStyle = function (group, styles, labels) { }; /** Much more performant way to style loaded geometry */ -methods.setStyleFast = function (group, colors, labels) { +methods.setStyleFast = function (group, colors, weights, labels) { window.map = this; var layers = this.layerManager.getLayerGroup(group).getLayers(); - for (var i = 0; i < colors.length; i++) { - layers[i].setStyle({ color: colors[i], fillColor: colors[i] }); - layers[i].bindTooltip(labels[i]); + if (labels) { + for (var i = 0; i < labels.length; i++) { + layers[i].bindTooltip(labels[i]); + } + } + + if (colors) { + for (var _i2 = 0; _i2 < colors.length; _i2++) { + layers[_i2].setStyle({ color: colors[_i2], fillColor: colors[_i2] }); + } + } + + if (weights) { + for (var _i3 = 0; _i3 < weights.length; _i3++) { + layers[_i3].setStyle({ weight: weights[_i3] }); + } } }; diff --git a/javascript/src/methods.js b/javascript/src/methods.js index 5a1589a2d..4054d4b09 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -31,13 +31,26 @@ methods.setStyle = function(group, styles, labels, offset = 0) { }; /** Much more performant way to style loaded geometry */ -methods.setStyleFast = function(group, colors, labels) { +methods.setStyleFast = function(group, colors, weights, labels) { window.map = this; let layers = this.layerManager.getLayerGroup(group).getLayers(); - for (let i = 0; i < colors.length; i++) { - layers[i].setStyle({color: colors[i], fillColor: colors[i]}) - layers[i].bindTooltip(labels[i]); + if (labels) { + for (let i = 0; i < labels.length; i++) { + layers[i].bindTooltip(labels[i]); + } + } + + if (colors) { + for (let i = 0; i < colors.length; i++) { + layers[i].setStyle({color: colors[i], fillColor: colors[i]}) + } + } + + if (weights) { + for (let i = 0; i < weights.length; i++) { + layers[i].setStyle({weight: weights[i]}) + } } }; From 5fa9b989763a91e7609a308465611cc3ffb36b2e Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 3 Jul 2018 20:09:56 +0100 Subject: [PATCH 5/7] Add stroke and fill arguments to setStyleFast Should pass pathOptions but can't be bothered to fix it rn Also fix devtools::document() unexporting setStyleFast. --- R/methods.R | 5 +++-- inst/htmlwidgets/leaflet.js | 14 +++++++++++++- javascript/src/methods.js | 18 +++++++++++++++--- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/R/methods.R b/R/methods.R index e6525ea00..d27113c22 100644 --- a/R/methods.R +++ b/R/methods.R @@ -52,8 +52,9 @@ setStyle = function(map, group, styles, label = NULL, offset = 0) { invokeMethod(map, NULL, "setStyle", group, styles, label, offset) } -setStyleFast = function(map, group, color = NULL, weight = NULL, label = NULL) { - invokeMethod(map, NULL, "setStyleFast", group, color, weight, label) +#' @export +setStyleFast = function(map, group, color = NULL, weight = NULL, label = NULL, stroke = NULL, fill = NULL) { + invokeMethod(map, NULL, "setStyleFast", group, color, weight, label, stroke, fill) } #' @describeIn map-methods Flys to a given location/zoom-level using smooth pan-zoom. diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index c5a3bd245..1c5fa1bc7 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -1303,7 +1303,7 @@ methods.setStyle = function (group, styles, labels) { }; /** Much more performant way to style loaded geometry */ -methods.setStyleFast = function (group, colors, weights, labels) { +methods.setStyleFast = function (group, colors, weights, labels, strokes, fills) { window.map = this; var layers = this.layerManager.getLayerGroup(group).getLayers(); @@ -1324,6 +1324,18 @@ methods.setStyleFast = function (group, colors, weights, labels) { layers[_i3].setStyle({ weight: weights[_i3] }); } } + + if (strokes) { + for (var _i4 = 0; _i4 < strokes.length; _i4++) { + layers[_i4].setStyle({ stroke: strokes[_i4] }); + } + } + + if (fills) { + for (var _i5 = 0; _i5 < fills.length; _i5++) { + layers[_i5].setStyle({ fill: fills[_i5] }); + } + } }; function mouseHandler(mapId, layerId, group, eventName, extraInfo) { diff --git a/javascript/src/methods.js b/javascript/src/methods.js index 4054d4b09..696185919 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -31,7 +31,7 @@ methods.setStyle = function(group, styles, labels, offset = 0) { }; /** Much more performant way to style loaded geometry */ -methods.setStyleFast = function(group, colors, weights, labels) { +methods.setStyleFast = function(group, colors, weights, labels, strokes, fills) { window.map = this; let layers = this.layerManager.getLayerGroup(group).getLayers(); @@ -43,13 +43,25 @@ methods.setStyleFast = function(group, colors, weights, labels) { if (colors) { for (let i = 0; i < colors.length; i++) { - layers[i].setStyle({color: colors[i], fillColor: colors[i]}) + layers[i].setStyle({color: colors[i], fillColor: colors[i]}); } } if (weights) { for (let i = 0; i < weights.length; i++) { - layers[i].setStyle({weight: weights[i]}) + layers[i].setStyle({weight: weights[i]}); + } + } + + if (strokes) { + for (let i = 0; i < strokes.length; i++) { + layers[i].setStyle({stroke: strokes[i]}); + } + } + + if (fills) { + for (let i = 0; i < fills.length; i++) { + layers[i].setStyle({fill: fills[i]}); } } }; From 9841cde51a8405a2ac4330c16f55f72b03e4cf43 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 3 Jul 2018 22:24:14 +0100 Subject: [PATCH 6/7] Fix offset on setStyle --- R/methods.R | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/R/methods.R b/R/methods.R index d27113c22..408d7c52e 100644 --- a/R/methods.R +++ b/R/methods.R @@ -49,7 +49,7 @@ setView <- function(map, lng, lat, zoom, options = list()) { #' } #' @export setStyle = function(map, group, styles, label = NULL, offset = 0) { - invokeMethod(map, NULL, "setStyle", group, styles, label, offset) + invokeMethod(map, NULL, "setStyle", group, styles, label, offset - 1) } #' @export From 012bec5a774547c6589beacfdc9a02d8d64fc9bb Mon Sep 17 00:00:00 2001 From: Mark D Date: Tue, 10 Jul 2018 14:46:17 +0100 Subject: [PATCH 7/7] Expose ctrl, shift, alt and meta on click events --- inst/htmlwidgets/leaflet.js | 16 ++++++++++++++-- javascript/src/index.js | 8 +++++++- javascript/src/methods.js | 8 +++++++- 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 1c5fa1bc7..21cf16342 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -655,7 +655,13 @@ _htmlwidgets2.default.widget({ _shiny2.default.onInputChange(map.id + "_click", { lat: e.latlng.lat, lng: e.latlng.lng, - ".nonce": Math.random() // Force reactivity if lat/lng hasn't changed + ".nonce": Math.random(), // Force reactivity if lat/lng hasn't changed + modifiers: { + alt: e.originalEvent.altKey, + ctrl: e.originalEvent.ctrlKey, + meta: e.originalEvent.metaKey, + shift: e.originalEvent.shiftKey + } }); }); @@ -1352,7 +1358,13 @@ function mouseHandler(mapId, layerId, group, eventName, extraInfo) { } var eventInfo = _jquery2.default.extend({ id: layerId, - ".nonce": Math.random() // force reactivity + ".nonce": Math.random(), // force reactivity + modifiers: { + alt: e.originalEvent.altKey, + ctrl: e.originalEvent.ctrlKey, + meta: e.originalEvent.metaKey, + shift: e.originalEvent.shiftKey + } }, group !== null ? { group: group } : null, latLng, extraInfo); _shiny2.default.onInputChange(mapId + "_" + eventName, eventInfo); diff --git a/javascript/src/index.js b/javascript/src/index.js index 3230ec298..711ac4efd 100644 --- a/javascript/src/index.js +++ b/javascript/src/index.js @@ -140,7 +140,13 @@ HTMLWidgets.widget({ Shiny.onInputChange(map.id + "_click", { lat: e.latlng.lat, lng: e.latlng.lng, - ".nonce": Math.random() // Force reactivity if lat/lng hasn't changed + ".nonce": Math.random(), // Force reactivity if lat/lng hasn't changed + modifiers: { + alt: e.originalEvent.altKey, + ctrl: e.originalEvent.ctrlKey, + meta: e.originalEvent.metaKey, + shift: e.originalEvent.shiftKey + } }); }); diff --git a/javascript/src/methods.js b/javascript/src/methods.js index 696185919..91c326b76 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -81,7 +81,13 @@ function mouseHandler(mapId, layerId, group, eventName, extraInfo) { let eventInfo = $.extend( { id: layerId, - ".nonce": Math.random() // force reactivity + ".nonce": Math.random(), // force reactivity + modifiers: { + alt: e.originalEvent.altKey, + ctrl: e.originalEvent.ctrlKey, + meta: e.originalEvent.metaKey, + shift: e.originalEvent.shiftKey + } }, group !== null ? {group: group} : null, latLng,