• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geo-engine / geoengine / 30617186306

31 Jul 2026 08:41AM UTC coverage: 87.684% (+0.04%) from 87.647%
30617186306

push

github

web-flow
feat: add VectorExpression operator to processing graph api (#1221)

* feat: add VectorExpression operator to processing graph api

* fix: update code blocks in documentation to ignore syntax highlighting

* fix: remove double type in MetaDataDefinition

* fix: update TypedResultDescriptor to use untagged serde representation

* fix: data id type

* fix: examples and default for VectorParams

250 of 259 new or added lines in 4 files covered. (96.53%)

124739 of 142260 relevant lines covered (87.68%)

476948.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

63.73
/geoengine/services/src/api/model/processing_graphs/parameters.rs
1
use crate::api::model::datatypes::{Coordinate2D, VectorDataType};
2
use anyhow::Context;
3
use geoengine_macros::type_tag;
4
use serde::{Deserialize, Serialize, Serializer};
5
use std::collections::BTreeMap;
6
use utoipa::ToSchema;
7

8
/// A raster data type.
9
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
10
pub enum RasterDataType {
11
    U8,
12
    U16,
13
    U32,
14
    U64,
15
    I8,
16
    I16,
17
    I32,
18
    I64,
19
    F32,
20
    F64,
21
}
22

23
impl From<geoengine_datatypes::raster::RasterDataType> for RasterDataType {
24
    fn from(value: geoengine_datatypes::raster::RasterDataType) -> Self {
1✔
25
        match value {
1✔
26
            geoengine_datatypes::raster::RasterDataType::U8 => Self::U8,
×
27
            geoengine_datatypes::raster::RasterDataType::U16 => Self::U16,
×
28
            geoengine_datatypes::raster::RasterDataType::U32 => Self::U32,
×
29
            geoengine_datatypes::raster::RasterDataType::U64 => Self::U64,
×
30
            geoengine_datatypes::raster::RasterDataType::I8 => Self::I8,
×
31
            geoengine_datatypes::raster::RasterDataType::I16 => Self::I16,
×
32
            geoengine_datatypes::raster::RasterDataType::I32 => Self::I32,
×
33
            geoengine_datatypes::raster::RasterDataType::I64 => Self::I64,
×
34
            geoengine_datatypes::raster::RasterDataType::F32 => Self::F32,
1✔
35
            geoengine_datatypes::raster::RasterDataType::F64 => Self::F64,
×
36
        }
37
    }
1✔
38
}
39

40
impl From<RasterDataType> for geoengine_datatypes::raster::RasterDataType {
41
    fn from(value: RasterDataType) -> Self {
5✔
42
        match value {
5✔
43
            RasterDataType::U8 => Self::U8,
×
44
            RasterDataType::U16 => Self::U16,
2✔
45
            RasterDataType::U32 => Self::U32,
×
46
            RasterDataType::U64 => Self::U64,
×
47
            RasterDataType::I8 => Self::I8,
×
48
            RasterDataType::I16 => Self::I16,
×
49
            RasterDataType::I32 => Self::I32,
×
50
            RasterDataType::I64 => Self::I64,
×
51
            RasterDataType::F32 => Self::F32,
3✔
52
            RasterDataType::F64 => Self::F64,
×
53
        }
54
    }
5✔
55
}
56

57
/// Measurement information for a raster band.
58
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
59
#[serde(rename_all = "camelCase", untagged)]
60
#[schema(
61
    discriminator = "type",
62
    examples(
63
        json!({"type": "unitless"}),
64
        json!({"type": "continuous", "measurement": "length", "unit": "m"}),
65
        json!({"type": "classification", "measurement": "severity", "classes": ["low", "medium", "high"]}),
66
    ),
67
)]
68
pub enum Measurement {
69
    Unitless(UnitlessMeasurement),
70
    Continuous(ContinuousMeasurement),
71
    Classification(ClassificationMeasurement),
72
}
73

74
impl Measurement {
NEW
75
    pub fn unitless() -> Self {
×
NEW
76
        Self::Unitless(Default::default())
×
NEW
77
    }
×
78
}
79

80
impl From<geoengine_datatypes::primitives::Measurement> for Measurement {
81
    fn from(value: geoengine_datatypes::primitives::Measurement) -> Self {
1✔
82
        match value {
1✔
83
            geoengine_datatypes::primitives::Measurement::Unitless => {
84
                Self::Unitless(UnitlessMeasurement {
1✔
85
                    r#type: Default::default(),
1✔
86
                })
1✔
87
            }
88
            geoengine_datatypes::primitives::Measurement::Continuous(cm) => {
×
89
                Self::Continuous(cm.into())
×
90
            }
91
            geoengine_datatypes::primitives::Measurement::Classification(cm) => {
×
92
                Self::Classification(cm.into())
×
93
            }
94
        }
95
    }
1✔
96
}
97

98
impl From<Measurement> for geoengine_datatypes::primitives::Measurement {
99
    fn from(value: Measurement) -> Self {
7✔
100
        match value {
7✔
101
            Measurement::Unitless(_) => Self::Unitless,
4✔
102
            Measurement::Continuous(cm) => Self::Continuous(cm.into()),
2✔
103
            Measurement::Classification(cm) => Self::Classification(cm.into()),
1✔
104
        }
105
    }
7✔
106
}
107

108
/// A measurement without a unit.
109
#[type_tag(value = "unitless")]
110
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema, Default)]
111
pub struct UnitlessMeasurement {}
112

113
/// A continuous measurement, e.g., "temperature".
114
/// It may have an optional unit, e.g., "°C" for degrees Celsius.
115
#[type_tag(value = "continuous")]
116
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
117
pub struct ContinuousMeasurement {
118
    pub measurement: String,
119
    pub unit: Option<String>,
120
}
121

122
impl From<geoengine_datatypes::primitives::ContinuousMeasurement> for ContinuousMeasurement {
123
    fn from(value: geoengine_datatypes::primitives::ContinuousMeasurement) -> Self {
×
124
        ContinuousMeasurement {
×
125
            r#type: Default::default(),
×
126
            measurement: value.measurement,
×
127
            unit: value.unit,
×
128
        }
×
129
    }
×
130
}
131

132
impl From<ContinuousMeasurement> for geoengine_datatypes::primitives::ContinuousMeasurement {
133
    fn from(value: ContinuousMeasurement) -> Self {
2✔
134
        Self {
2✔
135
            measurement: value.measurement,
2✔
136
            unit: value.unit,
2✔
137
        }
2✔
138
    }
2✔
139
}
140

141
impl From<geoengine_datatypes::primitives::ClassificationMeasurement>
142
    for ClassificationMeasurement
143
{
144
    fn from(value: geoengine_datatypes::primitives::ClassificationMeasurement) -> Self {
×
145
        let mut classes = BTreeMap::new();
×
146
        for (k, v) in value.classes {
×
147
            classes.insert(k, v);
×
148
        }
×
149
        ClassificationMeasurement {
×
150
            r#type: Default::default(),
×
151
            measurement: value.measurement,
×
152
            classes,
×
153
        }
×
154
    }
×
155
}
156

157
impl From<ClassificationMeasurement>
158
    for geoengine_datatypes::primitives::ClassificationMeasurement
159
{
160
    fn from(value: ClassificationMeasurement) -> Self {
1✔
161
        geoengine_datatypes::primitives::ClassificationMeasurement {
1✔
162
            measurement: value.measurement,
1✔
163
            classes: value.classes,
1✔
164
        }
1✔
165
    }
1✔
166
}
167

168
/// A classification measurement.
169
/// It contains a mapping from class IDs to class names.
170
#[type_tag(value = "classification")]
171
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
172
pub struct ClassificationMeasurement {
173
    pub measurement: String,
174
    #[serde(serialize_with = "serialize_classes")]
175
    #[serde(deserialize_with = "deserialize_classes")]
176
    pub classes: BTreeMap<u8, String>,
177
}
178

179
fn serialize_classes<S>(classes: &BTreeMap<u8, String>, serializer: S) -> Result<S::Ok, S::Error>
1✔
180
where
1✔
181
    S: Serializer,
1✔
182
{
183
    use serde::ser::SerializeMap;
184

185
    let mut map = serializer.serialize_map(Some(classes.len()))?;
1✔
186
    for (k, v) in classes {
3✔
187
        map.serialize_entry(&k.to_string(), v)?;
3✔
188
    }
189
    map.end()
1✔
190
}
1✔
191

192
fn deserialize_classes<'de, D>(deserializer: D) -> Result<BTreeMap<u8, String>, D::Error>
1✔
193
where
1✔
194
    D: serde::de::Deserializer<'de>,
1✔
195
{
196
    use serde::de::{MapAccess, Visitor};
197
    use std::fmt;
198

199
    struct ClassesVisitor;
200

201
    impl<'de> Visitor<'de> for ClassesVisitor {
202
        type Value = BTreeMap<u8, String>;
203

204
        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
×
205
            formatter.write_str("a map with numeric string keys")
×
206
        }
×
207

208
        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
1✔
209
        where
1✔
210
            M: MapAccess<'de>,
1✔
211
        {
212
            let mut map = BTreeMap::new();
1✔
213
            while let Some((key, value)) = access.next_entry::<String, String>()? {
4✔
214
                let k = key.parse::<u8>().map_err(serde::de::Error::custom)?;
3✔
215
                map.insert(k, value);
3✔
216
            }
217
            Ok(map)
1✔
218
        }
1✔
219
    }
220

221
    deserializer.deserialize_map(ClassesVisitor)
1✔
222
}
1✔
223

224
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema)]
225
#[serde(rename_all = "camelCase")]
226
pub struct RasterBandDescriptor {
227
    pub name: String,
228
    pub measurement: Measurement,
229
}
230

231
impl From<geoengine_operators::engine::RasterBandDescriptor> for RasterBandDescriptor {
232
    fn from(value: geoengine_operators::engine::RasterBandDescriptor) -> Self {
1✔
233
        Self {
1✔
234
            name: value.name,
1✔
235
            measurement: value.measurement.into(),
1✔
236
        }
1✔
237
    }
1✔
238
}
239

240
impl From<RasterBandDescriptor> for geoengine_operators::engine::RasterBandDescriptor {
241
    fn from(value: RasterBandDescriptor) -> Self {
2✔
242
        Self {
2✔
243
            name: value.name,
2✔
244
            measurement: value.measurement.into(),
2✔
245
        }
2✔
246
    }
2✔
247
}
248

249
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
250
#[serde(rename_all = "camelCase", tag = "type")]
251
pub enum ColumnNames {
252
    #[schema(title = "Default")]
253
    Default,
254
    #[schema(title = "Suffix")]
255
    Suffix { values: Vec<String> },
256
    #[schema(title = "Names")]
257
    Names { values: Vec<String> },
258
}
259

260
impl From<geoengine_operators::processing::ColumnNames> for ColumnNames {
261
    fn from(value: geoengine_operators::processing::ColumnNames) -> Self {
×
262
        match value {
×
263
            geoengine_operators::processing::ColumnNames::Default => ColumnNames::Default,
×
264
            geoengine_operators::processing::ColumnNames::Suffix(v) => {
×
265
                ColumnNames::Suffix { values: v }
×
266
            }
267
            geoengine_operators::processing::ColumnNames::Names(v) => {
×
268
                ColumnNames::Names { values: v }
×
269
            }
270
        }
271
    }
×
272
}
273

274
impl From<ColumnNames> for geoengine_operators::processing::ColumnNames {
275
    fn from(value: ColumnNames) -> Self {
2✔
276
        match value {
2✔
277
            ColumnNames::Default => geoengine_operators::processing::ColumnNames::Default,
×
278
            ColumnNames::Suffix { values } => {
×
279
                geoengine_operators::processing::ColumnNames::Suffix(values)
×
280
            }
281
            ColumnNames::Names { values } => {
2✔
282
                geoengine_operators::processing::ColumnNames::Names(values)
2✔
283
            }
284
        }
285
    }
2✔
286
}
287

288
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
289
#[serde(rename_all = "camelCase")]
290
pub enum FeatureAggregationMethod {
291
    First,
292
    Mean,
293
}
294

295
impl From<geoengine_operators::processing::FeatureAggregationMethod> for FeatureAggregationMethod {
296
    fn from(value: geoengine_operators::processing::FeatureAggregationMethod) -> Self {
×
297
        match value {
×
298
            geoengine_operators::processing::FeatureAggregationMethod::First => {
299
                FeatureAggregationMethod::First
×
300
            }
301
            geoengine_operators::processing::FeatureAggregationMethod::Mean => {
302
                FeatureAggregationMethod::Mean
×
303
            }
304
        }
305
    }
×
306
}
307

308
impl From<FeatureAggregationMethod> for geoengine_operators::processing::FeatureAggregationMethod {
309
    fn from(value: FeatureAggregationMethod) -> Self {
2✔
310
        match value {
2✔
311
            FeatureAggregationMethod::First => {
312
                geoengine_operators::processing::FeatureAggregationMethod::First
2✔
313
            }
314
            FeatureAggregationMethod::Mean => {
315
                geoengine_operators::processing::FeatureAggregationMethod::Mean
×
316
            }
317
        }
318
    }
2✔
319
}
320

321
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
322
#[serde(rename_all = "camelCase")]
323
pub enum TemporalAggregationMethod {
324
    None,
325
    First,
326
    Mean,
327
}
328

329
impl From<geoengine_operators::processing::TemporalAggregationMethod>
330
    for TemporalAggregationMethod
331
{
332
    fn from(value: geoengine_operators::processing::TemporalAggregationMethod) -> Self {
×
333
        match value {
×
334
            geoengine_operators::processing::TemporalAggregationMethod::None => {
335
                TemporalAggregationMethod::None
×
336
            }
337
            geoengine_operators::processing::TemporalAggregationMethod::First => {
338
                TemporalAggregationMethod::First
×
339
            }
340
            geoengine_operators::processing::TemporalAggregationMethod::Mean => {
341
                TemporalAggregationMethod::Mean
×
342
            }
343
        }
344
    }
×
345
}
346

347
impl From<TemporalAggregationMethod>
348
    for geoengine_operators::processing::TemporalAggregationMethod
349
{
350
    fn from(value: TemporalAggregationMethod) -> Self {
2✔
351
        match value {
2✔
352
            TemporalAggregationMethod::None => {
353
                geoengine_operators::processing::TemporalAggregationMethod::None
×
354
            }
355
            TemporalAggregationMethod::First => {
356
                geoengine_operators::processing::TemporalAggregationMethod::First
1✔
357
            }
358
            TemporalAggregationMethod::Mean => {
359
                geoengine_operators::processing::TemporalAggregationMethod::Mean
1✔
360
            }
361
        }
362
    }
2✔
363
}
364

365
/// Spatial bounds derivation options for the [`MockPointSource`].
366
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
367
#[serde(rename_all = "camelCase", untagged)]
368
#[schema(discriminator = "type")]
369
pub enum SpatialBoundsDerive {
370
    Derive(SpatialBoundsDeriveDerive),
371
    Bounds(SpatialBoundsDeriveBounds),
372
    None(SpatialBoundsDeriveNone),
373
}
374

375
impl Default for SpatialBoundsDerive {
376
    fn default() -> Self {
×
377
        SpatialBoundsDerive::None(SpatialBoundsDeriveNone::default())
×
378
    }
×
379
}
380

381
#[type_tag(value = "derive")]
382
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
383
pub struct SpatialBoundsDeriveDerive {}
384

385
#[type_tag(value = "bounds")]
386
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
387
pub struct SpatialBoundsDeriveBounds {
388
    #[serde(flatten)]
389
    pub bounding_box: BoundingBox2D,
390
}
391

392
#[type_tag(value = "none")]
393
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
394
pub struct SpatialBoundsDeriveNone {}
395

396
impl TryFrom<SpatialBoundsDerive> for geoengine_operators::mock::SpatialBoundsDerive {
397
    type Error = anyhow::Error;
398
    fn try_from(value: SpatialBoundsDerive) -> Result<Self, Self::Error> {
13✔
399
        Ok(match value {
13✔
400
            SpatialBoundsDerive::Derive(_) => {
401
                geoengine_operators::mock::SpatialBoundsDerive::Derive
12✔
402
            }
403
            SpatialBoundsDerive::Bounds(bounds) => {
×
404
                geoengine_operators::mock::SpatialBoundsDerive::Bounds(
405
                    bounds.bounding_box.try_into()?,
×
406
                )
407
            }
408
            SpatialBoundsDerive::None(_) => geoengine_operators::mock::SpatialBoundsDerive::None,
1✔
409
        })
410
    }
13✔
411
}
412

413
/// A bounding box that includes all border points.
414
/// Note: may degenerate to a point!
415
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
416
#[serde(rename_all = "camelCase")]
417
pub struct BoundingBox2D {
418
    lower_left_coordinate: Coordinate2D,
419
    upper_right_coordinate: Coordinate2D,
420
}
421

422
impl TryFrom<BoundingBox2D> for geoengine_datatypes::primitives::BoundingBox2D {
423
    type Error = anyhow::Error;
424
    fn try_from(value: BoundingBox2D) -> Result<Self, Self::Error> {
1✔
425
        geoengine_datatypes::primitives::BoundingBox2D::new(
1✔
426
            value.lower_left_coordinate.into(),
1✔
427
            value.upper_right_coordinate.into(),
1✔
428
        )
429
        .context("invalid bounding box")
1✔
430
    }
1✔
431
}
432

433
/// Specify the output of a vector expression.
434
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ToSchema)]
435
#[serde(tag = "type", content = "value", rename_all = "camelCase")]
436
#[schema(examples(
437
    json!({"type": "column", "value": "adjusted_temperature"}),
438
    json!({"type": "geometry", "value": "MultiPolygon"}),
439
))]
440
pub enum OutputColumn {
441
    /// The expression will override the current geometry
442
    #[schema(title = "GeometryOutputColumn")]
443
    Geometry(VectorDataType),
444
    /// The expression will append a new `Float` column
445
    #[schema(title = "NewOutputColumn")]
446
    Column(String),
447
}
448

449
#[cfg(test)]
450
mod tests {
451
    #![allow(clippy::float_cmp)] // ok for tests
452

453
    use geoengine_datatypes::primitives::AxisAlignedRectangle;
454

455
    use super::*;
456

457
    #[test]
458
    fn it_converts_coordinates() {
1✔
459
        let dt = geoengine_datatypes::primitives::Coordinate2D { x: 1.5, y: -2.25 };
1✔
460

461
        let api: Coordinate2D = dt.into();
1✔
462
        assert_eq!(api.x, 1.5);
1✔
463
        assert_eq!(api.y, -2.25);
1✔
464

465
        let back: geoengine_datatypes::primitives::Coordinate2D = api.into();
1✔
466
        assert_eq!(back.x, 1.5);
1✔
467
        assert_eq!(back.y, -2.25);
1✔
468
    }
1✔
469

470
    #[test]
471
    fn it_converts_raster_data_types() {
1✔
472
        use geoengine_datatypes::raster::RasterDataType as Dt;
473

474
        let dt = Dt::F32;
1✔
475
        let api: RasterDataType = dt.into();
1✔
476
        assert_eq!(api, RasterDataType::F32);
1✔
477

478
        let back: geoengine_datatypes::raster::RasterDataType = api.into();
1✔
479
        assert_eq!(back, Dt::F32);
1✔
480
    }
1✔
481

482
    #[test]
483
    fn it_converts_raster_band_descriptors() {
1✔
484
        use geoengine_datatypes::primitives::Measurement;
485
        use geoengine_operators::engine::RasterBandDescriptor as OpsDesc;
486

487
        let ops = OpsDesc {
1✔
488
            name: "band 0".into(),
1✔
489
            measurement: Measurement::Unitless,
1✔
490
        };
1✔
491

492
        let api: RasterBandDescriptor = ops.clone().into();
1✔
493
        assert_eq!(api.name, "band 0");
1✔
494

495
        let back: geoengine_operators::engine::RasterBandDescriptor = api.into();
1✔
496
        assert_eq!(back, ops);
1✔
497
    }
1✔
498

499
    #[test]
500
    fn it_converts_bounding_boxes() {
1✔
501
        let api_bbox = BoundingBox2D {
1✔
502
            lower_left_coordinate: Coordinate2D { x: 1.0, y: 2.0 },
1✔
503
            upper_right_coordinate: Coordinate2D { x: 3.0, y: 4.0 },
1✔
504
        };
1✔
505

506
        let dt_bbox: geoengine_datatypes::primitives::BoundingBox2D =
1✔
507
            api_bbox.try_into().expect("it should convert");
1✔
508

509
        assert_eq!(
1✔
510
            dt_bbox.upper_left(),
1✔
511
            geoengine_datatypes::primitives::Coordinate2D { x: 1.0, y: 4.0 }
512
        );
513
        assert_eq!(
1✔
514
            dt_bbox.lower_right(),
1✔
515
            geoengine_datatypes::primitives::Coordinate2D { x: 3.0, y: 2.0 }
516
        );
517
    }
1✔
518
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc