| Title: | Sparse Geometry Rasterization with Exact Coverage Fractions |
|---|---|
| Description: | Rasterize polygon, line, and point geometry without materializing any pixel values. For polygons it computes exact coverage fractions for polygon-grid intersections, returning sparse tables: run-length encoded interior cells and individually weighted boundary cells. The scanline algorithm is O(perimeter) in both time and memory, using the scan line fill attributed to Wylie et al. (1967) <doi:10.1145/1465611.1465619>. Exact coverage fractions use the 'exactextract' algorithm by Daniel Baston, vendored from the 'exactextractr' package <doi:10.32614/CRAN.package.exactextractr>. Derived from the 'fasterize' package by Noam Ross. |
| Authors: | Michael Sumner [aut, cre] (ORCID: <https://orcid.org/0000-0002-2471-7511>), Noam Ross [aut] (ORCID: <https://orcid.org/0000-0002-2136-0000>), EcoHealth Alliance [cph] (Original fasterize scanline logic), Jim Hester [ctb] (Wrote CollectorList.h for early version), Daniel Baston [cph] (exactextract C++ algorithm (vendored)), ISciences, LLC [cph] (exactextract C++ algorithm (vendored)) |
| Maintainer: | Michael Sumner <mdsumner@gmail.com> |
| License: | Apache License (== 2.0) |
| Version: | 0.3.1 |
| Built: | 2026-10-19 06:31:32 UTC |
| Source: | https://github.com/hypertidy/controlledburn |
Rasterizes geometry into a sparse output schema, with one type-pure
table per geometry kind. Polygons produce run-length-encoded interior
cells ($runs) and weighted boundary cells ($edges, with a
coverage fraction in [0, 1]). Lines produce one record per touched
cell with the absolute length of line within the cell, in CRS units
($lines). Points produce one record per touched cell, with no
measure column ($points). See the unified geometry rasterization
design doc in inst/docs-design/.
burn( x, extent = NULL, dimension = NULL, resolution = NULL, mode = c("coverage", "approx") ) burn_scanline(x, extent = NULL, dimension = NULL, resolution = NULL)burn( x, extent = NULL, dimension = NULL, resolution = NULL, mode = c("coverage", "approx") ) burn_scanline(x, extent = NULL, dimension = NULL, resolution = NULL)
x |
geometry input, one of:
|
extent |
numeric vector |
dimension |
integer vector |
resolution |
numeric, cell size (scalar for square cells, or
|
mode |
character, one of |
Mixed geometry-kind input (e.g. a vector containing both polygons
and lines) is permitted and produces tables populated for whichever
kinds are present. Each table's measure column means exactly one
thing — polygon $edges$fraction is dimensionless, $lines$length
is in CRS units, points have no measure — so combining them in a
single materialized matrix would silently mix units: run separate
burns for each kind before materialize_chunk() (which sums whatever
it is given) or extract_burn() with fun = "sum" (which refuses
mixed kinds).
GeometryCollection input warns and is skipped. Curved types (CircularString, CompoundCurve, etc.) must be linearised by the caller before calling.
Memory usage is O(perimeter) rather than O(bounding-box area), so
no tile_size parameter is required (in contrast to burn_sparse(),
which is polygon-only and tile-bounded).
A list with class "controlledburn" containing:
runsdata.frame with columns row, col_start, col_end,
id — run-length encoded polygon interior cells (always full
coverage). Empty for line/point input.
edgesdata.frame with columns row, col, fraction,
id — polygon boundary cells with partial coverage; fraction
is in (0, 1). Empty for line/point input, or when
mode = "approx".
linesdata.frame with columns row, col, length,
id — line cells; length is the absolute length of the line
within the cell, in CRS units. Empty for polygon/point input.
pointsdata.frame with columns row, col, id —
point cells; no measure column (a point is either in a cell or
it isn't). Empty for polygon/line input.
extentthe raster extent (c(xmin, xmax, ymin, ymax))
dimensionthe grid dimensions (c(ncol, nrow))
Row and column indices are 1-based. Row 1 is the top (ymax) row.
The id column is a 1-based index into the input geometry vector.
The measure is computed in the CRS the geometry and grid are specified in — not the geodesic measure on the Earth. Geometry in degrees produces area in square degrees and length in degrees. For geodesic measures, project to an appropriate CRS first.
if (requireNamespace("geos", quietly = TRUE)) { library(geos) # Polygon: produces $runs (interior) and $edges (fraction in [0, 1]) poly <- as_geos_geometry("POLYGON ((0.5 0.5, 2.5 0.5, 2.5 2.5, 0.5 2.5, 0.5 0.5))") r <- burn(poly, extent = c(0, 3, 0, 3), dimension = c(3, 3)) # Approx mode: fasterize-style cell-center rule, runs only r <- burn(poly, extent = c(0, 3, 0, 3), dimension = c(3, 3), mode = "approx") # Line: produces $lines with absolute length in CRS units line <- as_geos_geometry("LINESTRING (0.5 0.5, 2.5 2.5)") r <- burn(line, extent = c(0, 3, 0, 3), dimension = c(3, 3)) # Point: produces $points with row/col only (no measure column) pt <- as_geos_geometry("POINT (1.5 1.5)") r <- burn(pt, extent = c(0, 3, 0, 3), dimension = c(3, 3)) # Defaults: extent from bbox, 256-cell fitted grid r <- burn(poly) # By resolution r <- burn(poly, resolution = 0.01) }if (requireNamespace("geos", quietly = TRUE)) { library(geos) # Polygon: produces $runs (interior) and $edges (fraction in [0, 1]) poly <- as_geos_geometry("POLYGON ((0.5 0.5, 2.5 0.5, 2.5 2.5, 0.5 2.5, 0.5 0.5))") r <- burn(poly, extent = c(0, 3, 0, 3), dimension = c(3, 3)) # Approx mode: fasterize-style cell-center rule, runs only r <- burn(poly, extent = c(0, 3, 0, 3), dimension = c(3, 3), mode = "approx") # Line: produces $lines with absolute length in CRS units line <- as_geos_geometry("LINESTRING (0.5 0.5, 2.5 2.5)") r <- burn(line, extent = c(0, 3, 0, 3), dimension = c(3, 3)) # Point: produces $points with row/col only (no measure column) pt <- as_geos_geometry("POINT (1.5 1.5)") r <- burn(pt, extent = c(0, 3, 0, 3), dimension = c(3, 3)) # Defaults: extent from bbox, 256-cell fitted grid r <- burn(poly) # By resolution r <- burn(poly, resolution = 0.01) }
burn_sparse() has been removed. Use burn() instead, which
produces identical output for polygon input and also supports
lines and points.
burn_sparse(...)burn_sparse(...)
... |
ignored |
This function always errors; it never returns a value. Call
burn() instead.
# burn_sparse() is deprecated and errors; use burn() instead try(burn_sparse())# burn_sparse() is deprecated and errors; use burn() instead try(burn_sparse())
Filters and clips the sparse tables (runs, edges, lines, points)
to a sub-window defined by a target extent. Returns a new
controlledburn object with offset row/col indices and snapped
extent. No dense allocation — just data frame filtering.
crop_burn(x, target)crop_burn(x, target)
x |
A |
target |
numeric vector |
A controlledburn object covering only the target window,
with row/col indices re-based to 1.
if (requireNamespace("geos", quietly = TRUE)) { library(geos) poly <- as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(100L, 100L)) sub <- crop_burn(r, target = c(2, 5, 3, 7)) sub }if (requireNamespace("geos", quietly = TRUE)) { library(geos) poly <- as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(100L, 100L)) sub <- crop_burn(r, target = c(2, 5, 3, 7)) sub }
Queries the sparse tables directly; no dense matrix is created.
For points at cell centres this reproduces materialize_chunk()
exactly:
extract_burn(x, xy, fun) == materialize_chunk(x, fun)[cbind(row, col)]
extract_burn(x, xy, fun = c("id", "sum"))extract_burn(x, xy, fun = c("id", "sum"))
x |
A |
xy |
A two-column matrix or data.frame of x, y coordinates in the CRS of the burn. |
fun |
character, |
Interior runs are matched by row equality plus column interval containment; edges, lines, and points by exact (row, col) match. Cost is O(points + hits), independent of grid dimension.
A numeric vector of length nrow(xy). Points outside the
grid extent are NA for both funs.
if (requireNamespace("geos", quietly = TRUE)) { library(geos) poly <- as_geos_geometry("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(10L, 10L)) # value at three cell centres (fun = "id": geometry id, else NA) extract_burn(r, cbind(c(1.5, 5.5, 8.5), c(1.5, 5.5, 8.5)), fun = "id") }if (requireNamespace("geos", quietly = TRUE)) { library(geos) poly <- as_geos_geometry("POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(10L, 10L)) # value at three cell centres (fun = "id": geometry id, else NA) extract_burn(r, cbind(c(1.5, 5.5, 8.5), c(1.5, 5.5, 8.5)), fun = "id") }
Converts the sparse run/edge/line tables from burn() into a dense
matrix. With target, only the cells inside a sub-window of the
burn grid are materialised: the sparse tables are first filtered and
re-indexed by crop_burn() and only the sub-window is allocated, so
a chunk of a very large burn grid can be realised without ever
allocating the full grid.
materialise_chunk(x, fun = c("sum", "id"), target = NULL) materialize_chunk(x, fun = c("sum", "id"), target = NULL)materialise_chunk(x, fun = c("sum", "id"), target = NULL) materialize_chunk(x, fun = c("sum", "id"), target = NULL)
x |
A |
fun |
character, the pixel function:
|
target |
optional numeric |
The sub-window is snapped outward to the burn grid's cell boundaries
(see crop_burn()), so the returned matrix covers at least target.
Use crop_burn(x, target) to recover the snapped grid's extent and
dimension (for plotting with rasterImage(), say). The
materialised chunk is identical to the corresponding sub-matrix of the
full materialisation.
A numeric matrix with dimensions nrow x ncol of the burn
grid, or of the snapped target window.
if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(100L, 100L)) full <- materialise_chunk(r) chunk <- materialise_chunk(r, target = c(2, 5, 3, 7)) dim(chunk) crop_burn(r, c(2, 5, 3, 7))$extent }if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(100L, 100L)) full <- materialise_chunk(r) chunk <- materialise_chunk(r, target = c(2, 5, 3, 7)) dim(chunk) crop_burn(r, c(2, 5, 3, 7))$extent }
Aggregate a burn() result onto a grid of square tiles of block cells
and return the mean coverage per tile (in [0, 1] for non-overlapping
polygons). Tiles cut off by the grid boundary are normalised by their
actual cell count. Cost is proportional to the number of records plus
the number of tiles: a run that spans several tiles contributes partial
lengths to its two end tiles, and the fully covered tiles between them
are filled with a difference array, so nothing is expanded to cells
whatever the block size.
overview_burn(x, tiles = 256L, block = NULL)overview_burn(x, tiles = 256L, block = NULL)
x |
A |
tiles |
Target number of tiles along the longer grid axis; ignored
if |
block |
Tile size in cells. |
The result is a matrix in raster order (row 1 is the top), ready for
ximage::ximage(ov, extent = attr(ov, "extent")).
A numeric matrix (tile rows by tile columns) with attributes
extent (c(xmin, xmax, ymin, ymax) of the tile grid, which extends
past the burn grid on the right and bottom when block does not
divide its dimension) and block. Untouched tiles are NA.
if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(1000L, 1000L)) ov <- overview_burn(r, tiles = 20) dim(ov) attr(ov, "extent") }if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))") r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(1000L, 1000L)) ov <- overview_burn(r, tiles = 20) dim(ov) attr(ov, "extent") }
Draw a burn() result in map coordinates (asp = 1) with the grid
extent as the frame. Two renderings are available:
## S3 method for class 'controlledburn' plot( x, what = c("auto", "exact", "overview"), marginals = TRUE, col = grDevices::hcl.colors(64, "YlOrRd", rev = TRUE), by_id = FALSE, tiles = 256L, threshold = 50000, content = FALSE, main = NULL, ... )## S3 method for class 'controlledburn' plot( x, what = c("auto", "exact", "overview"), marginals = TRUE, col = grDevices::hcl.colors(64, "YlOrRd", rev = TRUE), by_id = FALSE, tiles = 256L, threshold = 50000, content = FALSE, main = NULL, ... )
x |
A |
what |
|
marginals |
Draw the row and column marginal strips. The layout is sized from the extent's aspect ratio so the strips span exactly the map; leftover device space is left blank. |
col |
Colour ramp (low to high coverage) for |
by_id |
In |
tiles |
Passed to |
threshold |
Record count above which |
content |
Draw the content extent (bounding box of touched cells) as a dashed rectangle. |
main |
Optional title, drawn in the outer margin above the strips. |
... |
Passed to |
"exact"every record is drawn: runs as rectangles, edge cells shaded by coverage fraction, line cells shaded by length (relative to the cell diagonal), points as cells. Cost is one rectangle per record, so this is the picture of the sparse representation itself.
"overview"the coarse mean-coverage matrix from
overview_burn() drawn with ximage::ximage(). Cost does not
depend on grid size.
"auto" (default) picks "exact" when the total record count is at
most threshold, else "overview". Either way the optional marginal
strips are exact: covered cells per grid row (right) and per grid
column (top), from summary.controlledburn().
x, invisibly, with the summary computed for the marginals
attached as attribute summary.
if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry(c("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))", "POLYGON ((2 2, 4 3, 3 5, 2 2))")) r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(40L, 40L)) plot(r) plot(r, by_id = TRUE, marginals = FALSE) plot(r, what = "overview", tiles = 10) }if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry(c("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))", "POLYGON ((2 2, 4 3, 3 5, 2 2))")) r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(40L, 40L)) plot(r) plot(r, by_id = TRUE, marginals = FALSE) plot(r, what = "overview", tiles = 10) }
Compute a compact description of a burn() result in time proportional
to the number of records in its sparse tables, never to the number of
grid cells, so it is cheap even for a national grid of billions of cells.
## S3 method for class 'controlledburn' summary(object, tiles = 256L, ...) ## S3 method for class 'summary.controlledburn' print(x, ascii = FALSE, ...)## S3 method for class 'controlledburn' summary(object, tiles = 256L, ...) ## S3 method for class 'summary.controlledburn' print(x, ascii = FALSE, ...)
object |
A |
tiles |
Target number of tiles along the longer grid axis for the
overview, passed to |
... |
Ignored. |
x |
A |
ascii |
Also print the overview as text. |
The pieces are:
overviewa coarse matrix of mean coverage per tile, see
overview_burn(). When the grid is small enough that one tile is one
cell, this is the exact coverage.
row_marginal, col_marginal
exact covered-cell totals per grid row and per grid column, at full resolution.
contentthe extent and 1-based row/col range of touched cells,
or NULL for an empty burn.
areaper-geometry totals in cells: interior (run cells),
boundary (summed edge fractions), lines (summed length, in
extent units), points (count).
fractionsquantiles of the edge coverage fractions.
recordsrow counts of the sparse tables.
Coverage here means: run cells 1 each, edge cells their fraction, line and point cells 1 each (presence).
The number of geometries is taken as the largest id present, so geometries after the last one that touched the grid are not counted.
A list of class summary.controlledburn.
if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry(c("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))", "POLYGON ((2 2, 4 3, 3 5, 2 2))")) r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(100L, 100L)) s <- summary(r) s s$area }if (requireNamespace("geos", quietly = TRUE)) { poly <- geos::as_geos_geometry(c("POLYGON ((1 1, 9 1, 9 9, 1 9, 1 1))", "POLYGON ((2 2, 4 3, 3 5, 2 2))")) r <- burn(poly, extent = c(0, 10, 0, 10), dimension = c(100L, 100L)) s <- summary(r) s s$area }