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
271 changes: 271 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<sourceType>:<path>`.
- **`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:<storeName>.<path>`**:
- 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:<path>`**:
- 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:<path>`**:
- Reads from previously stored WPS results (`app.wpsResults`), allowing chaining between steps.
- Example: in the `uom` step:
```json
"defaultValueSource": "wpsResult:regionSelection.suggested_uom"
```
- **`static:<value>`**:
- 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

Expand Down
5 changes: 0 additions & 5 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
ActiveFeatureProperties: typeof import('./src/components/ActiveFeatureProperties.vue')['default']
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']
LayerLegend: typeof import('./src/components/LayerLegend.vue')['default']
LayerList: typeof import('./src/components/LayerList.vue')['default']
MapComponent: typeof import('./src/components/MapComponent.vue')['default']
Expand Down
38 changes: 0 additions & 38 deletions src/components/HazardMenu.vue

This file was deleted.

24 changes: 22 additions & 2 deletions src/components/NumberInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
</template>

<script setup>
import { ref } from 'vue'
import { ref, watch, computed } from 'vue'
import { useAppStore } from '@/stores/app'
import { resolveInputValue } from '@/lib/wps/resolve-input'

const props = defineProps({
label: { type: String, required: true },
Expand All @@ -35,10 +37,28 @@
max: { type: Number, default: 10000 },
step: { type: Number, default: 1 },
defaultValue: { type: Number, default: 0 },
defaultValueSource: { type: String, default: null },
})

const emit = defineEmits(['step-complete'])
const value = ref(props.defaultValue)
const appStore = useAppStore()

const resolvedDefault = computed(() => {
if (!props.defaultValueSource) return props.defaultValue
const resolved = resolveInputValue(props.defaultValueSource, {
payload: {},
stores: { app: appStore },
})
return resolved != null ? Number(resolved) : props.defaultValue
})

const value = ref(resolvedDefault.value)

watch(resolvedDefault, (newVal) => {
if (newVal != null) {
value.value = newVal
}
})

function confirm () {
emit('step-complete', { value: value.value })
Expand Down
Loading