From 1085c6f826e759f219270ed18ed8fd9ba299ca5a Mon Sep 17 00:00:00 2001 From: Bhaskar Karambelkar Date: Fri, 3 Feb 2017 13:43:02 -0500 Subject: [PATCH] Support for Leaflet.heat plugin. Fixes #1. Also fixed gradient in webgl heatmap. --- NAMESPACE | 7 + R/heatmap.R | 220 ++++++++++++++++++ R/webGLHeatmap.R | 13 +- inst/examples/heatmaps.R | 41 ++-- inst/examples/webglHeatmaps.R | 104 +++++++++ inst/htmlwidgets/lib/heat/LICENSE | 22 ++ inst/htmlwidgets/lib/heat/heat-bindings.js | 181 ++++++++++++++ inst/htmlwidgets/lib/heat/leaflet-heat.js | 1 + inst/htmlwidgets/lib/heat/leaflet-heat2.js | 1 + inst/htmlwidgets/lib/heat/package.json | 54 +++++ .../lib/webgl-heatmap/deep-sea-gradient.png | Bin 0 -> 3403 bytes .../lib/webgl-heatmap/skyline-gradient.png | Bin 0 -> 3452 bytes .../webgl-heatmap/webgl-heatmap-bindings.js | 35 ++- man/heatmap.Rd | 115 ++++++++- 14 files changed, 759 insertions(+), 35 deletions(-) create mode 100644 R/heatmap.R create mode 100644 inst/examples/webglHeatmaps.R create mode 100644 inst/htmlwidgets/lib/heat/LICENSE create mode 100644 inst/htmlwidgets/lib/heat/heat-bindings.js create mode 100644 inst/htmlwidgets/lib/heat/leaflet-heat.js create mode 100644 inst/htmlwidgets/lib/heat/leaflet-heat2.js create mode 100644 inst/htmlwidgets/lib/heat/package.json create mode 100644 inst/htmlwidgets/lib/webgl-heatmap/deep-sea-gradient.png create mode 100644 inst/htmlwidgets/lib/webgl-heatmap/skyline-gradient.png diff --git a/NAMESPACE b/NAMESPACE index 5082c824..df9b14b2 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,17 +6,22 @@ export(activateGPS) export(addAwesomeMarkersDependencies) export(addBootstrapDependency) export(addCSV) +export(addCSVHeatmap) export(addControlGPS) export(addDrawToolbar) export(addFullscreenControl) export(addGPX) +export(addGPXHeatmap) export(addGeoJSONChoropleth) +export(addGeoJSONHeatmap) export(addGeoJSONv2) export(addGeodesicPolylines) export(addGreatCircles) export(addHash) +export(addHeatmap) export(addKML) export(addKMLChoropleth) +export(addKMLHeatmap) export(addMeasurePathToolbar) export(addPulseMarkers) export(addSearchMarker) @@ -29,6 +34,7 @@ export(addWebGLGPXHeatmap) export(addWebGLGeoJSONHeatmap) export(addWebGLHeatmap) export(addWebGLKMLHeatmap) +export(clearHeatmap) export(clearWebGLHeatmap) export(csvParserOptions) export(deactivateGPS) @@ -53,6 +59,7 @@ export(pulseIconList) export(pulseIcons) export(removeControlGPS) export(removeDrawToolbar) +export(removeHeatmap) export(removeStyleEditor) export(removeWebGLHeatmap) export(searchMarkersOptions) diff --git a/R/heatmap.R b/R/heatmap.R new file mode 100644 index 00000000..46283d55 --- /dev/null +++ b/R/heatmap.R @@ -0,0 +1,220 @@ + +# Source https://github.com/Leaflet/Leaflet.heat +heatmapDependency <- function() { + list( + htmltools::htmlDependency( + "Leaflet.heat",version = "0.1.0", + system.file("htmlwidgets/lib/heat", package = "leaflet.extras"), + script = c("leaflet-heat.js", "heat-bindings.js") + ) + ) +} + +#' Add a heatmap +#' @param map the map widget. +#' @param lng a numeric vector of longitudes, or a one-sided formula of the form +#' \code{~x} where \code{x} is a variable in \code{data}; by default (if not +#' explicitly provided), it will be automatically inferred from \code{data} by +#' looking for a column named \code{lng}, \code{long}, or \code{longitude} +#' (case-insensitively) +#' @param lat a vector of latitudes or a formula (similar to the \code{lng} +#' argument; the names \code{lat} and \code{latitude} are used when guessing +#' the latitude column from \code{data}) +#' @param intensity intensity of the heat. A vector of numeric values or a formula. +#' @param minOpacity minimum opacity at which the heat will start +#' @param max maximum point intensity. The default is \code{1.0} +#' @param radius radius of each "point" of the heatmap. The default is +#' \code{25}. +#' @param blur amount of blur to apply. The default is \code{15}. +#' \code{blur=1} means no blur. +#' @param gradient palette name from \code{RColorBrewer} or an array of +#' of colors to be provided to \code{\link{colorNumeric}}, or +#' a color mapping function returned from \code{colorNumeric} +#' @param cellSize the cell size in the grid. Points which are closer +#' than this may be merged. Defaults to `radius / 2`.s +#' Set to `1` to do almost no merging. +#' @param layerId the layer id +#' @param group the name of the group the newly created layers should belong to +#' (for \code{\link{clearGroup}} and \code{\link{addLayersControl}} purposes). +#' Human-friendly group names are permitted--they need not be short, +#' identifier-style names. Any number of layers and even different types of +#' layers (e.g. markers and polygons) can share the same group name. +#' @param data the data object from which the argument values are derived; by +#' default, it is the \code{data} object provided to \code{leaflet()} +#' initially, but can be overridden +#' @rdname heatmap +#' @export +addHeatmap = function( + map, lng = NULL, lat = NULL, intensity = NULL, layerId = NULL, group = NULL, + minOpacity = 0.05, + max = 1.0, radius = 25, + blur = 15, gradient = NULL, cellSize = NULL, + data = leaflet::getMapData(map) +) { + map$dependencies <- c(map$dependencies, + heatmapDependency()) + + #convert gradient to expected format from leaflet + if (!is.null(gradient) && !is.function(gradient)) { + gradient <- colorNumeric( gradient, 0:1 ) + gradient <- as.list(gradient(0:20 / 20)) + names(gradient) <- as.character(0:20 / 20) + } + + pts = leaflet::derivePoints( + data, lng, lat, missing(lng), missing(lat), "addHeatmap") + + if(is.null(intensity)) { + points <- cbind(pts$lat, pts$lng) + } else { + if(inherits(intensity,'formula')) { + intensity <- eval(intensity[[2]], data, environment(intensity)) + } + points <- cbind(pts$lat, pts$lng, intensity) + } + + leaflet::invokeMethod( + map, data, 'addHeatmap', points, + layerId, group, + leaflet::filterNULL(list( + minOpacity = minOpacity, + max = max, + radius = radius, + blur = blur, + gradient = gradient, + cellSize = cellSize + )) + ) %>% leaflet::expandLimits(pts$lat, pts$lng) +} + +#' Adds a heatmap with data from a GeoJSON/TopoJSON file/url +#' @param geojson The geojson or topojson url or contents as string. +#' @param intensityProperty The property to use for determining the intensity at a point. +#' Can be a 'string' or a JS function, or NULL. +#' @rdname heatmap +#' @export +addGeoJSONHeatmap = function( + map, geojson, layerId = NULL, group = NULL, + intensityProperty = NULL, + minOpacity = 0.05, + max = 1.0, radius = 25, + blur = 15, gradient = NULL, cellSize = NULL + ) { + map$dependencies <- c(map$dependencies, omnivoreDependencies()) + map$dependencies <- c(map$dependencies, heatmapDependency()) + + leaflet::invokeMethod( + map, leaflet::getMapData(map), + 'addGeoJSONHeatmap', geojson, intensityProperty, + layerId, group, + leaflet::filterNULL(list( + minOpacity = minOpacity, + max = max, + radius = radius, + blur = blur, + gradient = gradient, + cellSize = cellSize + ))) +} + +#' Adds a heatmap with data from a KML file/url +#' @param kml The KML url or contents as string. +#' @rdname heatmap +#' @export +addKMLHeatmap = function( + map, kml, layerId = NULL, group = NULL, + intensityProperty = NULL, + minOpacity = 0.05, + max = 1.0, radius = 25, + blur = 15, gradient = NULL, cellSize = NULL + ) { + map$dependencies <- c(map$dependencies, omnivoreDependencies()) + map$dependencies <- c(map$dependencies, heatmapDependency()) + + leaflet::invokeMethod( + map, leaflet::getMapData(map), + 'addKMLHeatmap', kml, intensityProperty, + layerId, group, + leaflet::filterNULL(list( + minOpacity = minOpacity, + max = max, + radius = radius, + blur = blur, + gradient = gradient, + cellSize = cellSize + ))) +} + +#' Adds a heatmap with data from a CSV file/url +#' @param csv The CSV url or contents as string. +#' @param csvParserOptions options for parsing the CSV. +#' Use \code{\link{csvParserOptions}}() to supply csv parser options. +#' @rdname heatmap +#' @export +addCSVHeatmap = function( + map, csv, csvParserOptions, layerId = NULL, group = NULL, + intensityProperty = NULL, + minOpacity = 0.05, + max = 1.0, radius = 25, + blur = 15, gradient = NULL, cellSize = NULL + ) { + map$dependencies <- c(map$dependencies, omnivoreDependencies()) + map$dependencies <- c(map$dependencies, heatmapDependency()) + + leaflet::invokeMethod( + map, leaflet::getMapData(map), + 'addCSVHeatmap', csv, intensityProperty, + layerId, group, + leaflet::filterNULL(list( + minOpacity = minOpacity, + max = max, + radius = radius, + blur = blur, + gradient = gradient, + cellSize = cellSize + )), + csvParserOptions) +} + +#' Adds a heatmap with data from a GPX file/url +#' @param gpx The GPX url or contents as string. +#' @rdname heatmap +#' @export +addGPXHeatmap = function( + map, gpx, layerId = NULL, group = NULL, + intensityProperty = NULL, + minOpacity = 0.05, + max = 1.0, radius = 25, + blur = 15, gradient = NULL, cellSize = NULL + ) { + map$dependencies <- c(map$dependencies, omnivoreDependencies()) + map$dependencies <- c(map$dependencies, heatmapDependency()) + + leaflet::invokeMethod( + map, leaflet::getMapData(map), + 'addGPXHeatmap', gpx, intensityProperty, + layerId, group, + leaflet::filterNULL(list( + minOpacity = minOpacity, + max = max, + radius = radius, + blur = blur, + gradient = gradient, + cellSize = cellSize + ))) +} + + +#' removes the heatmap +#' @rdname heatmap +#' @export +removeHeatmap = function(map, layerId) { + leaflet::invokeMethod(map, leaflet::getMapData(map), 'removeHeatmap', layerId) +} + +#' clears the heatmap +#' @rdname heatmap +#' @export +clearHeatmap = function(map) { + leaflet::invokeMethod(map, NULL, 'clearHeatmap') +} diff --git a/R/webGLHeatmap.R b/R/webGLHeatmap.R index c9e4ca2e..28ce3335 100644 --- a/R/webGLHeatmap.R +++ b/R/webGLHeatmap.R @@ -5,7 +5,10 @@ webGLHeatmapDependency <- function() { htmltools::htmlDependency( "webgl-heatmap",version = "0.1.0", system.file("htmlwidgets/lib/webgl-heatmap", package = "leaflet.extras"), - script = c("webgl-heatmap.js", "webgl-heatmap-leaflet.js", "webgl-heatmap-bindings.js") + script = c("webgl-heatmap.js", "webgl-heatmap-leaflet.js", + "webgl-heatmap-bindings.js"), + attachment = c("skyline" = "skyline-gradient.png", + "deep-sea" = "deep-sea-gradient.png") ) ) } @@ -30,7 +33,8 @@ webGLHeatmapDependency <- function() { #' @param size in meters or pixels #' @param units either 'm' or 'px' #' @param opacity for the canvas element -#' @param gradientTexture image url or image +#' @param gradientTexture Alternative colors for heatmap. +#' allowed values are "skyline", "deep-sea" #' @param alphaRange adjust transparency by changing to value between 0 and 1 #' @param data the data object from which the argument values are derived; by #' default, it is the \code{data} object provided to \code{leaflet()} @@ -49,6 +53,11 @@ addWebGLHeatmap = function( map$dependencies <- c(map$dependencies, webGLHeatmapDependency()) + if(!is.null(gradientTexture) && + !gradientTexture %in% c("skyline", "deep-sea")) { + stop("Only allowed values for gradientTexture are 'skyline' and 'deep-sea'") + } + pts = leaflet::derivePoints( data, lng, lat, missing(lng), missing(lat), "addWebGLHeatmap") diff --git a/inst/examples/heatmaps.R b/inst/examples/heatmaps.R index e9c37efd..2bf50e43 100644 --- a/inst/examples/heatmaps.R +++ b/inst/examples/heatmaps.R @@ -1,18 +1,13 @@ library(leaflet.extras) library(magrittr) -#' Just by number of quakes +#' Quakes #' #' leaflet(quakes) %>% addProviderTiles(providers$CartoDB.DarkMatter) %>% - addWebGLHeatmap(lng=~long, lat=~lat, size = 60000) - - -#'

By magnitude -#' -#' -leaflet(quakes) %>% addProviderTiles(providers$CartoDB.DarkMatter) %>% - addWebGLHeatmap(lng=~long, lat=~lat, intensity = ~mag, size=60000) + setView( 178, -20, 5 ) %>% + addHeatmap(lng = ~long, lat = ~lat, intensity = ~mag, + blur = 20, max = 0.05, radius = 15) #'

#' Roughly 1500 points dataset @@ -24,19 +19,17 @@ v8$source(jsURL) geoJson <- geojsonio::as.json(v8$get('pubsGeoJSON')) spdf <- geojsonio::geojson_sp(geoJson) -#'

Size in meters #' #' leaflet(spdf) %>% addProviderTiles(providers$Thunderforest.TransportDark) %>% - addWebGLHeatmap(size=60000) + addHeatmap(blur = 20, max = 0.05, radius = 15) -#'

Size in Pixels #' #' leaflet(spdf) %>% - addProviderTiles(providers$Thunderforest.TransportDark) %>% - addWebGLHeatmap(size=25,units='px') + addProviderTiles(providers$Thunderforest.Transport) %>% + addHeatmap(blur = 20, max = 0.05, radius = 15, gradient = 'Greys') #'

10,000 points #' @@ -46,7 +39,7 @@ v8 <- V8::v8() v8$source(jsURL) df <- data.frame(v8$get('addressPoints'), stringsAsFactors = F) %>% - set_colnames(c('lat', 'lng', 'intensity')) %>% + magrittr::set_colnames(c('lat', 'lng', 'intensity')) %>% dplyr::mutate( lat = as.numeric(lat), lng = as.numeric(lng) @@ -57,14 +50,8 @@ df <- data.frame(v8$get('addressPoints'), stringsAsFactors = F) %>% #' leaflet(df) %>% addProviderTiles(providers$CartoDB.Positron) %>% - addWebGLHeatmap(lng=~lng, lat=~lat,size=1000) - -#'

Size in Pixels -#' -#' -leaflet(df) %>% - addProviderTiles(providers$CartoDB.Positron) %>% - addWebGLHeatmap(lng=~lng, lat=~lat,size=20,units='px') + addHeatmap(lng=~lng, lat=~lat, + blur = 20, max = 0.05, radius = 15 ) #'

@@ -77,7 +64,7 @@ london.crimes <- suppressMessages( ~readr::read_csv(.) %>% dplyr::select(Latitude, Longitude) %>% dplyr::filter(!is.na(Latitude))) %>% - set_names(basename(Sys.glob( + magrittr::set_names(basename(Sys.glob( paste0(system.file('examples/data/London-Crimes', package='leaflet.extras'), '/2016*'))))) @@ -88,11 +75,11 @@ purrr::walk( names(london.crimes), function(month) { leaf <<- leaf %>% - addWebGLHeatmap( + addHeatmap( data = london.crimes[[month]], layerId = month, group = month, - lng=~Longitude, lat=~Latitude,size=40,units='px' - ) + lng=~Longitude, lat=~Latitude, + blur = 20, max = 0.05, radius = 15) }) leaf %>% diff --git a/inst/examples/webglHeatmaps.R b/inst/examples/webglHeatmaps.R new file mode 100644 index 00000000..ea9dc950 --- /dev/null +++ b/inst/examples/webglHeatmaps.R @@ -0,0 +1,104 @@ +library(leaflet.extras) +library(magrittr) + +#' Just by number of quakes +#' +#' +leaflet(quakes) %>% addProviderTiles(providers$CartoDB.DarkMatter) %>% + addWebGLHeatmap(lng=~long, lat=~lat, size = 60000) + + +#'

By magnitude +#' +#' +leaflet(quakes) %>% addProviderTiles(providers$CartoDB.DarkMatter) %>% + addWebGLHeatmap(lng=~long, lat=~lat, intensity = ~mag, size=60000) + +#'

+#' Roughly 1500 points dataset +#' +library(sp) +jsURL <- 'https://rawgit.com/Norkart/Leaflet-MiniMap/master/example/local_pubs_restaurant_norway.js' +v8 <- V8::v8() +v8$source(jsURL) +geoJson <- geojsonio::as.json(v8$get('pubsGeoJSON')) +spdf <- geojsonio::geojson_sp(geoJson) + +#'

Size in meters +#' +#' +leaflet(spdf) %>% + addProviderTiles(providers$Thunderforest.TransportDark) %>% + addWebGLHeatmap(size=60000) + +#'

Size in Pixels +#' +#' +leaflet(spdf) %>% + addProviderTiles(providers$Thunderforest.TransportDark) %>% + addWebGLHeatmap(size=25,units='px') + +#'

10,000 points +#' +#' +jsURL <- 'http://leaflet.github.io/Leaflet.markercluster/example/realworld.10000.js' +v8 <- V8::v8() +v8$source(jsURL) + +df <- data.frame(v8$get('addressPoints'), stringsAsFactors = F) %>% + set_colnames(c('lat', 'lng', 'intensity')) %>% + dplyr::mutate( + lat = as.numeric(lat), + lng = as.numeric(lng) + ) + +#'

Size in Meters +#' +#' +leaflet(df) %>% + addProviderTiles(providers$CartoDB.Positron) %>% + addWebGLHeatmap(lng=~lng, lat=~lat,size=1000) + +#'

Size in Pixels +#' +#' +leaflet(df) %>% + addProviderTiles(providers$CartoDB.Positron) %>% + addWebGLHeatmap(lng=~lng, lat=~lat,size=20,units='px') + +#'

+ +london.crimes.files <- Sys.glob( + paste0(system.file('examples/data/London-Crimes', package='leaflet.extras'), + '/*/*-city-of-london-street.csv.zip')) +london.crimes <- suppressMessages( + purrr::map( + london.crimes.files, + ~readr::read_csv(.) %>% + dplyr::select(Latitude, Longitude) %>% + dplyr::filter(!is.na(Latitude))) %>% + set_names(basename(Sys.glob( + paste0(system.file('examples/data/London-Crimes', package='leaflet.extras'), + '/2016*'))))) + +leaf <- leaflet() %>% + addProviderTiles(providers$CartoDB.Positron) + +purrr::walk( + names(london.crimes), + function(month) { + leaf <<- leaf %>% + addWebGLHeatmap( + data = london.crimes[[month]], + layerId = month, group = month, + lng=~Longitude, lat=~Latitude,size=40,units='px', + gradientTexture = 'skyline' + ) + }) + +leaf %>% + setView(-0.094106, 51.515, 14) %>% + addLayersControl( + baseGroups = names(london.crimes), + options = layersControlOptions(collapsed = FALSE) + ) diff --git a/inst/htmlwidgets/lib/heat/LICENSE b/inst/htmlwidgets/lib/heat/LICENSE new file mode 100644 index 00000000..91a7b5ed --- /dev/null +++ b/inst/htmlwidgets/lib/heat/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2014, Vladimir Agafonkin +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/inst/htmlwidgets/lib/heat/heat-bindings.js b/inst/htmlwidgets/lib/heat/heat-bindings.js new file mode 100644 index 00000000..b77bede3 --- /dev/null +++ b/inst/htmlwidgets/lib/heat/heat-bindings.js @@ -0,0 +1,181 @@ +/* global LeafletWidget, $, L, topojson, csv2geojson, toGeoJSON */ +LeafletWidget.methods.addHeatmap = function(points, layerId, group, options) { + + if(!$.isEmptyObject(points)) { + var heatmapLayer = L.heatLayer(points, options); + this.layerManager.addLayer(heatmapLayer, 'heatmap', layerId, group); + } +}; + +function getHeatmapIntensity(feature, intensityProperty) { + var intensity = null; + if(feature) { + if(typeof intensityProperty === 'string') { + intensity = feature.properties[intensityProperty]; + } else if(typeof intensityProperty === 'function') { + intensity = intensityProperty(feature); + } + } + return intensity; +} + +function getHeatmapCoords(geojson, intensityProperty) { + + var latlngs = []; + if(typeof geojson === 'undefined' || geojson === null) { + return latlngs; + } + + if(typeof geojson === 'string') { + geojson = JSON.parse(geojson); + } + + // if input is a TopoJSON + // iterate over each of its objects and add their coords + if (geojson.type === 'Topology') { + var topoJsonFeatures = []; + for (var key in geojson.objects) { + var topoToGeo = topojson.feature(geojson, geojson.objects[key]); + if(L.Util.isArray(topoToGeo)) { + topoJsonFeatures = topoJsonFeatures.concat(topoToGeo); + } else if('features' in topoToGeo ) { + topoJsonFeatures = topoJsonFeatures.concat(topoToGeo.features); + } else { + topoJsonFeatures.push(topoToGeo); + } + } + return getHeatmapCoords(topoJsonFeatures, intensityProperty); + } + + var features = L.Util.isArray(geojson) ? geojson : geojson.features; + + if(features) { // either a FeatureCollection or an Array of Features + $.each(features, function(index, feature) { + + var lat = null, lng = null; + + // We're only interested in Points and Multipoints + // every other geometry is a shape + if(feature.geometry.type === 'Point') { + lat = parseFloat(feature.geometry.coordinates[1]); + lng = parseFloat(feature.geometry.coordinates[0]); + + if(lat && lng) { + if(intensityProperty) { + latlngs.push([lat, lng, + getHeatmapIntensity(feature, intensityProperty)]); + } else { + latlngs.push([lat, lng]); + } + } + } else if(feature.geometry.type === 'MultiPoint') { + latlngs = latlngs.concat( + getHeatmapCoords(feature, intensityProperty)); + } + }); + } else if(geojson.type === 'Feature') { // Single GeoJSON Feature with MultiPoint dataset + $.each(geojson.geometry.coordinates, function(index, coordinate){ + var lat = null, lng = null; + lat = parseFloat(coordinate[1]); + lng = parseFloat(coordinate[0]); + if(lat && lng) { + if(intensityProperty) { + latlngs.push([lat, lng, + getHeatmapIntensity(geojson, intensityProperty)]); + } else { + latlngs.push([lat, lng]); + } + } + }); + } + + return latlngs; +} + +function addGenericGeoJSONHeatmap( widget, geojson, intensityProperty, layerId, group, options) { + var heatmapCoords = getHeatmapCoords(geojson, intensityProperty); + + if(!$.isEmptyObject(heatmapCoords)) { + var heatmapLayer = L.heatLayer(heatmapCoords, options); + widget.layerManager.addLayer( + heatmapLayer, 'heatmap', layerId, group); + } +} + +LeafletWidget.methods.addGeoJSONHeatmap = function(geojson, intensityProperty, layerId, group, options) { + var self = this; + if(LeafletWidget.utils.isURL(geojson)) { + $.getJSON(geojson, function(geojsondata){ + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + }); + } else { + addGenericGeoJSONHeatmap(self, + geojson, intensityProperty, layerId, group, options); + } +}; + +LeafletWidget.methods.addKMLHeatmap = function(kml, intensityProperty, layerId, group, options) { + var self = this; + if(LeafletWidget.utils.isURL(kml)) { + $.getJSON(kml, function(data){ + var geojsondata = toGeoJSON.kml( + LeafletWidget.utils.parseXML(data)); + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + }); + } else { + var geojsondata = toGeoJSON.kml( + LeafletWidget.utils.parseXML(kml)); + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + } +}; + +LeafletWidget.methods.addCSVHeatmap = function(csv, intensityProperty, layerId, group, options, parserOptions) { + var self = this; + if(LeafletWidget.utils.isURL(csv)) { + $.getJSON(csv, function(data){ + csv2geojson.csv2geojson( + data, parserOptions || {}, + function(err, geojsondata) { + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + } + ); + }); + } else { + csv2geojson.csv2geojson( + csv, parserOptions || {}, + function(err, geojsondata) { + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + } + ); + } +}; + +LeafletWidget.methods.addGPXHeatmap = function(gpx, intensityProperty, layerId, group, options) { + var self = this; + if(LeafletWidget.utils.isURL(gpx)) { + $.getJSON(gpx, function(data){ + var geojsondata = toGeoJSON.gpx( + LeafletWidget.utils.parseXML(data)); + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + }); + } else { + var geojsondata = toGeoJSON.gpx( + LeafletWidget.utils.parseXML(gpx)); + addGenericGeoJSONHeatmap(self, + geojsondata, intensityProperty, layerId, group, options); + } +}; + +LeafletWidget.methods.removeHeatmap = function(layerId) { + this.layerManager.removeLayer('heatmap', layerId); +}; + +LeafletWidget.methods.clearHeatmap = function() { + this.layerManager.clearLayers('heatmap'); +}; diff --git a/inst/htmlwidgets/lib/heat/leaflet-heat.js b/inst/htmlwidgets/lib/heat/leaflet-heat.js new file mode 100644 index 00000000..0f33c6ee --- /dev/null +++ b/inst/htmlwidgets/lib/heat/leaflet-heat.js @@ -0,0 +1 @@ +"use strict";!function(){function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;sthis._max&&(this._max=s[2])}for(this._heat.max(this._max),t=0,i=c.length;t zkVbv~3aI`Ms3Jr?|OfbmuvO3UAin`4r{I}V)lKulQZGmL5ywK?$$0_N5tM1 z85GNHi-p-af9jHX0LVLR*?!b>`eDy_3~3Lp7^a1LXvp3GlE9wq?8VtgwpHRh^%@Nz z*Q@gx+GyDVI3H}@zPB*n_gLp*jBR2!90hzbqRQJf`)tamA$wQ8SyO=@z^ZS4#msous>`4(1PdJ8Tl*Xvo(NUZ!td* zsBa(YTs9A0XEA^00c!^7kw1RNNa|}-9aC?=E|T{3`NZ~?xs+EZVQ~X*v2Q13KBNZ} zdeEW=%buH?B*qtVu~z`wXJ}L6L{K2I!mO0z$GtE{(B$|(B3yGqc`Sey8O|P-=4wma zr=dNI@`6SJP?#+^i`a$kG{lBq#`bS~Vu9z%$*(;7!%1x$yWZrH!&yQiBX93mBxW<& zvt#ecY|@LV4zr3AbAK-pGtPMeMIYIu9^gFN8ciQx&Rdaq51`1vE>ju%UV|?ktZxe4 zkYDh$%h(*=&>{?&GAxYmP=D%?c&`$-`A@Ee*$q#UB@~ncP0$dUTmag|1#l<6QyaG@ zy=l!d<{XlxLt4TIYs?6nWlwhA%W2Ev+U&avJbpZ2OWA}8GYMgpv9^U+3^%k>Tmu*U zJzx+4c-tJ{FwHGKY(O~~LIZe0nTH09o44Z=X;=+YxRZ0^VVe*afk8i5Fb#Q+CCVP|gQm_0K+g?NZw!J91+md9Y(mzyRF2CjL&EB-YU_ zDAH+2u{V(E_!VKCIBAms9hGnt1kv`)oibxon-<&q*0YwSlp=b8WCW4g(;BYCam8wo z>{}X5pPcy}C)ya*Pi z7F}+TG^({R%`hrFq)x0-NO!x2g$tv=FbnoMX1%;FEFQmw&2&(h3=HmYk3%7{Hv=cg z@CWNoj@z@?C9I2Vx)49?!8rMV4shDdIRDJ-h=(UE`Ak|5lFo7aALwuQO-rJAogtS7 zL7ad;%X9mK1<}5X8y;cM_XMCh7GEmNlsQjifUmulW~o7@O&{?@M~}F@cLoA^;bD|g zo=+Z^eV*#%U;&&7h7<%xu5K)hBmi(>4knj`ZP=oP=(rmv2vPESHpDylJTguZJX~x5 zWaD-^>T#Fnrn<>(D_mIcFU0@53Yhr=ijOS!naueCX(CcRnI!_1fb`~SF3@_UqH0Gx za0EIgBR*VvkdZuB>8T4PQ_^SX`u4I}Z1nu4GaG(H)Q2V|gm%<_dKYslCm>2Y|uxRPO+_8=NN z?qmul@B;BmHkYVP1g7DtLl>w#7nG2wR|=z!9~MmwRlvC>bfPSnGYs^=27e~32?1HC z00y{tQ`W-$C*1Vcvl&(+pvf2kB1C)aAPHXfYLhPfoBLoZ&9qJC;j#*^}IYy#pbw~{qaf(UdPO7o;iW_lzYk5-p3z?&~w z3g8%lN+%4SiI9^X4QW+_IN}Zs27+Qsv|=BfzF!O!0;9%Rit7D(sKyO#N7pQbqmGA? zaj9DM|i;EML~Na7?)Cd zs*?nQxi1lO{?O>{-ft|muV%Yt525S<%K}DWC9^Q-gLA%KhZkVNmM~FSL0Xv;^}yq^ zGv>8$FkQl_k&sb_QZGYTkP13sF}lMfDWvP#bYfNRu}s(prcEMjkD@uSV_!Z6rYB(I z0)6_bu6jU`3CcX7Vt__Yy!IASs~?gO*jN@K6PxZhaOF}9kXdl{)uf%eSJZJF1_%cg zicRzZ>W~g91FqxDpm1y0mv@-3OGw%d>cGvjs+AcVrU5vN%mJ?s08c(L{wU4lR=j6Y zq}pMC*8?^)77g-UNa9wj3ZA)kUqD)#^3EZHZ zZv>4FohzqDW*r8MZ4$b))8)3?Ht0lkJ)5D-o*0&OwZk-N{6v^qvia7E<)={d#Z}SM zd2S7CggP$_A+QHh{55pym91D&{r{hO@%g?3bo0Vu}o%!aw?~O&gdpPa+6w{KTb^-GM7uy%h01@ zsC4bxH>#dq%ZcbT)q;2FpN`R7$;&pI?(^tkiepofkH+4mr-k2a5#rV}RtR4)*w2k~ z;-Q7Xgh?jy$Q}s`vT8I`v!?u%P8_GfXKQx!ayo92oG^wL!40=Y%VkbmP=aa2NLU)Q zFDu^zm6=_XVIkfbBl45wp=h*)iji$O@N#fv5uYp2BQu(7bEZC!UfRJ!Y@&~Qo|Qj3 zzUj<_uD))nqqP`Z#D}SsAu$Z>-kT-hwoEIo5Oot_Qj1I6F2aNeILmG}K)6~tpvd~J z8O2k&M6m7eyzB40PZV3R9LRqP{kluy(Z6Y$1eGHy4BUh*%HvSf_UU( z6lkBCsPq6wymW<$D)Ybr{+_>8-OC-UnEYxVgQSBcUcK8iN8tu{(-N$3WH#LrLrS;N zqQ9cs?hA?a1__;AAHEr|lF`*;;ccn=i$FhrHKDgQ&uMO!6*I&VjP)?;-l!Xq1@Ydw z7*#Ynl;Kh`-ntC4g2`tNqd8Jxufnk7dOA**e&E`97x69h5%&u$NhMHEbDK9F(mlHz z>htc{y#S-DX$ZinPSx$N;*r{ETYVwY?d9);p&|le*|V+j1_T7VZk;;U7hk(eQhaE+ zJj!*J);E+*4SQh{AS%nk=HxYAmN zr!XD`1gXeRC$EuVK9#S{=H%UCZS9PzK2zu(;)IKbz`!ylA!iXmt>C6&Oux>)i;<+b z!8Whv8Q7a*#(%G_jD4?zE{3_^S&#O19}*5U5wTM5%0$`=Vu^IiOHagw5wj%#h`yG) zCwDEi+?^q;aJhf75|~=uRlSuvie7~3)>(t0KkM*dWe?akd)S*5T?D^c)7{CKpE~|g zuSkK)cd*XGmKeFs->yVkMZ}vf5PgGJ)Acs4c%xag3v`y2TC~+bV&8E);on>HQ$sxg zfG+nG-cz46nTkD<+>cLM|Hksg8qT{V2@1!7YQj+u8olwqvdgR)6?K5a|9Mv-H`6Ei z75I4@tE{N%Z)ZL)^qiJZlfYqZGO(9{2#++6NOvN?-rBe;2pc$IMeSpaS87%5+bdJd zgRoXydp8v7A9s-JZ!oIX;y~KaJzXfHH?>R&?}i}Zo!KekO=*SoP^3Kw_-1~!)U7^F hZf69X0o{~i8G{6YRf{vv)6AMh9XA-;ew@g{ zdiwpWmvR?u>l8sy0`G-{L;~?2a+~$sz6n6xOX91!JLdaw`B(vz3(n^w9$qH!=cJ(( z>!zDw7~{q4dcLVRy!F+`J%0P&J+vc%Vw@iqgq@-US?eAAEcZmZW0pJIqNY?xJp*qANK1W}~IrvbY6YqTbPko(ypE?JhqXXOv4si|+ z^&!svlsG(&078Z4JrIJ*PvscfuvHPRzCoNzA`=Ts62m6wO#ljfyZ{k#$NQG38`g?P zHGHZv0r3;e>0(SjuOdq8d%{X3*ATorAt5p`jAJVi`ISPm}sI%HY0$ieWKu zM&QLpC{iHYx|xc|u)UzCA*Mm($5@9g4twyKs^d!iZUPAFleOovxoVjV2Z%FSImo;) zQmM*f3NTb%(@s>m6_G5)DQo*Mv0P~#6BC5@behz^2kg|X+>%xANHRJ%v5N@iR-`%vc- z@Y0K5J>l(_@iN_%oGDuB0}Ng;LPhq6!I27D44-L^Z4oL@9c@~7l!0{8+p#xhC~re9 z(&J!vtK>wK;7eBgMqE@Nw4p0g5&ba-6YKADoatQa*-GIz>_gaf)4zOl=l&07Qq3nV*JvstX#h7Hk| zqLyFY5}!M12fl|e(6vt|H^X5wQpIOEPoFy9c$X(@g*b^}!ZfaE{d2S4Qib~+sS zw^g4OLUfBHHF?bJn{PExBvGvVEq9km!}NH^Bi%Cw-P9bJOL3~oM4rT#bef0Mfw;-7 zr}QVmp_SaCQZ)=-X?0pgxl=E-MrcG$eT57+I*3h*YQM{R#vr#zKMj(cc149!`FRFE zQyUOBGbfg}#`()wzOy%KrW}%E%gVW6&5r>s62mopO-h}fJaMD z-NcAOrt8wK$6b$L5LLwsP?2@GR)Yr~ z>>!k^059%c38plgY;RqeUQfxP4O41>mJ0G9us4{g7Y(}Pfb?}{V_-xX3qV-}wrON& zWX*d$yyBxwYes=MH7&ic@oJDvu}4EyscW>uoBIDSh=kCv@rDhA#4M#Ec2k+bjirctv;Zo<&rSjMCO zI)znLXJElliBj@1JCk|G5lv(>PoR?_3!hC6=rxW?i%VVpz)JrI_oPP?@4tC2N)3-uORcs*>5V9jY#m z^rls5WKpfas!n?AK+c1jf}6$`9Y2}KVFLT2H{~nL>UIx705u%t#gTPA!tt^*as4Br z)MJB`Hlv6}xcb*)*o6|-_@fKR+*-8F(U{W}rJkdd zSy^(an$#L~43|rkIaZ)PmFTM|wav!JKd4MyalX zlMI@A@BKw*S$y(SE*?!gU8^?eiqFe*jB6A_ucJYD9U@0-Om5Ul8-une@2pN`N>RGB z8`j+9a{x%KeCpNu@-Yy z?VjN60jvlfdean0yT)oIvj;P*UDa+bn*h@6qduk|N4uqFVC2*J(RqK<*PJg{3eAR; z0jWkqrsjhtO`YgeX9T7@%XwvRRWP*da@Uc@A99H?)< z0NBmIDljz$T<6r$8{9u9Wckkm&u`2E&7oq>AgbSNgwSY%C!%sZ-=zVOd?6fKt$v|9 z?@NchVv}H#{5Fi+Vx;I3M0M=WWcmZXQjJvr`$+8Q%{LFK%2h@jku_sTg$nJNo1c%) z#q*?$+tu$Hg1VFD7a_ZntNBDzQyxI zm{qnHbkb>kJK z66npPp*io_sLI}YaVKd~enpRh;M6s3M6-q*jEl<052eT0_>eY=!-(GGkV+(lmBCwv zV0qb?80(}XacZW;fH;7zR2vM|@G>yg8s3mTh6Jq_nmG^L=E1f4AG1_dD=8Y#ZWARM z^E~99hj1j4d_2^Cg*SKVZ9+bNVBv*|cBgBnKo(!8dcwQLJp1=3J&(r6FEcoX`Ff;| eCj0g@4F3i;vYelP3ls+c0000