diff --git a/src/config/workflow.json b/src/config/workflow.json index 5a980f7..f81b429 100644 --- a/src/config/workflow.json +++ b/src/config/workflow.json @@ -27,7 +27,7 @@ "id": "administration", "name": "Administration", "layerName": "region:nuts_2021_level_3", - "regionIdProperty": "nuts_id", + "regionIdProperty": "nuts_name", "layerNameForWPS": "region:nuts_2021" }, { @@ -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", @@ -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" + } + ] + } + } + ] } ] } \ No newline at end of file diff --git a/src/lib/wps/handle-output.js b/src/lib/wps/handle-output.js index 1059b11..bda09bd 100644 --- a/src/lib/wps/handle-output.js +++ b/src/lib/wps/handle-output.js @@ -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} 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({ @@ -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 }"`) } diff --git a/src/stores/map.js b/src/stores/map.js index 814918d..7f14805 100644 --- a/src/stores/map.js +++ b/src/stores/map.js @@ -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: {}, @@ -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)) }, }, })