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

geo-engine / geoengine / 23548132590

25 Mar 2026 03:06PM UTC coverage: 87.388%. First build
23548132590

Pull #1114

github

web-flow
Merge d0ebf151a into dccacebf2
Pull Request #1114: feat: STAC dataset import

592 of 1788 new or added lines in 12 files covered. (33.11%)

113703 of 130113 relevant lines covered (87.39%)

497588.03 hits per line

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

94.78
/services/src/api/model/processing_graphs/processing.rs
1
use crate::api::model::datatypes::{SpatialReference, TimeInstance, TimeStep};
2
use crate::api::model::processing_graphs::{
3
    parameters::{
4
        ColumnNames, FeatureAggregationMethod, RasterBandDescriptor, RasterDataType,
5
        TemporalAggregationMethod,
6
    },
7
    source_parameters::{
8
        MultipleRasterSources, SingleRasterOrVectorSource, SingleRasterSource,
9
        SingleVectorMultipleRasterSources,
10
    },
11
};
12
use geoengine_macros::{api_operator, type_tag};
13
use geoengine_operators::processing::{
14
    Aggregation as OperatorsAggregation, BandFilter as OperatorsBandFilter,
15
    BandFilterParams as OperatorsBandFilterParameters,
16
    DeriveOutRasterSpecsSource as OperatorsDeriveOutRasterSpecsSource,
17
    Expression as OperatorsExpression, ExpressionParams as OperatorsExpressionParameters,
18
    Interpolation as OperatorsInterpolation, InterpolationMethod as OperatorsInterpolationMethod,
19
    InterpolationParams as OperatorsInterpolationParameters,
20
    InterpolationResolution as OperatorsInterpolationResolution,
21
    RasterStacker as OperatorsRasterStacker,
22
    RasterStackerParams as OperatorsRasterStackerParameters,
23
    RasterTypeConversion as OperatorsRasterTypeConversion,
24
    RasterTypeConversionParams as OperatorsRasterTypeConversionParameters,
25
    RasterVectorJoin as OperatorsRasterVectorJoin,
26
    RasterVectorJoinParams as OperatorsRasterVectorJoinParameters,
27
    Reprojection as OperatorsReprojection, ReprojectionParams as OperatorsReprojectionParameters,
28
    TemporalRasterAggregation as OperatorsTemporalRasterAggregation,
29
    TemporalRasterAggregationParameters as OperatorsTemporalRasterAggregationParameters,
30
};
31
use serde::{Deserialize, Serialize};
32
use utoipa::ToSchema;
33

34
/// The `Expression` operator performs a pixel-wise mathematical expression on one or more bands of a raster source.
35
/// The expression is specified as a user-defined script in a very simple language.
36
/// 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.
37
/// Users can specify an output data type.
38
/// Internally, the expression is evaluated using floating-point numbers.
39
///
40
/// An example usage scenario is to calculate NDVI for a red and a near-infrared raster channel.
41
/// The expression uses a raster source with two bands, referred to as A and B, and calculates the formula `(A - B) / (A + B)`.
42
/// When the temporal resolution is months, our output NDVI will also be a monthly time series.
43
///
44
/// ## Types
45
///
46
/// The following describes the types used in the parameters.
47
///
48
/// ### Expression
49
///
50
/// Expressions are simple scripts to perform pixel-wise computations.
51
/// One can refer to the raster inputs as `A` for the first raster band, `B` for the second, and so on.
52
/// Furthermore, expressions can check with `A IS NODATA`, `B IS NODATA`, etc. for NO DATA values.
53
/// This is important if `mapNoData` is set to true.
54
/// Otherwise, NO DATA values are mapped automatically to the output NO DATA value.
55
/// Finally, the value `NODATA` can be used to output NO DATA.
56
///
57
/// Users can think of this implicit function signature for, e.g., two inputs:
58
///
59
/// ```Rust
60
/// fn (A: f64, B: f64) -> f64
61
/// ```
62
///
63
/// As a start, expressions contain algebraic operations and mathematical functions.
64
///
65
/// ```Rust
66
/// (A + B) / 2
67
/// ```
68
///
69
/// In addition, branches can be used to check for conditions.
70
///
71
/// ```Rust
72
/// if A IS NODATA {
73
///     B
74
/// } else {
75
///     A
76
/// }
77
/// ```
78
///
79
/// Function calls can be used to access utility functions.
80
///
81
/// ```Rust
82
/// max(A, 0)
83
/// ```
84
///
85
/// Currently, the following functions are available:
86
///
87
/// - `abs(a)`: absolute value
88
/// - `min(a, b)`, `min(a, b, c)`: minimum value
89
/// - `max(a, b)`, `max(a, b, c)`: maximum value
90
/// - `sqrt(a)`: square root
91
/// - `ln(a)`: natural logarithm
92
/// - `log10(a)`: base 10 logarithm
93
/// - `cos(a)`, `sin(a)`, `tan(a)`, `acos(a)`, `asin(a)`, `atan(a)`: trigonometric functions
94
/// - `pi()`, `e()`: mathematical constants
95
/// - `round(a)`, `ceil(a)`, `floor(a)`: rounding functions
96
/// - `mod(a, b)`: division remainder
97
/// - `to_degrees(a)`, `to_radians(a)`: conversion to degrees or radians
98
///
99
/// To generate more complex expressions, it is possible to have variable assignments.
100
///
101
/// ```Rust
102
/// let mean = (A + B) / 2;
103
/// let coefficient = 0.357;
104
/// mean * coefficient
105
/// ```
106
///
107
/// Note, that all assignments are separated by semicolons.
108
/// However, the last expression must be without a semicolon.
109
#[api_operator(
90✔
110
    title = "Raster Expression",
90✔
111
    examples(json!({
90✔
112
        "type": "Expression",
113
        "params": {
114
            "expression": "(A - B) / (A + B)",
115
            "outputType": "F32",
116
            "outputBand": {
117
                "name": "NDVI",
118
                "measurement": { "type": "unitless" },
119
            },
120
            "mapNoData": true
121
        },
122
        "sources": {
123
            "raster": {
124
                "type": "GdalSource",
125
                "params": {
126
                    "data": "ndvi"
127
                }
128
            }
129
        }
130
    })),
131
)]
132
pub struct Expression {
133
    pub params: ExpressionParameters,
134
    pub sources: Box<SingleRasterSource>,
135
}
136

137
/// ## Types
138
///
139
/// The following describes the types used in the parameters.
140
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
141
#[serde(rename_all = "camelCase")]
142
pub struct ExpressionParameters {
143
    /// Expression script
144
    ///
145
    /// Example: `"(A - B) / (A + B)"`
146
    #[schema(examples("(A - B) / (A + B)"))]
147
    pub expression: String,
148
    /// A raster data type for the output
149
    #[schema(examples("F32"))]
150
    pub output_type: RasterDataType,
151
    /// Description about the output
152
    #[schema(
153
        nullable = false /* cannot be null, but left out, avoids `Option<Option<_>>` in openapi client  */,
154
        examples(json!({
155
            "name": "NDVI",
156
            "measurement": { "type": "unitless" },
157
        }))
158
    )]
159
    pub output_band: Option<RasterBandDescriptor>,
160
    /// Should NO DATA values be mapped with the `expression`? Otherwise, they are mapped automatically to NO DATA.
161
    #[schema(examples(true))]
162
    pub map_no_data: bool,
163
}
164

165
impl TryFrom<Expression> for OperatorsExpression {
166
    type Error = anyhow::Error;
167

168
    fn try_from(value: Expression) -> Result<Self, Self::Error> {
1✔
169
        Ok(OperatorsExpression {
170
            params: OperatorsExpressionParameters {
1✔
171
                expression: value.params.expression,
1✔
172
                output_type: value.params.output_type.into(),
1✔
173
                output_band: value.params.output_band.map(Into::into),
1✔
174
                map_no_data: value.params.map_no_data,
1✔
175
            },
1✔
176
            sources: (*value.sources).try_into()?,
1✔
177
        })
178
    }
1✔
179
}
180

181
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
182
#[serde(rename_all = "camelCase", tag = "type", content = "values")]
183
pub enum RenameBands {
184
    Default,
185
    Suffix(Vec<String>),
186
    Rename(Vec<String>),
187
}
188

189
impl From<RenameBands> for geoengine_datatypes::raster::RenameBands {
190
    fn from(value: RenameBands) -> Self {
3✔
191
        match value {
3✔
NEW
192
            RenameBands::Default => Self::Default,
×
193
            RenameBands::Suffix(values) => Self::Suffix(values),
1✔
194
            RenameBands::Rename(values) => Self::Rename(values),
2✔
195
        }
196
    }
3✔
197
}
198

199
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
200
#[serde(untagged)]
201
pub enum BandsByNameOrIndex {
202
    Name(Vec<String>),
203
    Index(Vec<usize>),
204
}
205

206
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ToSchema, Default)]
207
#[serde(rename_all = "camelCase")]
208
pub enum DeriveOutRasterSpecsSource {
209
    DataBounds,
210
    #[default]
211
    ProjectionBounds,
212
}
213

214
impl From<DeriveOutRasterSpecsSource> for OperatorsDeriveOutRasterSpecsSource {
215
    fn from(value: DeriveOutRasterSpecsSource) -> Self {
2✔
216
        match value {
2✔
NEW
217
            DeriveOutRasterSpecsSource::DataBounds => Self::DataBounds,
×
218
            DeriveOutRasterSpecsSource::ProjectionBounds => Self::ProjectionBounds,
2✔
219
        }
220
    }
2✔
221
}
222

223
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
224
#[serde(rename_all = "camelCase", tag = "type")]
225
pub enum InterpolationResolution {
226
    Resolution { x: f64, y: f64 },
227
    Fraction { x: f64, y: f64 },
228
}
229

230
impl From<InterpolationResolution> for OperatorsInterpolationResolution {
231
    fn from(value: InterpolationResolution) -> Self {
1✔
232
        match value {
1✔
NEW
233
            InterpolationResolution::Resolution { x, y } => {
×
NEW
234
                Self::Resolution(geoengine_datatypes::primitives::SpatialResolution { x, y })
×
235
            }
236
            InterpolationResolution::Fraction { x, y } => Self::Fraction { x, y },
1✔
237
        }
238
    }
1✔
239
}
240

241
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ToSchema)]
242
#[serde(rename_all = "camelCase")]
243
pub enum InterpolationMethod {
244
    NearestNeighbor,
245
    BiLinear,
246
}
247

248
impl From<InterpolationMethod> for OperatorsInterpolationMethod {
249
    fn from(value: InterpolationMethod) -> Self {
1✔
250
        match value {
1✔
251
            InterpolationMethod::NearestNeighbor => Self::NearestNeighbor,
1✔
NEW
252
            InterpolationMethod::BiLinear => Self::BiLinear,
×
253
        }
254
    }
1✔
255
}
256

257
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
258
#[serde(rename_all = "camelCase", tag = "type")]
259
pub enum Aggregation {
260
    #[schema(title = "MinAggregation")]
261
    #[serde(rename_all = "camelCase")]
262
    Min { ignore_no_data: bool },
263
    #[schema(title = "MaxAggregation")]
264
    #[serde(rename_all = "camelCase")]
265
    Max { ignore_no_data: bool },
266
    #[schema(title = "FirstAggregation")]
267
    #[serde(rename_all = "camelCase")]
268
    First { ignore_no_data: bool },
269
    #[schema(title = "LastAggregation")]
270
    #[serde(rename_all = "camelCase")]
271
    Last { ignore_no_data: bool },
272
    #[schema(title = "MeanAggregation")]
273
    #[serde(rename_all = "camelCase")]
274
    Mean { ignore_no_data: bool },
275
    #[schema(title = "SumAggregation")]
276
    #[serde(rename_all = "camelCase")]
277
    Sum { ignore_no_data: bool },
278
    #[schema(title = "CountAggregation")]
279
    #[serde(rename_all = "camelCase")]
280
    Count { ignore_no_data: bool },
281
    #[schema(title = "PercentileEstimateAggregation")]
282
    #[serde(rename_all = "camelCase")]
283
    PercentileEstimate {
284
        ignore_no_data: bool,
285
        percentile: f64,
286
    },
287
}
288

289
impl From<Aggregation> for OperatorsAggregation {
290
    fn from(value: Aggregation) -> Self {
1✔
291
        match value {
1✔
NEW
292
            Aggregation::Min { ignore_no_data } => Self::Min { ignore_no_data },
×
NEW
293
            Aggregation::Max { ignore_no_data } => Self::Max { ignore_no_data },
×
NEW
294
            Aggregation::First { ignore_no_data } => Self::First { ignore_no_data },
×
NEW
295
            Aggregation::Last { ignore_no_data } => Self::Last { ignore_no_data },
×
296
            Aggregation::Mean { ignore_no_data } => Self::Mean { ignore_no_data },
1✔
NEW
297
            Aggregation::Sum { ignore_no_data } => Self::Sum { ignore_no_data },
×
NEW
298
            Aggregation::Count { ignore_no_data } => Self::Count { ignore_no_data },
×
299
            Aggregation::PercentileEstimate {
NEW
300
                ignore_no_data,
×
NEW
301
                percentile,
×
NEW
302
            } => Self::PercentileEstimate {
×
NEW
303
                ignore_no_data,
×
NEW
304
                percentile,
×
NEW
305
            },
×
306
        }
307
    }
1✔
308
}
309

310
/// The `Reprojection` operator reprojects data from one spatial reference system to another.
311
/// It accepts exactly one input which can either be a raster or a vector data stream.
312
/// The operator produces all data that, after reprojection, is contained in the query rectangle.
313
///
314
/// ## Data Type Specifics
315
///
316
/// The concrete behavior depends on the data type.
317
///
318
/// ### Vector Data
319
///
320
/// The operator reprojects all coordinates of the features individually.
321
/// The result contains all features that, after reprojection, are intersected by the query rectangle.
322
///
323
/// ### Raster Data
324
///
325
/// To create tiles in the target projection, the operator loads corresponding tiles in the source projection.
326
/// For each output pixel, the value of the nearest input pixel is used.
327
///
328
/// If parts of a tile are outside of the source extent after projection, the operator produces NO DATA values.
329
///
330
/// ## Parameters
331
///
332
/// - `targetSpatialReference`: target spatial reference system.
333
/// - `deriveOutSpec`: controls how raster output bounds are derived.
334
///   The default `projectionBounds` usually keeps a projection-aligned target grid,
335
///   while `dataBounds` derives it directly from the source data bounds.
336
///
337
/// ## Inputs
338
///
339
/// The `Reprojection` operator expects exactly one _raster_ or _vector_ input.
340
///
341
/// ## Errors
342
///
343
/// The operator returns an error if the target projection is unknown or if input data cannot be reprojected.
344
#[api_operator(
170✔
345
    title = "Reprojection",
170✔
346
    examples(json!({
170✔
347
        "type": "Reprojection",
348
        "params": {
349
            "deriveOutSpec": "projectionBounds",
350
            "targetSpatialReference": "EPSG:32632"
351
        },
352
        "sources": {
353
            "source": {
354
                "type": "MockPointSource",
355
                "params": {
356
                    "points": [{ "x": 8.77069, "y": 50.80904 }],
357
                    "spatialBounds": { "type": "none" }
358
                }
359
            }
360
        }
361
    }))
362
)]
363
pub struct Reprojection {
364
    pub params: ReprojectionParameters,
365
    pub sources: Box<SingleRasterOrVectorSource>,
366
}
367

368
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
369
#[serde(rename_all = "camelCase")]
370
pub struct ReprojectionParameters {
371
    #[schema(value_type = String)]
372
    pub target_spatial_reference: SpatialReference,
373
    #[serde(default)]
374
    pub derive_out_spec: DeriveOutRasterSpecsSource,
375
}
376

377
impl TryFrom<Reprojection> for OperatorsReprojection {
378
    type Error = anyhow::Error;
379

380
    fn try_from(value: Reprojection) -> Result<Self, Self::Error> {
2✔
381
        Ok(OperatorsReprojection {
382
            params: OperatorsReprojectionParameters {
2✔
383
                target_spatial_reference: value.params.target_spatial_reference.into(),
2✔
384
                derive_out_spec: value.params.derive_out_spec.into(),
2✔
385
            },
2✔
386
            sources: (*value.sources).try_into()?,
2✔
387
        })
388
    }
2✔
389
}
390

391
/// The `TemporalRasterAggregation` operator aggregates a raster time series into uniform time windows.
392
/// The output starts with the first window that contains the query start and contains all windows
393
/// that overlap the query interval.
394
///
395
/// Pixel values are computed by aggregating all input rasters that contribute to the current window
396
/// with the selected `aggregation` method.
397
///
398
/// The optional `windowReference` parameter allows specifying a custom anchor point for the windows.
399
/// If omitted, windows are anchored at `1970-01-01T00:00:00Z`.
400
///
401
/// ## Types
402
///
403
/// The following describes the types used in the parameters.
404
///
405
/// ### Aggregation
406
///
407
/// There are different methods that can be used to aggregate raster time series.
408
/// Encountering NO DATA makes the aggregation result NO DATA unless `ignoreNoData` is `true`.
409
///
410
/// - `min`, `max`, `first`, `last`, `mean`, `sum`, `count`
411
/// - `percentileEstimate` with a percentile in `(0, 1)`
412
///
413
/// ## Inputs
414
///
415
/// The `TemporalRasterAggregation` operator expects exactly one _raster_ input.
416
///
417
/// ## Errors
418
///
419
/// If the aggregation method is `first`, `last`, or `mean` and the input raster has no NO DATA value,
420
/// an error is returned.
421
#[api_operator(
90✔
422
    title = "Temporal Raster Aggregation",
90✔
423
    examples(json!({
90✔
424
        "type": "TemporalRasterAggregation",
425
        "params": {
426
            "aggregation": { "type": "mean", "ignoreNoData": true },
427
            "window": { "granularity": "months", "step": 1 }
428
        },
429
        "sources": {
430
            "raster": {
431
                "type": "Expression",
432
                "params": {
433
                    "expression": "(A - B) / (A + B)",
434
                    "outputType": "F32",
435
                    "outputBand": {
436
                        "name": "NDVI",
437
                        "measurement": { "type": "unitless" }
438
                    },
439
                    "mapNoData": false
440
                },
441
                "sources": {
442
                    "raster": {
443
                        "type": "GdalSource",
444
                        "params": { "data": "ndvi" }
445
                    }
446
                }
447
            }
448
        }
449
    }))
450
)]
451
pub struct TemporalRasterAggregation {
452
    pub params: TemporalRasterAggregationParameters,
453
    pub sources: Box<SingleRasterSource>,
454
}
455

456
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
457
#[serde(rename_all = "camelCase")]
458
pub struct TemporalRasterAggregationParameters {
459
    pub aggregation: Aggregation,
460
    pub window: TimeStep,
461
    pub window_reference: Option<TimeInstance>,
462
    pub output_type: Option<RasterDataType>,
463
}
464

465
impl TryFrom<TemporalRasterAggregation> for OperatorsTemporalRasterAggregation {
466
    type Error = anyhow::Error;
467

468
    fn try_from(value: TemporalRasterAggregation) -> Result<Self, Self::Error> {
1✔
469
        Ok(OperatorsTemporalRasterAggregation {
470
            params: OperatorsTemporalRasterAggregationParameters {
1✔
471
                aggregation: value.params.aggregation.into(),
1✔
472
                window: value.params.window.into(),
1✔
473
                window_reference: value.params.window_reference.map(Into::into),
1✔
474
                output_type: value.params.output_type.map(Into::into),
1✔
475
            },
1✔
476
            sources: (*value.sources).try_into()?,
1✔
477
        })
478
    }
1✔
479
}
480

481
/// The `RasterStacker` stacks all of its inputs into a single raster time series.
482
/// It queries all inputs and combines them by band, space, and then time.
483
///
484
/// The output raster has as many bands as the sum of all input bands.
485
/// Tiles are automatically temporally aligned.
486
///
487
/// All inputs must have the same data type and spatial reference.
488
///
489
/// ## Types
490
///
491
/// The following describes the types used in the parameters.
492
///
493
/// ### RenameBands
494
///
495
/// The `RenameBands` type specifies how to rename output bands to avoid naming conflicts:
496
///
497
/// - `default`: appends ` (n)` with the smallest `n` that avoids a conflict.
498
/// - `suffix`: appends one suffix per input.
499
/// - `rename`: explicitly provides names for all resulting bands.
500
///
501
/// ## Inputs
502
///
503
/// The `RasterStacker` operator expects multiple raster inputs.
504
#[api_operator(
90✔
505
    title = "Raster Stacker",
90✔
506
    examples(json!({
90✔
507
        "type": "RasterStacker",
508
        "params": {
509
            "renameBands": { "type": "default" }
510
        },
511
        "sources": {
512
            "rasters": [
513
                {
514
                    "type": "GdalSource",
515
                    "params": { "data": "example-a" }
516
                },
517
                {
518
                    "type": "GdalSource",
519
                    "params": { "data": "example-b" }
520
                }
521
            ]
522
        }
523
    }))
524
)]
525
pub struct RasterStacker {
526
    pub params: RasterStackerParameters,
527
    pub sources: Box<MultipleRasterSources>,
528
}
529

530
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
531
#[serde(rename_all = "camelCase")]
532
pub struct RasterStackerParameters {
533
    pub rename_bands: RenameBands,
534
}
535

536
impl TryFrom<RasterStacker> for OperatorsRasterStacker {
537
    type Error = anyhow::Error;
538

539
    fn try_from(value: RasterStacker) -> Result<Self, Self::Error> {
3✔
540
        Ok(OperatorsRasterStacker {
541
            params: OperatorsRasterStackerParameters {
3✔
542
                rename_bands: value.params.rename_bands.into(),
3✔
543
            },
3✔
544
            sources: (*value.sources).try_into()?,
3✔
545
        })
546
    }
3✔
547
}
548

549
/// The `RasterTypeConversion` operator changes the data type of raster pixels.
550
///
551
/// Applying this conversion may cause precision loss.
552
/// For example, converting `F32` value `3.1` to `U8` results in `3`.
553
///
554
/// If a value is outside of the range of the target data type,
555
/// it is clipped to the valid range of that type.
556
/// For example, converting `F32` value `300.0` to `U8` results in `255`.
557
///
558
/// ## Inputs
559
///
560
/// The `RasterTypeConversion` operator expects exactly one _raster_ input.
561
#[api_operator(
90✔
562
    title = "Raster Type Conversion",
90✔
563
    examples(json!({
90✔
564
        "type": "RasterTypeConversion",
565
        "params": {
566
            "outputDataType": "U16"
567
        },
568
        "sources": {
569
            "raster": {
570
                "type": "GdalSource",
571
                "params": { "data": "example" }
572
            }
573
        }
574
    }))
575
)]
576
pub struct RasterTypeConversion {
577
    pub params: RasterTypeConversionParameters,
578
    pub sources: Box<SingleRasterSource>,
579
}
580

581
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
582
#[serde(rename_all = "camelCase")]
583
pub struct RasterTypeConversionParameters {
584
    pub output_data_type: RasterDataType,
585
}
586

587
impl TryFrom<RasterTypeConversion> for OperatorsRasterTypeConversion {
588
    type Error = anyhow::Error;
589

590
    fn try_from(value: RasterTypeConversion) -> Result<Self, Self::Error> {
1✔
591
        Ok(OperatorsRasterTypeConversion {
592
            params: OperatorsRasterTypeConversionParameters {
1✔
593
                output_data_type: value.params.output_data_type.into(),
1✔
594
            },
1✔
595
            sources: (*value.sources).try_into()?,
1✔
596
        })
597
    }
1✔
598
}
599

600
/// The `Interpolation` operator increases raster resolution by interpolating values of an input raster.
601
///
602
/// If queried with a resolution that is coarser than the input resolution,
603
/// interpolation is not applicable and an error is returned.
604
///
605
/// ## Types
606
///
607
/// The following describes the types used in the parameters.
608
///
609
/// ### `InterpolationMethod`
610
///
611
/// The operator supports the following interpolation methods:
612
///
613
/// - `nearestNeighbor`: nearest-neighbor interpolation
614
/// - `biLinear`: bilinear interpolation
615
///
616
/// ### `InterpolationResolution`
617
///
618
/// The target resolution can be configured as:
619
///
620
/// - `resolution`: explicit output resolution (`x`, `y`)
621
/// - `fraction`: upscale factor relative to input resolution (`x >= 1`, `y >= 1`)
622
///
623
/// ## Parameters
624
///
625
/// - `interpolation`: interpolation method.
626
/// - `outputResolution`: output grid resolution.
627
/// - `outputOriginReference` (optional): reference point to align the output grid origin.
628
///
629
/// ## Inputs
630
///
631
/// The `Interpolation` operator expects exactly one _raster_ input.
632
#[api_operator(
90✔
633
    title = "Interpolation",
90✔
634
    examples(json!({
90✔
635
        "type": "Interpolation",
636
        "params": {
637
            "interpolation": "nearestNeighbor",
638
            "outputResolution": {
639
                "type": "fraction",
640
                "x": 2.0,
641
                "y": 2.0
642
            }
643
        },
644
        "sources": {
645
            "raster": {
646
                "type": "MultiBandGdalSource",
647
                "params": { "data": "sentinel-2-l2a_EPSG32632_U8_20" }
648
            }
649
        }
650
    }))
651
)]
652
pub struct Interpolation {
653
    pub params: InterpolationParameters,
654
    pub sources: Box<SingleRasterSource>,
655
}
656

657
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
658
#[serde(rename_all = "camelCase")]
659
pub struct InterpolationParameters {
660
    pub interpolation: InterpolationMethod,
661
    pub output_resolution: InterpolationResolution,
662
    pub output_origin_reference: Option<crate::api::model::datatypes::Coordinate2D>,
663
}
664

665
impl TryFrom<Interpolation> for OperatorsInterpolation {
666
    type Error = anyhow::Error;
667

668
    fn try_from(value: Interpolation) -> Result<Self, Self::Error> {
1✔
669
        Ok(OperatorsInterpolation {
670
            params: OperatorsInterpolationParameters {
1✔
671
                interpolation: value.params.interpolation.into(),
1✔
672
                output_resolution: value.params.output_resolution.into(),
1✔
673
                output_origin_reference: value.params.output_origin_reference.map(Into::into),
1✔
674
            },
1✔
675
            sources: (*value.sources).try_into()?,
1✔
676
        })
677
    }
1✔
678
}
679

680
/// The `BandFilter` operator selects bands from a raster source by band names or band indices.
681
///
682
/// It removes all non-selected bands while preserving the original order of remaining bands.
683
///
684
/// ## Parameters
685
///
686
/// - `bands`: selected bands either by names (`["nir", "red"]`) or indices (`[0, 2]`).
687
///
688
/// ## Inputs
689
///
690
/// The `BandFilter` operator expects exactly one _raster_ input.
691
///
692
/// ## Errors
693
///
694
/// The operator returns an error if no bands are selected or if selected band names/indices
695
/// cannot be mapped to existing input bands.
696
#[api_operator(
90✔
697
    title = "Band Filter",
90✔
698
    examples(json!({
90✔
699
        "type": "BandFilter",
700
        "params": {
701
            "bands": ["nir", "red"]
702
        },
703
        "sources": {
704
            "raster": {
705
                "type": "MultiBandGdalSource",
706
                "params": { "data": "sentinel-2-l2a_EPSG32632_U16_10" }
707
            }
708
        }
709
    }))
710
)]
711
pub struct BandFilter {
712
    pub params: BandFilterParameters,
713
    pub sources: Box<SingleRasterSource>,
714
}
715

716
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
717
#[serde(rename_all = "camelCase")]
718
pub struct BandFilterParameters {
719
    #[schema(examples(json!(["nir", "red"]), json!([0, 2])))]
720
    pub bands: BandsByNameOrIndex,
721
}
722

723
impl TryFrom<BandFilter> for OperatorsBandFilter {
724
    type Error = anyhow::Error;
725

726
    fn try_from(value: BandFilter) -> Result<Self, Self::Error> {
1✔
727
        let params = serde_json::from_value::<OperatorsBandFilterParameters>(
1✔
728
            serde_json::to_value(value.params)?,
1✔
729
        )?;
×
730

731
        Ok(OperatorsBandFilter {
732
            params,
1✔
733
            sources: (*value.sources).try_into()?,
1✔
734
        })
735
    }
1✔
736
}
737

738
/// The `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs.
739
/// For each raster input, a new column is added to the collection from the vector input.
740
/// The new column contains the value of the raster at the location of the vector feature.
741
/// For features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user.
742
/// The same is true if the temporal extent of a vector feature covers multiple raster time steps.
743
/// More details are described below.
744
///
745
/// **Example**:
746
/// You have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value.
747
/// For your application, you want to know the NDVI value of each field.
748
/// The `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies.
749
/// For example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field.
750
/// This is useful for exploratory analysis since the computation is very fast.
751
/// To calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function.
752
/// Since the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well.
753
/// The default is `none` which will create a new feature for each month.
754
/// Other options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time.
755
///
756
/// ## Inputs
757
///
758
/// The `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs.
759
///
760
/// | Parameter | Type                                |
761
/// | --------- | ----------------------------------- |
762
/// | `sources` | `SingleVectorMultipleRasterSources` |
763
///
764
/// ## Errors
765
///
766
/// If the length of `names` is not equal to the number of raster inputs, an error is thrown.
767
///
768
#[api_operator(
90✔
769
    title = "Raster Vector Join",
90✔
770
    examples(json!({
90✔
771
        "type": "RasterVectorJoin",
772
        "params": {
773
            "names": ["NDVI"],
774
            "featureAggregation": "first",
775
            "temporalAggregation": "mean",
776
            "temporalAggregationIgnoreNoData": true
777
        },
778
        "sources": {
779
            "vector": {
780
                "type": "OgrSource",
781
                "params": {
782
                    "data": "places"
783
                }
784
            },
785
            "rasters": [{
786
                "type": "GdalSource",
787
                "params": {
788
                "data": "ndvi"
789
                }
790
            }]
791
        }
792
    }))
793
)]
794
pub struct RasterVectorJoin {
795
    pub params: RasterVectorJoinParameters,
796
    pub sources: Box<SingleVectorMultipleRasterSources>,
797
}
798

799
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
800
#[serde(rename_all = "camelCase")]
801
pub struct RasterVectorJoinParameters {
802
    /// Specify how the new column names are derived from the raster band names.
803
    ///
804
    /// The `ColumnNames` type is used to specify how the new column names are derived from the raster band names.
805
    ///
806
    /// - **default**: Appends " (n)" to the band name with the smallest `n` that avoids a conflict.
807
    /// - **suffix**: Specifies a suffix for each input, to be appended to the band names.
808
    /// - **rename**: A list of names for each new column.
809
    ///
810
    #[schema(examples(
811
        json!({"type": "default"}),
812
        json!({"type": "suffix", "values": ["_sentinel2"]}),
813
        json!({"type": "rename", "values": ["red", "green", "blue"]}),
814
    ))]
815
    pub names: ColumnNames,
816
    /// The aggregation function to use for features covering multiple pixels.
817
    #[schema(examples("first"))]
818
    pub feature_aggregation: FeatureAggregationMethod,
819
    /// Whether to ignore no data values in the aggregation. Defaults to `false`.
820
    #[serde(default)]
821
    #[schema(examples(true))]
822
    pub feature_aggregation_ignore_no_data: bool,
823
    /// The aggregation function to use for features covering multiple (raster) time steps.
824
    #[schema(examples("mean"))]
825
    pub temporal_aggregation: TemporalAggregationMethod,
826
    /// Whether to ignore no data values in the aggregation. Defaults to `false`.
827
    #[serde(default)]
828
    #[schema(examples(true))]
829
    pub temporal_aggregation_ignore_no_data: bool,
830
}
831

832
impl TryFrom<RasterVectorJoin> for OperatorsRasterVectorJoin {
833
    type Error = anyhow::Error;
834

835
    fn try_from(value: RasterVectorJoin) -> Result<Self, Self::Error> {
1✔
836
        Ok(OperatorsRasterVectorJoin {
837
            params: OperatorsRasterVectorJoinParameters {
1✔
838
                names: value.params.names.into(),
1✔
839
                feature_aggregation: value.params.feature_aggregation.into(),
1✔
840
                feature_aggregation_ignore_no_data: value.params.feature_aggregation_ignore_no_data,
1✔
841
                temporal_aggregation: value.params.temporal_aggregation.into(),
1✔
842
                temporal_aggregation_ignore_no_data: value
1✔
843
                    .params
1✔
844
                    .temporal_aggregation_ignore_no_data,
1✔
845
            },
1✔
846
            sources: (*value.sources).try_into()?,
1✔
847
        })
848
    }
1✔
849
}
850

851
#[cfg(test)]
852
mod tests {
853

854
    use super::*;
855
    use crate::api::model::{
856
        datatypes::{Coordinate2D, TimeGranularity},
857
        processing_graphs::{
858
            RasterOperator, SingleRasterOrVectorOperator, VectorOperator,
859
            parameters::SpatialBoundsDerive,
860
            source::{
861
                GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters,
862
                MultiBandGdalSource,
863
            },
864
        },
865
    };
866
    use serde_json::json;
867

868
    #[test]
869
    fn it_converts_expressions() {
1✔
870
        let api = Expression {
1✔
871
            r#type: Default::default(),
1✔
872
            params: ExpressionParameters {
1✔
873
                expression: "2 * A + B".to_string(),
1✔
874
                output_type: RasterDataType::F32,
1✔
875
                output_band: None,
1✔
876
                map_no_data: true,
1✔
877
            },
1✔
878
            sources: Box::new(SingleRasterSource {
1✔
879
                raster: RasterOperator::GdalSource(GdalSource {
1✔
880
                    r#type: Default::default(),
1✔
881
                    params: GdalSourceParameters {
1✔
882
                        data: "example_data".to_string(),
1✔
883
                        overview_level: None,
1✔
884
                    },
1✔
885
                }),
1✔
886
            }),
1✔
887
        };
1✔
888

889
        let ops = OperatorsExpression::try_from(api).expect("conversion failed");
1✔
890

891
        assert_eq!(ops.params.expression, "2 * A + B");
1✔
892
        assert_eq!(
1✔
893
            ops.params.output_type,
894
            geoengine_datatypes::raster::RasterDataType::F32
895
        );
896
        assert!(ops.params.output_band.is_none());
1✔
897
        assert!(ops.params.map_no_data);
1✔
898
    }
1✔
899

900
    #[test]
901
    fn it_converts_raster_vector_join_params() {
1✔
902
        let api = RasterVectorJoin {
1✔
903
            r#type: Default::default(),
1✔
904
            params: RasterVectorJoinParameters {
1✔
905
                names: ColumnNames::Names {
1✔
906
                    values: vec!["a".to_string(), "b".to_string()],
1✔
907
                },
1✔
908
                feature_aggregation: FeatureAggregationMethod::First,
1✔
909
                feature_aggregation_ignore_no_data: true,
1✔
910
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
911
                temporal_aggregation_ignore_no_data: false,
1✔
912
            },
1✔
913
            sources: Box::new(SingleVectorMultipleRasterSources {
1✔
914
                vector: VectorOperator::MockPointSource(MockPointSource {
1✔
915
                    r#type: Default::default(),
1✔
916
                    params: MockPointSourceParameters {
1✔
917
                        points: vec![Coordinate2D { x: 0.0, y: 0.0 }],
1✔
918
                        spatial_bounds: SpatialBoundsDerive::Derive(Default::default()),
1✔
919
                    },
1✔
920
                }),
1✔
921
                rasters: vec![RasterOperator::GdalSource(GdalSource {
1✔
922
                    r#type: Default::default(),
1✔
923
                    params: GdalSourceParameters {
1✔
924
                        data: "example_data".to_string(),
1✔
925
                        overview_level: None,
1✔
926
                    },
1✔
927
                })],
1✔
928
            }),
1✔
929
        };
1✔
930

931
        let ops_params = OperatorsRasterVectorJoin::try_from(api).expect("conversion failed");
1✔
932

933
        assert!(matches!(
1✔
934
            ops_params.params.names,
1✔
935
            geoengine_operators::processing::ColumnNames::Names(_)
936
        ));
937
        assert_eq!(
1✔
938
            ops_params.params.feature_aggregation,
939
            geoengine_operators::processing::FeatureAggregationMethod::First
940
        );
941
        assert!(ops_params.params.feature_aggregation_ignore_no_data);
1✔
942
        assert_eq!(
1✔
943
            ops_params.params.temporal_aggregation,
944
            geoengine_operators::processing::TemporalAggregationMethod::Mean
945
        );
946
        assert!(!ops_params.params.temporal_aggregation_ignore_no_data);
1✔
947
    }
1✔
948

949
    #[test]
950
    fn it_converts_reprojection_params() {
1✔
951
        let api = Reprojection {
1✔
952
            r#type: Default::default(),
1✔
953
            params: ReprojectionParameters {
1✔
954
                target_spatial_reference: "EPSG:32632".parse().expect("valid srs"),
1✔
955
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
956
            },
1✔
957
            sources: Box::new(SingleRasterOrVectorSource {
1✔
958
                source: SingleRasterOrVectorOperator::Vector(VectorOperator::MockPointSource(
1✔
959
                    MockPointSource {
1✔
960
                        r#type: Default::default(),
1✔
961
                        params: MockPointSourceParameters {
1✔
962
                            points: vec![Coordinate2D { x: 0.0, y: 0.0 }],
1✔
963
                            spatial_bounds: SpatialBoundsDerive::Derive(Default::default()),
1✔
964
                        },
1✔
965
                    },
1✔
966
                )),
1✔
967
            }),
1✔
968
        };
1✔
969

970
        let ops = OperatorsReprojection::try_from(api).expect("conversion failed");
1✔
971

972
        assert_eq!(
1✔
973
            ops.params.target_spatial_reference.to_string(),
1✔
974
            "EPSG:32632"
975
        );
976
        assert!(matches!(
1✔
977
            ops.params.derive_out_spec,
1✔
978
            geoengine_operators::processing::DeriveOutRasterSpecsSource::ProjectionBounds
979
        ));
980
    }
1✔
981

982
    #[test]
983
    fn it_converts_temporal_raster_aggregation_params() {
1✔
984
        let api = TemporalRasterAggregation {
1✔
985
            r#type: Default::default(),
1✔
986
            params: TemporalRasterAggregationParameters {
1✔
987
                aggregation: Aggregation::Mean {
1✔
988
                    ignore_no_data: true,
1✔
989
                },
1✔
990
                window: crate::api::model::datatypes::TimeStep {
1✔
991
                    granularity: TimeGranularity::Months,
1✔
992
                    step: 1,
1✔
993
                },
1✔
994
                window_reference: None,
1✔
995
                output_type: None,
1✔
996
            },
1✔
997
            sources: Box::new(SingleRasterSource {
1✔
998
                raster: RasterOperator::GdalSource(GdalSource {
1✔
999
                    r#type: Default::default(),
1✔
1000
                    params: GdalSourceParameters {
1✔
1001
                        data: "example_data".to_string(),
1✔
1002
                        overview_level: None,
1✔
1003
                    },
1✔
1004
                }),
1✔
1005
            }),
1✔
1006
        };
1✔
1007

1008
        let ops = OperatorsTemporalRasterAggregation::try_from(api).expect("conversion failed");
1✔
1009

1010
        assert!(matches!(
1✔
1011
            ops.params.aggregation,
1✔
1012
            geoengine_operators::processing::Aggregation::Mean {
1013
                ignore_no_data: true
1014
            }
1015
        ));
1016
        assert_eq!(ops.params.window.step, 1);
1✔
1017
        assert!(ops.params.window_reference.is_none());
1✔
1018
        assert!(ops.params.output_type.is_none());
1✔
1019
    }
1✔
1020

1021
    #[test]
1022
    fn it_converts_raster_stacker_params() {
1✔
1023
        let api = RasterStacker {
1✔
1024
            r#type: Default::default(),
1✔
1025
            params: RasterStackerParameters {
1✔
1026
                rename_bands: RenameBands::Suffix(vec!["_a".to_string(), "_b".to_string()]),
1✔
1027
            },
1✔
1028
            sources: Box::new(MultipleRasterSources {
1✔
1029
                rasters: vec![
1✔
1030
                    RasterOperator::GdalSource(GdalSource {
1✔
1031
                        r#type: Default::default(),
1✔
1032
                        params: GdalSourceParameters {
1✔
1033
                            data: "example_data_a".to_string(),
1✔
1034
                            overview_level: None,
1✔
1035
                        },
1✔
1036
                    }),
1✔
1037
                    RasterOperator::GdalSource(GdalSource {
1✔
1038
                        r#type: Default::default(),
1✔
1039
                        params: GdalSourceParameters {
1✔
1040
                            data: "example_data_b".to_string(),
1✔
1041
                            overview_level: None,
1✔
1042
                        },
1✔
1043
                    }),
1✔
1044
                ],
1✔
1045
            }),
1✔
1046
        };
1✔
1047

1048
        let ops = OperatorsRasterStacker::try_from(api).expect("conversion failed");
1✔
1049

1050
        assert_eq!(
1✔
1051
            ops.params.rename_bands,
1052
            geoengine_datatypes::raster::RenameBands::Suffix(vec![
1✔
1053
                "_a".to_string(),
1✔
1054
                "_b".to_string()
1✔
1055
            ])
1✔
1056
        );
1057
        assert_eq!(ops.sources.rasters.len(), 2);
1✔
1058
    }
1✔
1059

1060
    #[test]
1061
    fn it_converts_raster_type_conversion_params() {
1✔
1062
        let api = RasterTypeConversion {
1✔
1063
            r#type: Default::default(),
1✔
1064
            params: RasterTypeConversionParameters {
1✔
1065
                output_data_type: RasterDataType::U16,
1✔
1066
            },
1✔
1067
            sources: Box::new(SingleRasterSource {
1✔
1068
                raster: RasterOperator::GdalSource(GdalSource {
1✔
1069
                    r#type: Default::default(),
1✔
1070
                    params: GdalSourceParameters {
1✔
1071
                        data: "example_data".to_string(),
1✔
1072
                        overview_level: None,
1✔
1073
                    },
1✔
1074
                }),
1✔
1075
            }),
1✔
1076
        };
1✔
1077

1078
        let ops = OperatorsRasterTypeConversion::try_from(api).expect("conversion failed");
1✔
1079

1080
        assert_eq!(
1✔
1081
            ops.params.output_data_type,
1082
            geoengine_datatypes::raster::RasterDataType::U16
1083
        );
1084
    }
1✔
1085

1086
    #[test]
1087
    fn it_converts_interpolation_params() {
1✔
1088
        let api = Interpolation {
1✔
1089
            r#type: Default::default(),
1✔
1090
            params: InterpolationParameters {
1✔
1091
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
1092
                output_resolution: InterpolationResolution::Fraction { x: 2.0, y: 2.0 },
1✔
1093
                output_origin_reference: None,
1✔
1094
            },
1✔
1095
            sources: Box::new(SingleRasterSource {
1✔
1096
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1097
                    r#type: Default::default(),
1✔
1098
                    params: GdalSourceParameters {
1✔
1099
                        data: "example_data".to_string(),
1✔
1100
                        overview_level: None,
1✔
1101
                    },
1✔
1102
                }),
1✔
1103
            }),
1✔
1104
        };
1✔
1105

1106
        let ops = OperatorsInterpolation::try_from(api).expect("conversion failed");
1✔
1107

1108
        assert!(matches!(
1✔
1109
            ops.params.interpolation,
1✔
1110
            geoengine_operators::processing::InterpolationMethod::NearestNeighbor
1111
        ));
1112
        assert!(matches!(
1✔
1113
            ops.params.output_resolution,
1✔
1114
            geoengine_operators::processing::InterpolationResolution::Fraction { x, y }
1✔
1115
            if (x - 2.0).abs() < f64::EPSILON && (y - 2.0).abs() < f64::EPSILON
1✔
1116
        ));
1117
        assert!(ops.params.output_origin_reference.is_none());
1✔
1118
    }
1✔
1119

1120
    #[test]
1121
    fn it_converts_band_filter_params() {
1✔
1122
        let api = BandFilter {
1✔
1123
            r#type: Default::default(),
1✔
1124
            params: BandFilterParameters {
1✔
1125
                bands: BandsByNameOrIndex::Name(vec!["nir".to_string(), "red".to_string()]),
1✔
1126
            },
1✔
1127
            sources: Box::new(SingleRasterSource {
1✔
1128
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1129
                    r#type: Default::default(),
1✔
1130
                    params: GdalSourceParameters {
1✔
1131
                        data: "example_data".to_string(),
1✔
1132
                        overview_level: None,
1✔
1133
                    },
1✔
1134
                }),
1✔
1135
            }),
1✔
1136
        };
1✔
1137

1138
        let ops = OperatorsBandFilter::try_from(api).expect("conversion failed");
1✔
1139

1140
        assert_eq!(
1✔
1141
            serde_json::to_value(ops.params).expect("params should serialize"),
1✔
1142
            json!({
1✔
1143
                "bands": ["nir", "red"]
1✔
1144
            })
1145
        );
1146
    }
1✔
1147
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc