diff --git a/DESCRIPTION b/DESCRIPTION index b038843b8..8a8dc3fdc 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -33,6 +33,7 @@ URL: http://rstudio.github.io/leaflet/ BugReports: https://github.com/rstudio/leaflet/issues Imports: base64enc, + crosstalk, htmlwidgets, htmltools, magrittr, @@ -50,4 +51,7 @@ Suggests: rgdal, R6, RJSONIO +Remotes: + rstudio/crosstalk, + jcheng5/plotly@joe/feature/crosstalk RoxygenNote: 5.0.1 diff --git a/NAMESPACE b/NAMESPACE index c0fd033c6..b992df8b0 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -2,11 +2,13 @@ S3method("[",leaflet_awesome_icon_set) S3method("[",leaflet_icon_set) +S3method(pointData,SharedData) S3method(pointData,SpatialPoints) S3method(pointData,SpatialPointsDataFrame) S3method(pointData,data.frame) S3method(pointData,default) S3method(pointData,matrix) +S3method(polygonData,SharedData) export("%>%") export(JS) export(WMSTileOptions) @@ -93,5 +95,6 @@ export(setMaxBounds) export(setView) export(showGroup) export(tileOptions) +import(crosstalk) importFrom(htmlwidgets,JS) importFrom(magrittr,"%>%") diff --git a/R/normalize.R b/R/normalize.R index c2089ac83..33f069c95 100644 --- a/R/normalize.R +++ b/R/normalize.R @@ -36,6 +36,10 @@ doResolveFormula.data.frame = function(data, f) { eval(f[[2]], data, environment(f)) } +doResolveFormula.SharedData = function(data, f) { + doResolveFormula(data$data(withSelection = TRUE, withFilter = FALSE, withKey = TRUE), f) +} + doResolveFormula.map = function(data, f) { eval(f[[2]], data, environment(f)) } @@ -160,6 +164,12 @@ pointData.SpatialPointsDataFrame = function(obj) { ) } +#' @export +pointData.SharedData = function(obj) { + pointData(obj$data(withSelection = FALSE, + withFilter = FALSE, withKey = FALSE)) +} + # A simple polygon is a list(lng=numeric(), lat=numeric()). A compound polygon # is a list of simple polygons. This function returns a list of compound # polygons, so list(list(list(lng=..., lat=...))). There is also a bbox @@ -221,6 +231,12 @@ polygonData.SpatialLinesDataFrame = function(obj) { polygonData(sp::SpatialLines(obj@lines)) } +#' @export +polygonData.SharedData = function(obj) { + polygonData(obj$data(withSelection = FALSE, + withFilter = FALSE, withKey = FALSE)) +} + dfbbox = function(df) { suppressWarnings(rbind( lng = range(df$lng, na.rm = TRUE), diff --git a/R/utils.R b/R/utils.R index 025bc42d4..fa350f11e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -42,9 +42,22 @@ filterNULL = function(x) { #' @param method the name of the JavaScript method to invoke #' @param ... unnamed arguments to be passed to the JavaScript method #' @rdname dispatch +#' @import crosstalk #' @export invokeMethod = function(map, data, method, ...) { - args = evalFormula(list(...), data) + crosstalkOptions <- if (crosstalk::is.SharedData(data)) { + map$dependencies <- c(map$dependencies, crosstalk::crosstalkLibs()) + sd <- data + data <- sd$data() + list( + ctKey = sd$key(), + ctGroup = sd$groupName() + ) + } else { + NULL + } + + args = c(evalFormula(list(...), data), list(crosstalkOptions)) dispatch(map, method, diff --git a/inst/htmlwidgets/leaflet.js b/inst/htmlwidgets/leaflet.js index 47dbaeb4c..8ee03906c 100644 --- a/inst/htmlwidgets/leaflet.js +++ b/inst/htmlwidgets/leaflet.js @@ -208,7 +208,7 @@ var DataFrame = function () { } }, { key: "get", - value: function get(row, col) { + value: function get(row, col, missingOK) { var _this3 = this; if (row > this.effectiveLength) throw new Error("Row argument was out of bounds: " + row + " > " + this.effectiveLength); @@ -231,7 +231,9 @@ var DataFrame = function () { } else if (typeof col === "number") { colIndex = col; } - if (colIndex < 0 || colIndex > this.columns.length) throw new Error("Unknown column index: " + col); + if (colIndex < 0 || colIndex > this.columns.length) { + if (missingOK) return void 0;else throw new Error("Unknown column index: " + col); + } return this.columns[colIndex][row % this.columns[colIndex].length]; } @@ -582,6 +584,7 @@ if (_htmlwidgets2.default.shinyMode) { },{"./control-store":2,"./fixup-default-icon":4,"./global/htmlwidgets":5,"./global/jquery":6,"./global/leaflet":7,"./global/shiny":8,"./layer-manager":10,"./methods":11,"./util":13}],10:[function(require,module,exports){ +(function (global){ "use strict"; Object.defineProperty(exports, "__esModule", { @@ -626,6 +629,8 @@ var LayerManager = function () { // } // } this._byStamp = {}; + // {: {: [, , ...], ...}} + this._byCrosstalkGroup = {}; // END layer indices @@ -637,7 +642,9 @@ var LayerManager = function () { _createClass(LayerManager, [{ key: "addLayer", - value: function addLayer(layer, category, layerId, group) { + value: function addLayer(layer, category, layerId, group, ctGroup, ctKey) { + var _this = this; + // Was a group provided? var hasId = typeof layerId === "string"; var grouped = typeof group === "string"; @@ -680,19 +687,151 @@ var LayerManager = function () { this._byCategory[category][stamp] = layer; // Update stamp index - this._byStamp[stamp] = { + var layerInfo = this._byStamp[stamp] = { layer: layer, group: group, + ctGroup: ctGroup, + ctKey: ctKey, layerId: layerId, category: category, - container: container + container: container, + hidden: false }; + // Update crosstalk group index + if (ctGroup) { + (function () { + if (layer.setStyle) { + // Need to save this info so we know what to set opacity to later + layer.options.origOpacity = typeof layer.options.opacity !== "undefined" ? layer.options.opacity : 0.5; + layer.options.origFillOpacity = typeof layer.options.fillOpacity !== "undefined" ? layer.options.fillOpacity : 0.2; + layer.options.origColor = typeof layer.options.color !== "undefined" ? layer.options.color : "#03F"; + layer.options.origFillColor = typeof layer.options.fillColor !== "undefined" ? layer.options.fillColor : layer.options.origColor; + } + + var ctg = _this._byCrosstalkGroup[ctGroup]; + if (!ctg) { + (function () { + ctg = _this._byCrosstalkGroup[ctGroup] = {}; + var crosstalk = global.crosstalk; + var fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + + var handleFilter = function handleFilter(e) { + if (!e.value) { + var groupKeys = Object.keys(ctg); + for (var i = 0; i < groupKeys.length; i++) { + var key = groupKeys[i]; + var _layerInfo = _this._byStamp[ctg[key]]; + _this._setVisibility(_layerInfo, true); + } + } else { + var selectedKeys = {}; + for (var _i = 0; _i < e.value.length; _i++) { + selectedKeys[e.value[_i]] = true; + } + var _groupKeys = Object.keys(ctg); + for (var _i2 = 0; _i2 < _groupKeys.length; _i2++) { + var _key = _groupKeys[_i2]; + var _layerInfo2 = _this._byStamp[ctg[_key]]; + _this._setVisibility(_layerInfo2, selectedKeys[_groupKeys[_i2]]); + } + } + }; + fs.on("change", handleFilter); + + var handleSelection = function handleSelection(e) { + if (!e.value || !e.value.length) { + var groupKeys = Object.keys(ctg); + for (var i = 0; i < groupKeys.length; i++) { + var key = groupKeys[i]; + var _layerInfo3 = _this._byStamp[ctg[key]]; + // reset the crosstalk style params + _layerInfo3.layer.options.ctOpacity = undefined; + _layerInfo3.layer.options.ctFillOpacity = undefined; + _layerInfo3.layer.options.ctColor = undefined; + _layerInfo3.layer.options.ctFillColor = undefined; + _this._setStyle(_layerInfo3); + } + } else { + var selectedKeys = {}; + for (var _i3 = 0; _i3 < e.value.length; _i3++) { + var val = e.value[_i3]; + // support 2D arrays (nested keys) + if (Array.isArray(val)) { + for (var j = 0; j < val.length; j++) { + selectedKeys[val[j]] = true; + } + } + selectedKeys[val] = true; + } + var _groupKeys2 = Object.keys(ctg); + // for compatability with plotly's ability to colour selections + // https://github.com/jcheng5/plotly/blob/71cf8a/R/crosstalk.R#L96-L100 + var selectionColour = crosstalk.var("plotlySelectionColour").get(); + var ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || { opacityDim: 0.2 }; + var persist = ctOpts.persistent === true; + for (var _i4 = 0; _i4 < _groupKeys2.length; _i4++) { + var _key2 = _groupKeys2[_i4]; + var _layerInfo4 = _this._byStamp[ctg[_key2]]; + var selected = selectedKeys[_groupKeys2[_i4]]; + var opts = _layerInfo4.layer.options; + + // remember "old" selection colors if this is persistent selection + _layerInfo4.layer.options.ctColor = selected ? selectionColour : persist ? opts.ctColor : opts.origColor; + _layerInfo4.layer.options.ctFillColor = selected ? selectionColour : persist ? opts.ctFillColor : opts.origFillColor; + + _layerInfo4.layer.options.ctOpacity = selected ? opts.origOpacity : persist && opts.origOpacity == opts.ctOpacity ? opts.origOpacity : ctOpts.opacityDim * opts.origOpacity; + + _this._setStyle(_layerInfo4); + } + } + }; + crosstalk.group(ctGroup).var("selection").on("change", handleSelection); + + setTimeout(function () { + handleFilter({ value: fs.filteredKeys }); + handleSelection({ value: crosstalk.group(ctGroup).var("selection").get() }); + }, 100); + })(); + } + + if (!ctg[ctKey]) ctg[ctKey] = []; + ctg[ctKey].push(stamp); + })(); + } + // Add to container - container.addLayer(layer); + if (!layerInfo.hidden) container.addLayer(layer); return oldLayer; } + }, { + key: "_setVisibility", + value: function _setVisibility(layerInfo, visible) { + if (layerInfo.hidden ^ visible) { + return; + } else if (visible) { + layerInfo.container.addLayer(layerInfo.layer); + layerInfo.hidden = false; + } else { + layerInfo.container.removeLayer(layerInfo.layer); + layerInfo.hidden = true; + } + } + }, { + key: "_setStyle", + value: function _setStyle(layerInfo) { + var opts = layerInfo.layer.options; + if (!layerInfo.layer.setStyle) { + return; + } + layerInfo.layer.setStyle({ + opacity: opts.ctOpacity || opts.origOpacity, + fillOpacity: opts.ctFillOpacity || opts.origFillOpacity, + color: opts.ctColor || opts.origColor, + fillColor: opts.ctFillColor || opts.origFillColor + }); + } }, { key: "getLayer", value: function getLayer(category, layerId) { @@ -701,20 +840,20 @@ var LayerManager = function () { }, { key: "removeLayer", value: function removeLayer(category, layerIds) { - var _this = this; + var _this2 = this; // Find layer info _jquery2.default.each((0, _util.asArray)(layerIds), function (i, layerId) { - var layer = _this._byLayerId[_this._layerIdKey(category, layerId)]; + var layer = _this2._byLayerId[_this2._layerIdKey(category, layerId)]; if (layer) { - _this._removeLayer(layer); + _this2._removeLayer(layer); } }); } }, { key: "clearLayers", value: function clearLayers(category) { - var _this2 = this; + var _this3 = this; // Find all layers in _byCategory[category] var catTable = this._byCategory[category]; @@ -729,7 +868,7 @@ var LayerManager = function () { stamps.push(k); }); _jquery2.default.each(stamps, function (i, stamp) { - _this2._removeLayer(stamp); + _this3._removeLayer(stamp); }); } }, { @@ -752,11 +891,11 @@ var LayerManager = function () { }, { key: "getVisibleGroups", value: function getVisibleGroups() { - var _this3 = this; + var _this4 = this; var result = []; _jquery2.default.each(this._groupContainers, function (k, v) { - if (_this3._map.hasLayer(v)) { + if (_this4._map.hasLayer(v)) { result.push(k); } }); @@ -765,7 +904,7 @@ var LayerManager = function () { }, { key: "clearGroup", value: function clearGroup(group) { - var _this4 = this; + var _this5 = this; // Find all layers in _byGroup[group] var groupTable = this._byGroup[group]; @@ -780,7 +919,7 @@ var LayerManager = function () { stamps.push(k); }); _jquery2.default.each(stamps, function (i, stamp) { - _this4._removeLayer(stamp); + _this5._removeLayer(stamp); }); } }, { @@ -794,6 +933,7 @@ var LayerManager = function () { this._byCategory = {}; this._byLayerId = {}; this._byStamp = {}; + this._byCrosstalkGroup = {}; _jquery2.default.each(this._categoryContainers, clearLayerGroup); this._categoryContainers = {}; _jquery2.default.each(this._groupContainers, clearLayerGroup); @@ -823,6 +963,18 @@ var LayerManager = function () { } delete this._byCategory[layerInfo.category][stamp]; delete this._byStamp[stamp]; + if (layerInfo.ctGroup) { + var ctGroup = this._byCrosstalkGroup[layerInfo.ctGroup]; + var layersForKey = ctGroup[layerInfo.ctKey]; + var idx = layersForKey ? layersForKey.indexOf(stamp) : -1; + if (idx >= 0) { + if (layersForKey.length === 1) { + delete ctGroup[layerInfo.ctKey]; + } else { + layersForKey.splice(idx, 1); + } + } + } } }, { key: "_layerIdKey", @@ -837,6 +989,7 @@ var LayerManager = function () { exports.default = LayerManager; +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) },{"./global/jquery":6,"./global/leaflet":7,"./util":13}],11:[function(require,module,exports){ (function (global){ "use strict"; @@ -916,17 +1069,17 @@ methods.setMaxBounds = function (lat1, lng1, lat2, lng2) { this.setMaxBounds([[lat1, lng1], [lat2, lng2]]); }; -methods.addPopups = function (lat, lng, popup, layerId, group, options) { +methods.addPopups = function (lat, lng, popup, layerId, group, options, crosstalkOptions) { var _this2 = this; - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("popup", popup).col("layerId", layerId).col("group", group).cbind(options); + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("popup", popup).col("layerId", layerId).col("group", group).cbind(options).cbind(crosstalkOptions || {}); var _loop = function _loop(i) { (function () { var popup = _leaflet2.default.popup(df.get(i)).setLatLng([df.get(i, "lat"), df.get(i, "lng")]).setContent(df.get(i, "popup")); var thisId = df.get(i, "layerId"); var thisGroup = df.get(i, "group"); - this.layerManager.addLayer(popup, "popup", thisId, thisGroup); + this.layerManager.addLayer(popup, "popup", thisId, thisGroup, df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); }).call(_this2); }; @@ -980,6 +1133,7 @@ function unpackStrings(iconset) { } function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { + (function () { var _this3 = this; @@ -994,17 +1148,18 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { var _loop2 = function _loop2(i) { (function () { var marker = markerFunc(df, i); - var thisId = df.get(i, "layerId"); - var thisGroup = cluster ? null : df.get(i, "group"); + var row = df.get(i); + var thisId = row.layerId; + var thisGroup = cluster ? null : row.group; if (cluster) { clusterGroup.clusterLayerStore.add(marker, thisId); } else { - this.layerManager.addLayer(marker, "marker", thisId, thisGroup); + this.layerManager.addLayer(marker, "marker", thisId, thisGroup, row.ctGroup, row.ctKey); } - var popup = df.get(i, "popup"); + var popup = row.popup; if (popup !== null) marker.bindPopup(popup); - var label = df.get(i, "label"); - var labelOptions = df.get(i, "labelOptions"); + var label = row.label; + var labelOptions = row.labelOptions; if (label !== null) { if (labelOptions !== null) { if (labelOptions.noHide) { @@ -1032,7 +1187,7 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { }).call(map); } -methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions) { +methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions, crosstalkOptions) { var icondf = void 0; var getIcon = void 0; @@ -1077,7 +1232,7 @@ methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, c }; } - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -1088,7 +1243,7 @@ methods.addMarkers = function (lat, lng, icon, layerId, group, options, popup, c }); }; -methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions) { +methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, popup, clusterOptions, clusterId, label, labelOptions, crosstalkOptions) { var icondf = void 0; var getIcon = void 0; if (icon) { @@ -1108,7 +1263,7 @@ methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, p }; } - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -1116,7 +1271,7 @@ methods.addAwesomeMarkers = function (lat, lng, icon, layerId, group, options, p var options = df.get(i); if (icon) options.icon = getIcon(i); return _leaflet2.default.marker([df.get(i, "lat"), df.get(i, "lng")], options); - }); + }, crosstalkOptions); }; function addLayers(map, category, df, layerFunc) { @@ -1125,7 +1280,7 @@ function addLayers(map, category, df, layerFunc) { var layer = layerFunc(df, i); var thisId = df.get(i, "layerId"); var thisGroup = df.get(i, "group"); - this.layerManager.addLayer(layer, category, thisId, thisGroup); + this.layerManager.addLayer(layer, category, thisId, thisGroup, df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); if (layer.bindPopup) { var popup = df.get(i, "popup"); if (popup !== null) layer.bindPopup(popup); @@ -1152,16 +1307,16 @@ function addLayers(map, category, df, layerFunc) { } } -methods.addCircles = function (lat, lng, radius, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addCircles = function (lat, lng, radius, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { return _leaflet2.default.circle([df.get(i, "lat"), df.get(i, "lng")], df.get(i, "radius"), df.get(i)); }); }; -methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions) { - var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("lat", lat).col("lng", lng).col("radius", radius).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addMarkers(this, df, group, clusterOptions, clusterId, function (df, i) { return _leaflet2.default.circleMarker([df.get(i, "lat"), df.get(i, "lng")], df.get(i)); @@ -1172,8 +1327,8 @@ methods.addCircleMarkers = function (lat, lng, radius, layerId, group, options, * @param lat Array of arrays of latitude coordinates for polylines * @param lng Array of arrays of longitude coordinates for polylines */ -methods.addPolylines = function (polygons, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addPolylines = function (polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { var shape = df.get(i, "shapes")[0]; @@ -1212,8 +1367,8 @@ methods.clearShapes = function () { this.layerManager.clearLayers("shape"); }; -methods.addRectangles = function (lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("lat1", lat1).col("lng1", lng1).col("lat2", lat2).col("lng2", lng2).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addRectangles = function (lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("lat1", lat1).col("lng1", lng1).col("lat2", lat2).col("lng2", lng2).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { return _leaflet2.default.rectangle([[df.get(i, "lat1"), df.get(i, "lng1")], [df.get(i, "lat2"), df.get(i, "lng2")]], df.get(i)); @@ -1224,8 +1379,8 @@ methods.addRectangles = function (lat1, lng1, lat2, lng2, layerId, group, option * @param lat Array of arrays of latitude coordinates for polygons * @param lng Array of arrays of longitude coordinates for polygons */ -methods.addPolygons = function (polygons, layerId, group, options, popup, label, labelOptions) { - var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options); +methods.addPolygons = function (polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { + var df = new _dataframe2.default().col("shapes", polygons).col("layerId", layerId).col("group", group).col("popup", popup).col("label", label).col("labelOptions", labelOptions).cbind(options).cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function (df, i) { var shapes = df.get(i, "shapes"); @@ -1846,7 +2001,6 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons // pixel of the original image has some contribution to the downscaled image) // as opposed to a single-step downscaling which will discard a lot of data // (and with sparse images at small scales can give very surprising results). - var Mipmapper = function () { function Mipmapper(img) { _classCallCheck(this, Mipmapper); diff --git a/javascript/src/dataframe.js b/javascript/src/dataframe.js index c56cde367..7272d9cee 100644 --- a/javascript/src/dataframe.js +++ b/javascript/src/dataframe.js @@ -62,7 +62,7 @@ export default class DataFrame { return this; } - get(row, col) { + get(row, col, missingOK) { if (row > this.effectiveLength) throw new Error("Row argument was out of bounds: " + row + " > " + this.effectiveLength); @@ -78,8 +78,12 @@ export default class DataFrame { } else if (typeof(col) === "number") { colIndex = col; } - if (colIndex < 0 || colIndex > this.columns.length) - throw new Error("Unknown column index: " + col); + if (colIndex < 0 || colIndex > this.columns.length) { + if (missingOK) + return void(0); + else + throw new Error("Unknown column index: " + col); + } return this.columns[colIndex][row % this.columns[colIndex].length]; } diff --git a/javascript/src/layer-manager.js b/javascript/src/layer-manager.js index a773c34d4..cb6c7ce9a 100644 --- a/javascript/src/layer-manager.js +++ b/javascript/src/layer-manager.js @@ -23,6 +23,8 @@ export default class LayerManager { // } // } this._byStamp = {}; + // {: {: [, , ...], ...}} + this._byCrosstalkGroup = {}; // END layer indices @@ -32,7 +34,7 @@ export default class LayerManager { this._groupContainers = {}; } - addLayer(layer, category, layerId, group) { + addLayer(layer, category, layerId, group, ctGroup, ctKey) { // Was a group provided? let hasId = typeof(layerId) === "string"; let grouped = typeof(group) === "string"; @@ -76,20 +78,155 @@ export default class LayerManager { this._byCategory[category][stamp] = layer; // Update stamp index - this._byStamp[stamp] = { + let layerInfo = this._byStamp[stamp] = { layer: layer, group: group, + ctGroup: ctGroup, + ctKey: ctKey, layerId: layerId, category: category, - container: container + container: container, + hidden: false }; + // Update crosstalk group index + if (ctGroup) { + if (layer.setStyle) { + // Need to save this info so we know what to set opacity to later + layer.options.origOpacity = typeof(layer.options.opacity) !== "undefined" ? layer.options.opacity : 0.5; + layer.options.origFillOpacity = typeof(layer.options.fillOpacity) !== "undefined" ? layer.options.fillOpacity : 0.2; + layer.options.origColor = typeof(layer.options.color) !== "undefined" ? layer.options.color : "#03F"; + layer.options.origFillColor = typeof(layer.options.fillColor) !== "undefined" ? layer.options.fillColor : layer.options.origColor; + } + + let ctg = this._byCrosstalkGroup[ctGroup]; + if (!ctg) { + ctg = this._byCrosstalkGroup[ctGroup] = {}; + let crosstalk = global.crosstalk; + let fs = crosstalk.filter.createHandle(crosstalk.group(ctGroup)); + + let handleFilter = (e) => { + if (!e.value) { + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setVisibility(layerInfo, true); + } + } else { + let selectedKeys = {}; + for (let i = 0; i < e.value.length; i++) { + selectedKeys[e.value[i]] = true; + } + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + this._setVisibility(layerInfo, selectedKeys[groupKeys[i]]); + } + } + }; + fs.on("change", handleFilter); + + let handleSelection = (e) => { + if (!e.value || !e.value.length) { + let groupKeys = Object.keys(ctg); + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + // reset the crosstalk style params + layerInfo.layer.options.ctOpacity = undefined; + layerInfo.layer.options.ctFillOpacity = undefined; + layerInfo.layer.options.ctColor = undefined; + layerInfo.layer.options.ctFillColor = undefined; + this._setStyle(layerInfo); + } + } else { + let selectedKeys = {}; + for (let i = 0; i < e.value.length; i++) { + let val = e.value[i]; + // support 2D arrays (nested keys) + if (Array.isArray(val)) { + for (let j = 0; j < val.length; j++) { + selectedKeys[val[j]] = true; + } + } + selectedKeys[val] = true; + } + let groupKeys = Object.keys(ctg); + // for compatability with plotly's ability to colour selections + // https://github.com/jcheng5/plotly/blob/71cf8a/R/crosstalk.R#L96-L100 + let selectionColour = crosstalk.var("plotlySelectionColour").get(); + let ctOpts = crosstalk.var("plotlyCrosstalkOpts").get() || {opacityDim: 0.2}; + let persist = ctOpts.persistent === true; + for (let i = 0; i < groupKeys.length; i++) { + let key = groupKeys[i]; + let layerInfo = this._byStamp[ctg[key]]; + let selected = selectedKeys[groupKeys[i]]; + let opts = layerInfo.layer.options; + + // remember "old" selection colors if this is persistent selection + layerInfo.layer.options.ctColor = + selected ? selectionColour : + persist ? opts.ctColor : opts.origColor; + layerInfo.layer.options.ctFillColor = + selected ? selectionColour : + persist ? opts.ctFillColor : opts.origFillColor; + + layerInfo.layer.options.ctOpacity = + selected ? opts.origOpacity : + (persist && opts.origOpacity == opts.ctOpacity) ? opts.origOpacity : + ctOpts.opacityDim * opts.origOpacity; + + this._setStyle(layerInfo); + } + } + }; + crosstalk.group(ctGroup).var("selection").on("change", handleSelection); + + setTimeout(() => { + handleFilter({value: fs.filteredKeys}); + handleSelection({value: crosstalk.group(ctGroup).var("selection").get()}); + }, 100); + } + + if (!ctg[ctKey]) + ctg[ctKey] = []; + ctg[ctKey].push(stamp); + } + // Add to container - container.addLayer(layer); + if (!layerInfo.hidden) + container.addLayer(layer); return oldLayer; } + _setVisibility(layerInfo, visible) { + if (layerInfo.hidden ^ visible) { + return; + } else if (visible) { + layerInfo.container.addLayer(layerInfo.layer); + layerInfo.hidden = false; + } else { + layerInfo.container.removeLayer(layerInfo.layer); + layerInfo.hidden = true; + } + } + + _setStyle(layerInfo) { + let opts = layerInfo.layer.options; + if (!layerInfo.layer.setStyle) { + return; + } + layerInfo.layer.setStyle({ + opacity: opts.ctOpacity || opts.origOpacity, + fillOpacity: opts.ctFillOpacity || opts.origFillOpacity, + color: opts.ctColor || opts.origColor, + fillColor: opts.ctFillColor || opts.origFillColor + }); + } + getLayer(category, layerId) { return this._byLayerId[this._layerIdKey(category, layerId)]; } @@ -174,6 +311,7 @@ export default class LayerManager { this._byCategory = {}; this._byLayerId = {}; this._byStamp = {}; + this._byCrosstalkGroup = {}; $.each(this._categoryContainers, clearLayerGroup); this._categoryContainers = {}; $.each(this._groupContainers, clearLayerGroup); @@ -202,9 +340,21 @@ export default class LayerManager { } delete this._byCategory[layerInfo.category][stamp]; delete this._byStamp[stamp]; + if (layerInfo.ctGroup) { + let ctGroup = this._byCrosstalkGroup[layerInfo.ctGroup]; + let layersForKey = ctGroup[layerInfo.ctKey]; + let idx = layersForKey ? layersForKey.indexOf(stamp) : -1; + if (idx >= 0) { + if (layersForKey.length === 1) { + delete ctGroup[layerInfo.ctKey]; + } else { + layersForKey.splice(idx, 1); + } + } + } } _layerIdKey(category, layerId) { return category + "\n" + layerId; } -} \ No newline at end of file +} diff --git a/javascript/src/methods.js b/javascript/src/methods.js index 827106a80..87f2e1285 100644 --- a/javascript/src/methods.js +++ b/javascript/src/methods.js @@ -53,14 +53,15 @@ methods.setMaxBounds = function(lat1, lng1, lat2, lng2) { ]); }; -methods.addPopups = function(lat, lng, popup, layerId, group, options) { +methods.addPopups = function(lat, lng, popup, layerId, group, options, crosstalkOptions) { let df = new DataFrame() .col("lat", lat) .col("lng", lng) .col("popup", popup) .col("layerId", layerId) .col("group", group) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); for (let i = 0; i < df.nrow(); i++) { (function() { @@ -69,7 +70,8 @@ methods.addPopups = function(lat, lng, popup, layerId, group, options) { .setContent(df.get(i, "popup")); let thisId = df.get(i, "layerId"); let thisGroup = df.get(i, "group"); - this.layerManager.addLayer(popup, "popup", thisId, thisGroup); + this.layerManager.addLayer(popup, "popup", thisId, thisGroup, + df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); }).call(this); } }; @@ -119,6 +121,7 @@ function unpackStrings(iconset) { } function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { + (function() { let clusterGroup = this.layerManager.getLayer("cluster", clusterId), cluster = clusterOptions !== null; @@ -131,17 +134,18 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { for (let i = 0; i < df.nrow(); i++) { (function() { let marker = markerFunc(df, i); - let thisId = df.get(i, "layerId"); - let thisGroup = cluster ? null : df.get(i, "group"); + let row = df.get(i); + let thisId = row.layerId; + let thisGroup = cluster ? null : row.group; if (cluster) { clusterGroup.clusterLayerStore.add(marker, thisId); } else { - this.layerManager.addLayer(marker, "marker", thisId, thisGroup); + this.layerManager.addLayer(marker, "marker", thisId, thisGroup, row.ctGroup, row.ctKey); } - let popup = df.get(i, "popup"); + let popup = row.popup; if (popup !== null) marker.bindPopup(popup); - let label = df.get(i, "label"); - let labelOptions = df.get(i, "labelOptions"); + let label = row.label; + let labelOptions = row.labelOptions; if (label !== null) { if (labelOptions !== null) { if(labelOptions.noHide) { @@ -166,7 +170,8 @@ function addMarkers(map, df, group, clusterOptions, clusterId, markerFunc) { } methods.addMarkers = function(lat, lng, icon, layerId, group, options, popup, - clusterOptions, clusterId, label, labelOptions) { + clusterOptions, clusterId, label, labelOptions, + crosstalkOptions) { let icondf; let getIcon; @@ -219,7 +224,8 @@ methods.addMarkers = function(lat, lng, icon, layerId, group, options, popup, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -231,7 +237,7 @@ methods.addMarkers = function(lat, lng, icon, layerId, group, options, popup, }; methods.addAwesomeMarkers = function(lat, lng, icon, layerId, group, options, popup, -clusterOptions, clusterId, label, labelOptions) { +clusterOptions, clusterId, label, labelOptions, crosstalkOptions) { let icondf; let getIcon; if (icon) { @@ -259,7 +265,8 @@ clusterOptions, clusterId, label, labelOptions) { .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); if (icon) icondf.effectiveLength = df.nrow(); @@ -267,7 +274,7 @@ clusterOptions, clusterId, label, labelOptions) { let options = df.get(i); if (icon) options.icon = getIcon(i); return L.marker([df.get(i, "lat"), df.get(i, "lng")], options); - }); + }, crosstalkOptions); }; function addLayers(map, category, df, layerFunc) { @@ -276,7 +283,8 @@ function addLayers(map, category, df, layerFunc) { let layer = layerFunc(df, i); let thisId = df.get(i, "layerId"); let thisGroup = df.get(i, "group"); - this.layerManager.addLayer(layer, category, thisId, thisGroup); + this.layerManager.addLayer(layer, category, thisId, thisGroup, + df.get(i, "ctGroup", true), df.get(i, "ctKey", true)); if (layer.bindPopup) { let popup = df.get(i, "popup"); if (popup !== null) layer.bindPopup(popup); @@ -299,7 +307,7 @@ function addLayers(map, category, df, layerFunc) { } } -methods.addCircles = function(lat, lng, radius, layerId, group, options, popup, label, labelOptions) { +methods.addCircles = function(lat, lng, radius, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("lat", lat) .col("lng", lng) @@ -309,14 +317,15 @@ methods.addCircles = function(lat, lng, radius, layerId, group, options, popup, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { return L.circle([df.get(i, "lat"), df.get(i, "lng")], df.get(i, "radius"), df.get(i)); }); }; -methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions) { +methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, clusterOptions, clusterId, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("lat", lat) .col("lng", lng) @@ -326,7 +335,8 @@ methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, c .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addMarkers(this, df, group, clusterOptions, clusterId, function(df, i) { return L.circleMarker([df.get(i, "lat"), df.get(i, "lng")], df.get(i)); @@ -337,7 +347,7 @@ methods.addCircleMarkers = function(lat, lng, radius, layerId, group, options, c * @param lat Array of arrays of latitude coordinates for polylines * @param lng Array of arrays of longitude coordinates for polylines */ -methods.addPolylines = function(polygons, layerId, group, options, popup, label, labelOptions) { +methods.addPolylines = function(polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("shapes", polygons) .col("layerId", layerId) @@ -345,7 +355,8 @@ methods.addPolylines = function(polygons, layerId, group, options, popup, label, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { let shape = df.get(i, "shapes")[0]; @@ -384,7 +395,7 @@ methods.clearShapes = function() { this.layerManager.clearLayers("shape"); }; -methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions) { +methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("lat1", lat1) .col("lng1", lng1) @@ -395,7 +406,8 @@ methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { return L.rectangle( @@ -411,7 +423,7 @@ methods.addRectangles = function(lat1, lng1, lat2, lng2, layerId, group, options * @param lat Array of arrays of latitude coordinates for polygons * @param lng Array of arrays of longitude coordinates for polygons */ -methods.addPolygons = function(polygons, layerId, group, options, popup, label, labelOptions) { +methods.addPolygons = function(polygons, layerId, group, options, popup, label, labelOptions, crosstalkOptions) { let df = new DataFrame() .col("shapes", polygons) .col("layerId", layerId) @@ -419,7 +431,8 @@ methods.addPolygons = function(polygons, layerId, group, options, popup, label, .col("popup", popup) .col("label", label) .col("labelOptions", labelOptions) - .cbind(options); + .cbind(options) + .cbind(crosstalkOptions || {}); addLayers(this, "shape", df, function(df, i) { let shapes = df.get(i, "shapes"); diff --git a/tests/testit/test-remote.R b/tests/testit/test-remote.R index 5e8b6557f..019d22fc8 100644 --- a/tests/testit/test-remote.R +++ b/tests/testit/test-remote.R @@ -151,7 +151,7 @@ assert( ) mockSession$.flush() expected <- list( - list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addPolygons\",\"args\":[[[{\"lng\":[1,2,3,4,5],\"lat\":[1,2,3,4,5]}]],null,null,{\"lineCap\":null,\"lineJoin\":null,\"clickable\":true,\"pointerEvents\":null,\"className\":\"\",\"stroke\":true,\"color\":\"#03F\",\"weight\":5,\"opacity\":0.5,\"fill\":true,\"fillColor\":\"#03F\",\"fillOpacity\":0.2,\"dashArray\":null,\"smoothFactor\":1,\"noClip\":false},null,null,null]}]}", class = "json")) + list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addPolygons\",\"args\":[[[{\"lng\":[1,2,3,4,5],\"lat\":[1,2,3,4,5]}]],null,null,{\"lineCap\":null,\"lineJoin\":null,\"clickable\":true,\"pointerEvents\":null,\"className\":\"\",\"stroke\":true,\"color\":\"#03F\",\"weight\":5,\"opacity\":0.5,\"fill\":true,\"fillColor\":\"#03F\",\"fillOpacity\":0.2,\"dashArray\":null,\"smoothFactor\":1,\"noClip\":false},null,null,null,null]}]}", class = "json")) ) # cat(deparse(mockSession$.calls), "\n") assert(identical(mockSession$.calls, expected)) @@ -167,7 +167,7 @@ remote2 <- leafletProxy("map", mockSession, ) # Check that addMarkers() takes effect immediately, no flush required remote2 %>% addMarkers() -expected2 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) +expected2 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) # cat(deparse(mockSession$.calls), "\n") assert(identical(mockSession$.calls, expected2)) # Flushing should do nothing @@ -184,6 +184,6 @@ remote3 <- leafletProxy("map", mockSession, remote3 %>% clearShapes() %>% addMarkers() assert(identical(mockSession$.calls, list())) mockSession$.flush() -expected3 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"clearShapes\",\"args\":[]}]}", class = "json")), .Names = c("type", "message")), structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) +expected3 <- list(structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"clearShapes\",\"args\":[null]}]}", class = "json")), .Names = c("type", "message")), structure(list(type = "leaflet-calls", message = structure("{\"id\":\"map\",\"calls\":[{\"dependencies\":[],\"method\":\"addMarkers\",\"args\":[[10,9,8,7,6,5,4,3,2,1],[10,9,8,7,6,5,4,3,2,1],null,null,null,{\"clickable\":true,\"draggable\":false,\"keyboard\":true,\"title\":\"\",\"alt\":\"\",\"zIndexOffset\":0,\"opacity\":1,\"riseOnHover\":false,\"riseOffset\":250},null,null,null,null,null,null]}]}", class = "json")), .Names = c("type", "message"))) # Check that multiple calls are invoked in order assert(identical(mockSession$.calls, expected3))