
Create, Replace or Port a Map Theme in a QGIS Project
Source:R/rfp_qgs_theme_set.R
rfp_qgs_theme_set.RdWrite map themes into a .qgs. Takes the shape rfp_qgs_themes() returns,
so porting a theme between projects is read-then-set with no intermediate
file, and building one from scratch is a data frame of layer names.
Arguments
- path
Character. Path to the destination
.qgs.- themes
Either the list
rfp_qgs_themes()returns, or a data frame with at leastthemeandlayer.visible,expandedandstyledefault toTRUE,TRUEand"default".- on_missing_layer
One of
"warn"(default),"error","skip".- membership
One of
"declared"(default) or"universal". A Mergin theme hides a layer by ABSENCE, so declared membership makes membership and visibility one axis and every operation on it lossy (#304)."universal"lists every layer the destination holds,visible = FALSEfor those the theme does not name, so absence carries no information andvisiblecarries all of it.Opt-in, because three things break under it and all three are measured:
rfp_project_subset()derives its kept layers from a preset's entries with novisiblefilter, so a universal theme selects the whole project;data-raw/qgs/themes_extract.Rwould scope every shipped theme per template; and a preset roughly doubles, up to 7.7x on the largest fleet project. See also #162 - it writes five remote basemaps into every preset.- backup
Logical. Write
<path>.bakbefore mutating. DefaultTRUE.
Value
Invisibly, a data frame with one row per theme written: theme,
layers, layers_missing, groups, groups_dropped.
Details
# build one
rfp_qgs_theme_set(p, data.frame(theme = "Coho",
layer = c("Streams - all", "Lake")))
# port every theme from another project
rfp_qgs_theme_set(dst, rfp_qgs_themes(src))A theme of the same name is replaced. That is what makes this usable as
an edit; use rfp_qgs_theme_rm() to remove one outright. Replacing appends,
so the theme moves to the end of the project's theme order — QWC2 keys on
name and is unaffected, but the Mergin mobile picker lists them in project
order, so a replaced theme moves down it.
Layers are matched by name
Layer ids are project-local, so themes travel by layer name. A destination layer whose name is shared with another is refused when a theme being written names it — the lookup would silently bind the reference to whichever came first, and the other layer would never appear in the theme. Resolve by renaming all but one copy, so the name resolves to a single layer.
Not with rfp_qgs_rename(): it renames every copy of a duplicated name,
so the theme's reference then matches none of them and the layer is dropped
from the preset — reported, but as a missing layer rather than as the
ambiguity you set out to fix. Measured on a real project: it turns a refusal
into four themes written without the basemap they name.
A duplicated name that none of the themes mention is left alone. It cannot be looked up, so it cannot mis-bind anything, and refusing on it made the theme step unreachable on projects carrying names inherited from years of desktop work — a check about a theme that does not touch them.
The refusal is for the whole call, not for the one theme: nothing is
written until every theme has been built, so an ambiguous name in one theme
stops the batch. on_missing_layer does not soften it either — an ambiguous
layer is present, not missing, and the check precedes the per-theme loop.
on_missing_layer decides what happens when the destination has no layer of
that name: "warn" (default) reports and drops the reference, "error"
stops, "skip" drops silently. In every case the reference is dropped rather
than written, so the project is never left with dangling ids.
Group state is resolved, not copied
A theme records checked and expanded groups by layer-tree path, rooted at a
project-specific top-level group name — bcrestoration Mobile /Streams. The
function this replaced copied those paths verbatim, so a theme ported into a
project whose tree is named differently carried paths matching nothing; QGIS
silently ignored them and the group state was lost.
Each path is now resolved against the destination:
it matches a real group there — keep it
it does not, and the destination has exactly one top-level group — re-root it under that group and try again
still nothing — drop it and report
Rule 2 is what makes a cross-project port work, and is safe only because the first segment is a project-specific prefix precisely when there is one top-level group. Where a theme carries no group state at all — one built from layer names — the groups holding its visible layers are checked instead.
See also
Other qgs:
rfp_qgs_form_add(),
rfp_qgs_layer_rm(),
rfp_qgs_preview_set(),
rfp_qgs_raster_add(),
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_unbury(),
rfp_qgs_themes(),
rfp_qgs_vector_add(),
rfp_qwc_config_create(),
rfp_raster_style_path(),
rfp_raster_styles()
Examples
src <- system.file("testdata", "test_themes.qgs", package = "rfp")
dst <- file.path(tempdir(), "themed.qgs")
file.copy(src, dst, overwrite = TRUE)
#> [1] TRUE
rfp_qgs_theme_set(
dst,
data.frame(theme = "Just A", layer = "Layer A"),
backup = FALSE
)
#> ✔ Set 1 theme in /tmp/RtmpfSNDuG/themed.qgs
rfp_qgs_theme_names(dst)
#> [1] "Theme One" "Theme Two" "Just A"