Skip to contents

Write 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.

Usage

rfp_qgs_theme_set(
  path,
  themes,
  on_missing_layer = c("warn", "error", "skip"),
  membership = c("declared", "universal"),
  backup = TRUE
)

Arguments

path

Character. Path to the destination .qgs.

themes

Either the list rfp_qgs_themes() returns, or a data frame with at least theme and layer. visible, expanded and style default to TRUE, TRUE and "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 = FALSE for those the theme does not name, so absence carries no information and visible carries 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 no visible filter, so a universal theme selects the whole project; data-raw/qgs/themes_extract.R would 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>.bak before mutating. Default TRUE.

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:

  1. it matches a real group there — keep it

  2. it does not, and the destination has exactly one top-level group — re-root it under that group and try again

  3. 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.

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"