protomapr/R/addProtomaps.R
2026-03-06 15:46:39 +11:00

435 lines
14 KiB
R

# Package-level constants for base flavor colors
.baseFlavors <- list(
light = list(
background = "#cccccc", earth = "#e0e0e0", park_a = "#cfddd5", park_b = "#9cd3b4",
hospital = "#e4dad9", industrial = "#d1dde1", school = "#e4ded7",
wood_a = "#d0ded0", wood_b = "#a0d9a0", pedestrian = "#e3e0d4",
scrub_a = "#cedcd7", scrub_b = "#99d2bb", glacier = "#e7e7e7",
sand = "#e2e0d7", beach = "#e8e4d0", aerodrome = "#dadbdf",
runway = "#e9e9ed", water = "#80deea", pier = "#e0e0e0",
zoo = "#c6dcdc", military = "#dcdcdc", ferry = "#5f9ea0",
boundary = "#aaaaaa", other = "#ebebeb", minor = "#ffffff",
link = "#ffffff", medium = "#f0eded", major = "#f5f5f5",
highway = "#ffffff", railway = "#a7b1b3",
ocean_label = "#5f9ea0", city_label = "#444444",
state_label = "#888888", country_label = "#666666"
),
dark = list(
background = "#2d2d2d", earth = "#3d3d3d", park_a = "#3a4a40", park_b = "#4a5a50",
hospital = "#4a4040", industrial = "#3a4044", school = "#4a4540",
wood_a = "#3a4a3a", wood_b = "#4a5a4a", pedestrian = "#3a3a35",
scrub_a = "#3a4540", scrub_b = "#4a5550", glacier = "#4a4a4a",
sand = "#4a4840", beach = "#4a4840", aerodrome = "#3a3a40",
runway = "#4a4a50", water = "#28404a", pier = "#3d3d3d",
zoo = "#3a4545", military = "#3a3a3a", ferry = "#3a5a6a",
boundary = "#555555", other = "#4a4a4a", minor = "#4a4a4a",
link = "#4a4a4a", medium = "#5a5a5a", major = "#5a5a5a",
highway = "#6a6a6a", railway = "#555555",
ocean_label = "#5a8a9a", city_label = "#cccccc",
state_label = "#888888", country_label = "#aaaaaa"
)
)
#' Add a Protomaps layer to a Leaflet map
#'
#' @description
#' Adds a vector tile layer from a PMTiles source to a Leaflet map using
#' the protomaps-leaflet library. Supports built-in flavors and custom
#' styling rules.
#'
#' @param map A leaflet map object created with \code{\link[leaflet]{leaflet}}.
#' @param url Character. URL to a PMTiles file or a tile endpoint with
#' \code{{z}/{x}/{y}.mvt} placeholders.
#' @param style Optional style object created with \code{\link{pmMinimal}} or
#' \code{\link{pmStyle}}. Provides a convenient way to apply preset styles.
#' If provided, overrides \code{colors} and \code{labelRules}.
#' @param flavor Character. Built-in flavor/theme to use. One of "light", "dark",
#' "white", "grayscale", or "black". Default is "light". Ignored if \code{style}
#' is provided.
#' @param colors Optional list of color overrides. Use \code{\link{pmColors}} to
#' create this. Overrides specific colors while keeping built-in rendering rules.
#' @param paintRules Optional list of paint rules created with \code{\link{pmPaintRule}}.
#' If provided, completely overrides the flavor's default paint rules.
#' For simple color changes, use \code{colors} instead.
#' @param labelRules Optional list of label rules created with \code{\link{pmLabelRule}}.
#' If provided, completely overrides the flavor's default label rules.
#' @param backgroundColor Character. Background color for the canvas.
#' Default is NULL (uses flavor default).
#' @param lang Character. Language code for labels (e.g., "en", "de", "zh").
#' Default is NULL (uses default language).
#' @param attribution Character. Attribution text for the layer.
#' Default is "Protomaps".
#' @param options A list of additional options created with \code{\link{protomapsOptions}}.
#' @param layerId Character. Layer ID for the protomaps layer.
#' @param group Character. Group name for layer control.
#'
#' @return A modified leaflet map object.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' # Basic usage with demo tiles
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(url = protomaps_demo_url())
#'
#' # Using dark flavor
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(url = protomaps_demo_url(), flavor = "dark")
#'
#' # Custom colors with proper rendering
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(
#' url = protomaps_demo_url(),
#' colors = pmColors(earth = "#d3d3d3", water = "#1a3a5c")
#' )
#'
#' # Using preset styles (recommended for common use cases)
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 10) %>%
#' addProtomaps(url = protomaps_demo_url(), style = pmStyle("minimal"))
#'
#' # Custom minimal style
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 10) %>%
#' addProtomaps(
#' url = protomaps_demo_url(),
#' style = pmMinimal(land = "#f5f5f0", water = "#1a3a5c", labels = TRUE)
#' )
#' }
#'
#' @export
#' @import leaflet
#' @importFrom htmlwidgets onRender
#' @importFrom jsonlite toJSON
addProtomaps <- function(map,
url,
style = NULL,
flavor = c("light", "dark", "white", "grayscale", "black"),
colors = NULL,
paintRules = NULL,
labelRules = NULL,
backgroundColor = NULL,
lang = NULL,
attribution = "Protomaps",
options = protomapsOptions(),
layerId = NULL,
group = NULL) {
flavor <- match.arg(flavor)
# Handle style parameter
if (!is.null(style)) {
if (inherits(style, "pm_style")) {
colors <- style$colors
labelRules <- style$labelRules
} else if (is.list(style)) {
if (!is.null(style$colors)) colors <- style$colors
if (!is.null(style$labelRules)) labelRules <- style$labelRules
}
}
# Build the options object for JavaScript
jsOptions <- list(
url = url,
flavor = flavor,
attribution = attribution
)
if (!is.null(colors)) {
jsOptions$customColors <- colors
jsOptions$baseFlavor <- .baseFlavors[[flavor]] %||% .baseFlavors$light
}
if (!is.null(backgroundColor)) {
jsOptions$backgroundColor <- backgroundColor
}
if (!is.null(lang)) {
jsOptions$lang <- lang
}
if (!is.null(paintRules)) {
jsOptions$paintRules <- paintRules
}
if (!is.null(labelRules)) {
jsOptions$labelRules <- labelRules
}
# Merge additional options
jsOptions <- c(jsOptions, options)
if (!is.null(layerId)) {
jsOptions$layerId <- layerId
}
if (!is.null(group)) {
jsOptions$group <- group
}
# Convert to JSON
jsOptionsJson <- jsonlite::toJSON(jsOptions, auto_unbox = TRUE, null = "null")
# Add the protomaps dependency to the widget
map$dependencies <- c(map$dependencies, list(protomapsDependency()))
# JavaScript code to add the layer
jsCode <- sprintf("
function(el, x) {
var map = this;
var options = %s;
// Helper to create symbolizer from config
var symTypes = {
polygon: protomapsL.PolygonSymbolizer,
line: protomapsL.LineSymbolizer,
circle: protomapsL.CircleSymbolizer,
text: protomapsL.TextSymbolizer,
centeredText: protomapsL.CenteredTextSymbolizer,
lineLabel: protomapsL.LineLabelSymbolizer,
shield: protomapsL.ShieldSymbolizer
};
function createSymbolizer(sym, defaultType) {
var Ctor = symTypes[sym.type] || symTypes[defaultType];
return new Ctor(sym.options || {});
}
function processRule(rule, defaultSymType) {
var result = {
dataLayer: rule.dataLayer,
symbolizer: createSymbolizer(rule.symbolizer, defaultSymType)
};
if (rule.minzoom !== null) result.minzoom = rule.minzoom;
if (rule.maxzoom !== null) result.maxzoom = rule.maxzoom;
if (rule.filter) {
try {
result.filter = new Function('zoom', 'feature', 'return ' + rule.filter);
} catch(e) {
console.error('Invalid filter:', rule.filter, e);
}
}
return result;
}
// Build layer options
var layerOptions = {
url: options.url,
attribution: options.attribution
};
// Handle custom colors - merge with base flavor
if (options.customColors && options.baseFlavor && !options.paintRules) {
var customFlavor = Object.assign({}, options.baseFlavor, options.customColors);
layerOptions.paintRules = protomapsL.paintRules(customFlavor);
layerOptions.labelRules = [];
if (customFlavor.background) {
layerOptions.backgroundColor = customFlavor.background;
}
}
else if (options.flavor && !options.paintRules && !options.customColors) {
layerOptions.flavor = options.flavor;
}
if (options.lang) layerOptions.lang = options.lang;
if (options.paintRules) {
layerOptions.paintRules = options.paintRules.map(function(r) {
return processRule(r, 'polygon');
});
}
if (options.labelRules) {
layerOptions.labelRules = options.labelRules.map(function(r) {
return processRule(r, 'centeredText');
});
}
if (options.backgroundColor) {
layerOptions.backgroundColor = options.backgroundColor;
}
if (options.maxDataZoom) {
layerOptions.maxDataZoom = options.maxDataZoom;
}
if (options.tileSize) {
layerOptions.tileSize = options.tileSize;
}
// Create and add the layer
var protoLayer = protomapsL.leafletLayer(layerOptions);
protoLayer.addTo(map);
// Store reference for potential later use
if (options.layerId) {
if (!map._protomapsLayers) map._protomapsLayers = {};
map._protomapsLayers[options.layerId] = protoLayer;
}
}
", jsOptionsJson)
htmlwidgets::onRender(map, jsCode)
}
#' Protomaps layer options
#'
#' @description
#' Create additional options for protomaps layer configuration.
#'
#' @param maxDataZoom Numeric. Maximum zoom level to fetch tile data.
#' Tiles beyond this zoom will be overzoomed.
#' @param tileSize Numeric. Size of tiles in pixels. Default is 256.
#' @param debug Logical. Enable debug mode to visualize tile boundaries.
#' @param ... Additional options passed to the layer.
#'
#' @return A list of options.
#'
#' @examples
#' protomapsOptions(maxDataZoom = 14, tileSize = 512)
#'
#' @export
protomapsOptions <- function(maxDataZoom = NULL,
tileSize = NULL,
debug = FALSE,
...) {
opts <- list(...)
if (!is.null(maxDataZoom)) opts$maxDataZoom <- maxDataZoom
if (!is.null(tileSize)) opts$tileSize <- tileSize
if (debug) opts$debug <- TRUE
opts
}
#' Create Protomaps HTML dependency
#'
#' @description
#' Creates the HTML dependency for the protomaps-leaflet JavaScript library.
#' This is automatically included when using \code{\link{addProtomaps}}.
#'
#' @param version Character. Version of protomaps-leaflet to use.
#' Default is "5.1.0".
#'
#' @return An htmltools::htmlDependency object.
#'
#' @examples
#' protomapsDependency()
#'
#' @export
#' @importFrom htmltools htmlDependency
protomapsDependency <- function(version = "5.1.0") {
htmltools::htmlDependency(
name = "protomaps-leaflet",
version = version,
src = system.file(
sprintf("htmlwidgets/lib/protomaps-leaflet-%s", version),
package = "protomapr"
),
script = "protomaps-leaflet.js"
)
}
#' Get Protomaps API tile URL
#'
#' @description
#' Returns a URL template for the Protomaps tile API. Requires an API key,
#' which can be passed directly or set via the \code{PROTOMAPS_API_KEY}
#' environment variable.
#'
#' Get a free API key (for non-commercial use) at \url{https://protomaps.com/}.
#' For commercial use or high traffic, consider self-hosting PMTiles files.
#'
#' @param api_key Character. Your Protomaps API key. If NULL (default),
#' uses the \code{PROTOMAPS_API_KEY} environment variable.
#'
#' @return Character. URL template for the tile API.
#'
#' @examples
#' \dontrun{
#' # Set your API key as an environment variable (recommended)
#' Sys.setenv(PROTOMAPS_API_KEY = "your-api-key-here")
#' leaflet() %>%
#' addProtomaps(url = protomaps_url())
#'
#' # Or pass the key directly
#' leaflet() %>%
#' addProtomaps(url = protomaps_url(api_key = "your-api-key-here"))
#' }
#'
#' @seealso \code{\link{set_protomaps_key}} for a convenient way to set the API key.
#'
#' @export
protomaps_url <- function(api_key = NULL) {
if (is.null(api_key)) {
api_key <- Sys.getenv("PROTOMAPS_API_KEY", unset = "")
}
if (api_key == "") {
stop(
"Protomaps API key required.\n",
"Get a free key at: https://protomaps.com/\n",
"Then either:\
",
"
- Set PROTOMAPS_API_KEY environment variable: Sys.setenv(PROTOMAPS_API_KEY = 'your-key')\n",
"
- Or pass directly: protomaps_url(api_key = 'your-key')\n",
"
- Or self-host PMTiles: see vignette('getting-started')",
call. = FALSE
)
}
sprintf("https://api.protomaps.com/tiles/v4/{z}/{x}/{y}.mvt?key=%s", api_key)
}
#' Set Protomaps API key
#'
#' @description
#' Convenience function to set your Protomaps API key for the current session.
#' The key is stored in the \code{PROTOMAPS_API_KEY} environment variable.
#'
#' For persistent storage, add to your \code{.Renviron} file:
#' \code{PROTOMAPS_API_KEY=your-key-here}
#'
#' Get a free API key at \url{https://protomaps.com/}.
#'
#' @param api_key Character. Your Protomaps API key.
#'
#' @return Invisibly returns the API key.
#'
#' @examples
#' \dontrun{
#' set_protomaps_key("your-api-key-here")
#'
#' # Now protomaps_url() will work without arguments
#' leaflet() %>%
#' addProtomaps(url = protomaps_url())
#' }
#'
#' @export
set_protomaps_key <- function(api_key) {
Sys.setenv(PROTOMAPS_API_KEY = api_key)
message("Protomaps API key set for this session.")
invisible(api_key)
}
#' @rdname protomaps_url
#' @export
protomaps_demo_url <- function(api_key = NULL) {
.Deprecated("protomaps_url")
protomaps_url(api_key = api_key)
}