From e3acfc85a09c1aad5b48b1917fb890d64847ddc8 Mon Sep 17 00:00:00 2001
From: IoannaMi <59479531+IoannaMi@users.noreply.github.com>
Date: Tue, 17 Feb 2026 14:24:21 +0100
Subject: [PATCH 1/6] chore: clean unused components
---
components.d.ts | 7 +------
src/components/HazardMenu.vue | 38 -----------------------------------
2 files changed, 1 insertion(+), 44 deletions(-)
delete mode 100644 src/components/HazardMenu.vue
diff --git a/components.d.ts b/components.d.ts
index 06f1d8e..e4827aa 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -6,17 +6,12 @@
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
-export {}
+export { }
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
- AppFooter: typeof import('./src/components/AppFooter.vue')['default']
AreaMenu: typeof import('./src/components/AreaMenu.vue')['default']
- copy: typeof import('./src/components/HazardMenu.vue')['default']
- HazardMenu: typeof import('./src/components/HazardMenu.vue')['default']
- HazardSelect: typeof import('./src/components/HazardSelect.vue')['default']
- HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
LayerList: typeof import('./src/components/LayerList.vue')['default']
MapComponent: typeof import('./src/components/MapComponent.vue')['default']
MapLayer: typeof import('./src/components/MapLayer.vue')['default']
diff --git a/src/components/HazardMenu.vue b/src/components/HazardMenu.vue
deleted file mode 100644
index fb3e4c4..0000000
--- a/src/components/HazardMenu.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
From 221f7649bb2e4e97b1df66fa20c21e8867986cf3 Mon Sep 17 00:00:00 2001
From: IoannaMi <59479531+IoannaMi@users.noreply.github.com>
Date: Fri, 20 Feb 2026 13:57:37 +0100
Subject: [PATCH 2/6] WIP: add describeCoverage requests of wps
---
components.d.ts | 2 +-
src/components/SubMenu.vue | 30 ++++-
src/config/navigation.json | 27 +++--
src/lib/wps/describe.js | 220 +++++++++++++++++++++++++++++++++++
src/lib/wps/handle-output.js | 63 ++++++++++
src/lib/wps/resolve-input.js | 84 +++++++++++++
6 files changed, 406 insertions(+), 20 deletions(-)
create mode 100644 src/lib/wps/describe.js
create mode 100644 src/lib/wps/handle-output.js
create mode 100644 src/lib/wps/resolve-input.js
diff --git a/components.d.ts b/components.d.ts
index 76534dd..484c0cd 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -6,7 +6,7 @@
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
-export { }
+export {}
/* prettier-ignore */
declare module 'vue' {
diff --git a/src/components/SubMenu.vue b/src/components/SubMenu.vue
index 75ac252..13b355b 100644
--- a/src/components/SubMenu.vue
+++ b/src/components/SubMenu.vue
@@ -33,7 +33,11 @@
diff --git a/src/config/navigation.json b/src/config/navigation.json
index 1acaf6f..cd04f9c 100644
--- a/src/config/navigation.json
+++ b/src/config/navigation.json
@@ -6,7 +6,6 @@
"title": "Region Selection",
"drawerTitle": "Select a base map",
"icon": "mdi-map-marker-radius",
- "completionEvent": "auto",
"components": [
{
"component": "LayerList",
@@ -30,10 +29,11 @@
}
],
"wps": {
- "identifier": "region_selection",
- "inputs": {
- "nutsid": "store:map.activeRegion.properties.NUTS_ID"
- },
+ "identifier": "lare_region",
+ "trigger": "mapClick",
+ "inputs": [
+ { "id": "nutsname", "type": "LiteralData", "source": "store:map.activeRegion.properties.nuts_name" }
+ ],
"storeResultAs": "regionSelection"
}
},
@@ -46,11 +46,12 @@
"regionSelection"
],
"wps": {
- "identifier": "hazard",
- "inputs": {
- "hazard_type": "payload:value",
- "nutsid": "store:map.activeRegion.properties.NUTS_ID"
- },
+ "identifier": "lare_hazard",
+ "trigger": "stepComplete",
+ "inputs": [
+ { "id": "nutsname", "type": "LiteralData", "source": "store:map.activeRegion.properties.nuts_name" },
+ { "id": "hazard", "type": "LiteralData", "source": "payload:value" }
+ ],
"storeResultAs": "hazard"
},
"components": [
@@ -92,16 +93,26 @@
"requiredSteps": [
"hazard"
],
+ "wps": {
+ "identifier": "lare_uom",
+ "trigger": "stepComplete",
+ "inputs": [
+ { "id": "nutsname", "type": "LiteralData", "source": "store:map.activeRegion.properties.nuts_name" },
+ { "id": "uomsize", "type": "LiteralData", "source": "payload:value" }
+ ],
+ "storeResultAs": "uom"
+ },
"components": [
{
"component": "NumberInput",
"componentProps": {
- "label": "Hexagon size (in m)",
- "suffix": "m",
+ "label": "Hexagon size (in m²)",
+ "suffix": "m²",
"min": 100,
- "max": 10000,
- "step": 100,
- "defaultValue": 1000
+ "max": 50000000,
+ "step": 100000,
+ "defaultValue": 1000,
+ "defaultValueSource": "wpsResult:regionSelection.suggested_uom"
}
}
]
@@ -156,4 +167,4 @@
]
}
]
-}
\ No newline at end of file
+}
diff --git a/src/lib/wps/describe.js b/src/lib/wps/describe.js
deleted file mode 100644
index 1851905..0000000
--- a/src/lib/wps/describe.js
+++ /dev/null
@@ -1,220 +0,0 @@
-import axios from 'axios'
-
-const cache = new Map()
-
-/**
- * Sends a WPS DescribeProcess request and returns a structured description
- * of the process inputs, outputs, and metadata.
- *
- * Results are cached per baseUrl+identifier so repeated calls are free.
- *
- * @param {string} baseUrl - WPS server URL
- * @param {string} identifier - Process identifier
- * @returns {Promise}
- *
- * @typedef {Object} ProcessDescription
- * @property {string} identifier
- * @property {string} title
- * @property {string} [abstract]
- * @property {ProcessInput[]} inputs
- * @property {ProcessOutput[]} outputs
- *
- * @typedef {Object} ProcessInput
- * @property {string} id
- * @property {string} title
- * @property {'LiteralData'|'ComplexData'|'BoundingBoxData'} type
- * @property {boolean} required
- * @property {string} [dataType] - For LiteralData (e.g. 'string', 'float')
- * @property {string} [defaultValue] - For LiteralData
- * @property {string[]} [allowedValues] - For LiteralData with constrained values
- * @property {string} [mimeType] - For ComplexData (default format)
- * @property {string[]} [supportedMimeTypes] - For ComplexData (all formats)
- *
- * @typedef {Object} ProcessOutput
- * @property {string} id
- * @property {string} title
- * @property {'LiteralData'|'ComplexData'} type
- * @property {string} [mimeType]
- */
-export async function describeProcess (baseUrl, identifier) {
- const cacheKey = `${ baseUrl }::${ identifier }`
- if (cache.has(cacheKey)) return cache.get(cacheKey)
-
- const { data } = await axios.get(baseUrl, {
- params: {
- service: 'WPS',
- version: '1.0.0',
- request: 'DescribeProcess',
- identifier,
- },
- headers: { Accept: 'application/xml' },
- responseType: 'text',
- })
-
- const result = parseProcessDescription(data, identifier)
- cache.set(cacheKey, result)
- return result
-}
-
-export function clearDescribeCache () {
- cache.clear()
-}
-
-// --- XML parsing -----------------------------------------------------------
-
-const OWS_NS = 'http://www.opengis.net/ows/1.1'
-const WPS_NS = 'http://www.opengis.net/wps/1.0.0'
-
-function parseProcessDescription (xml, identifier) {
- const doc = new DOMParser().parseFromString(xml, 'application/xml')
-
- const processNodes = doc.getElementsByTagNameNS(WPS_NS, 'ProcessDescription')
- const processNode = findProcessNode(processNodes, identifier) || processNodes[0]
-
- if (!processNode) {
- throw new Error(`DescribeProcess: no ProcessDescription found for "${ identifier }"`)
- }
-
- return {
- identifier,
- title: owsText(processNode, 'Title') || identifier,
- abstract: owsText(processNode, 'Abstract') || '',
- inputs: parseInputs(processNode),
- outputs: parseOutputs(processNode),
- }
-}
-
-function findProcessNode (nodes, identifier) {
- for (let i = 0; i < nodes.length; i++) {
- const id = owsText(nodes[i], 'Identifier')
- if (id === identifier) return nodes[i]
- }
- return null
-}
-
-function parseInputs (processNode) {
- const dataInputs = processNode.getElementsByTagNameNS(WPS_NS, 'Input')
- const inputs = []
-
- for (let i = 0; i < dataInputs.length; i++) {
- const node = dataInputs[i]
- const input = {
- id: owsText(node, 'Identifier'),
- title: owsText(node, 'Title') || owsText(node, 'Identifier'),
- required: parseInt(node.getAttribute('minOccurs') || '0', 10) > 0,
- }
-
- if (hasChild(node, 'LiteralData')) {
- input.type = 'LiteralData'
- Object.assign(input, parseLiteralData(node))
- } else if (hasChild(node, 'ComplexData')) {
- input.type = 'ComplexData'
- Object.assign(input, parseComplexData(node))
- } else if (hasChild(node, 'BoundingBoxData')) {
- input.type = 'BoundingBoxData'
- } else {
- input.type = 'LiteralData'
- }
-
- inputs.push(input)
- }
-
- return inputs
-}
-
-function parseOutputs (processNode) {
- const outputNodes = processNode.getElementsByTagNameNS(WPS_NS, 'Output')
- const outputs = []
-
- for (let i = 0; i < outputNodes.length; i++) {
- const node = outputNodes[i]
- const output = {
- id: owsText(node, 'Identifier'),
- title: owsText(node, 'Title') || owsText(node, 'Identifier'),
- }
-
- if (hasChild(node, 'ComplexOutput')) {
- output.type = 'ComplexData'
- const complexNode = firstChild(node, 'ComplexOutput')
- output.mimeType = parseDefaultMimeType(complexNode)
- } else {
- output.type = 'LiteralData'
- }
-
- outputs.push(output)
- }
-
- return outputs
-}
-
-function parseLiteralData (inputNode) {
- const result = {}
- const litNode = firstChild(inputNode, 'LiteralData')
- if (!litNode) return result
-
- const dataType = owsText(litNode, 'DataType')
- if (dataType) result.dataType = dataType
-
- const defaultVal = litNode.getElementsByTagNameNS(WPS_NS, 'DefaultValue')
- if (defaultVal.length > 0) {
- result.defaultValue = defaultVal[0].textContent.trim()
- }
-
- const allowedNode = litNode.getElementsByTagNameNS(OWS_NS, 'AllowedValues')
- if (allowedNode.length > 0) {
- const values = allowedNode[0].getElementsByTagNameNS(OWS_NS, 'Value')
- result.allowedValues = Array.from(values).map(v => v.textContent.trim())
- }
-
- return result
-}
-
-function parseComplexData (inputNode) {
- const result = {}
- const complexNode = firstChild(inputNode, 'ComplexData')
- if (!complexNode) return result
-
- result.mimeType = parseDefaultMimeType(complexNode)
- result.supportedMimeTypes = parseSupportedMimeTypes(complexNode)
-
- return result
-}
-
-function parseDefaultMimeType (parent) {
- const defNode = parent?.getElementsByTagName('Default')[0]
- const formatNode = defNode?.getElementsByTagName('Format')[0]
- const mimeNode = formatNode?.getElementsByTagName('MimeType')[0]
- ?? formatNode?.getElementsByTagNameNS(OWS_NS, 'MimeType')[0]
- return mimeNode?.textContent.trim() || null
-}
-
-function parseSupportedMimeTypes (parent) {
- const mimeTypes = []
- const supported = parent?.getElementsByTagName('Supported')[0]
- if (!supported) return mimeTypes
-
- const formats = supported.getElementsByTagName('Format')
- for (let i = 0; i < formats.length; i++) {
- const mimeNode = formats[i].getElementsByTagName('MimeType')[0]
- ?? formats[i].getElementsByTagNameNS(OWS_NS, 'MimeType')[0]
- if (mimeNode) mimeTypes.push(mimeNode.textContent.trim())
- }
- return mimeTypes
-}
-
-// --- DOM helpers ------------------------------------------------------------
-
-function owsText (parent, localName) {
- const el = parent.getElementsByTagNameNS(OWS_NS, localName)[0]
- return el?.textContent.trim() || null
-}
-
-function hasChild (parent, localName) {
- return firstChild(parent, localName) !== null
-}
-
-function firstChild (parent, localName) {
- return parent.getElementsByTagNameNS(WPS_NS, localName)[0]
- ?? parent.getElementsByTagName(localName)[0]
- ?? null
-}
diff --git a/src/lib/wps/handle-output.js b/src/lib/wps/handle-output.js
index 5dc649c..e624235 100644
--- a/src/lib/wps/handle-output.js
+++ b/src/lib/wps/handle-output.js
@@ -29,11 +29,25 @@ export function handleOutputActions (actions, response, stores) {
}
break
- case 'addLayer':
- if (value && stores.map?.addDynamicLayer) {
- stores.map.addDynamicLayer(value, action.layerConfig || {})
+ case 'addLayer': {
+ if (!value || !stores.map?.addDynamicLayer) break
+ const folders = Array.isArray(value) ? value : [value]
+ for (const folder of folders) {
+ const entries = folder?.contents ?? [folder]
+ for (const entry of entries) {
+ if (entry?.layer && entry?.url) {
+ stores.map.addDynamicLayer({
+ id: entry.layer,
+ name: entry.name || entry.layer,
+ layer: entry.layer,
+ url: entry.url,
+ ...(action.layerConfig || {}),
+ })
+ }
+ }
}
break
+ }
default:
console.warn(`[handleOutputActions] Unknown action: "${ action.action }"`)
diff --git a/src/lib/wps/resolve-input.js b/src/lib/wps/resolve-input.js
index 59acbbb..8a0f09c 100644
--- a/src/lib/wps/resolve-input.js
+++ b/src/lib/wps/resolve-input.js
@@ -1,84 +1,50 @@
/**
- * Resolves a WPS input value from a source string.
- *
- * Source format: "type:path"
- * - store:. → reads from a Pinia store
- * - payload: → reads from the step-complete event payload
- * - wpsResult: → reads from stored WPS results (appStore.wpsResults)
- * - static: → returns the literal string value
- *
- * @param {string} source - Source descriptor (e.g. "store:map.activeRegion.properties.NUTS_ID")
- * @param {Object} context
- * @param {Object} context.payload - The step-complete event payload
- * @param {Object} context.stores - Map of store name → Pinia store instance
- * @returns {*} The resolved value, or undefined if not found
+ * Resolve a single value from a source string.
+ * Source format: "store:path" | "payload:path" | "wpsResult:path" | "static:value"
*/
export function resolveInputValue (source, context) {
- const colonIndex = source.indexOf(':')
- if (colonIndex === -1) return undefined
-
- const type = source.substring(0, colonIndex)
- const path = source.substring(colonIndex + 1)
-
- switch (type) {
- case 'store': {
- const [storeName, ...pathParts] = path.split('.')
- const store = context.stores?.[storeName]
- if (!store) return undefined
- return getNestedValue(store, pathParts)
- }
-
- case 'payload':
- return getNestedValue(context.payload, path.split('.'))
-
- case 'wpsResult': {
- const wpsResults = context.stores?.app?.wpsResults
- if (!wpsResults) return undefined
- return getNestedValue(wpsResults, path.split('.'))
- }
-
- case 'static':
- return path
-
- default:
- console.warn(`[resolveInputValue] Unknown source type: "${ type }"`)
- return undefined
+ const i = source.indexOf(':')
+ if (i === -1) return undefined
+ const type = source.slice(0, i)
+ const path = source.slice(i + 1)
+ const pathParts = path.split('.')
+
+ if (type === 'store') {
+ const store = context.stores?.[pathParts[0]]
+ return store ? get(pathParts.slice(1), store) : undefined
+ }
+ if (type === 'payload') return get(pathParts, context.payload)
+ if (type === 'wpsResult') {
+ const results = context.stores?.app?.wpsResults
+ return results ? get(pathParts, results) : undefined
}
+ if (type === 'static') return path
+ return undefined
}
/**
- * Resolves all inputs for a WPS call by combining the source map from
- * navigation config with the type information from DescribeProcess.
- *
- * @param {Object} inputSourceMap - { wpsInputId: "source:path", ... } from navigation.json
- * @param {Array} processInputs - Input definitions from describeProcess()
- * @param {Object} context - { payload, stores }
- * @returns {Array} Ready-to-use inputs for sendWpsRequest
+ * Resolve WPS inputs from config.
+ * @param {Array} inputs - [{ id, type, source }, ...]
+ * @param {Object} context - { payload, stores }
+ * @returns {Array} [{ id, type, value }, ...] for sendWpsRequest
*/
-export function resolveAllInputs (inputSourceMap, processInputs, context) {
- return processInputs
- .map(inputDef => {
- const source = inputSourceMap[inputDef.id]
- if (!source) return null
-
- const value = resolveInputValue(source, context)
- if (value === undefined || value === null) return null
-
- return {
- id: inputDef.id,
- type: inputDef.type,
- value,
- mimeType: inputDef.mimeType,
- }
- })
- .filter(Boolean)
+export function resolveInputs (inputs, context) {
+ if (!Array.isArray(inputs)) return []
+ const out = []
+ for (const { id, type = 'LiteralData', source } of inputs) {
+ if (!id || !source) continue
+ const value = resolveInputValue(source, context)
+ if (value === undefined || value === null) continue
+ out.push({ id, type, value })
+ }
+ return out
}
-function getNestedValue (obj, pathParts) {
- let current = obj
+function get (pathParts, obj) {
+ let cur = obj
for (const key of pathParts) {
- if (current == null) return undefined
- current = current[key]
+ if (cur == null) return undefined
+ cur = cur[key]
}
- return current
+ return cur
}
diff --git a/src/stores/map.js b/src/stores/map.js
index cb51bb3..fa2534e 100644
--- a/src/stores/map.js
+++ b/src/stores/map.js
@@ -1,4 +1,3 @@
-// stores/map.js
import { defineStore } from 'pinia'
import layersConfig from '@/data/base-layers-config.json'
import buildMapboxLayer from '@/lib/build-mapbox-layer'
@@ -107,5 +106,24 @@ export const useMapStore = defineStore('map', {
clearActiveRegion () {
this.activeRegion = null
},
+
+ addDynamicLayer (layerConfig) {
+ const existing = this.mapboxLayers.find(l => l.id === layerConfig.id)
+ if (existing) return
+
+ const built = buildMapboxLayer({
+ ...layerConfig,
+ format: 'image/png',
+ })
+ if (built) {
+ this.mapboxLayers.push(built)
+ this.layerVisibility[layerConfig.id] = true
+ }
+ },
+
+ removeDynamicLayer (layerId) {
+ this.mapboxLayers = this.mapboxLayers.filter(l => l.id !== layerId)
+ delete this.layerVisibility[layerId]
+ },
},
})
From a43dbe50da2ad7beb321b39e7d49447ebdee933f Mon Sep 17 00:00:00 2001
From: IoannaMi <59479531+IoannaMi@users.noreply.github.com>
Date: Fri, 27 Feb 2026 10:55:41 +0100
Subject: [PATCH 5/6] fix lint
---
components.d.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/components.d.ts b/components.d.ts
index a7218cb..481364a 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -6,7 +6,7 @@
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
-export { }
+export {}
/* prettier-ignore */
declare module 'vue' {
From 5dfc8f03c316c06da69d9a4d02921b56ff2c9e44 Mon Sep 17 00:00:00 2001
From: IoannaMi <59479531+IoannaMi@users.noreply.github.com>
Date: Fri, 27 Feb 2026 11:08:12 +0100
Subject: [PATCH 6/6] add instructions for the navigation.json at the readme
---
README.md | 271 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 271 insertions(+)
diff --git a/README.md b/README.md
index cc806ee..c097f01 100644
--- a/README.md
+++ b/README.md
@@ -111,6 +111,277 @@ lare-viewer/
└── package.json # Project dependencies and scripts
```
+## Configuring `navigation.json`
+
+The workflow and the processes requests are configurable from the JSON file at `src/config/navigation.json`.
+
+### Overall structure
+
+```json
+{
+ "logo": "/desirmed_logo.png",
+ "menus": [
+ {
+ "id": "regionSelection",
+ "title": "Region Selection",
+ "drawerTitle": "Select a base map",
+ "icon": "mdi-map-marker-radius",
+ "requiredSteps": [],
+ "completionEvent": null,
+ "components": [ /* UI configuration */ ],
+ "wps": { /* optional WPS configuration */ }
+ }
+ ]
+}
+```
+
+- **`logo`**: Path (relative to `public/`) to the logo shown at the top of the main navigation drawer.
+- **`menus`**: Ordered list of workflow steps. Each menu item:
+ - **`id`**: Unique identifier used internally (e.g. for dependencies and WPS results).
+ - **`title`**: Label shown in the main navigation drawer.
+ - **`drawerTitle`**: Title shown at the top of the step drawer.
+ - **`icon`**: Vuetify Material Design Icon name (e.g. `mdi-map-marker-radius`).
+ - **`requiredSteps`** (optional): Array of menu `id`s that must be completed before this step is enabled.
+ - **`completionEvent`** (optional): How the step is marked complete. `null` (default) means completion is driven by child components; `"auto"` means the step completes as soon as its drawer opens.
+ - **`components`**: List of UI components rendered inside the step drawer.
+ - **`wps`** (optional): Configuration for a WPS Execute call associated with this step (see below).
+
+### Components inside a menu
+
+Each entry in `components` has the shape:
+
+```json
+{
+ "component": "LayerList",
+ "componentProps": {
+ "layers": [
+ {
+ "id": "layer-id",
+ "name": "Human readable name",
+ "active": true,
+ "clickable": true,
+ "propertiesBox": "region"
+ }
+ ]
+ }
+}
+```
+
+- **`component`**: Name of a Vue component in `src/components` (without `.vue`), e.g. `LayerList`, `SelectionList`, `NumberInput`.
+- **`componentProps`**: Arbitrary props passed straight through to the component.
+
+Different components expect different props:
+
+- **`LayerList`**:
+ - **`layers`**: Array of layer configs with:
+ - `id`: Map layer identifier.
+ - `name`: Display name in the UI.
+ - `active`: Whether the layer is initially visible.
+ - `clickable`: Whether clicking the map interacts with this layer.
+ - `propertiesBox` (optional): Name of a properties box configuration (used for showing feature attributes).
+- **`SelectionList`**:
+ - **`label`**: Label shown above the list.
+ - **`options`**: Array of `{ "id": "value", "name": "Label" }` entries.
+- **`NumberInput`**:
+ - Typical props include `label`, `suffix`, `min`, `max`, `step`, `defaultValue`, and `defaultValueSource` (see below under WPS input sources).
+
+### Wiring menus together with `requiredSteps`
+
+Menus can depend on the completion of earlier steps. For example:
+
+```json
+{
+ "id": "hazard",
+ "title": "Hazard mitigation selection",
+ "requiredSteps": ["regionSelection"],
+ "components": [ /* ... */ ]
+}
+```
+
+The `hazard` step becomes clickable only after the `regionSelection` step has been completed.
+
+### WPS configuration per step
+
+Each menu can optionally define a `wps` object describing a WPS Execute call to run when the step is completed or when the user interacts with the map.
+
+Example from `navigation.json`:
+
+```json
+{
+ "id": "regionSelection",
+ "title": "Region Selection",
+ "drawerTitle": "Select a base map",
+ "icon": "mdi-map-marker-radius",
+ "components": [
+ {
+ "component": "LayerList",
+ "componentProps": {
+ "layers": [
+ {
+ "id": "landuse:U2018_CLC2018_V2020_20u1_cog",
+ "name": "European Land Use Cover",
+ "active": true,
+ "clickable": false
+ },
+ {
+ "id": "region:nuts_2021_level_3",
+ "name": "NUTS 3 Regions",
+ "active": true,
+ "clickable": true,
+ "propertiesBox": "region"
+ }
+ ]
+ }
+ }
+ ],
+ "wps": {
+ "identifier": "lare_region",
+ "trigger": "mapClick",
+ "inputs": [
+ {
+ "id": "nutsname",
+ "type": "LiteralData",
+ "source": "store:map.activeRegion.properties.nuts_name"
+ }
+ ],
+ "storeResultAs": "regionSelection"
+ }
+}
+```
+
+The WPS configuration supports the following fields:
+
+- **`identifier`**: The WPS process identifier as exposed by the backend (e.g. `"lare_region"`, `"lare_hazard"`, `"lare_uom"`). This value is passed directly to the WPS `Execute` request.
+- **`trigger`**: When to execute the WPS call for this step:
+ - `"mapClick"`: Execute whenever the user selects a region on the map (using `mapStore.activeRegion`) while this step is open.
+ - `"stepComplete"`: Execute when the step signals completion (for example when the user picks an option or enters a number and the component emits a `step-complete` event).
+- **`inputs`**: Array of WPS input definitions:
+ - `id`: Input identifier expected by the WPS process.
+ - `type`: WPS data type, typically `"LiteralData"` (default) or `"ComplexData"`.
+ - `source`: Where the value comes from, using the pattern `:`.
+- **`storeResultAs`** (optional): Key under which the full WPS response is stored in the app store (`app.wpsResults[storeResultAs]`).
+- **`outputActions`** (optional): Array of post-processing instructions for the response (see below).
+
+The WPS calls are sent to the base URL configured via the environment variable:
+
+```env
+VITE_WPS_BASE_URL=https://your-wps-endpoint.example.com/wps
+```
+
+### WPS input `source` syntax
+
+The `source` field describes where to read the value for a given WPS input. Supported forms:
+
+- **`store:.`**:
+ - Reads from a Pinia store by name, e.g. `"store:map.activeRegion.properties.nuts_name"`.
+ - `storeName` is the key used in the WPS context (`app` or `map`).
+- **`payload:`**:
+ - Reads from the payload passed by the component that completes the step.
+ - Example: in the `hazard` step:
+ ```json
+ { "id": "hazard", "type": "LiteralData", "source": "payload:value" }
+ ```
+- **`wpsResult:`**:
+ - Reads from previously stored WPS results (`app.wpsResults`), allowing chaining between steps.
+ - Example: in the `uom` step:
+ ```json
+ "defaultValueSource": "wpsResult:regionSelection.suggested_uom"
+ ```
+- **`static:`**:
+ - Use a literal constant value.
+
+### Storing and reusing WPS results
+
+When `storeResultAs` is set, the full parsed WPS response is stored under:
+
+- `app.wpsResults[storeResultAs]`
+
+You can then reference this data in later steps via `source: "wpsResult:..."` or via component props like `defaultValueSource` (for `NumberInput`).
+
+Example:
+
+```json
+{
+ "id": "uom",
+ "title": "Calculate UOM",
+ "requiredSteps": ["hazard"],
+ "wps": {
+ "identifier": "lare_uom",
+ "trigger": "stepComplete",
+ "inputs": [
+ { "id": "nutsname", "type": "LiteralData", "source": "store:map.activeRegion.properties.nuts_name" },
+ { "id": "uomsize", "type": "LiteralData", "source": "payload:value" }
+ ],
+ "storeResultAs": "uom"
+ },
+ "components": [
+ {
+ "component": "NumberInput",
+ "componentProps": {
+ "label": "Hexagon size (in m²)",
+ "suffix": "m²",
+ "min": 100,
+ "max": 50000000,
+ "step": 100000,
+ "defaultValue": 1000,
+ "defaultValueSource": "wpsResult:regionSelection.suggested_uom"
+ }
+ }
+ ]
+}
+```
+
+### Post-processing with `outputActions`
+
+For more advanced cases, you can define `outputActions` inside the `wps` block to automatically store parts of the response or to add dynamic layers to the map.
+
+Supported actions:
+
+- **`storeValue`**: Store a (sub-)value into `app.wpsResults` under a nested key.
+- **`addLayer`**: Add one or more dynamic layers to the map (via `mapStore.addDynamicLayer`).
+
+Example:
+
+```json
+{
+ "wps": {
+ "identifier": "lare_example",
+ "trigger": "stepComplete",
+ "inputs": [ /* ... */ ],
+ "outputActions": [
+ {
+ "action": "storeValue",
+ "path": "response.statistics",
+ "storeAs": "example.statistics"
+ },
+ {
+ "action": "addLayer",
+ "path": "response.layers"
+ }
+ ]
+ }
+}
+```
+
+- **`path`**:
+ - Dot-notated path into the WPS response object.
+ - If omitted or set to `"response"`, the entire response is used.
+ - If it starts with `"response."`, that prefix is ignored (so `"response.layers"` and `"layers"` are equivalent).
+- **`storeAs`** (for `storeValue`):
+ - Dot-notated path where the value will be stored under `app.wpsResults`, e.g. `"example.statistics"`.
+- **`layerConfig`** (optional, for `addLayer`):
+ - Extra fields merged into each dynamic layer configuration (e.g. default opacity, visibility flags).
+
+The structure expected by `addLayer` is:
+
+- Either an array under the given `path`, or an object with a `contents` array.
+- Each entry in `contents` (or the array itself) should contain:
+ - `layer`: Layer identifier.
+ - `url`: WMS/WFS/WMS-T endpoint for the layer.
+ - `name` (optional): Human-readable name.
+
+This matches the output format produced by the backend WPS processes and is what the viewer uses to add layers dynamically.
+
## License