This commit is contained in:
evmo 2026-03-06 15:46:39 +11:00
commit 116abafc09
58 changed files with 5749 additions and 0 deletions

435
R/addProtomaps.R Normal file
View file

@ -0,0 +1,435 @@
# 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)
}

147
R/colors.R Normal file
View file

@ -0,0 +1,147 @@
#' Create custom color overrides
#'
#' @description
#' Creates a list of color overrides that can be applied to a built-in flavor.
#' This is the recommended way to customize map colors while keeping the
#' proper rendering rules (zoom handling, polygon simplification, etc.).
#'
#' @param background Background color
#' @param earth Land/earth color
#' @param water Water color
#' @param park Park/green space color (also called park_a or park_b)
#' @param wood Forest/woodland color (also called wood_a or wood_b)
#' @param hospital Hospital area color
#' @param industrial Industrial area color
#' @param school School/university area color
#' @param beach Beach color
#' @param glacier Glacier color
#' @param highway Highway road color
#' @param major Major road color
#' @param minor Minor road color
#' @param city_label City label color
#' @param state_label State/region label color
#' @param country_label Country label color
#' @param ocean_label Ocean label color
#' @param ... Additional color properties
#'
#' @return A list of color overrides to pass to \code{\link{addProtomaps}}.
#'
#' @examples
#' # Simple earth and water colors
#' pmColors(earth = "#d3d3d3", water = "#1a3a5c")
#'
#' # Dark theme with custom colors
#' pmColors(
#' background = "#1a1a2e",
#' earth = "#1a1a2e",
#' water = "#16213e",
#' park = "#1f4037",
#' highway = "#4a4a6a"
#' )
#'
#' # Minimal grayscale
#' pmColors(
#' background = "#ffffff",
#' earth = "#f5f5f5",
#' water = "#e0e0e0"
#' )
#'
#' @seealso \code{\link{addProtomaps}}, \code{\link{protomaps_colors}}
#' @export
pmColors <- function(background = NULL,
earth = NULL,
water = NULL,
park = NULL,
wood = NULL,
hospital = NULL,
industrial = NULL,
school = NULL,
beach = NULL,
glacier = NULL,
highway = NULL,
major = NULL,
minor = NULL,
city_label = NULL,
state_label = NULL,
country_label = NULL,
ocean_label = NULL,
...) {
# Start with extra args, add named params, remove NULLs
colors <- c(
list(...),
list(
background = background, earth = earth, water = water,
hospital = hospital, industrial = industrial, school = school,
beach = beach, glacier = glacier, highway = highway, major = major,
city_label = city_label, state_label = state_label,
country_label = country_label, ocean_label = ocean_label,
# Expand paired colors
park_a = park, park_b = park,
wood_a = wood, wood_b = wood,
minor_a = minor, minor_b = minor
)
)
colors[!vapply(colors, is.null, logical(1))]
}
#' Protomaps Color Properties Reference
#'
#' @description
#' Reference documentation for all available color properties that can be
#' customized using \code{\link{pmColors}}.
#'
#' @section Base Colors:
#' \describe{
#' \item{\code{background}}{Map background color}
#' \item{\code{earth}}{Land/terrain color}
#' \item{\code{water}}{Water bodies color}
#' }
#'
#' @section Land Use Colors:
#' \describe{
#' \item{\code{park_a}, \code{park_b}}{Park colors (use \code{park} in pmColors)}
#' \item{\code{wood_a}, \code{wood_b}}{Forest/woodland colors (use \code{wood} in pmColors)}
#' \item{\code{hospital}}{Hospital areas}
#' \item{\code{industrial}}{Industrial zones}
#' \item{\code{school}}{Schools and universities}
#' \item{\code{beach}}{Beach areas}
#' \item{\code{zoo}}{Zoo areas}
#' \item{\code{aerodrome}}{Airport areas}
#' \item{\code{glacier}}{Glacier areas}
#' }
#'
#' @section Road Colors:
#' \describe{
#' \item{\code{highway}}{Highway/motorway color}
#' \item{\code{major}}{Major road color}
#' \item{\code{minor_a}, \code{minor_b}}{Minor road colors (use \code{minor} in pmColors)}
#' \item{\code{railway}}{Railway lines}
#' \item{\code{pier}}{Pier/dock structures}
#' }
#'
#' @section Label Colors:
#' \describe{
#' \item{\code{city_label}}{City name labels}
#' \item{\code{state_label}}{State/region labels}
#' \item{\code{country_label}}{Country name labels}
#' \item{\code{ocean_label}}{Ocean/sea labels}
#' \item{\code{roads_label_major}}{Major road name labels}
#' \item{\code{roads_label_minor}}{Minor road name labels}
#' }
#'
#' @section Landcover Colors (optional object):
#' These are specified as a nested object:
#' \describe{
#' \item{\code{grassland}}{Grassland areas}
#' \item{\code{barren}}{Barren land}
#' \item{\code{urban_area}}{Urban zones}
#' \item{\code{farmland}}{Agricultural areas}
#' \item{\code{forest}}{Forest areas}
#' \item{\code{scrub}}{Scrubland}
#' }
#'
#' @name protomaps_colors
#' @seealso \code{\link{pmColors}}, \code{\link{addProtomaps}}
NULL

109
R/layers.R Normal file
View file

@ -0,0 +1,109 @@
#' Protomaps Basemap Layers Reference
#'
#' @description
#' Reference documentation for the available layers and properties in the
#' Protomaps basemap. Use these layer names with \code{\link{pmPaintRule}}
#' and \code{\link{pmLabelRule}}, and filter on these properties.
#'
#' @section Layer Names:
#' The following layers are available for styling:
#'
#' \describe{
#' \item{\code{earth}}{Land polygons}
#' \item{\code{water}}{Water polygons, lines, and label points}
#' \item{\code{landuse}}{Parks, forests, residential areas, etc.}
#' \item{\code{roads}}{Streets, highways, paths}
#' \item{\code{buildings}}{Building footprints and addresses}
#' \item{\code{places}}{City, town, and region labels}
#' \item{\code{pois}}{Points of interest}
#' \item{\code{boundaries}}{Administrative boundaries}
#' \item{\code{natural}}{Natural features like peaks, forests}
#' \item{\code{transit}}{Transit stations and lines}
#' }
#'
#' @section Places Layer Properties:
#' Use these in filter expressions like \code{filter = "feature.props.kind === 'locality'"}
#'
#' \describe{
#' \item{\code{kind}}{Place type: "country", "region", "locality", "macrohood", "neighbourhood"}
#' \item{\code{kind_detail}}{Detailed type: "city", "town", "village", "hamlet", "state", "province", "country"}
#' \item{\code{name}}{Place name}
#' \item{\code{population}}{Population count (integer)}
#' \item{\code{population_rank}}{Population rank (integer, higher = larger)}
#' \item{\code{min_zoom}}{Minimum zoom level where label appears (lower = more important)}
#' \item{\code{capital}}{Capital status (string)}
#' \item{\code{wikidata}}{Wikidata ID}
#' }
#'
#' @section Water Layer Properties:
#' \describe{
#' \item{\code{kind}}{Water type: "water", "lake", "playa", "ocean", "other"}
#' \item{\code{kind_detail}}{Detailed type: "basin", "canal", "ditch", "dock", "drain", "lake", "reservoir", "river", "riverbank", "stream"}
#' \item{\code{name}}{Water body name}
#' \item{\code{intermittent}}{Boolean, seasonal water}
#' \item{\code{reservoir}}{Boolean}
#' \item{\code{alkaline}}{Boolean}
#' }
#'
#' @section Roads Layer Properties:
#' \describe{
#' \item{\code{kind}}{Road class: "highway", "major_road", "medium_road", "minor_road", "path"}
#' \item{\code{kind_detail}}{Detailed type: "motorway", "trunk", "primary", "secondary", "tertiary", "residential", "service", "pedestrian", "footway", "cycleway"}
#' \item{\code{ref}}{Road reference number (e.g., "I-80", "US-101")}
#' \item{\code{name}}{Street name}
#' \item{\code{oneway}}{Boolean}
#' \item{\code{is_bridge}}{Boolean}
#' \item{\code{is_tunnel}}{Boolean}
#' }
#'
#' @section Landuse Layer Properties:
#' \describe{
#' \item{\code{kind}}{Land use type: "park", "forest", "residential", "commercial", "industrial", "aerodrome", "cemetery", "hospital", "school", "stadium", "zoo"}
#' \item{\code{sport}}{Sport type for sports facilities}
#' }
#'
#' @section Buildings Layer Properties:
#' \describe{
#' \item{\code{kind}}{Building type: "address", "building", "building_part"}
#' \item{\code{height}}{Building height in meters}
#' \item{\code{min_height}}{Base height for building parts}
#' \item{\code{addr_housenumber}}{Street address number}
#' }
#'
#' @section POIs Layer Properties:
#' \describe{
#' \item{\code{kind}}{POI type: "cafe", "restaurant", "hospital", "school", "bank", "pharmacy", "hotel", etc.}
#' \item{\code{name}}{POI name}
#' \item{\code{cuisine}}{Cuisine type for restaurants}
#' \item{\code{religion}}{Religion for places of worship}
#' }
#'
#' @section Filter Examples:
#' \preformatted{
#' # Major cities only
#' filter = "feature.props.kind_detail === 'city'"
#'
#' # States/provinces
#' filter = "feature.props.kind === 'region'"
#'
#' # Important places (low min_zoom = important)
#' filter = "feature.props.min_zoom <= 6"
#'
#' # Large cities by population rank
#' filter = "feature.props.population_rank >= 10"
#'
#' # Highways only
#' filter = "feature.props.kind === 'highway'"
#'
#' # Parks
#' filter = "feature.props.kind === 'park'"
#'
#' # Combine conditions
#' filter = "feature.props.kind_detail === 'city' && feature.props.min_zoom <= 8"
#' }
#'
#' @name protomaps_layers
#' @aliases layers
#' @seealso \code{\link{pmPaintRule}}, \code{\link{pmLabelRule}}
#' @references \url{https://docs.protomaps.com/basemaps/layers}
NULL

180
R/palette.R Normal file
View file

@ -0,0 +1,180 @@
#' Apply Color Palette to Land Use Categories
#'
#' @description
#' Maps colors from a palette to land use categories, enabling use of
#' viridis, RColorBrewer, and other R color palettes with Protomaps.
#'
#' @param palette Character vector of colors, or a function that generates colors.
#' Can be output from viridis::viridis(), RColorBrewer::brewer.pal(), etc.
#' @param categories Character vector of category names to map colors to.
#' Default maps to common land use types: water, park, wood, residential,
#' commercial, industrial.
#' @param n Integer. Number of colors to generate if palette is a function.
#' Default is NULL (uses length of categories).
#' @param background Character. Background/default color for unassigned categories.
#' Default is "#f8f8f8".
#'
#' @return A list of color mappings suitable for pmColors() or addProtomaps(colors=).
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' # Using viridis palette for land use
#' if (requireNamespace("viridisLite", quietly = TRUE)) {
#' colors <- pmPalette(viridisLite::viridis(6))
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(url = protomaps_url(), colors = colors)
#' }
#'
#' # Using RColorBrewer
#' if (requireNamespace("RColorBrewer", quietly = TRUE)) {
#' colors <- pmPalette(RColorBrewer::brewer.pal(6, "Set2"))
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(url = protomaps_url(), colors = colors)
#' }
#'
#' # Custom category mapping
#' colors <- pmPalette(
#' c("#264653", "#2a9d8f", "#e9c46a", "#f4a261", "#e76f51"),
#' categories = c("water", "park", "sand", "buildings", "highway")
#' )
#' }
#'
#' @seealso \code{\link{pmPaletteStyle}}, \code{\link{pmColors}}
#' @export
pmPalette <- function(palette,
categories = NULL,
n = NULL,
background = "#f8f8f8") {
if (is.null(categories)) {
categories <- c("water", "park", "wood", "residential",
"commercial", "industrial")
}
if (is.function(palette)) {
n_colors <- n %||% length(categories)
colors <- palette(n_colors)
} else {
colors <- palette
}
if (length(colors) < length(categories)) {
colors <- rep_len(colors, length(categories))
}
result <- list(background = background, earth = background)
category_mapping <- list(
water = "water",
park = c("park_a", "park_b"),
wood = c("wood_a", "wood_b"),
forest = c("wood_a", "wood_b"),
residential = "pedestrian",
commercial = "other",
industrial = "industrial",
hospital = "hospital",
school = "school",
beach = "beach",
sand = "sand",
buildings = "buildings",
highway = "highway",
roads = c("highway", "major", "medium", "minor"),
railway = "railway"
)
for (i in seq_along(categories)) {
cat_name <- categories[i]
color <- colors[i]
if (cat_name %in% names(category_mapping)) {
props <- category_mapping[[cat_name]]
for (prop in props) {
result[[prop]] <- color
}
} else {
result[[cat_name]] <- color
}
}
result
}
#' Create a Themed Palette Style
#'
#' @description
#' Creates a complete pm_style using a color palette. Combines pmPalette()
#' with styling for a consistent look.
#'
#' @param palette Character vector of colors or palette function.
#' @param water_color Character. Color for water features. If NULL, uses
#' first color from palette.
#' @param land_color Character. Color for land/background. Default is "#f8f8f8".
#' @param labels Logical. Whether to include city labels. Default is TRUE.
#' @param label_color Character. Color for labels. Default is "#333333".
#'
#' @return A pm_style object.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' # Viridis-themed map
#' if (requireNamespace("viridisLite", quietly = TRUE)) {
#' style <- pmPaletteStyle(viridisLite::viridis(5, option = "D"))
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(url = protomaps_url(), style = style)
#' }
#'
#' # Custom palette
#' style <- pmPaletteStyle(
#' c("#264653", "#2a9d8f", "#e9c46a", "#f4a261", "#e76f51"),
#' water_color = "#264653"
#' )
#' }
#'
#' @seealso \code{\link{pmPalette}}, \code{\link{pmStyle}}
#' @export
pmPaletteStyle <- function(palette,
water_color = NULL,
land_color = "#f8f8f8",
labels = TRUE,
label_color = "#333333") {
if (is.function(palette)) {
colors <- palette(5)
} else {
colors <- palette
}
water <- water_color %||% colors[1]
base_colors <- pmPalette(colors, background = land_color)
base_colors$water <- water
base_colors$earth <- land_color
base_colors$background <- land_color
labelRules <- list()
if (labels) {
labelRules <- list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 12px sans-serif",
fill = label_color,
stroke = land_color,
width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 6")
)
}
structure(
list(colors = base_colors, labelRules = labelRules),
class = "pm_style"
)
}

44
R/protomapr-package.R Normal file
View file

@ -0,0 +1,44 @@
#' protomapr: Add Protomaps Layers to Leaflet Maps
#'
#' The protomapr package provides functions to add Protomaps vector tile
#' layers to leaflet maps in R. Unlike raster tile providers, Protomaps
#' offers full customization of colors and features, self-hosting from a
#' single PMTiles file, and smooth vector rendering at any zoom level.
#' See \code{vignette("getting-started")} for why you might choose Protomaps
#' over standard provider tiles.
#'
#' @section Main Functions:
#' \describe{
#' \item{\code{\link{addProtomaps}}}{Add a Protomaps layer to a Leaflet map}
#' \item{\code{\link{protomapsOptions}}}{Configure additional layer options}
#' }
#'
#' @section Symbolizers:
#' \describe{
#' \item{\code{\link{pmPolygonSymbolizer}}}{Style polygon features}
#' \item{\code{\link{pmLineSymbolizer}}}{Style line features}
#' \item{\code{\link{pmCircleSymbolizer}}}{Style point features as circles}
#' \item{\code{\link{pmTextSymbolizer}}}{Add text labels}
#' \item{\code{\link{pmCenteredTextSymbolizer}}}{Add centered text labels}
#' \item{\code{\link{pmLineLabelSymbolizer}}}{Add labels along lines}
#' \item{\code{\link{pmShieldSymbolizer}}}{Add shield/badge labels}
#' }
#'
#' @section Rules:
#' \describe{
#' \item{\code{\link{pmPaintRule}}}{Define how to paint features}
#' \item{\code{\link{pmLabelRule}}}{Define how to label features}
#' }
#'
#' @section Available Themes:
#' The following built-in themes are available:
#' \itemize{
#' \item \code{"light"} - General-purpose light basemap
#' \item \code{"dark"} - General-purpose dark basemap
#' \item \code{"white"} - High-contrast white theme for data visualization
#' \item \code{"grayscale"} - Monochromatic theme
#' \item \code{"black"} - Dark theme for data visualization
#' }
#'
#' @keywords internal
"_PACKAGE"

106
R/rules.R Normal file
View file

@ -0,0 +1,106 @@
#' Create a Paint Rule
#'
#' @description
#' Creates a paint rule that specifies how to render features from a
#' particular data layer. Paint rules control the visual appearance of
#' polygon, line, and point features.
#'
#' @param dataLayer Character. The name of the data layer in the vector
#' tile source (e.g., "water", "earth", "roads").
#' @param symbolizer A symbolizer object created with one of the symbolizer
#' functions (e.g., \code{\link{pmPolygonSymbolizer}},
#' \code{\link{pmLineSymbolizer}}).
#' @param minzoom Numeric. Minimum zoom level at which this rule applies.
#' Default is NULL (applies at all zoom levels).
#' @param maxzoom Numeric. Maximum zoom level at which this rule applies.
#' Default is NULL (applies at all zoom levels).
#' @param filter Character. A JavaScript expression string that filters
#' features. The expression has access to \code{zoom} and \code{feature}
#' variables. Default is NULL (no filter).
#'
#' @return A list representing the paint rule configuration.
#'
#' @examples
#' # Render water polygons in blue
#' pmPaintRule("water", pmPolygonSymbolizer(fill = "steelblue"))
#'
#' # Render roads with zoom-dependent visibility
#' pmPaintRule("roads", pmLineSymbolizer(color = "gray", width = 2),
#' minzoom = 10)
#'
#' # Filter to only show highways
#' pmPaintRule("roads",
#' pmLineSymbolizer(color = "orange", width = 4),
#' filter = "feature.props.kind === 'highway'")
#'
#' @export
pmPaintRule <- function(dataLayer,
symbolizer,
minzoom = NULL,
maxzoom = NULL,
filter = NULL) {
rule <- list(
dataLayer = dataLayer,
symbolizer = symbolizer
)
if (!is.null(minzoom)) rule$minzoom <- minzoom
if (!is.null(maxzoom)) rule$maxzoom <- maxzoom
if (!is.null(filter)) rule$filter <- filter
rule
}
#' Create a Label Rule
#'
#' @description
#' Creates a label rule that specifies how to render text labels for
#' features from a particular data layer. Label rules control text
#' placement and styling, with automatic collision detection.
#'
#' @param dataLayer Character. The name of the data layer in the vector
#' tile source (e.g., "places", "roads").
#' @param symbolizer A text symbolizer object created with one of
#' \code{\link{pmTextSymbolizer}}, \code{\link{pmCenteredTextSymbolizer}},
#' \code{\link{pmLineLabelSymbolizer}}, or \code{\link{pmShieldSymbolizer}}.
#' @param minzoom Numeric. Minimum zoom level at which this rule applies.
#' Default is NULL (applies at all zoom levels).
#' @param maxzoom Numeric. Maximum zoom level at which this rule applies.
#' Default is NULL (applies at all zoom levels).
#' @param filter Character. A JavaScript expression string that filters
#' features. Default is NULL (no filter).
#'
#' @return A list representing the label rule configuration.
#'
#' @examples
#' # Label cities
#' pmLabelRule("places",
#' pmCenteredTextSymbolizer(font = "14px Arial",
#' fill = "black",
#' stroke = "white",
#' width = 2))
#'
#' # Label streets along their paths
#' pmLabelRule("roads",
#' pmLineLabelSymbolizer(font = "11px Arial",
#' fill = "#333"),
#' minzoom = 14)
#'
#' @export
pmLabelRule <- function(dataLayer,
symbolizer,
minzoom = NULL,
maxzoom = NULL,
filter = NULL) {
rule <- list(
dataLayer = dataLayer,
symbolizer = symbolizer
)
if (!is.null(minzoom)) rule$minzoom <- minzoom
if (!is.null(maxzoom)) rule$maxzoom <- maxzoom
if (!is.null(filter)) rule$filter <- filter
rule
}

128
R/sample-tiles.R Normal file
View file

@ -0,0 +1,128 @@
#' Get Path to Sample PMTiles File
#'
#' @description
#' Returns the path to a sample PMTiles file for demos and testing.
#' On first use, downloads a small regional extract to the user's cache
#' directory.
#'
#' @param region Character. Region to download. Currently only "sf-bay"
#' (San Francisco Bay Area) is available. Default is "sf-bay".
#' @param cache_dir Character. Directory to cache the downloaded file.
#' Default uses \code{tools::R_user_dir()}.
#' @param force_download Logical. Force re-download even if cached.
#' Default is FALSE.
#'
#' @return Character. Path to the PMTiles file.
#'
#' @details
#' The sample tiles are hosted on GitHub releases and downloaded on first use.
#' Subsequent calls use the cached file. The SF Bay Area extract is
#' approximately 10-15MB and covers the greater San Francisco region at all
#' zoom levels.
#'
#' For production use, consider self-hosting your own PMTiles file. See
#' \code{vignette("getting-started")} for options.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' # Use sample tiles for demos (downloads on first use)
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(url = protomaps_sample_tiles())
#' }
#'
#' @seealso \code{\link{protomaps_clear_cache}}, \code{\link{protomaps_url}}
#' @export
protomaps_sample_tiles <- function(region = "sf-bay",
cache_dir = NULL,
force_download = FALSE) {
region <- match.arg(region, choices = c("sf-bay"))
if (is.null(cache_dir)) {
cache_dir <- tools::R_user_dir("protomapr", which = "cache")
}
if (!dir.exists(cache_dir)) {
dir.create(cache_dir, recursive = TRUE)
}
filename <- sprintf("protomapr-sample-%s.pmtiles", region)
local_path <- file.path(cache_dir, filename)
if (file.exists(local_path) && !force_download) {
message("Using cached sample tiles: ", local_path)
return(local_path)
}
base_url <- "https://github.com/evmo/protomapr/releases/download"
version <- "sample-tiles-v1"
download_url <- sprintf("%s/%s/%s", base_url, version, filename)
message("Downloading sample tiles (~10MB)...")
message("Source: ", download_url)
tryCatch({
utils::download.file(
url = download_url,
destfile = local_path,
mode = "wb",
quiet = FALSE
)
message("Downloaded to: ", local_path)
}, error = function(e) {
stop(
"Failed to download sample tiles.\n",
"Error: ", conditionMessage(e), "\n",
"Try again later or use protomaps_url() with an API key instead.",
call. = FALSE
)
})
local_path
}
#' Clear Cached Sample Tiles
#'
#' @description
#' Removes cached sample PMTiles files to free disk space.
#'
#' @param cache_dir Character. Cache directory. Default uses same as
#' \code{\link{protomaps_sample_tiles}}.
#'
#' @return Invisibly returns TRUE if files were removed, FALSE otherwise.
#'
#' @examples
#' \dontrun{
#' # Clear all cached tiles
#' protomaps_clear_cache()
#' }
#'
#' @seealso \code{\link{protomaps_sample_tiles}}
#' @export
protomaps_clear_cache <- function(cache_dir = NULL) {
if (is.null(cache_dir)) {
cache_dir <- tools::R_user_dir("protomapr", which = "cache")
}
if (!dir.exists(cache_dir)) {
message("Cache directory does not exist: ", cache_dir)
return(invisible(FALSE))
}
files <- list.files(cache_dir, pattern = "\\.pmtiles$", full.names = TRUE)
if (length(files) == 0) {
message("No cached tiles found.")
return(invisible(FALSE))
}
unlink(files)
message(sprintf("Removed %d cached file(s).", length(files)))
invisible(TRUE)
}

673
R/styles.R Normal file
View file

@ -0,0 +1,673 @@
#' Create a minimal basemap style
#'
#' @description
#' Creates a minimal style with uniform land color, hiding roads, buildings,
#' and most labels. Ideal for data visualization overlays.
#'
#' @param land Character. Color for all land features. Default is "#f8f8f8".
#' @param water Character. Color for water features. Default is "#e0e8f0".
#' @param labels Logical. Whether to show city labels. Default is FALSE.
#' @param label_color Character. Color for labels if shown. Default is "#666666".
#'
#' @return A list with `colors` and `labelRules` components to pass to
#' \code{\link{addProtomaps}}.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' # Ultra-minimal basemap
#' style <- pmMinimal()
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 10) %>%
#' addProtomaps(url = protomaps_url(), style = style)
#'
#' # Custom colors with major city labels
#' style <- pmMinimal(land = "#f5f5f0", water = "#1a3a5c", labels = TRUE)
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 8) %>%
#' addProtomaps(url = protomaps_url(), style = style)
#' }
#'
#' @seealso \code{\link{pmStyle}}, \code{\link{addProtomaps}}
#' @export
pmMinimal <- function(land = "#f8f8f8",
water = "#e0e8f0",
labels = FALSE,
label_color = "#666666") {
colors <- pmColors(
background = land,
earth = land,
water = water,
# Land use - all same as land
park = land,
wood = land,
scrub_a = land,
scrub_b = land,
glacier = land,
sand = land,
beach = land,
hospital = land,
school = land,
industrial = land,
pedestrian = land,
zoo = land,
military = land,
aerodrome = land,
# Roads - hidden
highway = land,
major = land,
medium = land,
minor = land,
link = land,
other = land,
railway = land,
pier = land,
boundary = land,
# Buildings - hidden
buildings = land
)
labelRules <- list()
if (labels) {
labelRules <- list(
# Major cities only
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 12px sans-serif",
fill = label_color,
stroke = land,
width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 6")
)
}
structure(
list(colors = colors, labelRules = labelRules),
class = "pm_style"
)
}
#' Get a preset map style
#'
#' @description
#' Returns a preset style configuration. Available presets provide common
#' styling patterns without manual configuration.
#'
#' @param name Character. Name of the preset style. One of:
#' \describe{
#' \item{"minimal"}{Light gray land, light blue water, no labels}
#' \item{"minimal-dark"}{Dark land, dark blue water, no labels}
#' \item{"muted"}{Subtle colors, faint roads, major labels only}
#' \item{"watercolor"}{Soft, painterly aesthetic}
#' \item{"ink"}{Black lines on white, like a pen drawing}
#' \item{"terrain"}{Earthy tones with subtle elevation feel}
#' \item{"transit"}{Muted base with emphasized rail lines}
#' }
#'
#' @return A list with style components to pass to \code{\link{addProtomaps}}.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 10) %>%
#' addProtomaps(url = protomaps_url(), style = pmStyle("minimal"))
#'
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 10) %>%
#' addProtomaps(url = protomaps_url(), style = pmStyle("watercolor"))
#' }
#'
#' @seealso \code{\link{pmMinimal}}, \code{\link{addProtomaps}}
#' @export
pmStyle <- function(name = c("minimal", "minimal-dark", "muted", "watercolor",
"ink", "terrain", "transit")) {
name <- match.arg(name)
switch(name,
"minimal" = pmMinimal(),
"minimal-dark" = pmMinimal(land = "#1a1a1a", water = "#0d1520", label_color = "#888888"),
"muted" = {
colors <- pmColors(
background = "#fafafa",
earth = "#fafafa",
water = "#e8f0f4",
park = "#f0f4f0",
wood = "#eef2ee",
scrub_a = "#f5f5f5",
scrub_b = "#f5f5f5",
glacier = "#f8f8f8",
sand = "#f8f6f2",
beach = "#f8f6f2",
hospital = "#fafafa",
school = "#fafafa",
industrial = "#f5f5f5",
pedestrian = "#f8f8f8",
zoo = "#f0f4f0",
military = "#f5f5f5",
aerodrome = "#f5f5f5",
# Faint roads
highway = "#e0e0e0",
major = "#e8e8e8",
medium = "#f0f0f0",
minor = "#f5f5f5",
link = "#f5f5f5",
other = "#f8f8f8",
railway = "#e8e8e8",
pier = "#f0f0f0",
boundary = "#e8e8e8",
buildings = "#f0f0f0"
)
labelRules <- list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 11px sans-serif",
fill = "#666666",
stroke = "#fafafa",
width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 6"),
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "italic 10px sans-serif",
fill = "#888888",
stroke = "#fafafa",
width = 1
), filter = "feature.props.kind === 'region'")
)
structure(list(colors = colors, labelRules = labelRules), class = "pm_style")
},
"watercolor" = {
bg <- "#f4f1ea"
colors <- pmColors(
background = bg,
earth = bg,
water = "#a8c8d4",
park = "#d4e6c3",
wood = "#c8ddb8",
scrub_a = "#dce8d0",
scrub_b = "#d0e0c4",
glacier = "#e8f0f4",
sand = "#f0e8d8",
beach = "#f5e6c8",
hospital = "#f0ebe4",
school = "#f0ebe4",
industrial = "#ebe6de",
pedestrian = "#f0ece4",
zoo = "#dce8d0",
military = "#e8e4dc",
aerodrome = "#ebe6de",
highway = "#e8e4dc",
major = "#ebe7df",
medium = "#f0ece4",
minor = bg,
link = bg,
other = bg,
railway = "#e0dcd4",
pier = "#e8e4dc",
boundary = "#e0dcd4",
buildings = "#e8e4dc"
)
labelRules <- list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "italic 13px Georgia, serif",
fill = "#5c5c5c",
stroke = bg,
width = 2
), filter = "feature.props.min_zoom <= 6"),
pmLabelRule("water", pmCenteredTextSymbolizer(
font = "italic 11px Georgia, serif",
fill = "#4a7c8c",
stroke = "#a8c8d4",
width = 1,
lineHeight = 1.5
))
)
structure(list(colors = colors, labelRules = labelRules), class = "pm_style")
},
"ink" = {
bg <- "#ffffff"
colors <- pmColors(
background = bg,
earth = bg,
water = bg,
park = bg, wood = bg, scrub_a = bg, scrub_b = bg,
glacier = bg, sand = bg, beach = bg,
hospital = bg, school = bg, industrial = bg,
pedestrian = bg, zoo = bg, military = bg, aerodrome = bg,
highway = "#000000",
major = "#000000",
medium = "#333333",
minor = "#666666",
link = "#666666",
other = "#999999",
railway = "#000000",
pier = "#666666",
boundary = "#cccccc",
buildings = bg
)
labelRules <- list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 12px 'Courier New', monospace",
fill = "#000000",
stroke = bg,
width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 6")
)
structure(list(colors = colors, labelRules = labelRules), class = "pm_style")
},
"terrain" = {
bg <- "#f4f0e8"
colors <- pmColors(
background = bg,
earth = bg,
water = "#a8c8d8",
park = "#d4e4c8",
wood = "#c8dcc0",
scrub_a = "#e0e8d8",
scrub_b = "#d8e0d0",
glacier = "#e8f0f4",
sand = "#f0e8d4",
beach = "#f4ecd8",
hospital = "#f0ece8",
school = "#f0ece8",
industrial = "#e8e4dc",
pedestrian = "#f0ece4",
zoo = "#d8e4d0",
military = "#e4e0d8",
aerodrome = "#e8e4dc",
highway = "#d8d4c8",
major = "#e0dcd0",
medium = "#e8e4d8",
minor = "#f0ece4",
link = "#f0ece4",
other = bg,
railway = "#c8c4b8",
pier = "#e0dcd0",
boundary = "#c8c4b8",
buildings = "#e8e4dc"
)
labelRules <- list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 12px sans-serif",
fill = "#5a5040",
stroke = bg,
width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 6"),
pmLabelRule("natural", pmCenteredTextSymbolizer(
font = "italic 10px sans-serif",
fill = "#6a6050",
stroke = bg,
width = 1
))
)
structure(list(colors = colors, labelRules = labelRules), class = "pm_style")
},
"transit" = {
bg <- "#fafafa"
colors <- pmColors(
background = bg,
earth = bg,
water = "#e0e8f0",
park = "#f0f4f0",
wood = "#eef2ee",
scrub_a = "#f5f5f5",
scrub_b = "#f5f5f5",
glacier = "#f8f8f8",
sand = "#f8f6f2",
beach = "#f8f6f2",
hospital = bg,
school = bg,
industrial = "#f5f5f5",
pedestrian = "#f8f8f8",
zoo = "#f0f4f0",
military = "#f5f5f5",
aerodrome = "#f0f0f4",
highway = "#e8e8e8",
major = "#f0f0f0",
medium = "#f5f5f5",
minor = bg,
link = bg,
other = bg,
railway = "#e63946",
pier = "#f0f0f0",
boundary = "#e8e8e8",
buildings = "#f0f0f0"
)
labelRules <- list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 11px sans-serif",
fill = "#333333",
stroke = bg,
width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 6"),
pmLabelRule("transit", pmCenteredTextSymbolizer(
font = "600 10px sans-serif",
fill = "#e63946",
stroke = bg,
width = 2
))
)
structure(list(colors = colors, labelRules = labelRules), class = "pm_style")
}
)
}
#' Create color overrides to hide specific features
#'
#' @description
#' Creates color settings that hide specified feature categories by making
#' them match the background color.
#'
#' @param features Character vector. Features to hide. Options include:
#' "roads", "buildings", "landuse", "boundaries", "labels".
#' @param background Character. Background color that hidden features will
#' match. Default is "#f8f8f8".
#'
#' @return A list of color overrides to pass to \code{\link{pmColors}} or
#' merge with other colors.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' # Hide roads and buildings but keep parks visible
#' hidden <- pmHideFeatures(c("roads", "buildings"))
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 12) %>%
#' addProtomaps(
#' url = protomaps_url(),
#' colors = modifyList(pmColors(water = "#1a3a5c"), hidden)
#' )
#' }
#'
#' @export
pmHideFeatures <- function(features, background = "#f8f8f8") {
colors <- list()
if ("roads" %in% features) {
colors <- c(colors, list(
highway = background, major = background, medium = background,
minor = background, link = background, other = background,
railway = background, pier = background
))
}
if ("buildings" %in% features) {
colors <- c(colors, list(buildings = background))
}
if ("landuse" %in% features) {
colors <- c(colors, list(
park_a = background, park_b = background,
wood_a = background, wood_b = background,
scrub_a = background, scrub_b = background,
glacier = background, sand = background, beach = background,
hospital = background, school = background, industrial = background,
pedestrian = background, zoo = background, military = background,
aerodrome = background
))
}
if ("boundaries" %in% features) {
colors <- c(colors, list(boundary = background))
}
colors
}
#' Create preset label rules for city names
#'
#' @description
#' Creates label rules for displaying city/place names with common styling
#' patterns.
#'
#' @param style Character. Label style preset:
#' \describe{
#' \item{"hierarchical"}{Size varies by city importance (min_zoom)}
#' \item{"major-only"}{Only major cities (min_zoom <= 5)}
#' \item{"all"}{All cities with uniform styling}
#' }
#' @param color Character. Text color. Default is "#333333".
#' @param halo Character. Halo/stroke color. Default is "white".
#' @param include_regions Logical. Include state/region labels. Default is TRUE.
#'
#' @return A list of label rules to pass to \code{\link{addProtomaps}}.
#'
#' @examples
#' \dontrun{
#' library(leaflet)
#' library(protomapr)
#'
#' leaflet() %>%
#' setView(lng = -122.4, lat = 37.8, zoom = 8) %>%
#' addProtomaps(
#' url = protomaps_url(),
#' colors = pmColors(earth = "#f0f0f0", water = "#1a3a5c"),
#' labelRules = pmCityLabels("hierarchical")
#' )
#' }
#'
#' @export
pmCityLabels <- function(style = c("hierarchical", "major-only", "all"),
color = "#333333",
halo = "white",
include_regions = TRUE) {
style <- match.arg(style)
rules <- switch(style,
"hierarchical" = list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "600 16px sans-serif", fill = color, stroke = halo, width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 4"),
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "600 13px sans-serif", fill = color, stroke = halo, width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom > 4 && feature.props.min_zoom <= 6"),
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 11px sans-serif", fill = color, stroke = halo, width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom > 6 && feature.props.min_zoom <= 8"),
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "400 9px sans-serif", fill = color, stroke = halo, width = 1
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom > 8")
),
"major-only" = list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "600 14px sans-serif", fill = color, stroke = halo, width = 2
), filter = "feature.props.kind === 'locality' && feature.props.min_zoom <= 5")
),
"all" = list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "500 11px sans-serif", fill = color, stroke = halo, width = 2
), filter = "feature.props.kind === 'locality'")
)
)
if (include_regions) {
rules <- c(rules, list(
pmLabelRule("places", pmCenteredTextSymbolizer(
font = "italic 12px sans-serif",
fill = "#888888",
stroke = halo,
width = 1
), filter = "feature.props.kind === 'region'")
))
}
rules
}
#' Print method for pm_style objects
#'
#' @description
#' Prints a formatted summary of a pm_style object showing colors and label rules.
#'
#' @param x A pm_style object.
#' @param ... Additional arguments (ignored).
#'
#' @return Invisibly returns x.
#'
#' @export
print.pm_style <- function(x, ...) {
cat("<pm_style>\n")
if (!is.null(x$colors) && length(x$colors) > 0) {
cat("\nColors:\n")
base_names <- c("background", "earth", "water")
road_names <- c("highway", "major", "medium", "minor", "link", "other", "railway", "pier")
landuse_names <- c("park", "park_a", "park_b", "wood", "wood_a", "wood_b",
"scrub_a", "scrub_b", "glacier", "sand", "beach",
"hospital", "school", "industrial", "pedestrian",
"zoo", "military", "aerodrome")
# Base colors
base <- x$colors[names(x$colors) %in% base_names]
if (length(base) > 0) {
cat(" Base:\n")
for (nm in intersect(base_names, names(base))) {
cat(sprintf(" %s: %s\n", nm, base[[nm]]))
}
}
# Roads - summarize if uniform
roads <- x$colors[names(x$colors) %in% road_names]
if (length(roads) > 0) {
unique_colors <- unique(unlist(roads))
if (length(unique_colors) == 1) {
cat(sprintf(" Roads: %s (uniform)\n", unique_colors))
} else {
cat(" Roads:\n")
for (nm in intersect(road_names, names(roads))) {
cat(sprintf(" %s: %s\n", nm, roads[[nm]]))
}
}
}
# Land use - summarize if uniform
landuse <- x$colors[names(x$colors) %in% landuse_names]
if (length(landuse) > 0) {
unique_colors <- unique(unlist(landuse))
if (length(unique_colors) == 1) {
cat(sprintf(" Land use: %s (uniform)\n", unique_colors))
} else {
n_landuse <- length(landuse)
cat(sprintf(" Land use: %d custom colors\n", n_landuse))
}
}
# Other colors
other_names <- setdiff(names(x$colors), c(base_names, road_names, landuse_names))
if (length(other_names) > 0) {
cat(sprintf(" Other: %d additional colors\n", length(other_names)))
}
}
if (!is.null(x$labelRules) && length(x$labelRules) > 0) {
cat(sprintf("\nLabel Rules: %d\n", length(x$labelRules)))
for (i in seq_along(x$labelRules)) {
rule <- x$labelRules[[i]]
cat(sprintf(" [%d] %s", i, rule$dataLayer))
if (!is.null(rule$filter)) {
filter_display <- if (nchar(rule$filter) > 40) {
paste0(substr(rule$filter, 1, 37), "...")
} else {
rule$filter
}
cat(sprintf(" | %s", filter_display))
}
cat("\n")
}
} else {
cat("\nLabel Rules: none\n")
}
invisible(x)
}
#' Modify an Existing Style
#'
#' @description
#' Creates a new pm_style by modifying an existing one. Useful for tweaking
#' preset styles without rebuilding from scratch.
#'
#' @param style A pm_style object to modify.
#' @param colors Named list of color overrides (from pmColors() or manual list).
#' @param labelRules Optional list of label rules to replace or add.
#' @param replace_labels Logical. If TRUE, replaces all label rules. If FALSE,
#' appends new rules. Default is FALSE.
#' @param ... Additional color overrides as named arguments.
#'
#' @return A new pm_style object with modifications applied.
#'
#' @examples
#' \dontrun{
#' # Start with watercolor, change water color
#' my_style <- pmModifyStyle(pmStyle("watercolor"), water = "#1a3a5c")
#'
#' # Add label rules to minimal style
#' my_style <- pmModifyStyle(
#' pmMinimal(),
#' labelRules = pmCityLabels("major-only")
#' )
#'
#' # Multiple modifications
#' my_style <- pmModifyStyle(
#' pmStyle("muted"),
#' colors = pmColors(water = "#2a4a6c", park = "#c0d8c0"),
#' replace_labels = TRUE,
#' labelRules = list(
#' pmLabelRule("places", pmCenteredTextSymbolizer(
#' font = "bold 14px Arial",
#' fill = "#333"
#' ))
#' )
#' )
#' }
#'
#' @seealso \code{\link{pmStyle}}, \code{\link{pmMinimal}}
#' @export
pmModifyStyle <- function(style,
colors = NULL,
labelRules = NULL,
replace_labels = FALSE,
...) {
if (!inherits(style, "pm_style")) {
stop("'style' must be a pm_style object", call. = FALSE)
}
new_style <- style
extra_colors <- list(...)
if (!is.null(colors) || length(extra_colors) > 0) {
all_overrides <- c(colors, extra_colors)
new_style$colors <- utils::modifyList(
new_style$colors %||% list(),
all_overrides
)
}
if (!is.null(labelRules)) {
if (replace_labels) {
new_style$labelRules <- labelRules
} else {
new_style$labelRules <- c(new_style$labelRules, labelRules)
}
}
structure(new_style, class = "pm_style")
}

315
R/symbolizers.R Normal file
View file

@ -0,0 +1,315 @@
#' Create a Polygon Symbolizer
#'
#' @description
#' Creates a polygon symbolizer for rendering filled polygon features.
#'
#' @param fill Character. Fill color for the polygon. Can be a CSS color
#' string or a function specification.
#' @param stroke Character. Stroke (outline) color. Default is NULL (no stroke).
#' @param width Numeric. Stroke width in pixels. Default is 1.
#' @param opacity Numeric. Fill opacity from 0 to 1. Default is 1.
#' @param pattern Character. Fill pattern. One of NULL, "hatch", or "dot".
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' # Simple blue fill
#' pmPolygonSymbolizer(fill = "steelblue")
#'
#' # With stroke
#' pmPolygonSymbolizer(fill = "#f0f0f0", stroke = "#333", width = 2)
#'
#' @export
pmPolygonSymbolizer <- function(fill = "#cccccc",
stroke = NULL,
width = 1,
opacity = 1,
pattern = NULL,
...) {
opts <- list(fill = fill, ...)
if (!is.null(stroke)) opts$stroke <- stroke
if (width != 1) opts$width <- width
if (opacity != 1) opts$opacity <- opacity
if (!is.null(pattern)) opts$pattern <- pattern
list(
type = "polygon",
options = opts
)
}
#' Create a Line Symbolizer
#'
#' @description
#' Creates a line symbolizer for rendering line features.
#'
#' @param color Character. Line color. Default is "#000000".
#' @param width Numeric or function. Line width in pixels. Can be a fixed
#' value or a zoom-dependent specification.
#' @param dash List or NULL. Dash pattern as a vector of numbers, e.g.,
#' \code{c(4, 2)} for 4px dash, 2px gap.
#' @param dashColor Character. Color for dashes if using dash pattern.
#' @param dashWidth Numeric. Width of dashes.
#' @param lineCap Character. Line cap style: "butt", "round", or "square".
#' @param lineJoin Character
#' . Line join style: "miter", "round", or "bevel".
#' @param opacity Numeric. Line opacity from 0 to 1. Default is 1.
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' # Simple black line
#' pmLineSymbolizer(color = "black", width = 2)
#'
#' # Dashed line
#' pmLineSymbolizer(color = "gray", width = 1, dash = c(4, 2))
#'
#' @export
pmLineSymbolizer <- function(color = "#000000",
width = 1,
dash = NULL,
dashColor = NULL,
dashWidth = NULL,
lineCap = NULL,
lineJoin = NULL,
opacity = 1,
...) {
opts <- list(color = color, width = width, ...)
if (!is.null(dash)) opts$dash <- dash
if (!is.null(dashColor)) opts$dashColor <- dashColor
if (!is.null(dashWidth)) opts$dashWidth <- dashWidth
if (!is.null(lineCap)) opts$lineCap <- lineCap
if (!is.null(lineJoin)) opts$lineJoin <- lineJoin
if (opacity != 1) opts$opacity <- opacity
list(
type = "line",
options = opts
)
}
#' Create a Circle Symbolizer
#'
#' @description
#' Creates a circle symbolizer for rendering point features as circles.
#'
#' @param radius Numeric. Circle radius in pixels. Default is 4.
#' @param fill Character. Fill color for the circle. Default is "#000000".
#' @param stroke Character. Stroke (outline) color. Default is NULL.
#' @param width Numeric. Stroke width in pixels. Default is 1.
#' @param opacity Numeric. Fill opacity from 0 to 1. Default is 1.
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' # Simple red circle
#' pmCircleSymbolizer(radius = 6, fill = "red")
#'
#' # Circle with stroke
#' pmCircleSymbolizer(radius = 8, fill = "white", stroke = "black", width = 2)
#'
#' @export
pmCircleSymbolizer <- function(radius = 4,
fill = "#000000",
stroke = NULL,
width = 1,
opacity = 1,
...) {
opts <- list(radius = radius, fill = fill, ...)
if (!is.null(stroke)) opts$stroke <- stroke
if (width != 1) opts$width <- width
if (opacity != 1) opts$opacity <- opacity
list(
type = "circle",
options = opts
)
}
#' Create a Text Symbolizer
#'
#' @description
#' Creates a text symbolizer for rendering text labels.
#'
#' @param font Character. Font specification (e.g., "12px sans-serif").
#' @param fill Character. Text fill color. Default is "#000000".
#' @param stroke Character. Text stroke (halo) color. Default is NULL.
#' @param width Numeric. Stroke width for text halo. Default is 0.
#' @param labelProps List. Properties to use for label text, in order of
#' preference. Default is \code{list("name")}.
#' @param textTransform Character. Text transformation: "uppercase",
#' "lowercase", or NULL.
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' # Simple text label
#' pmTextSymbolizer(font = "12px Arial", fill = "black")
#'
#' # Text with halo
#' pmTextSymbolizer(font = "14px sans-serif", fill = "black",
#' stroke = "white", width = 2)
#'
#' @export
pmTextSymbolizer <- function(font = "12px sans-serif",
fill = "#000000",
stroke = NULL,
width = 0,
labelProps = NULL,
textTransform = NULL,
...) {
opts <- list(font = font, fill = fill, ...)
if (!is.null(stroke)) opts$stroke <- stroke
if (width > 0) opts$width <- width
if (!is.null(labelProps)) opts$labelProps <- labelProps
if (!is.null(textTransform)) opts$textTransform <- textTransform
list(
type = "text",
options = opts
)
}
#' Create a Centered Text Symbolizer
#'
#' @description
#' Creates a centered text symbolizer for rendering text labels centered
#' on point features.
#'
#' @param font Character. Font specification (e.g., "12px sans-serif").
#' @param fill Character. Text fill color. Default is "#000000".
#' @param stroke Character. Text stroke (halo) color. Default is NULL.
#' @param width Numeric. Stroke width for text halo. Default is 0.
#' @param lineHeight Numeric. Line height multiplier for multi-line labels.
#' Default is NULL (uses library default). Use values like 1.0-1.2 for
#' tighter spacing.
#' @param labelProps List. Properties to use for label text, in order of
#' preference. Default is \code{list("name")}.
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' pmCenteredTextSymbolizer(font = "14px Arial", fill = "black")
#'
#' # Tighter line spacing for multi-word labels
#' pmCenteredTextSymbolizer(font = "11px sans-serif", fill = "#444",
#' lineHeight = 1.1)
#'
#' @export
pmCenteredTextSymbolizer <- function(font = "12px sans-serif",
fill = "#000000",
stroke = NULL,
width = 0,
lineHeight = NULL,
labelProps = NULL,
...) {
opts <- list(font = font, fill = fill, ...)
if (!is.null(stroke)) opts$stroke <- stroke
if (width > 0) opts$width <- width
if (!is.null(lineHeight)) opts$lineHeight <- lineHeight
if (!is.null(labelProps)) opts$labelProps <- labelProps
list(
type = "centeredText",
options = opts
)
}
#' Create a Line Label Symbolizer
#'
#' @description
#' Creates a line label symbolizer for rendering text labels along line
#' features (e.g., street names).
#'
#' @param font Character. Font specification (e.g., "12px sans-serif").
#' @param fill Character. Text fill color. Default is "#000000".
#' @param stroke Character. Text stroke (halo) color. Default is NULL.
#' @param width Numeric. Stroke width for text halo. Default is 0.
#' @param labelProps List. Properties to use for label text.
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' pmLineLabelSymbolizer(font = "11px Arial", fill = "#333",
#' stroke = "white", width = 2)
#'
#' @export
pmLineLabelSymbolizer <- function(font = "12px sans-serif",
fill = "#000000",
stroke = NULL,
width = 0,
labelProps = NULL,
...) {
opts <- list(font = font, fill = fill, ...)
if (!is.null(stroke)) opts$stroke <- stroke
if (width > 0) opts$width <- width
if (!is.null(labelProps)) opts$labelProps <- labelProps
list(
type = "lineLabel",
options = opts
)
}
#' Create a Shield Symbolizer
#'
#' @description
#' Creates a shield symbolizer for rendering labeled badges or shields
#' (e.g., highway route markers).
#'
#' @param font Character. Font specification for shield text.
#' @param fill Character. Text fill color. Default is "#000000".
#' @param background Character. Shield background color. Default is "#ffffff".
#' @param stroke Character. Shield border color. Default is "#000000".
#' @param padding Numeric. Padding inside the shield in pixels. Default is 2.
#' @param labelProps List. Properties to use for shield text.
#' @param ... Additional symbolizer options.
#'
#' @return A list representing the symbolizer configuration.
#'
#' @examples
#' pmShieldSymbolizer(font = "10px Arial", fill = "black",
#' background = "white", stroke = "black")
#'
#' @export
pmShieldSymbolizer <- function(font = "10px sans-serif",
fill = "#000000",
background = "#ffffff",
stroke = "#000000",
padding = 2,
labelProps = NULL,
...) {
opts <- list(
font = font,
fill = fill,
background = background,
stroke = stroke,
padding = padding,
...
)
if (!is.null(labelProps)) opts$labelProps <- labelProps
list(
type = "shield",
options = opts
)
}