diff --git a/api-client/typescript/.openapi-generator/FILES b/api-client/typescript/.openapi-generator/FILES
index 17176b6..ebd99b5 100644
--- a/api-client/typescript/.openapi-generator/FILES
+++ b/api-client/typescript/.openapi-generator/FILES
@@ -28,6 +28,10 @@ models/BiodiversitySensitiveAreasProcessOutputs.ts
models/BiodiversitySensitiveAreasProcessParams.ts
models/BooleanField.ts
models/BoundingBox.ts
+models/ClimateRiskInputs.ts
+models/ClimateRiskOutputs.ts
+models/ClimateRiskProcessParams.ts
+models/ClimateVariable.ts
models/Conformance.ts
models/Constraints.ts
models/Constraints1.ts
@@ -48,6 +52,8 @@ models/Constraints6.ts
models/Constraints7.ts
models/Constraints8.ts
models/Constraints9.ts
+models/CordexModel.ts
+models/CordexRegion.ts
models/CreditsForJob.ts
models/DataResource.ts
models/DateField.ts
diff --git a/api-client/typescript/apis/ProcessesApi.ts b/api-client/typescript/apis/ProcessesApi.ts
index 3e226f5..94e6a5a 100644
--- a/api-client/typescript/apis/ProcessesApi.ts
+++ b/api-client/typescript/apis/ProcessesApi.ts
@@ -10,6 +10,8 @@ import {SecurityAuthentication} from '../auth/auth';
import { BiodiversitySensitiveAreasProcessOutputs } from '../models/BiodiversitySensitiveAreasProcessOutputs';
import { BiodiversitySensitiveAreasProcessParams } from '../models/BiodiversitySensitiveAreasProcessParams';
+import { ClimateRiskOutputs } from '../models/ClimateRiskOutputs';
+import { ClimateRiskProcessParams } from '../models/ClimateRiskProcessParams';
import { Exception } from '../models/Exception';
import { Execute } from '../models/Execute';
import { HabitatDistanceProcessOutputs } from '../models/HabitatDistanceProcessOutputs';
@@ -101,6 +103,46 @@ export class ProcessesApiRequestFactory extends BaseAPIRequestFactory {
return requestContext;
}
+ /**
+ * @param climateRiskProcessParams
+ */
+ public async executeClimateRisk(climateRiskProcessParams: ClimateRiskProcessParams, _options?: Configuration): Promise {
+ let _config = _options || this.configuration;
+
+ // verify required parameter 'climateRiskProcessParams' is not null or undefined
+ if (climateRiskProcessParams === null || climateRiskProcessParams === undefined) {
+ throw new RequiredError("ProcessesApi", "executeClimateRisk", "climateRiskProcessParams");
+ }
+
+
+ // Path Params
+ const localVarPath = '/processes/climate-risk/execution';
+
+ // Make Request Context
+ const requestContext = _config.baseServer.makeRequestContext(localVarPath, HttpMethod.POST);
+ requestContext.setHeaderParam("Accept", "application/json, */*;q=0.8")
+
+
+ // Body Params
+ const contentType = ObjectSerializer.getPreferredMediaType([
+ "application/json"
+ ]);
+ requestContext.setHeaderParam("Content-Type", contentType);
+ const serializedBody = ObjectSerializer.stringify(
+ ObjectSerializer.serialize(climateRiskProcessParams, "ClimateRiskProcessParams", ""),
+ contentType
+ );
+ requestContext.setBody(serializedBody);
+
+
+ const defaultAuth: SecurityAuthentication | undefined = _config?.authMethods?.default
+ if (defaultAuth?.applySecurityAuthentication) {
+ await defaultAuth?.applySecurityAuthentication(requestContext);
+ }
+
+ return requestContext;
+ }
+
/**
* @param habitatDistanceProcessParams
*/
@@ -498,6 +540,35 @@ export class ProcessesApiResponseProcessor {
throw new ApiException(response.httpStatusCode, "Unknown API Status Code!", await response.getBodyAsAny(), response.headers);
}
+ /**
+ * Unwraps the actual response sent by the server from the response context and deserializes the response content
+ * to the expected objects
+ *
+ * @params response Response returned by the server for a request to executeClimateRisk
+ * @throws ApiException if the response code was not in [200, 299]
+ */
+ public async executeClimateRiskWithHttpInfo(response: ResponseContext): Promise> {
+ const contentType = ObjectSerializer.normalizeMediaType(response.headers["content-type"]);
+ if (isCodeInRange("200", response.httpStatusCode)) {
+ const body: ClimateRiskOutputs = ObjectSerializer.deserialize(
+ ObjectSerializer.parse(await response.body.text(), contentType),
+ "ClimateRiskOutputs", ""
+ ) as ClimateRiskOutputs;
+ return new HttpInfo(response.httpStatusCode, response.headers, response.body, body);
+ }
+
+ // Work around for missing responses in specification, e.g. for petstore.yaml
+ if (response.httpStatusCode >= 200 && response.httpStatusCode <= 299) {
+ const body: ClimateRiskOutputs = ObjectSerializer.deserialize(
+ ObjectSerializer.parse(await response.body.text(), contentType),
+ "ClimateRiskOutputs", ""
+ ) as ClimateRiskOutputs;
+ return new HttpInfo(response.httpStatusCode, response.headers, response.body, body);
+ }
+
+ throw new ApiException(response.httpStatusCode, "Unknown API Status Code!", await response.getBodyAsAny(), response.headers);
+ }
+
/**
* Unwraps the actual response sent by the server from the response context and deserializes the response content
* to the expected objects
diff --git a/api-client/typescript/docs/ProcessesApi.md b/api-client/typescript/docs/ProcessesApi.md
index e8ca86b..c4bf5b0 100644
--- a/api-client/typescript/docs/ProcessesApi.md
+++ b/api-client/typescript/docs/ProcessesApi.md
@@ -6,6 +6,7 @@ Method | HTTP request | Description
------------- | ------------- | -------------
[**_delete**](ProcessesApi.md#_delete) | **DELETE** /jobs/{jobId} | Cancel a job execution, remove finished job
[**executeBiodiversitySensitiveAreas**](ProcessesApi.md#executeBiodiversitySensitiveAreas) | **POST** /processes/biodiversity-sensitive-areas/execution |
+[**executeClimateRisk**](ProcessesApi.md#executeClimateRisk) | **POST** /processes/climate-risk/execution |
[**executeHabitatDistance**](ProcessesApi.md#executeHabitatDistance) | **POST** /processes/habitatDistance/execution |
[**executeLandUseSealedArea**](ProcessesApi.md#executeLandUseSealedArea) | **POST** /processes/land-use-sealed-area/execution |
[**executeNdvi**](ProcessesApi.md#executeNdvi) | **POST** /processes/ndvi/execution |
@@ -127,6 +128,87 @@ No authorization required
- **Accept**: application/json
+### HTTP response details
+| Status code | Description | Response headers |
+|-------------|-------------|------------------|
+**200** | | - |
+
+[[Back to top]](#) [[Back to API list]](README.md#documentation-for-api-endpoints) [[Back to Model list]](README.md#documentation-for-models) [[Back to README]](README.md)
+
+# **executeClimateRisk**
+> ClimateRiskOutputs executeClimateRisk(climateRiskProcessParams)
+
+
+### Example
+
+
+```typescript
+import { createConfiguration, ProcessesApi } from '';
+import type { ProcessesApiExecuteClimateRiskRequest } from '';
+
+const configuration = createConfiguration();
+const apiInstance = new ProcessesApi(configuration);
+
+const request: ProcessesApiExecuteClimateRiskRequest = {
+
+ climateRiskProcessParams: {
+ inputs: {
+ coordinate: {
+ value: {
+ type: "Point",
+ coordinates: [
+ 3.14,
+ ],
+ bbox: [
+ 3.14,
+ ],
+ },
+ mediaType: "application/geo+json",
+ },
+ yearBegin: 0,
+ yearRange: 0,
+ referenceYearBegin: 0,
+ variables: [
+ "heatDays",
+ ],
+ models: [
+ "MPI-M-MPI-ESM-LR",
+ ],
+ region: null,
+ },
+ outputs: {
+ "key": null,
+ },
+ response: "raw",
+ },
+};
+
+const data = await apiInstance.executeClimateRisk(request);
+console.log('API called successfully. Returned data:', data);
+```
+
+
+### Parameters
+
+Name | Type | Description | Notes
+------------- | ------------- | ------------- | -------------
+ **climateRiskProcessParams** | **ClimateRiskProcessParams**| |
+
+
+### Return type
+
+**ClimateRiskOutputs**
+
+### Authorization
+
+No authorization required
+
+### HTTP request headers
+
+ - **Content-Type**: application/json
+ - **Accept**: application/json
+
+
### HTTP response details
| Status code | Description | Response headers |
|-------------|-------------|------------------|
diff --git a/api-client/typescript/models/ClimateRiskInputs.ts b/api-client/typescript/models/ClimateRiskInputs.ts
new file mode 100644
index 0000000..2b59bfb
--- /dev/null
+++ b/api-client/typescript/models/ClimateRiskInputs.ts
@@ -0,0 +1,96 @@
+/**
+ * BioIS API
+ * API for the BioIS service, providing access to geospatial processing and job management.
+ *
+ * OpenAPI spec version: 0.2.0
+ * Contact: info@geoengine.de
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { ClimateVariable } from '../models/ClimateVariable';
+import { CordexModel } from '../models/CordexModel';
+import { CordexRegion } from '../models/CordexRegion';
+import { PointGeoJsonInput } from '../models/PointGeoJsonInput';
+import { HttpFile } from '../http/http';
+
+/**
+* User-supplied inputs for the climate risk process.
+*/
+export class ClimateRiskInputs {
+ 'coordinate': PointGeoJsonInput;
+ /**
+ * Year of reporting or change (e.g., 2023, 2024, etc.)
+ */
+ 'yearBegin'?: number;
+ /**
+ * Length of a time window in years (e.g., 5 years).
+ */
+ 'yearRange'?: number;
+ /**
+ * Year of reporting or change (e.g., 2023, 2024, etc.)
+ */
+ 'referenceYearBegin': number;
+ 'variables'?: Array;
+ 'models'?: Array;
+ 'region'?: CordexRegion | null;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "coordinate",
+ "baseName": "coordinate",
+ "type": "PointGeoJsonInput",
+ "format": ""
+ },
+ {
+ "name": "yearBegin",
+ "baseName": "yearBegin",
+ "type": "number",
+ "format": "int32"
+ },
+ {
+ "name": "yearRange",
+ "baseName": "yearRange",
+ "type": "number",
+ "format": "int32"
+ },
+ {
+ "name": "referenceYearBegin",
+ "baseName": "referenceYearBegin",
+ "type": "number",
+ "format": "int32"
+ },
+ {
+ "name": "variables",
+ "baseName": "variables",
+ "type": "Array",
+ "format": ""
+ },
+ {
+ "name": "models",
+ "baseName": "models",
+ "type": "Array",
+ "format": ""
+ },
+ {
+ "name": "region",
+ "baseName": "region",
+ "type": "CordexRegion",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ClimateRiskInputs.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
+
+
diff --git a/api-client/typescript/models/ClimateRiskOutputs.ts b/api-client/typescript/models/ClimateRiskOutputs.ts
new file mode 100644
index 0000000..eb3e662
--- /dev/null
+++ b/api-client/typescript/models/ClimateRiskOutputs.ts
@@ -0,0 +1,75 @@
+/**
+ * BioIS API
+ * API for the BioIS service, providing access to geospatial processing and job management.
+ *
+ * OpenAPI spec version: 0.2.0
+ * Contact: info@geoengine.de
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { ClimateRiskInputs } from '../models/ClimateRiskInputs';
+import { DataResource } from '../models/DataResource';
+import { HttpFile } from '../http/http';
+
+/**
+* Output of the climate risk process: summary table and raw ensemble data.
+*/
+export class ClimateRiskOutputs {
+ 'inputs'?: ClimateRiskInputs | null;
+ /**
+ * Analysis window as `\"2041–2070\"`, used for display in result headlines.
+ */
+ 'analysisPeriod'?: string | null;
+ /**
+ * Reference window used for anomalies as `\"2006–2025\"`, `None` when no reference period.
+ */
+ 'referencePeriod'?: string | null;
+ 'climateRisk'?: DataResource | null;
+ 'rawEnsembleData'?: DataResource | null;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "inputs",
+ "baseName": "inputs",
+ "type": "ClimateRiskInputs",
+ "format": ""
+ },
+ {
+ "name": "analysisPeriod",
+ "baseName": "analysisPeriod",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "referencePeriod",
+ "baseName": "referencePeriod",
+ "type": "string",
+ "format": ""
+ },
+ {
+ "name": "climateRisk",
+ "baseName": "climateRisk",
+ "type": "DataResource",
+ "format": ""
+ },
+ {
+ "name": "rawEnsembleData",
+ "baseName": "rawEnsembleData",
+ "type": "DataResource",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ClimateRiskOutputs.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
diff --git a/api-client/typescript/models/ClimateRiskProcessParams.ts b/api-client/typescript/models/ClimateRiskProcessParams.ts
new file mode 100644
index 0000000..2185ebf
--- /dev/null
+++ b/api-client/typescript/models/ClimateRiskProcessParams.ts
@@ -0,0 +1,57 @@
+/**
+ * BioIS API
+ * API for the BioIS service, providing access to geospatial processing and job management.
+ *
+ * OpenAPI spec version: 0.2.0
+ * Contact: info@geoengine.de
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { ClimateRiskInputs } from '../models/ClimateRiskInputs';
+import { Response } from '../models/Response';
+import { HttpFile } from '../http/http';
+
+/**
+* Process execution (Climate Risk)
+*/
+export class ClimateRiskProcessParams {
+ 'inputs': ClimateRiskInputs;
+ 'outputs'?: { [key: string]: any; };
+ 'response'?: Response;
+
+ static readonly discriminator: string | undefined = undefined;
+
+ static readonly mapping: {[index: string]: string} | undefined = undefined;
+
+ static readonly attributeTypeMap: Array<{name: string, baseName: string, type: string, format: string}> = [
+ {
+ "name": "inputs",
+ "baseName": "inputs",
+ "type": "ClimateRiskInputs",
+ "format": ""
+ },
+ {
+ "name": "outputs",
+ "baseName": "outputs",
+ "type": "{ [key: string]: any; }",
+ "format": ""
+ },
+ {
+ "name": "response",
+ "baseName": "response",
+ "type": "Response",
+ "format": ""
+ } ];
+
+ static getAttributeTypeMap() {
+ return ClimateRiskProcessParams.attributeTypeMap;
+ }
+
+ public constructor() {
+ }
+}
+
+
diff --git a/api-client/typescript/models/ClimateVariable.ts b/api-client/typescript/models/ClimateVariable.ts
new file mode 100644
index 0000000..b0d6b5e
--- /dev/null
+++ b/api-client/typescript/models/ClimateVariable.ts
@@ -0,0 +1,25 @@
+/**
+ * BioIS API
+ * API for the BioIS service, providing access to geospatial processing and job management.
+ *
+ * OpenAPI spec version: 0.2.0
+ * Contact: info@geoengine.de
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+/**
+* Climate variable to compute (WMO-based daily threshold indicators).
+*/
+export enum ClimateVariable {
+ HeatDays = 'heatDays',
+ IceDays = 'iceDays',
+ TropicalNights = 'tropicalNights',
+ FrostDays = 'frostDays',
+ DryDays = 'dryDays',
+ HeavyRainDays = 'heavyRainDays'
+}
diff --git a/api-client/typescript/models/CordexModel.ts b/api-client/typescript/models/CordexModel.ts
new file mode 100644
index 0000000..16cc550
--- /dev/null
+++ b/api-client/typescript/models/CordexModel.ts
@@ -0,0 +1,21 @@
+/**
+ * BioIS API
+ * API for the BioIS service, providing access to geospatial processing and job management.
+ *
+ * OpenAPI spec version: 0.2.0
+ * Contact: info@geoengine.de
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+/**
+* CORDEX climate model.
+*/
+export enum CordexModel {
+ MpiMMpiEsmLr = 'MPI-M-MPI-ESM-LR',
+ MohcHadGem2Es = 'MOHC-HadGEM2-ES'
+}
diff --git a/api-client/typescript/models/CordexRegion.ts b/api-client/typescript/models/CordexRegion.ts
new file mode 100644
index 0000000..a27e73d
--- /dev/null
+++ b/api-client/typescript/models/CordexRegion.ts
@@ -0,0 +1,20 @@
+/**
+ * BioIS API
+ * API for the BioIS service, providing access to geospatial processing and job management.
+ *
+ * OpenAPI spec version: 0.2.0
+ * Contact: info@geoengine.de
+ *
+ * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+ * https://openapi-generator.tech
+ * Do not edit the class manually.
+ */
+
+import { HttpFile } from '../http/http';
+
+/**
+* CORDEX climate region.
+*/
+export enum CordexRegion {
+ Eur = 'Eur'
+}
diff --git a/api-client/typescript/models/ObjectSerializer.ts b/api-client/typescript/models/ObjectSerializer.ts
index daae640..5eb4e27 100644
--- a/api-client/typescript/models/ObjectSerializer.ts
+++ b/api-client/typescript/models/ObjectSerializer.ts
@@ -8,6 +8,10 @@ export * from '../models/BiodiversitySensitiveAreasProcessOutputs';
export * from '../models/BiodiversitySensitiveAreasProcessParams';
export * from '../models/BooleanField';
export * from '../models/BoundingBox';
+export * from '../models/ClimateRiskInputs';
+export * from '../models/ClimateRiskOutputs';
+export * from '../models/ClimateRiskProcessParams';
+export * from '../models/ClimateVariable';
export * from '../models/Conformance';
export * from '../models/Constraints';
export * from '../models/Constraints1';
@@ -28,6 +32,8 @@ export * from '../models/Constraints6';
export * from '../models/Constraints7';
export * from '../models/Constraints8';
export * from '../models/Constraints9';
+export * from '../models/CordexModel';
+export * from '../models/CordexRegion';
export * from '../models/CreditsForJob';
export * from '../models/DataResource';
export * from '../models/DateField';
@@ -126,6 +132,10 @@ import { BiodiversitySensitiveAreasProcessOutputs } from '../models/Biodiversity
import { BiodiversitySensitiveAreasProcessParams } from '../models/BiodiversitySensitiveAreasProcessParams';
import { BooleanField , BooleanFieldTypeEnum , BooleanFieldFormatEnum } from '../models/BooleanField';
import { BoundingBox } from '../models/BoundingBox';
+import { ClimateRiskInputs } from '../models/ClimateRiskInputs';
+import { ClimateRiskOutputs } from '../models/ClimateRiskOutputs';
+import { ClimateRiskProcessParams } from '../models/ClimateRiskProcessParams';
+import { ClimateVariable } from '../models/ClimateVariable';
import { Conformance } from '../models/Conformance';
import { Constraints } from '../models/Constraints';
import { Constraints1 } from '../models/Constraints1';
@@ -146,6 +156,8 @@ import { Constraints6 } from '../models/Constraints6';
import { Constraints7 } from '../models/Constraints7';
import { Constraints8 } from '../models/Constraints8';
import { Constraints9 } from '../models/Constraints9';
+import { CordexModel } from '../models/CordexModel';
+import { CordexRegion } from '../models/CordexRegion';
import { CreditsForJob } from '../models/CreditsForJob';
import { DataResourceClass } from '../models/DataResource';
import { DateField , DateFieldTypeEnum } from '../models/DateField';
@@ -252,6 +264,9 @@ let enumsMap: Set = new Set([
"ArrayFieldFormatEnum",
"BooleanFieldTypeEnum",
"BooleanFieldFormatEnum",
+ "ClimateVariable",
+ "CordexModel",
+ "CordexRegion",
"DateFieldTypeEnum",
"DateTimeFieldTypeEnum",
"DurationFieldTypeEnum",
@@ -308,6 +323,9 @@ let typeMap: {[index: string]: any} = {
"BiodiversitySensitiveAreasProcessParams": BiodiversitySensitiveAreasProcessParams,
"BooleanField": BooleanField,
"BoundingBox": BoundingBox,
+ "ClimateRiskInputs": ClimateRiskInputs,
+ "ClimateRiskOutputs": ClimateRiskOutputs,
+ "ClimateRiskProcessParams": ClimateRiskProcessParams,
"Conformance": Conformance,
"Constraints": Constraints,
"Constraints1": Constraints1,
diff --git a/api-client/typescript/models/all.ts b/api-client/typescript/models/all.ts
index 8a13110..dfcca62 100644
--- a/api-client/typescript/models/all.ts
+++ b/api-client/typescript/models/all.ts
@@ -8,6 +8,10 @@ export * from '../models/BiodiversitySensitiveAreasProcessOutputs'
export * from '../models/BiodiversitySensitiveAreasProcessParams'
export * from '../models/BooleanField'
export * from '../models/BoundingBox'
+export * from '../models/ClimateRiskInputs'
+export * from '../models/ClimateRiskOutputs'
+export * from '../models/ClimateRiskProcessParams'
+export * from '../models/ClimateVariable'
export * from '../models/Conformance'
export * from '../models/Constraints'
export * from '../models/Constraints1'
@@ -28,6 +32,8 @@ export * from '../models/Constraints6'
export * from '../models/Constraints7'
export * from '../models/Constraints8'
export * from '../models/Constraints9'
+export * from '../models/CordexModel'
+export * from '../models/CordexRegion'
export * from '../models/CreditsForJob'
export * from '../models/DataResource'
export * from '../models/DateField'
diff --git a/api-client/typescript/types/ObjectParamAPI.ts b/api-client/typescript/types/ObjectParamAPI.ts
index 28853f5..6c48802 100644
--- a/api-client/typescript/types/ObjectParamAPI.ts
+++ b/api-client/typescript/types/ObjectParamAPI.ts
@@ -12,6 +12,10 @@ import { BiodiversitySensitiveAreasProcessOutputs } from '../models/Biodiversity
import { BiodiversitySensitiveAreasProcessParams } from '../models/BiodiversitySensitiveAreasProcessParams';
import { BooleanField } from '../models/BooleanField';
import { BoundingBox } from '../models/BoundingBox';
+import { ClimateRiskInputs } from '../models/ClimateRiskInputs';
+import { ClimateRiskOutputs } from '../models/ClimateRiskOutputs';
+import { ClimateRiskProcessParams } from '../models/ClimateRiskProcessParams';
+import { ClimateVariable } from '../models/ClimateVariable';
import { Conformance } from '../models/Conformance';
import { Constraints } from '../models/Constraints';
import { Constraints1 } from '../models/Constraints1';
@@ -32,6 +36,8 @@ import { Constraints6 } from '../models/Constraints6';
import { Constraints7 } from '../models/Constraints7';
import { Constraints8 } from '../models/Constraints8';
import { Constraints9 } from '../models/Constraints9';
+import { CordexModel } from '../models/CordexModel';
+import { CordexRegion } from '../models/CordexRegion';
import { CreditsForJob } from '../models/CreditsForJob';
import { DataResource } from '../models/DataResource';
import { DateField } from '../models/DateField';
@@ -244,6 +250,15 @@ export interface ProcessesApiExecuteBiodiversitySensitiveAreasRequest {
biodiversitySensitiveAreasProcessParams: BiodiversitySensitiveAreasProcessParams
}
+export interface ProcessesApiExecuteClimateRiskRequest {
+ /**
+ *
+ * @type ClimateRiskProcessParams
+ * @memberof ProcessesApiexecuteClimateRisk
+ */
+ climateRiskProcessParams: ClimateRiskProcessParams
+}
+
export interface ProcessesApiExecuteHabitatDistanceRequest {
/**
*
@@ -378,6 +393,20 @@ export class ObjectProcessesApi {
return this.api.executeBiodiversitySensitiveAreas(param.biodiversitySensitiveAreasProcessParams, options).toPromise();
}
+ /**
+ * @param param the request object
+ */
+ public executeClimateRiskWithHttpInfo(param: ProcessesApiExecuteClimateRiskRequest, options?: ConfigurationOptions): Promise> {
+ return this.api.executeClimateRiskWithHttpInfo(param.climateRiskProcessParams, options).toPromise();
+ }
+
+ /**
+ * @param param the request object
+ */
+ public executeClimateRisk(param: ProcessesApiExecuteClimateRiskRequest, options?: ConfigurationOptions): Promise {
+ return this.api.executeClimateRisk(param.climateRiskProcessParams, options).toPromise();
+ }
+
/**
* @param param the request object
*/
diff --git a/api-client/typescript/types/ObservableAPI.ts b/api-client/typescript/types/ObservableAPI.ts
index 2bec73f..166ec44 100644
--- a/api-client/typescript/types/ObservableAPI.ts
+++ b/api-client/typescript/types/ObservableAPI.ts
@@ -13,6 +13,10 @@ import { BiodiversitySensitiveAreasProcessOutputs } from '../models/Biodiversity
import { BiodiversitySensitiveAreasProcessParams } from '../models/BiodiversitySensitiveAreasProcessParams';
import { BooleanField } from '../models/BooleanField';
import { BoundingBox } from '../models/BoundingBox';
+import { ClimateRiskInputs } from '../models/ClimateRiskInputs';
+import { ClimateRiskOutputs } from '../models/ClimateRiskOutputs';
+import { ClimateRiskProcessParams } from '../models/ClimateRiskProcessParams';
+import { ClimateVariable } from '../models/ClimateVariable';
import { Conformance } from '../models/Conformance';
import { Constraints } from '../models/Constraints';
import { Constraints1 } from '../models/Constraints1';
@@ -33,6 +37,8 @@ import { Constraints6 } from '../models/Constraints6';
import { Constraints7 } from '../models/Constraints7';
import { Constraints8 } from '../models/Constraints8';
import { Constraints9 } from '../models/Constraints9';
+import { CordexModel } from '../models/CordexModel';
+import { CordexRegion } from '../models/CordexRegion';
import { CreditsForJob } from '../models/CreditsForJob';
import { DataResource } from '../models/DataResource';
import { DateField } from '../models/DateField';
@@ -359,6 +365,36 @@ export class ObservableProcessesApi {
return this.executeBiodiversitySensitiveAreasWithHttpInfo(biodiversitySensitiveAreasProcessParams, _options).pipe(map((apiResponse: HttpInfo) => apiResponse.data));
}
+ /**
+ * @param climateRiskProcessParams
+ */
+ public executeClimateRiskWithHttpInfo(climateRiskProcessParams: ClimateRiskProcessParams, _options?: ConfigurationOptions): Observable> {
+ const _config = mergeConfiguration(this.configuration, _options);
+
+ const requestContextPromise = this.requestFactory.executeClimateRisk(climateRiskProcessParams, _config);
+ // build promise chain
+ let middlewarePreObservable = from(requestContextPromise);
+ for (const middleware of _config.middleware) {
+ middlewarePreObservable = middlewarePreObservable.pipe(mergeMap((ctx: RequestContext) => middleware.pre(ctx)));
+ }
+
+ return middlewarePreObservable.pipe(mergeMap((ctx: RequestContext) => _config.httpApi.send(ctx))).
+ pipe(mergeMap((response: ResponseContext) => {
+ let middlewarePostObservable = of(response);
+ for (const middleware of _config.middleware.reverse()) {
+ middlewarePostObservable = middlewarePostObservable.pipe(mergeMap((rsp: ResponseContext) => middleware.post(rsp)));
+ }
+ return middlewarePostObservable.pipe(map((rsp: ResponseContext) => this.responseProcessor.executeClimateRiskWithHttpInfo(rsp)));
+ }));
+ }
+
+ /**
+ * @param climateRiskProcessParams
+ */
+ public executeClimateRisk(climateRiskProcessParams: ClimateRiskProcessParams, _options?: ConfigurationOptions): Observable {
+ return this.executeClimateRiskWithHttpInfo(climateRiskProcessParams, _options).pipe(map((apiResponse: HttpInfo) => apiResponse.data));
+ }
+
/**
* @param habitatDistanceProcessParams
*/
diff --git a/api-client/typescript/types/PromiseAPI.ts b/api-client/typescript/types/PromiseAPI.ts
index 62e5e50..4a8790c 100644
--- a/api-client/typescript/types/PromiseAPI.ts
+++ b/api-client/typescript/types/PromiseAPI.ts
@@ -12,6 +12,10 @@ import { BiodiversitySensitiveAreasProcessOutputs } from '../models/Biodiversity
import { BiodiversitySensitiveAreasProcessParams } from '../models/BiodiversitySensitiveAreasProcessParams';
import { BooleanField } from '../models/BooleanField';
import { BoundingBox } from '../models/BoundingBox';
+import { ClimateRiskInputs } from '../models/ClimateRiskInputs';
+import { ClimateRiskOutputs } from '../models/ClimateRiskOutputs';
+import { ClimateRiskProcessParams } from '../models/ClimateRiskProcessParams';
+import { ClimateVariable } from '../models/ClimateVariable';
import { Conformance } from '../models/Conformance';
import { Constraints } from '../models/Constraints';
import { Constraints1 } from '../models/Constraints1';
@@ -32,6 +36,8 @@ import { Constraints6 } from '../models/Constraints6';
import { Constraints7 } from '../models/Constraints7';
import { Constraints8 } from '../models/Constraints8';
import { Constraints9 } from '../models/Constraints9';
+import { CordexModel } from '../models/CordexModel';
+import { CordexRegion } from '../models/CordexRegion';
import { CreditsForJob } from '../models/CreditsForJob';
import { DataResource } from '../models/DataResource';
import { DateField } from '../models/DateField';
@@ -285,6 +291,24 @@ export class PromiseProcessesApi {
return result.toPromise();
}
+ /**
+ * @param climateRiskProcessParams
+ */
+ public executeClimateRiskWithHttpInfo(climateRiskProcessParams: ClimateRiskProcessParams, _options?: PromiseConfigurationOptions): Promise> {
+ const observableOptions = wrapOptions(_options);
+ const result = this.api.executeClimateRiskWithHttpInfo(climateRiskProcessParams, observableOptions);
+ return result.toPromise();
+ }
+
+ /**
+ * @param climateRiskProcessParams
+ */
+ public executeClimateRisk(climateRiskProcessParams: ClimateRiskProcessParams, _options?: PromiseConfigurationOptions): Promise {
+ const observableOptions = wrapOptions(_options);
+ const result = this.api.executeClimateRisk(climateRiskProcessParams, observableOptions);
+ return result.toPromise();
+ }
+
/**
* @param habitatDistanceProcessParams
*/
diff --git a/backend/src/auth.rs b/backend/src/auth.rs
index 673ac2e..b64aaa6 100644
--- a/backend/src/auth.rs
+++ b/backend/src/auth.rs
@@ -139,6 +139,7 @@ impl GeoEngineAuthMiddleware {
const_concat!("/processes/", BiodiversitySensitiveAreasProcess::ID),
const_concat!("/processes/", HabitatDistanceProcess::ID),
const_concat!("/processes/", LandUseSealedAreaProcess::ID),
+ "/profiles/table-schema/climate-risk/1.0/schema.json",
],
prefix: vec!["/api", "/swagger", "/auth/"],
},
diff --git a/backend/src/handler.rs b/backend/src/handler.rs
index 340c4f8..3505129 100644
--- a/backend/src/handler.rs
+++ b/backend/src/handler.rs
@@ -8,6 +8,7 @@ use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
+ routing::get,
};
use geoengine_api_client::apis::session_api::{oidc_init, oidc_login};
use ogcapi::{
@@ -25,6 +26,13 @@ pub fn auth_router() -> OpenApiRouter {
.routes(routes!(auth_request_url_handler))
}
+pub fn profile_router() -> OpenApiRouter {
+ OpenApiRouter::new().route(
+ "/profiles/table-schema/climate-risk/1.0/schema.json",
+ get(crate::profile::climate_risk_table_schema_profile),
+ )
+}
+
#[utoipa::path(get, path = "/health", responses((status = NO_CONTENT)))]
pub async fn health_handler() -> StatusCode {
StatusCode::NO_CONTENT
diff --git a/backend/src/lib.rs b/backend/src/lib.rs
index f95ea9f..b338fbd 100644
--- a/backend/src/lib.rs
+++ b/backend/src/lib.rs
@@ -6,6 +6,7 @@ pub mod db;
mod handler;
mod jobs;
mod processes;
+mod profile;
mod server;
mod state;
mod util;
diff --git a/backend/src/processes/biodiversity_sensitive_areas/mod.rs b/backend/src/processes/biodiversity_sensitive_areas/mod.rs
index 5afc09d..f9138d8 100644
--- a/backend/src/processes/biodiversity_sensitive_areas/mod.rs
+++ b/backend/src/processes/biodiversity_sensitive_areas/mod.rs
@@ -6,8 +6,8 @@ use crate::{
habitat_distance::natura2000_exists,
parameters::{
Area, DataResource, DataResourceSchema, DocumentationSource,
- FeatureCollectionGeoJsonInput, Fields, Kilometers, RelativeJsonPointer, SquareMeter,
- TableSchemaField, TableSchemaItemType, TableSchemaType, UnitForArea,
+ FeatureCollectionGeoJsonInput, Kilometers, RelativeJsonPointer, SquareMeter,
+ TableSchema, TableSchemaField, TableSchemaItemType, TableSchemaType, UnitForArea,
},
util::json_input_value,
},
@@ -777,7 +777,7 @@ fn site_row_into_output(
nearby_biodiversity_sensitive_areas: row.nearby_biodiversity_sensitive_areas,
})
.collect(),
- schema: Fields {
+ schema: TableSchema {
fields: vec![
TableSchemaField {
name: "location".into(),
@@ -829,6 +829,7 @@ fn site_row_into_output(
},
],
primary_key: vec!["location".to_string()].into(),
+ ..Default::default()
},
}
}
diff --git a/backend/src/processes/climate_risk/compute.rs b/backend/src/processes/climate_risk/compute.rs
new file mode 100644
index 0000000..0307be4
--- /dev/null
+++ b/backend/src/processes/climate_risk/compute.rs
@@ -0,0 +1,1518 @@
+use crate::db::model::ComputationId;
+use crate::profile::CLIMATE_RISK_TABLE_SCHEMA_PROFILE;
+use crate::{
+ processes::parameters::{
+ BioISTableSchemaExtension, BioisDisplayKind, BioisDisplayMetadata, BoundingBox,
+ DataResource, Days, TableSchema, TableSchemaField, TableSchemaType, Year, YearRange,
+ },
+ util::{error_response, to_api_vector_process},
+};
+use anyhow::Result;
+use futures::{TryStreamExt, stream::StreamExt};
+use geoengine_api_client::{
+ apis::{
+ configuration::Configuration, ogcwfs_api::WfsHandlerError, ogcwfs_api::wfs_handler,
+ workflows_api::register_workflow_handler,
+ },
+ models::{
+ ColumnNames, Coordinate2D, FeatureAggregationMethod, GeoJson, MockPointSource,
+ MockPointSourceParameters, Names, RasterVectorJoin, RasterVectorJoinParameters,
+ SingleVectorMultipleRasterSources, SpatialBoundsDerive, SpatialBoundsDeriveNone,
+ TemporalAggregationMethod, VectorOperator, WfsRequest, WfsService,
+ },
+};
+use geojson::PointType;
+use ogcapi::types::processes::{
+ ExecuteResult, ExecuteResults, Format, InlineOrRefData, InputValue, Output, QualifiedInputValue,
+};
+use std::collections::HashMap;
+use std::str::FromStr;
+use tracing::instrument;
+
+use super::ClimateRiskProcess;
+use super::types::*;
+pub(crate) fn climate_risk_data_resource(
+ rows: Vec,
+ analysis_period: &str,
+ reference_period: Option<&str>,
+) -> DataResource> {
+ let mut fields = vec![TableSchemaField {
+ name: "scenario".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Scenario".into()),
+ ..Default::default()
+ }];
+ fields.extend(risk_fields(&rows, reference_period));
+ let name = if analysis_period.is_empty() {
+ "Climate Risk".to_string()
+ } else {
+ format!("Climate Risk · {analysis_period}")
+ };
+ let biois = climate_display_extension(&rows);
+ DataResource {
+ name,
+ data: rows,
+ schema: TableSchema {
+ fields,
+ primary_key: Some(vec!["variable".to_string(), "scenario".to_string()]),
+ schema: Some(CLIMATE_RISK_TABLE_SCHEMA_PROFILE.to_string()),
+ biois: Some(biois),
+ },
+ }
+}
+
+pub(crate) fn climate_risk_scenario_data_resource(
+ scenario_name: &str,
+ rows: Vec,
+ analysis_period: &str,
+ reference_period: Option<&str>,
+) -> DataResource> {
+ let fields = risk_fields(&rows, reference_period);
+ let name = if analysis_period.is_empty() {
+ scenario_name.to_string()
+ } else {
+ format!("{scenario_name} · {analysis_period}")
+ };
+ let biois = climate_display_extension(&rows);
+ DataResource {
+ name,
+ data: rows,
+ schema: TableSchema {
+ fields,
+ primary_key: Some(vec!["variable".to_string()]),
+ schema: Some(CLIMATE_RISK_TABLE_SCHEMA_PROFILE.to_string()),
+ biois: Some(biois),
+ },
+ }
+}
+
+/// Shared column layout for climate-risk tables, excluding any scenario column.
+fn risk_fields(rows: &[ClimateRiskRow], reference_period: Option<&str>) -> Vec {
+ let mut fields = vec![
+ TableSchemaField {
+ name: "variable".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Variable".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "mean".into(),
+ r#type: Some(TableSchemaType::Number),
+ title: Some("Mean (days/year)".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "min".into(),
+ r#type: Some(TableSchemaType::Number),
+ title: Some("Min (days/year)".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "max".into(),
+ r#type: Some(TableSchemaType::Number),
+ title: Some("Max (days/year)".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "occurrenceProbability".into(),
+ r#type: Some(TableSchemaType::Number),
+ title: Some("Occurrence Probability".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "occurrenceProbabilityLabel".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Occurrence Probability Label".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "occurrenceProbabilityColor".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Occurrence Probability Color".into()),
+ ..Default::default()
+ },
+ ];
+ if rows.iter().any(|row| row.anomaly.is_some()) {
+ fields.extend([
+ TableSchemaField {
+ name: "anomaly".into(),
+ r#type: Some(TableSchemaType::Number),
+ title: Some(anomaly_title(reference_period)),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "anomalyLabel".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Anomaly Label".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "anomalyColor".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Anomaly Color".into()),
+ ..Default::default()
+ },
+ ]);
+ }
+ fields
+}
+
+pub(crate) fn raw_ensemble_data_resource(
+ mut rows: Vec,
+) -> DataResource> {
+ rows.sort_by(|a, b| {
+ (&a.variable, &a.scenario, &a.model).cmp(&(&b.variable, &b.scenario, &b.model))
+ });
+ DataResource {
+ name: "Raw Ensemble Data".to_string(),
+ data: rows,
+ schema: TableSchema {
+ fields: vec![
+ TableSchemaField {
+ name: "variable".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Variable".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "scenario".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Scenario".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "model".into(),
+ r#type: Some(TableSchemaType::String),
+ title: Some("Model".into()),
+ ..Default::default()
+ },
+ TableSchemaField {
+ name: "value".into(),
+ r#type: Some(TableSchemaType::Number),
+ title: Some("Value".into()),
+ ..Default::default()
+ },
+ ],
+ primary_key: Some(vec![
+ "variable".to_string(),
+ "scenario".to_string(),
+ "model".to_string(),
+ ]),
+ ..Default::default()
+ },
+ }
+}
+
+fn climate_display_extension(rows: &[ClimateRiskRow]) -> BioISTableSchemaExtension {
+ let has_anomaly = rows.iter().any(|row| row.anomaly.is_some());
+ let mut display = HashMap::from([(
+ "occurrenceProbability".to_string(),
+ BioisDisplayMetadata {
+ kind: BioisDisplayKind::RiskProbability,
+ label_field: Some("occurrenceProbabilityLabel".to_string()),
+ color_field: Some("occurrenceProbabilityColor".to_string()),
+ },
+ )]);
+ if has_anomaly {
+ display.insert(
+ "anomaly".to_string(),
+ BioisDisplayMetadata {
+ kind: BioisDisplayKind::RiskAnomaly,
+ label_field: Some("anomalyLabel".to_string()),
+ color_field: Some("anomalyColor".to_string()),
+ },
+ );
+ }
+ BioISTableSchemaExtension {
+ display,
+ hidden_fields: [
+ "occurrenceProbabilityLabel",
+ "occurrenceProbabilityColor",
+ "anomalyLabel",
+ "anomalyColor",
+ ]
+ .into_iter()
+ // The probability label/color columns are always hidden; the anomaly ones only
+ // when an anomaly column is actually present.
+ .filter(|field| {
+ matches!(
+ *field,
+ "occurrenceProbabilityLabel" | "occurrenceProbabilityColor"
+ ) || (has_anomaly && matches!(*field, "anomalyLabel" | "anomalyColor"))
+ })
+ .map(str::to_string)
+ .collect(),
+ }
+}
+
+impl From for ExecuteResults {
+ fn from(outputs: ClimateRiskOutputs) -> Self {
+ let mut result = ExecuteResults::default();
+
+ if let Some(inputs) = outputs.inputs
+ && let Some(value) = build_inputs_value(&inputs)
+ {
+ result.insert("inputs".to_string(), value);
+ }
+
+ if let Some(climate_risk) = outputs.climate_risk {
+ let analysis_period = outputs.analysis_period.as_deref().unwrap_or("");
+ let reference_period = outputs.reference_period.as_deref();
+ let mut rows_by_scenario: std::collections::BTreeMap> =
+ std::collections::BTreeMap::new();
+ for row in climate_risk.data {
+ rows_by_scenario
+ .entry(row.scenario.clone())
+ .or_default()
+ .push(row);
+ }
+ for (scenario, rows) in rows_by_scenario {
+ match climate_risk_scenario_data_resource(
+ &scenario,
+ rows,
+ analysis_period,
+ reference_period,
+ )
+ .to_input_value()
+ {
+ Ok(value) => {
+ result.insert(
+ scenario_output_id(&scenario),
+ ExecuteResult {
+ output: Output {
+ format: Some(json_format()),
+ transmission_mode: Default::default(),
+ },
+ data: InlineOrRefData::QualifiedInputValue(QualifiedInputValue {
+ value,
+ format: Format {
+ media_type: Some(
+ "application/vnd.dataresource+json".to_string(),
+ ),
+ encoding: None,
+ schema: None,
+ },
+ }),
+ },
+ );
+ }
+ Err(error) => tracing::warn!(
+ "Failed to serialize the climate-risk output for scenario `{scenario}`: {error}"
+ ),
+ }
+ }
+ }
+
+ if let Some(raw_ensemble_data) = outputs.raw_ensemble_data {
+ match raw_ensemble_data.to_input_value() {
+ Ok(value) => {
+ result.insert(
+ "rawEnsembleData".to_string(),
+ ExecuteResult {
+ output: Output {
+ format: Some(json_format()),
+ transmission_mode: Default::default(),
+ },
+ data: InlineOrRefData::QualifiedInputValue(QualifiedInputValue {
+ value,
+ format: Format {
+ media_type: Some(
+ "application/vnd.dataresource+json".to_string(),
+ ),
+ encoding: None,
+ schema: None,
+ },
+ }),
+ },
+ );
+ }
+ Err(error) => {
+ tracing::warn!("Failed to serialize the raw ensemble data output: {error}");
+ }
+ }
+ }
+
+ result
+ }
+}
+
+/// Maps a scenario's display name (e.g. `"RCP 4.5 (Intermediate emissions)"`) to the output id
+/// declared in the process description (`"rcp45"`), so that the keys of the execute response
+/// match the declared output ids. Falls back to the given name when it is not a known
+/// display name, which keeps rows carrying output ids directly working as well.
+fn scenario_output_id(scenario: &str) -> String {
+ ClimateScenario::ALL
+ .iter()
+ .find(|s| s.properties().name == scenario)
+ .map_or_else(|| scenario.to_string(), |s| s.name().to_string())
+}
+
+fn json_format() -> Format {
+ Format {
+ media_type: Some("application/json".to_string()),
+ encoding: Some("utf-8".to_string()),
+ schema: None,
+ }
+}
+
+/// Converts a serialized object into the OGC API's qualified JSON input value.
+fn build_qualified_value(object_map: serde_json::Map) -> ExecuteResult {
+ ExecuteResult {
+ output: Output {
+ format: None,
+ transmission_mode: Default::default(),
+ },
+ data: InlineOrRefData::QualifiedInputValue(QualifiedInputValue {
+ value: InputValue::Object(object_map),
+ format: Format {
+ media_type: Some("application/json".to_string()),
+ encoding: Some("utf-8".to_string()),
+ schema: None,
+ },
+ }),
+ }
+}
+
+/// Serializes typed inputs at the OGC API boundary, warning and dropping on failure.
+fn build_inputs_value(inputs: &ClimateRiskInputs) -> Option {
+ let Ok(value) = serde_json::to_value(inputs) else {
+ tracing::warn!("Failed to serialize the inputs output");
+ return None;
+ };
+ match value {
+ serde_json::Value::Object(object_map) => Some(build_qualified_value(object_map)),
+ other => {
+ tracing::warn!("Unexpected non-object inputs serialization: {other}");
+ None
+ }
+ }
+}
+/// One geoengine workflow together with the metadata needed to interpret its results.
+struct WorkflowRequest {
+ models: Vec,
+ variable: ClimateVariable,
+ scenario: ClimateScenarioProperties,
+ workflow: geoengine_api_client::models::Workflow,
+}
+
+/// Builds one workflow per (variable, scenario) pair, using only models that support the scenario.
+fn build_workflows(
+ coordinate: &PointType,
+ requests: &[(ClimateVariableRequest, ClimateScenarioProperties)],
+ models: &[CordexModelProperties],
+ region: &CordexRegionProperties,
+) -> Vec {
+ requests
+ .iter()
+ .filter_map(|(var_req, scenario_props)| {
+ let compatible_models: Vec = models
+ .iter()
+ .filter(|model| model.scenarios.contains(&scenario_props.scenario))
+ .cloned()
+ .collect();
+ if compatible_models.is_empty() {
+ return None;
+ }
+
+ let variable_properties = var_req.variable.properties();
+ let raster_sources = compatible_models
+ .iter()
+ .map(|model| {
+ ClimateRiskProcess::build_variable_year_agg_workflow(
+ &variable_properties,
+ model,
+ scenario_props,
+ region,
+ )
+ })
+ .collect::>();
+ let model_var_names: Vec = compatible_models
+ .iter()
+ .map(|model| model.model.name().to_string())
+ .collect();
+
+ let workflow = to_api_vector_process(&VectorOperator::RasterVectorJoin(
+ RasterVectorJoin {
+ r#type: Default::default(),
+ params: RasterVectorJoinParameters {
+ names: ColumnNames::Names(
+ Names {
+ r#type: Default::default(),
+ values: model_var_names,
+ }
+ .into(),
+ )
+ .into(),
+ feature_aggregation: FeatureAggregationMethod::First,
+ feature_aggregation_ignore_no_data: Some(false),
+ temporal_aggregation: TemporalAggregationMethod::None,
+ temporal_aggregation_ignore_no_data: Some(false),
+ }
+ .into(),
+ sources: SingleVectorMultipleRasterSources {
+ vector: vector_source(coordinate).into(),
+ rasters: raster_sources,
+ }
+ .into(),
+ }
+ .into(),
+ ));
+ Some(WorkflowRequest {
+ models: compatible_models,
+ variable: var_req.variable,
+ scenario: scenario_props.clone(),
+ workflow,
+ })
+ })
+ .collect()
+}
+
+// bounds geoengine fan-out; the request count is finite but a single user
+// request can register ~18 workflows and run ~36 WFS queries.
+const MAX_CONCURRENT_GEOENGINE_REQUESTS: usize = 8;
+
+/// Runs async jobs with bounded concurrency, yielding results in input order.
+async fn run_limited(jobs: Vec) -> Result, E>
+where
+ F: FnOnce() -> Fut,
+ Fut: std::future::Future
@if (!input.optional || isFieldSet()[input.key]) {
@switch (input.type) {
- @case (FieldType.Boolean)
+ @case (FieldType.Boolean) {
+
+ }
@case (FieldType.Integer)
@case (FieldType.Number)
@case (FieldType.String) {
@@ -66,6 +78,16 @@ import { InfoIconComponent } from '../util/info-icon.component';
}
+ @case (FieldType.StringArray) {
+
+ {{ input.title }}
+
+ @for (option of stringArrayOptions(input.schema); track option) {
+ {{ option }}
+ }
+
+
+ }
@case (FieldType.IntegerWithSmallRange) {
{{ input.title }}
@@ -166,6 +188,7 @@ import { InfoIconComponent } from '../util/info-icon.component';
MatSlideToggleModule,
MatTooltipModule,
SimpleFormFieldComponent,
+ BooleanFieldComponent,
forwardRef(() => InputsFormComponent),
],
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -180,6 +203,7 @@ export class InputsFormComponent {
readonly FieldType = FieldType;
readonly enumOptions = enumOptions;
readonly integerRangeList = integerRangeList;
+ readonly stringArrayOptions = stringArrayOptions;
readonly isFieldSet = computed>(() => {
const form = this.form();
@@ -208,6 +232,10 @@ export class InputsFormComponent {
return formInput as FieldTree;
}
+ asStringArrayInput(formInput: MaybeFieldTree): FieldTree {
+ return formInput as FieldTree;
+ }
+
asGeoJsonInput(
formInput: MaybeFieldTree,
): FieldTree {
@@ -234,17 +262,12 @@ export class InputsFormComponent {
}
}
+/** Returns the options for a single string-enum input. */
export function enumOptions(schema: JSONSchema | undefined): string[] {
- if (!schema || typeof schema === 'boolean' || !schema.enum || !Array.isArray(schema.enum))
- return [];
-
- const options = [];
- for (const value of schema.enum) {
- if (typeof value === 'string') options.push(value);
- }
- return options;
+ return resolveSingleEnumSchema(schema) ?? [];
}
+/** Expands a small bounded integer schema into select options. */
export function integerRangeList(schema: JSONSchema | undefined): number[] {
if (
!schema ||
@@ -261,3 +284,11 @@ export function integerRangeList(schema: JSONSchema | undefined): number[] {
}
return range;
}
+
+/** Returns all enum values for a string-array input. */
+export function stringArrayOptions(schema: JSONSchema | undefined): string[] {
+ if (!schema || typeof schema === 'boolean') return [];
+
+ const items = resolveArrayEnumSchema(schema);
+ return items ? enumOptions(items) : [];
+}
diff --git a/frontend/src/app/create/schema-info.spec.ts b/frontend/src/app/create/schema-info.spec.ts
index 55898ef..b0e5f04 100644
--- a/frontend/src/app/create/schema-info.spec.ts
+++ b/frontend/src/app/create/schema-info.spec.ts
@@ -1,5 +1,12 @@
import { InputDescription as ApiInputDescription } from '@geoengine/biois';
-import { retrieveInputDescription, FieldType, jsonSchemaToZod } from './schema-info';
+import {
+ retrieveInputDescription,
+ FieldType,
+ jsonSchemaToZod,
+ defaultInput,
+ defaultInputs,
+} from './schema-info';
+import { enumOptions } from './inputs-visualizer.component';
const testInputs: {
sites: ApiInputDescription;
@@ -7,7 +14,10 @@ const testInputs: {
unitForArea: ApiInputDescription;
previousYearData: ApiInputDescription;
year: ApiInputDescription;
+ yearRange: ApiInputDescription;
+ referenceYearBegin: ApiInputDescription;
siteTypeField: ApiInputDescription;
+ region: ApiInputDescription;
} = {
sites: {
title: 'Sites',
@@ -194,6 +204,44 @@ const testInputs: {
type: 'integer',
},
},
+ yearRange: {
+ title: 'Range (years)',
+ description: 'Length of the climate-risk aggregation window in years (5-30).',
+ schema: {
+ $defs: {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ 'GeoJSON FeatureCollection': {
+ $ref: 'https://geojson.org/schema/FeatureCollection.json',
+ },
+ GeoJsonInputMediaType: {
+ enum: ['application/geo+json'],
+ type: 'string',
+ },
+ },
+ default: 20,
+ description: 'Length of the climate-risk aggregation window in years (5-30).',
+ examples: [20],
+ maximum: 30,
+ minimum: 5,
+ title: 'YearRange',
+ type: 'integer',
+ },
+ },
+ referenceYearBegin: {
+ title: 'Reference period start',
+ description:
+ 'First year of the reference period used to compute anomalies. Uses the same range as the analysis window.',
+ schema: {
+ description: 'Year of reporting or change (e.g., 2023, 2024, etc.)',
+ examples: [2020],
+ format: 'uint16',
+ maximum: 2100,
+ minimum: 2000,
+ title: 'Year',
+ type: 'integer',
+ default: 2020,
+ },
+ },
siteTypeField: {
title: 'Site Type Field',
description:
@@ -224,6 +272,28 @@ const testInputs: {
type: 'string',
},
},
+ region: {
+ title: 'CORDEX/CMIP5 region',
+ description: 'The CORDEX/CMIP5 region to use for the climate-risk aggregation.',
+ schema: {
+ $defs: {
+ CordexRegion: {
+ title: 'CordexRegion',
+ type: 'string',
+ enum: ['Eur'],
+ },
+ },
+ anyOf: [
+ {
+ $ref: '#/$defs/CordexRegion',
+ },
+ {
+ type: 'null',
+ },
+ ],
+ title: 'Nullable_CordexRegion',
+ },
+ },
} as const;
describe('retrieveInputDescription', () => {
@@ -274,6 +344,45 @@ describe('retrieveInputDescription', () => {
});
});
+ it('should process IntegerWithSmallRange input (yearRange)', () => {
+ const result = retrieveInputDescription('yearRange', testInputs.yearRange);
+
+ expect(result).toMatchObject({
+ key: 'yearRange',
+ title: 'Range (years)',
+ type: FieldType.IntegerWithSmallRange,
+ optional: false,
+ });
+ });
+
+ it('should process non-nullable Integer input with a default (referenceYearBegin)', () => {
+ const result = retrieveInputDescription('referenceYearBegin', testInputs.referenceYearBegin);
+
+ expect(result).toMatchObject({
+ key: 'referenceYearBegin',
+ title: 'Reference period start',
+ type: FieldType.Integer,
+ optional: false,
+ });
+
+ expect(defaultInput(result)).toBe(2020);
+ });
+
+ it('should process nullable StringEnum input (region) with a usable default', () => {
+ const result = retrieveInputDescription('region', testInputs.region);
+
+ expect(result).toMatchObject({
+ key: 'region',
+ title: 'CORDEX/CMIP5 region',
+ type: FieldType.StringEnum,
+ optional: true,
+ });
+
+ expect(defaultInput(result)).toBeNull();
+ expect(defaultInput(result, { ignoreOptional: true })).toBe('Eur');
+ expect(enumOptions(result.schema)).toEqual(['Eur']);
+ });
+
it('should process nullable input (previousYearData)', () => {
const result = retrieveInputDescription('previousYearData', testInputs.previousYearData);
@@ -310,6 +419,20 @@ describe('retrieveInputDescription', () => {
});
});
+describe('defaultInputs', () => {
+ it('keeps optional inputs disabled by default', () => {
+ const input = retrieveInputDescription('region', testInputs.region);
+ const result = defaultInputs([input]);
+ expect(result['region']).toBeNull();
+ });
+
+ it('enables required inputs with their schema default', () => {
+ const input = retrieveInputDescription('referenceYearBegin', testInputs.referenceYearBegin);
+ const result = defaultInputs([input]);
+ expect(result['referenceYearBegin']).toBe(2020);
+ });
+});
+
describe('jsonSchemaToZod', () => {
it('should convert GeoJSON input schema (sites) to Zod schema', () => {
const zodSchema = jsonSchemaToZod(retrieveInputDescription('sites', testInputs.sites).schema);
diff --git a/frontend/src/app/create/schema-info.ts b/frontend/src/app/create/schema-info.ts
index adf149b..7c5d026 100644
--- a/frontend/src/app/create/schema-info.ts
+++ b/frontend/src/app/create/schema-info.ts
@@ -36,9 +36,13 @@ export enum FieldType {
RelativeJsonPointer = 'relativeJsonPointer',
String = 'string',
StringEnum = 'stringEnum',
+ StringArray = 'stringArray',
NestedJson = 'nestedJson',
}
+// UI-only cutoff: bounded integer inputs with at most 40 choices use a select.
+const SMALL_INTEGER_RANGE = 40;
+
export function retrieveInputDescription(
key: string,
processInput: ApiInputDescription,
@@ -109,7 +113,7 @@ function typeFromSchema(schema: JSONSchema | undefined): FieldType {
if (
typeof schema.maximum === 'number' &&
typeof schema.minimum === 'number' &&
- schema.maximum - schema.minimum <= 12
+ schema.maximum - schema.minimum <= SMALL_INTEGER_RANGE
) {
return FieldType.IntegerWithSmallRange;
}
@@ -122,6 +126,34 @@ function typeFromSchema(schema: JSONSchema | undefined): FieldType {
if (schema.title === 'FeatureCollectionGeoJsonInput') return FieldType.GeoJson;
}
+ if (resolveArrayEnumSchema(schema)) return FieldType.StringArray;
+
+ // Resolve nullable primitives like {"anyOf": [{"$ref": ...}, {"type": "null"}]} to their type
+ if (!type && (schema.anyOf || schema.oneOf)) {
+ const branches = (schema.anyOf ?? schema.oneOf) as JSONSchema[];
+ const nonNull = branches.find(
+ (branch) =>
+ typeof branch !== 'object' ||
+ branch === null ||
+ (branch as BaseJSONSchema)['type'] !== 'null',
+ );
+ if (nonNull) {
+ const resolved = resolveSchemaRef(schema, nonNull);
+ const resolvedType =
+ typeof resolved === 'object' && resolved !== null
+ ? (resolved as BaseJSONSchema)['type']
+ : undefined;
+ if (
+ resolvedType === 'string' ||
+ resolvedType === 'number' ||
+ resolvedType === 'integer' ||
+ resolvedType === 'boolean'
+ ) {
+ return typeFromSchema(resolved);
+ }
+ }
+ }
+
// nested types (for now)
if (!type) {
return FieldType.NestedJson;
@@ -130,6 +162,81 @@ function typeFromSchema(schema: JSONSchema | undefined): FieldType {
return FieldType.String; // fallback to string if type cannot be determined
}
+/**
+ * Resolve the items schema from an array schema, following `$ref` through `$defs`.
+ */
+function resolveItemsSchema(
+ schema: Record,
+ rootSchema: JSONSchema,
+): Record | undefined {
+ const items = schema['items'];
+ if (!items || typeof items !== 'object' || Array.isArray(items)) return undefined;
+
+ const itemsObj = items as Record;
+ if (!('$ref' in itemsObj)) return itemsObj;
+ const refRoot = '$defs' in schema ? (schema as JSONSchema) : rootSchema;
+ return resolveSchemaRef(refRoot, itemsObj) as Record;
+}
+
+/**
+ * Type guard for an array items schema describing a string enum.
+ */
+function isStringEnumArray(
+ items: Record | undefined,
+): items is { type: 'string'; enum: unknown[] } {
+ return !!items && items['type'] === 'string' && Array.isArray(items['enum']);
+}
+
+/** Resolves a string-enum array, including nullable and `$ref`-wrapped schemas. */
+export function resolveArrayEnumSchema(
+ schema: JSONSchema | undefined,
+): Record | undefined {
+ if (!schema || typeof schema === 'boolean') return undefined;
+
+ const schemaRecord = schema as Record;
+
+ const direct = resolveItemsSchema(schemaRecord, schema);
+ if (isStringEnumArray(direct)) return direct;
+
+ const branches = schemaRecord['anyOf'] ?? schemaRecord['oneOf'];
+ if (Array.isArray(branches)) {
+ for (const branch of branches) {
+ if (typeof branch !== 'object' || branch === null) continue;
+ const items = resolveItemsSchema(branch as Record, schema);
+ if (isStringEnumArray(items)) return items;
+ }
+ }
+
+ return undefined;
+}
+
+/** Resolves the string enum for one input, including nullable `$ref` branches. */
+export function resolveSingleEnumSchema(schema: JSONSchema | undefined): string[] | undefined {
+ if (!schema || typeof schema === 'boolean') return undefined;
+
+ const schemaRecord = schema as Record;
+
+ const direct = schemaRecord['enum'];
+ if (Array.isArray(direct))
+ return direct.filter((value): value is string => typeof value === 'string');
+
+ const branches = schemaRecord['anyOf'] ?? schemaRecord['oneOf'];
+ if (Array.isArray(branches)) {
+ for (const branch of branches) {
+ if (typeof branch !== 'object' || branch === null) continue;
+ const resolved = resolveSchemaRef(schema, branch as JSONSchema);
+ if (resolved && typeof resolved === 'object') {
+ const enumValue = (resolved as Record)['enum'];
+ if (Array.isArray(enumValue)) {
+ return enumValue.filter((value): value is string => typeof value === 'string');
+ }
+ }
+ }
+ }
+
+ return undefined;
+}
+
function isOptional(schema: JSONSchema | undefined): boolean {
function anySubSchemaIsNull(subSchemas: JSONSchema[] | undefined): boolean {
if (!subSchemas) return false;
@@ -288,10 +395,10 @@ export function jsonSchemaToZod(jsonSchema: JSONSchema): z.ZodTypeAny {
throw new Error('Failed to convert JSON Schema to Zod schema.', { cause: errors });
}
+/** Creates initial form values, keeping optional inputs disabled unless they are enabled in the UI. */
export function defaultInputs(inputDescriptions: Array): Record {
const inputs: Record = {};
for (const input of inputDescriptions) {
- // `Input` consists of `any` type
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
inputs[input.key] = defaultInput(input);
}
@@ -322,6 +429,8 @@ export function defaultInput(
case FieldType.RelativeJsonPointer:
case FieldType.StringEnum:
return defaultString(schema, '');
+ case FieldType.StringArray:
+ return stringArrayValues(schema);
case FieldType.NestedJson:
return {
value: defaultInputs(Object.values(children ?? {})),
@@ -353,15 +462,35 @@ function defaultString(schema: JSONSchema, fallback: string = ''): string {
const defaultValue = schema.default;
if (typeof defaultValue === 'string') return defaultValue;
- if (!schema.examples || !Array.isArray(schema.examples)) return fallback;
+ if (!schema.examples || !Array.isArray(schema.examples))
+ return firstEnumOrFallback(schema, fallback);
for (const example of schema.examples ?? []) {
if (typeof example === 'string') return example;
}
+ return firstEnumOrFallback(schema, fallback);
+}
+
+/**
+ * Resolves a default string value for an enum schema.
+ * Returns the first enum value if the schema defines one, otherwise `fallback`.
+ * Used as a last resort in `defaultString` when neither `default` nor `examples` are set.
+ */
+function firstEnumOrFallback(schema: JSONSchema, fallback: string): string {
+ const firstEnum = resolveSingleEnumSchema(schema)?.[0];
+ if (firstEnum !== undefined) return firstEnum;
return fallback;
}
+function stringArrayValues(schema: JSONSchema): string[] {
+ const items = resolveArrayEnumSchema(schema);
+ const enumValues = items?.['enum'];
+ if (!Array.isArray(enumValues)) return [];
+
+ return enumValues.filter((value: unknown): value is string => typeof value === 'string');
+}
+
function defaultCoordinate(schema: JSONSchema, fallback: [number, number] = [0, 0]): GeoJSONPoint {
if (!schema || typeof schema === 'boolean') return geoJsonPointFeature(fallback);
diff --git a/frontend/src/app/create/simple-form-field.ts b/frontend/src/app/create/simple-form-field.ts
index bbea5d8..4fbd5ab 100644
--- a/frontend/src/app/create/simple-form-field.ts
+++ b/frontend/src/app/create/simple-form-field.ts
@@ -9,7 +9,6 @@ import {
import { CommonModule } from '@angular/common';
import { MatFormFieldModule } from '@angular/material/form-field';
import { FormValueControl, ValidationError, WithOptionalFieldTree } from '@angular/forms/signals';
-import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatInput, MatInputModule } from '@angular/material/input';
import { FieldType } from './schema-info';
@@ -20,10 +19,6 @@ import { FieldType } from './schema-info';
{{ title() }}
@switch (type()) {
- @case (FieldType.String)
- @default {
-
- }
@case (FieldType.Integer) {
}
- @case (FieldType.Boolean) {
- True/False
+ @case (FieldType.String)
+ @default {
+
}
}
@@ -56,7 +50,7 @@ import { FieldType } from './schema-info';
`,
styles: ``,
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [CommonModule, MatFormFieldModule, MatInputModule, MatCheckboxModule],
+ imports: [CommonModule, MatFormFieldModule, MatInputModule],
})
export class SimpleFormFieldComponent implements FormValueControl {
readonly title = input.required();
diff --git a/frontend/src/app/result/data-resource-table.component.spec.ts b/frontend/src/app/result/data-resource-table.component.spec.ts
index 74d14bc..989c0c8 100644
--- a/frontend/src/app/result/data-resource-table.component.spec.ts
+++ b/frontend/src/app/result/data-resource-table.component.spec.ts
@@ -47,7 +47,11 @@ describe('DataResourceTableComponent', () => {
fields: [
{ name: 'title', type: 'string', title: 'Title' },
{ name: 'reference', type: 'string', title: 'Reference' },
- { name: 'score', type: 'number', title: 'Score' },
+ {
+ name: 'score',
+ type: 'number',
+ title: 'Score',
+ },
{ name: 'active', type: 'boolean', title: 'Active' },
{ name: 'tags', type: 'list' },
],
@@ -67,12 +71,83 @@ describe('DataResourceTableComponent', () => {
expect(columns).toEqual([
{ name: 'Title', key: 'title', type: ColumnType.String, isPrimaryKey: true },
{ name: 'Reference', key: 'reference', type: ColumnType.Url, isPrimaryKey: false },
- { name: 'Score', key: 'score', type: ColumnType.Number, isPrimaryKey: false },
+ {
+ name: 'Score',
+ key: 'score',
+ type: ColumnType.Number,
+ isPrimaryKey: false,
+ },
{ name: 'Active', key: 'active', type: ColumnType.Boolean, isPrimaryKey: false },
{ name: 'tags', key: 'tags', type: ColumnType.List, isPrimaryKey: false },
]);
});
+ it('maps display metadata labelField and colorField onto columns', () => {
+ const columns = tableColumnInfoFromValue(
+ {
+ fields: [
+ { name: 'occurrenceProbability', type: 'number', title: 'Occurrence Probability' },
+ { name: 'anomaly', type: 'number', title: 'Anomaly' },
+ { name: 'occurrenceProbabilityLabel', type: 'string' },
+ { name: 'occurrenceProbabilityColor', type: 'string' },
+ { name: 'anomalyLabel', type: 'string' },
+ { name: 'anomalyColor', type: 'string' },
+ ],
+ biois: {
+ hiddenFields: [
+ 'occurrenceProbabilityLabel',
+ 'occurrenceProbabilityColor',
+ 'anomalyLabel',
+ 'anomalyColor',
+ ],
+ display: {
+ occurrenceProbability: {
+ kind: 'riskProbability',
+ labelField: 'occurrenceProbabilityLabel',
+ colorField: 'occurrenceProbabilityColor',
+ },
+ anomaly: {
+ kind: 'riskAnomaly',
+ labelField: 'anomalyLabel',
+ colorField: 'anomalyColor',
+ },
+ },
+ },
+ },
+ [
+ {
+ occurrenceProbability: 0.03,
+ occurrenceProbabilityLabel: '7 · high (3 %)',
+ occurrenceProbabilityColor: '#e53935',
+ anomaly: 10,
+ anomalyLabel: '+10 days (+20 %)',
+ anomalyColor: '#fddbc7',
+ },
+ ],
+ );
+
+ expect(columns).toEqual([
+ {
+ name: 'Occurrence Probability',
+ key: 'occurrenceProbability',
+ type: ColumnType.Number,
+ isPrimaryKey: false,
+ displayKind: 'riskProbability',
+ labelField: 'occurrenceProbabilityLabel',
+ colorField: 'occurrenceProbabilityColor',
+ },
+ {
+ name: 'Anomaly',
+ key: 'anomaly',
+ type: ColumnType.Number,
+ isPrimaryKey: false,
+ displayKind: 'riskAnomaly',
+ labelField: 'anomalyLabel',
+ colorField: 'anomalyColor',
+ },
+ ]);
+ });
+
it('renders typed columns for row values', async () => {
const columns: Column[] = [
{ name: 'Title', key: 'title', type: ColumnType.String, isPrimaryKey: true },
@@ -121,4 +196,79 @@ describe('DataResourceTableComponent', () => {
expect(root.textContent).toContain('forest');
expect(root.textContent).toContain('protected');
});
+
+ it('renders extension display metadata as a colored chip', async () => {
+ const columns: Column[] = [
+ {
+ name: 'Occurrence Probability',
+ key: 'occurrenceProbability',
+ type: ColumnType.Number,
+ isPrimaryKey: false,
+ displayKind: 'riskProbability',
+ labelField: 'occurrenceProbabilityLabel',
+ colorField: 'occurrenceProbabilityColor',
+ },
+ {
+ name: 'Anomaly',
+ key: 'anomaly',
+ type: ColumnType.Number,
+ isPrimaryKey: false,
+ displayKind: 'riskAnomaly',
+ labelField: 'anomalyLabel',
+ colorField: 'anomalyColor',
+ },
+ ];
+
+ fixture.componentRef.setInput('columns', columns);
+ fixture.componentRef.setInput('rows', [
+ {
+ occurrenceProbability: 0.13,
+ occurrenceProbabilityLabel: '8 · very high (13 %)',
+ occurrenceProbabilityColor: '#c62828',
+ anomaly: 10,
+ anomalyLabel: '+10 days (+20 %)',
+ anomalyColor: '#fddbc7',
+ },
+ ]);
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ const root = fixture.nativeElement as HTMLElement;
+ const cells = Array.from(root.querySelectorAll('tbody td'));
+
+ expect(cells.length).toBe(2);
+ const [probability, anomaly] = cells;
+ expect(probability.querySelector('.color-dot')).not.toBeNull();
+ expect(probability.textContent).toContain('8 · very high (13 %)');
+ expect(probability.textContent).not.toContain('0.13');
+
+ const chip = anomaly.querySelector('mat-chip');
+ expect(chip).not.toBeNull();
+ expect(chip?.textContent).toContain('+10 days (+20 %)');
+ const dot = anomaly.querySelector('.color-dot');
+ expect(dot?.style.backgroundColor).toBe('rgb(253, 219, 199)');
+ });
+
+ it('falls back to the raw value when display metadata is incomplete', async () => {
+ fixture.componentRef.setInput('columns', [
+ {
+ name: 'Anomaly',
+ key: 'anomaly',
+ type: ColumnType.Number,
+ isPrimaryKey: false,
+ displayKind: 'riskAnomaly',
+ },
+ ]);
+ fixture.componentRef.setInput('rows', [{ anomaly: 10 }]);
+
+ fixture.detectChanges();
+ await fixture.whenStable();
+ fixture.detectChanges();
+
+ const root = fixture.nativeElement as HTMLElement;
+ const cell = root.querySelector('tbody td');
+ expect(cell?.textContent).toContain('10');
+ });
});
diff --git a/frontend/src/app/result/data-resource-table.component.ts b/frontend/src/app/result/data-resource-table.component.ts
index c97a5e3..828fb8e 100644
--- a/frontend/src/app/result/data-resource-table.component.ts
+++ b/frontend/src/app/result/data-resource-table.component.ts
@@ -25,9 +25,21 @@ import { RowOverflowDirective } from './row-overflow.directive';
}
@case (ColumnType.Number) {
-
- {{ element[column.key] | number: '1.0-2' }}
- |
+ @if (column.displayKind) {
+
+
+
+ {{ displayValue(column, element) }}
+
+ |
+ } @else {
+
+ {{ formatValue(column, element) }}
+ |
+ }
}
@case (ColumnType.Boolean) {
@@ -94,6 +106,12 @@ import { RowOverflowDirective } from './row-overflow.directive';
padding-bottom: 1rem;
vertical-align: top; /* Keeps text nicely aligned at the top during expansion */
+ mat-chip:has(.color-dot) {
+ min-width: 5rem;
+ width: max-content;
+ justify-content: center;
+ }
+
.cell-content {
max-height: calc(2 * 1.4em); /* Limits text to roughly 2 lines */
line-height: 1.4;
@@ -104,6 +122,15 @@ import { RowOverflowDirective } from './row-overflow.directive';
transition: max-height 0.25s ease-out;
}
+
+ .color-dot {
+ display: inline-block;
+ width: 0.75rem;
+ height: 0.75rem;
+ margin-right: 0.5rem;
+ border: 1px solid var(--mat-sys-outline);
+ border-radius: 50%;
+ }
}
tr {
@@ -164,6 +191,23 @@ export class DataResourceTableComponent {
toggleRow(element: Row): void {
this.expandedElement.set(this.isExpanded(element) ? null : element);
}
+
+ formatValue(column: Column, element: Row): string {
+ const value = element[column.key];
+ if (typeof value !== 'number') return String(value);
+ return new Intl.NumberFormat('en', { maximumFractionDigits: 2 }).format(value);
+ }
+
+ displayValue(column: Column, element: Row): string {
+ const label = column.labelField ? element[column.labelField] : undefined;
+ if (typeof label === 'string') return label;
+ return this.formatValue(column, element);
+ }
+
+ displayColor(column: Column, element: Row): string {
+ const color = column.colorField ? element[column.colorField] : undefined;
+ return typeof color === 'string' ? color : 'transparent';
+ }
}
export type Row = Record;
@@ -173,8 +217,13 @@ export interface Column {
key: string;
type: ColumnType;
isPrimaryKey: boolean;
+ displayKind?: DisplayKind;
+ labelField?: string;
+ colorField?: string;
}
+export type DisplayKind = 'riskProbability' | 'riskAnomaly';
+
export enum ColumnType {
String = 'string',
Number = 'number',
@@ -221,13 +270,18 @@ export function tableColumnInfoFromValue(
): Array {
if (!('fields' in schema)) return [];
- const fields = schema['fields'] as [
- {
- name: string;
- type?: 'string' | 'number' | 'integer' | 'boolean' | 'list';
- title?: string;
- },
- ];
+ const fields = schema['fields'] as Array<{
+ name: string;
+ type?: 'string' | 'number' | 'integer' | 'boolean' | 'list';
+ title?: string;
+ }>;
+
+ const display = schema['biois'] as
+ | {
+ display?: Record;
+ hiddenFields?: string[];
+ }
+ | undefined;
const primaryKey = new Array();
if ('primaryKey' in schema) {
@@ -241,14 +295,21 @@ export function tableColumnInfoFromValue(
}
}
- return fields.map((field) => {
- const sampleValue = data[0]?.[field.name];
- const columnType = columnTypeOfField(field.type, sampleValue);
- return {
- name: field.title ?? field.name,
- key: field.name,
- type: columnType,
- isPrimaryKey: primaryKey.includes(field.name),
- };
- });
+ const hiddenFields = new Set(display?.hiddenFields ?? []);
+ return fields
+ .filter((field) => !hiddenFields.has(field.name))
+ .map((field) => {
+ const sampleValue = data[0]?.[field.name];
+ const columnType = columnTypeOfField(field.type, sampleValue);
+ const metadata = display?.display?.[field.name];
+ return {
+ name: field.title ?? field.name,
+ key: field.name,
+ type: columnType,
+ isPrimaryKey: primaryKey.includes(field.name),
+ displayKind: metadata?.kind,
+ labelField: metadata?.labelField,
+ colorField: metadata?.colorField,
+ };
+ });
}
diff --git a/frontend/src/app/result/result.component.ts b/frontend/src/app/result/result.component.ts
index d92f94d..abc1867 100644
--- a/frontend/src/app/result/result.component.ts
+++ b/frontend/src/app/result/result.component.ts
@@ -94,10 +94,11 @@ export class ResultComponent {
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey)) // TODO: get order from process description when available
.map(([key, rawValue]) => {
const value = fixDataValue(rawValue) as unknown;
+ const innerValue = value instanceof QualifiedInputValue ? (value.value as unknown) : value;
return {
key,
- title: this.fieldName(key),
- value: value instanceof QualifiedInputValue ? (value.value as unknown) : value,
+ title: dataResourceTitle(innerValue) ?? this.fieldName(key),
+ value: innerValue,
type: this.typeOfValue(value),
};
});
@@ -221,6 +222,12 @@ export class ResultComponent {
}
}
+/** Returns the Data Resource name when a result value contains one. */
+function dataResourceTitle(value: unknown): string | undefined {
+ if (typeof value !== 'object' || value === null || !('name' in value)) return undefined;
+ return typeof value.name === 'string' ? value.name : undefined;
+}
+
enum ResultType {
Boolean = 'boolean',
Errors = 'errors',
diff --git a/k8s/pod.yaml b/k8s/pod.yaml
index 489bcdd..f9daa2c 100644
--- a/k8s/pod.yaml
+++ b/k8s/pod.yaml
@@ -80,7 +80,7 @@ spec:
periodSeconds: 10
volumeMounts:
- name: pgdata
- mountPath: /var/lib/postgresql/data
+ mountPath: /var/lib/postgresql
volumes:
- name: pgdata
persistentVolumeClaim:
diff --git a/openapi.json b/openapi.json
index f0402be..ae18b12 100644
--- a/openapi.json
+++ b/openapi.json
@@ -749,6 +749,36 @@
}
}
},
+ "/processes/climate-risk/execution": {
+ "post": {
+ "tags": [
+ "Processes"
+ ],
+ "operationId": "execute_climate_risk",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ClimateRiskProcessParams"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ClimateRiskOutputs"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/processes/land-use-sealed-area/execution": {
"post": {
"tags": [
@@ -970,6 +1000,137 @@
}
}
},
+ "ClimateRiskInputs": {
+ "type": "object",
+ "description": "User-supplied inputs for the climate risk process.",
+ "required": [
+ "coordinate",
+ "referenceYearBegin"
+ ],
+ "properties": {
+ "coordinate": {
+ "$ref": "#/components/schemas/PointGeoJsonInput"
+ },
+ "yearBegin": {
+ "$ref": "#/components/schemas/Year"
+ },
+ "yearRange": {
+ "$ref": "#/components/schemas/YearRange"
+ },
+ "referenceYearBegin": {
+ "$ref": "#/components/schemas/Year"
+ },
+ "variables": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/ClimateVariable"
+ }
+ },
+ "models": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/CordexModel"
+ }
+ },
+ "region": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/CordexRegion"
+ }
+ ]
+ }
+ }
+ },
+ "ClimateRiskOutputs": {
+ "type": "object",
+ "description": "Output of the climate risk process: summary table and raw ensemble data.",
+ "properties": {
+ "inputs": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "#/components/schemas/ClimateRiskInputs"
+ }
+ ]
+ },
+ "analysisPeriod": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Analysis window as `\"2041–2070\"`, used for display in result headlines."
+ },
+ "referencePeriod": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Reference window used for anomalies as `\"2006–2025\"`, `None` when no reference period."
+ },
+ "climateRisk": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "https://datapackage.org/profiles/2.0/dataresource.json"
+ }
+ ]
+ },
+ "rawEnsembleData": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "$ref": "https://datapackage.org/profiles/2.0/dataresource.json"
+ }
+ ]
+ }
+ }
+ },
+ "ClimateRiskProcessParams": {
+ "type": "object",
+ "description": "Process execution (Climate Risk)",
+ "required": [
+ "inputs"
+ ],
+ "properties": {
+ "inputs": {
+ "$ref": "#/components/schemas/ClimateRiskInputs"
+ },
+ "outputs": {
+ "type": "object",
+ "additionalProperties": {
+ "default": null
+ },
+ "propertyNames": {
+ "type": "string"
+ }
+ },
+ "response": {
+ "$ref": "#/components/schemas/Response"
+ }
+ }
+ },
+ "ClimateVariable": {
+ "type": "string",
+ "title": "ClimateVariable",
+ "description": "Climate variable to compute (WMO-based daily threshold indicators).",
+ "enum": [
+ "heatDays",
+ "iceDays",
+ "tropicalNights",
+ "frostDays",
+ "dryDays",
+ "heavyRainDays"
+ ]
+ },
"Conformance": {
"type": "object",
"description": "The Conformance declaration states the conformance classes from standards or community\nspecifications, identified by a URI, that the API conforms to. Clients can but are not\nrequired to use this information. Accessing the Conformance declaration using HTTP GET\nreturns the list of URIs of conformance classes implemented by the server.",
@@ -985,6 +1146,23 @@
}
}
},
+ "CordexModel": {
+ "type": "string",
+ "title": "ClimateModel",
+ "description": "CORDEX climate model.",
+ "enum": [
+ "MPI-M-MPI-ESM-LR",
+ "MOHC-HadGEM2-ES"
+ ]
+ },
+ "CordexRegion": {
+ "type": "string",
+ "title": "CordexRegion",
+ "description": "CORDEX climate region.",
+ "enum": [
+ "Eur"
+ ]
+ },
"CreditsForJob": {
"type": "object",
"required": [
@@ -2073,6 +2251,12 @@
"format": "int32",
"description": "Year of reporting or change (e.g., 2023, 2024, etc.)",
"minimum": 0
+ },
+ "YearRange": {
+ "type": "integer",
+ "format": "int32",
+ "description": "Length of a time window in years (e.g., 5 years).",
+ "minimum": 0
}
}
}
|