Skip to content

Commit 5c2a924

Browse files
authored
Merge pull request #808 from rhijmans/main
support SpatRaster with color table or RGB
2 parents 42d9739 + 921ca46 commit 5c2a924

File tree

7 files changed

+325
-147
lines changed

7 files changed

+325
-147
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export(addPolylines)
7676
export(addPopups)
7777
export(addProviderTiles)
7878
export(addRasterImage)
79+
export(addRasterLegend)
7980
export(addRectangles)
8081
export(addScaleBar)
8182
export(addSimpleGraticule)

NEWS

Lines changed: 114 additions & 127 deletions
Large diffs are not rendered by default.

R/layers.R

Lines changed: 130 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ epsg3857 <- "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y
206206
#' @param x a \code{\link[terra]{SpatRaster}} or a \code{RasterLayer} object--see \code{\link[raster]{raster}}
207207
#' @param colors the color palette (see \code{\link{colorNumeric}}) or function
208208
#' to use to color the raster values (hint: if providing a function, set
209-
#' \code{na.color} to \code{"#00000000"} to make \code{NA} areas transparent)
209+
#' \code{na.color} to \code{"#00000000"} to make \code{NA} areas transparent).
210+
#' The palette is ignored if \code{x} is a SpatRaster with a color table or if
211+
#' it has RGB channels.
210212
#' @param opacity the base opacity of the raster, expressed from 0 to 1
211213
#' @param attribution the HTML string to show as the attribution for this layer
212214
#' @param layerId the layer id
@@ -225,15 +227,20 @@ epsg3857 <- "+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y
225227
#' (before base64 encoding); defaults to 4MB.
226228
#' @template data-getMapData
227229
#'
230+
#' @seealso \code{\link{addRasterLegend}} for an easy way to add a legend for a
231+
#' SpatRaster with a color table.
232+
#'
228233
#' @examples
229234
#' \donttest{library(raster)
230235
#'
231236
#' r <- raster(xmn = -2.8, xmx = -2.79, ymn = 54.04, ymx = 54.05, nrows = 30, ncols = 30)
232237
#' values(r) <- matrix(1:900, nrow(r), ncol(r), byrow = TRUE)
233238
#' crs(r) <- CRS("+init=epsg:4326")
234239
#'
240+
#' pal <- colorNumeric("Spectral", domain = c(0, 1000))
235241
#' leaflet() %>% addTiles() %>%
236-
#' addRasterImage(r, colors = "Spectral", opacity = 0.8)
242+
#' addRasterImage(r, colors = pal, opacity = 0.8) %>%
243+
#' addLegend(pal = pal, values = c(0, 1000))
237244
#' }
238245
#' @export
239246
addRasterImage <- function(
@@ -283,6 +290,93 @@ addRasterImage <- function(
283290
}
284291

285292

293+
#' Add a color legend for a SpatRaster to a map
294+
#'
295+
#' A function for adding a [legend][addLegend()] that is specifically designed
296+
#' for [terra::SpatRaster] objects, with categorical values, that carry their
297+
#' own [color table][terra::coltab()].
298+
#'
299+
#' @param map a map widget object
300+
#' @param x a [SpatRaster][terra::SpatRaster] object with a color table
301+
#' @param layer the layer of the raster to target
302+
#' @param ... additional arguments to pass through to [addLegend()]
303+
#' @seealso [addRasterImage()]
304+
#' @examples
305+
#'
306+
#' library(terra)
307+
#'
308+
#' r <- rast("/vsicurl/https://geodata.ucdavis.edu/test/pr_nlcd.tif")
309+
#' leaflet() %>%
310+
#' addTiles() %>%
311+
#' addRasterImage(r, opacity = 0.75) %>%
312+
#' addRasterLegend(r, opacity = 0.75)
313+
#'
314+
#' plot.new() # pause in interactive mode
315+
#'
316+
#' rr <- r
317+
#' levels(rr) <- NULL
318+
#' leaflet() %>%
319+
#' addTiles() %>%
320+
#' addRasterImage(rr, opacity = 0.75) %>%
321+
#' addRasterLegend(rr, opacity = 0.75)
322+
#'
323+
#' @md
324+
#' @export
325+
addRasterLegend <- function(map, x, layer = 1, ...) {
326+
stopifnot(inherits(x, "SpatRaster"))
327+
stopifnot(length(layer) == 1 && layer > 0 && layer <= terra::nlyr(x))
328+
329+
## might as well do this here and only once. Subsetting would otherwise have been necessary in
330+
## color_info <- base::subset(color_info, value %in% terra::values(x))
331+
x <- x[[layer]]
332+
333+
# Retrieve the color table from the layer. If one doesn't exist, that means
334+
# the raster was colored some other way, like using colorFactor or something,
335+
# and the regular addLegend() is designed for those cases.
336+
ct <- terra::coltab(x)[[1]]
337+
if (is.null(ct)) {
338+
stop("addRasterLegend() can only be used on layers with color tables (see ?terra::coltab). Otherwise, use addLegend().")
339+
}
340+
341+
# Create a data frame that has value and color columns
342+
# Extract the colors in #RRGGBBAA format
343+
color_info <- data.frame(
344+
value = ct[[1]],
345+
color = grDevices::rgb(ct$red/255, ct$green/255, ct$blue/255, ct$alpha/255)
346+
)
347+
348+
lvls <- terra::levels(x)[[1]]
349+
350+
# Drop values that aren't part of the layer
351+
## unlike "values", "unique" is memory-safe; it does not load all values
352+
## into memory if the raster is large. So instead of:
353+
354+
# color_info <- base::subset(color_info, value %in% terra::values(x))
355+
356+
## remove the levels to get the raw cell values
357+
levels(x) <- NULL
358+
color_info <- base::subset(color_info, value %in% terra::unique(x)[[1]])
359+
360+
res <- if (is.data.frame(lvls)) {
361+
# Use the labels from levels(x), and look up the matching colors in the
362+
# color table
363+
364+
# The levels data frame can have varying colnames, just normalize them
365+
colnames(lvls) <- c("value", "label")
366+
base::merge(color_info, lvls, by.x = "value", by.y = 1)
367+
} else {
368+
# No level labels provided; use the values as labels
369+
cbind(color_info, label = color_info$value)
370+
}
371+
372+
# At this point, res is a data frame with `value`, `color`, and `label` cols,
373+
# and values/colors not present in the raster layer have been dropped
374+
375+
addLegend(map, colors = res[["color"]], labels = res[["label"]], ...)
376+
}
377+
378+
379+
286380
addRasterImage_RasterLayer <- function(
287381
map,
288382
x,
@@ -370,50 +464,70 @@ addRasterImage_SpatRaster <- function(
370464
data = getMapData(map)
371465
) {
372466

373-
if (terra::nlyr(x) > 1) {
467+
# terra 1.5-50 has terra::has.RGB()
468+
if (has.RGB(x)) {
469+
# RGB(A) channels to color table
470+
x <- terra::colorize(x, "col")
471+
} else if (terra::nlyr(x) > 1) {
374472
x <- x[[1]]
375473
warning("using the first layer in 'x'", call. = FALSE)
376474
}
377475

378476
raster_is_factor <- terra::is.factor(x)
477+
478+
# there 1.5-50 has terra::has.colors(x)
479+
ctab <- terra::coltab(x)[[1]]
480+
has_colors <- !is.null(ctab)
481+
379482
method <- match.arg(method)
380483
if (method == "ngb") method = "near"
381484
if (method == "auto") {
382-
if (raster_is_factor) {
485+
if (raster_is_factor || has_colors) {
383486
method <- "near"
384487
} else {
385488
method <- "bilinear"
386489
}
387490
}
388491

389-
if (project) {
390-
# if we should project the data
391-
projected <- projectRasterForLeaflet(x, method)
392-
} else {
393-
# do not project data
394-
projected <- x
395-
}
396-
397492
bounds <- terra::ext(
398493
terra::project(
399494
terra::project(
400495
terra::as.points(terra::ext(x), crs=terra::crs(x)),
401496
epsg3857),
402497
epsg4326)
403498
)
499+
## can't the above be simplified to this?
500+
# bounds <- terra::ext(
501+
# terra::project(
502+
# terra::as.points(terra::ext(x), crs=terra::crs(x)),
503+
# epsg4326)
504+
# )
505+
506+
if (project) {
507+
# if we should project the data
508+
x <- projectRasterForLeaflet(x, method)
509+
if (method=="bilinear") {
510+
has_colors <- FALSE
511+
}
512+
}
404513

405514
if (!is.function(colors)) {
406-
if (method == "near") {
515+
if (method == "near" || has_colors) {
407516
# 'factors'
408-
colors <- colorFactor(colors, domain = NULL, na.color = "#00000000", alpha = TRUE)
517+
domain <- NULL
518+
if (has_colors) {
519+
colors <- rgb(ctab[,2], ctab[,3], ctab[,4], ctab[,5], maxColorValue=255)
520+
domain <- ctab[,1]
521+
}
522+
colors <- colorFactor(colors, domain = domain, na.color = "#00000000", alpha = TRUE)
409523
} else {
410524
# 'numeric'
411525
colors <- colorNumeric(colors, domain = NULL, na.color = "#00000000", alpha = TRUE)
412526
}
413527
}
414528

415-
tileData <- terra::values(projected) %>% as.vector() %>% colors() %>% col2rgb(alpha = TRUE) %>% as.raw()
416-
dim(tileData) <- c(4, ncol(projected), nrow(projected))
529+
tileData <- terra::values(x) %>% as.vector() %>% colors() %>% col2rgb(alpha = TRUE) %>% as.raw()
530+
dim(tileData) <- c(4, ncol(x), nrow(x))
417531
pngData <- png::writePNG(tileData)
418532
if (length(pngData) > maxBytes) {
419533
stop(

R/normalize-terra.R

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@ pointData.SpatVector <- function(obj) {
2424
polygonData.SpatVector <- function(obj) {
2525
check_crs_terra(obj)
2626

27-
# this is a bit convoluted. I will add a simpler
28-
# and more efficient method to terra to replace the below
2927
xy = data.frame(terra::geom(obj))
3028
names(xy)[3:4] = c("lng", "lat")
3129
xy = split(xy[,2:5], xy[,1]) # polygons
@@ -40,6 +38,9 @@ polygonData.SpatVector <- function(obj) {
4038
})
4139
})
4240

41+
# with terra >= 1.5-50 you can do this instead
42+
# xy = terra::geom(obj, list=TRUE, xnm="lng", ynm="lat")
43+
4344
structure(
4445
xy,
4546
bbox = terra_bbox(obj)
@@ -49,6 +50,21 @@ polygonData.SpatVector <- function(obj) {
4950

5051

5152
# helpers -----------------------------------------------------------------
53+
assure_crs_terra <- function(x) {
54+
prj <- crs(x, proj=TRUE)
55+
if (is.lonlat(x, perhaps=TRUE, warn=FALSE)) {
56+
if (!grepl("+datum=WGS84", prj, fixed = TRUE)) {
57+
x <- project(x, "+proj=longlat +datum=WGS84")
58+
}
59+
return(x)
60+
}
61+
# Don't have enough information to check
62+
if (is.na(crs) || (crs=="")) {
63+
warning("SpatVector layer is not long-lat data", call. = FALSE)
64+
return(x)
65+
}
66+
project(x, "+proj=longlat +datum=WGS84")
67+
}
5268

5369
check_crs_terra <- function(x) {
5470
crs <- crs(x)

man/addRasterImage.Rd

Lines changed: 10 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/addRasterLegend.Rd

Lines changed: 45 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-raster.R

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,11 @@ test_that("rasters", {
3737

3838
expect_maps_equal(r1, r2)
3939

40+
# test with color map
41+
r <- rast(ncols=10, nrows=10, vals=rep_len(10:15, length.out=100), xmin=0, xmax=10^6, ymin=0, ymax=10^6, crs=pmerc)
42+
r[5,] <- NA
43+
coltab(r) <- c(rep("#FFFFFF", 10), rainbow(6, end=.9))
44+
(r3 <- rtest(r))
45+
4046
})
47+

0 commit comments

Comments
 (0)