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

geo-engine / geoengine / 29338235936

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

push

github

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

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

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

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

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

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

* remove commented-out assert

* fix: deduplicate Fraction1/Fraction2 in generated API clients

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

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

* revert: remove non-essential changes from PR

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

These changes are not essential to the Downsampling feature.

* revert: non-essential docs improvements

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

* revert: non-essential types.py changes

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

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

2 existing lines in 1 file now uncovered.

121729 of 138692 relevant lines covered (87.77%)

471993.26 hits per line

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

94.32
/geoengine/operators/src/processing/interpolation/mod.rs
1
use std::marker::PhantomData;
2
use std::sync::Arc;
3

4
use crate::adapters::{
5
    FoldTileAccu, FoldTileAccuMut, RasterSubQueryAdapter, SubQueryTileAggregator,
6
};
7
use crate::engine::{
8
    CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedSources, Operator,
9
    OperatorName, QueryContext, QueryProcessor, RasterOperator, RasterQueryProcessor,
10
    RasterResultDescriptor, SingleRasterSource, TypedRasterQueryProcessor, WorkflowOperatorPath,
11
};
12
use crate::optimization::{OptimizableOperator, OptimizationError};
13
use crate::processing::{
14
    Downsampling, DownsamplingMethod, DownsamplingParams, DownsamplingResolution, Fraction,
15
};
16
use crate::util::Result;
17
use async_trait::async_trait;
18
use futures::future::BoxFuture;
19
use futures::stream::BoxStream;
20
use futures::{Future, FutureExt, TryFuture, TryFutureExt};
21
use geoengine_datatypes::primitives::{BandSelection, CacheHint, Coordinate2D};
22
use geoengine_datatypes::primitives::{RasterQueryRectangle, SpatialResolution, TimeInterval};
23
use geoengine_datatypes::raster::{
24
    Bilinear, ChangeGridBounds, GeoTransform, GridBlit, GridBoundingBox2D, GridOrEmpty,
25
    InterpolationAlgorithm, NearestNeighbor, Pixel, RasterTile2D, TileInformation,
26
    TilingSpecification,
27
};
28
use rayon::ThreadPool;
29
use serde::{Deserialize, Serialize};
30
use snafu::{Snafu, ensure};
31

32
#[derive(Debug, Serialize, Deserialize, Clone)]
33
#[serde(rename_all = "camelCase")]
34
pub struct InterpolationParams {
35
    pub interpolation: InterpolationMethod,
36
    pub output_resolution: InterpolationResolution,
37
    pub output_origin_reference: Option<Coordinate2D>,
38
}
39

40
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
41
#[serde(rename_all = "camelCase", tag = "type")]
42
pub enum InterpolationResolution {
43
    Resolution(SpatialResolution),
44
    Fraction(Fraction),
45
}
46

47
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
48
#[serde(rename_all = "camelCase")]
49
pub enum InterpolationMethod {
50
    NearestNeighbor,
51
    BiLinear,
52
}
53

54
#[derive(Debug, Snafu)]
55
#[snafu(visibility(pub(crate)), context(suffix(false)), module(error))]
56
pub enum InterpolationError {
57
    #[snafu(display("The fraction used to interpolate must be >= 1, was {f}."))]
58
    FractionMustBeOneOrLarger { f: f64 },
59
    #[snafu(display("The output resolution must be higher than the input resolution."))]
60
    OutputMustBeHigherResolutionThanInput {
61
        input: SpatialResolution,
62
        output: SpatialResolution,
63
    },
64
}
65

66
pub type Interpolation = Operator<InterpolationParams, SingleRasterSource>;
67

68
impl OperatorName for Interpolation {
69
    const TYPE_NAME: &'static str = "Interpolation";
70
}
71

72
#[typetag::serde]
×
73
#[async_trait]
74
impl RasterOperator for Interpolation {
75
    async fn _initialize(
76
        self: Box<Self>,
77
        path: WorkflowOperatorPath,
78
        context: &dyn ExecutionContext,
79
    ) -> Result<Box<dyn InitializedRasterOperator>> {
17✔
80
        let name = CanonicOperatorName::from(&self);
81

82
        let initialized_sources = self
83
            .sources
84
            .initialize_sources(path.clone(), context)
85
            .await?;
86
        let raster_source = initialized_sources.raster;
87
        InitializedInterpolation::new_with_source_and_params(
88
            name,
89
            path,
90
            raster_source,
91
            &self.params,
92
            context.tiling_specification(),
93
        )
94
        .map(InitializedRasterOperator::boxed)
95
    }
17✔
96

97
    span_fn!(Interpolation);
98
}
99

100
pub struct InitializedInterpolation<O: InitializedRasterOperator> {
101
    name: CanonicOperatorName,
102
    output_result_descriptor: RasterResultDescriptor,
103
    raster_source: O,
104
    path: WorkflowOperatorPath,
105
    interpolation_method: InterpolationMethod,
106
    tiling_specification: TilingSpecification,
107
}
108

109
impl<O: InitializedRasterOperator> InitializedInterpolation<O> {
110
    pub fn new_with_source_and_params(
17✔
111
        name: CanonicOperatorName,
17✔
112
        path: WorkflowOperatorPath,
17✔
113
        raster_source: O,
17✔
114
        params: &InterpolationParams,
17✔
115
        tiling_specification: TilingSpecification,
17✔
116
    ) -> Result<Self> {
17✔
117
        let in_descriptor = raster_source.result_descriptor();
17✔
118
        let in_spatial_grid = in_descriptor.spatial_grid_descriptor();
17✔
119

120
        let output_resolution = match params.output_resolution {
17✔
121
            InterpolationResolution::Resolution(res) => {
16✔
122
                ensure!(
16✔
123
                    res.x.abs() <= in_spatial_grid.spatial_resolution().x.abs(),
16✔
124
                    error::OutputMustBeHigherResolutionThanInput {
×
125
                        input: in_spatial_grid.spatial_resolution(),
×
126
                        output: res
×
127
                    }
×
128
                );
129
                ensure!(
16✔
130
                    res.y.abs() <= in_spatial_grid.spatial_resolution().y.abs(),
16✔
131
                    error::OutputMustBeHigherResolutionThanInput {
×
132
                        input: in_spatial_grid.spatial_resolution(),
×
133
                        output: res
×
134
                    }
×
135
                );
136
                res
16✔
137
            }
138

139
            InterpolationResolution::Fraction(fraction) => {
1✔
140
                ensure!(
1✔
141
                    fraction.x >= 1.0,
1✔
NEW
142
                    error::FractionMustBeOneOrLarger { f: fraction.x }
×
143
                );
144
                ensure!(
1✔
145
                    fraction.y >= 1.0,
1✔
NEW
146
                    error::FractionMustBeOneOrLarger { f: fraction.y }
×
147
                );
148

149
                SpatialResolution::new_unchecked(
1✔
150
                    in_spatial_grid.spatial_resolution().x / fraction.x,
1✔
151
                    in_spatial_grid.spatial_resolution().y.abs() / fraction.y,
1✔
152
                )
153
            }
154
        };
155

156
        let out_spatial_grid = if let Some(oc) = params.output_origin_reference {
17✔
157
            in_spatial_grid
1✔
158
                .with_changed_resolution(output_resolution)
1✔
159
                .with_moved_origin_to_nearest_grid_edge(oc)
1✔
160
                .replace_origin(oc)
1✔
161
        } else {
162
            in_spatial_grid.with_changed_resolution(output_resolution)
16✔
163
        };
164

165
        let out_descriptor = RasterResultDescriptor {
17✔
166
            spatial_reference: in_descriptor.spatial_reference,
17✔
167
            data_type: in_descriptor.data_type,
17✔
168
            time: in_descriptor.time,
17✔
169
            spatial_grid: out_spatial_grid,
17✔
170
            bands: in_descriptor.bands.clone(),
17✔
171
        };
17✔
172

173
        let initialized_operator = InitializedInterpolation {
17✔
174
            name,
17✔
175
            path,
17✔
176
            output_result_descriptor: out_descriptor,
17✔
177
            raster_source,
17✔
178
            interpolation_method: params.interpolation,
17✔
179
            tiling_specification,
17✔
180
        };
17✔
181

182
        Ok(initialized_operator)
17✔
183
    }
17✔
184
}
185

186
impl<O: InitializedRasterOperator> InitializedRasterOperator for InitializedInterpolation<O> {
187
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
8✔
188
        let source_processor = self.raster_source.query_processor()?;
8✔
189

190
        let res = call_on_generic_raster_processor!(
8✔
191
            source_processor, p => match self.interpolation_method  {
8✔
192
                InterpolationMethod::NearestNeighbor => InterploationProcessor::<_,_, NearestNeighbor>::new(
4✔
193
                        p,
4✔
194
                        self.output_result_descriptor.clone(),
4✔
195
                        self.tiling_specification,
4✔
196
                    ).boxed()
4✔
197
                    .into(),
4✔
198
                InterpolationMethod::BiLinear =>InterploationProcessor::<_,_, Bilinear>::new(
×
199
                        p,
×
200
                        self.output_result_descriptor.clone(),
×
201
                        self.tiling_specification,
×
202
                    ).boxed()
×
203
                    .into(),
×
204
            }
205
        );
206

207
        Ok(res)
8✔
208
    }
8✔
209

210
    fn result_descriptor(&self) -> &RasterResultDescriptor {
22✔
211
        &self.output_result_descriptor
22✔
212
    }
22✔
213

214
    fn canonic_name(&self) -> CanonicOperatorName {
×
215
        self.name.clone()
×
216
    }
×
217

218
    fn name(&self) -> &'static str {
5✔
219
        Interpolation::TYPE_NAME
5✔
220
    }
5✔
221

222
    fn path(&self) -> WorkflowOperatorPath {
5✔
223
        self.path.clone()
5✔
224
    }
5✔
225

226
    fn optimize(
3✔
227
        &self,
3✔
228
        target_resolution: SpatialResolution,
3✔
229
    ) -> Result<Box<dyn RasterOperator>, OptimizationError> {
3✔
230
        self.ensure_resolution_is_compatible_for_optimization(target_resolution)?;
3✔
231

232
        let out_descriptor = self.result_descriptor();
3✔
233
        let in_descriptor = self.raster_source.result_descriptor();
3✔
234

235
        let input_resolution = in_descriptor.spatial_grid.spatial_resolution();
3✔
236

237
        let new_origin = if in_descriptor.spatial_grid.geo_transform().origin_coordinate
3✔
238
            == out_descriptor
3✔
239
                .spatial_grid
3✔
240
                .geo_transform()
3✔
241
                .origin_coordinate
3✔
242
        {
243
            None
3✔
244
        } else {
245
            Some(
×
246
                out_descriptor
×
247
                    .spatial_grid
×
248
                    .geo_transform()
×
249
                    .origin_coordinate,
×
250
            )
×
251
        };
252

253
        if input_resolution == target_resolution {
3✔
254
            // special case where interpolation becomes redundant, unless it also regrids
255

256
            // TODO: source does not need to be optimized, but we need it as an `RasterOperator` and not `InitializedRasterOperator`
257
            let optimzed_source = self.raster_source.optimize(target_resolution)?;
1✔
258

259
            if new_origin.is_some() {
1✔
260
                return Ok(Interpolation {
×
261
                    params: InterpolationParams {
×
262
                        interpolation: self.interpolation_method,
×
263
                        output_resolution: InterpolationResolution::Resolution(target_resolution),
×
264
                        output_origin_reference: new_origin,
×
265
                    },
×
266
                    sources: SingleRasterSource {
×
267
                        raster: optimzed_source,
×
268
                    },
×
269
                }
×
270
                .boxed());
×
271
            }
1✔
272
            return Ok(optimzed_source);
1✔
273
        }
2✔
274

275
        // snap the input resolution to an overview level
276
        let mut snapped_input_resolution = input_resolution;
2✔
277

278
        while snapped_input_resolution * 2.0 < target_resolution {
3✔
279
            snapped_input_resolution = snapped_input_resolution * 2.0;
1✔
280
        }
1✔
281

282
        let optimzed_source = self.raster_source.optimize(snapped_input_resolution)?;
2✔
283

284
        if snapped_input_resolution < target_resolution {
2✔
285
            // result must be coarser than the source, so we need to convert to Downsampling
286
            return Ok(Downsampling {
1✔
287
                params: DownsamplingParams {
1✔
288
                    sampling_method: DownsamplingMethod::NearestNeighbor,
1✔
289
                    output_resolution: DownsamplingResolution::Resolution(target_resolution),
1✔
290
                    output_origin_reference: new_origin,
1✔
291
                },
1✔
292
                sources: SingleRasterSource {
1✔
293
                    raster: optimzed_source,
1✔
294
                },
1✔
295
            }
1✔
296
            .boxed());
1✔
297
        }
1✔
298

299
        // target resolution is still finer than what the source produces
300
        debug_assert!(snapped_input_resolution > target_resolution);
1✔
301

302
        Ok(Interpolation {
1✔
303
            params: InterpolationParams {
1✔
304
                interpolation: self.interpolation_method,
1✔
305
                output_resolution: InterpolationResolution::Resolution(target_resolution),
1✔
306
                output_origin_reference: new_origin,
1✔
307
            },
1✔
308
            sources: SingleRasterSource {
1✔
309
                raster: optimzed_source,
1✔
310
            },
1✔
311
        }
1✔
312
        .boxed())
1✔
313
    }
3✔
314
}
315

316
pub struct InterploationProcessor<Q, P, I>
317
where
318
    Q: RasterQueryProcessor<RasterType = P>,
319
    P: Pixel,
320
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
321
{
322
    source: Q,
323
    out_result_descriptor: RasterResultDescriptor,
324
    tiling_specification: TilingSpecification,
325
    interpolation: PhantomData<I>,
326
}
327

328
impl<Q, P, I> InterploationProcessor<Q, P, I>
329
where
330
    Q: RasterQueryProcessor<RasterType = P>,
331
    P: Pixel,
332
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
333
{
334
    pub fn new(
8✔
335
        source: Q,
8✔
336
        out_result_descriptor: RasterResultDescriptor,
8✔
337
        tiling_specification: TilingSpecification,
8✔
338
    ) -> Self {
8✔
339
        Self {
8✔
340
            source,
8✔
341
            out_result_descriptor,
8✔
342
            tiling_specification,
8✔
343
            interpolation: PhantomData,
8✔
344
        }
8✔
345
    }
8✔
346
}
347

348
#[async_trait]
349
impl<Q, P, I> QueryProcessor for InterploationProcessor<Q, P, I>
350
where
351
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
352
    P: Pixel,
353
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
354
{
355
    type Output = RasterTile2D<P>;
356
    type SpatialBounds = GridBoundingBox2D;
357
    type Selection = BandSelection;
358
    type ResultDescription = RasterResultDescriptor;
359

360
    async fn _query<'a>(
361
        &'a self,
362
        query: RasterQueryRectangle,
363
        ctx: &'a dyn QueryContext,
364
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
11✔
365
        // do not interpolate if the source resolution is already fine enough
366

367
        let in_spatial_grid = self.source.result_descriptor().spatial_grid_descriptor();
368
        let out_spatial_grid = self.result_descriptor().spatial_grid_descriptor();
369

370
        // if the output resolution is the same as the input resolution, we can just forward the query // TODO: except the origin changes?
371
        if in_spatial_grid == out_spatial_grid {
372
            return self.source.query(query, ctx).await;
373
        }
374

375
        let tiling_grid_definition =
376
            out_spatial_grid.tiling_grid_definition(ctx.tiling_specification());
377

378
        // This is the tiling strategy we want to fill
379
        let tiling_strategy: geoengine_datatypes::raster::TilingStrategy =
380
            tiling_grid_definition.generate_data_tiling_strategy();
381

382
        let input_geo_transform = in_spatial_grid
383
            .tiling_grid_definition(ctx.tiling_specification())
384
            .tiling_geo_transform();
385

386
        let output_geo_transform = tiling_grid_definition.tiling_geo_transform();
387

388
        let sub_query = InterpolationSubQuery::<_, P, I> {
389
            input_geo_transform,
390
            output_geo_transform,
391
            fold_fn: fold_future,
392
            tiling_specification: self.tiling_specification,
393
            phantom: PhantomData,
394
            _phantom_pixel_type: PhantomData,
395
        };
396

397
        let time_stream = self.time_query(query.time_interval(), ctx).await?;
398

399
        Ok(Box::pin(RasterSubQueryAdapter::<'a, P, _, _, _>::new(
400
            &self.source,
401
            query,
402
            tiling_strategy,
403
            ctx,
404
            sub_query,
405
            time_stream,
406
        )))
407
    }
11✔
408

409
    fn result_descriptor(&self) -> &RasterResultDescriptor {
63✔
410
        &self.out_result_descriptor
63✔
411
    }
63✔
412
}
413

414
#[async_trait]
415
impl<Q, P, I> RasterQueryProcessor for InterploationProcessor<Q, P, I>
416
where
417
    P: Pixel,
418
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
419
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
420
{
421
    type RasterType = P;
422

423
    async fn _time_query<'a>(
424
        &'a self,
425
        query: geoengine_datatypes::primitives::TimeInterval,
426
        ctx: &'a dyn crate::engine::QueryContext,
427
    ) -> Result<futures::stream::BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>>
428
    {
5✔
429
        self.source.time_query(query, ctx).await
430
    }
5✔
431
}
432

433
#[derive(Debug, Clone)]
434
pub struct InterpolationSubQuery<F, T, I> {
435
    input_geo_transform: GeoTransform,
436
    output_geo_transform: GeoTransform, // TODO remove because in adapter?
437
    fold_fn: F,
438
    tiling_specification: TilingSpecification,
439
    phantom: PhantomData<I>,
440
    _phantom_pixel_type: PhantomData<T>,
441
}
442

443
impl<'a, T, FoldM, FoldF, I> SubQueryTileAggregator<'a, T> for InterpolationSubQuery<FoldM, T, I>
444
where
445
    T: Pixel,
446
    FoldM: Send + Sync + 'a + Clone + Fn(InterpolationAccu<T, I>, RasterTile2D<T>) -> FoldF,
447
    FoldF: Send + TryFuture<Ok = InterpolationAccu<T, I>, Error = crate::error::Error>,
448
    I: InterpolationAlgorithm<GridBoundingBox2D, T>,
449
{
450
    type FoldFuture = FoldF;
451

452
    type FoldMethod = FoldM;
453

454
    type TileAccu = InterpolationAccu<T, I>;
455
    type TileAccuFuture = BoxFuture<'a, Result<Self::TileAccu>>;
456

457
    fn new_fold_accu(
75✔
458
        &self,
75✔
459
        tile_info: TileInformation,
75✔
460
        query_rect: RasterQueryRectangle,
75✔
461
        pool: &Arc<ThreadPool>,
75✔
462
    ) -> Self::TileAccuFuture {
75✔
463
        create_accu(
75✔
464
            self.input_geo_transform,
75✔
465
            self.output_geo_transform,
75✔
466
            tile_info,
75✔
467
            &query_rect,
75✔
468
            pool.clone(),
75✔
469
            self.tiling_specification,
75✔
470
        )
75✔
471
        .boxed()
75✔
472
    }
75✔
473

474
    fn tile_query_rectangle(
75✔
475
        &self,
75✔
476
        tile_info: TileInformation,
75✔
477
        _query_rect: RasterQueryRectangle,
75✔
478
        time: TimeInterval,
75✔
479
        band_idx: u32,
75✔
480
    ) -> Result<Option<RasterQueryRectangle>> {
75✔
481
        // enlarge the spatial bounds in order to have the neighbor pixels for the interpolation
482

483
        let tile_pixel_bounds = tile_info.global_pixel_bounds();
75✔
484
        let tile_spatial_bounds = self
75✔
485
            .output_geo_transform
75✔
486
            .grid_to_spatial_bounds(&tile_pixel_bounds);
75✔
487
        let input_pixel_bounds = self
75✔
488
            .input_geo_transform
75✔
489
            .spatial_to_grid_bounds(&tile_spatial_bounds);
75✔
490
        let enlarged_input_pixel_bounds = GridBoundingBox2D::new(
75✔
491
            [
75✔
492
                input_pixel_bounds.y_min() - 1,
75✔
493
                input_pixel_bounds.x_min() - 1,
75✔
494
            ],
75✔
495
            [
75✔
496
                input_pixel_bounds.y_max() + 1,
75✔
497
                input_pixel_bounds.x_max() + 1,
75✔
498
            ],
75✔
499
        )
500
        .expect("max bounds must be larger then min bounds already");
75✔
501

502
        Ok(Some(RasterQueryRectangle::new(
75✔
503
            enlarged_input_pixel_bounds,
75✔
504
            time,
75✔
505
            BandSelection::new_single(band_idx),
75✔
506
        )))
75✔
507
    }
75✔
508

509
    fn fold_method(&self) -> Self::FoldMethod {
75✔
510
        self.fold_fn.clone()
75✔
511
    }
75✔
512
}
513

514
#[derive(Clone, Debug)]
515
pub struct InterpolationAccu<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> {
516
    pub output_info: TileInformation,
517
    pub input_tile: GridOrEmpty<GridBoundingBox2D, T>,
518
    pub input_geo_transform: GeoTransform,
519
    pub time: TimeInterval,
520
    pub cache_hint: CacheHint,
521
    pub pool: Arc<ThreadPool>,
522
    phantom: PhantomData<I>,
523
}
524

525
impl<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> InterpolationAccu<T, I> {
526
    pub fn new(
75✔
527
        input_tile: GridOrEmpty<GridBoundingBox2D, T>,
75✔
528
        input_geo_transform: GeoTransform,
75✔
529
        time: TimeInterval,
75✔
530
        cache_hint: CacheHint,
75✔
531
        output_info: TileInformation,
75✔
532
        pool: Arc<ThreadPool>,
75✔
533
    ) -> Self {
75✔
534
        InterpolationAccu {
75✔
535
            input_tile,
75✔
536
            input_geo_transform,
75✔
537
            time,
75✔
538
            cache_hint,
75✔
539
            output_info,
75✔
540
            pool,
75✔
541
            phantom: Default::default(),
75✔
542
        }
75✔
543
    }
75✔
544
}
545

546
#[async_trait]
547
impl<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> FoldTileAccu
548
    for InterpolationAccu<T, I>
549
{
550
    type RasterType = T;
551

552
    async fn into_tile(self) -> Result<RasterTile2D<Self::RasterType>> {
75✔
553
        // now that we collected all the input tile pixels we perform the actual interpolation
554

555
        let output_tile = crate::util::spawn_blocking_with_thread_pool(self.pool, move || {
75✔
556
            I::interpolate(
75✔
557
                self.input_geo_transform,
75✔
558
                &self.input_tile,
75✔
559
                self.output_info.global_geo_transform,
75✔
560
                self.output_info.global_pixel_bounds(),
75✔
561
            )
562
        })
75✔
563
        .await??;
564

565
        let output_tile = RasterTile2D::new_with_tile_info(
566
            self.time,
567
            self.output_info,
568
            0,
569
            output_tile.unbounded(),
570
            self.cache_hint,
571
        );
572

573
        Ok(output_tile)
574
    }
75✔
575

576
    fn thread_pool(&self) -> &Arc<ThreadPool> {
×
577
        &self.pool
×
578
    }
×
579
}
580

581
impl<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> FoldTileAccuMut
582
    for InterpolationAccu<T, I>
583
{
584
    fn set_time(&mut self, time: TimeInterval) {
259✔
585
        self.time = time;
259✔
586
    }
259✔
587

588
    fn set_cache_hint(&mut self, cache_hint: CacheHint) {
×
589
        self.cache_hint = cache_hint;
×
590
    }
×
591
}
592

593
pub fn create_accu<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>>(
75✔
594
    input_geo_transform: GeoTransform,
75✔
595
    output_geo_transform: GeoTransform,
75✔
596
    tile_info: TileInformation,
75✔
597
    query_rect: &RasterQueryRectangle,
75✔
598
    pool: Arc<ThreadPool>,
75✔
599
    _tiling_specification: TilingSpecification,
75✔
600
) -> impl Future<Output = Result<InterpolationAccu<T, I>>> + use<T, I> {
75✔
601
    let query_rect = query_rect.clone();
75✔
602

603
    // create an accumulator as a single tile that fits all the input tiles
604
    let time_interval = query_rect.time_interval();
75✔
605

606
    crate::util::spawn_blocking(move || {
75✔
607
        let tile_pixel_bounds = tile_info.global_pixel_bounds();
75✔
608
        let tile_spatial_bounds = output_geo_transform.grid_to_spatial_bounds(&tile_pixel_bounds);
75✔
609
        let input_pixel_bounds = input_geo_transform.spatial_to_grid_bounds(&tile_spatial_bounds);
75✔
610
        let enlarged_input_pixel_bounds = GridBoundingBox2D::new(
75✔
611
            [
75✔
612
                input_pixel_bounds.y_min() - 1,
75✔
613
                input_pixel_bounds.x_min() - 1,
75✔
614
            ],
75✔
615
            [
75✔
616
                input_pixel_bounds.y_max() + 1,
75✔
617
                input_pixel_bounds.x_max() + 1,
75✔
618
            ],
75✔
619
        )
620
        .expect("max bounds must be larger then min bounds already");
75✔
621

622
        // create a non-aligned (w.r.t. the tiling specification) grid by setting the origin to the top-left of the tile and the tile-index to [0, 0]
623
        let grid = GridOrEmpty::new_empty_shape(enlarged_input_pixel_bounds);
75✔
624

625
        InterpolationAccu::new(
75✔
626
            grid,
75✔
627
            input_geo_transform,
75✔
628
            time_interval,
75✔
629
            CacheHint::max_duration(),
75✔
630
            tile_info,
75✔
631
            pool,
75✔
632
        )
633
    })
75✔
634
    .map_err(From::from)
75✔
635
}
75✔
636

637
pub fn fold_future<T, I>(
259✔
638
    accu: InterpolationAccu<T, I>,
259✔
639
    tile: RasterTile2D<T>,
259✔
640
) -> impl Future<Output = Result<InterpolationAccu<T, I>>>
259✔
641
where
259✔
642
    T: Pixel,
259✔
643
    I: InterpolationAlgorithm<GridBoundingBox2D, T>,
259✔
644
{
645
    crate::util::spawn_blocking(|| fold_impl(accu, tile)).then(|x| async move {
259✔
646
        match x {
259✔
647
            Ok(r) => Ok(r),
259✔
648
            Err(e) => Err(e.into()),
×
649
        }
650
    })
518✔
651
}
259✔
652

653
pub fn fold_impl<T, I>(
259✔
654
    mut accu: InterpolationAccu<T, I>,
259✔
655
    tile: RasterTile2D<T>,
259✔
656
) -> InterpolationAccu<T, I>
259✔
657
where
259✔
658
    T: Pixel,
259✔
659
    I: InterpolationAlgorithm<GridBoundingBox2D, T>,
259✔
660
{
661
    // get the time now because it is not known when the accu was created
662
    accu.set_time(tile.time);
259✔
663
    accu.cache_hint.merge_with(&tile.cache_hint);
259✔
664

665
    // TODO: add a skip if both tiles are empty?
666

667
    // copy all input tiles into the accu to have all data for interpolation
668
    let in_tile = &tile.into_inner_positioned_grid();
259✔
669

670
    accu.input_tile.grid_blit_from(in_tile);
259✔
671

672
    accu
259✔
673
}
259✔
674

675
#[cfg(test)]
676
mod tests {
677
    use super::*;
678
    use futures::StreamExt;
679
    use geoengine_datatypes::{
680
        primitives::{
681
            Coordinate2D, RasterQueryRectangle, SpatialResolution, TimeInterval, TimeStep,
682
        },
683
        raster::{
684
            Grid2D, GridOrEmpty, GridShape2D, RasterDataType, RasterTile2D, RenameBands,
685
            TileInformation, TilingSpecification,
686
        },
687
        spatial_reference::SpatialReference,
688
        util::test::TestDefault,
689
    };
690

691
    use crate::{
692
        engine::{
693
            MockExecutionContext, MultipleRasterSources, RasterBandDescriptors, RasterOperator,
694
            RasterResultDescriptor, SpatialGridDescriptor, TimeDescriptor,
695
        },
696
        mock::{MockRasterSource, MockRasterSourceParams},
697
        processing::{RasterStacker, RasterStackerParams},
698
    };
699

700
    #[tokio::test]
701
    async fn nearest_neighbor_operator() -> Result<()> {
1✔
702
        let exe_ctx =
1✔
703
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
704

705
        // test raster:
706
        // [0, 10)
707
        // || 1 | 2 || 3 | 4 ||
708
        // || 5 | 6 || 7 | 8 ||
709
        //
710
        // [10, 20)
711
        // || 8 | 7 || 6 | 5 ||
712
        // || 4 | 3 || 2 | 1 ||
713

714
        // exptected raster:
715
        // [0, 10)
716
        // ||1 | 1 || 2 | 2 ||
717
        // ||1 | 1 || 2 | 2 ||
718
        // ||5 | 5 || 6 | 6 ||
719
        // ||5 | 5 || 6 | 6 ||
720

721
        let raster = make_raster(CacheHint::max_duration());
1✔
722

723
        let operator = Interpolation {
1✔
724
            params: InterpolationParams {
1✔
725
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
726
                output_resolution: InterpolationResolution::Resolution(
1✔
727
                    SpatialResolution::zero_point_five(),
1✔
728
                ),
1✔
729
                output_origin_reference: None,
1✔
730
            },
1✔
731
            sources: SingleRasterSource { raster },
1✔
732
        }
1✔
733
        .boxed()
1✔
734
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
735
        .await?;
1✔
736

737
        let processor = operator.query_processor()?.get_i8().unwrap();
1✔
738

739
        let query_rect = RasterQueryRectangle::new(
1✔
740
            GridBoundingBox2D::new([-4, 0], [-1, 7]).unwrap(),
1✔
741
            TimeInterval::new_unchecked(0, 20),
1✔
742
            BandSelection::first(),
1✔
743
        );
744
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
745

746
        let result_stream = processor.query(query_rect, &query_ctx).await?;
1✔
747

748
        let result: Vec<Result<RasterTile2D<i8>>> = result_stream.collect().await;
1✔
749
        let result = result.into_iter().collect::<Result<Vec<_>>>()?;
1✔
750

751
        let mut times: Vec<TimeInterval> = vec![TimeInterval::new_unchecked(0, 10); 8];
1✔
752
        times.append(&mut vec![TimeInterval::new_unchecked(10, 20); 8]);
1✔
753

754
        let data = vec![
1✔
755
            vec![1; 4],
1✔
756
            vec![2; 4],
1✔
757
            vec![3; 4],
1✔
758
            vec![4; 4],
1✔
759
            vec![5; 4],
1✔
760
            vec![6; 4],
1✔
761
            vec![7; 4],
1✔
762
            vec![8; 4],
1✔
763
            vec![8; 4],
1✔
764
            vec![7; 4],
1✔
765
            vec![6; 4],
1✔
766
            vec![5; 4],
1✔
767
            vec![4; 4],
1✔
768
            vec![3; 4],
1✔
769
            vec![2; 4],
1✔
770
            vec![1; 4],
1✔
771
        ];
772

773
        let valid = vec![
1✔
774
            vec![true; 4],
1✔
775
            vec![true; 4],
1✔
776
            vec![true; 4],
1✔
777
            vec![true; 4],
1✔
778
            vec![true; 4],
1✔
779
            vec![true; 4],
1✔
780
            vec![true; 4],
1✔
781
            vec![true; 4],
1✔
782
            vec![true; 4],
1✔
783
            vec![true; 4],
1✔
784
            vec![true; 4],
1✔
785
            vec![true; 4],
1✔
786
            vec![true; 4],
1✔
787
            vec![true; 4],
1✔
788
            vec![true; 4],
1✔
789
            vec![true; 4],
1✔
790
        ];
791

792
        for (i, tile) in result.into_iter().enumerate() {
16✔
793
            let tile = tile.into_materialized_tile();
16✔
794
            assert_eq!(tile.time, times[i]);
16✔
795
            assert_eq!(tile.grid_array.validity_mask.data, valid[i]);
16✔
796
            assert_eq!(tile.grid_array.inner_grid.data, data[i]);
16✔
797
        }
1✔
798

1✔
799
        Ok(())
1✔
800
    }
1✔
801

802
    #[allow(clippy::too_many_lines)]
803
    fn make_raster(cache_hint: CacheHint) -> Box<dyn RasterOperator> {
4✔
804
        // test raster:
805
        // [0, 10)
806
        // || 1 | 2 || 3 | 4 ||
807
        // || 5 | 6 || 7 | 8 ||
808
        //
809
        // [10, 20)
810
        // || 8 | 7 || 6 | 5 ||
811
        // || 4 | 3 || 2 | 1 ||
812
        let raster_tiles = vec![
4✔
813
            // we need to add no-data tiles explicit to force the cahe_hint from the mock source!
814
            RasterTile2D::new_with_tile_info(
4✔
815
                TimeInterval::new_unchecked(0, 10),
4✔
816
                TileInformation {
4✔
817
                    global_tile_position: [-2, -1].into(),
4✔
818
                    tile_size_in_pixels: [2, 2].into(),
4✔
819
                    global_geo_transform: TestDefault::test_default(),
4✔
820
                },
4✔
821
                0,
822
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
823
                cache_hint,
4✔
824
            ),
825
            RasterTile2D::new_with_tile_info(
4✔
826
                TimeInterval::new_unchecked(0, 10),
4✔
827
                TileInformation {
4✔
828
                    global_tile_position: [-2, 0].into(),
4✔
829
                    tile_size_in_pixels: [2, 2].into(),
4✔
830
                    global_geo_transform: TestDefault::test_default(),
4✔
831
                },
4✔
832
                0,
833
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
834
                cache_hint,
4✔
835
            ),
836
            RasterTile2D::new_with_tile_info(
4✔
837
                TimeInterval::new_unchecked(0, 10),
4✔
838
                TileInformation {
4✔
839
                    global_tile_position: [-2, 1].into(),
4✔
840
                    tile_size_in_pixels: [2, 2].into(),
4✔
841
                    global_geo_transform: TestDefault::test_default(),
4✔
842
                },
4✔
843
                0,
844
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
845
                cache_hint,
4✔
846
            ),
847
            RasterTile2D::new_with_tile_info(
4✔
848
                TimeInterval::new_unchecked(0, 10),
4✔
849
                TileInformation {
4✔
850
                    global_tile_position: [-2, 2].into(),
4✔
851
                    tile_size_in_pixels: [2, 2].into(),
4✔
852
                    global_geo_transform: TestDefault::test_default(),
4✔
853
                },
4✔
854
                0,
855
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
856
                cache_hint,
4✔
857
            ),
858
            RasterTile2D::new_with_tile_info(
4✔
859
                TimeInterval::new_unchecked(0, 10),
4✔
860
                TileInformation {
4✔
861
                    global_tile_position: [-1, -1].into(),
4✔
862
                    tile_size_in_pixels: [2, 2].into(),
4✔
863
                    global_geo_transform: TestDefault::test_default(),
4✔
864
                },
4✔
865
                0,
866
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
867
                cache_hint,
4✔
868
            ),
869
            RasterTile2D::<i8>::new_with_tile_info(
4✔
870
                TimeInterval::new_unchecked(0, 10),
4✔
871
                TileInformation {
4✔
872
                    global_tile_position: [-1, 0].into(),
4✔
873
                    tile_size_in_pixels: [2, 2].into(),
4✔
874
                    global_geo_transform: TestDefault::test_default(),
4✔
875
                },
4✔
876
                0,
877
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![1, 2, 5, 6]).unwrap()),
4✔
878
                cache_hint,
4✔
879
            ),
880
            RasterTile2D::new_with_tile_info(
4✔
881
                TimeInterval::new_unchecked(0, 10),
4✔
882
                TileInformation {
4✔
883
                    global_tile_position: [-1, 1].into(),
4✔
884
                    tile_size_in_pixels: [2, 2].into(),
4✔
885
                    global_geo_transform: TestDefault::test_default(),
4✔
886
                },
4✔
887
                0,
888
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![3, 4, 7, 8]).unwrap()),
4✔
889
                cache_hint,
4✔
890
            ),
891
            RasterTile2D::new_with_tile_info(
4✔
892
                TimeInterval::new_unchecked(0, 10),
4✔
893
                TileInformation {
4✔
894
                    global_tile_position: [-1, 2].into(),
4✔
895
                    tile_size_in_pixels: [2, 2].into(),
4✔
896
                    global_geo_transform: TestDefault::test_default(),
4✔
897
                },
4✔
898
                0,
899
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
900
                cache_hint,
4✔
901
            ),
902
            RasterTile2D::new_with_tile_info(
4✔
903
                TimeInterval::new_unchecked(0, 10),
4✔
904
                TileInformation {
4✔
905
                    global_tile_position: [0, -1].into(),
4✔
906
                    tile_size_in_pixels: [2, 2].into(),
4✔
907
                    global_geo_transform: TestDefault::test_default(),
4✔
908
                },
4✔
909
                0,
910
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
911
                cache_hint,
4✔
912
            ),
913
            RasterTile2D::new_with_tile_info(
4✔
914
                TimeInterval::new_unchecked(0, 10),
4✔
915
                TileInformation {
4✔
916
                    global_tile_position: [0, 0].into(),
4✔
917
                    tile_size_in_pixels: [2, 2].into(),
4✔
918
                    global_geo_transform: TestDefault::test_default(),
4✔
919
                },
4✔
920
                0,
921
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
922
                cache_hint,
4✔
923
            ),
924
            RasterTile2D::new_with_tile_info(
4✔
925
                TimeInterval::new_unchecked(0, 10),
4✔
926
                TileInformation {
4✔
927
                    global_tile_position: [0, 1].into(),
4✔
928
                    tile_size_in_pixels: [2, 2].into(),
4✔
929
                    global_geo_transform: TestDefault::test_default(),
4✔
930
                },
4✔
931
                0,
932
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
933
                cache_hint,
4✔
934
            ),
935
            RasterTile2D::new_with_tile_info(
4✔
936
                TimeInterval::new_unchecked(0, 10),
4✔
937
                TileInformation {
4✔
938
                    global_tile_position: [0, 2].into(),
4✔
939
                    tile_size_in_pixels: [2, 2].into(),
4✔
940
                    global_geo_transform: TestDefault::test_default(),
4✔
941
                },
4✔
942
                0,
943
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
944
                cache_hint,
4✔
945
            ),
946
            RasterTile2D::new_with_tile_info(
4✔
947
                TimeInterval::new_unchecked(10, 20),
4✔
948
                TileInformation {
4✔
949
                    global_tile_position: [-2, -1].into(),
4✔
950
                    tile_size_in_pixels: [2, 2].into(),
4✔
951
                    global_geo_transform: TestDefault::test_default(),
4✔
952
                },
4✔
953
                0,
954
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
955
                cache_hint,
4✔
956
            ),
957
            RasterTile2D::new_with_tile_info(
4✔
958
                TimeInterval::new_unchecked(10, 20),
4✔
959
                TileInformation {
4✔
960
                    global_tile_position: [-2, 0].into(),
4✔
961
                    tile_size_in_pixels: [2, 2].into(),
4✔
962
                    global_geo_transform: TestDefault::test_default(),
4✔
963
                },
4✔
964
                0,
965
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
966
                cache_hint,
4✔
967
            ),
968
            RasterTile2D::new_with_tile_info(
4✔
969
                TimeInterval::new_unchecked(10, 20),
4✔
970
                TileInformation {
4✔
971
                    global_tile_position: [-2, 1].into(),
4✔
972
                    tile_size_in_pixels: [2, 2].into(),
4✔
973
                    global_geo_transform: TestDefault::test_default(),
4✔
974
                },
4✔
975
                0,
976
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
977
                cache_hint,
4✔
978
            ),
979
            RasterTile2D::new_with_tile_info(
4✔
980
                TimeInterval::new_unchecked(10, 20),
4✔
981
                TileInformation {
4✔
982
                    global_tile_position: [-2, 2].into(),
4✔
983
                    tile_size_in_pixels: [2, 2].into(),
4✔
984
                    global_geo_transform: TestDefault::test_default(),
4✔
985
                },
4✔
986
                0,
987
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
988
                cache_hint,
4✔
989
            ),
990
            RasterTile2D::new_with_tile_info(
4✔
991
                TimeInterval::new_unchecked(10, 20),
4✔
992
                TileInformation {
4✔
993
                    global_tile_position: [-1, -1].into(),
4✔
994
                    tile_size_in_pixels: [2, 2].into(),
4✔
995
                    global_geo_transform: TestDefault::test_default(),
4✔
996
                },
4✔
997
                0,
998
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
999
                cache_hint,
4✔
1000
            ),
1001
            RasterTile2D::new_with_tile_info(
4✔
1002
                TimeInterval::new_unchecked(10, 20),
4✔
1003
                TileInformation {
4✔
1004
                    global_tile_position: [-1, 0].into(),
4✔
1005
                    tile_size_in_pixels: [2, 2].into(),
4✔
1006
                    global_geo_transform: TestDefault::test_default(),
4✔
1007
                },
4✔
1008
                0,
1009
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![8, 7, 4, 3]).unwrap()),
4✔
1010
                cache_hint,
4✔
1011
            ),
1012
            RasterTile2D::new_with_tile_info(
4✔
1013
                TimeInterval::new_unchecked(10, 20),
4✔
1014
                TileInformation {
4✔
1015
                    global_tile_position: [-1, 1].into(),
4✔
1016
                    tile_size_in_pixels: [2, 2].into(),
4✔
1017
                    global_geo_transform: TestDefault::test_default(),
4✔
1018
                },
4✔
1019
                0,
1020
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![6, 5, 2, 1]).unwrap()),
4✔
1021
                cache_hint,
4✔
1022
            ),
1023
            RasterTile2D::new_with_tile_info(
4✔
1024
                TimeInterval::new_unchecked(10, 20),
4✔
1025
                TileInformation {
4✔
1026
                    global_tile_position: [-1, 2].into(),
4✔
1027
                    tile_size_in_pixels: [2, 2].into(),
4✔
1028
                    global_geo_transform: TestDefault::test_default(),
4✔
1029
                },
4✔
1030
                0,
1031
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
1032
                cache_hint,
4✔
1033
            ),
1034
            RasterTile2D::new_with_tile_info(
4✔
1035
                TimeInterval::new_unchecked(10, 20),
4✔
1036
                TileInformation {
4✔
1037
                    global_tile_position: [0, -1].into(),
4✔
1038
                    tile_size_in_pixels: [2, 2].into(),
4✔
1039
                    global_geo_transform: TestDefault::test_default(),
4✔
1040
                },
4✔
1041
                0,
1042
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
1043
                cache_hint,
4✔
1044
            ),
1045
            RasterTile2D::new_with_tile_info(
4✔
1046
                TimeInterval::new_unchecked(10, 20),
4✔
1047
                TileInformation {
4✔
1048
                    global_tile_position: [0, 0].into(),
4✔
1049
                    tile_size_in_pixels: [2, 2].into(),
4✔
1050
                    global_geo_transform: TestDefault::test_default(),
4✔
1051
                },
4✔
1052
                0,
1053
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
1054
                cache_hint,
4✔
1055
            ),
1056
            RasterTile2D::new_with_tile_info(
4✔
1057
                TimeInterval::new_unchecked(10, 20),
4✔
1058
                TileInformation {
4✔
1059
                    global_tile_position: [0, 1].into(),
4✔
1060
                    tile_size_in_pixels: [2, 2].into(),
4✔
1061
                    global_geo_transform: TestDefault::test_default(),
4✔
1062
                },
4✔
1063
                0,
1064
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
1065
                cache_hint,
4✔
1066
            ),
1067
            RasterTile2D::new_with_tile_info(
4✔
1068
                TimeInterval::new_unchecked(10, 20),
4✔
1069
                TileInformation {
4✔
1070
                    global_tile_position: [0, 2].into(),
4✔
1071
                    tile_size_in_pixels: [2, 2].into(),
4✔
1072
                    global_geo_transform: TestDefault::test_default(),
4✔
1073
                },
4✔
1074
                0,
1075
                GridOrEmpty::new_empty_shape(GridShape2D::new([2, 2])),
4✔
1076
                cache_hint,
4✔
1077
            ),
1078
        ];
1079

1080
        let result_descriptor = RasterResultDescriptor {
4✔
1081
            data_type: RasterDataType::I8,
4✔
1082
            spatial_reference: SpatialReference::epsg_4326().into(),
4✔
1083
            time: TimeDescriptor::new_regular_with_epoch(
4✔
1084
                Some(
4✔
1085
                    TimeInterval::new(
4✔
1086
                        raster_tiles.first().unwrap().time.start(),
4✔
1087
                        raster_tiles.last().unwrap().time.end(),
4✔
1088
                    )
4✔
1089
                    .unwrap(),
4✔
1090
                ),
4✔
1091
                TimeStep::millis(10).unwrap(),
4✔
1092
            ),
4✔
1093
            spatial_grid: SpatialGridDescriptor::source_from_parts(
4✔
1094
                GeoTransform::new(Coordinate2D::new(0., 0.), 1.0, -1.0),
4✔
1095
                GridBoundingBox2D::new_min_max(-2, -1, 0, 3).unwrap(),
4✔
1096
            ),
4✔
1097
            bands: RasterBandDescriptors::new_single_band(),
4✔
1098
        };
4✔
1099

1100
        MockRasterSource {
4✔
1101
            params: MockRasterSourceParams {
4✔
1102
                data: raster_tiles,
4✔
1103
                result_descriptor,
4✔
1104
            },
4✔
1105
        }
4✔
1106
        .boxed()
4✔
1107
    }
4✔
1108

1109
    #[tokio::test]
1110
    async fn it_attaches_cache_hint() -> Result<()> {
1✔
1111
        let exe_ctx =
1✔
1112
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
1113

1114
        let cache_hint = CacheHint::seconds(1234);
1✔
1115
        let raster = make_raster(cache_hint);
1✔
1116

1117
        let operator = Interpolation {
1✔
1118
            params: InterpolationParams {
1✔
1119
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
1120
                output_resolution: InterpolationResolution::Resolution(
1✔
1121
                    SpatialResolution::zero_point_five(),
1✔
1122
                ),
1✔
1123
                output_origin_reference: None,
1✔
1124
            },
1✔
1125
            sources: SingleRasterSource { raster },
1✔
1126
        }
1✔
1127
        .boxed()
1✔
1128
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1129
        .await?;
1✔
1130

1131
        let processor = operator.query_processor()?.get_i8().unwrap();
1✔
1132

1133
        let query_rect = RasterQueryRectangle::new(
1✔
1134
            GridBoundingBox2D::new([-2, 0], [-1, 3]).unwrap(),
1✔
1135
            TimeInterval::new_unchecked(0, 20),
1✔
1136
            BandSelection::first(),
1✔
1137
        );
1138
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1139

1140
        let result_stream = processor.query(query_rect, &query_ctx).await?;
1✔
1141

1142
        let result: Vec<Result<RasterTile2D<i8>>> = result_stream.collect().await;
1✔
1143
        let result = result.into_iter().collect::<Result<Vec<_>>>()?;
1✔
1144

1145
        for tile in result {
4✔
1146
            // dbg!(tile.time, tile.grid_array);
1✔
1147
            assert_eq!(tile.cache_hint.expires(), cache_hint.expires());
4✔
1148
        }
1✔
1149

1✔
1150
        Ok(())
1✔
1151
    }
1✔
1152

1153
    #[tokio::test]
1154
    #[allow(clippy::too_many_lines)]
1155
    async fn it_interpolates_multiple_bands() -> Result<()> {
1✔
1156
        let exe_ctx =
1✔
1157
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
1158
        let operator = Interpolation {
1✔
1159
            params: InterpolationParams {
1✔
1160
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
1161
                output_resolution: InterpolationResolution::Resolution(
1✔
1162
                    SpatialResolution::zero_point_five(),
1✔
1163
                ),
1✔
1164
                output_origin_reference: None,
1✔
1165
            },
1✔
1166
            sources: SingleRasterSource {
1✔
1167
                raster: RasterStacker {
1✔
1168
                    params: RasterStackerParams {
1✔
1169
                        rename_bands: RenameBands::Default,
1✔
1170
                    },
1✔
1171
                    sources: MultipleRasterSources {
1✔
1172
                        rasters: vec![
1✔
1173
                            make_raster(CacheHint::max_duration()),
1✔
1174
                            make_raster(CacheHint::max_duration()),
1✔
1175
                        ],
1✔
1176
                    },
1✔
1177
                }
1✔
1178
                .boxed(),
1✔
1179
            },
1✔
1180
        }
1✔
1181
        .boxed()
1✔
1182
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1183
        .await?;
1✔
1184

1185
        let processor = operator.query_processor()?.get_i8().unwrap();
1✔
1186

1187
        let query_rect = RasterQueryRectangle::new(
1✔
1188
            GridBoundingBox2D::new([-4, 0], [-1, 7]).unwrap(),
1✔
1189
            TimeInterval::new_unchecked(0, 20),
1✔
1190
            [0, 1].try_into().unwrap(),
1✔
1191
        );
1192
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
1193

1194
        let result_stream = processor.query(query_rect, &query_ctx).await?;
1✔
1195

1196
        let result: Vec<Result<RasterTile2D<i8>>> = result_stream.collect().await;
1✔
1197
        let result = result.into_iter().collect::<Result<Vec<_>>>()?;
1✔
1198

1199
        let mut times: Vec<TimeInterval> = vec![TimeInterval::new_unchecked(0, 10); 8];
1✔
1200
        times.append(&mut vec![TimeInterval::new_unchecked(10, 20); 8]);
1✔
1201

1202
        let times = times
1✔
1203
            .clone()
1✔
1204
            .into_iter()
1✔
1205
            .zip(times)
1✔
1206
            .flat_map(|(a, b)| vec![a, b])
16✔
1207
            .collect::<Vec<_>>();
1✔
1208

1209
        let data = vec![
1✔
1210
            vec![1; 4],
1✔
1211
            vec![2; 4],
1✔
1212
            vec![3; 4],
1✔
1213
            vec![4; 4],
1✔
1214
            vec![5; 4],
1✔
1215
            vec![6; 4],
1✔
1216
            vec![7; 4],
1✔
1217
            vec![8; 4],
1✔
1218
            vec![8; 4],
1✔
1219
            vec![7; 4],
1✔
1220
            vec![6; 4],
1✔
1221
            vec![5; 4],
1✔
1222
            vec![4; 4],
1✔
1223
            vec![3; 4],
1✔
1224
            vec![2; 4],
1✔
1225
            vec![1; 4],
1✔
1226
        ];
1227

1228
        let valid = vec![
1✔
1229
            vec![true; 4],
1✔
1230
            vec![true; 4],
1✔
1231
            vec![true; 4],
1✔
1232
            vec![true; 4],
1✔
1233
            vec![true; 4],
1✔
1234
            vec![true; 4],
1✔
1235
            vec![true; 4],
1✔
1236
            vec![true; 4],
1✔
1237
            vec![true; 4],
1✔
1238
            vec![true; 4],
1✔
1239
            vec![true; 4],
1✔
1240
            vec![true; 4],
1✔
1241
            vec![true; 4],
1✔
1242
            vec![true; 4],
1✔
1243
            vec![true; 4],
1✔
1244
            vec![true; 4],
1✔
1245
        ];
1246

1247
        let data = data
1✔
1248
            .clone()
1✔
1249
            .into_iter()
1✔
1250
            .zip(data)
1✔
1251
            .flat_map(|(a, b)| vec![a, b])
16✔
1252
            .collect::<Vec<_>>();
1✔
1253

1254
        let valid = valid
1✔
1255
            .clone()
1✔
1256
            .into_iter()
1✔
1257
            .zip(valid)
1✔
1258
            .flat_map(|(a, b)| vec![a, b])
16✔
1259
            .collect::<Vec<_>>();
1✔
1260

1261
        for (i, tile) in result.into_iter().enumerate() {
32✔
1262
            let tile = tile.into_materialized_tile();
32✔
1263
            assert_eq!(tile.time, times[i]);
32✔
1264
            assert_eq!(tile.grid_array.inner_grid.data, data[i]);
32✔
1265
            assert_eq!(tile.grid_array.validity_mask.data, valid[i]);
32✔
1266
        }
1✔
1267

1✔
1268
        Ok(())
1✔
1269
    }
1✔
1270
}
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