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