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

geo-engine / geoengine / 19438548963

17 Nov 2025 05:27PM UTC coverage: 88.512%. First build
19438548963

Pull #1083

github

web-flow
Merge c3ef76798 into f0e6b6470
Pull Request #1083: feat: new gdal source workflow optimization

6329 of 7650 new or added lines in 78 files covered. (82.73%)

116305 of 131400 relevant lines covered (88.51%)

494085.33 hits per line

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

91.86
/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,
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 { x: f64, y: f64 },
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>> {
12✔
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
    }
12✔
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(
12✔
111
        name: CanonicOperatorName,
12✔
112
        path: WorkflowOperatorPath,
12✔
113
        raster_source: O,
12✔
114
        params: &InterpolationParams,
12✔
115
        tiling_specification: TilingSpecification,
12✔
116
    ) -> Result<Self> {
12✔
117
        let in_descriptor = raster_source.result_descriptor();
12✔
118
        let in_spatial_grid = in_descriptor.spatial_grid_descriptor();
12✔
119

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

139
            InterpolationResolution::Fraction { x, y } => {
×
140
                ensure!(x >= 1.0, error::FractionMustBeOneOrLarger { f: x });
×
141
                ensure!(y >= 1.0, error::FractionMustBeOneOrLarger { f: y });
×
142

143
                SpatialResolution::new_unchecked(
×
144
                    in_spatial_grid.spatial_resolution().x / x,
×
145
                    in_spatial_grid.spatial_resolution().y.abs() / y,
×
146
                )
147
            }
148
        };
149

150
        let out_spatial_grid = if let Some(oc) = params.output_origin_reference {
12✔
151
            in_spatial_grid
1✔
152
                .with_changed_resolution(output_resolution)
1✔
153
                .with_moved_origin_to_nearest_grid_edge(oc)
1✔
154
                .replace_origin(oc)
1✔
155
        } else {
156
            in_spatial_grid.with_changed_resolution(output_resolution)
11✔
157
        };
158

159
        let out_descriptor = RasterResultDescriptor {
12✔
160
            spatial_reference: in_descriptor.spatial_reference,
12✔
161
            data_type: in_descriptor.data_type,
12✔
162
            time: in_descriptor.time,
12✔
163
            spatial_grid: out_spatial_grid,
12✔
164
            bands: in_descriptor.bands.clone(),
12✔
165
        };
12✔
166

167
        let initialized_operator = InitializedInterpolation {
12✔
168
            name,
12✔
169
            path,
12✔
170
            output_result_descriptor: out_descriptor,
12✔
171
            raster_source,
12✔
172
            interpolation_method: params.interpolation,
12✔
173
            tiling_specification,
12✔
174
        };
12✔
175

176
        Ok(initialized_operator)
12✔
177
    }
12✔
178
}
179

180
impl<O: InitializedRasterOperator> InitializedRasterOperator for InitializedInterpolation<O> {
181
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
7✔
182
        let source_processor = self.raster_source.query_processor()?;
7✔
183

184
        let res = call_on_generic_raster_processor!(
7✔
185
            source_processor, p => match self.interpolation_method  {
7✔
186
                InterpolationMethod::NearestNeighbor => InterploationProcessor::<_,_, NearestNeighbor>::new(
3✔
187
                        p,
3✔
188
                        self.output_result_descriptor.clone(),
3✔
189
                        self.tiling_specification,
3✔
190
                    ).boxed()
3✔
191
                    .into(),
3✔
192
                InterpolationMethod::BiLinear =>InterploationProcessor::<_,_, Bilinear>::new(
×
193
                        p,
×
194
                        self.output_result_descriptor.clone(),
×
195
                        self.tiling_specification,
×
196
                    ).boxed()
×
197
                    .into(),
×
198
            }
199
        );
200

201
        Ok(res)
7✔
202
    }
7✔
203

204
    fn result_descriptor(&self) -> &RasterResultDescriptor {
15✔
205
        &self.output_result_descriptor
15✔
206
    }
15✔
207

208
    fn canonic_name(&self) -> CanonicOperatorName {
×
209
        self.name.clone()
×
210
    }
×
211

212
    fn name(&self) -> &'static str {
4✔
213
        Interpolation::TYPE_NAME
4✔
214
    }
4✔
215

216
    fn path(&self) -> WorkflowOperatorPath {
4✔
217
        self.path.clone()
4✔
218
    }
4✔
219

220
    fn optimize(
3✔
221
        &self,
3✔
222
        target_resolution: SpatialResolution,
3✔
223
    ) -> Result<Box<dyn RasterOperator>, OptimizationError> {
3✔
224
        self.ensure_resolution_is_compatible_for_optimization(target_resolution)?;
3✔
225

226
        let out_descriptor = self.result_descriptor();
3✔
227
        let in_descriptor = self.raster_source.result_descriptor();
3✔
228

229
        let input_resolution = in_descriptor.spatial_grid.spatial_resolution();
3✔
230

231
        let new_origin = if in_descriptor.spatial_grid.geo_transform().origin_coordinate
3✔
232
            == out_descriptor
3✔
233
                .spatial_grid
3✔
234
                .geo_transform()
3✔
235
                .origin_coordinate
3✔
236
        {
237
            None
3✔
238
        } else {
NEW
239
            Some(
×
NEW
240
                out_descriptor
×
NEW
241
                    .spatial_grid
×
NEW
242
                    .geo_transform()
×
NEW
243
                    .origin_coordinate,
×
NEW
244
            )
×
245
        };
246

247
        if input_resolution == target_resolution {
3✔
248
            // special case where interpolation becomes redundant, unless it also regrids
249

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

253
            if new_origin.is_some() {
1✔
NEW
254
                return Ok(Interpolation {
×
NEW
255
                    params: InterpolationParams {
×
NEW
256
                        interpolation: self.interpolation_method,
×
NEW
257
                        output_resolution: InterpolationResolution::Resolution(target_resolution),
×
NEW
258
                        output_origin_reference: new_origin,
×
NEW
259
                    },
×
NEW
260
                    sources: SingleRasterSource {
×
NEW
261
                        raster: optimzed_source,
×
NEW
262
                    },
×
NEW
263
                }
×
NEW
264
                .boxed());
×
265
            }
1✔
266
            return Ok(optimzed_source);
1✔
267
        }
2✔
268

269
        // snap the input resolution to an overview level
270
        let mut snapped_input_resolution = input_resolution;
2✔
271

272
        while snapped_input_resolution * 2.0 < target_resolution {
3✔
273
            snapped_input_resolution = snapped_input_resolution * 2.0;
1✔
274
        }
1✔
275

276
        let optimzed_source = self.raster_source.optimize(snapped_input_resolution)?;
2✔
277

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

293
        // target resolution is still finer than what the source produces
294
        debug_assert!(snapped_input_resolution > target_resolution);
1✔
295

296
        Ok(Interpolation {
1✔
297
            params: InterpolationParams {
1✔
298
                interpolation: self.interpolation_method,
1✔
299
                output_resolution: InterpolationResolution::Resolution(target_resolution),
1✔
300
                output_origin_reference: new_origin,
1✔
301
            },
1✔
302
            sources: SingleRasterSource {
1✔
303
                raster: optimzed_source,
1✔
304
            },
1✔
305
        }
1✔
306
        .boxed())
1✔
307
    }
3✔
308
}
309

310
pub struct InterploationProcessor<Q, P, I>
311
where
312
    Q: RasterQueryProcessor<RasterType = P>,
313
    P: Pixel,
314
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
315
{
316
    source: Q,
317
    out_result_descriptor: RasterResultDescriptor,
318
    tiling_specification: TilingSpecification,
319
    interpolation: PhantomData<I>,
320
}
321

322
impl<Q, P, I> InterploationProcessor<Q, P, I>
323
where
324
    Q: RasterQueryProcessor<RasterType = P>,
325
    P: Pixel,
326
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
327
{
328
    pub fn new(
7✔
329
        source: Q,
7✔
330
        out_result_descriptor: RasterResultDescriptor,
7✔
331
        tiling_specification: TilingSpecification,
7✔
332
    ) -> Self {
7✔
333
        Self {
7✔
334
            source,
7✔
335
            out_result_descriptor,
7✔
336
            tiling_specification,
7✔
337
            interpolation: PhantomData,
7✔
338
        }
7✔
339
    }
7✔
340
}
341

342
#[async_trait]
343
impl<Q, P, I> QueryProcessor for InterploationProcessor<Q, P, I>
344
where
345
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
346
    P: Pixel,
347
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
348
{
349
    type Output = RasterTile2D<P>;
350
    type SpatialBounds = GridBoundingBox2D;
351
    type Selection = BandSelection;
352
    type ResultDescription = RasterResultDescriptor;
353

354
    async fn _query<'a>(
355
        &'a self,
356
        query: RasterQueryRectangle,
357
        ctx: &'a dyn QueryContext,
358
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
10✔
359
        // do not interpolate if the source resolution is already fine enough
360

361
        let in_spatial_grid = self.source.result_descriptor().spatial_grid_descriptor();
362
        let out_spatial_grid = self.result_descriptor().spatial_grid_descriptor();
363

364
        // if the output resolution is the same as the input resolution, we can just forward the query // TODO: except the origin changes?
365
        if in_spatial_grid == out_spatial_grid {
366
            return self.source.query(query, ctx).await;
367
        }
368

369
        let tiling_grid_definition =
370
            out_spatial_grid.tiling_grid_definition(ctx.tiling_specification());
371

372
        // This is the tiling strategy we want to fill
373
        let tiling_strategy: geoengine_datatypes::raster::TilingStrategy =
374
            tiling_grid_definition.generate_data_tiling_strategy();
375

376
        let input_geo_transform = in_spatial_grid
377
            .tiling_grid_definition(ctx.tiling_specification())
378
            .tiling_geo_transform();
379

380
        let output_geo_transform = tiling_grid_definition.tiling_geo_transform();
381

382
        let sub_query = InterpolationSubQuery::<_, P, I> {
383
            input_geo_transform,
384
            output_geo_transform,
385
            fold_fn: fold_future,
386
            tiling_specification: self.tiling_specification,
387
            phantom: PhantomData,
388
            _phantom_pixel_type: PhantomData,
389
        };
390

391
        let time_stream = self.time_query(query.time_interval(), ctx).await?;
392

393
        Ok(Box::pin(RasterSubQueryAdapter::<'a, P, _, _, _>::new(
394
            &self.source,
395
            query,
396
            tiling_strategy,
397
            ctx,
398
            sub_query,
399
            time_stream,
400
        )))
401
    }
10✔
402

403
    fn result_descriptor(&self) -> &RasterResultDescriptor {
56✔
404
        &self.out_result_descriptor
56✔
405
    }
56✔
406
}
407

408
#[async_trait]
409
impl<Q, P, I> RasterQueryProcessor for InterploationProcessor<Q, P, I>
410
where
411
    P: Pixel,
412
    Q: RasterQueryProcessor<RasterType = P> + Send + Sync,
413
    I: InterpolationAlgorithm<GridBoundingBox2D, P>,
414
{
415
    type RasterType = P;
416

417
    async fn _time_query<'a>(
418
        &'a self,
419
        query: geoengine_datatypes::primitives::TimeInterval,
420
        ctx: &'a dyn crate::engine::QueryContext,
421
    ) -> Result<futures::stream::BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>>
422
    {
5✔
423
        self.source.time_query(query, ctx).await
424
    }
5✔
425
}
426

427
#[derive(Debug, Clone)]
428
pub struct InterpolationSubQuery<F, T, I> {
429
    input_geo_transform: GeoTransform,
430
    output_geo_transform: GeoTransform, // TODO remove because in adapter?
431
    fold_fn: F,
432
    tiling_specification: TilingSpecification,
433
    phantom: PhantomData<I>,
434
    _phantom_pixel_type: PhantomData<T>,
435
}
436

437
impl<'a, T, FoldM, FoldF, I> SubQueryTileAggregator<'a, T> for InterpolationSubQuery<FoldM, T, I>
438
where
439
    T: Pixel,
440
    FoldM: Send + Sync + 'a + Clone + Fn(InterpolationAccu<T, I>, RasterTile2D<T>) -> FoldF,
441
    FoldF: Send + TryFuture<Ok = InterpolationAccu<T, I>, Error = crate::error::Error>,
442
    I: InterpolationAlgorithm<GridBoundingBox2D, T>,
443
{
444
    type FoldFuture = FoldF;
445

446
    type FoldMethod = FoldM;
447

448
    type TileAccu = InterpolationAccu<T, I>;
449
    type TileAccuFuture = BoxFuture<'a, Result<Self::TileAccu>>;
450

451
    fn new_fold_accu(
74✔
452
        &self,
74✔
453
        tile_info: TileInformation,
74✔
454
        query_rect: RasterQueryRectangle,
74✔
455
        pool: &Arc<ThreadPool>,
74✔
456
    ) -> Self::TileAccuFuture {
74✔
457
        create_accu(
74✔
458
            self.input_geo_transform,
74✔
459
            self.output_geo_transform,
74✔
460
            tile_info,
74✔
461
            &query_rect,
74✔
462
            pool.clone(),
74✔
463
            self.tiling_specification,
74✔
464
        )
74✔
465
        .boxed()
74✔
466
    }
74✔
467

468
    fn tile_query_rectangle(
74✔
469
        &self,
74✔
470
        tile_info: TileInformation,
74✔
471
        _query_rect: RasterQueryRectangle,
74✔
472
        time: TimeInterval,
74✔
473
        band_idx: u32,
74✔
474
    ) -> Result<Option<RasterQueryRectangle>> {
74✔
475
        // enlarge the spatial bounds in order to have the neighbor pixels for the interpolation
476

477
        let tile_pixel_bounds = tile_info.global_pixel_bounds();
74✔
478
        let tile_spatial_bounds = self
74✔
479
            .output_geo_transform
74✔
480
            .grid_to_spatial_bounds(&tile_pixel_bounds);
74✔
481
        let input_pixel_bounds = self
74✔
482
            .input_geo_transform
74✔
483
            .spatial_to_grid_bounds(&tile_spatial_bounds);
74✔
484
        let enlarged_input_pixel_bounds = GridBoundingBox2D::new(
74✔
485
            [
74✔
486
                input_pixel_bounds.y_min() - 1,
74✔
487
                input_pixel_bounds.x_min() - 1,
74✔
488
            ],
74✔
489
            [
74✔
490
                input_pixel_bounds.y_max() + 1,
74✔
491
                input_pixel_bounds.x_max() + 1,
74✔
492
            ],
74✔
493
        )
494
        .expect("max bounds must be larger then min bounds already");
74✔
495

496
        Ok(Some(RasterQueryRectangle::new(
74✔
497
            enlarged_input_pixel_bounds,
74✔
498
            time,
74✔
499
            BandSelection::new_single(band_idx),
74✔
500
        )))
74✔
501
    }
74✔
502

503
    fn fold_method(&self) -> Self::FoldMethod {
74✔
504
        self.fold_fn.clone()
74✔
505
    }
74✔
506
}
507

508
#[derive(Clone, Debug)]
509
pub struct InterpolationAccu<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> {
510
    pub output_info: TileInformation,
511
    pub input_tile: GridOrEmpty<GridBoundingBox2D, T>,
512
    pub input_geo_transform: GeoTransform,
513
    pub time: TimeInterval,
514
    pub cache_hint: CacheHint,
515
    pub pool: Arc<ThreadPool>,
516
    phantom: PhantomData<I>,
517
}
518

519
impl<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> InterpolationAccu<T, I> {
520
    pub fn new(
74✔
521
        input_tile: GridOrEmpty<GridBoundingBox2D, T>,
74✔
522
        input_geo_transform: GeoTransform,
74✔
523
        time: TimeInterval,
74✔
524
        cache_hint: CacheHint,
74✔
525
        output_info: TileInformation,
74✔
526
        pool: Arc<ThreadPool>,
74✔
527
    ) -> Self {
74✔
528
        InterpolationAccu {
74✔
529
            input_tile,
74✔
530
            input_geo_transform,
74✔
531
            time,
74✔
532
            cache_hint,
74✔
533
            output_info,
74✔
534
            pool,
74✔
535
            phantom: Default::default(),
74✔
536
        }
74✔
537
    }
74✔
538
}
539

540
#[async_trait]
541
impl<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> FoldTileAccu
542
    for InterpolationAccu<T, I>
543
{
544
    type RasterType = T;
545

546
    async fn into_tile(self) -> Result<RasterTile2D<Self::RasterType>> {
74✔
547
        // now that we collected all the input tile pixels we perform the actual interpolation
548

549
        let output_tile = crate::util::spawn_blocking_with_thread_pool(self.pool, move || {
74✔
550
            I::interpolate(
74✔
551
                self.input_geo_transform,
74✔
552
                &self.input_tile,
74✔
553
                self.output_info.global_geo_transform,
74✔
554
                self.output_info.global_pixel_bounds(),
74✔
555
            )
556
        })
74✔
557
        .await??;
558

559
        let output_tile = RasterTile2D::new_with_tile_info(
560
            self.time,
561
            self.output_info,
562
            0,
563
            output_tile.unbounded(),
564
            self.cache_hint,
565
        );
566

567
        Ok(output_tile)
568
    }
74✔
569

570
    fn thread_pool(&self) -> &Arc<ThreadPool> {
×
571
        &self.pool
×
572
    }
×
573
}
574

575
impl<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>> FoldTileAccuMut
576
    for InterpolationAccu<T, I>
577
{
578
    fn set_time(&mut self, time: TimeInterval) {
255✔
579
        self.time = time;
255✔
580
    }
255✔
581

582
    fn set_cache_hint(&mut self, cache_hint: CacheHint) {
×
583
        self.cache_hint = cache_hint;
×
584
    }
×
585
}
586

587
pub fn create_accu<T: Pixel, I: InterpolationAlgorithm<GridBoundingBox2D, T>>(
74✔
588
    input_geo_transform: GeoTransform,
74✔
589
    output_geo_transform: GeoTransform,
74✔
590
    tile_info: TileInformation,
74✔
591
    query_rect: &RasterQueryRectangle,
74✔
592
    pool: Arc<ThreadPool>,
74✔
593
    _tiling_specification: TilingSpecification,
74✔
594
) -> impl Future<Output = Result<InterpolationAccu<T, I>>> + use<T, I> {
74✔
595
    let query_rect = query_rect.clone();
74✔
596

597
    // create an accumulator as a single tile that fits all the input tiles
598
    let time_interval = query_rect.time_interval();
74✔
599

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

616
        // 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]
617
        let grid = GridOrEmpty::new_empty_shape(enlarged_input_pixel_bounds);
74✔
618

619
        InterpolationAccu::new(
74✔
620
            grid,
74✔
621
            input_geo_transform,
74✔
622
            time_interval,
74✔
623
            CacheHint::max_duration(),
74✔
624
            tile_info,
74✔
625
            pool,
74✔
626
        )
627
    })
74✔
628
    .map_err(From::from)
74✔
629
}
74✔
630

631
pub fn fold_future<T, I>(
255✔
632
    accu: InterpolationAccu<T, I>,
255✔
633
    tile: RasterTile2D<T>,
255✔
634
) -> impl Future<Output = Result<InterpolationAccu<T, I>>>
255✔
635
where
255✔
636
    T: Pixel,
255✔
637
    I: InterpolationAlgorithm<GridBoundingBox2D, T>,
255✔
638
{
639
    crate::util::spawn_blocking(|| fold_impl(accu, tile)).then(|x| async move {
255✔
640
        match x {
255✔
641
            Ok(r) => Ok(r),
255✔
642
            Err(e) => Err(e.into()),
×
643
        }
644
    })
510✔
645
}
255✔
646

647
pub fn fold_impl<T, I>(
255✔
648
    mut accu: InterpolationAccu<T, I>,
255✔
649
    tile: RasterTile2D<T>,
255✔
650
) -> InterpolationAccu<T, I>
255✔
651
where
255✔
652
    T: Pixel,
255✔
653
    I: InterpolationAlgorithm<GridBoundingBox2D, T>,
255✔
654
{
655
    // get the time now because it is not known when the accu was created
656
    accu.set_time(tile.time);
255✔
657
    accu.cache_hint.merge_with(&tile.cache_hint);
255✔
658

659
    // TODO: add a skip if both tiles are empty?
660

661
    // copy all input tiles into the accu to have all data for interpolation
662
    let in_tile = &tile.into_inner_positioned_grid();
255✔
663

664
    accu.input_tile.grid_blit_from(in_tile);
255✔
665

666
    accu
255✔
667
}
255✔
668

669
#[cfg(test)]
670
mod tests {
671
    use super::*;
672
    use futures::StreamExt;
673
    use geoengine_datatypes::{
674
        primitives::{
675
            Coordinate2D, RasterQueryRectangle, SpatialResolution, TimeInterval, TimeStep,
676
        },
677
        raster::{
678
            Grid2D, GridOrEmpty, RasterDataType, RasterTile2D, RenameBands, TileInformation,
679
            TilingSpecification,
680
        },
681
        spatial_reference::SpatialReference,
682
        util::test::TestDefault,
683
    };
684

685
    use crate::{
686
        engine::{
687
            MockExecutionContext, MultipleRasterSources, RasterBandDescriptors, RasterOperator,
688
            RasterResultDescriptor, SpatialGridDescriptor, TimeDescriptor,
689
        },
690
        mock::{MockRasterSource, MockRasterSourceParams},
691
        processing::{RasterStacker, RasterStackerParams},
692
    };
693

694
    #[tokio::test]
695
    async fn nearest_neighbor_operator() -> Result<()> {
1✔
696
        let exe_ctx =
1✔
697
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
698

699
        // test raster:
700
        // [0, 10)
701
        // || 1 | 2 || 3 | 4 ||
702
        // || 5 | 6 || 7 | 8 ||
703
        //
704
        // [10, 20)
705
        // || 8 | 7 || 6 | 5 ||
706
        // || 4 | 3 || 2 | 1 ||
707

708
        // exptected raster:
709
        // [0, 10)
710
        // ||1 | 1 || 2 | 2 ||
711
        // ||1 | 1 || 2 | 2 ||
712
        // ||5 | 5 || 6 | 6 ||
713
        // ||5 | 5 || 6 | 6 ||
714

715
        let raster = make_raster(CacheHint::max_duration());
1✔
716

717
        let operator = Interpolation {
1✔
718
            params: InterpolationParams {
1✔
719
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
720
                output_resolution: InterpolationResolution::Resolution(
1✔
721
                    SpatialResolution::zero_point_five(),
1✔
722
                ),
1✔
723
                output_origin_reference: None,
1✔
724
            },
1✔
725
            sources: SingleRasterSource { raster },
1✔
726
        }
1✔
727
        .boxed()
1✔
728
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
729
        .await?;
1✔
730

731
        let processor = operator.query_processor()?.get_i8().unwrap();
1✔
732

733
        let query_rect = RasterQueryRectangle::new(
1✔
734
            GridBoundingBox2D::new([-4, 0], [-1, 7]).unwrap(),
1✔
735
            TimeInterval::new_unchecked(0, 20),
1✔
736
            BandSelection::first(),
1✔
737
        );
738
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
739

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

742
        let result: Vec<Result<RasterTile2D<i8>>> = result_stream.collect().await;
1✔
743
        let result = result.into_iter().collect::<Result<Vec<_>>>()?;
1✔
744

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

748
        let data = vec![
1✔
749
            vec![1; 4],
1✔
750
            vec![2; 4],
1✔
751
            vec![3; 4],
1✔
752
            vec![4; 4],
1✔
753
            vec![5; 4],
1✔
754
            vec![6; 4],
1✔
755
            vec![7; 4],
1✔
756
            vec![8; 4],
1✔
757
            vec![8; 4],
1✔
758
            vec![7; 4],
1✔
759
            vec![6; 4],
1✔
760
            vec![5; 4],
1✔
761
            vec![4; 4],
1✔
762
            vec![3; 4],
1✔
763
            vec![2; 4],
1✔
764
            vec![1; 4],
1✔
765
        ];
766

767
        let valid = vec![
1✔
768
            vec![true; 4],
1✔
769
            vec![true; 4],
1✔
770
            vec![true; 4],
1✔
771
            vec![true; 4],
1✔
772
            vec![true; 4],
1✔
773
            vec![true; 4],
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
        ];
785

786
        for (i, tile) in result.into_iter().enumerate() {
16✔
787
            let tile = tile.into_materialized_tile();
16✔
788
            assert_eq!(tile.time, times[i]);
16✔
789
            assert_eq!(tile.grid_array.validity_mask.data, valid[i]);
16✔
790
            assert_eq!(tile.grid_array.inner_grid.data, data[i]);
16✔
791
        }
1✔
792

1✔
793
        Ok(())
1✔
794
    }
1✔
795

796
    fn make_raster(cache_hint: CacheHint) -> Box<dyn RasterOperator> {
4✔
797
        // test raster:
798
        // [0, 10)
799
        // || 1 | 2 || 3 | 4 ||
800
        // || 5 | 6 || 7 | 8 ||
801
        //
802
        // [10, 20)
803
        // || 8 | 7 || 6 | 5 ||
804
        // || 4 | 3 || 2 | 1 ||
805
        let raster_tiles = vec![
4✔
806
            RasterTile2D::<i8>::new_with_tile_info(
4✔
807
                TimeInterval::new_unchecked(0, 10),
4✔
808
                TileInformation {
4✔
809
                    global_tile_position: [-1, 0].into(),
4✔
810
                    tile_size_in_pixels: [2, 2].into(),
4✔
811
                    global_geo_transform: TestDefault::test_default(),
4✔
812
                },
4✔
813
                0,
814
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![1, 2, 5, 6]).unwrap()),
4✔
815
                cache_hint,
4✔
816
            ),
817
            RasterTile2D::new_with_tile_info(
4✔
818
                TimeInterval::new_unchecked(0, 10),
4✔
819
                TileInformation {
4✔
820
                    global_tile_position: [-1, 1].into(),
4✔
821
                    tile_size_in_pixels: [2, 2].into(),
4✔
822
                    global_geo_transform: TestDefault::test_default(),
4✔
823
                },
4✔
824
                0,
825
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![3, 4, 7, 8]).unwrap()),
4✔
826
                cache_hint,
4✔
827
            ),
828
            RasterTile2D::new_with_tile_info(
4✔
829
                TimeInterval::new_unchecked(10, 20),
4✔
830
                TileInformation {
4✔
831
                    global_tile_position: [-1, 0].into(),
4✔
832
                    tile_size_in_pixels: [2, 2].into(),
4✔
833
                    global_geo_transform: TestDefault::test_default(),
4✔
834
                },
4✔
835
                0,
836
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![8, 7, 4, 3]).unwrap()),
4✔
837
                cache_hint,
4✔
838
            ),
839
            RasterTile2D::new_with_tile_info(
4✔
840
                TimeInterval::new_unchecked(10, 20),
4✔
841
                TileInformation {
4✔
842
                    global_tile_position: [-1, 1].into(),
4✔
843
                    tile_size_in_pixels: [2, 2].into(),
4✔
844
                    global_geo_transform: TestDefault::test_default(),
4✔
845
                },
4✔
846
                0,
847
                GridOrEmpty::from(Grid2D::new([2, 2].into(), vec![6, 5, 2, 1]).unwrap()),
4✔
848
                cache_hint,
4✔
849
            ),
850
        ];
851

852
        let result_descriptor = RasterResultDescriptor {
4✔
853
            data_type: RasterDataType::I8,
4✔
854
            spatial_reference: SpatialReference::epsg_4326().into(),
4✔
855
            time: TimeDescriptor::new_regular_with_epoch(
4✔
856
                Some(
4✔
857
                    TimeInterval::new(
4✔
858
                        raster_tiles.first().unwrap().time.start(),
4✔
859
                        raster_tiles.last().unwrap().time.end(),
4✔
860
                    )
4✔
861
                    .unwrap(),
4✔
862
                ),
4✔
863
                TimeStep::millis(10).unwrap(),
4✔
864
            ),
4✔
865
            spatial_grid: SpatialGridDescriptor::source_from_parts(
4✔
866
                GeoTransform::new(Coordinate2D::new(0., 0.), 1.0, -1.0),
4✔
867
                GridBoundingBox2D::new_min_max(-2, -1, 0, 3).unwrap(),
4✔
868
            ),
4✔
869
            bands: RasterBandDescriptors::new_single_band(),
4✔
870
        };
4✔
871

872
        MockRasterSource {
4✔
873
            params: MockRasterSourceParams {
4✔
874
                data: raster_tiles,
4✔
875
                result_descriptor,
4✔
876
            },
4✔
877
        }
4✔
878
        .boxed()
4✔
879
    }
4✔
880

881
    #[tokio::test]
882
    async fn it_attaches_cache_hint() -> Result<()> {
1✔
883
        let exe_ctx =
1✔
884
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
885

886
        let cache_hint = CacheHint::seconds(1234);
1✔
887
        let raster = make_raster(cache_hint);
1✔
888

889
        let operator = Interpolation {
1✔
890
            params: InterpolationParams {
1✔
891
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
892
                output_resolution: InterpolationResolution::Resolution(
1✔
893
                    SpatialResolution::zero_point_five(),
1✔
894
                ),
1✔
895
                output_origin_reference: None,
1✔
896
            },
1✔
897
            sources: SingleRasterSource { raster },
1✔
898
        }
1✔
899
        .boxed()
1✔
900
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
901
        .await?;
1✔
902

903
        let processor = operator.query_processor()?.get_i8().unwrap();
1✔
904

905
        let query_rect = RasterQueryRectangle::new(
1✔
906
            GridBoundingBox2D::new([-2, 0], [-1, 3]).unwrap(),
1✔
907
            TimeInterval::new_unchecked(0, 20),
1✔
908
            BandSelection::first(),
1✔
909
        );
910
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
911

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

914
        let result: Vec<Result<RasterTile2D<i8>>> = result_stream.collect().await;
1✔
915
        let result = result.into_iter().collect::<Result<Vec<_>>>()?;
1✔
916

917
        for tile in result {
5✔
918
            // dbg!(tile.time, tile.grid_array);
1✔
919
            assert_eq!(tile.cache_hint.expires(), cache_hint.expires());
4✔
920
        }
1✔
921

1✔
922
        Ok(())
1✔
923
    }
1✔
924

925
    #[tokio::test]
926
    #[allow(clippy::too_many_lines)]
927
    async fn it_interpolates_multiple_bands() -> Result<()> {
1✔
928
        let exe_ctx =
1✔
929
            MockExecutionContext::new_with_tiling_spec(TilingSpecification::new([2, 2].into()));
1✔
930
        let operator = Interpolation {
1✔
931
            params: InterpolationParams {
1✔
932
                interpolation: InterpolationMethod::NearestNeighbor,
1✔
933
                output_resolution: InterpolationResolution::Resolution(
1✔
934
                    SpatialResolution::zero_point_five(),
1✔
935
                ),
1✔
936
                output_origin_reference: None,
1✔
937
            },
1✔
938
            sources: SingleRasterSource {
1✔
939
                raster: RasterStacker {
1✔
940
                    params: RasterStackerParams {
1✔
941
                        rename_bands: RenameBands::Default,
1✔
942
                    },
1✔
943
                    sources: MultipleRasterSources {
1✔
944
                        rasters: vec![
1✔
945
                            make_raster(CacheHint::max_duration()),
1✔
946
                            make_raster(CacheHint::max_duration()),
1✔
947
                        ],
1✔
948
                    },
1✔
949
                }
1✔
950
                .boxed(),
1✔
951
            },
1✔
952
        }
1✔
953
        .boxed()
1✔
954
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
955
        .await?;
1✔
956

957
        let processor = operator.query_processor()?.get_i8().unwrap();
1✔
958

959
        let query_rect = RasterQueryRectangle::new(
1✔
960
            GridBoundingBox2D::new([-4, 0], [-1, 7]).unwrap(),
1✔
961
            TimeInterval::new_unchecked(0, 20),
1✔
962
            [0, 1].try_into().unwrap(),
1✔
963
        );
964
        let query_ctx = exe_ctx.mock_query_context(TestDefault::test_default());
1✔
965

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

968
        let result: Vec<Result<RasterTile2D<i8>>> = result_stream.collect().await;
1✔
969
        let result = result.into_iter().collect::<Result<Vec<_>>>()?;
1✔
970

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

974
        let times = times
1✔
975
            .clone()
1✔
976
            .into_iter()
1✔
977
            .zip(times)
1✔
978
            .flat_map(|(a, b)| vec![a, b])
16✔
979
            .collect::<Vec<_>>();
1✔
980

981
        let data = vec![
1✔
982
            vec![1; 4],
1✔
983
            vec![2; 4],
1✔
984
            vec![3; 4],
1✔
985
            vec![4; 4],
1✔
986
            vec![5; 4],
1✔
987
            vec![6; 4],
1✔
988
            vec![7; 4],
1✔
989
            vec![8; 4],
1✔
990
            vec![8; 4],
1✔
991
            vec![7; 4],
1✔
992
            vec![6; 4],
1✔
993
            vec![5; 4],
1✔
994
            vec![4; 4],
1✔
995
            vec![3; 4],
1✔
996
            vec![2; 4],
1✔
997
            vec![1; 4],
1✔
998
        ];
999

1000
        let valid = vec![
1✔
1001
            vec![true; 4],
1✔
1002
            vec![true; 4],
1✔
1003
            vec![true; 4],
1✔
1004
            vec![true; 4],
1✔
1005
            vec![true; 4],
1✔
1006
            vec![true; 4],
1✔
1007
            vec![true; 4],
1✔
1008
            vec![true; 4],
1✔
1009
            vec![true; 4],
1✔
1010
            vec![true; 4],
1✔
1011
            vec![true; 4],
1✔
1012
            vec![true; 4],
1✔
1013
            vec![true; 4],
1✔
1014
            vec![true; 4],
1✔
1015
            vec![true; 4],
1✔
1016
            vec![true; 4],
1✔
1017
        ];
1018

1019
        let data = data
1✔
1020
            .clone()
1✔
1021
            .into_iter()
1✔
1022
            .zip(data)
1✔
1023
            .flat_map(|(a, b)| vec![a, b])
16✔
1024
            .collect::<Vec<_>>();
1✔
1025

1026
        let valid = valid
1✔
1027
            .clone()
1✔
1028
            .into_iter()
1✔
1029
            .zip(valid)
1✔
1030
            .flat_map(|(a, b)| vec![a, b])
16✔
1031
            .collect::<Vec<_>>();
1✔
1032

1033
        for (i, tile) in result.into_iter().enumerate() {
32✔
1034
            let tile = tile.into_materialized_tile();
32✔
1035
            assert_eq!(tile.time, times[i]);
32✔
1036
            assert_eq!(tile.grid_array.inner_grid.data, data[i]);
32✔
1037
            assert_eq!(tile.grid_array.validity_mask.data, valid[i]);
32✔
1038
        }
1✔
1039

1✔
1040
        Ok(())
1✔
1041
    }
1✔
1042
}
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