Skip to contents

Overview

rfp_project_create() assembles a complete field-ready QGIS project in one call. It downloads spatial data, copies field forms and templates, and builds a project directory ready to push to Mergin Maps for mobile collection.

Quick start

library(rfp)

rfp_project_create(
  name = "elk_river_2026",
  watershed_groups = c("ELKR"),
  template = "bcfishpass_mobile",
  forms = c("pscis", "fiss_site")
)
#> ℹ Creating project directory: ~/Projects/gis/elk_river_2026
#> ℹ Downloading bcdata layers
#> ℹ Downloading fwa layers
#> ℹ Downloading aws layers
#> ℹ Copying QGIS template: bcfishpass_mobile
#> ℹ Copying form: pscis
#> ℹ Copying form: fiss_site
#> ✔ Project created: ~/Projects/gis/elk_river_2026

Output directory structure

~/Projects/gis/elk_river_2026/
├── elk_river_2026.qgs              QGIS project file
├── background_layers.gpkg          Spatial data (16+ layers)
├── habitat_lateral.tif             Lateral habitat raster
├── form_pscis.gpkg                 PSCIS assessment form
├── form_fiss_site.gpkg             Fish habitat site form
└── ignore_mobile/
    ├── photos/                     Field photo directory
    ├── methods/                    Provincial methods PDFs
    └── templates/                  Provincial form templates

The ignore_mobile/ directory is excluded from Mergin Maps sync — it holds reference documents for office use.

A small thematic project

A template is a starting point, not a fixed layer set. Pass layer_config to build a project around one theme, and the .qgs is reconciled to what was downloaded — you keep the template’s cartography for the layers you asked for and QGIS opens without a Handle Bad Layers dialog.

cfg <- rfp_project_layers("bcrestoration_mobile")

floodplains <- cfg[cfg$source_layer %in% c(
  "whse_basemapping.cwb_floodplains_bc_area_svw",
  "whse_basemapping.fwa_lakes_poly",
  "bcfishpass.streams_vw",
  "whse_basemapping.transport_line"
), ]

rfp_project_create(
  name = "floodplains_bulkley",
  watershed_groups = "BULK",
  template = "bcrestoration_mobile",
  layer_config = floodplains
)
#> ℹ Trimmed 45 layers with no data in this project:
#>     Conservancy                whse_tantalis.ta_conservancy_areas_svw
#>     Fire Severity              whse_forest_vegetation.veg_burn_severity_sp
#>     …
#> ✔ Project created: ~/Projects/gis/floodplains_bulkley

Two things follow from the fact that layer_config selects tables:

  • One table can back several styled layers. Asking for bcfishpass.streams_vw keeps all four of the restoration template’s stream layers — Streams - all, streams_bt, streams_salmon, streams_st — each with its own symbology. To pick among them, subset afterwards with rfp_project_subset(), whose layers = names layers rather than tables.
  • Basemaps and any other source that is not a file inside the project are never trimmed. Only an artifact rfp can point at on disk, and prove is missing, is removed — so a layer type added to the gq registry later (a STAC floodplain COG, a WMS collection) survives with no change to rfp.

A layer that was requested but failed to download is kept, with a warning, rather than trimmed. That keeps a transient outage recoverable by a refresh instead of quietly costing you the styled layer.

The project says what it is

Creation writes rfp_project.json beside the manifest:

jsonlite::read_json("~/Projects/gis/floodplains_bulkley/rfp_project.json")
#> $template
#> [1] "bcrestoration_mobile"
#> $created
#> [1] "2026-08-13"
#> $rfp_version
#> [1] "0.15.0"

The template is the part that gets used. rfp_project_update(), rfp_project_audit() and rfp_manifest_create() read it, so they stop assuming bcfishpass_mobile when you do not pass template =:

# no template argument - resolved from the project
rfp_project_audit("~/Projects/gis/floodplains_bulkley")
#> ℹ Template: bcrestoration_mobile (recorded by the project)

That matters because the registries differ. Without the record, rfp_manifest_create() on a restoration project marked whse_basemapping.cwb_floodplains_bc_area_svw as frozen — never refreshed, and a hard error later if it ever went missing.

An explicit template = always wins, so you can override a stale record without editing the project. A project created before this existed has no record and behaves exactly as it did.

The project’s birth certificate

Creation writes rfp_manifest.csv at the project root, so the project starts inside the lifecycle instead of needing one backfilled:

rfp_manifest_read("~/Projects/gis/floodplains_bulkley/rfp_manifest.csv")
#> # A tibble: 4 x 2
#>   source_layer                               source_type
#>   <chr>                                      <chr>
#> 1 whse_basemapping.cwb_floodplains_bc_area_svw bcdata
#> 2 whse_basemapping.fwa_lakes_poly              bcdata
#> 3 whse_basemapping.fwa_watershed_groups_poly   bcdata
#> 4 bcfishpass.streams_vw                        aws

The watershed-group layer is there even though the layer_config above never asked for it — every source pass rewrites it, so it is always part of a project’s intent.

Two things follow from the manifest being an intent record rather than an inventory:

  • It is derived from what you asked for, not from what landed. A layer that was empty in this area, or whose download failed, stays listed — rfp_project_audit() reports it as informational and the next rfp_project_update() adds it. That is the same reasoning that keeps such a layer in the .qgs.
  • It is itself a valid layer_config, so a project can be rebuilt from one:
m <- rfp_manifest_read("~/Projects/gis/floodplains_bulkley/rfp_manifest.csv")

rfp_project_create("floodplains_morice", watershed_groups = "MORR",
                   template = "bcrestoration_mobile", layer_config = m)

Edit the CSV to change what the project carries — that edit is the intent, which is why rfp_project_update() has no add = argument in manifest mode.

What a rebuild carries over, and what it does not

The manifest is two columns, so it restores the layer set — and from that, the new project’s own manifest, its rfp_project.json and a .qgs trimmed to those layers. It does not, on its own, restore the source project’s arrangement:

  • layer-tree group placement and order for anything added after creation
  • a map theme that exists only in that one project’s .qgs

Map themes are the part that is now recoverable, because a theme is data — see the next section. What no artifact records is where a layer sits in the tree and in what order, which is why a rebuilt project draws its added layers wherever the template puts them.

For the example above this costs nothing — a new area gets a fresh project and there is no arrangement to carry. It matters when you are rebuilding a project you want back the way it was.

A map theme is data too

A theme is a list of layer names and visibility flags, so it lives in a shipped registry rather than inside whichever project happened to have it first:

reg <- rfp_qgs_theme_lookup()
unique(reg$layers$theme)
#> [1] "High Detail - Crossings"       "Imagery"
#> [3] "Land Tenure"                   "Low Detail - Bull Trout Model"
#> [5] "Low Detail - Salmon Model"     "Low Detail - Steelhead Model"

rfp_project_create() applies every theme in it by default. That is safe because a theme already in the project is left alone — so the five a restoration template carries are skipped, and only Imagery is added:

rfp_project_create("floodplains_morice", watershed_groups = "MORR",
                   template = "bcrestoration_mobile")
#> ℹ 5 themes already in the project and left alone: …
#> ✔ Set 1 theme in …

An existing theme is skipped rather than rewritten on purpose. Writing a theme regenerates it from scratch, so anything the writer does not read would be destroyed — and a theme someone curated in QGIS Desktop is exactly what a re-run must not flatten. Replacing one takes force = TRUE.

To give an existing project a theme, or to change your mind later:

rfp_qgs_theme_add("~/Projects/gis/floodplains_bulkley", "Imagery")
#> ℹ 5 themes already in the project and left alone: …
#> ✔ Set 1 theme in …

Two things about how far a theme travels:

  • Its layer list goes anywhere. A layer the project does not have drops its reference and is reported, rather than leaving a broken one behind — so Imagery reaches 11 of its 13 layers in both shipped templates and needs no per-project variant.
  • Its group state does not. Which layer-tree groups a theme checks is recorded per template, because a group path is recorded from the project group down — so every one begins bcfishpass Mobile / or bcrestoration Mobile / and resolves in one template only. (Not because the templates name their groups differently: measured, they spell every shared group identically. The templates do not carry all the same groups, which is the second and narrower reason.) Applied to the template it was recorded against, a theme comes back exactly; applied elsewhere, the group state is inferred from where the layers sit and the theme still works.

A theme is rarely authored from nothing — it is nearly always another theme plus a small delta. rfp_qgs_theme_create() says that directly:

rfp_qgs_theme_create(
  "~/Projects/gis/floodplains_bulkley",
  name = "Low Detail - Bull Trout Model",
  base = "Low Detail - Salmon Model",
  add  = "streams_bt",
  drop = "streams_salmon"
)

That really is the difference between those two shipped themes. Every one of the seven non-Imagery themes contains all 21 of the layers they share and differs by four to seven members.

To change one layer’s membership without rebuilding the theme, use rfp_qgs_theme_layer_add() / rfp_qgs_theme_layer_rm(). A theme hides a layer by absence, so visible = FALSE is the way to keep a layer reachable in a theme without drawing it:

rfp_qgs_theme_layer_add(p, "Trails", themes = "all")
rfp_qgs_theme_layer_add(p, "ESRI World Topo", themes = "all", visible = FALSE)

rfp_qgs_themes() and rfp_qgs_theme_set() remain the low level — read one project’s themes and write them into another, with no intermediate file. Prefer them for a whole-theme port; prefer the two above for a single layer, because rfp_qgs_theme_set() regenerates every preset it touches.

The data carries its own cartography

The background GeoPackages are written with a layer_styles table taken from the project’s own .qgs. QGIS reads that table on Add Layer — never on project open — so this is what makes a layer arrive styled somewhere else:

# in another project, or a blank one
# Layer > Add Layer > Add Vector Layer >
#   floodplains_bulkley/background_layers/whse_basemapping.fwa_lakes_poly.gpkg
# draws with the restoration template's symbology, no .qgs involved

A table styled several ways keeps all of them, distinguished by styleName, with the project’s first-listed layer as the default — so bcfishpass.streams_vw carries Streams - all, streams_bt, streams_salmon and streams_st, and opens as Streams - all.

Adding a layer to an existing project runs the other way around: it has to be added in QGIS Desktop to display at all, and the next rfp_project_update() then writes its style into the data.

Recording where the crew walked

Mergin Maps can record a track while a crew works. It is off by default; pass tracking = TRUE and the project ships ready to record:

rfp_project_create("elk_river_2026", watershed_groups = "ELKR",
                   tracking = TRUE)
#> ✔ Added "Position track" to …/elk_river_2026.qgs
#> ✔ Position tracking enabled (normal) in …/elk_river_2026.qgs

That writes three things together — the tracking_layer.gpkg the app appends to, a Position track layer declaring it, and the project properties that switch recording on. They go together on purpose: the setting names the layer by id, so a project with tracking on and no layer records nothing and reports nothing.

Check or change it later:

rfp_qgs_tracking_get("~/Projects/gis/elk_river_2026/elk_river_2026.qgs")
#> $enabled   [1] TRUE
#> $frequency [1] "normal"
#> $resolves  [1] TRUE

rfp_qgs_tracking_set(qgs, frequency = "best")

Two things worth knowing before choosing a frequency:

  • It is not an interval. "best", "normal" and "low" mean different things on each platform, and iOS is distance-only — a stationary phone records nothing at any setting. See ?rfp_qgs_tracking_set for the table.
  • A track is written once, when the crew taps Stop. A crash before that loses the session, and on Android recording stops with the screen off.

A track carries a timestamp on every vertex, so points collected separately — observations, photos — are matched to it afterwards by time rather than being attached at capture.

Naming a track in the field

A recorded track says when, how far and who — and nothing about what the session was. track_fields = TRUE adds three columns and gives them widgets, so the person who walked it can say:

rfp_project_create("elk_river_2026", watershed_groups = "ELKR",
                   tracking = TRUE, track_fields = TRUE)
field
track_name what the session was
track_description free text — conditions, what was found, why it ended
track_type day log (default), stream survey, bushwhack, access route, drive, other
named_by fills in automatically — who named it, where that differs from who walked it

An existing project takes the two halves directly, in this order — the project half refuses to configure a field the GeoPackage does not have:

rfp_tracking_fields_add("~/Projects/gis/elk_river_2026/tracking_layer.gpkg")
rfp_qgs_tracking_fields_add(qgs)

Both are idempotent, so re-running against a project already in the field adds nothing and leaves the GeoPackage byte-identical.

track_type starts at day log, because that is the normal case — a track runs all day so anything recorded separately can be found by its time. The others name the exceptions.

Verified on a phone: the form opens on tapping a track, both the text box and the dropdown take input, and named_by fills itself in. It is still FALSE by default so existing calls are unchanged. rfp_project_audit() reports which halves a project carries, and rfp_qgs_tracking_fields_add(force = TRUE) updates a project that already has an older version of the set.

Available templates

rfp_project_templates()
#> # A tibble: 2 x 3
#>   name                  description                                        path
#>   <chr>                 <chr>                                              <chr>
#> 1 bcfishpass_mobile     Fish passage assessment workflow                   /...
#> 2 bcrestoration_mobile  Restoration planning and monitoring workflow       /...

Available forms

Forms are pre-built GeoPackage databases shipped with the package. The list is rendered from the registry rather than written out here: a hand-maintained copy of something the package can print drifts, and this one had listed six of the sixteen forms for several releases.

forms <- rfp::rfp_form_types()
knitr::kable(
  forms[, c("type", "label", "parent", "description")],
  row.names = FALSE,
  col.names = c("Form", "Label", "Parent", "Description")
)
Form Label Parent Description
pscis PSCIS Fish Passage Stream Crossing Assessment
fiss_site FISS Site Fish Habitat Site Assessment
fish_sample Fish Sample Fish Individual Sample Data
fhap FHAP Fish Habitat Assessment Protocol
edna eDNA Environmental DNA Sampling
monitoring Monitoring Restoration Monitoring
vri_qa VRI QA VRI Polygon Field QA
transition_qa Transition QA Land Cover Transition QA
exceedance Exceedance LRMP Zero-Target Exceedance
viewscape Viewscape Viewscape Repeat Photo Station
monitoring_fish_passage Fish Passage Monitoring Fish Passage Effectiveness Monitoring
monitoring_general General Monitoring Inventory and monitoring of past and potential restoration sites
scan Scan Phone LiDAR or Photogrammetry Scan
observation Observation Generic Incidental Observation
cabin_visit CABIN Visit CABIN Benthic Site Visit
cabin_visit_pebble CABIN Pebbles cabin_visit CABIN Pebble Count

label is what a field crew sees in the layer list and in the observation form’s “form wanted” dropdown. A row with a non-blank Parent is a child table — it has no GeoPackage of its own and is deployed by its parent, so it cannot be passed to rfp_form_create() directly.

Customising data sources

By default, rfp_project_create() downloads all three source types (bcdata, fwa, aws). To skip a source:

rfp_project_create(
  name = "quick_project",
  watershed_groups = c("ADMS"),
  sources = c("bcdata", "fwa")   # skip aws
)

Applying styles

After creating a project, apply gq registry styles so layers look right in QGIS:

rfp_styles_apply("~/Projects/gis/elk_river_2026/background_layers.gpkg")
#> ✔ Styles applied: 11 styled, 5 skipped

See vignette("styles") for details on the style pipeline.

Deploying to Mergin Maps

Push the project to Mergin Maps for mobile field collection:

rfp_mergin_create(
  "newgraph/elk_river_2026",
  "~/Projects/gis/elk_river_2026"
)

# Grant access to field crew
rfp_mergin_share("newgraph/elk_river_2026", "field_tech_1", "writer")
rfp_mergin_share("newgraph/elk_river_2026", "field_tech_2", "writer")

See vignette("mergin") for the full sync workflow.