|
| 1 | +##' Create a crop mask for map shapes |
| 2 | +##' |
| 3 | +##' This function generates a mask polygon that can be overlaid on a map to crop it to various shapes (circle, ellipse, etc.). The mask is a polygon with a hole in the desired shape. |
| 4 | +##' |
| 5 | +##' @param bbox Bounding box as a numeric vector c(xmin, ymin, xmax, ymax) |
| 6 | +##' @param crop_shape Shape to crop to. Options: "circle", "ellipse" |
| 7 | +##' @param bg_color Background color for the mask (should match plot background) |
| 8 | +##' @return A list with 'mask' (sf polygon object) and 'adjusted_bbox' (bbox, unchanged) |
| 9 | +##' @import sf |
| 10 | +##' @export |
| 11 | +##' @examples |
| 12 | +##' bbox <- c(-0.1, 51.5, 0.1, 51.6) |
| 13 | +##' result <- create_crop_mask(bbox, "circle", "#0a0e27") |
| 14 | +create_crop_mask <- function(bbox, crop_shape, bg_color) { |
| 15 | + if (is.null(crop_shape) || !crop_shape %in% c("circle", "ellipse")) { |
| 16 | + return(list(mask = NULL, adjusted_bbox = bbox)) |
| 17 | + } |
| 18 | + |
| 19 | + center_x <- base::mean(c(bbox[1], bbox[3])) |
| 20 | + center_y <- base::mean(c(bbox[2], bbox[4])) |
| 21 | + |
| 22 | + # Create the inner boundary (the shape we want to preserve) |
| 23 | + if (crop_shape == "circle") { |
| 24 | + # For circle, use the smaller dimension as diameter to ensure it fits |
| 25 | + width <- bbox[3] - bbox[1] |
| 26 | + height <- bbox[4] - bbox[2] |
| 27 | + radius <- base::min(width, height) / 2 |
| 28 | + |
| 29 | + # Create circle coordinates |
| 30 | + angles <- base::seq(0, 2 * base::pi, length.out = 100) |
| 31 | + inner_coords <- base::data.frame( |
| 32 | + x = center_x + radius * base::cos(angles), |
| 33 | + y = center_y + radius * base::sin(angles) |
| 34 | + ) |
| 35 | + } else if (crop_shape == "ellipse") { |
| 36 | + # Use bbox dimensions for ellipse (respects original aspect ratio) |
| 37 | + radius_x <- (bbox[3] - bbox[1]) / 2 |
| 38 | + radius_y <- (bbox[4] - bbox[2]) / 2 |
| 39 | + |
| 40 | + angles <- base::seq(0, 2 * base::pi, length.out = 100) |
| 41 | + inner_coords <- base::data.frame( |
| 42 | + x = center_x + radius_x * base::cos(angles), |
| 43 | + y = center_y + radius_y * base::sin(angles) |
| 44 | + ) |
| 45 | + } |
| 46 | + |
| 47 | + # Create outer boundary (rectangular bbox) |
| 48 | + outer_coords <- base::data.frame( |
| 49 | + x = c(bbox[1], bbox[3], bbox[3], bbox[1], bbox[1]), |
| 50 | + y = c(bbox[2], bbox[2], bbox[4], bbox[4], bbox[2]) |
| 51 | + ) |
| 52 | + |
| 53 | + # Create polygon with hole: outer boundary with inner boundary cut out |
| 54 | + # The hole is created by reversing the direction of the inner polygon |
| 55 | + mask_polygon <- sf::st_polygon( |
| 56 | + list( |
| 57 | + base::as.matrix(outer_coords[, c("x", "y")]), |
| 58 | + base::as.matrix(inner_coords[base::nrow(inner_coords):1, c("x", "y")]) |
| 59 | + ) |
| 60 | + ) |
| 61 | + |
| 62 | + # Convert to sfc and set CRS to match typical lat/lon |
| 63 | + mask_sf <- sf::st_sfc(mask_polygon, crs = 4326) |
| 64 | + |
| 65 | + return(list(mask = mask_sf, adjusted_bbox = bbox)) |
| 66 | +} |
0 commit comments