Insert a raster into a .qgs with surgical xml2 edits, leaving the project
at whatever version QGIS stamped it. Adding a layer in QGIS Desktop remains
the alternative; this exists so a project assembled headlessly - from a STAC
query, a DEM clip, an orthophoto - does not require a desktop round trip.
Usage
rfp_qgs_raster_add(
qgs,
raster,
name,
style = NULL,
qml = NULL,
stretch = NULL,
group = "Base - lidar",
position = c("bottom", "top"),
visible = TRUE,
themes = "all",
companion = FALSE,
name_companion = NULL,
restyle = FALSE
)Arguments
- qgs
Character. Path to the
.qgs.- raster
Character. Path to the raster, relative to the project directory or absolute within it. Written as
./<path>- a datasource outside the project resolves nowhere on another machine.- name
Character. Layer name as it appears in QGIS.
- style
Character. A key from
rfp_raster_styles(). Resolves to both the.qmland its stretch rule, which is why it exists alongsideqml-rfp_raster_style_path()returns a bare path and discards the row.- qml
Character. Path to a style to apply, for a
.qmlthat is not registered.NULLleaves the layer with QGIS's default rendering.- stretch
How the renderer's range is set:
"from_data"reads the raster's min and max,"from_data_p2_98"reads its 2nd-98th percentile,"none"leaves the style's own range, orc(min, max)sets it explicitly. Defaults to the registry row whenstyleis given."from_data_p2_98"is for scanned imagery, where a scan border or a dust speck puts min/max at an extreme no meaningful part of the image occupies and the rest of the range compresses toward flat. On four scanned airphotos it reproduces the stretch a human chose in QGIS.Applies to every renderer that carries a range, not only ramped ones - a
singlebandgraykeeps its stretch in<contrastEnhancement>and was silently unreachable until #235. Apalettedrenderer has no stretch: itsvalue=are class codes, and rescaling those is data corruption.Two rasters that must agree need the same explicit range. Either
from_datamode gives each its own, so a detail tile inside a context tile renders discontinuously at their shared boundary - which is the defect this argument exists for."none"is not "no stretch" for every style.airphoto_grayshipsminMaxOrigin/extent = UpdatedCanvas, so left alone it re-derives its range from what is on the canvas - which is what lets one greyscale style fit any scan, and is also the seam above. Any range written here is pinned withlimits = Noneso it cannot be re-derived.- group
Character. Layer-tree group to place it in, matched on trimmed name and created when absent. Defaults to
"Base - lidar", which both shipped templates already carry.- position
One of
"bottom"(default) or"top"- where in the group. A context raster belongs under a detail raster; a hillshade belongs over the elevation it shades.- visible
Logical. Checked in the layer tree. Default
TRUE.- themes
Character. Map themes to add the layer to. A layer absent from a theme is hidden whenever that theme is applied, so
"all"(the default) keeps it visible;character(0)adds it to none.- companion
Logical. Also add the style's registered companion as a second layer over the same raster, drawn above it. Requires
style. DefaultFALSE.- name_companion
Character. Name for that second layer. Default is
"<label> - <name>"from the companion's registry row.- restyle
Logical. Re-render a layer this project already holds, instead of the no-op the idempotence rule otherwise gives. Default
FALSE.Without it there is no way to change a layer's style: the id is derived from name and datasource, so a restyled layer hashes to the id already present and the call returns early. Removing and re-adding is not the workaround it looks like -
.prune_empty_groups()drops the layer-tree group once its last layer goes, taking hand-configured map-theme entries with it.Because the id is unchanged, only
<maplayer>is replaced. Every other surface holding that id - layer tree, themes, custom-order, legend - keeps pointing at a node that still exists, so group position and theme visibility survive.
Details
rfp_qgs_raster_add(qgs, "background_layers/dem.tif", name = "Elevation",
qml = "background_layers/dem.qml")Why this does not re-save the project
No available headless QGIS matches the version the field projects are saved
at, so project.write() either downgrades or upgrades the file. xml2 edits
the document in place and write_xml() preserves the root version
attribute - asserted here, not assumed.
A layer id lives in several places
Inserting is the inverse of .qgs_drop_layer_refs(), and harder: a sweep can
walk the whole document looking for an id, while an insert has to know every
surface. Those surfaces differ by project - the legacy legendlayerfile
legend and custom-order are present in some projects and absent in others -
so each is written only when the project already uses it, and a reference
checker asserts afterwards that nothing dangles.
Idempotence
The layer id is derived from the name and datasource, so adding the same
raster twice is a no-op and leaves the file byte-identical. A random id would
dirty the .qgs on every run, and these projects sync on file bytes.
A pair is two layers over one file
A DEM reads as terrain only with a hillshade over it; the elevation ramp
alone is a smooth paint blob. companion = TRUE adds the second style named
by the registry as its own <maplayer> over the same raster, above the
first. It is opt-in because asking for a DEM should not silently produce two
layers.
A pair cannot be shipped as a sidecar - QGIS reads one foo.qml per
foo.tif - so it exists only where a project is written.
See also
Other qgs:
rfp_qgs_form_add(),
rfp_qgs_layer_rm(),
rfp_qgs_preview_set(),
rfp_qgs_service_add(),
rfp_qgs_service_rm(),
rfp_qgs_services(),
rfp_qgs_style_export(),
rfp_qgs_style_set(),
rfp_qgs_subset_get(),
rfp_qgs_subset_set(),
rfp_qgs_theme_add(),
rfp_qgs_theme_lookup(),
rfp_qgs_theme_names(),
rfp_qgs_theme_rm(),
rfp_qgs_theme_set(),
rfp_qgs_theme_unbury(),
rfp_qgs_themes(),
rfp_qgs_vector_add(),
rfp_qwc_config_create(),
rfp_raster_style_path(),
rfp_raster_styles()
Examples
qgs <- system.file("testdata", "raster_add_fixture.qgs", package = "rfp")
dir <- withr::local_tempdir()
file.copy(qgs, fs::path(dir, "p.qgs"))
#> Warning: cannot create file '/tmp/RtmpfSNDuG/file1c082ae7831f/p.qgs', reason 'No such file or directory'
#> [1] FALSE
file.copy(system.file("testdata", "subset_fixture", "parent", "dtm.tif",
package = "rfp"), fs::path(dir, "dtm.tif"))
#> Warning: cannot create file '/tmp/RtmpfSNDuG/file1c082ae7831f/dtm.tif', reason 'No such file or directory'
#> [1] FALSE
rfp_qgs_raster_add(fs::path(dir, "p.qgs"), "dtm.tif", name = "Elevation")
#> Error: File does not exist: /tmp/RtmpfSNDuG/file1c082ae7831f/p.qgs
# a DEM and its hillshade, over one file, in one call
rfp_qgs_raster_add(fs::path(dir, "p.qgs"), "dtm.tif", name = "Terrain",
style = "dem_turbo", companion = TRUE)
#> Error: File does not exist: /tmp/RtmpfSNDuG/file1c082ae7831f/p.qgs
