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

geo-engine / geoengine / 22500486493

27 Feb 2026 07:22PM UTC coverage: 88.19%. First build
22500486493

push

github

web-flow
fix: Cache and Stacker should keep band order for subsets (#1120)

* fix(cache):filter returned band by query.band

* stacker: produce tiles with band selection stored

* update cache stored bands

* lint test

* add debug outputs on error tiles (only in debug mode)

* remove no needed method

* enhance expect method

* remove comment regarding TimeInstants

* remove band update in stored query since bands must not change

* remove redundant filterin in cache

* remove and cleanup code fix MockRasterSource

* back to old StackerAdapter stratey where bands are not selected

* baseline stacker approach. Fix tests

* improve comment

* lints

* add band selection logic to SimpleRasterStacker

* add regular time stackerwith band selection to RasterStacker Operator

* more tests and checks

* lints

* more lint

1002 of 1044 new or added lines in 20 files covered. (95.98%)

112379 of 127429 relevant lines covered (88.19%)

508212.08 hits per line

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

95.49
/operators/src/processing/raster_stacker.rs
1
use crate::adapters::{
2
    PartialQueryRect, QueryWrapper, RasterStackerAdapter, RasterStackerSource,
3
    SimpleRasterStackerAdapter, SimpleRasterStackerError,
4
};
5
use crate::engine::{
6
    BoxRasterQueryProcessor, CanonicOperatorName, ExecutionContext, InitializedRasterOperator,
7
    InitializedSources, MultipleRasterSources, Operator, OperatorName, QueryContext,
8
    QueryProcessor, RasterBandDescriptor, RasterOperator, RasterQueryProcessor,
9
    RasterResultDescriptor, TypedRasterQueryProcessor, WorkflowOperatorPath,
10
};
11
use crate::error::{
12
    InvalidNumberOfRasterStackerInputs, RasterInputsMustHaveSameSpatialReferenceAndDatatype,
13
};
14
use crate::optimization::OptimizationError;
15
use crate::util::Result;
16
use async_trait::async_trait;
17
use futures::StreamExt;
18
use futures::stream::BoxStream;
19
use geoengine_datatypes::primitives::{BandSelection, RasterQueryRectangle, SpatialResolution};
20
use geoengine_datatypes::raster::{
21
    DynamicRasterDataType, GridBoundingBox2D, Pixel, RasterTile2D, RenameBands,
22
};
23
use serde::{Deserialize, Serialize};
24
use snafu::ensure;
25

26
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27
#[serde(rename_all = "camelCase")]
28
pub struct RasterStackerParams {
29
    pub rename_bands: RenameBands,
30
}
31

32
/// This `QueryProcessor` stacks all of it's inputs into a single raster time-series.
33
/// It does so by querying all of it's inputs outputting them by band, space and then time.
34
/// The tiles are automatically temporally aligned.
35
///
36
/// All inputs must have the same data type and spatial reference.
37
pub type RasterStacker = Operator<RasterStackerParams, MultipleRasterSources>;
38

39
impl OperatorName for RasterStacker {
40
    const TYPE_NAME: &'static str = "RasterStacker";
41
}
42

43
#[typetag::serde]
×
44
#[async_trait]
45
impl RasterOperator for RasterStacker {
46
    async fn _initialize(
47
        self: Box<Self>,
48
        path: WorkflowOperatorPath,
49
        context: &dyn ExecutionContext,
50
    ) -> Result<Box<dyn InitializedRasterOperator>> {
39✔
51
        let name = CanonicOperatorName::from(&self);
52

53
        ensure!(
54
            !self.sources.rasters.is_empty() && self.sources.rasters.len() <= 8,
55
            InvalidNumberOfRasterStackerInputs
56
        );
57

58
        let raster_sources = self
59
            .sources
60
            .initialize_sources(path.clone(), context)
61
            .await?
62
            .rasters;
63

64
        let in_descriptors = raster_sources
65
            .iter()
66
            .map(InitializedRasterOperator::result_descriptor)
67
            .collect::<Vec<_>>();
68

69
        ensure!(
70
            in_descriptors.iter().all(|d| d.spatial_reference
89✔
71
                == in_descriptors[0].spatial_reference
89✔
72
                && d.data_type == in_descriptors[0].data_type),
89✔
73
            RasterInputsMustHaveSameSpatialReferenceAndDatatype {
74
                datatypes: in_descriptors
75
                    .iter()
76
                    .map(|d| d.data_type)
77
                    .collect::<Vec<_>>(),
78
                spatial_references: in_descriptors
79
                    .iter()
80
                    .map(|d| d.spatial_reference)
81
                    .collect::<Vec<_>>(),
82
            }
83
        );
84

85
        let first_spatial_grid = in_descriptors[0].spatial_grid;
86
        let result_spatial_grid = in_descriptors
87
            .iter()
88
            .skip(1)
89
            .map(|x| x.spatial_grid_descriptor())
50✔
90
            .try_fold(first_spatial_grid, |a, &b| {
50✔
91
                a.merge(&b)
50✔
92
                    .ok_or(crate::error::Error::CantMergeSpatialGridDescriptor { a, b })
50✔
93
            })?;
50✔
94

95
        let time = in_descriptors.iter().skip(1).map(|rd| rd.time).fold(
96
            in_descriptors
97
                .first()
98
                .expect("There must be at least one input")
99
                .time,
100
            |a, b| a.merge(b),
50✔
101
        );
102

103
        let data_type = in_descriptors[0].data_type;
104
        let spatial_reference = in_descriptors[0].spatial_reference;
105

106
        let bands_per_source = in_descriptors
107
            .iter()
108
            .map(|d| d.bands.count())
89✔
109
            .collect::<Vec<_>>();
110

111
        let band_names = self.params.rename_bands.apply(
112
            in_descriptors
113
                .iter()
114
                .map(|d| d.bands.iter().map(|b| b.name.clone()).collect())
93✔
115
                .collect(),
116
        )?;
117

118
        let output_band_descriptors = in_descriptors
119
            .into_iter()
120
            .flat_map(|d| d.bands.iter().cloned())
89✔
121
            .zip(band_names)
122
            .map(|(descriptor, name)| RasterBandDescriptor { name, ..descriptor })
93✔
123
            .collect::<Vec<_>>()
124
            .try_into()?;
125

126
        let result_descriptor = RasterResultDescriptor {
127
            data_type,
128
            spatial_reference,
129
            time,
130
            spatial_grid: result_spatial_grid,
131
            bands: output_band_descriptors,
132
        };
133

134
        Ok(Box::new(InitializedRasterStacker {
135
            name,
136
            path,
137
            result_descriptor,
138
            rename_bands: self.params.rename_bands.clone(),
139
            raster_sources,
140
            bands_per_source,
141
        }))
142
    }
39✔
143

144
    span_fn!(RasterStacker);
145
}
146

147
pub struct InitializedRasterStacker {
148
    name: CanonicOperatorName,
149
    path: WorkflowOperatorPath,
150
    result_descriptor: RasterResultDescriptor,
151
    rename_bands: RenameBands,
152
    raster_sources: Vec<Box<dyn InitializedRasterOperator>>,
153
    bands_per_source: Vec<u32>,
154
}
155

156
impl InitializedRasterOperator for InitializedRasterStacker {
157
    fn result_descriptor(&self) -> &RasterResultDescriptor {
44✔
158
        &self.result_descriptor
44✔
159
    }
44✔
160

161
    fn query_processor(&self) -> Result<TypedRasterQueryProcessor> {
29✔
162
        let typed_raster_processors = self
29✔
163
            .raster_sources
29✔
164
            .iter()
29✔
165
            .map(InitializedRasterOperator::query_processor)
29✔
166
            .collect::<Result<Vec<_>>>()?;
29✔
167

168
        // unpack all processors
169
        let datatype = typed_raster_processors[0].raster_data_type();
29✔
170

171
        let bands_per_source = self.bands_per_source.clone();
29✔
172

173
        // TODO: use a macro to unpack all the input processor to the same datatype?
174
        Ok(match datatype {
29✔
175
            geoengine_datatypes::raster::RasterDataType::U8 => {
176
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_u8().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
30✔
177
                let p = RasterStackerProcessor::new(
14✔
178
                    inputs,
14✔
179
                    self.result_descriptor.clone(),
14✔
180
                    bands_per_source,
14✔
181
                );
182
                TypedRasterQueryProcessor::U8(Box::new(p))
14✔
183
            }
184
            geoengine_datatypes::raster::RasterDataType::U16 => {
185
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_u16().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
×
186
                let p = RasterStackerProcessor::new(
×
187
                    inputs,
×
188
                    self.result_descriptor.clone(),
×
189
                    bands_per_source,
×
190
                );
191
                TypedRasterQueryProcessor::U16(Box::new(p))
×
192
            }
193
            geoengine_datatypes::raster::RasterDataType::U32 => {
194
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_u32().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
×
195
                let p = RasterStackerProcessor::new(
×
196
                    inputs,
×
197
                    self.result_descriptor.clone(),
×
198
                    bands_per_source,
×
199
                );
200
                TypedRasterQueryProcessor::U32(Box::new(p))
×
201
            }
202
            geoengine_datatypes::raster::RasterDataType::U64 => {
203
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_u64().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
×
204
                let p = RasterStackerProcessor::new(
×
205
                    inputs,
×
206
                    self.result_descriptor.clone(),
×
207
                    bands_per_source,
×
208
                );
209
                TypedRasterQueryProcessor::U64(Box::new(p))
×
210
            }
211
            geoengine_datatypes::raster::RasterDataType::I8 => {
212
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_i8().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
28✔
213
                let p = RasterStackerProcessor::new(
11✔
214
                    inputs,
11✔
215
                    self.result_descriptor.clone(),
11✔
216
                    bands_per_source,
11✔
217
                );
218
                TypedRasterQueryProcessor::I8(Box::new(p))
11✔
219
            }
220
            geoengine_datatypes::raster::RasterDataType::I16 => {
221
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_i16().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
2✔
222
                let p = RasterStackerProcessor::new(
1✔
223
                    inputs,
1✔
224
                    self.result_descriptor.clone(),
1✔
225
                    bands_per_source,
1✔
226
                );
227
                TypedRasterQueryProcessor::I16(Box::new(p))
1✔
228
            }
229
            geoengine_datatypes::raster::RasterDataType::I32 => {
230
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_i32().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
×
231
                let p = RasterStackerProcessor::new(
×
232
                    inputs,
×
233
                    self.result_descriptor.clone(),
×
234
                    bands_per_source,
×
235
                );
236
                TypedRasterQueryProcessor::I32(Box::new(p))
×
237
            }
238
            geoengine_datatypes::raster::RasterDataType::I64 => {
239
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_i64().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
×
240
                let p = RasterStackerProcessor::new(
×
241
                    inputs,
×
242
                    self.result_descriptor.clone(),
×
243
                    bands_per_source,
×
244
                );
245
                TypedRasterQueryProcessor::I64(Box::new(p))
×
246
            }
247
            geoengine_datatypes::raster::RasterDataType::F32 => {
248
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_f32().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
7✔
249
                let p = RasterStackerProcessor::new(
3✔
250
                    inputs,
3✔
251
                    self.result_descriptor.clone(),
3✔
252
                    bands_per_source,
3✔
253
                );
254
                TypedRasterQueryProcessor::F32(Box::new(p))
3✔
255
            }
256
            geoengine_datatypes::raster::RasterDataType::F64 => {
257
                let inputs = typed_raster_processors.into_iter().map(|p| p.get_f64().expect("all inputs should have the same datatype because it was checked in the initialization of the operator")).collect();
×
258
                let p = RasterStackerProcessor::new(
×
259
                    inputs,
×
260
                    self.result_descriptor.clone(),
×
261
                    bands_per_source,
×
262
                );
263
                TypedRasterQueryProcessor::F64(Box::new(p))
×
264
            }
265
        })
266
    }
29✔
267

268
    fn canonic_name(&self) -> CanonicOperatorName {
2✔
269
        self.name.clone()
2✔
270
    }
2✔
271

272
    fn name(&self) -> &'static str {
3✔
273
        RasterStacker::TYPE_NAME
3✔
274
    }
3✔
275

276
    fn path(&self) -> WorkflowOperatorPath {
3✔
277
        self.path.clone()
3✔
278
    }
3✔
279

280
    fn optimize(
6✔
281
        &self,
6✔
282
        resolution: SpatialResolution,
6✔
283
    ) -> Result<Box<dyn RasterOperator>, OptimizationError> {
6✔
284
        Ok(RasterStacker {
285
            params: RasterStackerParams {
6✔
286
                rename_bands: self.rename_bands.clone(),
6✔
287
            },
6✔
288
            sources: MultipleRasterSources {
289
                rasters: self
6✔
290
                    .raster_sources
6✔
291
                    .iter()
6✔
292
                    .map(|s| s.optimize(resolution))
14✔
293
                    .collect::<Result<Vec<_>, _>>()?,
6✔
294
            },
295
        }
296
        .boxed())
6✔
297
    }
6✔
298
}
299

300
pub(crate) struct RasterStackerProcessor<T> {
301
    sources: Vec<BoxRasterQueryProcessor<T>>,
302
    result_descriptor: RasterResultDescriptor,
303
    bands_per_source: Vec<u32>,
304
}
305

306
impl<T> RasterStackerProcessor<T> {
307
    pub fn new(
29✔
308
        sources: Vec<BoxRasterQueryProcessor<T>>,
29✔
309
        result_descriptor: RasterResultDescriptor,
29✔
310
        bands_per_source: Vec<u32>,
29✔
311
    ) -> Self {
29✔
312
        Self {
29✔
313
            sources,
29✔
314
            result_descriptor,
29✔
315
            bands_per_source,
29✔
316
        }
29✔
317
    }
29✔
318
}
319

320
#[async_trait]
321
impl<T> QueryProcessor for RasterStackerProcessor<T>
322
where
323
    T: Pixel,
324
{
325
    type Output = RasterTile2D<T>;
326
    type ResultDescription = RasterResultDescriptor;
327
    type Selection = BandSelection;
328
    type SpatialBounds = GridBoundingBox2D;
329

330
    async fn _query<'a>(
331
        &'a self,
332
        query: RasterQueryRectangle,
333
        ctx: &'a dyn QueryContext,
334
    ) -> Result<BoxStream<'a, Result<RasterTile2D<T>>>> {
93✔
335
        // First try to create simple raster stacker for temporal aligned data
336
        let sdp = SimpleRasterStackerAdapter::<
337
            SimpleRasterStackerAdapter<BoxRasterQueryProcessor<T>>,
338
        >::stack_selected_regular_aligned_raster_bands(&query, ctx, &self.sources)
339
        .await;
340

341
        let x = match sdp {
342
            Ok(p) => Ok(Some(p)),
343
            Err(SimpleRasterStackerError::InputsNotTemporalAligned) => Ok(None),
344
            Err(e) => Err(crate::error::Error::SimpleRasterStacker { source: e }),
345
        }?;
346

347
        if let Some(sdp) = x {
348
            tracing::trace!("Using regular time aligned stacker processor");
349
            return Ok(Box::pin(sdp));
350
        }
351

352
        // if the simple stacker can not be used, try to use the more complex stacker
353

354
        tracing::trace!("Using non-regular time aligned stacker processor");
355

356
        let mut sources = vec![];
357
        let tiling_strat = self
358
            .result_descriptor
359
            .tiling_grid_definition(ctx.tiling_specification())
360
            .generate_data_tiling_strategy();
361

362
        for (idx, source) in self.sources.iter().enumerate() {
363
            // FIXME: find a better way to do the selection and avoid work done without benefit.
364
            let bands = BandSelection::first_n(self.bands_per_source[idx]);
365

366
            sources.push(RasterStackerSource {
367
                queryable: QueryWrapper { p: source, ctx },
368
                band_idxs: bands.as_vec(),
369
            });
370
        }
371

372
        #[cfg(debug_assertions)]
373
        {
374
            let num_input_bands = self.bands_per_source.iter().sum::<u32>() as usize;
375
            let num_query_bands = query.attributes().as_vec().len();
376

377
            let fact = num_input_bands as f32 / num_query_bands as f32;
378

379
            tracing::debug!(
380
                "StackerAdapter queries {num_input_bands} to produce {num_query_bands}. This is {fact}x the work required."
381
            );
382
        }
383

384
        let query_band_selection = query.attributes().clone();
385
        let partial_query = PartialQueryRect::from(query);
386
        let output =
387
            RasterStackerAdapter::new(sources, partial_query, tiling_strat).filter_map(move |o| {
287✔
388
                let pred = match o {
287✔
389
                    Ok(tile) if query_band_selection.contains(tile.band) => Some(Ok(tile)),
287✔
390
                    Ok(_) => None,
132✔
NEW
391
                    Err(e) => Some(Err(e)),
×
392
                };
393
                std::future::ready(pred)
287✔
394
            });
287✔
395

396
        Ok(Box::pin(output))
397
    }
93✔
398

399
    fn result_descriptor(&self) -> &Self::ResultDescription {
274✔
400
        &self.result_descriptor
274✔
401
    }
274✔
402
}
403

404
#[async_trait]
405
impl<T> RasterQueryProcessor for RasterStackerProcessor<T>
406
where
407
    T: Pixel,
408
{
409
    type RasterType = T;
410

411
    async fn _time_query<'a>(
412
        &'a self,
413
        query: geoengine_datatypes::primitives::TimeInterval,
414
        ctx: &'a dyn crate::engine::QueryContext,
415
    ) -> Result<futures::stream::BoxStream<'a, Result<geoengine_datatypes::primitives::TimeInterval>>>
416
    {
2✔
417
        let mut time_sources = Vec::with_capacity(self.sources.len());
418
        for source in &self.sources {
419
            let s = source.time_query(query, ctx).await?;
420
            time_sources.push(s);
421
        }
422
        let output = crate::adapters::TimeIntervalStreamMerge::new(time_sources);
423
        Ok(Box::pin(output))
424
    }
2✔
425
}
426

427
#[cfg(test)]
428
mod tests {
429
    use std::str::FromStr;
430

431
    use futures::StreamExt;
432
    use geoengine_datatypes::{
433
        primitives::{CacheHint, TimeInstance, TimeInterval, TimeStep},
434
        raster::{
435
            GeoTransform, Grid, GridBoundingBox2D, GridShape, RasterDataType,
436
            TilesEqualIgnoringCacheHint,
437
        },
438
        spatial_reference::SpatialReference,
439
        util::test::{TestDefault, assert_eq_two_list_of_tiles},
440
    };
441

442
    use crate::{
443
        engine::{
444
            MockExecutionContext, RasterBandDescriptor, RasterBandDescriptors, SingleRasterSource,
445
            SpatialGridDescriptor,
446
        },
447
        mock::{MockRasterSource, MockRasterSourceParams},
448
        processing::{Expression, ExpressionParams},
449
        source::{GdalSource, GdalSourceParameters},
450
        util::gdal::add_ndvi_dataset,
451
    };
452

453
    use super::*;
454

455
    #[tokio::test]
456
    async fn it_stacks() {
1✔
457
        it_stacks_impl(crate::engine::TimeDescriptor::new_irregular(None)).await;
1✔
458
    }
1✔
459

460
    #[tokio::test]
461

462
    async fn it_stacks_regular() {
1✔
463
        it_stacks_impl(crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
464
            Some(TimeInterval::new_unchecked(0, 5)),
1✔
465
            TimeStep::millis(5).unwrap(),
1✔
466
        ))
1✔
467
        .await;
1✔
468
    }
1✔
469

470
    #[allow(clippy::too_many_lines)]
471
    async fn it_stacks_impl(time_desc: crate::engine::TimeDescriptor) {
2✔
472
        let data: Vec<RasterTile2D<u8>> = vec![
2✔
473
            RasterTile2D {
2✔
474
                time: TimeInterval::new_unchecked(0, 5),
2✔
475
                tile_position: [-1, 0].into(),
2✔
476
                band: 0,
2✔
477
                global_geo_transform: TestDefault::test_default(),
2✔
478
                grid_array: Grid::new([2, 2].into(), vec![0, 1, 2, 3]).unwrap().into(),
2✔
479
                properties: Default::default(),
2✔
480
                cache_hint: CacheHint::default(),
2✔
481
            },
2✔
482
            RasterTile2D {
2✔
483
                time: TimeInterval::new_unchecked(0, 5),
2✔
484
                tile_position: [-1, 1].into(),
2✔
485
                band: 0,
2✔
486
                global_geo_transform: TestDefault::test_default(),
2✔
487
                grid_array: Grid::new([2, 2].into(), vec![4, 5, 6, 7]).unwrap().into(),
2✔
488
                properties: Default::default(),
2✔
489
                cache_hint: CacheHint::default(),
2✔
490
            },
2✔
491
            RasterTile2D {
2✔
492
                time: TimeInterval::new_unchecked(5, 10),
2✔
493
                tile_position: [-1, 0].into(),
2✔
494
                band: 0,
2✔
495
                global_geo_transform: TestDefault::test_default(),
2✔
496
                grid_array: Grid::new([2, 2].into(), vec![8, 9, 10, 11]).unwrap().into(),
2✔
497
                properties: Default::default(),
2✔
498
                cache_hint: CacheHint::default(),
2✔
499
            },
2✔
500
            RasterTile2D {
2✔
501
                time: TimeInterval::new_unchecked(5, 10),
2✔
502
                tile_position: [-1, 1].into(),
2✔
503
                band: 0,
2✔
504
                global_geo_transform: TestDefault::test_default(),
2✔
505
                grid_array: Grid::new([2, 2].into(), vec![12, 13, 14, 15])
2✔
506
                    .unwrap()
2✔
507
                    .into(),
2✔
508
                properties: Default::default(),
2✔
509
                cache_hint: CacheHint::default(),
2✔
510
            },
2✔
511
        ];
512

513
        let data2: Vec<RasterTile2D<u8>> = vec![
2✔
514
            RasterTile2D {
2✔
515
                time: TimeInterval::new_unchecked(0, 5),
2✔
516
                tile_position: [-1, 0].into(),
2✔
517
                band: 0,
2✔
518
                global_geo_transform: TestDefault::test_default(),
2✔
519
                grid_array: Grid::new([2, 2].into(), vec![16, 17, 18, 19])
2✔
520
                    .unwrap()
2✔
521
                    .into(),
2✔
522
                properties: Default::default(),
2✔
523
                cache_hint: CacheHint::default(),
2✔
524
            },
2✔
525
            RasterTile2D {
2✔
526
                time: TimeInterval::new_unchecked(0, 5),
2✔
527
                tile_position: [-1, 1].into(),
2✔
528
                band: 0,
2✔
529
                global_geo_transform: TestDefault::test_default(),
2✔
530
                grid_array: Grid::new([2, 2].into(), vec![20, 21, 22, 23])
2✔
531
                    .unwrap()
2✔
532
                    .into(),
2✔
533
                properties: Default::default(),
2✔
534
                cache_hint: CacheHint::default(),
2✔
535
            },
2✔
536
            RasterTile2D {
2✔
537
                time: TimeInterval::new_unchecked(5, 10),
2✔
538
                tile_position: [-1, 0].into(),
2✔
539
                band: 0,
2✔
540
                global_geo_transform: TestDefault::test_default(),
2✔
541
                grid_array: Grid::new([2, 2].into(), vec![24, 25, 26, 27])
2✔
542
                    .unwrap()
2✔
543
                    .into(),
2✔
544
                properties: Default::default(),
2✔
545
                cache_hint: CacheHint::default(),
2✔
546
            },
2✔
547
            RasterTile2D {
2✔
548
                time: TimeInterval::new_unchecked(5, 10),
2✔
549
                tile_position: [-1, 1].into(),
2✔
550
                band: 0,
2✔
551
                global_geo_transform: TestDefault::test_default(),
2✔
552
                grid_array: Grid::new([2, 2].into(), vec![28, 29, 30, 31])
2✔
553
                    .unwrap()
2✔
554
                    .into(),
2✔
555
                properties: Default::default(),
2✔
556
                cache_hint: CacheHint::default(),
2✔
557
            },
2✔
558
        ];
559

560
        let result_descriptor1 = RasterResultDescriptor {
2✔
561
            data_type: RasterDataType::U8,
2✔
562
            spatial_reference: SpatialReference::epsg_4326().into(),
2✔
563
            time: time_desc,
2✔
564
            spatial_grid: SpatialGridDescriptor::source_from_parts(
2✔
565
                GeoTransform::test_default(),
2✔
566
                GridBoundingBox2D::new([-2, 0], [-1, 3]).unwrap(),
2✔
567
            ),
2✔
568
            bands: RasterBandDescriptors::new_single_band(),
2✔
569
        };
2✔
570

571
        let mrs1 = MockRasterSource {
2✔
572
            params: MockRasterSourceParams {
2✔
573
                data: data.clone(),
2✔
574
                result_descriptor: result_descriptor1.clone(),
2✔
575
            },
2✔
576
        }
2✔
577
        .boxed();
2✔
578

579
        let mrs2 = MockRasterSource {
2✔
580
            params: MockRasterSourceParams {
2✔
581
                data: data2.clone(),
2✔
582
                result_descriptor: result_descriptor1,
2✔
583
            },
2✔
584
        }
2✔
585
        .boxed();
2✔
586

587
        let stacker = RasterStacker {
2✔
588
            params: RasterStackerParams {
2✔
589
                rename_bands: RenameBands::Default,
2✔
590
            },
2✔
591
            sources: MultipleRasterSources {
2✔
592
                rasters: vec![mrs1, mrs2],
2✔
593
            },
2✔
594
        }
2✔
595
        .boxed();
2✔
596

597
        let mut exe_ctx = MockExecutionContext::test_default();
2✔
598
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
2✔
599
            shape_array: [2, 2],
2✔
600
        };
2✔
601

602
        let query_rect = RasterQueryRectangle::new(
2✔
603
            GridBoundingBox2D::new([-2, 0], [-1, 3]).unwrap(),
2✔
604
            TimeInterval::new_unchecked(0, 10),
2✔
605
            [0, 1].try_into().unwrap(),
2✔
606
        );
607

608
        let query_ctx = exe_ctx.mock_query_context_test_default();
2✔
609

610
        let op = stacker
2✔
611
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
2✔
612
            .await
2✔
613
            .unwrap();
2✔
614

615
        let qp = op.query_processor().unwrap().get_u8().unwrap();
2✔
616

617
        let result = qp
2✔
618
            .raster_query(query_rect, &query_ctx)
2✔
619
            .await
2✔
620
            .unwrap()
2✔
621
            .collect::<Vec<_>>()
2✔
622
            .await;
2✔
623
        let result = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
2✔
624

625
        let expected: Vec<_> = data
2✔
626
            .into_iter()
2✔
627
            .zip(data2.into_iter().map(|mut tile| {
8✔
628
                tile.band = 1;
8✔
629
                tile
8✔
630
            }))
8✔
631
            .flat_map(|(a, b)| vec![a.clone(), b.clone()])
8✔
632
            .collect();
2✔
633

634
        assert!(expected.tiles_equal_ignoring_cache_hint(&result));
2✔
635
    }
2✔
636

637
    #[tokio::test]
638
    async fn it_stacks_stacks() {
1✔
639
        it_stacks_stacks_impl(crate::engine::TimeDescriptor::new_irregular(None)).await;
1✔
640
    }
1✔
641

642
    #[tokio::test]
643
    async fn it_stacks_stacks_regular() {
1✔
644
        it_stacks_stacks_impl(crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
645
            Some(TimeInterval::new_unchecked(0, 10)),
1✔
646
            TimeStep::millis(5).unwrap(),
1✔
647
        ))
1✔
648
        .await;
1✔
649
    }
1✔
650

651
    #[allow(clippy::too_many_lines)]
652
    async fn it_stacks_stacks_impl(time_desc: crate::engine::TimeDescriptor) {
2✔
653
        let data: Vec<RasterTile2D<u8>> = vec![
2✔
654
            RasterTile2D {
2✔
655
                time: TimeInterval::new_unchecked(0, 5),
2✔
656
                tile_position: [-1, 0].into(),
2✔
657
                band: 0,
2✔
658
                global_geo_transform: TestDefault::test_default(),
2✔
659
                grid_array: Grid::new([2, 2].into(), vec![0, 1, 2, 3]).unwrap().into(),
2✔
660
                properties: Default::default(),
2✔
661
                cache_hint: CacheHint::default(),
2✔
662
            },
2✔
663
            RasterTile2D {
2✔
664
                time: TimeInterval::new_unchecked(0, 5),
2✔
665
                tile_position: [-1, 0].into(),
2✔
666
                band: 1,
2✔
667
                global_geo_transform: TestDefault::test_default(),
2✔
668
                grid_array: Grid::new([2, 2].into(), vec![3, 2, 1, 0]).unwrap().into(),
2✔
669
                properties: Default::default(),
2✔
670
                cache_hint: CacheHint::default(),
2✔
671
            },
2✔
672
            RasterTile2D {
2✔
673
                time: TimeInterval::new_unchecked(0, 5),
2✔
674
                tile_position: [-1, 1].into(),
2✔
675
                band: 0,
2✔
676
                global_geo_transform: TestDefault::test_default(),
2✔
677
                grid_array: Grid::new([2, 2].into(), vec![4, 5, 6, 7]).unwrap().into(),
2✔
678
                properties: Default::default(),
2✔
679
                cache_hint: CacheHint::default(),
2✔
680
            },
2✔
681
            RasterTile2D {
2✔
682
                time: TimeInterval::new_unchecked(0, 5),
2✔
683
                tile_position: [-1, 1].into(),
2✔
684
                band: 1,
2✔
685
                global_geo_transform: TestDefault::test_default(),
2✔
686
                grid_array: Grid::new([2, 2].into(), vec![7, 6, 5, 4]).unwrap().into(),
2✔
687
                properties: Default::default(),
2✔
688
                cache_hint: CacheHint::default(),
2✔
689
            },
2✔
690
            RasterTile2D {
2✔
691
                time: TimeInterval::new_unchecked(5, 10),
2✔
692
                tile_position: [-1, 0].into(),
2✔
693
                band: 0,
2✔
694
                global_geo_transform: TestDefault::test_default(),
2✔
695
                grid_array: Grid::new([2, 2].into(), vec![8, 9, 10, 11]).unwrap().into(),
2✔
696
                properties: Default::default(),
2✔
697
                cache_hint: CacheHint::default(),
2✔
698
            },
2✔
699
            RasterTile2D {
2✔
700
                time: TimeInterval::new_unchecked(5, 10),
2✔
701
                tile_position: [-1, 0].into(),
2✔
702
                band: 1,
2✔
703
                global_geo_transform: TestDefault::test_default(),
2✔
704
                grid_array: Grid::new([2, 2].into(), vec![11, 10, 9, 8]).unwrap().into(),
2✔
705
                properties: Default::default(),
2✔
706
                cache_hint: CacheHint::default(),
2✔
707
            },
2✔
708
            RasterTile2D {
2✔
709
                time: TimeInterval::new_unchecked(5, 10),
2✔
710
                tile_position: [-1, 1].into(),
2✔
711
                band: 0,
2✔
712
                global_geo_transform: TestDefault::test_default(),
2✔
713
                grid_array: Grid::new([2, 2].into(), vec![12, 13, 14, 15])
2✔
714
                    .unwrap()
2✔
715
                    .into(),
2✔
716
                properties: Default::default(),
2✔
717
                cache_hint: CacheHint::default(),
2✔
718
            },
2✔
719
            RasterTile2D {
2✔
720
                time: TimeInterval::new_unchecked(5, 10),
2✔
721
                tile_position: [-1, 1].into(),
2✔
722
                band: 1,
2✔
723
                global_geo_transform: TestDefault::test_default(),
2✔
724
                grid_array: Grid::new([2, 2].into(), vec![15, 14, 13, 12])
2✔
725
                    .unwrap()
2✔
726
                    .into(),
2✔
727
                properties: Default::default(),
2✔
728
                cache_hint: CacheHint::default(),
2✔
729
            },
2✔
730
        ];
731

732
        let data2: Vec<RasterTile2D<u8>> = vec![
2✔
733
            RasterTile2D {
2✔
734
                time: TimeInterval::new_unchecked(0, 5),
2✔
735
                tile_position: [-1, 0].into(),
2✔
736
                band: 0,
2✔
737
                global_geo_transform: TestDefault::test_default(),
2✔
738
                grid_array: Grid::new([2, 2].into(), vec![16, 17, 18, 19])
2✔
739
                    .unwrap()
2✔
740
                    .into(),
2✔
741
                properties: Default::default(),
2✔
742
                cache_hint: CacheHint::default(),
2✔
743
            },
2✔
744
            RasterTile2D {
2✔
745
                time: TimeInterval::new_unchecked(0, 5),
2✔
746
                tile_position: [-1, 0].into(),
2✔
747
                band: 1,
2✔
748
                global_geo_transform: TestDefault::test_default(),
2✔
749
                grid_array: Grid::new([2, 2].into(), vec![19, 18, 17, 16])
2✔
750
                    .unwrap()
2✔
751
                    .into(),
2✔
752
                properties: Default::default(),
2✔
753
                cache_hint: CacheHint::default(),
2✔
754
            },
2✔
755
            RasterTile2D {
2✔
756
                time: TimeInterval::new_unchecked(0, 5),
2✔
757
                tile_position: [-1, 1].into(),
2✔
758
                band: 0,
2✔
759
                global_geo_transform: TestDefault::test_default(),
2✔
760
                grid_array: Grid::new([2, 2].into(), vec![20, 21, 22, 23])
2✔
761
                    .unwrap()
2✔
762
                    .into(),
2✔
763
                properties: Default::default(),
2✔
764
                cache_hint: CacheHint::default(),
2✔
765
            },
2✔
766
            RasterTile2D {
2✔
767
                time: TimeInterval::new_unchecked(0, 5),
2✔
768
                tile_position: [-1, 1].into(),
2✔
769
                band: 1,
2✔
770
                global_geo_transform: TestDefault::test_default(),
2✔
771
                grid_array: Grid::new([2, 2].into(), vec![32, 22, 21, 20])
2✔
772
                    .unwrap()
2✔
773
                    .into(),
2✔
774
                properties: Default::default(),
2✔
775
                cache_hint: CacheHint::default(),
2✔
776
            },
2✔
777
            RasterTile2D {
2✔
778
                time: TimeInterval::new_unchecked(5, 10),
2✔
779
                tile_position: [-1, 0].into(),
2✔
780
                band: 0,
2✔
781
                global_geo_transform: TestDefault::test_default(),
2✔
782
                grid_array: Grid::new([2, 2].into(), vec![24, 25, 26, 27])
2✔
783
                    .unwrap()
2✔
784
                    .into(),
2✔
785
                properties: Default::default(),
2✔
786
                cache_hint: CacheHint::default(),
2✔
787
            },
2✔
788
            RasterTile2D {
2✔
789
                time: TimeInterval::new_unchecked(5, 10),
2✔
790
                tile_position: [-1, 0].into(),
2✔
791
                band: 1,
2✔
792
                global_geo_transform: TestDefault::test_default(),
2✔
793
                grid_array: Grid::new([2, 2].into(), vec![27, 26, 25, 24])
2✔
794
                    .unwrap()
2✔
795
                    .into(),
2✔
796
                properties: Default::default(),
2✔
797
                cache_hint: CacheHint::default(),
2✔
798
            },
2✔
799
            RasterTile2D {
2✔
800
                time: TimeInterval::new_unchecked(5, 10),
2✔
801
                tile_position: [-1, 1].into(),
2✔
802
                band: 0,
2✔
803
                global_geo_transform: TestDefault::test_default(),
2✔
804
                grid_array: Grid::new([2, 2].into(), vec![28, 29, 30, 31])
2✔
805
                    .unwrap()
2✔
806
                    .into(),
2✔
807
                properties: Default::default(),
2✔
808
                cache_hint: CacheHint::default(),
2✔
809
            },
2✔
810
            RasterTile2D {
2✔
811
                time: TimeInterval::new_unchecked(5, 10),
2✔
812
                tile_position: [-1, 1].into(),
2✔
813
                band: 1,
2✔
814
                global_geo_transform: TestDefault::test_default(),
2✔
815
                grid_array: Grid::new([2, 2].into(), vec![31, 30, 39, 28])
2✔
816
                    .unwrap()
2✔
817
                    .into(),
2✔
818
                properties: Default::default(),
2✔
819
                cache_hint: CacheHint::default(),
2✔
820
            },
2✔
821
        ];
822

823
        let result_descriptor = RasterResultDescriptor {
2✔
824
            data_type: RasterDataType::U8,
2✔
825
            spatial_reference: SpatialReference::epsg_4326().into(),
2✔
826
            time: time_desc,
2✔
827
            spatial_grid: SpatialGridDescriptor::source_from_parts(
2✔
828
                GeoTransform::test_default(),
2✔
829
                GridBoundingBox2D::new([-2, 0], [-1, 3]).unwrap(),
2✔
830
            ),
2✔
831
            bands: RasterBandDescriptors::new(vec![
2✔
832
                RasterBandDescriptor::new_unitless("band_0".into()),
2✔
833
                RasterBandDescriptor::new_unitless("band_1".into()),
2✔
834
            ])
2✔
835
            .unwrap(),
2✔
836
        };
2✔
837

838
        let mrs1 = MockRasterSource {
2✔
839
            params: MockRasterSourceParams {
2✔
840
                data: data.clone(),
2✔
841
                result_descriptor: result_descriptor.clone(),
2✔
842
            },
2✔
843
        }
2✔
844
        .boxed();
2✔
845

846
        let mrs2 = MockRasterSource {
2✔
847
            params: MockRasterSourceParams {
2✔
848
                data: data2.clone(),
2✔
849
                result_descriptor,
2✔
850
            },
2✔
851
        }
2✔
852
        .boxed();
2✔
853

854
        let stacker = RasterStacker {
2✔
855
            params: RasterStackerParams {
2✔
856
                rename_bands: RenameBands::Default,
2✔
857
            },
2✔
858
            sources: MultipleRasterSources {
2✔
859
                rasters: vec![mrs1, mrs2],
2✔
860
            },
2✔
861
        }
2✔
862
        .boxed();
2✔
863

864
        let mut exe_ctx = MockExecutionContext::test_default();
2✔
865
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
2✔
866
            shape_array: [2, 2],
2✔
867
        };
2✔
868

869
        let query_rect = RasterQueryRectangle::new(
2✔
870
            GridBoundingBox2D::new([-1, 0], [-1, 2]).unwrap(),
2✔
871
            TimeInterval::new_unchecked(0, 10),
2✔
872
            [0, 1, 2, 3].try_into().unwrap(),
2✔
873
        );
874

875
        let query_ctx = exe_ctx.mock_query_context_test_default();
2✔
876

877
        let op = stacker
2✔
878
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
2✔
879
            .await
2✔
880
            .unwrap();
2✔
881

882
        let qp = op.query_processor().unwrap().get_u8().unwrap();
2✔
883

884
        let result = qp
2✔
885
            .raster_query(query_rect, &query_ctx)
2✔
886
            .await
2✔
887
            .unwrap()
2✔
888
            .collect::<Vec<_>>()
2✔
889
            .await;
2✔
890
        let result = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
2✔
891

892
        let expected: Vec<_> = data
2✔
893
            .chunks(2)
2✔
894
            .zip(
2✔
895
                data2
2✔
896
                    .into_iter()
2✔
897
                    .map(|mut tile| {
16✔
898
                        tile.band += 2;
16✔
899
                        tile
16✔
900
                    })
16✔
901
                    .collect::<Vec<_>>()
2✔
902
                    .chunks(2),
2✔
903
            )
904
            .flat_map(|(chunk1, chunk2)| chunk1.iter().chain(chunk2.iter()))
8✔
905
            .cloned()
2✔
906
            .collect();
2✔
907

908
        assert!(expected.tiles_equal_ignoring_cache_hint(&result));
2✔
909
    }
2✔
910

911
    #[tokio::test]
912
    async fn it_selects_band_from_stack() {
1✔
913
        it_selects_band_from_stack_impl(crate::engine::TimeDescriptor::new_irregular(None)).await;
1✔
914
    }
1✔
915

916
    #[tokio::test]
917
    async fn it_selects_band_from_stack_regular() {
1✔
918
        it_selects_band_from_stack_impl(crate::engine::TimeDescriptor::new_regular_with_epoch(
1✔
919
            Some(TimeInterval::new_unchecked(0, 10)),
1✔
920
            TimeStep::millis(5).unwrap(),
1✔
921
        ))
1✔
922
        .await;
1✔
923
    }
1✔
924

925
    #[allow(clippy::too_many_lines)]
926
    async fn it_selects_band_from_stack_impl(time_desc: crate::engine::TimeDescriptor) {
2✔
927
        let data: Vec<RasterTile2D<u8>> = vec![
2✔
928
            RasterTile2D {
2✔
929
                time: TimeInterval::new_unchecked(0, 5),
2✔
930
                tile_position: [-1, 0].into(),
2✔
931
                band: 0,
2✔
932
                global_geo_transform: TestDefault::test_default(),
2✔
933
                grid_array: Grid::new([2, 2].into(), vec![0, 1, 2, 3]).unwrap().into(),
2✔
934
                properties: Default::default(),
2✔
935
                cache_hint: CacheHint::default(),
2✔
936
            },
2✔
937
            RasterTile2D {
2✔
938
                time: TimeInterval::new_unchecked(0, 5),
2✔
939
                tile_position: [-1, 1].into(),
2✔
940
                band: 0,
2✔
941
                global_geo_transform: TestDefault::test_default(),
2✔
942
                grid_array: Grid::new([2, 2].into(), vec![4, 5, 6, 7]).unwrap().into(),
2✔
943
                properties: Default::default(),
2✔
944
                cache_hint: CacheHint::default(),
2✔
945
            },
2✔
946
            RasterTile2D {
2✔
947
                time: TimeInterval::new_unchecked(5, 10),
2✔
948
                tile_position: [-1, 0].into(),
2✔
949
                band: 0,
2✔
950
                global_geo_transform: TestDefault::test_default(),
2✔
951
                grid_array: Grid::new([2, 2].into(), vec![8, 9, 10, 11]).unwrap().into(),
2✔
952
                properties: Default::default(),
2✔
953
                cache_hint: CacheHint::default(),
2✔
954
            },
2✔
955
            RasterTile2D {
2✔
956
                time: TimeInterval::new_unchecked(5, 10),
2✔
957
                tile_position: [-1, 1].into(),
2✔
958
                band: 0,
2✔
959
                global_geo_transform: TestDefault::test_default(),
2✔
960
                grid_array: Grid::new([2, 2].into(), vec![12, 13, 14, 15])
2✔
961
                    .unwrap()
2✔
962
                    .into(),
2✔
963
                properties: Default::default(),
2✔
964
                cache_hint: CacheHint::default(),
2✔
965
            },
2✔
966
        ];
967

968
        let data2: Vec<RasterTile2D<u8>> = vec![
2✔
969
            RasterTile2D {
2✔
970
                time: TimeInterval::new_unchecked(0, 5),
2✔
971
                tile_position: [-1, 0].into(),
2✔
972
                band: 0,
2✔
973
                global_geo_transform: TestDefault::test_default(),
2✔
974
                grid_array: Grid::new([2, 2].into(), vec![16, 17, 18, 19])
2✔
975
                    .unwrap()
2✔
976
                    .into(),
2✔
977
                properties: Default::default(),
2✔
978
                cache_hint: CacheHint::default(),
2✔
979
            },
2✔
980
            RasterTile2D {
2✔
981
                time: TimeInterval::new_unchecked(0, 5),
2✔
982
                tile_position: [-1, 1].into(),
2✔
983
                band: 0,
2✔
984
                global_geo_transform: TestDefault::test_default(),
2✔
985
                grid_array: Grid::new([2, 2].into(), vec![20, 21, 22, 23])
2✔
986
                    .unwrap()
2✔
987
                    .into(),
2✔
988
                properties: Default::default(),
2✔
989
                cache_hint: CacheHint::default(),
2✔
990
            },
2✔
991
            RasterTile2D {
2✔
992
                time: TimeInterval::new_unchecked(5, 10),
2✔
993
                tile_position: [-1, 0].into(),
2✔
994
                band: 0,
2✔
995
                global_geo_transform: TestDefault::test_default(),
2✔
996
                grid_array: Grid::new([2, 2].into(), vec![24, 25, 26, 27])
2✔
997
                    .unwrap()
2✔
998
                    .into(),
2✔
999
                properties: Default::default(),
2✔
1000
                cache_hint: CacheHint::default(),
2✔
1001
            },
2✔
1002
            RasterTile2D {
2✔
1003
                time: TimeInterval::new_unchecked(5, 10),
2✔
1004
                tile_position: [-1, 1].into(),
2✔
1005
                band: 0,
2✔
1006
                global_geo_transform: TestDefault::test_default(),
2✔
1007
                grid_array: Grid::new([2, 2].into(), vec![28, 29, 30, 31])
2✔
1008
                    .unwrap()
2✔
1009
                    .into(),
2✔
1010
                properties: Default::default(),
2✔
1011
                cache_hint: CacheHint::default(),
2✔
1012
            },
2✔
1013
        ];
1014

1015
        let result_descriptor = RasterResultDescriptor {
2✔
1016
            data_type: RasterDataType::U8,
2✔
1017
            spatial_reference: SpatialReference::epsg_4326().into(),
2✔
1018
            time: time_desc,
2✔
1019
            spatial_grid: SpatialGridDescriptor::source_from_parts(
2✔
1020
                GeoTransform::test_default(),
2✔
1021
                GridBoundingBox2D::new([-2, 0], [-1, 3]).unwrap(),
2✔
1022
            ),
2✔
1023
            bands: RasterBandDescriptors::new_single_band(),
2✔
1024
        };
2✔
1025

1026
        let mrs1 = MockRasterSource {
2✔
1027
            params: MockRasterSourceParams {
2✔
1028
                data: data.clone(),
2✔
1029
                result_descriptor: result_descriptor.clone(),
2✔
1030
            },
2✔
1031
        }
2✔
1032
        .boxed();
2✔
1033

1034
        let mrs2 = MockRasterSource {
2✔
1035
            params: MockRasterSourceParams {
2✔
1036
                data: data2.clone(),
2✔
1037
                result_descriptor,
2✔
1038
            },
2✔
1039
        }
2✔
1040
        .boxed();
2✔
1041

1042
        let stacker = RasterStacker {
2✔
1043
            params: RasterStackerParams {
2✔
1044
                rename_bands: RenameBands::Default,
2✔
1045
            },
2✔
1046
            sources: MultipleRasterSources {
2✔
1047
                rasters: vec![mrs1, mrs2],
2✔
1048
            },
2✔
1049
        }
2✔
1050
        .boxed();
2✔
1051

1052
        let mut exe_ctx = MockExecutionContext::test_default();
2✔
1053
        exe_ctx.tiling_specification.tile_size_in_pixels = GridShape {
2✔
1054
            shape_array: [2, 2],
2✔
1055
        };
2✔
1056

1057
        let query_rect = RasterQueryRectangle::new(
2✔
1058
            GridBoundingBox2D::new([-1, 0], [-1, 2]).unwrap(),
2✔
1059
            TimeInterval::new_unchecked(0, 10),
2✔
1060
            1.into(),
2✔
1061
        );
1062

1063
        let query_ctx = exe_ctx.mock_query_context_test_default();
2✔
1064

1065
        let op = stacker
2✔
1066
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
2✔
1067
            .await
2✔
1068
            .unwrap();
2✔
1069

1070
        let qp = op.query_processor().unwrap().get_u8().unwrap();
2✔
1071

1072
        let result = qp
2✔
1073
            .raster_query(query_rect, &query_ctx)
2✔
1074
            .await
2✔
1075
            .unwrap()
2✔
1076
            .collect::<Vec<_>>()
2✔
1077
            .await;
2✔
1078
        let result = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
2✔
1079

1080
        let expected_band1: Vec<_> = data2
2✔
1081
            .iter()
2✔
1082
            .map(|t| {
8✔
1083
                let mut t_1 = t.clone();
8✔
1084
                t_1.band = 1;
8✔
1085
                t_1
8✔
1086
            })
8✔
1087
            .collect();
2✔
1088

1089
        assert_eq_two_list_of_tiles(&result, &expected_band1, false);
2✔
1090
    }
2✔
1091

1092
    #[tokio::test]
1093
    #[allow(clippy::too_many_lines)]
1094
    async fn it_stacks_ndvi() {
1✔
1095
        let mut exe_ctx = MockExecutionContext::test_default();
1✔
1096

1097
        let ndvi_id = add_ndvi_dataset(&mut exe_ctx);
1✔
1098

1099
        let expression = Expression {
1✔
1100
            params: ExpressionParams {
1✔
1101
                expression: "if A > 100 { A } else { 0 }".into(),
1✔
1102
                output_type: RasterDataType::U8,
1✔
1103
                output_band: None,
1✔
1104
                map_no_data: false,
1✔
1105
            },
1✔
1106
            sources: SingleRasterSource {
1✔
1107
                raster: GdalSource {
1✔
1108
                    params: GdalSourceParameters {
1✔
1109
                        data: ndvi_id.clone(),
1✔
1110
                        overview_level: None,
1✔
1111
                    },
1✔
1112
                }
1✔
1113
                .boxed(),
1✔
1114
            },
1✔
1115
        }
1✔
1116
        .boxed();
1✔
1117

1118
        let operator = RasterStacker {
1✔
1119
            params: RasterStackerParams {
1✔
1120
                rename_bands: RenameBands::Default,
1✔
1121
            },
1✔
1122
            sources: MultipleRasterSources {
1✔
1123
                rasters: vec![
1✔
1124
                    GdalSource {
1✔
1125
                        params: GdalSourceParameters::new(ndvi_id),
1✔
1126
                    }
1✔
1127
                    .boxed(),
1✔
1128
                    expression,
1✔
1129
                ],
1✔
1130
            },
1✔
1131
        }
1✔
1132
        .boxed();
1✔
1133

1134
        let operator = operator
1✔
1135
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctx)
1✔
1136
            .await
1✔
1137
            .unwrap();
1✔
1138

1139
        let processor = operator.query_processor().unwrap().get_u8().unwrap();
1✔
1140

1141
        let query_ctx = exe_ctx.mock_query_context_test_default();
1✔
1142

1143
        // query both bands
1144
        let query_rect = RasterQueryRectangle::new(
1✔
1145
            GridBoundingBox2D::new([-900, -1800], [899, 1799]).unwrap(),
1✔
1146
            TimeInterval::new_unchecked(
1✔
1147
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1148
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1149
            ),
1150
            [0, 1].try_into().unwrap(),
1✔
1151
        );
1152

1153
        let result = processor
1✔
1154
            .raster_query(query_rect, &query_ctx)
1✔
1155
            .await
1✔
1156
            .unwrap()
1✔
1157
            .collect::<Vec<_>>()
1✔
1158
            .await;
1✔
1159
        let result = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
1✔
1160

1161
        assert!(!result.is_empty());
1✔
1162

1163
        // query only first band
1164
        let query_rect = RasterQueryRectangle::new(
1✔
1165
            GridBoundingBox2D::new([-900, -1800], [899, 1799]).unwrap(),
1✔
1166
            TimeInterval::new_unchecked(
1✔
1167
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1168
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1169
            ),
1170
            [0].try_into().unwrap(),
1✔
1171
        );
1172

1173
        let result = processor
1✔
1174
            .raster_query(query_rect, &query_ctx)
1✔
1175
            .await
1✔
1176
            .unwrap()
1✔
1177
            .collect::<Vec<_>>()
1✔
1178
            .await;
1✔
1179
        let result_0 = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
1✔
1180

1181
        assert!(!result_0.is_empty());
1✔
1182
        assert!(result_0.iter().all(|t| t.band == 0));
32✔
1183

1184
        // query only second band
1185
        let query_rect = RasterQueryRectangle::new(
1✔
1186
            GridBoundingBox2D::new([-900, -1800], [899, 1799]).unwrap(),
1✔
1187
            TimeInterval::new_unchecked(
1✔
1188
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1189
                TimeInstance::from_str("2014-01-01T00:00:00.000Z").unwrap(),
1✔
1190
            ),
1191
            [1].try_into().unwrap(),
1✔
1192
        );
1193

1194
        let result = processor
1✔
1195
            .raster_query(query_rect, &query_ctx)
1✔
1196
            .await
1✔
1197
            .unwrap()
1✔
1198
            .collect::<Vec<_>>()
1✔
1199
            .await;
1✔
1200
        let result_1 = result.into_iter().collect::<Result<Vec<_>>>().unwrap();
1✔
1201

1202
        assert!(!result_1.is_empty());
1✔
1203
        assert!(result_1.iter().all(|t| t.band == 1));
32✔
1204

1205
        assert_eq!(result_0.len(), result_1.len());
1✔
1206
    }
1✔
1207

1208
    #[test]
1209
    fn it_renames() {
1✔
1210
        let names = vec![
1✔
1211
            vec!["foo".to_string(), "bar".to_string()],
1✔
1212
            vec!["foo".to_string(), "bla".to_string()],
1✔
1213
            vec!["foo".to_string(), "baz".to_string()],
1✔
1214
        ];
1215

1216
        assert_eq!(
1✔
1217
            RenameBands::Default.apply(names.clone()).unwrap(),
1✔
1218
            vec![
1✔
1219
                "foo".to_string(),
1✔
1220
                "bar".to_string(),
1✔
1221
                "foo (1)".to_string(),
1✔
1222
                "bla".to_string(),
1✔
1223
                "foo (2)".to_string(),
1✔
1224
                "baz".to_string()
1✔
1225
            ]
1226
        );
1227

1228
        assert_eq!(
1✔
1229
            RenameBands::Suffix(vec![
1✔
1230
                String::new(),
1✔
1231
                " second".to_string(),
1✔
1232
                " third".to_string()
1✔
1233
            ])
1✔
1234
            .apply(names.clone())
1✔
1235
            .unwrap(),
1✔
1236
            vec![
1✔
1237
                "foo".to_string(),
1✔
1238
                "bar".to_string(),
1✔
1239
                "foo second".to_string(),
1✔
1240
                "bla second".to_string(),
1✔
1241
                "foo third".to_string(),
1✔
1242
                "baz third".to_string()
1✔
1243
            ]
1244
        );
1245

1246
        assert_eq!(
1✔
1247
            RenameBands::Rename(vec![
1✔
1248
                "A".to_string(),
1✔
1249
                "B".to_string(),
1✔
1250
                "C".to_string(),
1✔
1251
                "D".to_string(),
1✔
1252
                "E".to_string(),
1✔
1253
                "F".to_string()
1✔
1254
            ])
1✔
1255
            .apply(names.clone())
1✔
1256
            .unwrap(),
1✔
1257
            vec![
1✔
1258
                "A".to_string(),
1✔
1259
                "B".to_string(),
1✔
1260
                "C".to_string(),
1✔
1261
                "D".to_string(),
1✔
1262
                "E".to_string(),
1✔
1263
                "F".to_string()
1✔
1264
            ]
1265
        );
1266
    }
1✔
1267
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc