This document describes the Web Feature Service (WFS) that shares the current tree dataset — including the daily-updated Soil Water Balance Model (SWBM) values and the AutoML drought risk scores — with external partners (d.b.g. Database Company GmbH, Landeshauptstadt Hannover).
The service is served through the QGIS Web Client (QWC) stack, which proxies WFS requests to QGIS Server with role-based permission filtering.
public roleThe WFS exposes a single layer, wfs_trees_current, backed by the PostGIS
view bgcki.wfs_trees_current.
The WFS is reached through the QWC API gateway. The partner-facing URL is the
short service path /ila/bgcki (a transparent nginx alias in
qwc-docker/api-gateway/nginx.conf that proxies to the QWC OGC service):
https://<qwc-host>:<qwc-port>/ila/bgcki?SERVICE=WFS&VERSION=1.1.0&REQUEST=...
| Parameter | Value |
|---|---|
| Host | <qwc-host> (project network / Tailscale) |
| Port | <qwc-port> (QWC API gateway) |
| Path | /ila/bgcki (partner alias) |
| Service | WFS |
| Version | 1.1.0 |
| Layer / TypeName | wfs_trees_current |
The legacy QWC service path /ows/scan/bgcki_trees (<service_name> =
scan/bgcki_trees) still works and is used internally; new partners should
receive the /ila/bgcki link. The alias is dataset-agnostic on purpose —
future layers (non-tree data) will be served under the same service path and
distinguished by TYPENAME.
Note: The exact host and port are internal infrastructure values and are intentionally not published here. They are provided to partners out of band.
GET /ila/bgcki?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetCapabilities
Returns the service metadata, including the title “BlueGreenCity-KI Bäume” and the layer list.
GET /ila/bgcki?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetFeature&TYPENAME=wfs_trees_current&MAXFEATURES=10
Returns up to 10 features as GML 3.1.1.
GET /ila/bgcki?SERVICE=WFS&VERSION=1.1.0&REQUEST=GetFeature&TYPENAME=wfs_trees_current&FILTER=<url-encoded CQL>
CQL filters are supported, e.g. to select a bounding box or attribute subset.
The view bgcki.wfs_trees_current joins three sources:
bgcki.analysis_tree_risk_scores_operational_current — the current
operational tree risk scores (all inventory and AutoML columns).
Since Issue #253 (2026-10) the AutoML columns include
drought_risk_ml_calibrated — the Platt-calibrated damage probability;
the risk-class thresholds apply to that column, while drought_risk_ml
keeps the raw model output.bgcki.swbm_daily_state — the latest SWBM state per tree
(model version *:irrigation-lower) plus the 7-day forecast window
(state_date = CURRENT_DATE + 1 … + 7), joined via a single
LATERAL subquery on tree_id = objectid. The window is evaluated at
query time, so it advances automatically with the daily 04:00 run.
swbm_daily_state holds multiple model_version variants per tree
(unknown-age / unknown-SVF scenarios) and stale variants from earlier
runs. The join therefore keeps only the current variants (the ones the
latest run wrote) and computes the conservative envelope over them —
the same convention as the irrigation planning run: demand/stress = max,
fractions/volumes = min (driest), trigger date = min (earliest),
avoidable-by-rain = AND. Trees with several current variants are labelled
conservative-scenario-envelope in swbm_model_version.
bgcki.operational_tree_product — the daily operational product
(04:00 refresh, same tree set, 1:1 on objectid). Exposed via the view:
irrigation_action_priority — the 0–1 irrigation prioritization score
combining ml_damage_score_t1, site_vulnerability_rulebased, the SWBM
forecast stress and the irrigation demand in liters (see
bluegreencity_ki/risk_products.py, calculate_action_priority; applied
per tree in bluegreencity_ki/operational_products.py). This is the score
used to rank trees for irrigation action.The SWBM columns are prefixed with swbm_ to avoid name collisions, e.g.:
| Column | Description |
|---|---|
swbm_state_date |
date of the latest SWBM state |
swbm_soil_water_fraction_total |
total soil water fraction (0–1) |
swbm_soil_water_fraction_layer_1 … _layer_3 |
per-layer soil water fractions |
swbm_soil_water_stress_today |
soil water stress for the current day |
swbm_soil_water_stress_forecast_max_7d |
max forecast soil water stress (7 d) |
swbm_irrigation_demand_liters |
irrigation demand in liters |
swbm_irrigation_trigger_date |
date the irrigation trigger is reached |
swbm_irrigation_avoidable_by_rain |
whether irrigation is avoidable by rain |
swbm_data_quality |
data quality flag |
swbm_soil_water_m3_total … _layer_3 |
soil water volume in m³ |
swbm_surface_wet_fraction |
surface wet fraction |
swbm_meteorology_source |
meteorology source |
swbm_model_version |
SWBM model version |
swbm_irrigation_demand_day_1 … _day_7 |
forecast irrigation demand in liters for CURRENT_DATE + N (day 1 = tomorrow) |
swbm_fraction_layer_1_day_1 … _day_7 |
forecast layer-1 soil water fraction (0–1.2) for CURRENT_DATE + N |
Forecast columns are NULL when the forecast row for that day is missing
(e.g. trees the daily resume run has not backfilled yet). The per-day soil
water stress is intentionally not exposed here — the aggregated
swbm_soil_water_stress_forecast_max_7d column covers the warning case, and
day-by-day stress values remain available in swbm_daily_state.
The migrations that define the view are
sql/migrations/2026-09-14_create_wfs_trees_view.sql,
sql/migrations/2026-09-16_fix_wfs_trees_view_forecast_rows.sql
sql/migrations/2026-09-21_add_wfs_forecast_day_columns.sql
and
sql/migrations/2026-10-09_add_wfs_calibrated_column.sql
(adds drought_risk_ml_calibrated, issue #253) and
sql/migrations/2026-10-09_add_wfs_priority_column.sql
(current definition, adds irrigation_action_priority via the
operational_tree_product join).
OUTPUTFORMAT=application/vnd.geo+json, verified 2026-10-09)irrigation_action_priority)QGIS Server supports GML/XML and GeoJSON for WFS GetFeature. Requests
with OUTPUTFORMAT=gpkg, csv, or shp are ignored and GML is returned
instead.
This is a known QGIS Server limitation. GML 3.1.1 can be imported directly by:
postgis_fdw or ST_GeomFromGML)geopandas.read_file with the WFS URL, or owslib)For partners who need a flat file (GPKG/CSV/Shapefile), the recommended workflow is to fetch the GML and convert it locally, e.g.:
import geopandas as gpd
gdf = gpd.read_file(
"https://<qwc-host>:<qwc-port>/ila/bgcki",
layer="wfs_trees_current",
)
gdf.to_file("trees.gpkg", driver="GPKG")
The WFS service is restricted by default in QWC. Access is granted to the
public role via the QWC ConfigDB:
wfs_service, name scan/bgcki_treespublic (read-only, write = false)Once the service is permitted, all layers and attributes of the service are
allowed by default. The generated permissions.json reflects this.
To change permissions, use the QWC Admin GUI or update the ConfigDB directly, then regenerate the configs.
volumes/qgs-resources/scan/bgcki_trees.qgs (and a copy in
the qgs-resources/ root). The project connects to the bgcki PostGIS
database and exposes the wfs_trees_current view as a WFS layer..qgs copies in
volumes/qgs-resources/),volumes/config/default/ogcConfig.json (WMS + WFS attribute maps),volumes/config/default/permissions.json (public role, WMS + WFS lists),volumes/config/default/featureInfoConfig.json (attribute list of the
click / maptip popup — without an entry here the column is served by
WFS but not shown in the QWC feature-info dialog).
Run the sync script after every view migration; it adds missing
columns to all three places (idempotent), restarts
qwc-qgis-server + qwc-ogc-service, and clears
volumes/config/__capabilities_cache/:
conda run -n bgcki_wesemann python scripts/sync_qwc_wfs_attributes.py
Note: WFS whitelist entries use QWC’s cleaned attribute names (spaces
to underscores, non-word chars dropped) — the script applies the same
cleaning, so hand-edited entries for special characters (e.g.
Wurzel Fäule (Wurzel- / Stockfäule)) can silently mismatch.
xml.etree.ElementTree.ParseError: out of memory) on
full-layer GML GetFeature. In QGIS: enable Download features on
demand (layer properties → provider) or load with a filter. A full
layer fetch works via OUTPUTFORMAT=geojson (~800 MB) or direct from
QGIS Server, bypassing the QWC proxy.analysis_tree_risk_scores_operational_current and the latest
swbm_daily_state rows. No manual refresh is needed.| Symptom | Cause | Fix |
|---|---|---|
Service unknown or not permitted |
WFS service not permitted for the requesting role | Grant wfs_service permission in ConfigDB, regenerate configs, restart OGC service |
Layer(s) not valid in QGIS Server logs |
QGIS project layer is invalid (e.g. broken DB connection) | Check the PostGIS connection in the .qgs project |
| QGIS Desktop: “Layerausdehnung ist nicht richtig” bei Layer-Reload | Achsen-Order-Mismatch: Bei Projekt-CRS EPSG:4326 sendet der Desktop-Client (QGIS 4.x) den WFS-BBOX in der offiziellen 4326-Achsenreihenfolge (lat/lon), der Server interpretiert “EPSG:4326” als lon/lat (GeoServer-Legacy-Konvention, im Server-Code hartkodiert) → Box liegt an falscher Stelle → 0 Features. Der 5. BBOX-Parameter (CRS) greift nur zusätzlich zu SRSNAME. Derselbe lat/lon-BBOX funktioniert im WMS 1.3.0 desselben Servers. Nicht gefixt bis QGIS Server 4.3-dev (Code identisch, am 2026-10-09 geprüft). Reproduziert am 2026-10-09 mit minimalem Testprojekt (3-Punkt-Shapefile, ohne Postgres/QWC) — keine Projekt-Konfiguration schuld | Option 1 (Client): ignoreAxisOrientation=1 an die URL des WFS-Layers hängen (Quellverwaltungen → WFS → Layer) → Client sendet lon/lat → funktioniert (Server-Seite verifiziert: SRSNAME=EPSG:4326 + lon/lat-BBOX liefert Features). Option 2 (Projekt): Projekt-CRS auf EPSG:25832 setzen (Projekt-Standard) — Client fragt dann in 25832 an. Übergangsweise: einmal zum Layer zoomen. Bug-Report bei qgis/QGIS ist in Vorbereitung |
| GML returned instead of GPKG/CSV | QGIS Server does not support those WFS output formats | Convert GML locally (see Output formats) |
| Full-city download fails: “Operation abgebrochen” / 500 | Complete GetFeature (89k features, ~122 MB GML) exceeds the memory of the QWC ogc-service attribute-whitelist filter (ElementTree DOM parse). BBOX-restricted and RESULTTYPE=hits requests work fine. |
Zoom to the area of interest before loading (layer is restricted to the view extent), or use the direct PostGIS connection for full-dataset work. Server side: QGIS Server generates the full GML in ~50 s without issue |
| Config generation fails with “critical error” | Pre-existing broken QGIS projects produce ERROR logs | Set "ignore_errors": true in the tenant config config block |