# Evaluate list members that are formulae, using the map data as the environment
# (if provided, otherwise the formula environment)
evalFormula = function(list, data) {
evalAll = function(x) {
if (is.list(x)) {
structure(lapply(x, evalAll), class = class(x))
} else resolveFormula(x, data)
}
evalAll(list)
}
# jcheng 12/10/2014: The limits/bbox handling was pretty rushed, unfortunately
# we have ended up with too many concepts. expandLimits just takes random
# lat/lng vectors, the sp package's Spatial objects can use `bbox()`, and our
# polygon lists (returned from polygonData()) use `attr(x, "bbox")` (though at
# least they are the same shape as the Spatial bounding boxes).
# Notifies the map of new latitude/longitude of items of interest on the map, so
# that we can expand the limits (i.e. bounding box). We will use this as the
# initial view if the user doesn't explicitly specify bounds using fitBounds.
expandLimits = function(map, lat, lng) {
if (is.null(map$x$limits)) map$x$limits = list()
# We remove NA's and check the lengths so we never call range() with an empty
# set of arguments (or all NA's), which will cause a warning.
lat = lat[is.finite(lat)]
lng = lng[is.finite(lng)]
if (length(lat) > 0) map$x$limits$lat = range(map$x$limits$lat, lat)
if (length(lng) > 0) map$x$limits$lng = range(map$x$limits$lng, lng)
map
}
# Same as expandLimits, but takes a polygon (that presumably has a bbox attr)
# rather than lat/lng.
expandLimitsBbox = function(map, poly) {
bbox = attr(poly, "bbox", exact = TRUE)
if (is.null(bbox)) stop("Polygon data had no bbox")
expandLimits(map, bbox[2, ], bbox[1, ])
}
# Represents an initial bbox; if combined with any other bbox value using
# bboxAdd, the other bbox will be the result.
bboxNull = cbind(min = c(x = Inf, y = Inf), max = c(x = -Inf, y = -Inf))
# Combine two bboxes; the result will use the mins of the mins and the maxes of
# the maxes.
bboxAdd = function(a, b) {
cbind(
min = pmin(a[, 1], b[, 1]),
max = pmax(a[, 2], b[, 2])
)
}
#' @param group the name of the group whose members should be removed
#' @rdname remove
#' @export
clearGroup <- function(map, group) {
invokeMethod(map, getMapData(map), 'clearGroup', group);
}
#' Show or hide layer groups
#'
#' Hide groups of layers without removing them from the map entirely. Groups are
#' created using the \code{group} parameter that is included on most layer
#' adding functions.
#'
#' @param map the map to modify
#' @param group character vector of one or more group names to show or hide
#'
#' @seealso \code{\link{addLayersControl}} to allow users to show/hide layer
#' groups interactively
#'
#' @export
showGroup = function(map, group) {
invokeMethod(map, getMapData(map), 'showGroup', group)
}
#' @rdname showGroup
#' @export
hideGroup = function(map, group) {
invokeMethod(map, getMapData(map), 'hideGroup', group)
}
#' Graphics elements and layers
#'
#' Add graphics elements and layers to the map widget.
#' @inheritParams setView
#' @param urlTemplate a character string as the URL template
#' @param attribution the attribution text of the tile layer (HTML)
#' @param options a list of extra options for tile layers, popups, paths
#' (circles, rectangles, polygons, ...), or other map elements
#' @return the new \code{map} object
#' @seealso \code{\link{tileOptions}}, \code{\link{WMSTileOptions}},
#' \code{\link{popupOptions}}, \code{\link{markerOptions}},
#' \code{\link{pathOptions}}
#' @references The Leaflet API documentation:
#' \url{http://leafletjs.com/reference.html}
#' @describeIn map-layers Add a tile layer to the map
#' @export
addTiles = function(
map,
urlTemplate = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution = NULL,
layerId = NULL,
group = NULL,
options = tileOptions()
) {
options$attribution = attribution
if (missing(urlTemplate) && is.null(options$attribution))
options$attribution = paste(
'© OpenStreetMap',
'contributors, CC-BY-SA'
)
invokeMethod(map, getMapData(map), 'addTiles', urlTemplate, layerId, group,
options)
}
epsg4326 <- "+proj=longlat +datum=WGS84 +no_defs"
epsg3857 <- "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs"
#' Add a raster image as a layer
#'
#' Create an image overlay from a \code{RasterLayer} object. \emph{This is only
#' suitable for small to medium sized rasters}, as the entire image will be
#' embedded into the HTML page (or passed over the websocket in a Shiny
#' context).
#'
#' The \code{maxBytes} parameter serves to prevent you from accidentally
#' embedding an excessively large amount of data into your htmlwidget. This
#' value is compared to the size of the final compressed image (after the raster
#' has been projected, colored, and PNG encoded, but before base64 encoding is
#' applied). Set \code{maxBytes} to \code{Inf} to disable this check, but be
#' aware that very large rasters may not only make your map a large download but
#' also may cause the browser to become slow or unresponsive.
#'
#' By default, the \code{addRasterImage} function will project the RasterLayer
#' \code{x} to EPSG:3857 using the \code{raster} package's
#' \code{\link[raster]{projectRaster}} function. This can be a time-consuming
#' operation for even moderately sized rasters. Upgrading the \code{raster}
#' package to 2.4 or later will provide a large speedup versus previous
#' versions. If you are repeatedly adding a particular raster to your Leaflet
#' maps, you can perform the projection ahead of time using
#' \code{projectRasterForLeaflet()}, and call \code{addRasterImage} with
#' \code{project=FALSE}.
#'
#' @param map a map widget object
#' @param x a \code{RasterLayer} object--see \code{\link[raster]{raster}}
#' @param colors the color palette (see \code{\link{colorNumeric}}) or function
#' to use to color the raster values (hint: if providing a function, set
#' \code{na.color} to \code{"#00000000"} to make \code{NA} areas transparent)
#' @param opacity the base opacity of the raster, expressed from 0 to 1
#' @param attribution the HTML string to show as the attribution for this layer
#' @param layerId the layer id
#' @param group the name of the group this raster image should belong to (see
#' the same parameter under \code{\link{addTiles}})
#' @param project if \code{TRUE}, automatically project \code{x} to the map
#' projection expected by Leaflet (\code{EPSG:3857}); if \code{FALSE}, it's
#' the caller's responsibility to ensure that \code{x} is already projected,
#' and that \code{extent(x)} is expressed in WGS84 latitude/longitude
#' coordinates
#' @param maxBytes the maximum number of bytes to allow for the projected image
#' (before base64 encoding); defaults to 4MB.
#'
#' @examples
#' library(raster)
#'
#' r <- raster(xmn=-2.8, xmx=-2.79, ymn=54.04, ymx=54.05, nrows=30, ncols=30)
#' values(r) <- matrix(1:900, nrow(r), ncol(r), byrow = TRUE)
#' crs(r) <- CRS("+init=epsg:4326")
#'
#' leaflet() %>% addTiles() %>%
#' addRasterImage(r, colors = "Spectral", opacity = 0.8)
#' @export
addRasterImage = function(
map,
x,
colors = "Spectral",
opacity = 1,
attribution = NULL,
layerId = NULL,
group = NULL,
project = TRUE,
maxBytes = 4*1024*1024
) {
stopifnot(inherits(x, "RasterLayer"))
if (project) {
projected <- projectRasterForLeaflet(x)
} else {
projected <- x
}
bounds <- raster::extent(raster::projectExtent(raster::projectExtent(x, crs = sp::CRS(epsg3857)), crs = sp::CRS(epsg4326)))
if (!is.function(colors)) {
colors <- colorNumeric(colors, domain = NULL, na.color = "#00000000", alpha = TRUE)
}
tileData <- raster::values(projected) %>% colors() %>% col2rgb(alpha = TRUE) %>% as.raw()
dim(tileData) <- c(4, ncol(projected), nrow(projected))
pngData <- png::writePNG(tileData)
if (length(pngData) > maxBytes) {
stop("Raster image too large; ", length(pngData), " bytes is greater than maximum ", maxBytes, " bytes")
}
encoded <- base64enc::base64encode(pngData)
uri <- paste0("data:image/png;base64,", encoded)
latlng <- list(
list(raster::ymax(bounds), raster::xmin(bounds)),
list(raster::ymin(bounds), raster::xmax(bounds))
)
invokeMethod(map, getMapData(map), "addRasterImage", uri, latlng, opacity, attribution, layerId, group) %>%
expandLimits(c(raster::ymin(bounds), raster::ymax(bounds)), c(raster::xmin(bounds), raster::xmax(bounds)))
}
#' @rdname addRasterImage
#' @export
projectRasterForLeaflet <- function(x) {
raster::projectRaster(x, raster::projectExtent(x, crs = sp::CRS(epsg3857)))
}
#' @rdname remove
#' @export
removeImage = function(map, layerId) {
invokeMethod(map, NULL, 'removeImage', layerId)
}
#' @rdname remove
#' @export
clearImages = function(map) {
invokeMethod(map, NULL, 'clearImages')
}
#' Extra options for map elements and layers
#'
#' The rest of all possible options for map elements and layers that are not
#' listed in the layer functions.
#' @param
#' minZoom,maxZoom,maxNativeZoom,tileSize,subdomains,errorTileUrl,tms,continuousWorld,noWrap,zoomOffset,zoomReverse,zIndex,unloadInvisibleTiles,updateWhenIdle,detectRetina,reuseTiles
#' the tile layer options; see
#' \url{http://leafletjs.com/reference.html#tilelayer}
#' @describeIn map-options Options for tile layers
#' @export
tileOptions = function(
minZoom = 0,
maxZoom = 18,
maxNativeZoom = NULL,
tileSize = 256,
subdomains = 'abc',
errorTileUrl = '',
tms = FALSE,
continuousWorld = FALSE,
noWrap = FALSE,
zoomOffset = 0,
zoomReverse = FALSE,
opacity = 1.0,
zIndex = NULL,
unloadInvisibleTiles = NULL,
updateWhenIdle = NULL,
detectRetina = FALSE,
reuseTiles = FALSE
# bounds = TODO
) {
list(
minZoom = minZoom, maxZoom = maxZoom, maxNativeZoom = maxNativeZoom,
tileSize = tileSize, subdomains = subdomains, errorTileUrl = errorTileUrl,
tms = tms, continuousWorld = continuousWorld, noWrap = noWrap,
zoomOffset = zoomOffset, zoomReverse = zoomReverse, opacity = opacity,
zIndex = zIndex, unloadInvisibleTiles = unloadInvisibleTiles,
updateWhenIdle = updateWhenIdle, detectRetina = detectRetina,
reuseTiles = reuseTiles
)
}
#' Remove elements from a map
#'
#' Remove one or more features from a map, identified by \code{layerId}; or,
#' clear all features of the given type or group.
#'
#' @note When used with a \code{\link{leaflet}}() map object, these functions
#' don't actually remove the features from the map object, but simply add an
#' operation that will cause those features to be removed after they are
#' added. In other words, if you add a polygon \code{"foo"} and the call
#' \code{removeShape("foo")}, it's not smart enough to prevent the polygon
#' from being added in the first place; instead, when the map is rendered, the
#' polygon will be added and then removed.
#'
#' For that reason, these functions aren't that useful with \code{leaflet} map
#' objects and are really intended to be used with \code{\link{leafletProxy}}
#' instead.
#'
#' WMS tile layers are extensions of tile layers, so they can also be removed
#' or cleared via \code{removeTiles()} or \code{clearTiles()}.
#' @param map a map widget object, possibly created from \code{\link{leaflet}}()
#' but more likely from \code{\link{leafletProxy}}()
#' @param layerId character vector; the layer id(s) of the item to remove
#' @return the new \code{map} object
#'
#' @name remove
#' @export
removeTiles = function(map, layerId) {
invokeMethod(map, getMapData(map), 'removeTiles', layerId)
}
#' @rdname remove
#' @export
clearTiles = function(map) {
invokeMethod(map, NULL, 'clearTiles')
}
#' @param baseUrl a base URL of the WMS service
#' @param layers comma-separated list of WMS layers to show
#' @describeIn map-layers Add a WMS tile layer to the map
#' @export
addWMSTiles = function(
map, baseUrl, layerId = NULL, group = NULL,
options = WMSTileOptions(), attribution = NULL, layers = ''
) {
options$attribution = attribution
options$layers = layers
invokeMethod(map, getMapData(map), 'addWMSTiles', baseUrl, layerId, group, options)
}
#' @param styles comma-separated list of WMS styles
#' @param format WMS image format (use \code{'image/png'} for layers with
#' transparency)
#' @param transparent if \code{TRUE}, the WMS service will return images with
#' transparency
#' @param version version of the WMS service to use
#' @param crs Coordinate Reference System to use for the WMS requests, defaults
#' to map CRS (don't change this if you're not sure what it means)
#' @param ... other tile options for \code{WMSTileOptions()} (all arguments of
#' \code{tileOptions()} can be used)
#' @describeIn map-options Options for WMS tile layers
#' @export
WMSTileOptions = function(
styles = '', format = 'image/jpeg', transparent = FALSE, version = '1.1.1',
crs = NULL, ...
) {
list(
styles = styles, format = format, transparent = transparent,
version = version, crs = crs, ...
)
}
#' @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 popup a character vector of the HTML content for the popups (you are
#' recommended to escape the text using \code{\link[htmltools]{htmlEscape}()}
#' for security reasons)
#' @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
#' @describeIn map-layers Add popups to the map
#' @export
addPopups = function(
map, lng = NULL, lat = NULL, popup, layerId = NULL, group = NULL,
options = popupOptions(),
data = getMapData(map)
) {
pts = derivePoints(data, lng, lat, missing(lng), missing(lat), "addPopups")
invokeMethod(map, data, 'addPopups', pts$lat, pts$lng, popup, layerId, group, options) %>%
expandLimits(pts$lat, pts$lng)
}
#' @param className a CSS class name set on an element
#' @param
#' maxWidth,minWidth,maxHeight,autoPan,keepInView,closeButton,zoomAnimation,closeOnClick
#' popup options; see \url{http://leafletjs.com/reference.html#popup}
#' @describeIn map-options Options for popups
#' @export
popupOptions = function(
maxWidth = 300,
minWidth = 50,
maxHeight = NULL,
autoPan = TRUE,
keepInView = FALSE,
closeButton = TRUE,
# offset = TODO,
# autoPanPaddingTopLeft = TODO,
# autoPanPaddingBottomRight = TODO,
# autoPanPadding = TODO,
zoomAnimation = TRUE,
closeOnClick = NULL,
className = ""
) {
list(
maxWidth = maxWidth, minWidth = minWidth, maxHeight = maxHeight,
autoPan = autoPan, keepInView = keepInView, closeButton = closeButton,
zoomAnimation = zoomAnimation, closeOnClick = closeOnClick, className = className
)
}
#' @rdname remove
#' @export
removePopup = function(map, layerId) {
invokeMethod(map, getMapData(map), 'removePopup', layerId)
}
#' @rdname remove
#' @export
clearPopups = function(map) {
invokeMethod(map, NULL, 'clearPopups')
}
# Helper Function to create a safe label
safeLabel <- function(label, data) {
if (is.null(label)) {
return(label)
}
label <- evalFormula(label, data)
if(! (inherits(label, "html") ||
sum(sapply(label,function(x){!inherits(x,'html')})) == 0)) {
label <- htmltools::htmlEscape(label)
}
label
}
#' Extra options for marker and polygon labels
#'
#' @param
#' noHide,direction,offset
#' label options; see \url{https://github.com/Leaflet/Leaflet.label#options}
#' @describeIn map-options Options for labels
#' @export
labelOptions = function(
clickable = FALSE,
noHide = FALSE,
className = '',
direction = 'right',
#pane = NULL,
offset = c(12,-15),
opacity = 1,
zoomAnimation = TRUE
) {
list(
clickable = clickable, noHide = noHide, direction = direction,
opacity = opacity, offset = offset,
zoomAnimation = zoomAnimation, className = className
)
}
#' @param icon the icon(s) for markers; an icon is represented by an R list of
#' the form \code{list(iconUrl = '?', iconSize = c(x, y))}, and you can use
#' \code{\link{icons}()} to create multiple icons; note when you use an R list
#' that contains images as local files, these local image files will be base64
#' encoded into the HTML page so the icon images will still be available even
#' when you publish the map elsewhere
#' @param label a character vector of the HTML content for the labels
#' @param labelOptions A Vector of \code{\link{labelOptions}} to provide label
#' options for each label. Default \code{NULL}
#' @param clusterOptions if not \code{NULL}, markers will be clustered using
#' \href{https://github.com/Leaflet/Leaflet.markercluster}{Leaflet.markercluster};
#' you can use \code{\link{markerClusterOptions}()} to specify marker cluster
#' options
#' @param clusterId the id for the marker cluster layer
#' @describeIn map-layers Add markders to the map
#' @export
addMarkers = function(
map, lng = NULL, lat = NULL, layerId = NULL, group = NULL,
icon = NULL,
popup = NULL,
label = NULL,
labelOptions = NULL,
options = markerOptions(),
clusterOptions = NULL,
clusterId = NULL,
data = getMapData(map)
) {
if (!is.null(icon)) {
# If custom icons are specified, we need to 1) deduplicate any URLs/files,
# so we can efficiently send e.g. 1000 markers that all use the same 2
# icons; and 2) do base64 encoding on any local icon files (as opposed to
# URLs [absolute or relative] which will be left alone).
# If formulas are present, they must be evaluated first so we can pack the
# resulting values
icon = evalFormula(list(icon), data)[[1]]
if (inherits(icon, "leaflet_icon_set")) {
icon = iconSetToIcons(icon)
}
# Pack and encode each URL vector; this will be reversed on the client
icon$iconUrl = b64EncodePackedIcons(packStrings(icon$iconUrl))
icon$iconRetinaUrl = b64EncodePackedIcons(packStrings(icon$iconRetinaUrl))
icon$shadowUrl = b64EncodePackedIcons(packStrings(icon$shadowUrl))
icon$shadowRetinaUrl = b64EncodePackedIcons(packStrings(icon$shadowRetinaUrl))
icon = filterNULL(icon)
}
if (!is.null(clusterOptions))
map$dependencies = c(map$dependencies, markerClusterDependencies())
pts = derivePoints(data, lng, lat, missing(lng), missing(lat), "addMarkers")
invokeMethod(
map, data, 'addMarkers', pts$lat, pts$lng, icon, layerId, group, options, popup,
clusterOptions, clusterId, safeLabel(label, data), labelOptions
) %>% expandLimits(pts$lat, pts$lng)
}
markerClusterDependencies = function() {
list(
htmltools::htmlDependency(
'leaflet-markercluster',
'0.4.0',
system.file('htmlwidgets/plugins/Leaflet.markercluster', package = 'leaflet'),
script = 'leaflet.markercluster.js',
stylesheet = c('MarkerCluster.css', 'MarkerCluster.Default.css')
)
)
}
#' Make icon set
#'
#' @param ... icons created from \code{\link{makeIcon}()}
#' @export
#' @examples
#'
#' iconSet = iconList(
#' red = makeIcon("leaf-red.png", iconWidth=32, iconHeight=32),
#' green = makeIcon("leaf-green.png", iconWidth=32, iconHeight=32)
#' )
#'
#' iconSet[c('red', 'green', 'red')]
iconList = function(...) {
res = structure(
list(...),
class = "leaflet_icon_set"
)
cls = unlist(lapply(res, inherits, 'leaflet_icon'))
if (any(!cls))
stop('Arguments passed to iconList() must be icon objects returned from makeIcon()')
res
}
#' @export
`[.leaflet_icon_set` = function(x, i) {
if (is.factor(i)) {
i = as.character(i)
}
if (!is.character(i) && !is.numeric(i) && !is.integer(i)) {
stop("Invalid subscript type '", typeof(i), "'")
}
structure(.subset(x, i), class = "leaflet_icon_set")
}
iconSetToIcons = function(x) {
# c("iconUrl", "iconRetinaUrl", ...)
cols = names(formals(makeIcon))
# list(iconUrl = "iconUrl", iconRetinaUrl = "iconRetinaUrl", ...)
cols = structure(as.list(cols), names = cols)
# Construct an equivalent output to icons().
filterNULL(lapply(cols, function(col) {
# Pluck the `col` member off of each item in iconObjs and put them in an
# unnamed list (or vector if possible).
colVals = unname(sapply(x, `[[`, col))
# If this is the common case where there's lots of values but they're all
# actually the same exact thing, then just return one value; this will be
# much cheaper to send to the client, and we'll do recycling on the client
# side anyway.
if (length(unique(colVals)) == 1) {
return(colVals[[1]])
} else {
return(colVals)
}
}))
}
#' Define icon sets
#'
#' @inheritParams icons
#'
#' @export
makeIcon = function(iconUrl = NULL, iconRetinaUrl = NULL, iconWidth = NULL, iconHeight = NULL,
iconAnchorX = NULL, iconAnchorY = NULL, shadowUrl = NULL, shadowRetinaUrl = NULL,
shadowWidth = NULL, shadowHeight = NULL, shadowAnchorX = NULL, shadowAnchorY = NULL,
popupAnchorX = NULL, popupAnchorY = NULL, className = NULL) {
icon = filterNULL(list(
iconUrl = iconUrl, iconRetinaUrl = iconRetinaUrl,
iconWidth = iconWidth, iconHeight = iconHeight,
iconAnchorX = iconAnchorX, iconAnchorY = iconAnchorY,
shadowUrl = shadowUrl, shadowRetinaUrl = shadowRetinaUrl,
shadowWidth = shadowWidth, shadowHeight = shadowHeight,
shadowAnchorX = shadowAnchorX, shadowAnchorY = shadowAnchorY,
popupAnchorX = popupAnchorX, popupAnchorY = popupAnchorY,
className = className
))
structure(icon, class = "leaflet_icon")
}
#' Create a list of icon data
#'
#' An icon can be represented as a list of the form \code{list(iconUrl,
#' iconSize, ...)}. This function is vectorized over its arguments to create a
#' list of icon data. Shorter argument values will be re-cycled. \code{NULL}
#' values for these arguments will be ignored.
#' @param iconUrl the URL or file path to the icon image
#' @param iconRetinaUrl the URL or file path to a retina sized version of the
#' icon image
#' @param iconWidth,iconHeight size of the icon image in pixels
#' @param iconAnchorX,iconAnchorY the coordinates of the "tip" of the icon
#' (relative to its top left corner, i.e. the top left corner means
#' \code{iconAnchorX = 0} and \code{iconAnchorY = 0)}, and the icon will be
#' aligned so that this point is at the marker's geographical location
#' @param shadowUrl the URL or file path to the icon shadow image
#' @param shadowRetinaUrl the URL or file path to the retina sized version of
#' the icon shadow image
#' @param shadowWidth,shadowHeight size of the shadow image in pixels
#' @param shadowAnchorX,shadowAnchorY the coordinates of the "tip" of the shadow
#' @param popupAnchorX,popupAnchorY the coordinates of the point from which
#' popups will "open", relative to the icon anchor
#' @param className a custom class name to assign to both icon and shadow images
#' @return A list of icon data that can be passed to the \code{icon} argument of
#' \code{\link{addMarkers}()}.
#' @export
#' @example inst/examples/icons.R
icons = function(
iconUrl = NULL, iconRetinaUrl = NULL, iconWidth = NULL, iconHeight = NULL,
iconAnchorX = NULL, iconAnchorY = NULL, shadowUrl = NULL, shadowRetinaUrl = NULL,
shadowWidth = NULL, shadowHeight = NULL, shadowAnchorX = NULL, shadowAnchorY = NULL,
popupAnchorX = NULL, popupAnchorY = NULL, className = NULL
) {
filterNULL(list(
iconUrl = iconUrl, iconRetinaUrl = iconRetinaUrl,
iconWidth = iconWidth, iconHeight = iconHeight,
iconAnchorX = iconAnchorX, iconAnchorY = iconAnchorY,
shadowUrl = shadowUrl, shadowRetinaUrl = shadowRetinaUrl,
shadowWidth = shadowWidth, shadowHeight = shadowHeight,
shadowAnchorX = shadowAnchorX, shadowAnchorY = shadowAnchorY,
popupAnchorX = popupAnchorX, popupAnchorY = popupAnchorY,
className = className
))
}
packStrings = function(strings) {
if (length(strings) == 0) {
return(NULL)
}
uniques = unique(strings)
indices = match(strings, uniques)
indices = indices - 1 # convert to 0-based for easy JS usage
list(
data = uniques,
index = indices
)
}
b64EncodePackedIcons = function(packedIcons) {
if (is.null(packedIcons))
return(packedIcons)
# TODO: remove this when we've got our own encoding function
markdown::markdownToHTML
image_uri = getFromNamespace('.b64EncodeFile', 'markdown')
packedIcons$data = sapply(packedIcons$data, function(icon) {
if (is.character(icon) && file.exists(icon)) {
image_uri(icon)
} else {
icon
}
}, USE.NAMES = FALSE)
packedIcons
}
#' @param clickable whether the element emits mouse events
#' @param
#' draggable,keyboard,title,alt,zIndexOffset,opacity,riseOnHover,riseOffset
#' marker options; see \url{http://leafletjs.com/reference.html#marker}
#' @describeIn map-options Options for markers
#' @export
markerOptions = function(
clickable = TRUE,
draggable = FALSE,
keyboard = TRUE,
title = "",
alt = "",
zIndexOffset = 0,
opacity = 1.0,
riseOnHover = FALSE,
riseOffset = 250
) {
list(
clickable = clickable, draggable = draggable, keyboard = keyboard,
title = title, alt = alt, zIndexOffset = zIndexOffset, opacity = opacity,
riseOnHover = riseOnHover, riseOffset = riseOffset
)
}
#' @param showCoverageOnHover when you mouse over a cluster it shows the bounds
#' of its markers
#' @param zoomToBoundsOnClick when you click a cluster we zoom to its bounds
#' @param spiderfyOnMaxZoom when you click a cluster at the bottom zoom level we
#' spiderfy it so you can see all of its markers
#' @param removeOutsideVisibleBounds clusters and markers too far from the
#' viewport are removed from the map for performance
#' @describeIn map-options Options for marker clusters
#' @export
markerClusterOptions = function(
showCoverageOnHover = TRUE, zoomToBoundsOnClick = TRUE,
spiderfyOnMaxZoom = TRUE, removeOutsideVisibleBounds = TRUE, ...
) {
list(
showCoverageOnHover = showCoverageOnHover,
zoomToBoundsOnClick = zoomToBoundsOnClick,
spiderfyOnMaxZoom = spiderfyOnMaxZoom,
removeOutsideVisibleBounds = removeOutsideVisibleBounds, ...
)
}
#' @param radius a numeric vector of radii for the circles; it can also be a
#' one-sided formula, in which case the radius values are derived from the
#' \code{data} (units in meters for circles, and pixels for circle markers)
#' @param stroke whether to draw stroke along the path (e.g. the borders of
#' polygons or circles)
#' @param color stroke color
#' @param weight stroke width in pixels
#' @param opacity stroke opacity (or layer opacity for tile layers)
#' @param fill whether to fill the path with color (e.g. filling on polygons or
#' circles)
#' @param fillColor fill color
#' @param fillOpacity fill opacity
#' @param dashArray a string that defines the stroke
#' \href{https://developer.mozilla.org/en/SVG/Attribute/stroke-dasharray}{dash
#' pattern}
#' @describeIn map-layers Add circle markers to the map
#' @export
addCircleMarkers = function(
map, lng = NULL, lat = NULL, radius = 10, layerId = NULL, group = NULL,
stroke = TRUE,
color = "#03F",
weight = 5,
opacity = 0.5,
fill = TRUE,
fillColor = color,
fillOpacity = 0.2,
dashArray = NULL,
popup = NULL,
label = NULL,
labelOptions = NULL,
options = pathOptions(),
clusterOptions = NULL,
clusterId = NULL,
data = getMapData(map)
) {
options = c(options, list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
dashArray = dashArray
))
if (!is.null(clusterOptions))
map$dependencies = c(map$dependencies, markerClusterDependencies())
pts = derivePoints(data, lng, lat, missing(lng), missing(lat), "addCircleMarkers")
invokeMethod(map, data, 'addCircleMarkers', pts$lat, pts$lng, radius,
layerId, group, options, clusterOptions, clusterId, popup, safeLabel(label, data), labelOptions) %>%
expandLimits(pts$lat, pts$lng)
}
#' @rdname remove
#' @export
removeMarker = function(map, layerId) {
invokeMethod(map, getMapData(map), 'removeMarker', layerId)
}
#' @rdname remove
#' @export
clearMarkers = function(map) {
invokeMethod(map, NULL, 'clearMarkers')
}
#' @rdname remove
#' @export
removeMarkerCluster = function(map, layerId) {
invokeMethod(map, getMapData(map), 'removeMarkerCluster', layerId)
}
#' @rdname remove
#' @export
clearMarkerClusters = function(map) {
invokeMethod(map, NULL, 'clearMarkerClusters')
}
#' @param clusterId the id of the marker cluster layer
#' @rdname remove
#' @export
removeMarkerFromCluster = function(map, layerId, clusterId) {
invokeMethod(map, getMapData(map), 'removeMarkerFromCluster', layerId, clusterId)
}
#' @param lineCap a string that defines
#' \href{https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap}{shape
#' to be used at the end} of the stroke
#' @param lineJoin a string that defines
#' \href{https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linejoin}{shape
#' to be used at the corners} of the stroke
#' @param pointerEvents sets the \code{pointer-events} attribute on the path if
#' SVG backend is used
#' @describeIn map-options Options for vector layers (polylines, polygons,
#' rectangles, and circles, etc)
#' @export
pathOptions = function(
lineCap = NULL,
lineJoin = NULL,
clickable = TRUE,
pointerEvents = NULL,
className = ""
) {
list(
lineCap = lineCap, lineJoin = lineJoin, clickable = clickable,
pointerEvents = pointerEvents, className = className
)
}
#' @describeIn map-layers Add circles to the map
#' @export
addCircles = function(
map, lng = NULL, lat = NULL, radius = 10, layerId = NULL, group = NULL,
stroke = TRUE,
color = "#03F",
weight = 5,
opacity = 0.5,
fill = TRUE,
fillColor = color,
fillOpacity = 0.2,
dashArray = NULL,
popup = NULL,
label = NULL,
labelOptions = NULL,
options = pathOptions(),
data = getMapData(map)
) {
options = c(options, list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
dashArray = dashArray
))
pts = derivePoints(data, lng, lat, missing(lng), missing(lat), "addCircles")
invokeMethod(map, data, 'addCircles', pts$lat, pts$lng, radius, layerId, group, options, popup, safeLabel(label, data), labelOptions) %>%
expandLimits(pts$lat, pts$lng)
}
#' @param smoothFactor how much to simplify the polyline on each zoom level
#' (more means better performance and less accurate representation)
#' @param noClip whether to disable polyline clipping
#' @describeIn map-layers Add polylines to the map
#' @export
addPolylines = function(
map, lng = NULL, lat = NULL, layerId = NULL, group = NULL,
stroke = TRUE,
color = "#03F",
weight = 5,
opacity = 0.5,
fill = FALSE,
fillColor = color,
fillOpacity = 0.2,
dashArray = NULL,
smoothFactor = 1.0,
noClip = FALSE,
popup = NULL,
label = NULL,
labelOptions = NULL,
options = pathOptions(),
data = getMapData(map)
) {
options = c(options, list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
dashArray = dashArray, smoothFactor = smoothFactor, noClip = noClip
))
pgons = derivePolygons(data, lng, lat, missing(lng), missing(lat), "addPolylines")
invokeMethod(map, data, 'addPolylines', pgons, layerId, group, options, popup, safeLabel(label, data), labelOptions) %>%
expandLimitsBbox(pgons)
}
#' @param lng1,lat1,lng2,lat2 latitudes and longitudes of the south-west and
#' north-east corners of rectangles
#' @describeIn map-layers Add rectangles to the map
#' @export
addRectangles = function(
map, lng1, lat1, lng2, lat2, layerId = NULL, group = NULL,
stroke = TRUE,
color = "#03F",
weight = 5,
opacity = 0.5,
fill = TRUE,
fillColor = color,
fillOpacity = 0.2,
dashArray = NULL,
smoothFactor = 1.0,
noClip = FALSE,
popup = NULL,
label = NULL,
labelOptions = NULL,
options = pathOptions(),
data = getMapData(map)
) {
options = c(options, list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
dashArray = dashArray, smoothFactor = smoothFactor, noClip = noClip
))
lng1 = resolveFormula(lng1, data)
lat1 = resolveFormula(lat1, data)
lng2 = resolveFormula(lng2, data)
lat2 = resolveFormula(lat2, data)
invokeMethod(map, data, 'addRectangles',lat1, lng1, lat2, lng2, layerId, group, options, popup, safeLabel(label, data), labelOptions) %>%
expandLimits(c(lat1, lat2), c(lng1, lng2))
}
#' @describeIn map-layers Add polygons to the map
#' @export
addPolygons = function(
map, lng = NULL, lat = NULL, layerId = NULL, group = NULL,
stroke = TRUE,
color = "#03F",
weight = 5,
opacity = 0.5,
fill = TRUE,
fillColor = color,
fillOpacity = 0.2,
dashArray = NULL,
smoothFactor = 1.0,
noClip = FALSE,
popup = NULL,
label = NULL,
labelOptions = NULL,
options = pathOptions(),
data = getMapData(map)
) {
options = c(options, list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
dashArray = dashArray, smoothFactor = smoothFactor, noClip = noClip
))
pgons = derivePolygons(data, lng, lat, missing(lng), missing(lat), "addPolygons")
invokeMethod(map, data, 'addPolygons', pgons, layerId, group, options, popup, safeLabel(label, data), labelOptions) %>%
expandLimitsBbox(pgons)
}
#' @rdname remove
#' @export
removeShape = function(map, layerId) {
invokeMethod(map, getMapData(map), 'removeShape', layerId)
}
#' @rdname remove
#' @export
clearShapes = function(map) {
invokeMethod(map, NULL, 'clearShapes')
}
#' @param geojson a GeoJSON list, or character vector of length 1
#' @describeIn map-layers Add GeoJSON layers to the map
#' @export
addGeoJSON = function(map, geojson, layerId = NULL, group = NULL,
stroke = TRUE,
color = "#03F",
weight = 5,
opacity = 0.5,
fill = TRUE,
fillColor = color,
fillOpacity = 0.2,
dashArray = NULL,
smoothFactor = 1.0,
noClip = FALSE,
options = pathOptions()
) {
options = c(options, list(
stroke = stroke, color = color, weight = weight, opacity = opacity,
fill = fill, fillColor = fillColor, fillOpacity = fillOpacity,
dashArray = dashArray, smoothFactor = smoothFactor, noClip = noClip
))
invokeMethod(map, getMapData(map), 'addGeoJSON', geojson, layerId, group, options)
}
#' @rdname remove
#' @export
removeGeoJSON = function(map, layerId) {
invokeMethod(map, getMapData(map), 'removeGeoJSON', layerId)
}
#' @rdname remove
#' @export
clearGeoJSON = function(map) {
invokeMethod(map, NULL, 'clearGeoJSON')
}
#' Add UI controls to switch layers on and off
#'
#' Uses Leaflet's built-in
#' \href{http://leafletjs.com/reference.html#control-layers}{layers control}
#' feature to allow users to choose one of several base layers, and to choose
#' any number of overlay layers to view.
#'
#' @param map the map to add the layers control to
#' @param baseGroups character vector where each element is the name of a group.
#' The user will be able to choose one base group (only) at a time. This is
#' most commonly used for mostly-opaque tile layers.
#' @param overlayGroups character vector where each element is the name of a
#' group. The user can turn each overlay group on or off independently.
#' @param position position of control: 'topleft', 'topright', 'bottomleft', or
#' 'bottomright'
#' @param options a list of additional options, intended to be provided by
#' a call to \code{layersControlOptions}
#'
#' @examples
#' \donttest{
#' leaflet() %>%
#' addTiles(group = "OpenStreetMap") %>%
#' addProviderTiles("Stamen.Toner", group = "Toner by Stamen") %>%
#' addMarkers(runif(20, -75, -74), runif(20, 41, 42), group = "Markers") %>%
#' addLayersControl(
#' baseGroups = c("OpenStreetMap", "Toner by Stamen"),
#' overlayGroups = c("Markers")
#' )
#' }
#'
#' @export
addLayersControl = function(map,
baseGroups = character(0), overlayGroups = character(0),
position = c('topright', 'bottomright', 'bottomleft', 'topleft'),
options = layersControlOptions()) {
options = c(options, list(position = match.arg(position)))
invokeMethod(map, getMapData(map), 'addLayersControl', baseGroups,
overlayGroups, options)
}
#' @rdname addLayersControl
#' @param collapsed if \code{TRUE} (the default), the layers control will be
#' rendered as an icon that expands when hovered over. Set to \code{FALSE}
#' to have the layers control always appear in its expanded state.
#' @param autoZIndex if \code{TRUE}, the control will automatically maintain
#' the z-order of its various groups as overlays are switched on and off.
#' @export
layersControlOptions = function(collapsed = TRUE, autoZIndex = TRUE) {
list(collapsed = collapsed, autoZIndex = autoZIndex)
}
#' @rdname addLayersControl
#' @export
removeLayersControl = function(map) {
invokeMethod(map, NULL, 'removeLayersControl')
}