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

geo-engine / geoengine / 28904863508

07 Jul 2026 11:05PM UTC coverage: 87.668% (-0.1%) from 87.774%
28904863508

Pull #1199

github

web-flow
Merge b0fc4a891 into 94c12fa63
Pull Request #1199: fix: add resolution to python down/up sampling operators

71 of 92 new or added lines in 6 files covered. (77.17%)

148 existing lines in 2 files now uncovered.

121588 of 138692 relevant lines covered (87.67%)

471978.04 hits per line

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

95.78
/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, Interpolation as OperatorsInterpolation,
21
    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")]
235
pub struct Fraction {
236
    /// Scaling factor in x direction.
237
    pub x: f64,
238
    /// Scaling factor in y direction.
239
    pub y: f64,
240
}
241

242
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
243
#[serde(rename_all = "camelCase", tag = "type")]
244
pub enum InterpolationResolution {
245
    #[schema(title = "Resolution")]
246
    /// Explicit output resolution (`x`, `y`) in target coordinates.
247
    Resolution { x: f64, y: f64 },
248
    #[schema(title = "Fraction")]
249
    /// Upscale factor relative to input resolution (`x >= 1`, `y >= 1`).
250
    Fraction(Fraction),
251
}
252

253
impl From<InterpolationResolution> for OperatorsInterpolationResolution {
254
    fn from(value: InterpolationResolution) -> Self {
2✔
255
        match value {
2✔
256
            InterpolationResolution::Resolution { x, y } => {
×
257
                Self::Resolution(geoengine_datatypes::primitives::SpatialResolution { x, y })
×
258
            }
259
            InterpolationResolution::Fraction(fraction) => Self::Fraction {
2✔
260
                x: fraction.x,
2✔
261
                y: fraction.y,
2✔
262
            },
2✔
263
        }
264
    }
2✔
265
}
266

267
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, ToSchema)]
268
#[serde(rename_all = "camelCase")]
269
pub enum InterpolationMethod {
270
    /// Nearest-neighbor interpolation.
271
    NearestNeighbor,
272
    /// Bilinear interpolation.
273
    BiLinear,
274
}
275

276
impl From<InterpolationMethod> for OperatorsInterpolationMethod {
277
    fn from(value: InterpolationMethod) -> Self {
2✔
278
        match value {
2✔
279
            InterpolationMethod::NearestNeighbor => Self::NearestNeighbor,
2✔
280
            InterpolationMethod::BiLinear => Self::BiLinear,
×
281
        }
282
    }
2✔
283
}
284

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

321
impl From<Aggregation> for OperatorsAggregation {
322
    fn from(value: Aggregation) -> Self {
2✔
323
        match value {
2✔
324
            Aggregation::Min { ignore_no_data } => Self::Min { ignore_no_data },
×
325
            Aggregation::Max { ignore_no_data } => Self::Max { ignore_no_data },
×
326
            Aggregation::First { ignore_no_data } => Self::First { ignore_no_data },
1✔
327
            Aggregation::Last { ignore_no_data } => Self::Last { ignore_no_data },
×
328
            Aggregation::Mean { ignore_no_data } => Self::Mean { ignore_no_data },
1✔
329
            Aggregation::Sum { ignore_no_data } => Self::Sum { ignore_no_data },
×
330
            Aggregation::Count { ignore_no_data } => Self::Count { ignore_no_data },
×
331
            Aggregation::PercentileEstimate {
332
                ignore_no_data,
×
333
                percentile,
×
334
            } => Self::PercentileEstimate {
×
335
                ignore_no_data,
×
336
                percentile,
×
337
            },
×
338
        }
339
    }
2✔
340
}
341

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

393
/// Parameters for the `Reprojection` operator.
394
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
395
#[serde(rename_all = "camelCase")]
396
pub struct ReprojectionParameters {
397
    /// Target spatial reference system.
398
    #[schema(value_type = String, examples("EPSG:32632"))]
399
    pub target_spatial_reference: SpatialReference,
400
    /// Controls how raster output bounds are derived.
401
    ///
402
    /// The default `projectionBounds` usually keeps a projection-aligned target grid,
403
    /// while `dataBounds` derives it directly from source data bounds.
404
    #[schema(examples("projectionBounds"))]
405
    #[serde(default)]
406
    pub derive_out_spec: DeriveOutRasterSpecsSource,
407
}
408

409
impl TryFrom<Reprojection> for OperatorsReprojection {
410
    type Error = anyhow::Error;
411

412
    fn try_from(value: Reprojection) -> Result<Self, Self::Error> {
12✔
413
        Ok(OperatorsReprojection {
414
            params: OperatorsReprojectionParameters {
12✔
415
                target_spatial_reference: value.params.target_spatial_reference.into(),
12✔
416
                derive_out_spec: value.params.derive_out_spec.into(),
12✔
417
            },
12✔
418
            sources: (*value.sources).try_into()?,
12✔
419
        })
420
    }
12✔
421
}
422

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

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

495
impl TryFrom<TemporalRasterAggregation> for OperatorsTemporalRasterAggregation {
496
    type Error = anyhow::Error;
497

498
    fn try_from(value: TemporalRasterAggregation) -> Result<Self, Self::Error> {
2✔
499
        Ok(OperatorsTemporalRasterAggregation {
500
            params: OperatorsTemporalRasterAggregationParameters {
2✔
501
                aggregation: value.params.aggregation.into(),
2✔
502
                window: value.params.window.into(),
2✔
503
                window_reference: value.params.window_reference.map(Into::into),
2✔
504
                output_type: value.params.output_type.map(Into::into),
2✔
505
            },
2✔
506
            sources: (*value.sources).try_into()?,
2✔
507
        })
508
    }
2✔
509
}
510

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

548
/// Parameters for the `RasterStacker` operator.
549
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
550
#[serde(rename_all = "camelCase")]
551
pub struct RasterStackerParameters {
552
    /// Strategy for deriving output band names.
553
    ///
554
    /// - `default`: appends ` (n)` with the smallest `n` that avoids a conflict.
555
    /// - `suffix`: appends one suffix per input.
556
    /// - `rename`: explicitly provides names for all resulting bands.
557
    #[schema(examples(json!({ "type": "default" }), json!({ "type": "suffix", "values": ["_a", "_b"] })))]
558
    pub rename_bands: RenameBands,
559
}
560

561
impl TryFrom<RasterStacker> for OperatorsRasterStacker {
562
    type Error = anyhow::Error;
563

564
    fn try_from(value: RasterStacker) -> Result<Self, Self::Error> {
4✔
565
        Ok(OperatorsRasterStacker {
566
            params: OperatorsRasterStackerParameters {
4✔
567
                rename_bands: value.params.rename_bands.into(),
4✔
568
            },
4✔
569
            sources: (*value.sources).try_into()?,
4✔
570
        })
571
    }
4✔
572
}
573

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

606
/// Parameters for the `RasterTypeConversion` operator.
607
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
608
#[serde(rename_all = "camelCase")]
609
pub struct RasterTypeConversionParameters {
610
    /// Output raster data type.
611
    #[schema(examples("U16"))]
612
    pub output_data_type: RasterDataType,
613
}
614

615
impl TryFrom<RasterTypeConversion> for OperatorsRasterTypeConversion {
616
    type Error = anyhow::Error;
617

618
    fn try_from(value: RasterTypeConversion) -> Result<Self, Self::Error> {
2✔
619
        Ok(OperatorsRasterTypeConversion {
620
            params: OperatorsRasterTypeConversionParameters {
2✔
621
                output_data_type: value.params.output_data_type.into(),
2✔
622
            },
2✔
623
            sources: (*value.sources).try_into()?,
2✔
624
        })
625
    }
2✔
626
}
627

628
/// The `Interpolation` operator increases raster resolution by interpolating values of an input raster.
629
///
630
/// If queried with a resolution that is coarser than the input resolution,
631
/// interpolation is not applicable and an error is returned.
632
///
633
/// ## Inputs
634
///
635
/// The `Interpolation` operator expects exactly one _raster_ input.
636
#[api_operator(
90✔
637
    title = "Interpolation",
90✔
638
    examples(json!({
90✔
639
        "type": "Interpolation",
640
        "params": {
641
            "interpolation": "nearestNeighbor",
642
            "outputResolution": {
643
                "type": "fraction",
644
                "x": 2.0,
645
                "y": 2.0
646
            }
647
        },
648
        "sources": {
649
            "raster": {
650
                "type": "MultiBandGdalSource",
651
                "params": { "data": "sentinel-2-l2a_EPSG32632_U8_20" }
652
            }
653
        }
654
    }))
655
)]
656
pub struct Interpolation {
657
    pub params: InterpolationParameters,
658
    pub sources: Box<SingleRasterSource>,
659
}
660

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

676
impl TryFrom<Interpolation> for OperatorsInterpolation {
677
    type Error = anyhow::Error;
678

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

691
/// The `Downsampling` operator decreases raster resolution by sampling values of an input raster.
692
///
693
/// If queried with a resolution that is finer than the input resolution,
694
/// downsampling is not applicable and an error is returned.
695
///
696
/// ## Inputs
697
///
698
/// The `Downsampling` operator expects exactly one _raster_ input.
699
#[api_operator(
90✔
700
    title = "Downsampling",
90✔
701
    examples(json!({
90✔
702
        "type": "Downsampling",
703
        "params": {
704
            "samplingMethod": "nearestNeighbor",
705
            "outputResolution": {
706
                "type": "fraction",
707
                "x": 2.0,
708
                "y": 2.0
709
            }
710
        },
711
        "sources": {
712
            "raster": {
713
                "type": "MultiBandGdalSource",
714
                "params": { "data": "sentinel-2-l2a_EPSG32632_U8_20" }
715
            }
716
        }
717
    }))
718
)]
719
pub struct Downsampling {
720
    pub params: DownsamplingParameters,
721
    pub sources: Box<SingleRasterSource>,
722
}
723

724
/// Parameters for the `Downsampling` operator.
725
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
726
#[serde(rename_all = "camelCase")]
727
pub struct DownsamplingParameters {
728
    /// Downsampling method.
729
    #[schema(examples("nearestNeighbor"))]
730
    pub sampling_method: DownsamplingMethod,
731
    /// Target output resolution.
732
    #[schema(examples(json!({ "type": "fraction", "x": 2.0, "y": 2.0 })))]
733
    pub output_resolution: DownsamplingResolution,
734
    /// Optional reference point used to align the output grid origin.
735
    #[schema(examples(json!({ "x": 0.0, "y": 0.0 })))]
736
    pub output_origin_reference: Option<crate::api::model::datatypes::Coordinate2D>,
737
}
738

739
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
740
#[serde(rename_all = "camelCase")]
741
pub enum DownsamplingMethod {
742
    /// Nearest-neighbor downsampling.
743
    NearestNeighbor,
744
}
745

746
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, ToSchema)]
747
#[serde(rename_all = "camelCase", tag = "type")]
748
pub enum DownsamplingResolution {
749
    #[schema(title = "Resolution")]
750
    /// Explicit output resolution (`x`, `y`) in target coordinates.
751
    Resolution { x: f64, y: f64 },
752
    #[schema(title = "Fraction")]
753
    /// Downscale factor relative to input resolution (`x >= 1`, `y >= 1`).
754
    Fraction(Fraction),
755
}
756

757
impl From<DownsamplingResolution> for OperatorsDownsamplingResolution {
758
    fn from(value: DownsamplingResolution) -> Self {
1✔
759
        match value {
1✔
NEW
760
            DownsamplingResolution::Resolution { x, y } => {
×
NEW
761
                Self::Resolution(geoengine_datatypes::primitives::SpatialResolution { x, y })
×
762
            }
763
            DownsamplingResolution::Fraction(fraction) => Self::Fraction {
1✔
764
                x: fraction.x,
1✔
765
                y: fraction.y,
1✔
766
            },
1✔
767
        }
768
    }
1✔
769
}
770

771
impl From<DownsamplingMethod> for OperatorsDownsamplingMethod {
772
    fn from(value: DownsamplingMethod) -> Self {
1✔
773
        match value {
1✔
774
            DownsamplingMethod::NearestNeighbor => Self::NearestNeighbor,
1✔
775
        }
776
    }
1✔
777
}
778

779
impl TryFrom<Downsampling> for OperatorsDownsampling {
780
    type Error = anyhow::Error;
781

782
    fn try_from(value: Downsampling) -> Result<Self, Self::Error> {
1✔
783
        Ok(OperatorsDownsampling {
784
            params: OperatorsDownsamplingParameters {
1✔
785
                sampling_method: value.params.sampling_method.into(),
1✔
786
                output_resolution: value.params.output_resolution.into(),
1✔
787
                output_origin_reference: value.params.output_origin_reference.map(Into::into),
1✔
788
            },
1✔
789
            sources: (*value.sources).try_into()?,
1✔
790
        })
791
    }
1✔
792
}
793

794
/// The `BandFilter` operator selects bands from a raster source by band names or band indices.
795
///
796
/// It removes all non-selected bands while preserving the original order of remaining bands.
797
///
798
/// ## Inputs
799
///
800
/// The `BandFilter` operator expects exactly one _raster_ input.
801
///
802
/// ## Errors
803
///
804
/// The operator returns an error if no bands are selected or if selected band names/indices
805
/// cannot be mapped to existing input bands.
806
#[api_operator(
90✔
807
    title = "Band Filter",
90✔
808
    examples(json!({
90✔
809
        "type": "BandFilter",
810
        "params": {
811
            "bands": ["nir", "red"]
812
        },
813
        "sources": {
814
            "raster": {
815
                "type": "MultiBandGdalSource",
816
                "params": { "data": "sentinel-2-l2a_EPSG32632_U16_10" }
817
            }
818
        }
819
    }))
820
)]
821
pub struct BandFilter {
822
    pub params: BandFilterParameters,
823
    pub sources: Box<SingleRasterSource>,
824
}
825

826
/// Parameters for the `BandFilter` operator.
827
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]
828
#[serde(rename_all = "camelCase")]
829
pub struct BandFilterParameters {
830
    /// Selected bands either by names (e.g. `["nir", "red"]`) or indices (e.g. `[0, 2]`).
831
    #[schema(examples(json!(["nir", "red"]), json!([0, 2])))]
832
    pub bands: BandsByNameOrIndex,
833
}
834

835
impl TryFrom<BandFilter> for OperatorsBandFilter {
836
    type Error = anyhow::Error;
837

838
    fn try_from(value: BandFilter) -> Result<Self, Self::Error> {
3✔
839
        let params = serde_json::from_value::<OperatorsBandFilterParameters>(
3✔
840
            serde_json::to_value(value.params)?,
3✔
841
        )?;
×
842

843
        Ok(OperatorsBandFilter {
844
            params,
3✔
845
            sources: (*value.sources).try_into()?,
3✔
846
        })
847
    }
3✔
848
}
849

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

911
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
912
#[serde(rename_all = "camelCase")]
913
pub struct RasterVectorJoinParameters {
914
    /// Specify how the new column names are derived from the raster band names.
915
    ///
916
    /// The `ColumnNames` type is used to specify how the new column names are derived from the raster band names.
917
    ///
918
    /// - **default**: Appends " (n)" to the band name with the smallest `n` that avoids a conflict.
919
    /// - **suffix**: Specifies a suffix for each input, to be appended to the band names.
920
    /// - **rename**: A list of names for each new column.
921
    ///
922
    #[schema(examples(
923
        json!({"type": "default"}),
924
        json!({"type": "suffix", "values": ["_sentinel2"]}),
925
        json!({"type": "rename", "values": ["red", "green", "blue"]}),
926
    ))]
927
    pub names: ColumnNames,
928
    /// The aggregation function to use for features covering multiple pixels.
929
    #[schema(examples("first"))]
930
    pub feature_aggregation: FeatureAggregationMethod,
931
    /// Whether to ignore no data values in the aggregation. Defaults to `false`.
932
    #[serde(default)]
933
    #[schema(examples(true))]
934
    pub feature_aggregation_ignore_no_data: bool,
935
    /// The aggregation function to use for features covering multiple (raster) time steps.
936
    #[schema(examples("mean"))]
937
    pub temporal_aggregation: TemporalAggregationMethod,
938
    /// Whether to ignore no data values in the aggregation. Defaults to `false`.
939
    #[serde(default)]
940
    #[schema(examples(true))]
941
    pub temporal_aggregation_ignore_no_data: bool,
942
}
943

944
impl TryFrom<RasterVectorJoin> for OperatorsRasterVectorJoin {
945
    type Error = anyhow::Error;
946

947
    fn try_from(value: RasterVectorJoin) -> Result<Self, Self::Error> {
2✔
948
        Ok(OperatorsRasterVectorJoin {
949
            params: OperatorsRasterVectorJoinParameters {
2✔
950
                names: value.params.names.into(),
2✔
951
                feature_aggregation: value.params.feature_aggregation.into(),
2✔
952
                feature_aggregation_ignore_no_data: value.params.feature_aggregation_ignore_no_data,
2✔
953
                temporal_aggregation: value.params.temporal_aggregation.into(),
2✔
954
                temporal_aggregation_ignore_no_data: value
2✔
955
                    .params
2✔
956
                    .temporal_aggregation_ignore_no_data,
2✔
957
            },
2✔
958
            sources: (*value.sources).try_into()?,
2✔
959
        })
960
    }
2✔
961
}
962

963
#[cfg(test)]
964
mod tests {
965

966
    use super::*;
967
    use crate::api::model::{
968
        datatypes::{Coordinate2D, TimeGranularity},
969
        processing_graphs::{
970
            RasterOperator, SingleRasterOrVectorOperator, VectorOperator,
971
            parameters::SpatialBoundsDerive,
972
            source::{
973
                GdalSource, GdalSourceParameters, MockPointSource, MockPointSourceParameters,
974
                MultiBandGdalSource,
975
            },
976
        },
977
    };
978
    use serde_json::json;
979

980
    #[test]
981
    fn it_converts_expressions() {
1✔
982
        let api = Expression {
1✔
983
            r#type: Default::default(),
1✔
984
            params: ExpressionParameters {
1✔
985
                expression: "2 * A + B".to_string(),
1✔
986
                output_type: RasterDataType::F32,
1✔
987
                output_band: None,
1✔
988
                map_no_data: true,
1✔
989
            },
1✔
990
            sources: Box::new(SingleRasterSource {
1✔
991
                raster: RasterOperator::GdalSource(GdalSource {
1✔
992
                    r#type: Default::default(),
1✔
993
                    params: GdalSourceParameters {
1✔
994
                        data: "example_data".to_string(),
1✔
995
                        overview_level: None,
1✔
996
                    },
1✔
997
                }),
1✔
998
            }),
1✔
999
        };
1✔
1000

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

1003
        assert_eq!(ops.params.expression, "2 * A + B");
1✔
1004
        assert_eq!(
1✔
1005
            ops.params.output_type,
1006
            geoengine_datatypes::raster::RasterDataType::F32
1007
        );
1008
        assert!(ops.params.output_band.is_none());
1✔
1009
        assert!(ops.params.map_no_data);
1✔
1010
    }
1✔
1011

1012
    #[test]
1013
    fn it_converts_raster_vector_join_params() {
1✔
1014
        let api = RasterVectorJoin {
1✔
1015
            r#type: Default::default(),
1✔
1016
            params: RasterVectorJoinParameters {
1✔
1017
                names: ColumnNames::Names {
1✔
1018
                    values: vec!["a".to_string(), "b".to_string()],
1✔
1019
                },
1✔
1020
                feature_aggregation: FeatureAggregationMethod::First,
1✔
1021
                feature_aggregation_ignore_no_data: true,
1✔
1022
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
1023
                temporal_aggregation_ignore_no_data: false,
1✔
1024
            },
1✔
1025
            sources: Box::new(SingleVectorMultipleRasterSources {
1✔
1026
                vector: VectorOperator::MockPointSource(MockPointSource {
1✔
1027
                    r#type: Default::default(),
1✔
1028
                    params: MockPointSourceParameters {
1✔
1029
                        points: vec![Coordinate2D { x: 0.0, y: 0.0 }],
1✔
1030
                        spatial_bounds: SpatialBoundsDerive::Derive(Default::default()),
1✔
1031
                    },
1✔
1032
                }),
1✔
1033
                rasters: vec![RasterOperator::GdalSource(GdalSource {
1✔
1034
                    r#type: Default::default(),
1✔
1035
                    params: GdalSourceParameters {
1✔
1036
                        data: "example_data".to_string(),
1✔
1037
                        overview_level: None,
1✔
1038
                    },
1✔
1039
                })],
1✔
1040
            }),
1✔
1041
        };
1✔
1042

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

1045
        assert!(matches!(
1✔
1046
            ops_params.params.names,
1✔
1047
            geoengine_operators::processing::ColumnNames::Names(_)
1048
        ));
1049
        assert_eq!(
1✔
1050
            ops_params.params.feature_aggregation,
1051
            geoengine_operators::processing::FeatureAggregationMethod::First
1052
        );
1053
        assert!(ops_params.params.feature_aggregation_ignore_no_data);
1✔
1054
        assert_eq!(
1✔
1055
            ops_params.params.temporal_aggregation,
1056
            geoengine_operators::processing::TemporalAggregationMethod::Mean
1057
        );
1058
        assert!(!ops_params.params.temporal_aggregation_ignore_no_data);
1✔
1059
    }
1✔
1060

1061
    #[test]
1062
    fn it_converts_reprojection_params() {
1✔
1063
        let api = Reprojection {
1✔
1064
            r#type: Default::default(),
1✔
1065
            params: ReprojectionParameters {
1✔
1066
                target_spatial_reference: "EPSG:32632".parse().expect("valid srs"),
1✔
1067
                derive_out_spec: DeriveOutRasterSpecsSource::ProjectionBounds,
1✔
1068
            },
1✔
1069
            sources: Box::new(SingleRasterOrVectorSource {
1✔
1070
                source: SingleRasterOrVectorOperator::Vector(VectorOperator::MockPointSource(
1✔
1071
                    MockPointSource {
1✔
1072
                        r#type: Default::default(),
1✔
1073
                        params: MockPointSourceParameters {
1✔
1074
                            points: vec![Coordinate2D { x: 0.0, y: 0.0 }],
1✔
1075
                            spatial_bounds: SpatialBoundsDerive::Derive(Default::default()),
1✔
1076
                        },
1✔
1077
                    },
1✔
1078
                )),
1✔
1079
            }),
1✔
1080
        };
1✔
1081

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

1084
        assert_eq!(
1✔
1085
            ops.params.target_spatial_reference.to_string(),
1✔
1086
            "EPSG:32632"
1087
        );
1088
        assert!(matches!(
1✔
1089
            ops.params.derive_out_spec,
1✔
1090
            geoengine_operators::processing::DeriveOutRasterSpecsSource::ProjectionBounds
1091
        ));
1092
    }
1✔
1093

1094
    #[test]
1095
    fn it_converts_temporal_raster_aggregation_params() {
1✔
1096
        let api = TemporalRasterAggregation {
1✔
1097
            r#type: Default::default(),
1✔
1098
            params: TemporalRasterAggregationParameters {
1✔
1099
                aggregation: Aggregation::Mean {
1✔
1100
                    ignore_no_data: true,
1✔
1101
                },
1✔
1102
                window: crate::api::model::datatypes::TimeStep {
1✔
1103
                    granularity: TimeGranularity::Months,
1✔
1104
                    step: 1,
1✔
1105
                },
1✔
1106
                window_reference: None,
1✔
1107
                output_type: None,
1✔
1108
            },
1✔
1109
            sources: Box::new(SingleRasterSource {
1✔
1110
                raster: RasterOperator::GdalSource(GdalSource {
1✔
1111
                    r#type: Default::default(),
1✔
1112
                    params: GdalSourceParameters {
1✔
1113
                        data: "example_data".to_string(),
1✔
1114
                        overview_level: None,
1✔
1115
                    },
1✔
1116
                }),
1✔
1117
            }),
1✔
1118
        };
1✔
1119

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

1122
        assert!(matches!(
1✔
1123
            ops.params.aggregation,
1✔
1124
            geoengine_operators::processing::Aggregation::Mean {
1125
                ignore_no_data: true
1126
            }
1127
        ));
1128
        assert_eq!(ops.params.window.step, 1);
1✔
1129
        assert!(ops.params.window_reference.is_none());
1✔
1130
        assert!(ops.params.output_type.is_none());
1✔
1131
    }
1✔
1132

1133
    #[test]
1134
    fn it_converts_raster_stacker_params() {
1✔
1135
        let api = RasterStacker {
1✔
1136
            r#type: Default::default(),
1✔
1137
            params: RasterStackerParameters {
1✔
1138
                rename_bands: RenameBands::Suffix(vec!["_a".to_string(), "_b".to_string()]),
1✔
1139
            },
1✔
1140
            sources: Box::new(MultipleRasterSources {
1✔
1141
                rasters: vec![
1✔
1142
                    RasterOperator::GdalSource(GdalSource {
1✔
1143
                        r#type: Default::default(),
1✔
1144
                        params: GdalSourceParameters {
1✔
1145
                            data: "example_data_a".to_string(),
1✔
1146
                            overview_level: None,
1✔
1147
                        },
1✔
1148
                    }),
1✔
1149
                    RasterOperator::GdalSource(GdalSource {
1✔
1150
                        r#type: Default::default(),
1✔
1151
                        params: GdalSourceParameters {
1✔
1152
                            data: "example_data_b".to_string(),
1✔
1153
                            overview_level: None,
1✔
1154
                        },
1✔
1155
                    }),
1✔
1156
                ],
1✔
1157
            }),
1✔
1158
        };
1✔
1159

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

1162
        assert_eq!(
1✔
1163
            ops.params.rename_bands,
1164
            geoengine_datatypes::raster::RenameBands::Suffix(vec![
1✔
1165
                "_a".to_string(),
1✔
1166
                "_b".to_string()
1✔
1167
            ])
1✔
1168
        );
1169
        assert_eq!(ops.sources.rasters.len(), 2);
1✔
1170
    }
1✔
1171

1172
    #[test]
1173
    fn it_converts_raster_type_conversion_params() {
1✔
1174
        let api = RasterTypeConversion {
1✔
1175
            r#type: Default::default(),
1✔
1176
            params: RasterTypeConversionParameters {
1✔
1177
                output_data_type: RasterDataType::U16,
1✔
1178
            },
1✔
1179
            sources: Box::new(SingleRasterSource {
1✔
1180
                raster: RasterOperator::GdalSource(GdalSource {
1✔
1181
                    r#type: Default::default(),
1✔
1182
                    params: GdalSourceParameters {
1✔
1183
                        data: "example_data".to_string(),
1✔
1184
                        overview_level: None,
1✔
1185
                    },
1✔
1186
                }),
1✔
1187
            }),
1✔
1188
        };
1✔
1189

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

1192
        assert_eq!(
1✔
1193
            ops.params.output_data_type,
1194
            geoengine_datatypes::raster::RasterDataType::U16
1195
        );
1196
    }
1✔
1197

1198
    #[test]
1199
    fn it_converts_interpolation_params() {
1✔
1200
        let api = Interpolation {
1✔
1201
            r#type: Default::default(),
1✔
1202
            params: InterpolationParameters {
1✔
1203
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
1204
                output_resolution: InterpolationResolution::Fraction(Fraction { x: 2.0, y: 2.0 }),
1✔
1205
                output_origin_reference: None,
1✔
1206
            },
1✔
1207
            sources: Box::new(SingleRasterSource {
1✔
1208
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1209
                    r#type: Default::default(),
1✔
1210
                    params: GdalSourceParameters {
1✔
1211
                        data: "example_data".to_string(),
1✔
1212
                        overview_level: None,
1✔
1213
                    },
1✔
1214
                }),
1✔
1215
            }),
1✔
1216
        };
1✔
1217

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

1220
        assert!(matches!(
1✔
1221
            ops.params.interpolation,
1✔
1222
            geoengine_operators::processing::InterpolationMethod::NearestNeighbor
1223
        ));
1224
        assert!(matches!(
1✔
1225
            ops.params.output_resolution,
1✔
1226
            geoengine_operators::processing::InterpolationResolution::Fraction { x, y }
1✔
1227
            if (x - 2.0).abs() < f64::EPSILON && (y - 2.0).abs() < f64::EPSILON
1✔
1228
        ));
1229
        assert!(ops.params.output_origin_reference.is_none());
1✔
1230
    }
1✔
1231

1232
    #[test]
1233
    fn it_converts_downsampling_params() {
1✔
1234
        let api = Downsampling {
1✔
1235
            r#type: Default::default(),
1✔
1236
            params: DownsamplingParameters {
1✔
1237
                sampling_method: DownsamplingMethod::NearestNeighbor,
1✔
1238
                output_resolution: DownsamplingResolution::Fraction(Fraction { x: 2.0, y: 2.0 }),
1✔
1239
                output_origin_reference: Some(crate::api::model::datatypes::Coordinate2D {
1✔
1240
                    x: 0.0,
1✔
1241
                    y: 0.0,
1✔
1242
                }),
1✔
1243
            },
1✔
1244
            sources: Box::new(SingleRasterSource {
1✔
1245
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1246
                    r#type: Default::default(),
1✔
1247
                    params: GdalSourceParameters {
1✔
1248
                        data: "example_data".to_string(),
1✔
1249
                        overview_level: None,
1✔
1250
                    },
1✔
1251
                }),
1✔
1252
            }),
1✔
1253
        };
1✔
1254

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

1257
        assert!(matches!(
1✔
1258
            ops.params.sampling_method,
1✔
1259
            geoengine_operators::processing::DownsamplingMethod::NearestNeighbor
1260
        ));
1261
        assert!(matches!(
1✔
1262
            ops.params.output_resolution,
1✔
1263
            geoengine_operators::processing::DownsamplingResolution::Fraction { x, y }
1✔
1264
            if (x - 2.0).abs() < f64::EPSILON && (y - 2.0).abs() < f64::EPSILON
1✔
1265
        ));
1266
        assert_eq!(
1✔
1267
            ops.params.output_origin_reference,
1268
            Some(geoengine_datatypes::primitives::Coordinate2D::new(0.0, 0.0))
1✔
1269
        );
1270
    }
1✔
1271

1272
    #[test]
1273
    fn it_converts_band_filter_params() {
1✔
1274
        let api = BandFilter {
1✔
1275
            r#type: Default::default(),
1✔
1276
            params: BandFilterParameters {
1✔
1277
                bands: BandsByNameOrIndex::Name(vec!["nir".to_string(), "red".to_string()]),
1✔
1278
            },
1✔
1279
            sources: Box::new(SingleRasterSource {
1✔
1280
                raster: RasterOperator::MultiBandGdalSource(MultiBandGdalSource {
1✔
1281
                    r#type: Default::default(),
1✔
1282
                    params: GdalSourceParameters {
1✔
1283
                        data: "example_data".to_string(),
1✔
1284
                        overview_level: None,
1✔
1285
                    },
1✔
1286
                }),
1✔
1287
            }),
1✔
1288
        };
1✔
1289

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

1292
        assert_eq!(
1✔
1293
            serde_json::to_value(ops.params).expect("params should serialize"),
1✔
1294
            json!({
1✔
1295
                "bands": ["nir", "red"]
1✔
1296
            })
1297
        );
1298
    }
1✔
1299
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc