Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 131 additions & 6 deletions src/config/workflow.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"id": "administration",
"name": "Administration",
"layerName": "region:nuts_2021_level_3",
"regionIdProperty": "nuts_id",
"regionIdProperty": "nuts_name",
"layerNameForWPS": "region:nuts_2021"
},
{
Expand All @@ -47,12 +47,8 @@
"title": "Region Selection",
"drawerTitle": "Select a base map",
"icon": "mdi-map-marker-radius",
"requiredSteps": [
"userCaseSelection"
],
"explanation": "Select a region on the map by clicking it, then confirm to continue.",
"requiresConfirmation": true,
"confirmationSource": "mapClick",
"requiresConfirmation": false,
"components": [
{
"component": "LayerList",
Expand Down Expand Up @@ -145,6 +141,135 @@
}
}
]
},
{
"id": "archetypeSelection",
"title": "Archetype Selection",
"drawerTitle": "Select the archetype",
"requiredSteps": [
"uom"
],
"explanation": "Select the archetype and confirm to continue.",
"requiresConfirmation": true,
"confirmationSource": "component",
"wps": {
"identifier": "lare_coastal",
"trigger": "stepComplete",
"inputs": [
{
"id": "sessionid",
"type": "LiteralData",
"source": "store:app.wpsResults.initialSetup.sessionid"
}
],
"storeResultAs": "coastalUseCase",
"outputActions": [
{
"action": "removeLayer",
"fromResultKey": "uom",
"path": "layers"
},
{
"action": "addLayer",
"path": "layers"
}
]
},
"components": [
{
"component": "SelectionList",
"componentProps": {
"label": "Archetypes",
"selectionKey": "archetypeSelection",
"options": [
{
"id": "coastal",
"name": "Urban Coastal"
},
{
"id": "agricultural_land",
"name": "Agricultural Land"
},
{
"id": "rural_settlements",
"name": "Rural Settlements"
}
]
}
}
]
},
{
"id": "kcsSelection",
"title": "KCS & Hazards",
"drawerTitle": "Select KCS and hazard",
"requiredSteps": [
"archetypeSelection"
],
"explanation": "Select KCS and hazard type, then confirm to run the UOM KCS calculation.",
"requiresConfirmation": true,
"confirmationSource": "component",
"wps": {
"identifier": "lare_kcs",
"trigger": "stepComplete",
"inputs": [
{
"id": "archetype",
"type": "LiteralData",
"source": "store:app.selections.archetypeSelection"
},
{
"id": "kcs",
"type": "LiteralData",
"source": "store:app.selections.kcsSelection"
},
{
"id": "sessionid",
"type": "LiteralData",
"source": "store:app.wpsResults.initialSetup.sessionid"
},
{
"id": "hazard",
"type": "LiteralData",
"source": "store:app.selections.hazardSelection"
}
],
"storeResultAs": "uomKcs",
"outputActions": [
{
"action": "addLayer",
"path": "layers"
}
]
},
"components": [
{
"component": "SelectionList",
"componentProps": {
"label": "KCS",
"selectionKey": "kcsSelection",
"options": [
{
"id": "transport",
"name": "Transport"
}
]
}
},
{
"component": "SelectionList",
"componentProps": {
"label": "Hazards",
"selectionKey": "hazardSelection",
"options": [
{
"id": "pluvial_RP200",
"name": "Pluvial floods"
}
]
}
}
]
}
]
}
58 changes: 47 additions & 11 deletions src/lib/wps/handle-output.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,40 @@
* from workflow.json.
*
* Supported actions:
* - storeValue: saves a value (or sub-path of response) into appStore.wpsResults
* - addLayer: adds a dynamic layer to the map via mapStore
* - storeValue: saves a value (or sub-path of response) into appStore.wpsResults
* - addLayer: adds a dynamic layer to the map via mapStore
* - removeLayer: removes a dynamic layer that was previously added
*
* @param {Array<OutputAction>} actions - Output action definitions from config
* @param {Object} response - The parsed WPS response
* @param {Object} stores - { app: appStore, map: mapStore }
*
* @typedef {Object} OutputAction
* @property {'storeValue'|'addLayer'} action
* @property {string} [path] - Dot-notated path into the response (omit or "response" for full response)
* @property {string} [storeAs] - For storeValue: key under wpsResults to store into
* @property {Object} [layerConfig] - For addLayer: layer configuration
* @property {'storeValue'|'addLayer'|'removeLayer'} action
* @property {string} [path] - For storeValue/addLayer: dot-notated path into the current WPS response (omit or "response" for full response)
* @property {string} [storeAs] - For storeValue: key under wpsResults to store into
* @property {Object} [layerConfig] - For addLayer: layer configuration
* @property {string} [fromResultKey] - For removeLayer: key in appStore.wpsResults to resolve layers from
*/
export function handleOutputActions (actions, response, stores) {
if (!actions?.length || !response) return

for (const action of actions) {
const value = resolvePath(response, action.path)

switch (action.action) {
case 'storeValue':
case 'storeValue': {
const value = resolvePath(response, action.path)
if (action.storeAs && stores.app) {
setNestedValue(stores.app.wpsResults, action.storeAs, value)
}
break
}

case 'addLayer': {
const value = resolvePath(response, action.path)
if (!value || !stores.map?.addDynamicLayer) break
const folders = Array.isArray(value) ? value : [value]
const folders = Array.isArray(value) ? value : [ value ]
for (const folder of folders) {
const entries = folder?.contents ?? [folder]
const entries = folder?.contents ?? [ folder ]
for (const entry of entries) {
if (entry?.layer && entry?.url) {
stores.map.addDynamicLayer({
Expand All @@ -49,6 +52,39 @@ export function handleOutputActions (actions, response, stores) {
break
}

case 'removeLayer': {
const { fromResultKey } = action
const previousResult = fromResultKey && stores.app?.wpsResults
? stores.app.wpsResults[fromResultKey]
: null

// If we cannot resolve a previous result or the path,
// fall back to clearing all dynamic layers.
if (!stores.map?.removeDynamicLayer) break

if (previousResult) {
const value = resolvePath(previousResult, action.path)
if (value) {
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) {
stores.map.removeDynamicLayer(entry.layer)
}
}
}
break
}
}

// Fallback: remove all dynamic layers (those not in the static config)
if (stores.map?.clearDynamicLayers) {
stores.map.clearDynamicLayers()
}
break
}

default:
console.warn(`[handleOutputActions] Unknown action: "${ action.action }"`)
}
Expand Down
36 changes: 36 additions & 0 deletions src/stores/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import buildMapboxLayer from '@/lib/build-mapbox-layer'
export const useMapStore = defineStore('map', {
state: () => ({
layersConfig,
staticLayerIds: layersConfig.map(cfg => cfg.id),
mapboxLayers: [],
layerVisibility: {},
layerClickable: {},
Expand Down Expand Up @@ -167,12 +168,47 @@ export const useMapStore = defineStore('map', {
if (built) {
this.mapboxLayers.push(built)
this.layerVisibility[layerConfig.id] = true

// Ensure dynamic layers also show up in the legend by
// creating a corresponding layersConfig entry when needed.
const hasConfig = this.layersConfig.some(cfg => cfg.id === layerConfig.id)
if (!hasConfig) {
this.layersConfig.push({
id: layerConfig.id,
url: layerConfig.url,
layer: layerConfig.layer,
name: layerConfig.name || layerConfig.id,
dynamic: true,
})
}
}
},

removeDynamicLayer (layerId) {
this.mapboxLayers = this.mapboxLayers.filter(l => l.id !== layerId)
delete this.layerVisibility[layerId]

// Remove any dynamic-only config entry so legends stay in sync
this.layersConfig = this.layersConfig.filter(cfg => !(cfg.id === layerId && cfg.dynamic))
},

/**
* Removes all dynamically added layers (those not present in the static layersConfig).
*/
clearDynamicLayers () {
const staticIds = new Set(this.staticLayerIds)

// Remove all non-static layers from the map and visibility state
this.mapboxLayers = this.mapboxLayers.filter(layer => {
const isStatic = staticIds.has(layer.id)
if (!isStatic) {
delete this.layerVisibility[layer.id]
}
return isStatic
})

// Strip any dynamic layer configs so legends update accordingly
this.layersConfig = this.layersConfig.filter(cfg => staticIds.has(cfg.id))
},
},
})
Loading