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

geo-engine / geoengine / 29338235936

14 Jul 2026 01:17PM UTC coverage: 87.769% (-0.005%) from 87.774%
29338235936

push

github

web-flow
fix: add resolution to python down/up sampling operators (#1199)

* fix: add resolution and outputOriginReference to python down/up sampling operators

- Added output_origin_reference support to Interpolation and Downsampling
- Fix duplicate 'fraction' branch bug in outputResolution parser
- Add Downsampling to RasterOperator dispatch
- Add websocket error logging in workflows.rs
- Add inspect_err on tile stream

* feat(backend): add Downsampling operator and Fraction type to API model

Also update www plugin to extract oneOf descriptions for operator parameter tables.

* chore(api-clients): regenerate OpenAPI spec, API clients, and docs

* remove commented-out assert

* fix: deduplicate Fraction1/Fraction2 in generated API clients

Remove variant-level doc comments from Fraction(Fraction) in both
InterpolationResolution and DownsamplingResolution enums. The
Fraction struct's own field docs already describe the data — the
context-specific descriptions ('Upscale factor' / 'Downscale factor')
caused the OpenAPI generator to create separate Fraction1 and Fraction2
classes for structurally identical allOf wrappers.

With identical allOf wrappers the generator now produces a single shared
Fraction1 (the tagged variant with the 'type: fraction' discriminator)
used by both enums.

* revert: remove non-essential changes from PR

- Revert websocket error logging (workflows.rs, websocket_stream.rs)
- Revert Jupyter notebook metadata changes (interpolation.ipynb)
- Revert unrelated TemporalRasterAggregation doc updates
- Revert auto-generated python types.md to_api_dict docs

These changes are not essential to the Downsampling feature.

* revert: non-essential docs improvements

Revert Interpolation.md description update and astro-openapi-plugin.ts
oneOf fallback — not essential to the Downsampling feature.

* revert: non-essential types.py changes

Revert SpatialResolution typed dict, assert deletion, and GeoTransform
docstring — not required for the Downsampli... (continued)

72 of 97 new or added lines in 5 files covered. (74.23%)

2 existing lines in 1 file now uncovered.

121729 of 138692 relevant lines covered (87.77%)

471993.26 hits per line

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

95.73
/geoengine/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
    Downsampling as OperatorsDownsampling, DownsamplingMethod as OperatorsDownsamplingMethod,
18
    DownsamplingParams as OperatorsDownsamplingParameters,
19
    DownsamplingResolution as OperatorsDownsamplingResolution, Expression as OperatorsExpression,
20
    ExpressionParams as OperatorsExpressionParameters, Fraction as OperatorsFraction,
21
    Interpolation as OperatorsInterpolation, InterpolationMethod as OperatorsInterpolationMethod,
22
    InterpolationParams as OperatorsInterpolationParameters,
23
    InterpolationResolution as OperatorsInterpolationResolution,
24
    RasterStacker as OperatorsRasterStacker,
25
    RasterStackerParams as OperatorsRasterStackerParameters,
26
    RasterTypeConversion as OperatorsRasterTypeConversion,
27
    RasterTypeConversionParams as OperatorsRasterTypeConversionParameters,
28
    RasterVectorJoin as OperatorsRasterVectorJoin,
29
    RasterVectorJoinParams as OperatorsRasterVectorJoinParameters,
30
    Reprojection as OperatorsReprojection, ReprojectionParams as OperatorsReprojectionParameters,
31
    TemporalRasterAggregation as OperatorsTemporalRasterAggregation,
32
    TemporalRasterAggregationParameters as OperatorsTemporalRasterAggregationParameters,
33
};
34
use serde::{Deserialize, Serialize};
35
use utoipa::ToSchema;
36

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

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

168
impl TryFrom<Expression> for OperatorsExpression {
169
    type Error = anyhow::Error;
170

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

184
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
185
#[serde(rename_all = "camelCase", tag = "type", content = "values")]
186
pub enum RenameBands {
187
    #[schema(title = "Default")]
188
    Default,
189
    #[schema(title = "Suffix")]
190
    Suffix(Vec<String>),
191
    #[schema(title = "Rename")]
192
    Rename(Vec<String>),
193
}
194

195
impl From<RenameBands> for geoengine_datatypes::raster::RenameBands {
196
    fn from(value: RenameBands) -> Self {
4✔
197
        match value {
4✔
198
            RenameBands::Default => Self::Default,
1✔
199
            RenameBands::Suffix(values) => Self::Suffix(values),
1✔
200
            RenameBands::Rename(values) => Self::Rename(values),
2✔
201
        }
202
    }
4✔
203
}
204

205
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
206
#[serde(untagged)]
207
pub enum BandsByNameOrIndex {
208
    /// Select bands by their names.
209
    Name(Vec<String>),
210
    /// Select bands by zero-based band indices.
211
    Index(Vec<usize>),
212
}
213

214
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ToSchema, Default)]
215
#[serde(rename_all = "camelCase")]
216
pub enum DeriveOutRasterSpecsSource {
217
    /// Derive output bounds from source data bounds.
218
    DataBounds,
219
    /// Derive output bounds from the target projection bounds.
220
    #[default]
221
    ProjectionBounds,
222
}
223

224
impl From<DeriveOutRasterSpecsSource> for OperatorsDeriveOutRasterSpecsSource {
225
    fn from(value: DeriveOutRasterSpecsSource) -> Self {
12✔
226
        match value {
12✔
227
            DeriveOutRasterSpecsSource::DataBounds => Self::DataBounds,
10✔
228
            DeriveOutRasterSpecsSource::ProjectionBounds => Self::ProjectionBounds,
2✔
229
        }
230
    }
12✔
231
}
232

233
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
234
#[serde(rename_all = "camelCase", tag = "type")]
235
pub enum InterpolationResolution {
236
    #[schema(title = "Resolution")]
237
    /// Explicit output resolution (`x`, `y`) in target coordinates.
238
    Resolution { x: f64, y: f64 },
239
    #[schema(title = "Fraction")]
240
    /// Scaling factor in x/y direction.
241
    Fraction { x: f64, y: f64 },
242
}
243

244
impl From<InterpolationResolution> for OperatorsInterpolationResolution {
245
    fn from(value: InterpolationResolution) -> Self {
2✔
246
        match value {
2✔
247
            InterpolationResolution::Resolution { x, y } => {
×
248
                Self::Resolution(geoengine_datatypes::primitives::SpatialResolution { x, y })
×
249
            }
250
            InterpolationResolution::Fraction { x, y } => {
2✔
251
                Self::Fraction(OperatorsFraction { x, y })
2✔
252
            }
253
        }
254
    }
2✔
255
}
256

257
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ToSchema)]
258
#[serde(rename_all = "camelCase")]
259
pub enum InterpolationMethod {
260
    /// Nearest-neighbor interpolation.
261
    NearestNeighbor,
262
    /// Bilinear interpolation.
263
    BiLinear,
264
}
265

266
impl From<InterpolationMethod> for OperatorsInterpolationMethod {
267
    fn from(value: InterpolationMethod) -> Self {
2✔
268
        match value {
2✔
269
            InterpolationMethod::NearestNeighbor => Self::NearestNeighbor,
2✔
270
            InterpolationMethod::BiLinear => Self::BiLinear,
×
271
        }
272
    }
2✔
273
}
274

275
/// Aggregation methods for `TemporalRasterAggregation`.
276
///
277
/// Available variants are `min`, `max`, `first`, `last`, `mean`, `sum`, `count`, and `percentileEstimate`.
278
/// Encountering NO DATA makes the aggregation result NO DATA unless `ignoreNoData` is `true`.
279
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
280
#[serde(rename_all = "camelCase", tag = "type")]
281
pub enum Aggregation {
282
    #[schema(title = "MinAggregation")]
283
    #[serde(rename_all = "camelCase")]
284
    Min { ignore_no_data: bool },
285
    #[schema(title = "MaxAggregation")]
286
    #[serde(rename_all = "camelCase")]
287
    Max { ignore_no_data: bool },
288
    #[schema(title = "FirstAggregation")]
289
    #[serde(rename_all = "camelCase")]
290
    First { ignore_no_data: bool },
291
    #[schema(title = "LastAggregation")]
292
    #[serde(rename_all = "camelCase")]
293
    Last { ignore_no_data: bool },
294
    #[schema(title = "MeanAggregation")]
295
    #[serde(rename_all = "camelCase")]
296
    Mean { ignore_no_data: bool },
297
    #[schema(title = "SumAggregation")]
298
    #[serde(rename_all = "camelCase")]
299
    Sum { ignore_no_data: bool },
300
    #[schema(title = "CountAggregation")]
301
    #[serde(rename_all = "camelCase")]
302
    Count { ignore_no_data: bool },
303
    #[schema(title = "PercentileEstimateAggregation")]
304
    #[serde(rename_all = "camelCase")]
305
    PercentileEstimate {
306
        ignore_no_data: bool,
307
        percentile: f64,
308
    },
309
}
310

311
impl From<Aggregation> for OperatorsAggregation {
312
    fn from(value: Aggregation) -> Self {
2✔
313
        match value {
2✔
314
            Aggregation::Min { ignore_no_data } => Self::Min { ignore_no_data },
×
315
            Aggregation::Max { ignore_no_data } => Self::Max { ignore_no_data },
×
316
            Aggregation::First { ignore_no_data } => Self::First { ignore_no_data },
1✔
317
            Aggregation::Last { ignore_no_data } => Self::Last { ignore_no_data },
×
318
            Aggregation::Mean { ignore_no_data } => Self::Mean { ignore_no_data },
1✔
319
            Aggregation::Sum { ignore_no_data } => Self::Sum { ignore_no_data },
×
320
            Aggregation::Count { ignore_no_data } => Self::Count { ignore_no_data },
×
321
            Aggregation::PercentileEstimate {
322
                ignore_no_data,
×
323
                percentile,
×
324
            } => Self::PercentileEstimate {
×
325
                ignore_no_data,
×
326
                percentile,
×
327
            },
×
328
        }
329
    }
2✔
330
}
331

332
/// The `Reprojection` operator reprojects data from one spatial reference system to another.
333
/// It accepts exactly one input which can either be a raster or a vector data stream.
334
/// The operator produces all data that, after reprojection, is contained in the query rectangle.
335
///
336
/// ## Data Type Specifics
337
///
338
/// The concrete behavior depends on the data type.
339
///
340
/// ### Vector Data
341
///
342
/// The operator reprojects all coordinates of the features individually.
343
/// The result contains all features that, after reprojection, are intersected by the query rectangle.
344
///
345
/// ### Raster Data
346
///
347
/// To create tiles in the target projection, the operator loads corresponding tiles in the source projection.
348
/// For each output pixel, the value of the nearest input pixel is used.
349
///
350
/// If parts of a tile are outside of the source extent after projection, the operator produces NO DATA values.
351
///
352
/// ## Inputs
353
///
354
/// The `Reprojection` operator expects exactly one _raster_ or _vector_ input.
355
///
356
/// ## Errors
357
///
358
/// The operator returns an error if the target projection is unknown or if input data cannot be reprojected.
359
#[api_operator(
170✔
360
    title = "Reprojection",
170✔
361
    examples(json!({
170✔
362
        "type": "Reprojection",
363
        "params": {
364
            "deriveOutSpec": "projectionBounds",
365
            "targetSpatialReference": "EPSG:32632"
366
        },
367
        "sources": {
368
            "source": {
369
                "type": "MockPointSource",
370
                "params": {
371
                    "points": [{ "x": 8.77069, "y": 50.80904 }],
372
                    "spatialBounds": { "type": "none" }
373
                }
374
            }
375
        }
376
    }))
377
)]
378
pub struct Reprojection {
379
    pub params: ReprojectionParameters,
380
    pub sources: Box<SingleRasterOrVectorSource>,
381
}
382

383
/// Parameters for the `Reprojection` operator.
384
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
385
#[serde(rename_all = "camelCase")]
386
pub struct ReprojectionParameters {
387
    /// Target spatial reference system.
388
    #[schema(value_type = String, examples("EPSG:32632"))]
389
    pub target_spatial_reference: SpatialReference,
390
    /// Controls how raster output bounds are derived.
391
    ///
392
    /// The default `projectionBounds` usually keeps a projection-aligned target grid,
393
    /// while `dataBounds` derives it directly from source data bounds.
394
    #[schema(examples("projectionBounds"))]
395
    #[serde(default)]
396
    pub derive_out_spec: DeriveOutRasterSpecsSource,
397
}
398

399
impl TryFrom<Reprojection> for OperatorsReprojection {
400
    type Error = anyhow::Error;
401

402
    fn try_from(value: Reprojection) -> Result<Self, Self::Error> {
12✔
403
        Ok(OperatorsReprojection {
404
            params: OperatorsReprojectionParameters {
12✔
405
                target_spatial_reference: value.params.target_spatial_reference.into(),
12✔
406
                derive_out_spec: value.params.derive_out_spec.into(),
12✔
407
            },
12✔
408
            sources: (*value.sources).try_into()?,
12✔
409
        })
410
    }
12✔
411
}
412

413
/// The `TemporalRasterAggregation` operator aggregates a raster time series into uniform time windows.
414
/// The output starts with the first window that contains the query start and contains all windows
415
/// that overlap the query interval.
416
///
417
/// Pixel values are computed by aggregating all input rasters that contribute to the current window.
418
///
419
/// ## Inputs
420
///
421
/// The `TemporalRasterAggregation` operator expects exactly one _raster_ input.
422
///
423
/// ## Errors
424
///
425
/// If the aggregation method is `first`, `last`, or `mean` and the input raster has no NO DATA value,
426
/// an error is returned.
427
#[api_operator(
90✔
428
    title = "Temporal Raster Aggregation",
90✔
429
    examples(json!({
90✔
430
        "type": "TemporalRasterAggregation",
431
        "params": {
432
            "aggregation": { "type": "mean", "ignoreNoData": true },
433
            "window": { "granularity": "months", "step": 1 }
434
        },
435
        "sources": {
436
            "raster": {
437
                "type": "Expression",
438
                "params": {
439
                    "expression": "(A - B) / (A + B)",
440
                    "outputType": "F32",
441
                    "outputBand": {
442
                        "name": "NDVI",
443
                        "measurement": { "type": "unitless" }
444
                    },
445
                    "mapNoData": false
446
                },
447
                "sources": {
448
                    "raster": {
449
                        "type": "GdalSource",
450
                        "params": { "data": "ndvi" }
451
                    }
452
                }
453
            }
454
        }
455
    }))
456
)]
457
pub struct TemporalRasterAggregation {
458
    pub params: TemporalRasterAggregationParameters,
459
    pub sources: Box<SingleRasterSource>,
460
}
461

462
/// Parameters for the `TemporalRasterAggregation` operator.
463
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
464
#[serde(rename_all = "camelCase")]
465
pub struct TemporalRasterAggregationParameters {
466
    /// Aggregation method for values within each time window.
467
    ///
468
    /// Encountering NO DATA makes the aggregation result NO DATA unless
469
    /// `ignoreNoData` is `true` for the selected aggregation variant.
470
    #[schema(examples(json!({ "type": "mean", "ignoreNoData": true })))]
471
    pub aggregation: Aggregation,
472
    /// Window size and granularity for the output time series.
473
    #[schema(examples(json!({ "granularity": "months", "step": 1 })))]
474
    pub window: TimeStep,
475
    /// Optional reference timestamp used as the anchor for window boundaries.
476
    ///
477
    /// If omitted, windows are anchored at `1970-01-01T00:00:00Z`.
478
    #[schema(examples("2020-01-01T00:00:00.000Z"))]
479
    pub window_reference: Option<TimeInstance>,
480
    /// Optional output raster data type.
481
    #[schema(examples("F32"))]
482
    pub output_type: Option<RasterDataType>,
483
}
484

485
impl TryFrom<TemporalRasterAggregation> for OperatorsTemporalRasterAggregation {
486
    type Error = anyhow::Error;
487

488
    fn try_from(value: TemporalRasterAggregation) -> Result<Self, Self::Error> {
2✔
489
        Ok(OperatorsTemporalRasterAggregation {
490
            params: OperatorsTemporalRasterAggregationParameters {
2✔
491
                aggregation: value.params.aggregation.into(),
2✔
492
                window: value.params.window.into(),
2✔
493
                window_reference: value.params.window_reference.map(Into::into),
2✔
494
                output_type: value.params.output_type.map(Into::into),
2✔
495
            },
2✔
496
            sources: (*value.sources).try_into()?,
2✔
497
        })
498
    }
2✔
499
}
500

501
/// The `RasterStacker` stacks all of its inputs into a single raster time series.
502
/// It queries all inputs and combines them by band, space, and then time.
503
///
504
/// The output raster has as many bands as the sum of all input bands.
505
/// Tiles are automatically temporally aligned.
506
///
507
/// All inputs must have the same data type and spatial reference.
508
///
509
/// ## Inputs
510
///
511
/// The `RasterStacker` operator expects multiple raster inputs.
512
#[api_operator(
90✔
513
    title = "Raster Stacker",
90✔
514
    examples(json!({
90✔
515
        "type": "RasterStacker",
516
        "params": {
517
            "renameBands": { "type": "default" }
518
        },
519
        "sources": {
520
            "rasters": [
521
                {
522
                    "type": "GdalSource",
523
                    "params": { "data": "example-a" }
524
                },
525
                {
526
                    "type": "GdalSource",
527
                    "params": { "data": "example-b" }
528
                }
529
            ]
530
        }
531
    }))
532
)]
533
pub struct RasterStacker {
534
    pub params: RasterStackerParameters,
535
    pub sources: Box<MultipleRasterSources>,
536
}
537

538
/// Parameters for the `RasterStacker` operator.
539
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
540
#[serde(rename_all = "camelCase")]
541
pub struct RasterStackerParameters {
542
    /// Strategy for deriving output band names.
543
    ///
544
    /// - `default`: appends ` (n)` with the smallest `n` that avoids a conflict.
545
    /// - `suffix`: appends one suffix per input.
546
    /// - `rename`: explicitly provides names for all resulting bands.
547
    #[schema(examples(json!({ "type": "default" }), json!({ "type": "suffix", "values": ["_a", "_b"] })))]
548
    pub rename_bands: RenameBands,
549
}
550

551
impl TryFrom<RasterStacker> for OperatorsRasterStacker {
552
    type Error = anyhow::Error;
553

554
    fn try_from(value: RasterStacker) -> Result<Self, Self::Error> {
4✔
555
        Ok(OperatorsRasterStacker {
556
            params: OperatorsRasterStackerParameters {
4✔
557
                rename_bands: value.params.rename_bands.into(),
4✔
558
            },
4✔
559
            sources: (*value.sources).try_into()?,
4✔
560
        })
561
    }
4✔
562
}
563

564
/// The `RasterTypeConversion` operator changes the data type of raster pixels.
565
///
566
/// Applying this conversion may cause precision loss.
567
/// For example, converting `F32` value `3.1` to `U8` results in `3`.
568
///
569
/// If a value is outside of the range of the target data type,
570
/// it is clipped to the valid range of that type.
571
/// For example, converting `F32` value `300.0` to `U8` results in `255`.
572
///
573
/// ## Inputs
574
///
575
/// The `RasterTypeConversion` operator expects exactly one _raster_ input.
576
#[api_operator(
90✔
577
    title = "Raster Type Conversion",
90✔
578
    examples(json!({
90✔
579
        "type": "RasterTypeConversion",
580
        "params": {
581
            "outputDataType": "U16"
582
        },
583
        "sources": {
584
            "raster": {
585
                "type": "GdalSource",
586
                "params": { "data": "example" }
587
            }
588
        }
589
    }))
590
)]
591
pub struct RasterTypeConversion {
592
    pub params: RasterTypeConversionParameters,
593
    pub sources: Box<SingleRasterSource>,
594
}
595

596
/// Parameters for the `RasterTypeConversion` operator.
597
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
598
#[serde(rename_all = "camelCase")]
599
pub struct RasterTypeConversionParameters {
600
    /// Output raster data type.
601
    #[schema(examples("U16"))]
602
    pub output_data_type: RasterDataType,
603
}
604

605
impl TryFrom<RasterTypeConversion> for OperatorsRasterTypeConversion {
606
    type Error = anyhow::Error;
607

608
    fn try_from(value: RasterTypeConversion) -> Result<Self, Self::Error> {
2✔
609
        Ok(OperatorsRasterTypeConversion {
610
            params: OperatorsRasterTypeConversionParameters {
2✔
611
                output_data_type: value.params.output_data_type.into(),
2✔
612
            },
2✔
613
            sources: (*value.sources).try_into()?,
2✔
614
        })
615
    }
2✔
616
}
617

618
/// The `Interpolation` operator increases raster resolution by interpolating values of an input raster.
619
///
620
/// If queried with a resolution that is coarser than the input resolution,
621
/// interpolation is not applicable and an error is returned.
622
///
623
/// ## Inputs
624
///
625
/// The `Interpolation` operator expects exactly one _raster_ input.
626
///
627
/// ## Resolution
628
///
629
/// The target resolution can be specified either as an explicit `Resolution` (in pixel units)
630
/// or as a `Fraction` that scales the input resolution.
631
///
632
/// ```rust,ignore
633
/// // Scale the input resolution by a factor of 2 in both x and y directions
634
/// InterpolationResolution::Fraction(Fraction { x: 2.0, y: 2.0 })
635
/// ```
636
///
637
/// ```rust,ignore
638
/// // Use an explicit resolution of 50×50 pixel units
639
/// InterpolationResolution::Resolution(SpatialResolution { x: 50.0, y: 50.0 })
640
/// ```
641
#[api_operator(
90✔
642
    title = "Interpolation",
90✔
643
    examples(json!({
90✔
644
        "type": "Interpolation",
645
        "params": {
646
            "interpolation": "nearestNeighbor",
647
            "outputResolution": {
648
                "type": "fraction",
649
                "x": 2.0,
650
                "y": 2.0
651
            }
652
        },
653
        "sources": {
654
            "raster": {
655
                "type": "MultiBandGdalSource",
656
                "params": { "data": "sentinel-2-l2a_EPSG32632_U8_20" }
657
            }
658
        }
659
    }))
660
)]
661
pub struct Interpolation {
662
    pub params: InterpolationParameters,
663
    pub sources: Box<SingleRasterSource>,
664
}
665

666
/// Parameters for the `Interpolation` operator.
667
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
668
#[serde(rename_all = "camelCase")]
669
pub struct InterpolationParameters {
670
    /// Interpolation method.
671
    #[schema(examples("nearestNeighbor"))]
672
    pub interpolation: InterpolationMethod,
673
    /// Target output resolution.
674
    #[schema(examples(json!({ "type": "fraction", "x": 2.0, "y": 2.0 })))]
675
    pub output_resolution: InterpolationResolution,
676
    /// Optional reference point used to align the output grid origin.
677
    #[schema(examples(json!({ "x": 0.0, "y": 0.0 })))]
678
    pub output_origin_reference: Option<crate::api::model::datatypes::Coordinate2D>,
679
}
680

681
impl TryFrom<Interpolation> for OperatorsInterpolation {
682
    type Error = anyhow::Error;
683

684
    fn try_from(value: Interpolation) -> Result<Self, Self::Error> {
2✔
685
        Ok(OperatorsInterpolation {
686
            params: OperatorsInterpolationParameters {
2✔
687
                interpolation: value.params.interpolation.into(),
2✔
688
                output_resolution: value.params.output_resolution.into(),
2✔
689
                output_origin_reference: value.params.output_origin_reference.map(Into::into),
2✔
690
            },
2✔
691
            sources: (*value.sources).try_into()?,
2✔
692
        })
693
    }
2✔
694
}
695

696
/// The `Downsampling` operator decreases raster resolution by sampling values of an input raster.
697
///
698
/// If queried with a resolution that is finer than the input resolution,
699
/// downsampling is not applicable and an error is returned.
700
///
701
/// ## Inputs
702
///
703
/// The `Downsampling` operator expects exactly one _raster_ input.
704
///
705
/// ## Resolution
706
///
707
/// The target resolution can be specified either as an explicit `Resolution` (in pixel units)
708
/// or as a `Fraction` that scales the input resolution.
709
///
710
/// ```rust,ignore
711
/// // Scale the input resolution by a factor of 2 in both x and y directions
712
/// DownsamplingResolution::Fraction(Fraction { x: 2.0, y: 2.0 })
713
/// ```
714
///
715
/// ```rust,ignore
716
/// // Use an explicit resolution of 200×200 pixel units
717
/// DownsamplingResolution::Resolution(SpatialResolution { x: 200.0, y: 200.0 })
718
/// ```
719
#[api_operator(
90✔
720
    title = "Downsampling",
90✔
721
    examples(json!({
90✔
722
        "type": "Downsampling",
723
        "params": {
724
            "samplingMethod": "nearestNeighbor",
725
            "outputResolution": {
726
                "type": "fraction",
727
                "x": 2.0,
728
                "y": 2.0
729
            }
730
        },
731
        "sources": {
732
            "raster": {
733
                "type": "MultiBandGdalSource",
734
                "params": { "data": "sentinel-2-l2a_EPSG32632_U8_20" }
735
            }
736
        }
737
    }))
738
)]
739
pub struct Downsampling {
740
    pub params: DownsamplingParameters,
741
    pub sources: Box<SingleRasterSource>,
742
}
743

744
/// Parameters for the `Downsampling` operator.
745
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
746
#[serde(rename_all = "camelCase")]
747
pub struct DownsamplingParameters {
748
    /// Downsampling method.
749
    #[schema(examples("nearestNeighbor"))]
750
    pub sampling_method: DownsamplingMethod,
751
    /// Target output resolution.
752
    #[schema(examples(json!({ "type": "fraction", "x": 2.0, "y": 2.0 })))]
753
    pub output_resolution: DownsamplingResolution,
754
    /// Optional reference point used to align the output grid origin.
755
    #[schema(examples(json!({ "x": 0.0, "y": 0.0 })))]
756
    pub output_origin_reference: Option<crate::api::model::datatypes::Coordinate2D>,
757
}
758

759
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
760
#[serde(rename_all = "camelCase")]
761
pub enum DownsamplingMethod {
762
    /// Nearest-neighbor downsampling.
763
    NearestNeighbor,
764
}
765

766
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
767
#[serde(rename_all = "camelCase", tag = "type")]
768
pub enum DownsamplingResolution {
769
    #[schema(title = "Resolution")]
770
    /// Explicit output resolution (`x`, `y`) in target coordinates.
771
    Resolution { x: f64, y: f64 },
772
    #[schema(title = "Fraction")]
773
    /// Scaling factor in x/y direction.
774
    Fraction { x: f64, y: f64 },
775
}
776

777
impl From<DownsamplingResolution> for OperatorsDownsamplingResolution {
778
    fn from(value: DownsamplingResolution) -> Self {
1✔
779
        match value {
1✔
NEW
780
            DownsamplingResolution::Resolution { x, y } => {
×
NEW
781
                Self::Resolution(geoengine_datatypes::primitives::SpatialResolution { x, y })
×
782
            }
783
            DownsamplingResolution::Fraction { x, y } => Self::Fraction(OperatorsFraction { x, y }),
1✔
784
        }
785
    }
1✔
786
}
787

788
impl From<DownsamplingMethod> for OperatorsDownsamplingMethod {
789
    fn from(value: DownsamplingMethod) -> Self {
1✔
790
        match value {
1✔
791
            DownsamplingMethod::NearestNeighbor => Self::NearestNeighbor,
1✔
792
        }
793
    }
1✔
794
}
795

796
impl TryFrom<Downsampling> for OperatorsDownsampling {
797
    type Error = anyhow::Error;
798

799
    fn try_from(value: Downsampling) -> Result<Self, Self::Error> {
1✔
800
        Ok(OperatorsDownsampling {
801
            params: OperatorsDownsamplingParameters {
1✔
802
                sampling_method: value.params.sampling_method.into(),
1✔
803
                output_resolution: value.params.output_resolution.into(),
1✔
804
                output_origin_reference: value.params.output_origin_reference.map(Into::into),
1✔
805
            },
1✔
806
            sources: (*value.sources).try_into()?,
1✔
807
        })
808
    }
1✔
809
}
810

811
/// The `BandFilter` operator selects bands from a raster source by band names or band indices.
812
///
813
/// It removes all non-selected bands while preserving the original order of remaining bands.
814
///
815
/// ## Inputs
816
///
817
/// The `BandFilter` operator expects exactly one _raster_ input.
818
///
819
/// ## Errors
820
///
821
/// The operator returns an error if no bands are selected or if selected band names/indices
822
/// cannot be mapped to existing input bands.
823
#[api_operator(
90✔
824
    title = "Band Filter",
90✔
825
    examples(json!({
90✔
826
        "type": "BandFilter",
827
        "params": {
828
            "bands": ["nir", "red"]
829
        },
830
        "sources": {
831
            "raster": {
832
                "type": "MultiBandGdalSource",
833
                "params": { "data": "sentinel-2-l2a_EPSG32632_U16_10" }
834
            }
835
        }
836
    }))
837
)]
838
pub struct BandFilter {
839
    pub params: BandFilterParameters,
840
    pub sources: Box<SingleRasterSource>,
841
}
842

843
/// Parameters for the `BandFilter` operator.
844
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
845
#[serde(rename_all = "camelCase")]
846
pub struct BandFilterParameters {
847
    /// Selected bands either by names (e.g. `["nir", "red"]`) or indices (e.g. `[0, 2]`).
848
    #[schema(examples(json!(["nir", "red"]), json!([0, 2])))]
849
    pub bands: BandsByNameOrIndex,
850
}
851

852
impl TryFrom<BandFilter> for OperatorsBandFilter {
853
    type Error = anyhow::Error;
854

855
    fn try_from(value: BandFilter) -> Result<Self, Self::Error> {
3✔
856
        let params = serde_json::from_value::<OperatorsBandFilterParameters>(
3✔
857
            serde_json::to_value(value.params)?,
3✔
858
        )?;
×
859

860
        Ok(OperatorsBandFilter {
861
            params,
3✔
862
            sources: (*value.sources).try_into()?,
3✔
863
        })
864
    }
3✔
865
}
866

867
/// The `RasterVectorJoin` operator allows combining a single vector input and multiple raster inputs.
868
/// For each raster input, a new column is added to the collection from the vector input.
869
/// The new column contains the value of the raster at the location of the vector feature.
870
/// For features covering multiple pixels like `MultiPoints` or `MultiPolygons`, the value is calculated using an aggregation function selected by the user.
871
/// The same is true if the temporal extent of a vector feature covers multiple raster time steps.
872
/// More details are described below.
873
///
874
/// **Example**:
875
/// You have a collection of agricultural fields (`Polygons`) and a collection of raster images containing each pixel's monthly NDVI value.
876
/// For your application, you want to know the NDVI value of each field.
877
/// The `RasterVectorJoin` operator allows you to combine the vector and raster data and offers multiple spatial and temporal aggregation strategies.
878
/// For example, you can use the `first` aggregation function to get the NDVI value of the first pixel that intersects with each field.
879
/// This is useful for exploratory analysis since the computation is very fast.
880
/// To calculate the mean NDVI value of all pixels that intersect with the field you should use the `mean` aggregation function.
881
/// Since the NDVI data is a monthly time series, you have to specify the temporal aggregation function as well.
882
/// The default is `none` which will create a new feature for each month.
883
/// Other options are `first` and `mean` which will calculate the first or mean NDVI value for each field over time.
884
///
885
/// ## Inputs
886
///
887
/// The `RasterVectorJoin` operator expects one _vector_ input and one or more _raster_ inputs.
888
///
889
/// | Parameter | Type                                |
890
/// | --------- | ----------------------------------- |
891
/// | `sources` | `SingleVectorMultipleRasterSources` |
892
///
893
/// ## Errors
894
///
895
/// If the length of `names` is not equal to the number of raster inputs, an error is thrown.
896
///
897
#[api_operator(
90✔
898
    title = "Raster Vector Join",
90✔
899
    examples(json!({
90✔
900
        "type": "RasterVectorJoin",
901
        "params": {
902
            "names": ["NDVI"],
903
            "featureAggregation": "first",
904
            "temporalAggregation": "mean",
905
            "temporalAggregationIgnoreNoData": true
906
        },
907
        "sources": {
908
            "vector": {
909
                "type": "OgrSource",
910
                "params": {
911
                    "data": "places"
912
                }
913
            },
914
            "rasters": [{
915
                "type": "GdalSource",
916
                "params": {
917
                "data": "ndvi"
918
                }
919
            }]
920
        }
921
    }))
922
)]
923
pub struct RasterVectorJoin {
924
    pub params: RasterVectorJoinParameters,
925
    pub sources: Box<SingleVectorMultipleRasterSources>,
926
}
927

928
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
929
#[serde(rename_all = "camelCase")]
930
pub struct RasterVectorJoinParameters {
931
    /// Specify how the new column names are derived from the raster band names.
932
    ///
933
    /// The `ColumnNames` type is used to specify how the new column names are derived from the raster band names.
934
    ///
935
    /// - **default**: Appends " (n)" to the band name with the smallest `n` that avoids a conflict.
936
    /// - **suffix**: Specifies a suffix for each input, to be appended to the band names.
937
    /// - **rename**: A list of names for each new column.
938
    ///
939
    #[schema(examples(
940
        json!({"type": "default"}),
941
        json!({"type": "suffix", "values": ["_sentinel2"]}),
942
        json!({"type": "rename", "values": ["red", "green", "blue"]}),
943
    ))]
944
    pub names: ColumnNames,
945
    /// The aggregation function to use for features covering multiple pixels.
946
    #[schema(examples("first"))]
947
    pub feature_aggregation: FeatureAggregationMethod,
948
    /// Whether to ignore no data values in the aggregation. Defaults to `false`.
949
    #[serde(default)]
950
    #[schema(examples(true))]
951
    pub feature_aggregation_ignore_no_data: bool,
952
    /// The aggregation function to use for features covering multiple (raster) time steps.
953
    #[schema(examples("mean"))]
954
    pub temporal_aggregation: TemporalAggregationMethod,
955
    /// Whether to ignore no data values in the aggregation. Defaults to `false`.
956
    #[serde(default)]
957
    #[schema(examples(true))]
958
    pub temporal_aggregation_ignore_no_data: bool,
959
}
960

961
impl TryFrom<RasterVectorJoin> for OperatorsRasterVectorJoin {
962
    type Error = anyhow::Error;
963

964
    fn try_from(value: RasterVectorJoin) -> Result<Self, Self::Error> {
2✔
965
        Ok(OperatorsRasterVectorJoin {
966
            params: OperatorsRasterVectorJoinParameters {
2✔
967
                names: value.params.names.into(),
2✔
968
                feature_aggregation: value.params.feature_aggregation.into(),
2✔
969
                feature_aggregation_ignore_no_data: value.params.feature_aggregation_ignore_no_data,
2✔
970
                temporal_aggregation: value.params.temporal_aggregation.into(),
2✔
971
                temporal_aggregation_ignore_no_data: value
2✔
972
                    .params
2✔
973
                    .temporal_aggregation_ignore_no_data,
2✔
974
            },
2✔
975
            sources: (*value.sources).try_into()?,
2✔
976
        })
977
    }
2✔
978
}
979

980
#[cfg(test)]
981
mod tests {
982

983
    use super::*;
984
    use crate::api::model::{
985
        datatypes::{Coordinate2D, TimeGranularity},
986
        processing_graphs::{
987
            RasterOperator, SingleRasterOrVectorOperator, VectorOperator,
988
            parameters::SpatialBoundsDerive,
989
            source::{
990
                GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters,
991
                MultiBandGdalSource,
992
            },
993
        },
994
    };
995
    use serde_json::json;
996

997
    #[test]
998
    fn it_converts_expressions() {
1✔
999
        let api = Expression {
1✔
1000
            r#type: Default::default(),
1✔
1001
            params: ExpressionParameters {
1✔
1002
                expression: "2 * A + B".to_string(),
1✔
1003
                output_type: RasterDataType::F32,
1✔
1004
                output_band: None,
1✔
1005
                map_no_data: true,
1✔
1006
            },
1✔
1007
            sources: Box::new(SingleRasterSource {
1✔
1008
                raster: RasterOperator::GdalSource(GdalSource {
1✔
1009
                    r#type: Default::default(),
1✔
1010
                    params: GdalSourceParameters {
1✔
1011
                        data: "example_data".to_string(),
1✔
1012
                        overview_level: None,
1✔
1013
                    },
1✔
1014
                }),
1✔
1015
            }),
1✔
1016
        };
1✔
1017

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

1020
        assert_eq!(ops.params.expression, "2 * A + B");
1✔
1021
        assert_eq!(
1✔
1022
            ops.params.output_type,
1023
            geoengine_datatypes::raster::RasterDataType::F32
1024
        );
1025
        assert!(ops.params.output_band.is_none());
1✔
1026
        assert!(ops.params.map_no_data);
1✔
1027
    }
1✔
1028

1029
    #[test]
1030
    fn it_converts_raster_vector_join_params() {
1✔
1031
        let api = RasterVectorJoin {
1✔
1032
            r#type: Default::default(),
1✔
1033
            params: RasterVectorJoinParameters {
1✔
1034
                names: ColumnNames::Names {
1✔
1035
                    values: vec!["a".to_string(), "b".to_string()],
1✔
1036
                },
1✔
1037
                feature_aggregation: FeatureAggregationMethod::First,
1✔
1038
                feature_aggregation_ignore_no_data: true,
1✔
1039
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
1040
                temporal_aggregation_ignore_no_data: false,
1✔
1041
            },
1✔
1042
            sources: Box::new(SingleVectorMultipleRasterSources {
1✔
1043
                vector: VectorOperator::MockPointSource(MockPointSource {
1✔
1044
                    r#type: Default::default(),
1✔
1045
                    params: MockPointSourceParameters {
1✔
1046
                        points: vec![Coordinate2D { x: 0.0, y: 0.0 }],
1✔
1047
                        spatial_bounds: SpatialBoundsDerive::Derive(Default::default()),
1✔
1048
                    },
1✔
1049
                }),
1✔
1050
                rasters: vec![RasterOperator::GdalSource(GdalSource {
1✔
1051
                    r#type: Default::default(),
1✔
1052
                    params: GdalSourceParameters {
1✔
1053
                        data: "example_data".to_string(),
1✔
1054
                        overview_level: None,
1✔
1055
                    },
1✔
1056
                })],
1✔
1057
            }),
1✔
1058
        };
1✔
1059

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

1062
        assert!(matches!(
1✔
1063
            ops_params.params.names,
1✔
1064
            geoengine_operators::processing::ColumnNames::Names(_)
1065
        ));
1066
        assert_eq!(
1✔
1067
            ops_params.params.feature_aggregation,
1068
            geoengine_operators::processing::FeatureAggregationMethod::First
1069
        );
1070
        assert!(ops_params.params.feature_aggregation_ignore_no_data);
1✔
1071
        assert_eq!(
1✔
1072
            ops_params.params.temporal_aggregation,
1073
            geoengine_operators::processing::TemporalAggregationMethod::Mean
1074
        );
1075
        assert!(!ops_params.params.temporal_aggregation_ignore_no_data);
1✔
1076
    }
1✔
1077

1078
    #[test]
1079
    fn it_converts_reprojection_params() {
1✔
1080
        let api = Reprojection {
1✔
1081
            r#type: Default::default(),
1✔
1082
            params: ReprojectionParameters {
1✔
1083
                target_spatial_reference: "EPSG:32632".parse().expect("valid srs"),
1✔
1084
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1085
            },
1✔
1086
            sources: Box::new(SingleRasterOrVectorSource {
1✔
1087
                source: SingleRasterOrVectorOperator::Vector(VectorOperator::MockPointSource(
1✔
1088
                    MockPointSource {
1✔
1089
                        r#type: Default::default(),
1✔
1090
                        params: MockPointSourceParameters {
1✔
1091
                            points: vec![Coordinate2D { x: 0.0, y: 0.0 }],
1✔
1092
                            spatial_bounds: SpatialBoundsDerive::Derive(Default::default()),
1✔
1093
                        },
1✔
1094
                    },
1✔
1095
                )),
1✔
1096
            }),
1✔
1097
        };
1✔
1098

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

1101
        assert_eq!(
1✔
1102
            ops.params.target_spatial_reference.to_string(),
1✔
1103
            "EPSG:32632"
1104
        );
1105
        assert!(matches!(
1✔
1106
            ops.params.derive_out_spec,
1✔
1107
            geoengine_operators::processing::DeriveOutRasterSpecsSource::ProjectionBounds
1108
        ));
1109
    }
1✔
1110

1111
    #[test]
1112
    fn it_converts_temporal_raster_aggregation_params() {
1✔
1113
        let api = TemporalRasterAggregation {
1✔
1114
            r#type: Default::default(),
1✔
1115
            params: TemporalRasterAggregationParameters {
1✔
1116
                aggregation: Aggregation::Mean {
1✔
1117
                    ignore_no_data: true,
1✔
1118
                },
1✔
1119
                window: crate::api::model::datatypes::TimeStep {
1✔
1120
                    granularity: TimeGranularity::Months,
1✔
1121
                    step: 1,
1✔
1122
                },
1✔
1123
                window_reference: None,
1✔
1124
                output_type: None,
1✔
1125
            },
1✔
1126
            sources: Box::new(SingleRasterSource {
1✔
1127
                raster: RasterOperator::GdalSource(GdalSource {
1✔
1128
                    r#type: Default::default(),
1✔
1129
                    params: GdalSourceParameters {
1✔
1130
                        data: "example_data".to_string(),
1✔
1131
                        overview_level: None,
1✔
1132
                    },
1✔
1133
                }),
1✔
1134
            }),
1✔
1135
        };
1✔
1136

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

1139
        assert!(matches!(
1✔
1140
            ops.params.aggregation,
1✔
1141
            geoengine_operators::processing::Aggregation::Mean {
1142
                ignore_no_data: true
1143
            }
1144
        ));
1145
        assert_eq!(ops.params.window.step, 1);
1✔
1146
        assert!(ops.params.window_reference.is_none());
1✔
1147
        assert!(ops.params.output_type.is_none());
1✔
1148
    }
1✔
1149

1150
    #[test]
1151
    fn it_converts_raster_stacker_params() {
1✔
1152
        let api = RasterStacker {
1✔
1153
            r#type: Default::default(),
1✔
1154
            params: RasterStackerParameters {
1✔
1155
                rename_bands: RenameBands::Suffix(vec!["_a".to_string(), "_b".to_string()]),
1✔
1156
            },
1✔
1157
            sources: Box::new(MultipleRasterSources {
1✔
1158
                rasters: vec![
1✔
1159
                    RasterOperator::GdalSource(GdalSource {
1✔
1160
                        r#type: Default::default(),
1✔
1161
                        params: GdalSourceParameters {
1✔
1162
                            data: "example_data_a".to_string(),
1✔
1163
                            overview_level: None,
1✔
1164
                        },
1✔
1165
                    }),
1✔
1166
                    RasterOperator::GdalSource(GdalSource {
1✔
1167
                        r#type: Default::default(),
1✔
1168
                        params: GdalSourceParameters {
1✔
1169
                            data: "example_data_b".to_string(),
1✔
1170
                            overview_level: None,
1✔
1171
                        },
1✔
1172
                    }),
1✔
1173
                ],
1✔
1174
            }),
1✔
1175
        };
1✔
1176

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

1179
        assert_eq!(
1✔
1180
            ops.params.rename_bands,
1181
            geoengine_datatypes::raster::RenameBands::Suffix(vec![
1✔
1182
                "_a".to_string(),
1✔
1183
                "_b".to_string()
1✔
1184
            ])
1✔
1185
        );
1186
        assert_eq!(ops.sources.rasters.len(), 2);
1✔
1187
    }
1✔
1188

1189
    #[test]
1190
    fn it_converts_raster_type_conversion_params() {
1✔
1191
        let api = RasterTypeConversion {
1✔
1192
            r#type: Default::default(),
1✔
1193
            params: RasterTypeConversionParameters {
1✔
1194
                output_data_type: RasterDataType::U16,
1✔
1195
            },
1✔
1196
            sources: Box::new(SingleRasterSource {
1✔
1197
                raster: RasterOperator::GdalSource(GdalSource {
1✔
1198
                    r#type: Default::default(),
1✔
1199
                    params: GdalSourceParameters {
1✔
1200
                        data: "example_data".to_string(),
1✔
1201
                        overview_level: None,
1✔
1202
                    },
1✔
1203
                }),
1✔
1204
            }),
1✔
1205
        };
1✔
1206

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

1209
        assert_eq!(
1✔
1210
            ops.params.output_data_type,
1211
            geoengine_datatypes::raster::RasterDataType::U16
1212
        );
1213
    }
1✔
1214

1215
    #[test]
1216
    fn it_converts_interpolation_params() {
1✔
1217
        let api = Interpolation {
1✔
1218
            r#type: Default::default(),
1✔
1219
            params: InterpolationParameters {
1✔
1220
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
1221
                output_resolution: InterpolationResolution::Fraction { x: 2.0, y: 2.0 },
1✔
1222
                output_origin_reference: None,
1✔
1223
            },
1✔
1224
            sources: Box::new(SingleRasterSource {
1✔
1225
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1226
                    r#type: Default::default(),
1✔
1227
                    params: GdalSourceParameters {
1✔
1228
                        data: "example_data".to_string(),
1✔
1229
                        overview_level: None,
1✔
1230
                    },
1✔
1231
                }),
1✔
1232
            }),
1✔
1233
        };
1✔
1234

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

1237
        assert!(matches!(
1✔
1238
            ops.params.interpolation,
1✔
1239
            geoengine_operators::processing::InterpolationMethod::NearestNeighbor
1240
        ));
1241
        assert!(matches!(
1✔
1242
            ops.params.output_resolution,
1✔
1243
            geoengine_operators::processing::InterpolationResolution::Fraction(
1244
                geoengine_operators::processing::Fraction { x, y }
1✔
1245
            )
1246
            if (x - 2.0).abs() < f64::EPSILON && (y - 2.0).abs() < f64::EPSILON
1✔
1247
        ));
1248
        assert!(ops.params.output_origin_reference.is_none());
1✔
1249
    }
1✔
1250

1251
    #[test]
1252
    fn it_converts_downsampling_params() {
1✔
1253
        let api = Downsampling {
1✔
1254
            r#type: Default::default(),
1✔
1255
            params: DownsamplingParameters {
1✔
1256
                sampling_method: DownsamplingMethod::NearestNeighbor,
1✔
1257
                output_resolution: DownsamplingResolution::Fraction { x: 2.0, y: 2.0 },
1✔
1258
                output_origin_reference: Some(crate::api::model::datatypes::Coordinate2D {
1✔
1259
                    x: 0.0,
1✔
1260
                    y: 0.0,
1✔
1261
                }),
1✔
1262
            },
1✔
1263
            sources: Box::new(SingleRasterSource {
1✔
1264
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1265
                    r#type: Default::default(),
1✔
1266
                    params: GdalSourceParameters {
1✔
1267
                        data: "example_data".to_string(),
1✔
1268
                        overview_level: None,
1✔
1269
                    },
1✔
1270
                }),
1✔
1271
            }),
1✔
1272
        };
1✔
1273

1274
        let ops = OperatorsDownsampling::try_from(api).expect("conversion failed");
1✔
1275

1276
        assert!(matches!(
1✔
1277
            ops.params.sampling_method,
1✔
1278
            geoengine_operators::processing::DownsamplingMethod::NearestNeighbor
1279
        ));
1280
        assert!(matches!(
1✔
1281
            ops.params.output_resolution,
1✔
1282
            geoengine_operators::processing::DownsamplingResolution::Fraction(
1283
                geoengine_operators::processing::Fraction { x, y }
1✔
1284
            )
1285
            if (x - 2.0).abs() < f64::EPSILON && (y - 2.0).abs() < f64::EPSILON
1✔
1286
        ));
1287
        assert_eq!(
1✔
1288
            ops.params.output_origin_reference,
1289
            Some(geoengine_datatypes::primitives::Coordinate2D::new(0.0, 0.0))
1✔
1290
        );
1291
    }
1✔
1292

1293
    #[test]
1294
    fn it_converts_band_filter_params() {
1✔
1295
        let api = BandFilter {
1✔
1296
            r#type: Default::default(),
1✔
1297
            params: BandFilterParameters {
1✔
1298
                bands: BandsByNameOrIndex::Name(vec!["nir".to_string(), "red".to_string()]),
1✔
1299
            },
1✔
1300
            sources: Box::new(SingleRasterSource {
1✔
1301
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1302
                    r#type: Default::default(),
1✔
1303
                    params: GdalSourceParameters {
1✔
1304
                        data: "example_data".to_string(),
1✔
1305
                        overview_level: None,
1✔
1306
                    },
1✔
1307
                }),
1✔
1308
            }),
1✔
1309
        };
1✔
1310

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

1313
        assert_eq!(
1✔
1314
            serde_json::to_value(ops.params).expect("params should serialize"),
1✔
1315
            json!({
1✔
1316
                "bands": ["nir", "red"]
1✔
1317
            })
1318
        );
1319
    }
1✔
1320
}
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