From f2235087e762620a425feec9ce3c812b396dadc0 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Tue, 3 Feb 2026 10:34:29 +0100 Subject: [PATCH 01/19] build: use correct vergen-gitcl version --- Cargo.lock | 66 +++++++++++++++++++++++++++++++++++------------------- Cargo.toml | 2 +- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a1856a3f5..d1a37aee36 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -804,9 +804,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" +checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" dependencies = [ "anstyle", "bstr", @@ -1198,18 +1198,19 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.1.9" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" dependencies = [ "serde", + "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.19.2" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" dependencies = [ "camino", "cargo-platform", @@ -2547,6 +2548,24 @@ dependencies = [ "serde", ] +[[package]] +name = "geoengine-api" +version = "0.8.0" +dependencies = [ + "anyhow", + "futures", + "geoengine-datatypes", + "geoengine-macros", + "geoengine-operators", + "pretty_assertions", + "serde", + "serde_json", + "serde_with", + "tracing-opentelemetry", + "tracing-subscriber", + "utoipa", +] + [[package]] name = "geoengine-datatypes" version = "0.8.0" @@ -2707,6 +2726,7 @@ dependencies = [ "futures-util", "gdal", "geo 0.32.0", + "geoengine-api", "geoengine-datatypes", "geoengine-expression", "geoengine-macros", @@ -4350,9 +4370,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-derive" @@ -6906,9 +6926,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.44" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -6916,22 +6936,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -7764,9 +7784,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" dependencies = [ "anyhow", "cargo_metadata", @@ -7779,9 +7799,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "1.0.8" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +checksum = "77ff3b5300a085d6bcd8fc96a507f706a28ae3814693236c9b409db71a1d15b9" dependencies = [ "anyhow", "derive_builder", @@ -7793,9 +7813,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.6" +version = "9.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" dependencies = [ "anyhow", "derive_builder", @@ -8313,9 +8333,9 @@ dependencies = [ [[package]] name = "xml" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2df5825faced2427b2da74d9100f1e2e93c533fff063506a81ede1cf517b2e7e" +checksum = "b8aa498d22c9bbaf482329839bc5620c46be275a19a812e9a22a2b07529a642a" [[package]] name = "y4m" diff --git a/Cargo.toml b/Cargo.toml index a5c43a108f..bdd913027f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,7 +216,7 @@ uuid = { version = "1.17", features = [ ] } # must be compatible with `bb8-postgres` validator = { version = "0.20", features = ["derive"] } vergen = "9.0" -vergen-gitcl = "1.0" +vergen-gitcl = "9.1" walkdir = "2.4" wkt = "0.14" xml = "1.2" From 47454b91b78304d39cbb4cc983ce8a880094a65b Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Tue, 3 Feb 2026 10:41:56 +0100 Subject: [PATCH 02/19] feat: GdalSource as openapi --- Cargo.toml | 2 +- api/Cargo.toml | 30 ++++++++++++ api/README.md | 2 + api/src/lib.rs | 1 + api/src/processes/mod.rs | 53 ++++++++++++++++++++ api/src/processes/source.rs | 96 +++++++++++++++++++++++++++++++++++++ openapi.json | 46 ++++++++++++++++++ services/Cargo.toml | 1 + services/src/api/apidoc.rs | 3 ++ 9 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 api/Cargo.toml create mode 100644 api/README.md create mode 100644 api/src/lib.rs create mode 100644 api/src/processes/mod.rs create mode 100644 api/src/processes/source.rs diff --git a/Cargo.toml b/Cargo.toml index bdd913027f..931c8e0b46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["datatypes", "expression", "macros", "operators", "services"] +members = ["api", "datatypes", "expression", "macros", "operators", "services"] exclude = [ "expression/deps-workspace", # Buggy, cf. https://github.com/rust-lang/cargo/issues/6745 ".scripts", diff --git a/api/Cargo.toml b/api/Cargo.toml new file mode 100644 index 0000000000..ea2a972bf5 --- /dev/null +++ b/api/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "geoengine-api" +version.workspace = true +authors.workspace = true +edition.workspace = true +publish.workspace = true +license-file.workspace = true +documentation.workspace = true +repository.workspace = true +# build = "build.rs" +# default-run = "geoengine-server" + +[dependencies] +anyhow = { workspace = true } +futures = { workspace = true } +geoengine-datatypes = { path = "../datatypes" } +geoengine-macros = { path = "../macros" } +geoengine-operators = { path = "../operators" } +serde = { workspace = true } +serde_json = { workspace = true } +serde_with = { workspace = true } +tracing-opentelemetry = { workspace = true } +tracing-subscriber = { workspace = true } +utoipa = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } + +[lints] +workspace = true diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000000..8b5a616abd --- /dev/null +++ b/api/README.md @@ -0,0 +1,2 @@ +# geo engine services + This crate contains the services for the geo engine. diff --git a/api/src/lib.rs b/api/src/lib.rs new file mode 100644 index 0000000000..6f5902e45f --- /dev/null +++ b/api/src/lib.rs @@ -0,0 +1 @@ +pub mod processes; diff --git a/api/src/processes/mod.rs b/api/src/processes/mod.rs new file mode 100644 index 0000000000..d7395f8339 --- /dev/null +++ b/api/src/processes/mod.rs @@ -0,0 +1,53 @@ +#![allow(clippy::needless_for_each)] // TODO: remove when clippy is fixed for utoipa + +use crate::processes::source::{GdalSource, GdalSourceParameters}; +use geoengine_operators::{ + engine::{RasterOperator as OperatorsRasterOperator, TypedOperator as OperatorsTypedOperator}, + source::GdalSource as OperatorsGdalSource, +}; +use serde::{Deserialize, Serialize}; +use utoipa::{OpenApi, ToSchema}; + +mod source; + +/// Operator outputs are distinguished by their data type. +/// There are `raster`, `vector` and `plot` operators. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(tag = "type", content = "operator")] +pub enum TypedOperator { + // Vector(Box), + Raster(RasterOperator), + // Plot(Box), +} + +/// An operator that produces raster data. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase", untagged)] +#[schema(discriminator = "type")] +pub enum RasterOperator { + GdalSource(GdalSource), +} + +impl TryFrom for Box { + type Error = anyhow::Error; + fn try_from(operator: RasterOperator) -> Result { + match operator { + RasterOperator::GdalSource(gdal_source) => { + OperatorsGdalSource::try_from(gdal_source).map(OperatorsRasterOperator::boxed) + } + } + } +} + +impl TryFrom for OperatorsTypedOperator { + type Error = anyhow::Error; + fn try_from(operator: TypedOperator) -> Result { + match operator { + TypedOperator::Raster(raster_operator) => Ok(Self::Raster(raster_operator.try_into()?)), + } + } +} + +#[derive(OpenApi)] +#[openapi(components(schemas(TypedOperator, RasterOperator, GdalSource, GdalSourceParameters)))] +pub struct OperatorsApi; diff --git a/api/src/processes/source.rs b/api/src/processes/source.rs new file mode 100644 index 0000000000..7298185f73 --- /dev/null +++ b/api/src/processes/source.rs @@ -0,0 +1,96 @@ +use geoengine_datatypes::dataset::NamedData; +use geoengine_macros::type_tag; +use geoengine_operators::source::{ + GdalSource as OperatorsGdalSource, GdalSourceParameters as OperatorsGdalSourceParameters, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// # GdalSource +/// +/// The [`GdalSource`] is a source operator that reads raster data using GDAL. +/// The counterpart for vector data is the [`OgrSource`]. +/// +/// ## Errors +/// +/// If the given dataset does not exist or is not readable, an error is thrown. +/// +/// ## Example JSON +/// +/// ```json +/// { +/// "type": "GdalSource", +/// "params": { +/// "data": "ndvi" +/// } +/// } +/// ``` +#[type_tag(value = "GdalSource")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GdalSource { + pub params: GdalSourceParameters, +} + +/// Parameters for the [`GdalSource`] operator. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GdalSourceParameters { + /// Dataset name or identifier to be loaded. + /// + /// ### Example + /// `"ndvi"` + pub data: String, +} + +impl TryFrom for OperatorsGdalSource { + type Error = anyhow::Error; + fn try_from(value: GdalSource) -> Result { + Ok(OperatorsGdalSource { + params: OperatorsGdalSourceParameters { + data: serde_json::from_str::(&serde_json::to_string( + &value.params.data, + )?)?, + }, + }) + } +} + +// TODO: OpenAPI and conversions for other operators: +// - MockPointSource +// - Expression +// - RasterVectorJoin + +#[cfg(test)] +mod tests { + use super::*; + use crate::processes::{RasterOperator, TypedOperator}; + use geoengine_operators::engine::TypedOperator as OperatorsTypedOperator; + + #[test] + fn it_converts_into_gdal_source() { + let api_operator = GdalSource { + r#type: Default::default(), + params: GdalSourceParameters { + data: "example_dataset".to_string(), + }, + }; + + let operators_operator: OperatorsGdalSource = + api_operator.try_into().expect("it should convert"); + + assert_eq!( + operators_operator.params.data, + NamedData::with_system_name("example_dataset") + ); + + let typed_operator = TypedOperator::Raster(RasterOperator::GdalSource(GdalSource { + r#type: Default::default(), + params: GdalSourceParameters { + data: "example_dataset".to_string(), + }, + })); + + OperatorsTypedOperator::try_from(typed_operator).expect("it should convert"); + } +} diff --git a/openapi.json b/openapi.json index 1267e3ca9e..3da0d76742 100644 --- a/openapi.json +++ b/openapi.json @@ -7066,6 +7066,38 @@ } } }, + "GdalSource": { + "type": "object", + "description": "# GdalSource\n\nThe [`GdalSource`] is a source operator that reads raster data using GDAL.\nThe counterpart for vector data is the [`OgrSource`].\n\n## Errors\n\nIf the given dataset does not exist or is not readable, an error is thrown.\n\n## Example JSON\n\n```json\n{\n \"type\": \"GdalSource\",\n \"params\": {\n \"data\": \"ndvi\"\n }\n}\n```", + "required": [ + "type", + "params" + ], + "properties": { + "params": { + "$ref": "#/components/schemas/GdalSourceParameters" + }, + "type": { + "type": "string", + "enum": [ + "GdalSource" + ] + } + } + }, + "GdalSourceParameters": { + "type": "object", + "description": "Parameters for the [`GdalSource`] operator.", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "string", + "description": "Dataset name or identifier to be loaded.\n\n### Example\n`\"ndvi\"`" + } + } + }, "GdalSourceTimePlaceholder": { "type": "object", "required": [ @@ -9253,6 +9285,20 @@ } } }, + "RasterOperator": { + "oneOf": [ + { + "$ref": "#/components/schemas/GdalSource" + } + ], + "description": "An operator that produces raster data.", + "discriminator": { + "propertyName": "type", + "mapping": { + "GdalSource": "#/components/schemas/GdalSource" + } + } + }, "RasterPropertiesEntryType": { "type": "string", "enum": [ diff --git a/services/Cargo.toml b/services/Cargo.toml index ad80747d47..26664be076 100644 --- a/services/Cargo.toml +++ b/services/Cargo.toml @@ -39,6 +39,7 @@ futures = { workspace = true } futures-util = { workspace = true } gdal = { workspace = true } geo = { workspace = true } +geoengine-api = { path = "../api" } # TODO: should be the other way around? geoengine-datatypes = { path = "../datatypes" } geoengine-expression = { path = "../expression" } geoengine-macros = { path = "../macros" } diff --git a/services/src/api/apidoc.rs b/services/src/api/apidoc.rs index a7cc0ae4a6..669efcfb95 100644 --- a/services/src/api/apidoc.rs +++ b/services/src/api/apidoc.rs @@ -466,6 +466,9 @@ use utoipa::{Modify, OpenApi}; MlTensorShape3D, ), ), + nest( + (path = "/process", api = geoengine_api::processes::OperatorsApi), + ), modifiers(&SecurityAddon, &ApiDocInfo, &OpenApiServerInfo, &DeriveDiscriminatorMapping), external_docs(url = "https://docs.geoengine.io", description = "Geo Engine Docs") )] From 97124aa811eae06a454a5394d520f5a86c9dda38 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Tue, 3 Feb 2026 10:54:25 +0100 Subject: [PATCH 03/19] feat: MockPointSource OpenAPI --- api/README.md | 6 ++-- api/src/lib.rs | 1 + api/src/parameters.rs | 16 ++++++++++ api/src/processes/mod.rs | 44 +++++++++++++++++++++++--- api/src/processes/processing.rs | 0 api/src/processes/source.rs | 55 +++++++++++++++++++++++++++++++-- openapi.json | 49 +++++++++++++++++++++++++++++ 7 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 api/src/parameters.rs create mode 100644 api/src/processes/processing.rs diff --git a/api/README.md b/api/README.md index 8b5a616abd..e2acd7b98b 100644 --- a/api/README.md +++ b/api/README.md @@ -1,2 +1,4 @@ -# geo engine services - This crate contains the services for the geo engine. +# Geo Engine API + +This directory contains the API definition of the Geo Engine. +It is implemented in the [`geoengine-api`] crate. diff --git a/api/src/lib.rs b/api/src/lib.rs index 6f5902e45f..6c15f34f9b 100644 --- a/api/src/lib.rs +++ b/api/src/lib.rs @@ -1 +1,2 @@ +pub mod parameters; pub mod processes; diff --git a/api/src/parameters.rs b/api/src/parameters.rs new file mode 100644 index 0000000000..014f415ba2 --- /dev/null +++ b/api/src/parameters.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, Default, ToSchema)] +pub struct Coordinate2D { + pub x: f64, + pub y: f64, +} +impl From for geoengine_datatypes::primitives::Coordinate2D { + fn from(value: Coordinate2D) -> Self { + geoengine_datatypes::primitives::Coordinate2D { + x: value.x, + y: value.y, + } + } +} diff --git a/api/src/processes/mod.rs b/api/src/processes/mod.rs index d7395f8339..6926eded37 100644 --- a/api/src/processes/mod.rs +++ b/api/src/processes/mod.rs @@ -1,13 +1,20 @@ #![allow(clippy::needless_for_each)] // TODO: remove when clippy is fixed for utoipa -use crate::processes::source::{GdalSource, GdalSourceParameters}; +use crate::processes::source::{ + GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters, +}; use geoengine_operators::{ - engine::{RasterOperator as OperatorsRasterOperator, TypedOperator as OperatorsTypedOperator}, + engine::{ + RasterOperator as OperatorsRasterOperator, TypedOperator as OperatorsTypedOperator, + VectorOperator as OperatorsVectorOperator, + }, + mock::MockPointSource as OperatorsMockPointSource, source::GdalSource as OperatorsGdalSource, }; use serde::{Deserialize, Serialize}; use utoipa::{OpenApi, ToSchema}; +mod processing; mod source; /// Operator outputs are distinguished by their data type. @@ -15,7 +22,7 @@ mod source; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] #[serde(tag = "type", content = "operator")] pub enum TypedOperator { - // Vector(Box), + Vector(VectorOperator), Raster(RasterOperator), // Plot(Box), } @@ -28,6 +35,14 @@ pub enum RasterOperator { GdalSource(GdalSource), } +/// An operator that produces vector data. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase", untagged)] +#[schema(discriminator = "type")] +pub enum VectorOperator { + MockPointSource(MockPointSource), +} + impl TryFrom for Box { type Error = anyhow::Error; fn try_from(operator: RasterOperator) -> Result { @@ -39,15 +54,36 @@ impl TryFrom for Box { } } +impl TryFrom for Box { + type Error = anyhow::Error; + fn try_from(operator: VectorOperator) -> Result { + match operator { + VectorOperator::MockPointSource(mock_point_source) => { + OperatorsMockPointSource::try_from(mock_point_source) + .map(OperatorsVectorOperator::boxed) + } + } + } +} + impl TryFrom for OperatorsTypedOperator { type Error = anyhow::Error; fn try_from(operator: TypedOperator) -> Result { match operator { TypedOperator::Raster(raster_operator) => Ok(Self::Raster(raster_operator.try_into()?)), + TypedOperator::Vector(vector_operator) => Ok(Self::Vector(vector_operator.try_into()?)), } } } #[derive(OpenApi)] -#[openapi(components(schemas(TypedOperator, RasterOperator, GdalSource, GdalSourceParameters)))] +#[openapi(components(schemas( + TypedOperator, + RasterOperator, + VectorOperator, + GdalSource, + GdalSourceParameters, + MockPointSource, + MockPointSourceParameters +)))] pub struct OperatorsApi; diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/api/src/processes/source.rs b/api/src/processes/source.rs index 7298185f73..59ab840bdb 100644 --- a/api/src/processes/source.rs +++ b/api/src/processes/source.rs @@ -1,11 +1,19 @@ use geoengine_datatypes::dataset::NamedData; use geoengine_macros::type_tag; -use geoengine_operators::source::{ - GdalSource as OperatorsGdalSource, GdalSourceParameters as OperatorsGdalSourceParameters, +use geoengine_operators::{ + mock::{ + MockPointSource as OperatorsMockPointSource, + MockPointSourceParams as OperatorsMockPointSourceParameters, + }, + source::{ + GdalSource as OperatorsGdalSource, GdalSourceParameters as OperatorsGdalSourceParameters, + }, }; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; +use crate::parameters::Coordinate2D; + /// # GdalSource /// /// The [`GdalSource`] is a source operator that reads raster data using GDAL. @@ -56,8 +64,49 @@ impl TryFrom for OperatorsGdalSource { } } +/// # MockPointSource +/// +/// The [`MockPointSource`] is a source operator that provides mock vector point data for testing and development purposes. +/// +/// ## Example JSON +/// ```json +/// { +/// "type": "MockPointSource", +/// "params": { +/// "points": [ { "x": 1.0, "y": 2.0 }, { "x": 3.0, "y": 4.0 } ] +/// } +/// } +/// ``` +#[type_tag(value = "MockPointSource")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MockPointSource { + pub params: MockPointSourceParameters, +} + +/// Parameters for the [`MockPointSource`] operator. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MockPointSourceParameters { + /// Points to be output by the mock point source. + /// + /// ### Example + /// `[{"x": 1.0, "y": 2.0}, {"x": 3.0, "y": 4.0}]` + pub points: Vec, +} + +impl TryFrom for OperatorsMockPointSource { + type Error = anyhow::Error; + fn try_from(value: MockPointSource) -> Result { + Ok(OperatorsMockPointSource { + params: OperatorsMockPointSourceParameters { + points: value.params.points.into_iter().map(Into::into).collect(), + }, + }) + } +} + // TODO: OpenAPI and conversions for other operators: -// - MockPointSource // - Expression // - RasterVectorJoin diff --git a/openapi.json b/openapi.json index 3da0d76742..41b6da83fd 100644 --- a/openapi.json +++ b/openapi.json @@ -7901,6 +7901,41 @@ } } }, + "MockPointSource": { + "type": "object", + "description": "# MockPointSource\n\nThe [`MockPointSource`] is a source operator that provides mock vector point data for testing and development purposes.\n\n## Example JSON\n```json\n{\n \"type\": \"MockPointSource\",\n \"params\": {\n \"points\": [ { \"x\": 1.0, \"y\": 2.0 }, { \"x\": 3.0, \"y\": 4.0 } ]\n }\n}\n```", + "required": [ + "type", + "params" + ], + "properties": { + "params": { + "$ref": "#/components/schemas/MockPointSourceParameters" + }, + "type": { + "type": "string", + "enum": [ + "MockPointSource" + ] + } + } + }, + "MockPointSourceParameters": { + "type": "object", + "description": "Parameters for the [`MockPointSource`] operator.", + "required": [ + "points" + ], + "properties": { + "points": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Coordinate2D" + }, + "description": "Points to be output by the mock point source.\n\n### Example\n`[{\"x\": 1.0, \"y\": 2.0}, {\"x\": 3.0, \"y\": 4.0}]`" + } + } + }, "MultiBandRasterColorizer": { "type": "object", "required": [ @@ -10919,6 +10954,20 @@ "MultiPolygon" ] }, + "VectorOperator": { + "oneOf": [ + { + "$ref": "#/components/schemas/MockPointSource" + } + ], + "description": "An operator that produces vector data.", + "discriminator": { + "propertyName": "type", + "mapping": { + "MockPointSource": "#/components/schemas/MockPointSource" + } + } + }, "VectorQueryRectangle": { "type": "object", "description": "A spatio-temporal rectangle with a specified resolution", From 8b0cf018d42157f7fdde78efe438a08fe31e863a Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Tue, 3 Feb 2026 14:18:44 +0100 Subject: [PATCH 04/19] feat: expression openapi --- api/src/parameters.rs | 289 +++++++++++++++++++++++++++++++- api/src/processes/processing.rs | 151 +++++++++++++++++ api/src/processes/source.rs | 40 ++++- 3 files changed, 474 insertions(+), 6 deletions(-) diff --git a/api/src/parameters.rs b/api/src/parameters.rs index 014f415ba2..fa575853a4 100644 --- a/api/src/parameters.rs +++ b/api/src/parameters.rs @@ -1,7 +1,10 @@ -use serde::{Deserialize, Serialize}; +use geoengine_macros::type_tag; +use serde::{Deserialize, Serialize, Serializer}; +use std::collections::BTreeMap; use utoipa::ToSchema; #[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, Default, ToSchema)] +#[serde(rename_all = "camelCase")] pub struct Coordinate2D { pub x: f64, pub y: f64, @@ -14,3 +17,287 @@ impl From for geoengine_datatypes::primitives::Coordinate2D { } } } + +impl From for Coordinate2D { + fn from(value: geoengine_datatypes::primitives::Coordinate2D) -> Self { + Coordinate2D { + x: value.x, + y: value.y, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum RasterDataType { + U8, + U16, + U32, + U64, + I8, + I16, + I32, + I64, + F32, + F64, +} + +impl From for RasterDataType { + fn from(value: geoengine_datatypes::raster::RasterDataType) -> Self { + match value { + geoengine_datatypes::raster::RasterDataType::U8 => Self::U8, + geoengine_datatypes::raster::RasterDataType::U16 => Self::U16, + geoengine_datatypes::raster::RasterDataType::U32 => Self::U32, + geoengine_datatypes::raster::RasterDataType::U64 => Self::U64, + geoengine_datatypes::raster::RasterDataType::I8 => Self::I8, + geoengine_datatypes::raster::RasterDataType::I16 => Self::I16, + geoengine_datatypes::raster::RasterDataType::I32 => Self::I32, + geoengine_datatypes::raster::RasterDataType::I64 => Self::I64, + geoengine_datatypes::raster::RasterDataType::F32 => Self::F32, + geoengine_datatypes::raster::RasterDataType::F64 => Self::F64, + } + } +} + +impl From for geoengine_datatypes::raster::RasterDataType { + fn from(value: RasterDataType) -> Self { + match value { + RasterDataType::U8 => Self::U8, + RasterDataType::U16 => Self::U16, + RasterDataType::U32 => Self::U32, + RasterDataType::U64 => Self::U64, + RasterDataType::I8 => Self::I8, + RasterDataType::I16 => Self::I16, + RasterDataType::I32 => Self::I32, + RasterDataType::I64 => Self::I64, + RasterDataType::F32 => Self::F32, + RasterDataType::F64 => Self::F64, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase", untagged)] +#[schema(discriminator = "type")] +pub enum Measurement { + Unitless(UnitlessMeasurement), + Continuous(ContinuousMeasurement), + Classification(ClassificationMeasurement), +} + +impl From for Measurement { + fn from(value: geoengine_datatypes::primitives::Measurement) -> Self { + match value { + geoengine_datatypes::primitives::Measurement::Unitless => { + Self::Unitless(UnitlessMeasurement { + r#type: Default::default(), + }) + } + geoengine_datatypes::primitives::Measurement::Continuous(cm) => { + Self::Continuous(cm.into()) + } + geoengine_datatypes::primitives::Measurement::Classification(cm) => { + Self::Classification(cm.into()) + } + } + } +} + +impl From for geoengine_datatypes::primitives::Measurement { + fn from(value: Measurement) -> Self { + match value { + Measurement::Unitless(_) => Self::Unitless, + Measurement::Continuous(cm) => Self::Continuous(cm.into()), + Measurement::Classification(cm) => Self::Classification(cm.into()), + } + } +} + +#[type_tag(value = "unitless")] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema, Default)] +pub struct UnitlessMeasurement {} + +#[type_tag(value = "continuous")] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +pub struct ContinuousMeasurement { + pub measurement: String, + pub unit: Option, +} + +impl From for ContinuousMeasurement { + fn from(value: geoengine_datatypes::primitives::ContinuousMeasurement) -> Self { + ContinuousMeasurement { + r#type: Default::default(), + measurement: value.measurement, + unit: value.unit, + } + } +} + +impl From for geoengine_datatypes::primitives::ContinuousMeasurement { + fn from(value: ContinuousMeasurement) -> Self { + Self { + measurement: value.measurement, + unit: value.unit, + } + } +} + +impl From + for ClassificationMeasurement +{ + fn from(value: geoengine_datatypes::primitives::ClassificationMeasurement) -> Self { + let mut classes = BTreeMap::new(); + for (k, v) in value.classes { + classes.insert(k, v); + } + ClassificationMeasurement { + r#type: Default::default(), + measurement: value.measurement, + classes, + } + } +} + +impl From + for geoengine_datatypes::primitives::ClassificationMeasurement +{ + fn from(value: ClassificationMeasurement) -> Self { + let mut classes = std::collections::HashMap::new(); + for (k, v) in value.classes { + classes.insert(k, v); + } + geoengine_datatypes::primitives::ClassificationMeasurement { + measurement: value.measurement, + classes, + } + } +} + +#[type_tag(value = "classification")] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +pub struct ClassificationMeasurement { + pub measurement: String, + #[serde(serialize_with = "serialize_classes")] + #[serde(deserialize_with = "deserialize_classes")] + pub classes: BTreeMap, +} + +fn serialize_classes(classes: &BTreeMap, serializer: S) -> Result +where + S: Serializer, +{ + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(Some(classes.len()))?; + for (k, v) in classes { + map.serialize_entry(&k.to_string(), v)?; + } + map.end() +} + +fn deserialize_classes<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + use serde::de::{MapAccess, Visitor}; + use std::fmt; + + struct ClassesVisitor; + + impl<'de> Visitor<'de> for ClassesVisitor { + type Value = BTreeMap; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a map with numeric string keys") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut map = BTreeMap::new(); + while let Some((key, value)) = access.next_entry::()? { + let k = key.parse::().map_err(serde::de::Error::custom)?; + map.insert(k, value); + } + Ok(map) + } + } + + deserializer.deserialize_map(ClassesVisitor) +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RasterBandDescriptor { + pub name: String, + pub measurement: Measurement, +} + +impl From for RasterBandDescriptor { + fn from(value: geoengine_operators::engine::RasterBandDescriptor) -> Self { + Self { + name: value.name, + measurement: value.measurement.into(), + } + } +} + +impl From for geoengine_operators::engine::RasterBandDescriptor { + fn from(value: RasterBandDescriptor) -> Self { + Self { + name: value.name, + measurement: value.measurement.into(), + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::float_cmp)] // ok for tests + + use super::*; + + #[test] + fn it_converts_coordinates() { + let dt = geoengine_datatypes::primitives::Coordinate2D { x: 1.5, y: -2.25 }; + + let api: Coordinate2D = dt.into(); + assert_eq!(api.x, 1.5); + assert_eq!(api.y, -2.25); + + let back: geoengine_datatypes::primitives::Coordinate2D = api.into(); + assert_eq!(back.x, 1.5); + assert_eq!(back.y, -2.25); + } + + #[test] + fn it_converts_raster_data_types() { + use geoengine_datatypes::raster::RasterDataType as Dt; + + let dt = Dt::F32; + let api: RasterDataType = dt.into(); + assert_eq!(api, RasterDataType::F32); + + let back: geoengine_datatypes::raster::RasterDataType = api.into(); + assert_eq!(back, Dt::F32); + } + + #[test] + fn it_converts_raster_band_descriptors() { + use geoengine_datatypes::primitives::Measurement; + use geoengine_operators::engine::RasterBandDescriptor as OpsDesc; + + let ops = OpsDesc { + name: "band 0".into(), + measurement: Measurement::Unitless, + }; + + let api: RasterBandDescriptor = ops.clone().into(); + assert_eq!(api.name, "band 0"); + + let back: geoengine_operators::engine::RasterBandDescriptor = api.into(); + assert_eq!(back, ops); + } +} diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index e69de29bb2..a694c7aa7a 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -0,0 +1,151 @@ +use crate::parameters::{RasterBandDescriptor, RasterDataType}; +use geoengine_macros::type_tag; +use geoengine_operators::processing::ExpressionParams as OperatorsExpressionParamsStruct; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +/// # Raster Expression +/// +/// The `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source. +/// The expression is specified as a user-defined script in a very simple language. +/// The output is a raster time series with the result of the expression and with time intervals that are the same as for the inputs. +/// Users can specify an output data type. +/// Internally, the expression is evaluated using floating-point numbers. +/// +/// An example usage scenario is to calculate NDVI for a red and a near-infrared raster channel. +/// The expression uses a raster source with two bands, referred to as A and B, and calculates the formula `(A - B) / (A + B)`. +/// When the temporal resolution is months, our output NDVI will also be a monthly time series. +/// +/// ## Types +/// +/// The following describes the types used in the parameters. +/// +/// ### Expression +/// +/// Expressions are simple scripts to perform pixel-wise computations. +/// One can refer to the raster inputs as `A` for the first raster band, `B` for the second, and so on. +/// Furthermore, expressions can check with `A IS NODATA`, `B IS NODATA`, etc. for NO DATA values. +/// This is important if `mapNoData` is set to true. +/// Otherwise, NO DATA values are mapped automatically to the output NO DATA value. +/// Finally, the value `NODATA` can be used to output NO DATA. +/// +/// Users can think of this implicit function signature for, e.g., two inputs: +/// +/// ```Rust +/// fn (A: f64, B: f64) -> f64 +/// ``` +/// +/// As a start, expressions contain algebraic operations and mathematical functions. +/// +/// ```Rust +/// (A + B) / 2 +/// ``` +/// +/// In addition, branches can be used to check for conditions. +/// +/// ```Rust +/// if A IS NODATA { +/// B +/// } else { +/// A +/// } +/// ``` +/// +/// Function calls can be used to access utility functions. +/// +/// ```Rust +/// max(A, 0) +/// ``` +/// +/// Currently, the following functions are available: +/// +/// - `abs(a)`: absolute value +/// - `min(a, b)`, `min(a, b, c)`: minimum value +/// - `max(a, b)`, `max(a, b, c)`: maximum value +/// - `sqrt(a)`: square root +/// - `ln(a)`: natural logarithm +/// - `log10(a)`: base 10 logarithm +/// - `cos(a)`, `sin(a)`, `tan(a)`, `acos(a)`, `asin(a)`, `atan(a)`: trigonometric functions +/// - `pi()`, `e()`: mathematical constants +/// - `round(a)`, `ceil(a)`, `floor(a)`: rounding functions +/// - `mod(a, b)`: division remainder +/// - `to_degrees(a)`, `to_radians(a)`: conversion to degrees or radians +/// +/// To generate more complex expressions, it is possible to have variable assignments. +/// +/// ```Rust +/// let mean = (A + B) / 2; +/// let coefficient = 0.357; +/// mean * coefficient +/// ``` +/// +/// Note, that all assignments are separated by semicolons. +/// However, the last expression must be without a semicolon. +#[type_tag(value = "Expression")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct Expression { + pub params: ExpressionParameters, +} + +/// ## Types +/// +/// The following describes the types used in the parameters. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct ExpressionParameters { + /// Expression script + /// + /// Example: `"(A - B) / (A + B)"` + pub expression: String, + /// A raster data type for the output + pub output_type: RasterDataType, + /// Description about the output + pub output_band: Option, + /// Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA. + pub map_no_data: bool, +} + +impl TryFrom for OperatorsExpressionParamsStruct { + type Error = anyhow::Error; + + fn try_from(value: Expression) -> Result { + Ok(OperatorsExpressionParamsStruct { + expression: value.params.expression, + output_type: value.params.output_type.into(), + output_band: value.params.output_band.map(Into::into), + map_no_data: value.params.map_no_data, + }) + } +} + +// TODO: OpenAPI and conversions for other operators: +// - RasterVectorJoin + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_converts_expressions() { + let api = Expression { + r#type: Default::default(), + params: ExpressionParameters { + expression: "2 * A + B".to_string(), + output_type: RasterDataType::F32, + output_band: None, + map_no_data: true, + }, + }; + + let ops = OperatorsExpressionParamsStruct::try_from(api).expect("conversion failed"); + + assert_eq!(ops.expression, "2 * A + B"); + assert_eq!( + ops.output_type, + geoengine_datatypes::raster::RasterDataType::F32 + ); + assert!(ops.output_band.is_none()); + assert!(ops.map_no_data); + } +} diff --git a/api/src/processes/source.rs b/api/src/processes/source.rs index 59ab840bdb..001cb877fa 100644 --- a/api/src/processes/source.rs +++ b/api/src/processes/source.rs @@ -106,14 +106,10 @@ impl TryFrom for OperatorsMockPointSource { } } -// TODO: OpenAPI and conversions for other operators: -// - Expression -// - RasterVectorJoin - #[cfg(test)] mod tests { use super::*; - use crate::processes::{RasterOperator, TypedOperator}; + use crate::processes::{RasterOperator, TypedOperator, VectorOperator}; use geoengine_operators::engine::TypedOperator as OperatorsTypedOperator; #[test] @@ -142,4 +138,38 @@ mod tests { OperatorsTypedOperator::try_from(typed_operator).expect("it should convert"); } + + #[test] + fn it_converts_mock_point_source() { + let api_operator = MockPointSource { + r#type: Default::default(), + params: MockPointSourceParameters { + points: vec![ + Coordinate2D { x: 1.0, y: 2.0 }, + Coordinate2D { x: 3.0, y: 4.0 }, + ], + }, + }; + + let operators_operator: OperatorsMockPointSource = + api_operator.try_into().expect("it should convert"); + + assert_eq!( + operators_operator.params.points, + vec![ + geoengine_datatypes::primitives::Coordinate2D { x: 1.0, y: 2.0 }, + geoengine_datatypes::primitives::Coordinate2D { x: 3.0, y: 4.0 } + ] + ); + + let typed_operator = + TypedOperator::Vector(VectorOperator::MockPointSource(MockPointSource { + r#type: Default::default(), + params: MockPointSourceParameters { + points: vec![Coordinate2D { x: 1.0, y: 2.0 }], + }, + })); + + OperatorsTypedOperator::try_from(typed_operator).expect("it should convert"); + } } From 96e6643b8fbd3b1bc1acfe185673c1a79c99a454 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Tue, 3 Feb 2026 14:43:37 +0100 Subject: [PATCH 05/19] feat: RasterVectorJoin openapi --- api/src/parameters.rs | 113 ++++++++++++++++++++ api/src/processes/mod.rs | 27 +++-- api/src/processes/processing.rs | 144 ++++++++++++++++++++++++- openapi.json | 183 +++++++++++++++++++++++++++++++- 4 files changed, 454 insertions(+), 13 deletions(-) diff --git a/api/src/parameters.rs b/api/src/parameters.rs index fa575853a4..8da8264186 100644 --- a/api/src/parameters.rs +++ b/api/src/parameters.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize, Serializer}; use std::collections::BTreeMap; use utoipa::ToSchema; +/// A 2D coordinate with `x` and `y` values. #[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, Default, ToSchema)] #[serde(rename_all = "camelCase")] pub struct Coordinate2D { @@ -27,6 +28,7 @@ impl From for Coordinate2D { } } +/// A raster data type. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub enum RasterDataType { @@ -76,6 +78,7 @@ impl From for geoengine_datatypes::raster::RasterDataType { } } +/// Measurement information for a raster band. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase", untagged)] #[schema(discriminator = "type")] @@ -113,10 +116,13 @@ impl From for geoengine_datatypes::primitives::Measurement { } } +/// A measurement without a unit. #[type_tag(value = "unitless")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema, Default)] pub struct UnitlessMeasurement {} +/// A continuous measurement, e.g., "temperature". +/// It may have an optional unit, e.g., "°C" for degrees Celsius. #[type_tag(value = "continuous")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] pub struct ContinuousMeasurement { @@ -174,6 +180,8 @@ impl From } } +/// A classification measurement. +/// It contains a mapping from class IDs to class names. #[type_tag(value = "classification")] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] pub struct ClassificationMeasurement { @@ -253,6 +261,111 @@ impl From for geoengine_operators::engine::RasterBandDescr } } +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase", tag = "type", content = "values")] +pub enum ColumnNames { + Default, + Suffix(Vec), + Names(Vec), +} + +impl From for ColumnNames { + fn from(value: geoengine_operators::processing::ColumnNames) -> Self { + match value { + geoengine_operators::processing::ColumnNames::Default => ColumnNames::Default, + geoengine_operators::processing::ColumnNames::Suffix(v) => ColumnNames::Suffix(v), + geoengine_operators::processing::ColumnNames::Names(v) => ColumnNames::Names(v), + } + } +} + +impl From for geoengine_operators::processing::ColumnNames { + fn from(value: ColumnNames) -> Self { + match value { + ColumnNames::Default => geoengine_operators::processing::ColumnNames::Default, + ColumnNames::Suffix(v) => geoengine_operators::processing::ColumnNames::Suffix(v), + ColumnNames::Names(v) => geoengine_operators::processing::ColumnNames::Names(v), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum FeatureAggregationMethod { + First, + Mean, +} + +impl From for FeatureAggregationMethod { + fn from(value: geoengine_operators::processing::FeatureAggregationMethod) -> Self { + match value { + geoengine_operators::processing::FeatureAggregationMethod::First => { + FeatureAggregationMethod::First + } + geoengine_operators::processing::FeatureAggregationMethod::Mean => { + FeatureAggregationMethod::Mean + } + } + } +} + +impl From for geoengine_operators::processing::FeatureAggregationMethod { + fn from(value: FeatureAggregationMethod) -> Self { + match value { + FeatureAggregationMethod::First => { + geoengine_operators::processing::FeatureAggregationMethod::First + } + FeatureAggregationMethod::Mean => { + geoengine_operators::processing::FeatureAggregationMethod::Mean + } + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub enum TemporalAggregationMethod { + None, + First, + Mean, +} + +impl From + for TemporalAggregationMethod +{ + fn from(value: geoengine_operators::processing::TemporalAggregationMethod) -> Self { + match value { + geoengine_operators::processing::TemporalAggregationMethod::None => { + TemporalAggregationMethod::None + } + geoengine_operators::processing::TemporalAggregationMethod::First => { + TemporalAggregationMethod::First + } + geoengine_operators::processing::TemporalAggregationMethod::Mean => { + TemporalAggregationMethod::Mean + } + } + } +} + +impl From + for geoengine_operators::processing::TemporalAggregationMethod +{ + fn from(value: TemporalAggregationMethod) -> Self { + match value { + TemporalAggregationMethod::None => { + geoengine_operators::processing::TemporalAggregationMethod::None + } + TemporalAggregationMethod::First => { + geoengine_operators::processing::TemporalAggregationMethod::First + } + TemporalAggregationMethod::Mean => { + geoengine_operators::processing::TemporalAggregationMethod::Mean + } + } + } +} + #[cfg(test)] mod tests { #![allow(clippy::float_cmp)] // ok for tests diff --git a/api/src/processes/mod.rs b/api/src/processes/mod.rs index 6926eded37..9e26e4a994 100644 --- a/api/src/processes/mod.rs +++ b/api/src/processes/mod.rs @@ -1,7 +1,8 @@ #![allow(clippy::needless_for_each)] // TODO: remove when clippy is fixed for utoipa -use crate::processes::source::{ - GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters, +use crate::processes::{ + processing::{Expression, ExpressionParameters, RasterVectorJoin, RasterVectorJoinParameters}, + source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, }; use geoengine_operators::{ engine::{ @@ -41,6 +42,7 @@ pub enum RasterOperator { #[schema(discriminator = "type")] pub enum VectorOperator { MockPointSource(MockPointSource), + RasterVectorJoin(RasterVectorJoin), } impl TryFrom for Box { @@ -58,10 +60,11 @@ impl TryFrom for Box { type Error = anyhow::Error; fn try_from(operator: VectorOperator) -> Result { match operator { - VectorOperator::MockPointSource(mock_point_source) => { - OperatorsMockPointSource::try_from(mock_point_source) - .map(OperatorsVectorOperator::boxed) - } + VectorOperator::MockPointSource(mock_point_source) => OperatorsMockPointSource::try_from(mock_point_source) + .map(OperatorsVectorOperator::boxed), + VectorOperator::RasterVectorJoin(_rvj) => Err(anyhow::anyhow!( + "conversion of RasterVectorJoin to runtime operator is not supported here" + )), } } } @@ -78,12 +81,16 @@ impl TryFrom for OperatorsTypedOperator { #[derive(OpenApi)] #[openapi(components(schemas( - TypedOperator, - RasterOperator, - VectorOperator, + Expression, + ExpressionParameters, GdalSource, GdalSourceParameters, MockPointSource, - MockPointSourceParameters + MockPointSourceParameters, + RasterVectorJoin, + RasterVectorJoinParameters, + RasterOperator, + TypedOperator, + VectorOperator, )))] pub struct OperatorsApi; diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index a694c7aa7a..908deb74ed 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -1,3 +1,4 @@ +use crate::parameters::{ColumnNames, FeatureAggregationMethod, TemporalAggregationMethod}; use crate::parameters::{RasterBandDescriptor, RasterDataType}; use geoengine_macros::type_tag; use geoengine_operators::processing::ExpressionParams as OperatorsExpressionParamsStruct; @@ -119,8 +120,116 @@ impl TryFrom for OperatorsExpressionParamsStruct { } } -// TODO: OpenAPI and conversions for other operators: -// - RasterVectorJoin +/// # RasterVectorJoin +/// +/// The `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs. +/// For each raster input, a new column is added to the collection from the vector input. +/// The new column contains the value of the raster at the location of the vector feature. +/// For features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user. +/// The same is true if the temporal extent of a vector feature covers multiple raster time steps. +/// More details are described below. +/// +/// **Example**: +/// You have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value. +/// For your application, you want to know the NDVI value of each field. +/// The `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies. +/// For example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field. +/// This is useful for exploratory analysis since the computation is very fast. +/// To calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function. +/// Since the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well. +/// The default is `none` which will create a new feature for each month. +/// Other options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time. +/// +/// ## Inputs +/// +/// The `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs. +/// +/// | Parameter | Type | +/// | --------- | ----------------------------------- | +/// | `sources` | `SingleVectorMultipleRasterSources` | +/// +/// ## Errors +/// +/// If the length of `names` is not equal to the number of raster inputs, an error is thrown. +/// +/// ## Example JSON +/// +/// ```json +/// { +/// "type": "RasterVectorJoin", +/// "params": { +/// "names": ["NDVI"], +/// "featureAggregation": "first", +/// "temporalAggregation": "mean", +/// "temporalAggregationIgnoreNoData": true +/// }, +/// "sources": { +/// "vector": { +/// "type": "OgrSource", +/// "params": { +/// "data": "places" +/// } +/// }, +/// "rasters": [ +/// { +/// "type": "GdalSource", +/// "params": { +/// "data": "ndvi" +/// } +/// } +/// ] +/// } +/// } +/// ``` + +#[type_tag(value = "RasterVectorJoin")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RasterVectorJoin { + pub params: RasterVectorJoinParameters, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct RasterVectorJoinParameters { + /// Specify how the new column names are derived from the raster band names. + /// + /// The `ColumnNames` type is used to specify how the new column names are derived from the raster band names. + /// + /// | Value | Description | + /// | ---------------------------------------- | ---------------------------------------------------------------------------- | + /// | `{"type": "default"}` | Appends " (n)" to the band name with the smallest `n` that avoids a conflict | + /// | `{"type": "suffix", "values": [string]}` | Specifies a suffix for each input, to be appended to the band names | + /// | `{"type": "rename", "values": [string]}` | A list of names for each new column | + /// + pub names: ColumnNames, + /// The aggregation function to use for features covering multiple pixels. + pub feature_aggregation: FeatureAggregationMethod, + /// Whether to ignore no data values in the aggregation. Defaults to `false`. + #[serde(default)] + pub feature_aggregation_ignore_no_data: bool, + /// The aggregation function to use for features covering multiple (raster) time steps. + pub temporal_aggregation: TemporalAggregationMethod, + /// Whether to ignore no data values in the aggregation. Defaults to `false`. + #[serde(default)] + pub temporal_aggregation_ignore_no_data: bool, +} + +use geoengine_operators::processing::RasterVectorJoinParams as OperatorsRasterVectorJoinParams; + +impl TryFrom for OperatorsRasterVectorJoinParams { + type Error = anyhow::Error; + + fn try_from(value: RasterVectorJoin) -> Result { + Ok(OperatorsRasterVectorJoinParams { + names: value.params.names.into(), + feature_aggregation: value.params.feature_aggregation.into(), + feature_aggregation_ignore_no_data: value.params.feature_aggregation_ignore_no_data, + temporal_aggregation: value.params.temporal_aggregation.into(), + temporal_aggregation_ignore_no_data: value.params.temporal_aggregation_ignore_no_data, + }) + } +} #[cfg(test)] mod tests { @@ -148,4 +257,35 @@ mod tests { assert!(ops.output_band.is_none()); assert!(ops.map_no_data); } + + #[test] + fn it_converts_raster_vector_join_params() { + let api = RasterVectorJoin { + r#type: Default::default(), + params: RasterVectorJoinParameters { + names: ColumnNames::Names(vec!["a".to_string(), "b".to_string()]), + feature_aggregation: FeatureAggregationMethod::First, + feature_aggregation_ignore_no_data: true, + temporal_aggregation: TemporalAggregationMethod::Mean, + temporal_aggregation_ignore_no_data: false, + }, + }; + + let ops_params = OperatorsRasterVectorJoinParams::try_from(api).expect("conversion failed"); + + assert!(matches!( + ops_params.names, + geoengine_operators::processing::ColumnNames::Names(_) + )); + assert_eq!( + ops_params.feature_aggregation, + geoengine_operators::processing::FeatureAggregationMethod::First + ); + assert!(ops_params.feature_aggregation_ignore_no_data); + assert_eq!( + ops_params.temporal_aggregation, + geoengine_operators::processing::TemporalAggregationMethod::Mean + ); + assert!(!ops_params.temporal_aggregation_ignore_no_data); + } } diff --git a/openapi.json b/openapi.json index 41b6da83fd..c1887a8fd8 100644 --- a/openapi.json +++ b/openapi.json @@ -5879,6 +5879,66 @@ } } }, + "ColumnNames": { + "oneOf": [ + { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "default" + ] + } + } + }, + { + "type": "object", + "required": [ + "values", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "suffix" + ] + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + { + "type": "object", + "required": [ + "values", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "names" + ] + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ] + }, "ComputationQuota": { "type": "object", "required": [ @@ -6670,6 +6730,59 @@ } } }, + "Expression": { + "type": "object", + "description": "# Raster Expression\n\nThe `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source.\nThe expression is specified as a user-defined script in a very simple language.\nThe output is a raster time series with the result of the expression and with time intervals that are the same as for the inputs.\nUsers can specify an output data type.\nInternally, the expression is evaluated using floating-point numbers.\n\nAn example usage scenario is to calculate NDVI for a red and a near-infrared raster channel.\nThe expression uses a raster source with two bands, referred to as A and B, and calculates the formula `(A - B) / (A + B)`.\nWhen the temporal resolution is months, our output NDVI will also be a monthly time series.\n\n## Types\n\nThe following describes the types used in the parameters.\n\n### Expression\n\nExpressions are simple scripts to perform pixel-wise computations.\nOne can refer to the raster inputs as `A` for the first raster band, `B` for the second, and so on.\nFurthermore, expressions can check with `A IS NODATA`, `B IS NODATA`, etc. for NO DATA values.\nThis is important if `mapNoData` is set to true.\nOtherwise, NO DATA values are mapped automatically to the output NO DATA value.\nFinally, the value `NODATA` can be used to output NO DATA.\n\nUsers can think of this implicit function signature for, e.g., two inputs:\n\n```Rust\nfn (A: f64, B: f64) -> f64\n```\n\nAs a start, expressions contain algebraic operations and mathematical functions.\n\n```Rust\n(A + B) / 2\n```\n\nIn addition, branches can be used to check for conditions.\n\n```Rust\nif A IS NODATA {\n B\n} else {\n A\n}\n```\n\nFunction calls can be used to access utility functions.\n\n```Rust\nmax(A, 0)\n```\n\nCurrently, the following functions are available:\n\n- `abs(a)`: absolute value\n- `min(a, b)`, `min(a, b, c)`: minimum value\n- `max(a, b)`, `max(a, b, c)`: maximum value\n- `sqrt(a)`: square root\n- `ln(a)`: natural logarithm\n- `log10(a)`: base 10 logarithm\n- `cos(a)`, `sin(a)`, `tan(a)`, `acos(a)`, `asin(a)`, `atan(a)`: trigonometric functions\n- `pi()`, `e()`: mathematical constants\n- `round(a)`, `ceil(a)`, `floor(a)`: rounding functions\n- `mod(a, b)`: division remainder\n- `to_degrees(a)`, `to_radians(a)`: conversion to degrees or radians\n\nTo generate more complex expressions, it is possible to have variable assignments.\n\n```Rust\nlet mean = (A + B) / 2;\nlet coefficient = 0.357;\nmean * coefficient\n```\n\nNote, that all assignments are separated by semicolons.\nHowever, the last expression must be without a semicolon.", + "required": [ + "type", + "params" + ], + "properties": { + "params": { + "$ref": "#/components/schemas/ExpressionParameters" + }, + "type": { + "type": "string", + "enum": [ + "Expression" + ] + } + } + }, + "ExpressionParameters": { + "type": "object", + "description": "## Types\n\nThe following describes the types used in the parameters.", + "required": [ + "expression", + "outputType", + "mapNoData" + ], + "properties": { + "expression": { + "type": "string", + "description": "Expression script\n\nExample: `\"(A - B) / (A + B)\"`" + }, + "mapNoData": { + "type": "boolean", + "description": "Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA." + }, + "outputBand": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/components/schemas/RasterBandDescriptor", + "description": "Description about the output" + } + ] + }, + "outputType": { + "$ref": "#/components/schemas/RasterDataType", + "description": "A raster data type for the output" + } + } + }, "ExternalDataId": { "type": "object", "required": [ @@ -6692,6 +6805,13 @@ } } }, + "FeatureAggregationMethod": { + "type": "string", + "enum": [ + "first", + "mean" + ] + }, "FeatureDataType": { "type": "string", "enum": [ @@ -9458,6 +9578,55 @@ } } }, + "RasterVectorJoin": { + "type": "object", + "description": "# RasterVectorJoin\n\nThe `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs.\nFor each raster input, a new column is added to the collection from the vector input.\nThe new column contains the value of the raster at the location of the vector feature.\nFor features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user.\nThe same is true if the temporal extent of a vector feature covers multiple raster time steps.\nMore details are described below.\n\n**Example**:\nYou have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value.\nFor your application, you want to know the NDVI value of each field.\nThe `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies.\nFor example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field.\nThis is useful for exploratory analysis since the computation is very fast.\nTo calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function.\nSince the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well.\nThe default is `none` which will create a new feature for each month.\nOther options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time.\n\n## Inputs\n\nThe `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs.\n\n| Parameter | Type |\n| --------- | ----------------------------------- |\n| `sources` | `SingleVectorMultipleRasterSources` |\n\n## Errors\n\nIf the length of `names` is not equal to the number of raster inputs, an error is thrown.\n\n## Example JSON\n\n```json\n{\n \"type\": \"RasterVectorJoin\",\n \"params\": {\n \"names\": [\"NDVI\"],\n \"featureAggregation\": \"first\",\n \"temporalAggregation\": \"mean\",\n \"temporalAggregationIgnoreNoData\": true\n },\n \"sources\": {\n \"vector\": {\n \"type\": \"OgrSource\",\n \"params\": {\n \"data\": \"places\"\n }\n },\n \"rasters\": [\n {\n \"type\": \"GdalSource\",\n \"params\": {\n \"data\": \"ndvi\"\n }\n }\n ]\n }\n}\n```", + "required": [ + "type", + "params" + ], + "properties": { + "params": { + "$ref": "#/components/schemas/RasterVectorJoinParameters" + }, + "type": { + "type": "string", + "enum": [ + "RasterVectorJoin" + ] + } + } + }, + "RasterVectorJoinParameters": { + "type": "object", + "required": [ + "names", + "featureAggregation", + "temporalAggregation" + ], + "properties": { + "featureAggregation": { + "$ref": "#/components/schemas/FeatureAggregationMethod", + "description": "The aggregation function to use for features covering multiple pixels." + }, + "featureAggregationIgnoreNoData": { + "type": "boolean", + "description": "Whether to ignore no data values in the aggregation. Defaults to `false`." + }, + "names": { + "$ref": "#/components/schemas/ColumnNames", + "description": "Specify how the new column names are derived from the raster band names.\n\nThe `ColumnNames` type is used to specify how the new column names are derived from the raster band names.\n\n| Value | Description |\n| ---------------------------------------- | ---------------------------------------------------------------------------- |\n| `{\"type\": \"default\"}` | Appends \" (n)\" to the band name with the smallest `n` that avoids a conflict |\n| `{\"type\": \"suffix\", \"values\": [string]}` | Specifies a suffix for each input, to be appended to the band names |\n| `{\"type\": \"rename\", \"values\": [string]}` | A list of names for each new column |\n" + }, + "temporalAggregation": { + "$ref": "#/components/schemas/TemporalAggregationMethod", + "description": "The aggregation function to use for features covering multiple (raster) time steps." + }, + "temporalAggregationIgnoreNoData": { + "type": "boolean", + "description": "Whether to ignore no data values in the aggregation. Defaults to `false`." + } + } + }, "Resource": { "oneOf": [ { @@ -10185,6 +10354,14 @@ } ] }, + "TemporalAggregationMethod": { + "type": "string", + "enum": [ + "none", + "first", + "mean" + ] + }, "TextSymbology": { "type": "object", "required": [ @@ -10958,13 +11135,17 @@ "oneOf": [ { "$ref": "#/components/schemas/MockPointSource" + }, + { + "$ref": "#/components/schemas/RasterVectorJoin" } ], "description": "An operator that produces vector data.", "discriminator": { "propertyName": "type", "mapping": { - "MockPointSource": "#/components/schemas/MockPointSource" + "MockPointSource": "#/components/schemas/MockPointSource", + "RasterVectorJoin": "#/components/schemas/RasterVectorJoin" } } }, From 1b9d7208f92cbcac19ad21743fbb3c4c5b18829e Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Tue, 3 Feb 2026 14:45:35 +0100 Subject: [PATCH 06/19] fmt --- api/src/processes/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api/src/processes/mod.rs b/api/src/processes/mod.rs index 9e26e4a994..c9b9481552 100644 --- a/api/src/processes/mod.rs +++ b/api/src/processes/mod.rs @@ -60,8 +60,10 @@ impl TryFrom for Box { type Error = anyhow::Error; fn try_from(operator: VectorOperator) -> Result { match operator { - VectorOperator::MockPointSource(mock_point_source) => OperatorsMockPointSource::try_from(mock_point_source) - .map(OperatorsVectorOperator::boxed), + VectorOperator::MockPointSource(mock_point_source) => { + OperatorsMockPointSource::try_from(mock_point_source) + .map(OperatorsVectorOperator::boxed) + } VectorOperator::RasterVectorJoin(_rvj) => Err(anyhow::anyhow!( "conversion of RasterVectorJoin to runtime operator is not supported here" )), From b87d752a77ea9e3423d00948bb9cdcd9a3c2ba2f Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Wed, 4 Feb 2026 14:43:14 +0100 Subject: [PATCH 07/19] empty plot operator type --- api/src/processes/mod.rs | 13 +++++++++++-- openapi.json | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/api/src/processes/mod.rs b/api/src/processes/mod.rs index c9b9481552..1f03a1a238 100644 --- a/api/src/processes/mod.rs +++ b/api/src/processes/mod.rs @@ -25,7 +25,7 @@ mod source; pub enum TypedOperator { Vector(VectorOperator), Raster(RasterOperator), - // Plot(Box), + Plot(PlotOperator), } /// An operator that produces raster data. @@ -45,6 +45,14 @@ pub enum VectorOperator { RasterVectorJoin(RasterVectorJoin), } +/// An operator that produces plot data. +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[serde(rename_all = "camelCase", untagged)] +// #[schema(discriminator = "type")] +pub enum PlotOperator { + // Currently no plot operators are defined +} + impl TryFrom for Box { type Error = anyhow::Error; fn try_from(operator: RasterOperator) -> Result { @@ -89,10 +97,11 @@ impl TryFrom for OperatorsTypedOperator { GdalSourceParameters, MockPointSource, MockPointSourceParameters, + RasterOperator, RasterVectorJoin, RasterVectorJoinParameters, - RasterOperator, TypedOperator, VectorOperator, + PlotOperator, )))] pub struct OperatorsApi; diff --git a/openapi.json b/openapi.json index c1887a8fd8..fbe2aaff20 100644 --- a/openapi.json +++ b/openapi.json @@ -8881,6 +8881,11 @@ } } }, + "PlotOperator": { + "type": "null", + "description": "An operator that produces plot data.", + "default": null + }, "PlotOutputFormat": { "type": "string", "enum": [ From ab9b2e6a095c6aa1f0984c871323fae8a1452c63 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Wed, 4 Feb 2026 17:46:11 +0100 Subject: [PATCH 08/19] fix: enhance expression and raster/vector join operators with new source structures --- api/src/parameters.rs | 87 +++++++++++++++++++--- api/src/processes/processing.rs | 106 +++++++++++++++++++-------- api/src/processes/source.rs | 4 +- openapi.json | 124 +++++++++++++++++++++++--------- 4 files changed, 246 insertions(+), 75 deletions(-) diff --git a/api/src/parameters.rs b/api/src/parameters.rs index c89d52b40d..2cf259aa11 100644 --- a/api/src/parameters.rs +++ b/api/src/parameters.rs @@ -1,3 +1,4 @@ +use crate::processes::{RasterOperator, VectorOperator}; use anyhow::Context; use geoengine_macros::type_tag; use serde::{Deserialize, Serialize, Serializer}; @@ -365,24 +366,48 @@ impl From /// Spatial bounds derivation options for the [`MockPointSource`]. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] -#[serde(rename_all = "camelCase", tag = "type")] -#[derive(Default)] +#[serde(rename_all = "camelCase", untagged)] +#[schema(discriminator = "type")] pub enum SpatialBoundsDerive { - Derive, - Bounds(BoundingBox2D), - #[default] - None, + Derive(SpatialBoundsDeriveDerive), + Bounds(SpatialBoundsDeriveBounds), + None(SpatialBoundsDeriveNone), } +impl Default for SpatialBoundsDerive { + fn default() -> Self { + SpatialBoundsDerive::None(SpatialBoundsDeriveNone::default()) + } +} + +#[type_tag(value = "derive")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)] +pub struct SpatialBoundsDeriveDerive {} + +#[type_tag(value = "bounds")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +pub struct SpatialBoundsDeriveBounds { + #[serde(flatten)] + pub bounding_box: BoundingBox2D, +} + +#[type_tag(value = "none")] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)] +pub struct SpatialBoundsDeriveNone {} + impl TryFrom for geoengine_operators::mock::SpatialBoundsDerive { type Error = anyhow::Error; fn try_from(value: SpatialBoundsDerive) -> Result { Ok(match value { - SpatialBoundsDerive::Derive => geoengine_operators::mock::SpatialBoundsDerive::Derive, - SpatialBoundsDerive::Bounds(bbox) => { - geoengine_operators::mock::SpatialBoundsDerive::Bounds(bbox.try_into()?) + SpatialBoundsDerive::Derive(_) => { + geoengine_operators::mock::SpatialBoundsDerive::Derive } - SpatialBoundsDerive::None => geoengine_operators::mock::SpatialBoundsDerive::None, + SpatialBoundsDerive::Bounds(bounds) => { + geoengine_operators::mock::SpatialBoundsDerive::Bounds( + bounds.bounding_box.try_into()?, + ) + } + SpatialBoundsDerive::None(_) => geoengine_operators::mock::SpatialBoundsDerive::None, }) } } @@ -407,6 +432,48 @@ impl TryFrom for geoengine_datatypes::primitives::BoundingBox2D { } } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[schema(no_recursion)] +#[serde(rename_all = "camelCase")] +pub struct SingleRasterSource { + pub raster: RasterOperator, +} + +impl TryFrom for geoengine_operators::engine::SingleRasterSource { + type Error = anyhow::Error; + + fn try_from(value: SingleRasterSource) -> Result { + Ok(Self { + raster: value.raster.try_into()?, + }) + } +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] +#[schema(no_recursion)] +#[serde(rename_all = "camelCase")] +pub struct SingleVectorMultipleRasterSources { + pub vector: VectorOperator, + pub rasters: Vec, +} + +impl TryFrom + for geoengine_operators::engine::SingleVectorMultipleRasterSources +{ + type Error = anyhow::Error; + + fn try_from(value: SingleVectorMultipleRasterSources) -> Result { + Ok(Self { + vector: value.vector.try_into()?, + rasters: value + .rasters + .into_iter() + .map(std::convert::TryInto::try_into) + .collect::>()?, + }) + } +} + #[cfg(test)] mod tests { #![allow(clippy::float_cmp)] // ok for tests diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index 908deb74ed..095f51fd8a 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -1,7 +1,14 @@ -use crate::parameters::{ColumnNames, FeatureAggregationMethod, TemporalAggregationMethod}; +use crate::parameters::{ + ColumnNames, FeatureAggregationMethod, SingleRasterSource, SingleVectorMultipleRasterSources, + TemporalAggregationMethod, +}; use crate::parameters::{RasterBandDescriptor, RasterDataType}; use geoengine_macros::type_tag; -use geoengine_operators::processing::ExpressionParams as OperatorsExpressionParamsStruct; +use geoengine_operators::processing::{ + Expression as OperatorsExpression, ExpressionParams as OperatorsExpressionParameters, + RasterVectorJoin as OperatorsRasterVectorJoin, + RasterVectorJoinParams as OperatorsRasterVectorJoinParameters, +}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -87,6 +94,7 @@ use utoipa::ToSchema; #[serde(rename_all = "camelCase")] pub struct Expression { pub params: ExpressionParameters, + pub sources: SingleRasterSource, } /// ## Types @@ -107,15 +115,18 @@ pub struct ExpressionParameters { pub map_no_data: bool, } -impl TryFrom for OperatorsExpressionParamsStruct { +impl TryFrom for OperatorsExpression { type Error = anyhow::Error; fn try_from(value: Expression) -> Result { - Ok(OperatorsExpressionParamsStruct { - expression: value.params.expression, - output_type: value.params.output_type.into(), - output_band: value.params.output_band.map(Into::into), - map_no_data: value.params.map_no_data, + Ok(OperatorsExpression { + params: OperatorsExpressionParameters { + expression: value.params.expression, + output_type: value.params.output_type.into(), + output_band: value.params.output_band.map(Into::into), + map_no_data: value.params.map_no_data, + }, + sources: value.sources.try_into()?, }) } } @@ -187,6 +198,7 @@ impl TryFrom for OperatorsExpressionParamsStruct { #[serde(rename_all = "camelCase")] pub struct RasterVectorJoin { pub params: RasterVectorJoinParameters, + pub sources: Box, } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] @@ -215,25 +227,38 @@ pub struct RasterVectorJoinParameters { pub temporal_aggregation_ignore_no_data: bool, } -use geoengine_operators::processing::RasterVectorJoinParams as OperatorsRasterVectorJoinParams; - -impl TryFrom for OperatorsRasterVectorJoinParams { +impl TryFrom for OperatorsRasterVectorJoin { type Error = anyhow::Error; fn try_from(value: RasterVectorJoin) -> Result { - Ok(OperatorsRasterVectorJoinParams { - names: value.params.names.into(), - feature_aggregation: value.params.feature_aggregation.into(), - feature_aggregation_ignore_no_data: value.params.feature_aggregation_ignore_no_data, - temporal_aggregation: value.params.temporal_aggregation.into(), - temporal_aggregation_ignore_no_data: value.params.temporal_aggregation_ignore_no_data, + Ok(OperatorsRasterVectorJoin { + params: OperatorsRasterVectorJoinParameters { + names: value.params.names.into(), + feature_aggregation: value.params.feature_aggregation.into(), + feature_aggregation_ignore_no_data: value.params.feature_aggregation_ignore_no_data, + temporal_aggregation: value.params.temporal_aggregation.into(), + temporal_aggregation_ignore_no_data: value + .params + .temporal_aggregation_ignore_no_data, + }, + sources: (*value.sources).try_into()?, }) } } #[cfg(test)] mod tests { + use super::*; + use crate::{ + parameters::{Coordinate2D, SpatialBoundsDerive}, + processes::{ + RasterOperator, VectorOperator, + source::{ + GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters, + }, + }, + }; #[test] fn it_converts_expressions() { @@ -245,17 +270,26 @@ mod tests { output_band: None, map_no_data: true, }, + sources: SingleRasterSource { + raster: RasterOperator::GdalSource(GdalSource { + r#type: Default::default(), + params: GdalSourceParameters { + data: "example_data".to_string(), + overview_level: None, + }, + }), + }, }; - let ops = OperatorsExpressionParamsStruct::try_from(api).expect("conversion failed"); + let ops = OperatorsExpression::try_from(api).expect("conversion failed"); - assert_eq!(ops.expression, "2 * A + B"); + assert_eq!(ops.params.expression, "2 * A + B"); assert_eq!( - ops.output_type, + ops.params.output_type, geoengine_datatypes::raster::RasterDataType::F32 ); - assert!(ops.output_band.is_none()); - assert!(ops.map_no_data); + assert!(ops.params.output_band.is_none()); + assert!(ops.params.map_no_data); } #[test] @@ -269,23 +303,39 @@ mod tests { temporal_aggregation: TemporalAggregationMethod::Mean, temporal_aggregation_ignore_no_data: false, }, + sources: Box::new(SingleVectorMultipleRasterSources { + vector: VectorOperator::MockPointSource(MockPointSource { + r#type: Default::default(), + params: MockPointSourceParameters { + points: vec![Coordinate2D { x: 0.0, y: 0.0 }], + spatial_bounds: SpatialBoundsDerive::Derive(Default::default()), + }, + }), + rasters: vec![RasterOperator::GdalSource(GdalSource { + r#type: Default::default(), + params: GdalSourceParameters { + data: "example_data".to_string(), + overview_level: None, + }, + })], + }), }; - let ops_params = OperatorsRasterVectorJoinParams::try_from(api).expect("conversion failed"); + let ops_params = OperatorsRasterVectorJoin::try_from(api).expect("conversion failed"); assert!(matches!( - ops_params.names, + ops_params.params.names, geoengine_operators::processing::ColumnNames::Names(_) )); assert_eq!( - ops_params.feature_aggregation, + ops_params.params.feature_aggregation, geoengine_operators::processing::FeatureAggregationMethod::First ); - assert!(ops_params.feature_aggregation_ignore_no_data); + assert!(ops_params.params.feature_aggregation_ignore_no_data); assert_eq!( - ops_params.temporal_aggregation, + ops_params.params.temporal_aggregation, geoengine_operators::processing::TemporalAggregationMethod::Mean ); - assert!(!ops_params.temporal_aggregation_ignore_no_data); + assert!(!ops_params.params.temporal_aggregation_ignore_no_data); } } diff --git a/api/src/processes/source.rs b/api/src/processes/source.rs index 52909c7cd4..25d7dadfc5 100644 --- a/api/src/processes/source.rs +++ b/api/src/processes/source.rs @@ -161,7 +161,7 @@ mod tests { Coordinate2D { x: 1.0, y: 2.0 }, Coordinate2D { x: 3.0, y: 4.0 }, ], - spatial_bounds: SpatialBoundsDerive::Derive, + spatial_bounds: SpatialBoundsDerive::Derive(Default::default()), }, }; @@ -181,7 +181,7 @@ mod tests { r#type: Default::default(), params: MockPointSourceParameters { points: vec![Coordinate2D { x: 1.0, y: 2.0 }], - spatial_bounds: SpatialBoundsDerive::Derive, + spatial_bounds: SpatialBoundsDerive::Derive(Default::default()), }, })); diff --git a/openapi.json b/openapi.json index 34af9919e8..a021d038bf 100644 --- a/openapi.json +++ b/openapi.json @@ -6501,12 +6501,16 @@ "description": "# Raster Expression\n\nThe `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source.\nThe expression is specified as a user-defined script in a very simple language.\nThe output is a raster time series with the result of the expression and with time intervals that are the same as for the inputs.\nUsers can specify an output data type.\nInternally, the expression is evaluated using floating-point numbers.\n\nAn example usage scenario is to calculate NDVI for a red and a near-infrared raster channel.\nThe expression uses a raster source with two bands, referred to as A and B, and calculates the formula `(A - B) / (A + B)`.\nWhen the temporal resolution is months, our output NDVI will also be a monthly time series.\n\n## Types\n\nThe following describes the types used in the parameters.\n\n### Expression\n\nExpressions are simple scripts to perform pixel-wise computations.\nOne can refer to the raster inputs as `A` for the first raster band, `B` for the second, and so on.\nFurthermore, expressions can check with `A IS NODATA`, `B IS NODATA`, etc. for NO DATA values.\nThis is important if `mapNoData` is set to true.\nOtherwise, NO DATA values are mapped automatically to the output NO DATA value.\nFinally, the value `NODATA` can be used to output NO DATA.\n\nUsers can think of this implicit function signature for, e.g., two inputs:\n\n```Rust\nfn (A: f64, B: f64) -> f64\n```\n\nAs a start, expressions contain algebraic operations and mathematical functions.\n\n```Rust\n(A + B) / 2\n```\n\nIn addition, branches can be used to check for conditions.\n\n```Rust\nif A IS NODATA {\n B\n} else {\n A\n}\n```\n\nFunction calls can be used to access utility functions.\n\n```Rust\nmax(A, 0)\n```\n\nCurrently, the following functions are available:\n\n- `abs(a)`: absolute value\n- `min(a, b)`, `min(a, b, c)`: minimum value\n- `max(a, b)`, `max(a, b, c)`: maximum value\n- `sqrt(a)`: square root\n- `ln(a)`: natural logarithm\n- `log10(a)`: base 10 logarithm\n- `cos(a)`, `sin(a)`, `tan(a)`, `acos(a)`, `asin(a)`, `atan(a)`: trigonometric functions\n- `pi()`, `e()`: mathematical constants\n- `round(a)`, `ceil(a)`, `floor(a)`: rounding functions\n- `mod(a, b)`: division remainder\n- `to_degrees(a)`, `to_radians(a)`: conversion to degrees or radians\n\nTo generate more complex expressions, it is possible to have variable assignments.\n\n```Rust\nlet mean = (A + B) / 2;\nlet coefficient = 0.357;\nmean * coefficient\n```\n\nNote, that all assignments are separated by semicolons.\nHowever, the last expression must be without a semicolon.", "required": [ "type", - "params" + "params", + "sources" ], "properties": { "params": { "$ref": "#/components/schemas/ExpressionParameters" }, + "sources": { + "$ref": "#/components/schemas/SingleRasterSource" + }, "type": { "type": "string", "enum": [ @@ -9328,12 +9332,16 @@ "description": "# RasterVectorJoin\n\nThe `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs.\nFor each raster input, a new column is added to the collection from the vector input.\nThe new column contains the value of the raster at the location of the vector feature.\nFor features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user.\nThe same is true if the temporal extent of a vector feature covers multiple raster time steps.\nMore details are described below.\n\n**Example**:\nYou have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value.\nFor your application, you want to know the NDVI value of each field.\nThe `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies.\nFor example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field.\nThis is useful for exploratory analysis since the computation is very fast.\nTo calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function.\nSince the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well.\nThe default is `none` which will create a new feature for each month.\nOther options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time.\n\n## Inputs\n\nThe `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs.\n\n| Parameter | Type |\n| --------- | ----------------------------------- |\n| `sources` | `SingleVectorMultipleRasterSources` |\n\n## Errors\n\nIf the length of `names` is not equal to the number of raster inputs, an error is thrown.\n\n## Example JSON\n\n```json\n{\n \"type\": \"RasterVectorJoin\",\n \"params\": {\n \"names\": [\"NDVI\"],\n \"featureAggregation\": \"first\",\n \"temporalAggregation\": \"mean\",\n \"temporalAggregationIgnoreNoData\": true\n },\n \"sources\": {\n \"vector\": {\n \"type\": \"OgrSource\",\n \"params\": {\n \"data\": \"places\"\n }\n },\n \"rasters\": [\n {\n \"type\": \"GdalSource\",\n \"params\": {\n \"data\": \"ndvi\"\n }\n }\n ]\n }\n}\n```", "required": [ "type", - "params" + "params", + "sources" ], "properties": { "params": { "$ref": "#/components/schemas/RasterVectorJoinParameters" }, + "sources": { + "$ref": "#/components/schemas/SingleVectorMultipleRasterSources" + }, "type": { "type": "string", "enum": [ @@ -9633,42 +9641,61 @@ } } }, + "SingleRasterSource": { + "type": "object", + "required": [ + "raster" + ], + "properties": { + "raster": { + "$ref": "#/components/schemas/RasterOperator" + } + } + }, + "SingleVectorMultipleRasterSources": { + "type": "object", + "required": [ + "vector", + "rasters" + ], + "properties": { + "rasters": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RasterOperator" + } + }, + "vector": { + "$ref": "#/components/schemas/VectorOperator" + } + } + }, "SpatialBoundsDerive": { "oneOf": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "derive" - ] - } - } + "$ref": "#/components/schemas/SpatialBoundsDeriveDerive" }, { - "allOf": [ - { - "$ref": "#/components/schemas/BoundingBox2D" - }, - { - "type": "object", - "required": [ - "type" - ], - "properties": { - "type": { - "type": "string", - "enum": [ - "bounds" - ] - } - } - } - ] + "$ref": "#/components/schemas/SpatialBoundsDeriveBounds" + }, + { + "$ref": "#/components/schemas/SpatialBoundsDeriveNone" + } + ], + "description": "Spatial bounds derivation options for the [`MockPointSource`].", + "discriminator": { + "propertyName": "type", + "mapping": { + "bounds": "#/components/schemas/SpatialBoundsDeriveBounds", + "derive": "#/components/schemas/SpatialBoundsDeriveDerive", + "none": "#/components/schemas/SpatialBoundsDeriveNone" + } + } + }, + "SpatialBoundsDeriveBounds": { + "allOf": [ + { + "$ref": "#/components/schemas/BoundingBox2D" }, { "type": "object", @@ -9679,13 +9706,40 @@ "type": { "type": "string", "enum": [ - "none" + "bounds" ] } } } + ] + }, + "SpatialBoundsDeriveDerive": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "derive" + ] + } + } + }, + "SpatialBoundsDeriveNone": { + "type": "object", + "required": [ + "type" ], - "description": "Spatial bounds derivation options for the [`MockPointSource`]." + "properties": { + "type": { + "type": "string", + "enum": [ + "none" + ] + } + } }, "SpatialGridDefinition": { "type": "object", From 126f71e6fcb449701885fb5d7984d6c4022c0b5a Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Wed, 4 Feb 2026 18:01:27 +0100 Subject: [PATCH 09/19] fix: add Expression operator and support for RasterVectorJoin conversion --- api/src/processes/mod.rs | 13 ++++++++++--- api/src/processes/processing.rs | 8 ++++---- openapi.json | 4 ++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/api/src/processes/mod.rs b/api/src/processes/mod.rs index 1f03a1a238..2102e6461f 100644 --- a/api/src/processes/mod.rs +++ b/api/src/processes/mod.rs @@ -10,6 +10,9 @@ use geoengine_operators::{ VectorOperator as OperatorsVectorOperator, }, mock::MockPointSource as OperatorsMockPointSource, + processing::{ + Expression as OperatorsExpression, RasterVectorJoin as OperatorsRasterVectorJoin, + }, source::GdalSource as OperatorsGdalSource, }; use serde::{Deserialize, Serialize}; @@ -33,6 +36,7 @@ pub enum TypedOperator { #[serde(rename_all = "camelCase", untagged)] #[schema(discriminator = "type")] pub enum RasterOperator { + Expression(Expression), GdalSource(GdalSource), } @@ -57,6 +61,9 @@ impl TryFrom for Box { type Error = anyhow::Error; fn try_from(operator: RasterOperator) -> Result { match operator { + RasterOperator::Expression(expression) => { + OperatorsExpression::try_from(expression).map(OperatorsRasterOperator::boxed) + } RasterOperator::GdalSource(gdal_source) => { OperatorsGdalSource::try_from(gdal_source).map(OperatorsRasterOperator::boxed) } @@ -72,9 +79,9 @@ impl TryFrom for Box { OperatorsMockPointSource::try_from(mock_point_source) .map(OperatorsVectorOperator::boxed) } - VectorOperator::RasterVectorJoin(_rvj) => Err(anyhow::anyhow!( - "conversion of RasterVectorJoin to runtime operator is not supported here" - )), + VectorOperator::RasterVectorJoin(rvj) => { + OperatorsRasterVectorJoin::try_from(rvj).map(OperatorsVectorOperator::boxed) + } } } } diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index 095f51fd8a..3277bf3a75 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -94,7 +94,7 @@ use utoipa::ToSchema; #[serde(rename_all = "camelCase")] pub struct Expression { pub params: ExpressionParameters, - pub sources: SingleRasterSource, + pub sources: Box, } /// ## Types @@ -126,7 +126,7 @@ impl TryFrom for OperatorsExpression { output_band: value.params.output_band.map(Into::into), map_no_data: value.params.map_no_data, }, - sources: value.sources.try_into()?, + sources: (*value.sources).try_into()?, }) } } @@ -270,7 +270,7 @@ mod tests { output_band: None, map_no_data: true, }, - sources: SingleRasterSource { + sources: Box::new(SingleRasterSource { raster: RasterOperator::GdalSource(GdalSource { r#type: Default::default(), params: GdalSourceParameters { @@ -278,7 +278,7 @@ mod tests { overview_level: None, }, }), - }, + }), }; let ops = OperatorsExpression::try_from(api).expect("conversion failed"); diff --git a/openapi.json b/openapi.json index a021d038bf..aa7eb7eb94 100644 --- a/openapi.json +++ b/openapi.json @@ -9217,6 +9217,9 @@ }, "RasterOperator": { "oneOf": [ + { + "$ref": "#/components/schemas/Expression" + }, { "$ref": "#/components/schemas/GdalSource" } @@ -9225,6 +9228,7 @@ "discriminator": { "propertyName": "type", "mapping": { + "Expression": "#/components/schemas/Expression", "GdalSource": "#/components/schemas/GdalSource" } } From 9d0869cc4281aad7c2d761f11909cf292ba9e111 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 5 Feb 2026 11:22:31 +0100 Subject: [PATCH 10/19] fix: update ColumnNames enum to use structured values and enhance OpenAPI documentation --- api/src/parameters.rs | 25 ++++++++++++++++++------- api/src/processes/processing.rs | 4 +++- openapi.json | 3 +++ 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/api/src/parameters.rs b/api/src/parameters.rs index 2cf259aa11..fb0328571a 100644 --- a/api/src/parameters.rs +++ b/api/src/parameters.rs @@ -260,19 +260,26 @@ impl From for geoengine_operators::engine::RasterBandDescr } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] -#[serde(rename_all = "camelCase", tag = "type", content = "values")] +#[serde(rename_all = "camelCase", tag = "type")] pub enum ColumnNames { + #[schema(title = "Default")] Default, - Suffix(Vec), - Names(Vec), + #[schema(title = "Suffix")] + Suffix { values: Vec }, + #[schema(title = "Names")] + Names { values: Vec }, } impl From for ColumnNames { fn from(value: geoengine_operators::processing::ColumnNames) -> Self { match value { geoengine_operators::processing::ColumnNames::Default => ColumnNames::Default, - geoengine_operators::processing::ColumnNames::Suffix(v) => ColumnNames::Suffix(v), - geoengine_operators::processing::ColumnNames::Names(v) => ColumnNames::Names(v), + geoengine_operators::processing::ColumnNames::Suffix(v) => { + ColumnNames::Suffix { values: v } + } + geoengine_operators::processing::ColumnNames::Names(v) => { + ColumnNames::Names { values: v } + } } } } @@ -281,8 +288,12 @@ impl From for geoengine_operators::processing::ColumnNames { fn from(value: ColumnNames) -> Self { match value { ColumnNames::Default => geoengine_operators::processing::ColumnNames::Default, - ColumnNames::Suffix(v) => geoengine_operators::processing::ColumnNames::Suffix(v), - ColumnNames::Names(v) => geoengine_operators::processing::ColumnNames::Names(v), + ColumnNames::Suffix { values } => { + geoengine_operators::processing::ColumnNames::Suffix(values) + } + ColumnNames::Names { values } => { + geoengine_operators::processing::ColumnNames::Names(values) + } } } } diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index 3277bf3a75..453a24c3e0 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -297,7 +297,9 @@ mod tests { let api = RasterVectorJoin { r#type: Default::default(), params: RasterVectorJoinParameters { - names: ColumnNames::Names(vec!["a".to_string(), "b".to_string()]), + names: ColumnNames::Names { + values: vec!["a".to_string(), "b".to_string()], + }, feature_aggregation: FeatureAggregationMethod::First, feature_aggregation_ignore_no_data: true, temporal_aggregation: TemporalAggregationMethod::Mean, diff --git a/openapi.json b/openapi.json index aa7eb7eb94..6828b60ec5 100644 --- a/openapi.json +++ b/openapi.json @@ -5645,6 +5645,7 @@ "oneOf": [ { "type": "object", + "title": "Default", "required": [ "type" ], @@ -5659,6 +5660,7 @@ }, { "type": "object", + "title": "Suffix", "required": [ "values", "type" @@ -5680,6 +5682,7 @@ }, { "type": "object", + "title": "Names", "required": [ "values", "type" From 9d1255a7a1c42d2133f3ccb6b07a987631154da5 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 5 Feb 2026 11:51:15 +0100 Subject: [PATCH 11/19] fix: add schema annotation to output_band in ExpressionParameters to prevent null values --- api/src/processes/processing.rs | 1 + openapi.json | 11 ++--------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index 453a24c3e0..53b530a091 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -110,6 +110,7 @@ pub struct ExpressionParameters { /// A raster data type for the output pub output_type: RasterDataType, /// Description about the output + #[schema(nullable = false /* cannot be null, but left out, avoids `Option>` in openapi client */)] pub output_band: Option, /// Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA. pub map_no_data: bool, diff --git a/openapi.json b/openapi.json index 6828b60ec5..0dc075deba 100644 --- a/openapi.json +++ b/openapi.json @@ -6540,15 +6540,8 @@ "description": "Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA." }, "outputBand": { - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/components/schemas/RasterBandDescriptor", - "description": "Description about the output" - } - ] + "$ref": "#/components/schemas/RasterBandDescriptor", + "description": "Description about the output" }, "outputType": { "$ref": "#/components/schemas/RasterDataType", From 4bd6c476d0f534a5457a928844e640ab35d62203 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 5 Feb 2026 14:34:45 +0100 Subject: [PATCH 12/19] fix: update OpenAPI documentation and schema annotations for Expression and GdalSource operators --- api/src/processes/processing.rs | 113 +++++++++++++++++---------- api/src/processes/source.rs | 55 +++++++------ openapi.json | 134 ++++++++++++++++++++++++++++---- 3 files changed, 219 insertions(+), 83 deletions(-) diff --git a/api/src/processes/processing.rs b/api/src/processes/processing.rs index 53b530a091..469db958b8 100644 --- a/api/src/processes/processing.rs +++ b/api/src/processes/processing.rs @@ -12,8 +12,6 @@ use geoengine_operators::processing::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -/// # Raster Expression -/// /// The `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source. /// The expression is specified as a user-defined script in a very simple language. /// The output is a raster time series with the result of the expression and with time intervals that are the same as for the inputs. @@ -92,6 +90,29 @@ use utoipa::ToSchema; #[type_tag(value = "Expression")] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema( + title = "Raster Expression", + examples(json!({ + "type": "Expression", + "params": { + "expression": "(A - B) / (A + B)", + "outputType": "F32", + "outputBand": { + "name": "NDVI", + "measurement": { "type": "unitless" }, + }, + "mapNoData": true + }, + "sources": { + "raster": { + "type": "GdalSource", + "params": { + "data": "ndvi" + } + } + } + })), +)] pub struct Expression { pub params: ExpressionParameters, pub sources: Box, @@ -106,13 +127,22 @@ pub struct ExpressionParameters { /// Expression script /// /// Example: `"(A - B) / (A + B)"` + #[schema(examples("(A - B) / (A + B)"))] pub expression: String, /// A raster data type for the output + #[schema(examples("F32"))] pub output_type: RasterDataType, /// Description about the output - #[schema(nullable = false /* cannot be null, but left out, avoids `Option>` in openapi client */)] + #[schema( + nullable = false /* cannot be null, but left out, avoids `Option>` in openapi client */, + examples(json!({ + "name": "NDVI", + "measurement": { "type": "unitless" }, + })) + )] pub output_band: Option, /// Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA. + #[schema(examples(true))] pub map_no_data: bool, } @@ -132,8 +162,6 @@ impl TryFrom for OperatorsExpression { } } -/// # RasterVectorJoin -/// /// The `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs. /// For each raster input, a new column is added to the collection from the vector input. /// The new column contains the value of the raster at the location of the vector feature. @@ -164,39 +192,37 @@ impl TryFrom for OperatorsExpression { /// /// If the length of `names` is not equal to the number of raster inputs, an error is thrown. /// -/// ## Example JSON -/// -/// ```json -/// { -/// "type": "RasterVectorJoin", -/// "params": { -/// "names": ["NDVI"], -/// "featureAggregation": "first", -/// "temporalAggregation": "mean", -/// "temporalAggregationIgnoreNoData": true -/// }, -/// "sources": { -/// "vector": { -/// "type": "OgrSource", -/// "params": { -/// "data": "places" -/// } -/// }, -/// "rasters": [ -/// { -/// "type": "GdalSource", -/// "params": { -/// "data": "ndvi" -/// } -/// } -/// ] -/// } -/// } -/// ``` - #[type_tag(value = "RasterVectorJoin")] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema( + title = "Raster Vector Join", + examples(json!({ + "type": "RasterVectorJoin", + "params": { + "names": ["NDVI"], + "featureAggregation": "first", + "temporalAggregation": "mean", + "temporalAggregationIgnoreNoData": true + }, + "sources": { + "vector": { + "type": "OgrSource", + "params": { + "data": "places" + } + }, + "rasters": [ + { + "type": "GdalSource", + "params": { + "data": "ndvi" + } + } + ] + } + })) +)] pub struct RasterVectorJoin { pub params: RasterVectorJoinParameters, pub sources: Box, @@ -209,22 +235,29 @@ pub struct RasterVectorJoinParameters { /// /// The `ColumnNames` type is used to specify how the new column names are derived from the raster band names. /// - /// | Value | Description | - /// | ---------------------------------------- | ---------------------------------------------------------------------------- | - /// | `{"type": "default"}` | Appends " (n)" to the band name with the smallest `n` that avoids a conflict | - /// | `{"type": "suffix", "values": [string]}` | Specifies a suffix for each input, to be appended to the band names | - /// | `{"type": "rename", "values": [string]}` | A list of names for each new column | + /// - **default**: Appends " (n)" to the band name with the smallest `n` that avoids a conflict. + /// - **suffix**: Specifies a suffix for each input, to be appended to the band names. + /// - **rename**: A list of names for each new column. /// + #[schema(examples( + json!({"type": "default"}), + json!({"type": "suffix", "values": ["_sentinel2"]}), + json!({"type": "rename", "values": ["red", "green", "blue"]}), + ))] pub names: ColumnNames, /// The aggregation function to use for features covering multiple pixels. + #[schema(examples("first"))] pub feature_aggregation: FeatureAggregationMethod, /// Whether to ignore no data values in the aggregation. Defaults to `false`. #[serde(default)] + #[schema(examples(true))] pub feature_aggregation_ignore_no_data: bool, /// The aggregation function to use for features covering multiple (raster) time steps. + #[schema(examples("mean"))] pub temporal_aggregation: TemporalAggregationMethod, /// Whether to ignore no data values in the aggregation. Defaults to `false`. #[serde(default)] + #[schema(examples(true))] pub temporal_aggregation_ignore_no_data: bool, } diff --git a/api/src/processes/source.rs b/api/src/processes/source.rs index 25d7dadfc5..24324da41a 100644 --- a/api/src/processes/source.rs +++ b/api/src/processes/source.rs @@ -13,8 +13,6 @@ use geoengine_operators::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -/// # GdalSource -/// /// The [`GdalSource`] is a source operator that reads raster data using GDAL. /// The counterpart for vector data is the [`OgrSource`]. /// @@ -22,19 +20,19 @@ use utoipa::ToSchema; /// /// If the given dataset does not exist or is not readable, an error is thrown. /// -/// ## Example JSON -/// -/// ```json -/// { -/// "type": "GdalSource", -/// "params": { -/// "data": "ndvi" -/// } -/// } -/// ``` #[type_tag(value = "GdalSource")] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema( + title = "GDAL Source", + examples(json!({ + "type": "GdalSource", + "params": { + "data": "ndvi", + "overviewLevel": null + } + })) +)] pub struct GdalSource { pub params: GdalSourceParameters, } @@ -44,14 +42,13 @@ pub struct GdalSource { #[serde(rename_all = "camelCase")] pub struct GdalSourceParameters { /// Dataset name or identifier to be loaded. - /// - /// ### Example - /// `"ndvi"` + #[schema(examples("ndvi"))] pub data: String, /// *Optional*: overview level to use. /// /// If not provided, the data source will determine the resolution, i.e., uses its native resolution. + #[schema(examples(3))] pub overview_level: Option, } @@ -69,22 +66,21 @@ impl TryFrom for OperatorsGdalSource { } } -/// # MockPointSource -/// /// The [`MockPointSource`] is a source operator that provides mock vector point data for testing and development purposes. /// -/// ## Example JSON -/// ```json -/// { -/// "type": "MockPointSource", -/// "params": { -/// "points": [ { "x": 1.0, "y": 2.0 }, { "x": 3.0, "y": 4.0 } ] -/// } -/// } -/// ``` #[type_tag(value = "MockPointSource")] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] #[serde(rename_all = "camelCase")] +#[schema( + title = "Mock Point Source", + examples(json!({ + "type": "MockPointSource", + "params": { + "points": [ { "x": 1.0, "y": 2.0 }, { "x": 3.0, "y": 4.0 } ], + "spatialBounds": { "type": "derive" } + } + })) +)] pub struct MockPointSource { pub params: MockPointSourceParameters, } @@ -95,13 +91,16 @@ pub struct MockPointSource { pub struct MockPointSourceParameters { /// Points to be output by the mock point source. /// - /// ### Example - /// `[{"x": 1.0, "y": 2.0}, {"x": 3.0, "y": 4.0}]` + #[schema(examples(json!([ + { "x": 1.0, "y": 2.0 }, + { "x": 3.0, "y": 4.0 } + ])))] pub points: Vec, /// Defines how the spatial bounds of the source are derived. /// /// Defaults to `None`. + #[schema(examples(json!({ "type": "derive" })))] pub spatial_bounds: SpatialBoundsDerive, } diff --git a/openapi.json b/openapi.json index 0dc075deba..37eaa3da25 100644 --- a/openapi.json +++ b/openapi.json @@ -6501,7 +6501,8 @@ }, "Expression": { "type": "object", - "description": "# Raster Expression\n\nThe `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source.\nThe expression is specified as a user-defined script in a very simple language.\nThe output is a raster time series with the result of the expression and with time intervals that are the same as for the inputs.\nUsers can specify an output data type.\nInternally, the expression is evaluated using floating-point numbers.\n\nAn example usage scenario is to calculate NDVI for a red and a near-infrared raster channel.\nThe expression uses a raster source with two bands, referred to as A and B, and calculates the formula `(A - B) / (A + B)`.\nWhen the temporal resolution is months, our output NDVI will also be a monthly time series.\n\n## Types\n\nThe following describes the types used in the parameters.\n\n### Expression\n\nExpressions are simple scripts to perform pixel-wise computations.\nOne can refer to the raster inputs as `A` for the first raster band, `B` for the second, and so on.\nFurthermore, expressions can check with `A IS NODATA`, `B IS NODATA`, etc. for NO DATA values.\nThis is important if `mapNoData` is set to true.\nOtherwise, NO DATA values are mapped automatically to the output NO DATA value.\nFinally, the value `NODATA` can be used to output NO DATA.\n\nUsers can think of this implicit function signature for, e.g., two inputs:\n\n```Rust\nfn (A: f64, B: f64) -> f64\n```\n\nAs a start, expressions contain algebraic operations and mathematical functions.\n\n```Rust\n(A + B) / 2\n```\n\nIn addition, branches can be used to check for conditions.\n\n```Rust\nif A IS NODATA {\n B\n} else {\n A\n}\n```\n\nFunction calls can be used to access utility functions.\n\n```Rust\nmax(A, 0)\n```\n\nCurrently, the following functions are available:\n\n- `abs(a)`: absolute value\n- `min(a, b)`, `min(a, b, c)`: minimum value\n- `max(a, b)`, `max(a, b, c)`: maximum value\n- `sqrt(a)`: square root\n- `ln(a)`: natural logarithm\n- `log10(a)`: base 10 logarithm\n- `cos(a)`, `sin(a)`, `tan(a)`, `acos(a)`, `asin(a)`, `atan(a)`: trigonometric functions\n- `pi()`, `e()`: mathematical constants\n- `round(a)`, `ceil(a)`, `floor(a)`: rounding functions\n- `mod(a, b)`: division remainder\n- `to_degrees(a)`, `to_radians(a)`: conversion to degrees or radians\n\nTo generate more complex expressions, it is possible to have variable assignments.\n\n```Rust\nlet mean = (A + B) / 2;\nlet coefficient = 0.357;\nmean * coefficient\n```\n\nNote, that all assignments are separated by semicolons.\nHowever, the last expression must be without a semicolon.", + "title": "Raster Expression", + "description": "The `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source.\nThe expression is specified as a user-defined script in a very simple language.\nThe output is a raster time series with the result of the expression and with time intervals that are the same as for the inputs.\nUsers can specify an output data type.\nInternally, the expression is evaluated using floating-point numbers.\n\nAn example usage scenario is to calculate NDVI for a red and a near-infrared raster channel.\nThe expression uses a raster source with two bands, referred to as A and B, and calculates the formula `(A - B) / (A + B)`.\nWhen the temporal resolution is months, our output NDVI will also be a monthly time series.\n\n## Types\n\nThe following describes the types used in the parameters.\n\n### Expression\n\nExpressions are simple scripts to perform pixel-wise computations.\nOne can refer to the raster inputs as `A` for the first raster band, `B` for the second, and so on.\nFurthermore, expressions can check with `A IS NODATA`, `B IS NODATA`, etc. for NO DATA values.\nThis is important if `mapNoData` is set to true.\nOtherwise, NO DATA values are mapped automatically to the output NO DATA value.\nFinally, the value `NODATA` can be used to output NO DATA.\n\nUsers can think of this implicit function signature for, e.g., two inputs:\n\n```Rust\nfn (A: f64, B: f64) -> f64\n```\n\nAs a start, expressions contain algebraic operations and mathematical functions.\n\n```Rust\n(A + B) / 2\n```\n\nIn addition, branches can be used to check for conditions.\n\n```Rust\nif A IS NODATA {\n B\n} else {\n A\n}\n```\n\nFunction calls can be used to access utility functions.\n\n```Rust\nmax(A, 0)\n```\n\nCurrently, the following functions are available:\n\n- `abs(a)`: absolute value\n- `min(a, b)`, `min(a, b, c)`: minimum value\n- `max(a, b)`, `max(a, b, c)`: maximum value\n- `sqrt(a)`: square root\n- `ln(a)`: natural logarithm\n- `log10(a)`: base 10 logarithm\n- `cos(a)`, `sin(a)`, `tan(a)`, `acos(a)`, `asin(a)`, `atan(a)`: trigonometric functions\n- `pi()`, `e()`: mathematical constants\n- `round(a)`, `ceil(a)`, `floor(a)`: rounding functions\n- `mod(a, b)`: division remainder\n- `to_degrees(a)`, `to_radians(a)`: conversion to degrees or radians\n\nTo generate more complex expressions, it is possible to have variable assignments.\n\n```Rust\nlet mean = (A + B) / 2;\nlet coefficient = 0.357;\nmean * coefficient\n```\n\nNote, that all assignments are separated by semicolons.\nHowever, the last expression must be without a semicolon.", "required": [ "type", "params", @@ -6520,7 +6521,31 @@ "Expression" ] } - } + }, + "examples": [ + { + "type": "Expression", + "params": { + "expression": "(A - B) / (A + B)", + "outputType": "F32", + "outputBand": { + "name": "NDVI", + "measurement": { + "type": "unitless" + } + }, + "mapNoData": true + }, + "sources": { + "raster": { + "type": "GdalSource", + "params": { + "data": "ndvi" + } + } + } + } + ] }, "ExpressionParameters": { "type": "object", @@ -6533,11 +6558,17 @@ "properties": { "expression": { "type": "string", - "description": "Expression script\n\nExample: `\"(A - B) / (A + B)\"`" + "description": "Expression script\n\nExample: `\"(A - B) / (A + B)\"`", + "examples": [ + "(A - B) / (A + B)" + ] }, "mapNoData": { "type": "boolean", - "description": "Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA." + "description": "Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA.", + "examples": [ + true + ] }, "outputBand": { "$ref": "#/components/schemas/RasterBandDescriptor", @@ -6951,7 +6982,8 @@ }, "GdalSource": { "type": "object", - "description": "# GdalSource\n\nThe [`GdalSource`] is a source operator that reads raster data using GDAL.\nThe counterpart for vector data is the [`OgrSource`].\n\n## Errors\n\nIf the given dataset does not exist or is not readable, an error is thrown.\n\n## Example JSON\n\n```json\n{\n \"type\": \"GdalSource\",\n \"params\": {\n \"data\": \"ndvi\"\n }\n}\n```", + "title": "GDAL Source", + "description": "The [`GdalSource`] is a source operator that reads raster data using GDAL.\nThe counterpart for vector data is the [`OgrSource`].\n\n## Errors\n\nIf the given dataset does not exist or is not readable, an error is thrown.\n", "required": [ "type", "params" @@ -6966,7 +6998,16 @@ "GdalSource" ] } - } + }, + "examples": [ + { + "type": "GdalSource", + "params": { + "data": "ndvi", + "overviewLevel": null + } + } + ] }, "GdalSourceParameters": { "type": "object", @@ -6977,7 +7018,10 @@ "properties": { "data": { "type": "string", - "description": "Dataset name or identifier to be loaded.\n\n### Example\n`\"ndvi\"`" + "description": "Dataset name or identifier to be loaded.", + "examples": [ + "ndvi" + ] }, "overviewLevel": { "type": [ @@ -6986,6 +7030,9 @@ ], "format": "int32", "description": "*Optional*: overview level to use.\n\nIf not provided, the data source will determine the resolution, i.e., uses its native resolution.", + "examples": [ + 3 + ], "minimum": 0 } } @@ -7808,7 +7855,8 @@ }, "MockPointSource": { "type": "object", - "description": "# MockPointSource\n\nThe [`MockPointSource`] is a source operator that provides mock vector point data for testing and development purposes.\n\n## Example JSON\n```json\n{\n \"type\": \"MockPointSource\",\n \"params\": {\n \"points\": [ { \"x\": 1.0, \"y\": 2.0 }, { \"x\": 3.0, \"y\": 4.0 } ]\n }\n}\n```", + "title": "Mock Point Source", + "description": "The [`MockPointSource`] is a source operator that provides mock vector point data for testing and development purposes.\n", "required": [ "type", "params" @@ -7823,7 +7871,27 @@ "MockPointSource" ] } - } + }, + "examples": [ + { + "type": "MockPointSource", + "params": { + "points": [ + { + "x": 1.0, + "y": 2.0 + }, + { + "x": 3.0, + "y": 4.0 + } + ], + "spatialBounds": { + "type": "derive" + } + } + } + ] }, "MockPointSourceParameters": { "type": "object", @@ -7838,7 +7906,7 @@ "items": { "$ref": "#/components/schemas/Coordinate2D" }, - "description": "Points to be output by the mock point source.\n\n### Example\n`[{\"x\": 1.0, \"y\": 2.0}, {\"x\": 3.0, \"y\": 4.0}]`" + "description": "Points to be output by the mock point source.\n" }, "spatialBounds": { "$ref": "#/components/schemas/SpatialBoundsDerive", @@ -9329,7 +9397,8 @@ }, "RasterVectorJoin": { "type": "object", - "description": "# RasterVectorJoin\n\nThe `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs.\nFor each raster input, a new column is added to the collection from the vector input.\nThe new column contains the value of the raster at the location of the vector feature.\nFor features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user.\nThe same is true if the temporal extent of a vector feature covers multiple raster time steps.\nMore details are described below.\n\n**Example**:\nYou have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value.\nFor your application, you want to know the NDVI value of each field.\nThe `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies.\nFor example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field.\nThis is useful for exploratory analysis since the computation is very fast.\nTo calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function.\nSince the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well.\nThe default is `none` which will create a new feature for each month.\nOther options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time.\n\n## Inputs\n\nThe `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs.\n\n| Parameter | Type |\n| --------- | ----------------------------------- |\n| `sources` | `SingleVectorMultipleRasterSources` |\n\n## Errors\n\nIf the length of `names` is not equal to the number of raster inputs, an error is thrown.\n\n## Example JSON\n\n```json\n{\n \"type\": \"RasterVectorJoin\",\n \"params\": {\n \"names\": [\"NDVI\"],\n \"featureAggregation\": \"first\",\n \"temporalAggregation\": \"mean\",\n \"temporalAggregationIgnoreNoData\": true\n },\n \"sources\": {\n \"vector\": {\n \"type\": \"OgrSource\",\n \"params\": {\n \"data\": \"places\"\n }\n },\n \"rasters\": [\n {\n \"type\": \"GdalSource\",\n \"params\": {\n \"data\": \"ndvi\"\n }\n }\n ]\n }\n}\n```", + "title": "Raster Vector Join", + "description": "The `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs.\nFor each raster input, a new column is added to the collection from the vector input.\nThe new column contains the value of the raster at the location of the vector feature.\nFor features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user.\nThe same is true if the temporal extent of a vector feature covers multiple raster time steps.\nMore details are described below.\n\n**Example**:\nYou have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value.\nFor your application, you want to know the NDVI value of each field.\nThe `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies.\nFor example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field.\nThis is useful for exploratory analysis since the computation is very fast.\nTo calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function.\nSince the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well.\nThe default is `none` which will create a new feature for each month.\nOther options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time.\n\n## Inputs\n\nThe `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs.\n\n| Parameter | Type |\n| --------- | ----------------------------------- |\n| `sources` | `SingleVectorMultipleRasterSources` |\n\n## Errors\n\nIf the length of `names` is not equal to the number of raster inputs, an error is thrown.\n", "required": [ "type", "params", @@ -9348,7 +9417,36 @@ "RasterVectorJoin" ] } - } + }, + "examples": [ + { + "type": "RasterVectorJoin", + "params": { + "names": [ + "NDVI" + ], + "featureAggregation": "first", + "temporalAggregation": "mean", + "temporalAggregationIgnoreNoData": true + }, + "sources": { + "vector": { + "type": "OgrSource", + "params": { + "data": "places" + } + }, + "rasters": [ + { + "type": "GdalSource", + "params": { + "data": "ndvi" + } + } + ] + } + } + ] }, "RasterVectorJoinParameters": { "type": "object", @@ -9364,11 +9462,14 @@ }, "featureAggregationIgnoreNoData": { "type": "boolean", - "description": "Whether to ignore no data values in the aggregation. Defaults to `false`." + "description": "Whether to ignore no data values in the aggregation. Defaults to `false`.", + "examples": [ + true + ] }, "names": { "$ref": "#/components/schemas/ColumnNames", - "description": "Specify how the new column names are derived from the raster band names.\n\nThe `ColumnNames` type is used to specify how the new column names are derived from the raster band names.\n\n| Value | Description |\n| ---------------------------------------- | ---------------------------------------------------------------------------- |\n| `{\"type\": \"default\"}` | Appends \" (n)\" to the band name with the smallest `n` that avoids a conflict |\n| `{\"type\": \"suffix\", \"values\": [string]}` | Specifies a suffix for each input, to be appended to the band names |\n| `{\"type\": \"rename\", \"values\": [string]}` | A list of names for each new column |\n" + "description": "Specify how the new column names are derived from the raster band names.\n\nThe `ColumnNames` type is used to specify how the new column names are derived from the raster band names.\n\n- **default**: Appends \" (n)\" to the band name with the smallest `n` that avoids a conflict.\n- **suffix**: Specifies a suffix for each input, to be appended to the band names.\n- **rename**: A list of names for each new column.\n" }, "temporalAggregation": { "$ref": "#/components/schemas/TemporalAggregationMethod", @@ -9376,7 +9477,10 @@ }, "temporalAggregationIgnoreNoData": { "type": "boolean", - "description": "Whether to ignore no data values in the aggregation. Defaults to `false`." + "description": "Whether to ignore no data values in the aggregation. Defaults to `false`.", + "examples": [ + true + ] } } }, From ce6c524dd6580562e5e7106ea0d32f25ba3a712f Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Thu, 5 Mar 2026 17:48:45 +0100 Subject: [PATCH 13/19] Refactor API structure and remove deprecated files - Deleted the README.md and lib.rs files from the api module as they are no longer needed. - Updated services/Cargo.toml to remove the geoengine-api dependency. - Refactored the API documentation to point to the new processingGraphs endpoint. - Introduced a new processing_graphs module with operators for raster and vector processing. - Added parameters and processing logic for Expression and RasterVectorJoin operators. - Implemented GdalSource and MockPointSource for data input handling. - Enhanced serialization and deserialization for new data types and structures. --- Cargo.lock | 19 ------------ Cargo.toml | 2 +- api/Cargo.toml | 30 ------------------- api/README.md | 4 --- api/src/lib.rs | 2 -- services/Cargo.toml | 1 - services/src/api/apidoc.rs | 2 +- services/src/api/model/mod.rs | 1 + .../src/api/model/processing_graphs}/mod.rs | 3 +- .../model/processing_graphs}/parameters.rs | 2 +- .../model/processing_graphs}/processing.rs | 17 ++++------- .../api/model/processing_graphs}/source.rs | 4 +-- 12 files changed, 14 insertions(+), 73 deletions(-) delete mode 100644 api/Cargo.toml delete mode 100644 api/README.md delete mode 100644 api/src/lib.rs rename {api/src/processes => services/src/api/model/processing_graphs}/mod.rs (98%) rename {api/src => services/src/api/model/processing_graphs}/parameters.rs (99%) rename {api/src/processes => services/src/api/model/processing_graphs}/processing.rs (96%) rename {api/src/processes => services/src/api/model/processing_graphs}/source.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 113e465104..f2617e875e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2548,24 +2548,6 @@ dependencies = [ "serde", ] -[[package]] -name = "geoengine-api" -version = "0.9.0" -dependencies = [ - "anyhow", - "futures", - "geoengine-datatypes", - "geoengine-macros", - "geoengine-operators", - "pretty_assertions", - "serde", - "serde_json", - "serde_with", - "tracing-opentelemetry", - "tracing-subscriber", - "utoipa", -] - [[package]] name = "geoengine-datatypes" version = "0.9.0" @@ -2727,7 +2709,6 @@ dependencies = [ "futures-util", "gdal", "geo 0.32.0", - "geoengine-api", "geoengine-datatypes", "geoengine-expression", "geoengine-macros", diff --git a/Cargo.toml b/Cargo.toml index d8b6ee8dd6..9d87b04afb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["api", "datatypes", "expression", "macros", "operators", "services"] +members = ["datatypes", "expression", "macros", "operators", "services"] exclude = [ "expression/deps-workspace", # Buggy, cf. https://github.com/rust-lang/cargo/issues/6745 ".scripts", diff --git a/api/Cargo.toml b/api/Cargo.toml deleted file mode 100644 index ea2a972bf5..0000000000 --- a/api/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "geoengine-api" -version.workspace = true -authors.workspace = true -edition.workspace = true -publish.workspace = true -license-file.workspace = true -documentation.workspace = true -repository.workspace = true -# build = "build.rs" -# default-run = "geoengine-server" - -[dependencies] -anyhow = { workspace = true } -futures = { workspace = true } -geoengine-datatypes = { path = "../datatypes" } -geoengine-macros = { path = "../macros" } -geoengine-operators = { path = "../operators" } -serde = { workspace = true } -serde_json = { workspace = true } -serde_with = { workspace = true } -tracing-opentelemetry = { workspace = true } -tracing-subscriber = { workspace = true } -utoipa = { workspace = true } - -[dev-dependencies] -pretty_assertions = { workspace = true } - -[lints] -workspace = true diff --git a/api/README.md b/api/README.md deleted file mode 100644 index e2acd7b98b..0000000000 --- a/api/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Geo Engine API - -This directory contains the API definition of the Geo Engine. -It is implemented in the [`geoengine-api`] crate. diff --git a/api/src/lib.rs b/api/src/lib.rs deleted file mode 100644 index 6c15f34f9b..0000000000 --- a/api/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod parameters; -pub mod processes; diff --git a/services/Cargo.toml b/services/Cargo.toml index 0389a487bc..112abf8b75 100644 --- a/services/Cargo.toml +++ b/services/Cargo.toml @@ -39,7 +39,6 @@ futures = { workspace = true } futures-util = { workspace = true } gdal = { workspace = true } geo = { workspace = true } -geoengine-api = { path = "../api" } # TODO: should be the other way around? geoengine-datatypes = { path = "../datatypes" } geoengine-expression = { path = "../expression" } geoengine-macros = { path = "../macros" } diff --git a/services/src/api/apidoc.rs b/services/src/api/apidoc.rs index eac988be44..a42e2b0abd 100644 --- a/services/src/api/apidoc.rs +++ b/services/src/api/apidoc.rs @@ -466,7 +466,7 @@ use utoipa::{Modify, OpenApi}; ), ), nest( - (path = "/process", api = geoengine_api::processes::OperatorsApi), + (path = "/processingGraphs", api = crate::api::model::processing_graphs::OperatorsApi), ), modifiers(&SecurityAddon, &ApiDocInfo, &OpenApiServerInfo, &DeriveDiscriminatorMapping), external_docs(url = "https://docs.geoengine.io", description = "Geo Engine Docs") diff --git a/services/src/api/model/mod.rs b/services/src/api/model/mod.rs index 71bf052020..6eff2eed21 100644 --- a/services/src/api/model/mod.rs +++ b/services/src/api/model/mod.rs @@ -1,4 +1,5 @@ pub mod datatypes; pub mod operators; +pub mod processing_graphs; pub mod responses; pub mod services; diff --git a/api/src/processes/mod.rs b/services/src/api/model/processing_graphs/mod.rs similarity index 98% rename from api/src/processes/mod.rs rename to services/src/api/model/processing_graphs/mod.rs index 2102e6461f..c79b90b19a 100644 --- a/api/src/processes/mod.rs +++ b/services/src/api/model/processing_graphs/mod.rs @@ -1,6 +1,6 @@ #![allow(clippy::needless_for_each)] // TODO: remove when clippy is fixed for utoipa -use crate::processes::{ +use crate::api::model::processing_graphs::{ processing::{Expression, ExpressionParameters, RasterVectorJoin, RasterVectorJoinParameters}, source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, }; @@ -18,6 +18,7 @@ use geoengine_operators::{ use serde::{Deserialize, Serialize}; use utoipa::{OpenApi, ToSchema}; +mod parameters; mod processing; mod source; diff --git a/api/src/parameters.rs b/services/src/api/model/processing_graphs/parameters.rs similarity index 99% rename from api/src/parameters.rs rename to services/src/api/model/processing_graphs/parameters.rs index fb0328571a..799b4eb80c 100644 --- a/api/src/parameters.rs +++ b/services/src/api/model/processing_graphs/parameters.rs @@ -1,4 +1,4 @@ -use crate::processes::{RasterOperator, VectorOperator}; +use crate::api::model::processing_graphs::{RasterOperator, VectorOperator}; use anyhow::Context; use geoengine_macros::type_tag; use serde::{Deserialize, Serialize, Serializer}; diff --git a/api/src/processes/processing.rs b/services/src/api/model/processing_graphs/processing.rs similarity index 96% rename from api/src/processes/processing.rs rename to services/src/api/model/processing_graphs/processing.rs index 469db958b8..ba6839b2bf 100644 --- a/api/src/processes/processing.rs +++ b/services/src/api/model/processing_graphs/processing.rs @@ -1,8 +1,7 @@ -use crate::parameters::{ - ColumnNames, FeatureAggregationMethod, SingleRasterSource, SingleVectorMultipleRasterSources, - TemporalAggregationMethod, +use crate::api::model::processing_graphs::parameters::{ + ColumnNames, FeatureAggregationMethod, RasterBandDescriptor, RasterDataType, + SingleRasterSource, SingleVectorMultipleRasterSources, TemporalAggregationMethod, }; -use crate::parameters::{RasterBandDescriptor, RasterDataType}; use geoengine_macros::type_tag; use geoengine_operators::processing::{ Expression as OperatorsExpression, ExpressionParams as OperatorsExpressionParameters, @@ -284,14 +283,10 @@ impl TryFrom for OperatorsRasterVectorJoin { mod tests { use super::*; - use crate::{ + use crate::api::model::processing_graphs::{ + RasterOperator, VectorOperator, parameters::{Coordinate2D, SpatialBoundsDerive}, - processes::{ - RasterOperator, VectorOperator, - source::{ - GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters, - }, - }, + source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, }; #[test] diff --git a/api/src/processes/source.rs b/services/src/api/model/processing_graphs/source.rs similarity index 97% rename from api/src/processes/source.rs rename to services/src/api/model/processing_graphs/source.rs index 24324da41a..56912a1d58 100644 --- a/api/src/processes/source.rs +++ b/services/src/api/model/processing_graphs/source.rs @@ -1,4 +1,4 @@ -use crate::parameters::{Coordinate2D, SpatialBoundsDerive}; +use crate::api::model::processing_graphs::parameters::{Coordinate2D, SpatialBoundsDerive}; use geoengine_datatypes::dataset::NamedData; use geoengine_macros::type_tag; use geoengine_operators::{ @@ -119,7 +119,7 @@ impl TryFrom for OperatorsMockPointSource { #[cfg(test)] mod tests { use super::*; - use crate::processes::{RasterOperator, TypedOperator, VectorOperator}; + use crate::api::model::processing_graphs::{RasterOperator, TypedOperator, VectorOperator}; use geoengine_operators::engine::TypedOperator as OperatorsTypedOperator; #[test] From d9b50cb86a668cf5515862d6360a18e20199a4dd Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Fri, 6 Mar 2026 15:37:29 +0100 Subject: [PATCH 14/19] refactor: remove 4th impl of coordinate2d --- .../api/model/processing_graphs/parameters.rs | 30 +++---------------- .../api/model/processing_graphs/processing.rs | 13 +++++--- .../src/api/model/processing_graphs/source.rs | 4 ++- 3 files changed, 16 insertions(+), 31 deletions(-) diff --git a/services/src/api/model/processing_graphs/parameters.rs b/services/src/api/model/processing_graphs/parameters.rs index 799b4eb80c..aabffbe91f 100644 --- a/services/src/api/model/processing_graphs/parameters.rs +++ b/services/src/api/model/processing_graphs/parameters.rs @@ -1,35 +1,13 @@ -use crate::api::model::processing_graphs::{RasterOperator, VectorOperator}; +use crate::api::model::{ + datatypes::Coordinate2D, + processing_graphs::{RasterOperator, VectorOperator}, +}; use anyhow::Context; use geoengine_macros::type_tag; use serde::{Deserialize, Serialize, Serializer}; use std::collections::BTreeMap; use utoipa::ToSchema; -/// A 2D coordinate with `x` and `y` values. -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, Default, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct Coordinate2D { - pub x: f64, - pub y: f64, -} -impl From for geoengine_datatypes::primitives::Coordinate2D { - fn from(value: Coordinate2D) -> Self { - geoengine_datatypes::primitives::Coordinate2D { - x: value.x, - y: value.y, - } - } -} - -impl From for Coordinate2D { - fn from(value: geoengine_datatypes::primitives::Coordinate2D) -> Self { - Coordinate2D { - x: value.x, - y: value.y, - } - } -} - /// A raster data type. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] diff --git a/services/src/api/model/processing_graphs/processing.rs b/services/src/api/model/processing_graphs/processing.rs index ba6839b2bf..178b3f9ac2 100644 --- a/services/src/api/model/processing_graphs/processing.rs +++ b/services/src/api/model/processing_graphs/processing.rs @@ -283,10 +283,15 @@ impl TryFrom for OperatorsRasterVectorJoin { mod tests { use super::*; - use crate::api::model::processing_graphs::{ - RasterOperator, VectorOperator, - parameters::{Coordinate2D, SpatialBoundsDerive}, - source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, + use crate::api::model::{ + datatypes::Coordinate2D, + processing_graphs::{ + RasterOperator, VectorOperator, + parameters::SpatialBoundsDerive, + source::{ + GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters, + }, + }, }; #[test] diff --git a/services/src/api/model/processing_graphs/source.rs b/services/src/api/model/processing_graphs/source.rs index 56912a1d58..2d2952987a 100644 --- a/services/src/api/model/processing_graphs/source.rs +++ b/services/src/api/model/processing_graphs/source.rs @@ -1,4 +1,6 @@ -use crate::api::model::processing_graphs::parameters::{Coordinate2D, SpatialBoundsDerive}; +use crate::api::model::{ + datatypes::Coordinate2D, processing_graphs::parameters::SpatialBoundsDerive, +}; use geoengine_datatypes::dataset::NamedData; use geoengine_macros::type_tag; use geoengine_operators::{ From fec6177974e13edb4dbfdc850f0cb19483a7bda7 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 9 Mar 2026 11:58:03 +0100 Subject: [PATCH 15/19] refactor: update Workflow usage to Legacy variant across multiple modules --- services/benches/quota_check.rs | 2 +- services/src/api/handlers/datasets.rs | 2 +- services/src/api/handlers/layers.rs | 23 ++++--- services/src/api/handlers/permissions.rs | 2 +- services/src/api/handlers/plots.rs | 8 +-- services/src/api/handlers/wcs.rs | 4 +- services/src/api/handlers/wfs.rs | 10 +-- services/src/api/handlers/wms.rs | 4 +- services/src/api/handlers/workflows.rs | 32 ++++----- .../src/api/model/processing_graphs/mod.rs | 16 +++-- services/src/cli/tile_import.rs | 2 +- services/src/contexts/postgres.rs | 24 +++---- services/src/datasets/create_from_workflow.rs | 2 +- .../src/datasets/dataset_listing_provider.rs | 2 +- services/src/datasets/external/aruna/mod.rs | 6 +- .../external/copernicus_dataspace/provider.rs | 2 +- services/src/datasets/external/edr.rs | 2 +- services/src/datasets/external/gbif.rs | 4 +- services/src/datasets/external/gfbio_abcd.rs | 2 +- .../datasets/external/gfbio_collections.rs | 2 +- .../src/datasets/external/netcdfcf/loading.rs | 2 +- .../external/sentinel_s2_l2a_cogs/mod.rs | 4 +- .../src/datasets/external/wildlive/mod.rs | 14 ++-- services/src/util/tests.rs | 4 +- services/src/util/workflows.rs | 2 +- services/src/workflows/workflow.rs | 67 ++++++++++++++----- 26 files changed, 145 insertions(+), 99 deletions(-) diff --git a/services/benches/quota_check.rs b/services/benches/quota_check.rs index 4aabc7b223..d497ad264e 100644 --- a/services/benches/quota_check.rs +++ b/services/benches/quota_check.rs @@ -40,7 +40,7 @@ async fn bench() { let (_, dataset) = add_ndvi_to_datasets2(&app_ctx, true, true).await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster( TemporalRasterAggregation { params: TemporalRasterAggregationParameters { diff --git a/services/src/api/handlers/datasets.rs b/services/src/api/handlers/datasets.rs index 2224804962..ff391cb067 100755 --- a/services/src/api/handlers/datasets.rs +++ b/services/src/api/handlers/datasets.rs @@ -3544,7 +3544,7 @@ mod tests { assert_eq!(res.status(), 200, "response: {res:?}"); // create workflow - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: geoengine_operators::engine::TypedOperator::Raster( MultiBandGdalSource { params: MultiBandGdalSourceParameters::new(dataset_name.into()), diff --git a/services/src/api/handlers/layers.rs b/services/src/api/handlers/layers.rs index 2bcc90346b..521927ae8b 100644 --- a/services/src/api/handlers/layers.rs +++ b/services/src/api/handlers/layers.rs @@ -1441,7 +1441,7 @@ mod tests { AddLayer { name: "Layer Name".to_string(), description: "Layer Description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: MockPointSource { params: MockPointSourceParams::new(vec![ (0.0, 0.1).into(), @@ -1570,7 +1570,7 @@ mod tests { name: "Foo".to_string(), description: "Bar".to_string(), properties: Default::default(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { @@ -1597,7 +1597,7 @@ mod tests { let update_layer = UpdateLayer { name: "Foo new".to_string(), description: "Bar new".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { @@ -1672,7 +1672,7 @@ mod tests { name: "Foo".to_string(), description: "Bar".to_string(), properties: Default::default(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { @@ -1722,7 +1722,7 @@ mod tests { name: "Foo".to_string(), description: "Bar".to_string(), properties: Default::default(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { @@ -1840,7 +1840,7 @@ mod tests { AddLayer { name: "Layer Name".to_string(), description: "Layer Description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: MockPointSource { params: MockPointSourceParams::new(vec![ (0.0, 0.1).into(), @@ -2618,11 +2618,11 @@ mod tests { .boxed(); let workflow = if time_shift_millis == 0 { - Workflow { + Workflow::Legacy { operator: raster_source.into(), } } else { - Workflow { + Workflow::Legacy { operator: TypedOperator::Raster(Box::new(TimeShift { params: TimeShiftParams::Relative { granularity: TimeGranularity::Millis, @@ -2761,7 +2761,12 @@ mod tests { }; // query the layer - let workflow_operator = mock_source.workflow.operator.get_raster().unwrap(); + let workflow_operator = mock_source + .workflow + .operator() + .unwrap() + .get_raster() + .unwrap(); // query the newly created dataset let dataset_operator = GdalSource { diff --git a/services/src/api/handlers/permissions.rs b/services/src/api/handlers/permissions.rs index b152222b29..3f9da0aefd 100644 --- a/services/src/api/handlers/permissions.rs +++ b/services/src/api/handlers/permissions.rs @@ -671,7 +671,7 @@ mod tests { let layer = AddLayer { name: "layer".to_string(), description: "description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { diff --git a/services/src/api/handlers/plots.rs b/services/src/api/handlers/plots.rs index a118bedb45..ec0a474841 100644 --- a/services/src/api/handlers/plots.rs +++ b/services/src/api/handlers/plots.rs @@ -111,7 +111,7 @@ async fn get_plot_handler( let workflow_id = WorkflowId(id.into_inner()); let workflow = ctx.db().load_workflow(&workflow_id).await?; - let operator = workflow.operator.get_plot()?; + let operator = workflow.operator()?.get_plot()?; let execution_context = ctx.execution_context()?; @@ -287,7 +287,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: Statistics { params: StatisticsParams { column_names: vec![], @@ -353,7 +353,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: Histogram { params: HistogramParams { attribute_name: "band".to_string(), @@ -487,7 +487,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: Statistics { params: StatisticsParams { column_names: vec![], diff --git a/services/src/api/handlers/wcs.rs b/services/src/api/handlers/wcs.rs index 689d273bf8..6c0db1896d 100644 --- a/services/src/api/handlers/wcs.rs +++ b/services/src/api/handlers/wcs.rs @@ -243,7 +243,7 @@ async fn wcs_describe_coverage( let workflow_operator_path_root = WorkflowOperatorPath::initialize_root(); let operator = workflow - .operator + .operator()? .get_raster()? .initialize(workflow_operator_path_root, &exe_ctx) .await?; @@ -387,7 +387,7 @@ async fn wcs_get_coverage( let workflow = ctx.db().load_workflow(&identifier).await?; - let operator = workflow.operator.get_raster()?; + let operator = workflow.operator()?.get_raster()?; let execution_context = ctx.execution_context()?; diff --git a/services/src/api/handlers/wfs.rs b/services/src/api/handlers/wfs.rs index 53cff55f73..0675ff3d86 100644 --- a/services/src/api/handlers/wfs.rs +++ b/services/src/api/handlers/wfs.rs @@ -340,7 +340,7 @@ where let workflow_operator_path_root = WorkflowOperatorPath::initialize_root(); let operator = workflow - .operator + .operator()? .get_vector()? .initialize(workflow_operator_path_root, &exe_ctx) .await?; @@ -494,7 +494,7 @@ async fn wfs_get_feature( let workflow: Workflow = ctx.db().load_workflow(&type_names).await?; - let operator = workflow.operator.get_vector()?; + let operator = workflow.operator()?.get_vector()?; let execution_context = ctx.execution_context()?; let workflow_operator_path_root = WorkflowOperatorPath::initialize_root(); @@ -840,7 +840,7 @@ x;y let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector(Box::new(CsvSource { params: CsvSourceParameters { file_path: temp_file.path().into(), @@ -910,7 +910,7 @@ x;y let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector(Box::new(CsvSource { params: CsvSourceParameters { file_path: temp_file.path().into(), @@ -1032,7 +1032,7 @@ x;y let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector(Box::new(CsvSource { params: CsvSourceParameters { file_path: temp_file.path().into(), diff --git a/services/src/api/handlers/wms.rs b/services/src/api/handlers/wms.rs index 23b7460e4a..5435b810f6 100644 --- a/services/src/api/handlers/wms.rs +++ b/services/src/api/handlers/wms.rs @@ -214,7 +214,7 @@ where let workflow_operator_path_root = WorkflowOperatorPath::initialize_root(); let operator = workflow - .operator + .operator()? .get_raster()? .initialize(workflow_operator_path_root, &exe_ctx) .await?; @@ -344,7 +344,7 @@ async fn wms_get_map( let workflow_id = WorkflowId::from_str(&request.layers)?; let workflow = ctx.db().load_workflow(&workflow_id).await?; - let operator = workflow.operator.get_raster()?; + let operator = workflow.operator()?.get_raster()?; let execution_context = ctx.execution_context()?; diff --git a/services/src/api/handlers/workflows.rs b/services/src/api/handlers/workflows.rs index 418e9459cd..d009526b0f 100755 --- a/services/src/api/handlers/workflows.rs +++ b/services/src/api/handlers/workflows.rs @@ -204,7 +204,7 @@ async fn workflow_metadata( let workflow_operator_path_root = WorkflowOperatorPath::initialize_root(); let result_descriptor: geoengine_operators::engine::TypedResultDescriptor = call_on_typed_operator!( - workflow.operator, + workflow.operator()?, operator => { let operator = operator .initialize(workflow_operator_path_root, &execution_context).await @@ -262,7 +262,7 @@ async fn workflow_provenance( let db = ctx.db(); let execution_ctx = ctx.execution_context()?; - let data_names = workflow.operator.data_names(); + let data_names = workflow.operator()?.data_names(); let mut datasets = Vec::::with_capacity(data_names.len()); for data_name in data_names { let data_id = execution_ctx.resolve_named_data(&data_name).await?; @@ -519,7 +519,7 @@ async fn raster_stream_websocket( let workflow = ctx.db().load_workflow(&workflow_id).await?; let operator = workflow - .operator + .operator()? .get_raster() .boxed_context(error::WorkflowMustBeOfTypeRaster)?; @@ -645,7 +645,7 @@ async fn vector_stream_websocket( let workflow = ctx.db().load_workflow(&workflow_id).await?; let operator = workflow - .operator + .operator()? .get_vector() .boxed_context(error::WorkflowMustBeOfTypeVector)?; @@ -718,6 +718,8 @@ pub enum WorkflowApiError { WorkflowMustBeOfTypeRaster { source: Box }, #[snafu(display("You can only query a vector stream for a vector workflow"))] WorkflowMustBeOfTypeVector { source: Box }, + #[snafu(display("Unsupported operator type in workflow: {source}"))] + EngineTypeConversion { source: anyhow::Error }, } #[cfg(test)] @@ -790,7 +792,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: MockPointSource { params: MockPointSourceParams::new(vec![(0.0, 0.1).into(), (1.0, 1.1).into()]), } @@ -828,7 +830,7 @@ mod tests { #[ge_context::test] async fn register_missing_header(app_ctx: PostgresContext) { - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: MockPointSource { params: MockPointSourceParams::new(vec![(0.0, 0.1).into(), (1.0, 1.1).into()]), } @@ -993,7 +995,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: MockFeatureCollectionSource::single( MultiPointCollection::from_data( MultiPoint::many(vec![(0.0, 0.1)]).unwrap(), @@ -1063,7 +1065,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: MockRasterSource:: { params: MockRasterSourceParams:: { data: vec![], @@ -1156,7 +1158,7 @@ mod tests { let session = app_ctx.create_anonymous_session().await.unwrap(); let ctx = app_ctx.session_context(session.clone()); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: MockFeatureCollectionSource::single( MultiPointCollection::from_data( MultiPoint::many(vec![(0.0, 0.1)]).unwrap(), @@ -1197,7 +1199,7 @@ mod tests { let session_id = session.id(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: Statistics { params: StatisticsParams { column_names: vec![], @@ -1241,7 +1243,7 @@ mod tests { let session_id = session.id(); let (dataset_id, dataset) = add_ndvi_to_datasets(&app_ctx).await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters::new(dataset), @@ -1344,7 +1346,7 @@ mod tests { let (dataset_id, dataset_name) = add_ndvi_to_datasets(&app_ctx).await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters::new(dataset_name.clone()), @@ -1473,7 +1475,7 @@ mod tests { } .boxed(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster(operator_a.clone()), }; @@ -1569,7 +1571,7 @@ mod tests { let (_, dataset) = add_ndvi_to_datasets(&app_ctx).await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters { @@ -1648,7 +1650,7 @@ mod tests { let (_, dataset) = add_ports_to_datasets(&app_ctx, true, true).await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( OgrSource { params: OgrSourceParameters { diff --git a/services/src/api/model/processing_graphs/mod.rs b/services/src/api/model/processing_graphs/mod.rs index c79b90b19a..388c09b9a9 100644 --- a/services/src/api/model/processing_graphs/mod.rs +++ b/services/src/api/model/processing_graphs/mod.rs @@ -1,9 +1,9 @@ #![allow(clippy::needless_for_each)] // TODO: remove when clippy is fixed for utoipa -use crate::api::model::processing_graphs::{ - processing::{Expression, ExpressionParameters, RasterVectorJoin, RasterVectorJoinParameters}, - source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, -}; +// use crate::api::model::processing_graphs::{ +// processing::{Expression, ExpressionParameters, RasterVectorJoin, RasterVectorJoinParameters}, +// source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, +// }; use geoengine_operators::{ engine::{ RasterOperator as OperatorsRasterOperator, TypedOperator as OperatorsTypedOperator, @@ -22,6 +22,14 @@ mod parameters; mod processing; mod source; +// TODO: avoid exporting them to outside of API module +#[cfg(test)] +pub(crate) use crate::api::model::processing_graphs::parameters::SpatialBoundsDerive; +pub(crate) use crate::api::model::processing_graphs::{ + processing::{Expression, ExpressionParameters, RasterVectorJoin, RasterVectorJoinParameters}, + source::{GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters}, +}; + /// Operator outputs are distinguished by their data type. /// There are `raster`, `vector` and `plot` operators. #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)] diff --git a/services/src/cli/tile_import.rs b/services/src/cli/tile_import.rs index 07a511551b..b1f2cc049c 100644 --- a/services/src/cli/tile_import.rs +++ b/services/src/cli/tile_import.rs @@ -223,7 +223,7 @@ async fn add_dataset_to_collection( let add_layer = AddLayer { name: layer_name.to_string(), description: String::new(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: geoengine_operators::engine::TypedOperator::Raster( MultiBandGdalSource { params: MultiBandGdalSourceParameters { diff --git a/services/src/contexts/postgres.rs b/services/src/contexts/postgres.rs index 92696a0946..62392425a3 100644 --- a/services/src/contexts/postgres.rs +++ b/services/src/contexts/postgres.rs @@ -730,7 +730,7 @@ mod tests { .unwrap(); let layer_workflow_id = db - .register_workflow(Workflow { + .register_workflow(Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), @@ -744,7 +744,7 @@ mod tests { assert!(db.load_workflow(&layer_workflow_id).await.is_ok()); let plot_workflow_id = db - .register_workflow(Workflow { + .register_workflow(Workflow::Legacy { operator: Statistics { params: StatisticsParams { column_names: vec![], @@ -974,7 +974,7 @@ mod tests { #[ge_context::test] async fn it_persists_workflows(app_ctx: PostgresContext) { - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), @@ -1761,7 +1761,7 @@ mod tests { let layer_db = app_ctx.session_context(session).db(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), @@ -1959,7 +1959,7 @@ mod tests { let layer_db = app_ctx.session_context(session).db(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), @@ -2315,7 +2315,7 @@ mod tests { let user_session = app_ctx.create_anonymous_session().await.unwrap(); let user_layer_db = app_ctx.session_context(user_session.clone()).db(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { @@ -2838,7 +2838,7 @@ mod tests { let layer_db = app_ctx.session_context(session).db(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), @@ -3018,7 +3018,7 @@ mod tests { let user_session = app_ctx.create_anonymous_session().await.unwrap(); let user_layer_db = app_ctx.session_context(user_session.clone()).db(); - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams { @@ -3555,7 +3555,7 @@ mod tests { let layer = AddLayer { name: "layer".to_string(), description: "description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), @@ -3751,7 +3751,7 @@ mod tests { AddLayer { name: "layer".to_string(), description: "description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![ @@ -3821,7 +3821,7 @@ mod tests { AddLayer { name: "layer 1".to_string(), description: "description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![ @@ -3846,7 +3846,7 @@ mod tests { AddLayer { name: "layer 2".to_string(), description: "description".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( MockPointSource { params: MockPointSourceParams::new(vec![ diff --git a/services/src/datasets/create_from_workflow.rs b/services/src/datasets/create_from_workflow.rs index b4b6f2bca5..f84a2449b2 100644 --- a/services/src/datasets/create_from_workflow.rs +++ b/services/src/datasets/create_from_workflow.rs @@ -120,7 +120,7 @@ impl RasterDatasetFromWorkflowTask { let initialized_operator = workflow .clone() - .operator + .operator()? .get_raster() .expect("must be raster here") .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx) diff --git a/services/src/datasets/dataset_listing_provider.rs b/services/src/datasets/dataset_listing_provider.rs index eebb176b90..f3746eb763 100644 --- a/services/src/datasets/dataset_listing_provider.rs +++ b/services/src/datasets/dataset_listing_provider.rs @@ -277,7 +277,7 @@ where }, name: dataset.display_name, description: dataset.description, - workflow: Workflow { operator }, + workflow: Workflow::Legacy { operator }, symbology: dataset.symbology, properties: vec![], metadata: HashMap::new(), diff --git a/services/src/datasets/external/aruna/mod.rs b/services/src/datasets/external/aruna/mod.rs index 60e903ef52..d945509324 100644 --- a/services/src/datasets/external/aruna/mod.rs +++ b/services/src/datasets/external/aruna/mod.rs @@ -918,7 +918,7 @@ impl LayerCollectionProvider for ArunaDataProvider { }, name: dataset.name, description: dataset.description, - workflow: Workflow { operator }, + workflow: Workflow::Legacy { operator }, symbology: None, properties: vec![], metadata: HashMap::new(), @@ -2004,7 +2004,7 @@ mod tests { } } }), - serde_json::to_value(&result.workflow.operator).unwrap() + serde_json::to_value(result.workflow.operator().unwrap()).unwrap() ); } @@ -2041,7 +2041,7 @@ mod tests { } } }), - serde_json::to_value(&result.workflow.operator).unwrap() + serde_json::to_value(result.workflow.operator().unwrap()).unwrap() ); } diff --git a/services/src/datasets/external/copernicus_dataspace/provider.rs b/services/src/datasets/external/copernicus_dataspace/provider.rs index d59f03c4d9..e69c549f1b 100644 --- a/services/src/datasets/external/copernicus_dataspace/provider.rs +++ b/services/src/datasets/external/copernicus_dataspace/provider.rs @@ -300,7 +300,7 @@ impl CopernicusDataspaceDataProvider { id.product_band.band_name() ), description: String::new(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters { diff --git a/services/src/datasets/external/edr.rs b/services/src/datasets/external/edr.rs index 25ff48ce87..55f9bfccc7 100644 --- a/services/src/datasets/external/edr.rs +++ b/services/src/datasets/external/edr.rs @@ -1036,7 +1036,7 @@ impl LayerCollectionProvider for EdrDataProvider { }, name: collection.title.unwrap_or(collection.id), description: String::new(), - workflow: Workflow { operator }, + workflow: Workflow::Legacy { operator }, symbology: None, // TODO properties: vec![], metadata: HashMap::new(), diff --git a/services/src/datasets/external/gbif.rs b/services/src/datasets/external/gbif.rs index fdeee22037..6d49549259 100644 --- a/services/src/datasets/external/gbif.rs +++ b/services/src/datasets/external/gbif.rs @@ -787,7 +787,7 @@ impl LayerCollectionProvider for GbifDataProvider { }, name: canonicalname.to_string(), description: format!("All occurrences with a {taxonrank} of {canonicalname}"), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( OgrSource { params: OgrSourceParameters { @@ -3616,7 +3616,7 @@ mod tests { }, name: "Rhipidia willistoniana".to_string(), description: "All occurrences with a species of Rhipidia willistoniana".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( OgrSource { params: OgrSourceParameters { diff --git a/services/src/datasets/external/gfbio_abcd.rs b/services/src/datasets/external/gfbio_abcd.rs index b35042fd14..9912ff169d 100644 --- a/services/src/datasets/external/gfbio_abcd.rs +++ b/services/src/datasets/external/gfbio_abcd.rs @@ -352,7 +352,7 @@ impl LayerCollectionProvider for GfbioAbcdDataProvider { }, name: row.get(0), description: row.try_get(1).unwrap_or_else(|_| String::new()), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( OgrSource { params: OgrSourceParameters { diff --git a/services/src/datasets/external/gfbio_collections.rs b/services/src/datasets/external/gfbio_collections.rs index fa828cb65b..2d94454dca 100644 --- a/services/src/datasets/external/gfbio_collections.rs +++ b/services/src/datasets/external/gfbio_collections.rs @@ -683,7 +683,7 @@ impl LayerCollectionProvider for GfbioCollectionsDataProvider { }, name: layer.name, description: String::new(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Vector( OgrSource { params: OgrSourceParameters { diff --git a/services/src/datasets/external/netcdfcf/loading.rs b/services/src/datasets/external/netcdfcf/loading.rs index 3af2de9156..439d1c1a5e 100644 --- a/services/src/datasets/external/netcdfcf/loading.rs +++ b/services/src/datasets/external/netcdfcf/loading.rs @@ -145,7 +145,7 @@ pub fn create_layer( id: provider_layer_id, name: netcdf_entity.name.clone(), description: netcdf_entity.name, - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters::new(data_id), diff --git a/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs b/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs index f4d1237c46..3708c6a68f 100644 --- a/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs +++ b/services/src/datasets/external/sentinel_s2_l2a_cogs/mod.rs @@ -238,7 +238,7 @@ impl SentinelS2L2aCogsDataProvider { }, name: format!("Sentinel S2 L2A COGS {}:{} ({})", zone, band.long_name(), band.name()), description: String::new(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: source_operator_from_dataset( GdalSource::TYPE_NAME, &NamedData { @@ -373,7 +373,7 @@ impl LayerCollectionProvider for SentinelS2L2aCogsDataProvider { }, name: dataset.listing.name.clone(), description: dataset.listing.description.clone(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters::new(NamedData { diff --git a/services/src/datasets/external/wildlive/mod.rs b/services/src/datasets/external/wildlive/mod.rs index 5b28543f51..6a36bed18d 100644 --- a/services/src/datasets/external/wildlive/mod.rs +++ b/services/src/datasets/external/wildlive/mod.rs @@ -364,7 +364,7 @@ impl LayerCollectionProvider for WildliveDataConnector { id: self.layer_id(WildliveLayerId::Projects)?, name: "Projects".to_string(), description: "Overview of all projects".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: VectorExpression { params: VectorExpressionParams { expression: "centroid(geom)".into(), @@ -432,7 +432,7 @@ impl LayerCollectionProvider for WildliveDataConnector { id: self.layer_id(WildliveLayerId::ProjectBounds)?, name: "Project Bounds".to_string(), description: "Overview of all project bounds".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: OgrSource { params: OgrSourceParameters { data: self.named_data(WildliveLayerId::ProjectBounds)?, @@ -462,7 +462,7 @@ impl LayerCollectionProvider for WildliveDataConnector { })?, name: format!("Stations for project {project_name}"), description: format!("Overview of all stations within project {project_id}"), - workflow: Workflow { + workflow: Workflow::Legacy { operator: OgrSource { params: OgrSourceParameters { data: self.named_data(WildliveLayerId::Stations { project_id })?, @@ -493,7 +493,7 @@ impl LayerCollectionProvider for WildliveDataConnector { })?, name: format!("Captures for project {project_name}"), description: format!("Overview of all captures within project {project_id}"), - workflow: Workflow { + workflow: Workflow::Legacy { operator: OgrSource { params: OgrSourceParameters { data: self.named_data(WildliveLayerId::Captures { project_id })?, @@ -1207,7 +1207,7 @@ mod tests { }, name: "Project Bounds".to_string(), description: "Overview of all project bounds".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: OgrSource { params: OgrSourceParameters { data: connector @@ -1239,7 +1239,7 @@ mod tests { }, name: "Projects".to_string(), description: "Overview of all projects".to_string(), - workflow: Workflow { + workflow: Workflow::Legacy { operator: VectorExpression { params: VectorExpressionParams { expression: "centroid(geom)".into(), @@ -1578,7 +1578,7 @@ mod tests { }, name: format!("Captures for project {project_name}"), description: format!("Overview of all captures within project {project_id}"), - workflow: Workflow { + workflow: Workflow::Legacy { operator: OgrSource { params: OgrSourceParameters { data: connector diff --git a/services/src/util/tests.rs b/services/src/util/tests.rs index 42dc4e00e6..ed7510faf8 100644 --- a/services/src/util/tests.rs +++ b/services/src/util/tests.rs @@ -148,7 +148,7 @@ pub async fn register_ndvi_workflow_helper_with_cache_ttl( ) -> (Workflow, WorkflowId) { let (_, dataset) = add_ndvi_to_datasets_with_cache_ttl(app_ctx, cache_ttl).await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster( GdalSource { params: GdalSourceParameters::new(dataset), @@ -366,7 +366,7 @@ pub async fn register_ne2_multiband_workflow( ) .await; - let workflow = Workflow { + let workflow = Workflow::Legacy { operator: TypedOperator::Raster( RasterStacker { params: RasterStackerParams { diff --git a/services/src/util/workflows.rs b/services/src/util/workflows.rs index 736c34021b..da94e3f27f 100644 --- a/services/src/util/workflows.rs +++ b/services/src/util/workflows.rs @@ -8,7 +8,7 @@ pub async fn validate_workflow( ) -> Result<()> { let workflow_operator_path_root = WorkflowOperatorPath::initialize_root(); - match workflow.clone().operator { + match workflow.clone().operator()? { TypedOperator::Vector(o) => { o.initialize(workflow_operator_path_root, execution_context) .await?; diff --git a/services/src/workflows/workflow.rs b/services/src/workflows/workflow.rs index 7f1d09b24c..db20fa55e6 100644 --- a/services/src/workflows/workflow.rs +++ b/services/src/workflows/workflow.rs @@ -1,10 +1,11 @@ +use crate::api::{handlers::workflows::WorkflowApiError, model::processing_graphs::TypedOperator}; +use crate::error::Result; +use crate::identifier; +use geoengine_operators::engine::TypedOperator as OperatorsTypedOperator; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; -use crate::identifier; -use geoengine_operators::engine::TypedOperator; - identifier!(WorkflowId); impl WorkflowId { @@ -20,10 +21,18 @@ impl WorkflowId { } #[derive(Clone, Debug, Serialize, Deserialize, ToSchema)] -pub struct Workflow { - #[serde(flatten)] - #[schema(value_type = crate::api::model::operators::TypedOperator)] - pub operator: TypedOperator, +#[serde(untagged)] +pub enum Workflow { + Typed { + #[serde(flatten)] + operator: TypedOperator, + }, + // TODO: remove this variant when all workflows are migrated to typed ones + Legacy { + #[serde(flatten)] + #[schema(value_type = crate::api::model::operators::TypedOperator)] + operator: OperatorsTypedOperator, + }, } impl PartialEq for Workflow { @@ -35,22 +44,42 @@ impl PartialEq for Workflow { } } +impl Workflow { + pub fn operator(&self) -> Result { + match self { + Workflow::Typed { operator } => { + operator + .clone() + .try_into() + .map_err(|source: anyhow::Error| crate::error::Error::WorkflowApi { + source: WorkflowApiError::EngineTypeConversion { source }, + }) + } + Workflow::Legacy { operator } => Ok(operator.clone()), + } + } +} + #[cfg(test)] mod tests { use super::*; - use geoengine_datatypes::primitives::Coordinate2D; - use geoengine_operators::engine::VectorOperator; - use geoengine_operators::mock::{MockPointSource, MockPointSourceParams}; + use crate::api::model::{ + datatypes::Coordinate2D, + processing_graphs::{ + MockPointSource, MockPointSourceParameters, SpatialBoundsDerive, VectorOperator, + }, + }; #[test] fn serde() { - let workflow = Workflow { - operator: TypedOperator::Vector( - MockPointSource { - params: MockPointSourceParams::new(vec![Coordinate2D::new(1., 2.); 3]), - } - .boxed(), - ), + let workflow = Workflow::Typed { + operator: TypedOperator::Vector(VectorOperator::MockPointSource(MockPointSource { + r#type: Default::default(), + params: MockPointSourceParameters { + points: vec![Coordinate2D { x: 1., y: 2. }; 3], + spatial_bounds: SpatialBoundsDerive::None(Default::default()), + }, + })), }; let serialized_workflow = serde_json::to_value(&workflow).unwrap(); @@ -80,6 +109,8 @@ mod tests { }) ); - // TODO: check deserialization + let deserialized_workflow: Workflow = serde_json::from_value(serialized_workflow).unwrap(); + + assert_eq!(workflow, deserialized_workflow); } } From c3d11c3a4e15e3e8dd0593e63054db497504d163 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 9 Mar 2026 11:58:52 +0100 Subject: [PATCH 16/19] build: add justfile for build, lint, run, and test commands --- justfile | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 justfile diff --git a/justfile b/justfile new file mode 100644 index 0000000000..6c000ed6fc --- /dev/null +++ b/justfile @@ -0,0 +1,41 @@ +_default: + @just --list + +# Generate OpenAPI specification as JSON file. The file is generated in the current directory. +[group("build")] +generate-openapi-spec: + @-clear + cargo run --bin geoengine-cli -- openapi > openapi.json + +# Run lints. +[group("lint")] +lint: + @-clear + just _lint-clippy + +_lint-clippy: + cargo clippy --all-features --all-targets + +# Run clippy for all features and all targets. +[group("lint")] +lint-clippy: + @-clear + just _lint-clippy + +# Run the application. +[group("run")] +run: + @-clear + cargo run + +# Run the tests. Optionally, a filter can be provided to run only a subset of the tests. +[group("test")] +test filter="": + @-clear + cargo test -- {{ filter }} --nocapture + +# Run the tests for the geoengine-services package. Optionally, a filter can be provided to run only a subset of the tests. Example: just test-services workflows::workflow::tests +[group("test")] +test-services filter="": + @-clear + cargo test --package geoengine-services -- {{ filter }} --nocapture \ No newline at end of file From 20b34df7f87e45ec42c15065ef9155eaedd6fc95 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 9 Mar 2026 12:03:38 +0100 Subject: [PATCH 17/19] refactor: rename TypedOperator to LegacyTypedOperator and update references in API and workflows --- openapi.json | 171 ++++++++++++++++++++-------- services/src/api/apidoc.rs | 14 +-- services/src/api/model/operators.rs | 6 +- services/src/workflows/workflow.rs | 2 +- 4 files changed, 134 insertions(+), 59 deletions(-) diff --git a/openapi.json b/openapi.json index 37eaa3da25..2f34636d8c 100644 --- a/openapi.json +++ b/openapi.json @@ -5551,6 +5551,7 @@ }, "ClassificationMeasurement": { "type": "object", + "description": "A classification measurement.\nIt contains a mapping from class IDs to class names.", "required": [ "type", "measurement", @@ -5734,6 +5735,7 @@ }, "ContinuousMeasurement": { "type": "object", + "description": "A continuous measurement, e.g., \"temperature\".\nIt may have an optional unit, e.g., \"°C\" for degrees Celsius.", "required": [ "type", "measurement" @@ -7468,6 +7470,58 @@ } } }, + "LegacyTypedOperator": { + "type": "object", + "description": "An enum to differentiate between `Operator` variants", + "required": [ + "type", + "operator" + ], + "properties": { + "operator": { + "type": "object", + "required": [ + "type" + ], + "properties": { + "params": { + "type": "object" + }, + "sources": { + "type": "object" + }, + "type": { + "type": "string" + } + } + }, + "type": { + "type": "string", + "enum": [ + "Vector", + "Raster", + "Plot" + ] + } + }, + "examples": [ + { + "type": "MockPointSource", + "params": { + "points": [ + { + "x": 0.0, + "y": 0.1 + }, + { + "x": 1.0, + "y": 1.1 + } + ] + } + } + ] + }, "LineSymbology": { "type": "object", "required": [ @@ -7578,6 +7632,7 @@ "$ref": "#/components/schemas/ClassificationMeasurement" } ], + "description": "Measurement information for a raster band.", "discriminator": { "propertyName": "type", "mapping": { @@ -9193,17 +9248,18 @@ }, "RasterDataType": { "type": "string", + "description": "A raster data type.", "enum": [ - "U8", - "U16", - "U32", - "U64", - "I8", - "I16", - "I32", - "I64", - "F32", - "F64" + "u8", + "u16", + "u32", + "u64", + "i8", + "i16", + "i32", + "i64", + "f32", + "f64" ] }, "RasterDatasetFromWorkflow": { @@ -10563,56 +10619,63 @@ ] }, "TypedOperator": { - "type": "object", - "description": "An enum to differentiate between `Operator` variants", - "required": [ - "type", - "operator" - ], - "properties": { - "operator": { + "oneOf": [ + { "type": "object", "required": [ + "operator", "type" ], "properties": { - "params": { - "type": "object" + "operator": { + "$ref": "#/components/schemas/VectorOperator" }, - "sources": { - "type": "object" + "type": { + "type": "string", + "enum": [ + "Vector" + ] + } + } + }, + { + "type": "object", + "required": [ + "operator", + "type" + ], + "properties": { + "operator": { + "$ref": "#/components/schemas/RasterOperator" }, "type": { - "type": "string" + "type": "string", + "enum": [ + "Raster" + ] } } }, - "type": { - "type": "string", - "enum": [ - "Vector", - "Raster", - "Plot" - ] - } - }, - "examples": [ { - "type": "MockPointSource", - "params": { - "points": [ - { - "x": 0.0, - "y": 0.1 - }, - { - "x": 1.0, - "y": 1.1 - } - ] + "type": "object", + "required": [ + "operator", + "type" + ], + "properties": { + "operator": { + "$ref": "#/components/schemas/PlotOperator" + }, + "type": { + "type": "string", + "enum": [ + "Plot" + ] + } } } - ] + ], + "description": "Operator outputs are distinguished by their data type.\nThere are `raster`, `vector` and `plot` operators." }, "TypedPlotResultDescriptor": { "allOf": [ @@ -10700,6 +10763,7 @@ }, "UnitlessMeasurement": { "type": "object", + "description": "A measurement without a unit.", "required": [ "type" ], @@ -11391,9 +11455,20 @@ ] }, "Workflow": { - "allOf": [ + "oneOf": [ + { + "allOf": [ + { + "$ref": "#/components/schemas/TypedOperator" + } + ] + }, { - "$ref": "#/components/schemas/TypedOperator" + "allOf": [ + { + "$ref": "#/components/schemas/LegacyTypedOperator" + } + ] } ] }, diff --git a/services/src/api/apidoc.rs b/services/src/api/apidoc.rs index a42e2b0abd..8fa75a5720 100644 --- a/services/src/api/apidoc.rs +++ b/services/src/api/apidoc.rs @@ -23,12 +23,12 @@ use crate::api::model::datatypes::{ use crate::api::model::operators::{ CsvHeader, FileNotFoundHandling, FormatSpecifics, GdalDatasetParameters, GdalLoadingInfoTemporalSlice, GdalMetaDataList, GdalMetaDataRegular, GdalMetaDataStatic, - GdalMetadataMapping, GdalMetadataNetCdfCf, GdalSourceTimePlaceholder, MlModelMetadata, - MockDatasetDataSourceLoadingInfo, MockMetaData, OgrMetaData, OgrSourceColumnSpec, - OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceDurationSpec, OgrSourceErrorSpec, - OgrSourceTimeFormat, PlotResultDescriptor, RasterBandDescriptor, RasterBandDescriptors, - RasterResultDescriptor, TimeReference, TypedGeometry, TypedOperator, TypedResultDescriptor, - UnixTimeStampType, VectorColumnInfo, VectorResultDescriptor, + GdalMetadataMapping, GdalMetadataNetCdfCf, GdalSourceTimePlaceholder, LegacyTypedOperator, + MlModelMetadata, MockDatasetDataSourceLoadingInfo, MockMetaData, OgrMetaData, + OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceDurationSpec, + OgrSourceErrorSpec, OgrSourceTimeFormat, PlotResultDescriptor, RasterBandDescriptor, + RasterBandDescriptors, RasterResultDescriptor, TimeReference, TypedGeometry, + TypedResultDescriptor, UnixTimeStampType, VectorColumnInfo, VectorResultDescriptor, }; use crate::api::model::responses::datasets::DatasetNameResponse; use crate::api::model::responses::ml_models::MlModelNameResponse; @@ -275,7 +275,7 @@ use utoipa::{Modify, OpenApi}; ServerInfo, Workflow, - TypedOperator, + LegacyTypedOperator, TypedResultDescriptor, PlotResultDescriptor, RasterResultDescriptor, diff --git a/services/src/api/model/operators.rs b/services/src/api/model/operators.rs index ecd71ed2bd..e439d80fa7 100644 --- a/services/src/api/model/operators.rs +++ b/services/src/api/model/operators.rs @@ -245,13 +245,13 @@ impl From for geoengine_operators::engine::RasterResultD /// An enum to differentiate between `Operator` variants #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(tag = "type", content = "operator")] -pub enum TypedOperator { +pub enum LegacyTypedOperator { Vector(Box), Raster(Box), Plot(Box), } -impl PartialSchema for TypedOperator { +impl PartialSchema for LegacyTypedOperator { fn schema() -> utoipa::openapi::RefOr { use utoipa::openapi::schema::{Object, ObjectBuilder, SchemaType, Type}; ObjectBuilder::new() @@ -288,7 +288,7 @@ impl PartialSchema for TypedOperator { } } -impl ToSchema for TypedOperator {} +impl ToSchema for LegacyTypedOperator {} #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] diff --git a/services/src/workflows/workflow.rs b/services/src/workflows/workflow.rs index db20fa55e6..03b63f2104 100644 --- a/services/src/workflows/workflow.rs +++ b/services/src/workflows/workflow.rs @@ -30,7 +30,7 @@ pub enum Workflow { // TODO: remove this variant when all workflows are migrated to typed ones Legacy { #[serde(flatten)] - #[schema(value_type = crate::api::model::operators::TypedOperator)] + #[schema(value_type = crate::api::model::operators::LegacyTypedOperator)] operator: OperatorsTypedOperator, }, } From 002d2c647517990efee70c3144ba8a1c4d9e437e Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Mon, 9 Mar 2026 17:05:31 +0100 Subject: [PATCH 18/19] refactor: update visit_schema and visit_response functions to include already_visited parameter for recursion prevention --- services/src/api/handlers/workflows.rs | 2 +- services/src/util/openapi_visitor.rs | 106 +++++++++++++++++++++---- services/src/util/openapi_visitors.rs | 16 +++- 3 files changed, 106 insertions(+), 18 deletions(-) diff --git a/services/src/api/handlers/workflows.rs b/services/src/api/handlers/workflows.rs index d009526b0f..b55ced6771 100755 --- a/services/src/api/handlers/workflows.rs +++ b/services/src/api/handlers/workflows.rs @@ -898,7 +898,7 @@ mod tests { res, 400, "BodyDeserializeError", - "Error in user input: missing field `type` at line 1 column 2", + "Error in user input: data did not match any variant of untagged enum Workflow", ) .await; } diff --git a/services/src/util/openapi_visitor.rs b/services/src/util/openapi_visitor.rs index bb8a73242b..83e3eb601d 100644 --- a/services/src/util/openapi_visitor.rs +++ b/services/src/util/openapi_visitor.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use utoipa::openapi::{ Components, HttpMethod, OpenApi, PathItem, Ref, RefOr, Response, Schema, path::Operation, @@ -26,36 +28,61 @@ pub fn visit_schema( components: &Components, visitor: &mut T, source_location: &str, + already_visited: &mut HashSet, ) { match schema { RefOr::Ref(reference) => { - visit_reference(reference, components, visitor, source_location); + visit_reference( + reference, + components, + visitor, + source_location, + already_visited, + ); } RefOr::T(concrete) => match concrete { Schema::Array(arr) => { if let ArrayItems::RefOrSchema(schema) = &arr.items { - visit_schema(schema, components, visitor, source_location); + visit_schema( + schema, + components, + visitor, + source_location, + already_visited, + ); } } Schema::Object(obj) => { for property in obj.properties.values() { - visit_schema(property, components, visitor, source_location); + visit_schema( + property, + components, + visitor, + source_location, + already_visited, + ); } if let Some(additional_properties) = &obj.additional_properties && let AdditionalProperties::RefOr(properties_schema) = additional_properties.as_ref() { - visit_schema(properties_schema, components, visitor, source_location); + visit_schema( + properties_schema, + components, + visitor, + source_location, + already_visited, + ); } } Schema::OneOf(oo) => { for item in &oo.items { - visit_schema(item, components, visitor, source_location); + visit_schema(item, components, visitor, source_location, already_visited); } } Schema::AllOf(ao) => { for item in &ao.items { - visit_schema(item, components, visitor, source_location); + visit_schema(item, components, visitor, source_location, already_visited); } } _ => panic!("Unknown schema type"), @@ -73,17 +100,30 @@ fn visit_response( components: &Components, visitor: &mut T, source_location: &str, + already_visited: &mut HashSet, ) { match response { RefOr::Ref(reference) => { - visit_reference(reference, components, visitor, source_location); + visit_reference( + reference, + components, + visitor, + source_location, + already_visited, + ); } RefOr::T(concrete) => { for content in concrete.content.values() { let Some(content_schema) = &content.schema else { continue; }; - visit_schema(content_schema, components, visitor, source_location); + visit_schema( + content_schema, + components, + visitor, + source_location, + already_visited, + ); } } } @@ -100,6 +140,7 @@ fn visit_reference( components: &Components, visitor: &mut T, source_location: &str, + already_visited: &mut HashSet, ) { const SCHEMA_REF_PREFIX: &str = "#/components/schemas/"; const RESPONSE_REF_PREFIX: &str = "#/components/responses/"; @@ -111,13 +152,18 @@ fn visit_reference( None => visitor.resolve_failed(ref_location), Some(resolved) => { visitor.visit_schema_component(schema_name, resolved, source_location); - visit_schema(resolved, components, visitor, ref_location); + if !already_visited.insert(ref_location.to_string()) { + return; // prevent infinite recursion + } + visit_schema(resolved, components, visitor, ref_location, already_visited); } } } else if let Some(response_name) = ref_location.strip_prefix(RESPONSE_REF_PREFIX) { match components.responses.get(response_name) { None => visitor.resolve_failed(ref_location), - Some(resolved) => visit_response(resolved, components, visitor, ref_location), + Some(resolved) => { + visit_response(resolved, components, visitor, ref_location, already_visited); + } } } else { visitor.resolve_failed(ref_location); @@ -141,7 +187,13 @@ pub fn visit_api(api: &OpenApi, visitor: &mut T) { if let Some(parameters) = &path_item.parameters { for parameter in parameters { if let Some(schema) = parameter.schema.as_ref() { - visit_schema(schema, components, visitor, source_location); + visit_schema( + schema, + components, + visitor, + source_location, + &mut Default::default(), + ); } } } @@ -152,14 +204,26 @@ pub fn visit_api(api: &OpenApi, visitor: &mut T) { let Some(content_schema) = &content.schema else { continue; }; - visit_schema(content_schema, components, visitor, source_location); + visit_schema( + content_schema, + components, + visitor, + source_location, + &mut Default::default(), + ); } } if let Some(parameters) = operation.parameters.as_ref() { for parameter in parameters { if let Some(schema) = parameter.schema.as_ref() { - visit_schema(schema, components, visitor, source_location); + visit_schema( + schema, + components, + visitor, + source_location, + &mut Default::default(), + ); } } } @@ -167,14 +231,26 @@ pub fn visit_api(api: &OpenApi, visitor: &mut T) { for response in operation.responses.responses.values() { match response { RefOr::Ref(reference) => { - visit_reference(reference, components, visitor, source_location); + visit_reference( + reference, + components, + visitor, + source_location, + &mut Default::default(), + ); } RefOr::T(concrete) => { for content in concrete.content.values() { let Some(content_schema) = &content.schema else { continue; }; - visit_schema(content_schema, components, visitor, source_location); + visit_schema( + content_schema, + components, + visitor, + source_location, + &mut Default::default(), + ); } } } diff --git a/services/src/util/openapi_visitors.rs b/services/src/util/openapi_visitors.rs index 075317cf37..cf506921b8 100644 --- a/services/src/util/openapi_visitors.rs +++ b/services/src/util/openapi_visitors.rs @@ -79,7 +79,13 @@ mod tests { fn try_resolve_schema(schema: &RefOr, components: &Components) { let mut visitor = CanResolveVisitor { unknown_ref: None }; - visit_schema(schema, components, &mut visitor, "root"); + visit_schema( + schema, + components, + &mut visitor, + "root", + &mut Default::default(), + ); if let Some(unknown_ref) = visitor.unknown_ref { panic!("Cannot resolve reference {unknown_ref}"); @@ -93,7 +99,13 @@ mod tests { let mut visitor = SchemaUseCounter { parents: HashMap::new(), }; - visit_schema(schema, components, &mut visitor, "root"); + visit_schema( + schema, + components, + &mut visitor, + "root", + &mut Default::default(), + ); visitor.get_schema_use_counts() } From d90c741522e09970434bb65ff50a609c2d18b833 Mon Sep 17 00:00:00 2001 From: Christian Beilschmidt Date: Wed, 11 Mar 2026 09:00:23 +0100 Subject: [PATCH 19/19] build: update dependencies to latest versions in Cargo.lock --- Cargo.lock | 59 ++++++++++++++++++++---------------------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09d294c67..0fbe6ce45b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -806,9 +806,9 @@ dependencies = [ [[package]] name = "assert_cmd" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c5bcfa8749ac45dd12cb11055aeeb6b27a3895560d60d71e3c23bf979e60514" +checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" dependencies = [ "anstyle", "bstr", @@ -3529,9 +3529,9 @@ dependencies = [ [[package]] name = "image" -version = "0.25.9" +version = "0.25.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" dependencies = [ "bytemuck", "byteorder-lite", @@ -3547,8 +3547,8 @@ dependencies = [ "rayon", "rgb", "tiff", - "zune-core 0.5.1", - "zune-jpeg 0.5.12", + "zune-core", + "zune-jpeg", ] [[package]] @@ -4219,9 +4219,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.11" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" dependencies = [ "num-traits", "pxfm", @@ -5748,9 +5748,9 @@ dependencies = [ [[package]] name = "ravif" -version = "0.12.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" dependencies = [ "avif-serialize", "imgref", @@ -6260,9 +6260,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" dependencies = [ "windows-sys 0.61.2", ] @@ -6949,9 +6949,9 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.26.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", "getrandom 0.4.2", @@ -7017,23 +7017,23 @@ dependencies = [ [[package]] name = "tiff" -version = "0.10.3" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" dependencies = [ "fax", "flate2", "half", "quick-error", "weezl", - "zune-jpeg 0.4.21", + "zune-jpeg", ] [[package]] name = "time" -version = "0.3.46" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", @@ -7055,9 +7055,9 @@ checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.26" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -8779,12 +8779,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "zune-core" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" - [[package]] name = "zune-core" version = "0.5.1" @@ -8800,20 +8794,11 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "zune-jpeg" -version = "0.4.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" -dependencies = [ - "zune-core 0.4.12", -] - [[package]] name = "zune-jpeg" version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" dependencies = [ - "zune-core 0.5.1", + "zune-core", ]