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

geo-engine / geoengine / 28980782324

08 Jul 2026 10:41PM UTC coverage: 87.772% (-0.002%) from 87.774%
28980782324

Pull #1199

github

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

70 of 84 new or added lines in 3 files covered. (83.33%)

4 existing lines in 2 files now uncovered.

121730 of 138689 relevant lines covered (87.77%)

471979.27 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
    Fraction(Fraction),
250
}
251

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

777
impl TryFrom<Downsampling> for OperatorsDownsampling {
778
    type Error = anyhow::Error;
779

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

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

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

833
impl TryFrom<BandFilter> for OperatorsBandFilter {
834
    type Error = anyhow::Error;
835

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

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

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

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

942
impl TryFrom<RasterVectorJoin> for OperatorsRasterVectorJoin {
943
    type Error = anyhow::Error;
944

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

961
#[cfg(test)]
962
mod tests {
963

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1290
        assert_eq!(
1✔
1291
            serde_json::to_value(ops.params).expect("params should serialize"),
1✔
1292
            json!({
1✔
1293
                "bands": ["nir", "red"]
1✔
1294
            })
1295
        );
1296
    }
1✔
1297
}
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