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

geo-engine / geoengine / 7006568925

27 Nov 2023 02:07PM UTC coverage: 89.651% (+0.2%) from 89.498%
7006568925

push

github

web-flow
Merge pull request #888 from geo-engine/raster_stacks

raster stacking

4032 of 4274 new or added lines in 107 files covered. (94.34%)

12 existing lines in 8 files now uncovered.

113020 of 126066 relevant lines covered (89.65%)

59901.79 hits per line

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

95.39
/operators/src/processing/raster_vector_join/non_aggregated.rs
1
use crate::adapters::FeatureCollectionStreamExt;
2
use crate::processing::raster_vector_join::create_feature_aggregator;
3
use futures::stream::{once as once_stream, BoxStream};
4
use futures::{StreamExt, TryStreamExt};
5
use geoengine_datatypes::primitives::{
6
    BandSelection, BoundingBox2D, CacheHint, ColumnSelection, FeatureDataType, Geometry,
7
    RasterQueryRectangle, VectorQueryRectangle,
8
};
9
use geoengine_datatypes::util::arrow::ArrowTyped;
10
use std::marker::PhantomData;
11
use std::sync::Arc;
12

13
use geoengine_datatypes::raster::{
14
    DynamicRasterDataType, GridIdx2D, GridIndexAccess, RasterTile2D,
15
};
16
use geoengine_datatypes::{
17
    collections::FeatureCollectionModifications, primitives::TimeInterval, raster::Pixel,
18
};
19

20
use super::util::{CoveredPixels, PixelCoverCreator};
21
use crate::engine::{
22
    QueryContext, QueryProcessor, RasterQueryProcessor, TypedRasterQueryProcessor,
23
    VectorQueryProcessor,
24
};
25
use crate::util::Result;
26
use crate::{adapters::RasterStreamExt, error::Error};
27
use async_trait::async_trait;
28
use geoengine_datatypes::collections::GeometryCollection;
29
use geoengine_datatypes::collections::{FeatureCollection, FeatureCollectionInfos};
30

31
use super::aggregator::TypedAggregator;
32
use super::FeatureAggregationMethod;
33

34
pub struct RasterVectorJoinProcessor<G> {
35
    collection: Box<dyn VectorQueryProcessor<VectorType = FeatureCollection<G>>>,
36
    raster_processors: Vec<TypedRasterQueryProcessor>,
37
    raster_bands: Vec<usize>, // TODO: store result descriptors instead? could drive column names from measurement, once there is one measurement for each band
38
    column_names: Vec<String>,
39
    aggregation_method: FeatureAggregationMethod,
40
    ignore_no_data: bool,
41
}
42

43
impl<G> RasterVectorJoinProcessor<G>
44
where
45
    G: Geometry + ArrowTyped + 'static,
46
    FeatureCollection<G>: GeometryCollection + PixelCoverCreator<G>,
47
{
48
    pub fn new(
7✔
49
        collection: Box<dyn VectorQueryProcessor<VectorType = FeatureCollection<G>>>,
7✔
50
        raster_processors: Vec<TypedRasterQueryProcessor>,
7✔
51
        raster_bands: Vec<usize>,
7✔
52
        column_names: Vec<String>,
7✔
53
        aggregation_method: FeatureAggregationMethod,
7✔
54
        ignore_no_data: bool,
7✔
55
    ) -> Self {
7✔
56
        Self {
7✔
57
            collection,
7✔
58
            raster_processors,
7✔
59
            raster_bands,
7✔
60
            column_names,
7✔
61
            aggregation_method,
7✔
62
            ignore_no_data,
7✔
63
        }
7✔
64
    }
7✔
65

66
    #[allow(clippy::too_many_arguments)]
67
    fn process_collections<'a>(
7✔
68
        collection: BoxStream<'a, Result<FeatureCollection<G>>>,
7✔
69
        raster_processor: &'a TypedRasterQueryProcessor,
7✔
70
        bands: usize,
7✔
71
        new_column_name: &'a str,
7✔
72
        query: VectorQueryRectangle,
7✔
73
        ctx: &'a dyn QueryContext,
7✔
74
        aggregation_method: FeatureAggregationMethod,
7✔
75
        ignore_no_data: bool,
7✔
76
    ) -> BoxStream<'a, Result<FeatureCollection<G>>> {
7✔
77
        let stream = collection.and_then(move |collection| {
7✔
78
            Self::process_collection_chunk(
7✔
79
                collection,
7✔
80
                raster_processor,
7✔
81
                bands,
7✔
82
                new_column_name,
7✔
83
                query.clone(),
7✔
84
                ctx,
7✔
85
                aggregation_method,
7✔
86
                ignore_no_data,
7✔
87
            )
7✔
88
        });
7✔
89

7✔
90
        stream
7✔
91
            .try_flatten()
7✔
92
            .merge_chunks(ctx.chunk_byte_size().into())
7✔
93
            .boxed()
7✔
94
    }
7✔
95

96
    #[allow(clippy::too_many_arguments)]
97
    async fn process_collection_chunk<'a>(
7✔
98
        collection: FeatureCollection<G>,
7✔
99
        raster_processor: &'a TypedRasterQueryProcessor,
7✔
100
        bands: usize,
7✔
101
        new_column_name: &'a str,
7✔
102
        query: VectorQueryRectangle,
7✔
103
        ctx: &'a dyn QueryContext,
7✔
104
        aggregation_method: FeatureAggregationMethod,
7✔
105
        ignore_no_data: bool,
7✔
106
    ) -> Result<BoxStream<'a, Result<FeatureCollection<G>>>> {
7✔
107
        let new_column_names_vec = (0..bands)
7✔
108
            .map(|i| {
8✔
109
                if i == 0 {
8✔
110
                    new_column_name.to_owned()
7✔
111
                } else {
112
                    format!("{new_column_name}_{i}")
1✔
113
                }
114
            })
8✔
115
            .collect::<Vec<_>>();
7✔
116

7✔
117
        if collection.is_empty() {
7✔
118
            log::debug!(
119
                "input collection is empty, returning empty collection, skipping raster query"
×
120
            );
121

NEW
122
            return Self::collection_with_new_null_columns(
×
123
                &collection,
×
NEW
124
                bands,
×
NEW
125
                &new_column_names_vec,
×
126
                raster_processor.raster_data_type().into(),
×
127
            );
×
128
        }
7✔
129

7✔
130
        let bbox = collection
7✔
131
            .bbox()
7✔
132
            .and_then(|bbox| bbox.intersection(&query.spatial_bounds));
7✔
133

7✔
134
        let time = collection
7✔
135
            .time_bounds()
7✔
136
            .and_then(|time| time.intersect(&query.time_interval));
7✔
137

138
        // TODO: also intersect with raster spatial / time bounds
139

140
        let (Some(_spatial_bounds), Some(_time_interval)) = (bbox, time) else {
7✔
141
            log::debug!(
142
                "spatial or temporal intersection is empty, returning the same collection, skipping raster query"
×
143
            );
144

NEW
145
            return Self::collection_with_new_null_columns(
×
146
                &collection,
×
NEW
147
                bands,
×
NEW
148
                &new_column_names_vec,
×
149
                raster_processor.raster_data_type().into(),
×
150
            );
×
151
        };
152

153
        let query =
7✔
154
            RasterQueryRectangle::from_qrect_and_bands(&query, BandSelection::first_n(bands));
7✔
155

7✔
156
        call_on_generic_raster_processor!(raster_processor, raster_processor => {
7✔
157
            Self::process_typed_collection_chunk(
158
                collection,
7✔
159
                raster_processor,
7✔
160
                bands,
7✔
161
                new_column_names_vec,
7✔
162
                query,
7✔
163
                ctx,
7✔
164
                aggregation_method,
7✔
165
                ignore_no_data,
7✔
166
            )
167
            .await
×
168
        })
169
    }
7✔
170

NEW
171
    fn collection_with_new_null_columns<'a>(
×
172
        collection: &FeatureCollection<G>,
×
NEW
173
        bands: usize,
×
NEW
174
        new_column_names: &[String],
×
175
        feature_data_type: FeatureDataType,
×
176
    ) -> Result<BoxStream<'a, Result<FeatureCollection<G>>>> {
×
NEW
177
        let feature_data = (0..bands)
×
NEW
178
            .map(|_| feature_data_type.null_feature_data(collection.len()))
×
NEW
179
            .collect::<Vec<_>>();
×
NEW
180

×
NEW
181
        let columns = new_column_names
×
NEW
182
            .iter()
×
NEW
183
            .map(String::as_str)
×
NEW
184
            .zip(feature_data)
×
NEW
185
            .collect::<Vec<_>>();
×
186

NEW
187
        let collection = collection.add_columns(&columns)?;
×
188

189
        let collection_stream = once_stream(async move { Ok(collection) }).boxed();
×
190
        Ok(collection_stream)
×
191
    }
×
192

193
    #[allow(clippy::too_many_arguments)]
194
    async fn process_typed_collection_chunk<'a, P: Pixel>(
7✔
195
        collection: FeatureCollection<G>,
7✔
196
        raster_processor: &'a dyn RasterQueryProcessor<RasterType = P>,
7✔
197
        bands: usize,
7✔
198
        new_column_names: Vec<String>,
7✔
199
        query: RasterQueryRectangle,
7✔
200
        ctx: &'a dyn QueryContext,
7✔
201
        aggregation_method: FeatureAggregationMethod,
7✔
202
        ignore_no_data: bool,
7✔
203
    ) -> Result<BoxStream<'a, Result<FeatureCollection<G>>>> {
7✔
204
        let raster_query = raster_processor.raster_query(query, ctx).await?;
7✔
205

206
        let collection = Arc::new(collection);
7✔
207

7✔
208
        let collection_stream = raster_query
7✔
209
            .time_multi_fold(
7✔
210
                move || {
12✔
211
                    Ok(VectorRasterJoiner::new(
12✔
212
                        bands,
12✔
213
                        aggregation_method,
12✔
214
                        ignore_no_data,
12✔
215
                    ))
12✔
216
                },
12✔
217
                move |accum, raster| {
240✔
218
                    let collection = collection.clone();
240✔
219
                    async move {
240✔
220
                        let accum = accum?;
240✔
221
                        let raster = raster?;
240✔
222
                        accum.extract_raster_values(&collection, &raster)
240✔
223
                    }
240✔
224
                },
240✔
225
            )
7✔
226
            .map(move |accum| accum?.into_collection(&new_column_names));
12✔
227

7✔
228
        return Ok(collection_stream.boxed());
7✔
229
    }
7✔
230
}
231

232
struct JoinerState<G, C> {
233
    covered_pixels: C,
234
    feature_pixels: Option<Vec<Vec<GridIdx2D>>>,
235
    current_tile: GridIdx2D,
236
    current_band: usize,
237
    aggregators: Vec<TypedAggregator>, // one aggregator per band
238
    g: PhantomData<G>,
239
}
240

241
struct VectorRasterJoiner<G, C> {
242
    state: Option<JoinerState<G, C>>,
243
    bands: usize,
244
    aggregation_method: FeatureAggregationMethod,
245
    ignore_no_data: bool,
246
    cache_hint: CacheHint,
247
}
248

249
impl<G, C> VectorRasterJoiner<G, C>
250
where
251
    G: Geometry + ArrowTyped + 'static,
252
    C: CoveredPixels<G>,
253
    FeatureCollection<G>: PixelCoverCreator<G, C = C>,
254
{
255
    fn new(
12✔
256
        bands: usize,
12✔
257
        aggregation_method: FeatureAggregationMethod,
12✔
258
        ignore_no_data: bool,
12✔
259
    ) -> Self {
12✔
260
        // TODO: is it possible to do the initialization here?
12✔
261

12✔
262
        Self {
12✔
263
            state: None,
12✔
264
            bands,
12✔
265
            aggregation_method,
12✔
266
            ignore_no_data,
12✔
267
            cache_hint: CacheHint::max_duration(),
12✔
268
        }
12✔
269
    }
12✔
270

271
    fn initialize<P: Pixel>(
12✔
272
        &mut self,
12✔
273
        collection: &FeatureCollection<G>,
12✔
274
        raster_time: &TimeInterval,
12✔
275
    ) -> Result<()> {
12✔
276
        // TODO: could be paralellized
12✔
277

12✔
278
        let (indexes, time_intervals): (Vec<_>, Vec<_>) = collection
12✔
279
            .time_intervals()
12✔
280
            .iter()
12✔
281
            .enumerate()
12✔
282
            .filter_map(|(i, time)| {
33✔
283
                time.intersect(raster_time)
33✔
284
                    .map(|time_intersection| (i, time_intersection))
33✔
285
            })
33✔
286
            .unzip();
12✔
287

12✔
288
        let mut valid = vec![false; collection.len()];
12✔
289
        for i in indexes {
41✔
290
            valid[i] = true;
29✔
291
        }
29✔
292

293
        let collection = collection.filter(valid)?;
12✔
294
        let collection = collection.replace_time(&time_intervals)?;
12✔
295

296
        self.state = Some(JoinerState::<G, C> {
12✔
297
            aggregators: (0..self.bands)
12✔
298
                .map(|_| {
14✔
299
                    create_feature_aggregator::<P>(
14✔
300
                        collection.len(),
14✔
301
                        self.aggregation_method,
14✔
302
                        self.ignore_no_data,
14✔
303
                    )
14✔
304
                })
14✔
305
                .collect(),
12✔
306
            covered_pixels: collection.create_covered_pixels(),
12✔
307
            feature_pixels: None,
12✔
308
            current_tile: [0, 0].into(),
12✔
309
            current_band: 0,
12✔
310
            g: Default::default(),
12✔
311
        });
12✔
312

12✔
313
        Ok(())
12✔
314
    }
12✔
315

316
    fn extract_raster_values<P: Pixel>(
240✔
317
        mut self,
240✔
318
        initial_collection: &FeatureCollection<G>,
240✔
319
        raster: &RasterTile2D<P>,
240✔
320
    ) -> Result<Self> {
240✔
321
        let state = loop {
240✔
322
            if let Some(state) = &mut self.state {
252✔
323
                break state;
240✔
324
            }
12✔
325

12✔
326
            self.initialize::<P>(initial_collection, &raster.time)?;
12✔
327
        };
328
        let collection = &state.covered_pixels.collection_ref();
240✔
329
        let aggregator = &mut state.aggregators[raster.band];
240✔
330
        let covered_pixels = &state.covered_pixels;
240✔
331

240✔
332
        if state.feature_pixels.is_some() && raster.tile_position == state.current_tile {
240✔
333
            // same tile as before, but a different band. We can re-use the covered pixels
12✔
334
            state.current_band = raster.band;
12✔
335
            // state
12✔
336
            //     .feature_pixels
12✔
337
            //     .expect("feature_pixels should exist because we checked it above")
12✔
338
        } else {
228✔
339
            // first or new tile, we need to calculcate the covered pixels
228✔
340
            state.current_tile = raster.tile_position;
228✔
341
            state.current_band = raster.band;
228✔
342

228✔
343
            state.feature_pixels = Some(
228✔
344
                (0..collection.len())
228✔
345
                    .map(|feature_index| covered_pixels.covered_pixels(feature_index, raster))
720✔
346
                    .collect::<Vec<_>>(),
228✔
347
            );
228✔
348
        };
228✔
349

350
        for (feature_index, feature_pixels) in state
732✔
351
            .feature_pixels
240✔
352
            .as_ref()
240✔
353
            .expect("should exist because it was calculated before")
240✔
354
            .iter()
240✔
355
            .enumerate()
240✔
356
        {
357
            for grid_idx in feature_pixels {
792✔
358
                let Ok(value) = raster.get_at_grid_index(*grid_idx) else {
60✔
UNCOV
359
                    continue; // not found in this raster tile
×
360
                };
361

362
                if let Some(data) = value {
60✔
363
                    aggregator.add_value(feature_index, data, 1);
60✔
364
                } else {
60✔
365
                    aggregator.add_null(feature_index);
×
366
                }
×
367
            }
368
        }
369

370
        self.cache_hint.merge_with(&raster.cache_hint);
240✔
371

240✔
372
        Ok(self)
240✔
373
    }
240✔
374

375
    fn into_collection(self, new_column_names: &[String]) -> Result<FeatureCollection<G>> {
12✔
376
        let Some(state) = self.state else {
12✔
377
            return Err(Error::EmptyInput); // TODO: maybe output empty dataset or just nulls
×
378
        };
379

380
        let columns = new_column_names
12✔
381
            .iter()
12✔
382
            .map(String::as_str)
12✔
383
            .zip(
12✔
384
                state
12✔
385
                    .aggregators
12✔
386
                    .into_iter()
12✔
387
                    .map(TypedAggregator::into_data),
12✔
388
            )
12✔
389
            .collect::<Vec<_>>();
12✔
390

391
        let mut new_collection = state.covered_pixels.collection().add_columns(&columns)?;
12✔
392

393
        new_collection.cache_hint = self.cache_hint;
12✔
394

12✔
395
        Ok(new_collection)
12✔
396
    }
12✔
397
}
398

399
#[async_trait]
400
impl<G> QueryProcessor for RasterVectorJoinProcessor<G>
401
where
402
    G: Geometry + ArrowTyped + 'static,
403
    FeatureCollection<G>: GeometryCollection + PixelCoverCreator<G>,
404
{
405
    type Output = FeatureCollection<G>;
406
    type SpatialBounds = BoundingBox2D;
407
    type Selection = ColumnSelection;
408

409
    async fn _query<'a>(
7✔
410
        &'a self,
7✔
411
        query: VectorQueryRectangle,
7✔
412
        ctx: &'a dyn QueryContext,
7✔
413
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
7✔
414
        let mut stream = self.collection.query(query.clone(), ctx).await?;
7✔
415

416
        // TODO: adjust raster bands to the vector attribute selection in the query once we support it
417
        for ((raster_processor, new_column_name), bands) in self
7✔
418
            .raster_processors
7✔
419
            .iter()
7✔
420
            .zip(&self.column_names)
7✔
421
            .zip(&self.raster_bands)
7✔
422
        {
423
            log::debug!("processing raster for new column {:?}", new_column_name);
7✔
424
            // TODO: spawn task
425
            stream = Self::process_collections(
7✔
426
                stream,
7✔
427
                raster_processor,
7✔
428
                *bands,
7✔
429
                new_column_name,
7✔
430
                query.clone(),
7✔
431
                ctx,
7✔
432
                self.aggregation_method,
7✔
433
                self.ignore_no_data,
7✔
434
            );
7✔
435
        }
436

437
        Ok(stream)
7✔
438
    }
14✔
439
}
440

441
#[cfg(test)]
442
mod tests {
443
    use super::*;
444

445
    use crate::engine::{
446
        ChunkByteSize, MockExecutionContext, MockQueryContext, QueryProcessor,
447
        RasterBandDescriptor, RasterBandDescriptors, RasterOperator, RasterResultDescriptor,
448
        VectorOperator, WorkflowOperatorPath,
449
    };
450
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
451
    use crate::source::{GdalSource, GdalSourceParameters};
452
    use crate::util::gdal::add_ndvi_dataset;
453
    use geoengine_datatypes::collections::{
454
        ChunksEqualIgnoringCacheHint, MultiPointCollection, MultiPolygonCollection,
455
    };
456
    use geoengine_datatypes::primitives::CacheHint;
457
    use geoengine_datatypes::primitives::SpatialResolution;
458
    use geoengine_datatypes::primitives::{BoundingBox2D, DateTime, FeatureData, MultiPolygon};
459
    use geoengine_datatypes::primitives::{MultiPoint, TimeInterval};
460
    use geoengine_datatypes::raster::{
461
        Grid2D, RasterDataType, TileInformation, TilingSpecification,
462
    };
463
    use geoengine_datatypes::spatial_reference::SpatialReference;
464
    use geoengine_datatypes::util::test::TestDefault;
465

466
    #[tokio::test]
1✔
467
    async fn both_instant() {
1✔
468
        let time_instant =
1✔
469
            TimeInterval::new_instant(DateTime::new_utc(2014, 1, 1, 0, 0, 0)).unwrap();
1✔
470

1✔
471
        let points = MockFeatureCollectionSource::single(
1✔
472
            MultiPointCollection::from_data(
1✔
473
                MultiPoint::many(vec![
1✔
474
                    vec![(-13.95, 20.05)],
1✔
475
                    vec![(-14.05, 20.05)],
1✔
476
                    vec![(-13.95, 19.95)],
1✔
477
                    vec![(-14.05, 19.95)],
1✔
478
                    vec![(-13.95, 19.95), (-14.05, 19.95)],
1✔
479
                ])
1✔
480
                .unwrap(),
1✔
481
                vec![time_instant; 5],
1✔
482
                Default::default(),
1✔
483
                CacheHint::default(),
1✔
484
            )
1✔
485
            .unwrap(),
1✔
486
        )
1✔
487
        .boxed();
1✔
488

1✔
489
        let mut execution_context = MockExecutionContext::test_default();
1✔
490

1✔
491
        let raster_source = GdalSource {
1✔
492
            params: GdalSourceParameters {
1✔
493
                data: add_ndvi_dataset(&mut execution_context),
1✔
494
            },
1✔
495
        }
1✔
496
        .boxed();
1✔
497

498
        let points = points
1✔
499
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
500
            .await
×
501
            .unwrap()
1✔
502
            .query_processor()
1✔
503
            .unwrap()
1✔
504
            .multi_point()
1✔
505
            .unwrap();
1✔
506

507
        let rasters = raster_source
1✔
508
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
509
            .await
×
510
            .unwrap()
1✔
511
            .query_processor()
1✔
512
            .unwrap();
1✔
513

1✔
514
        let processor = RasterVectorJoinProcessor::new(
1✔
515
            points,
1✔
516
            vec![rasters],
1✔
517
            vec![1],
1✔
518
            vec!["ndvi".to_owned()],
1✔
519
            FeatureAggregationMethod::First,
1✔
520
            false,
1✔
521
        );
1✔
522

523
        let mut result = processor
1✔
524
            .query(
1✔
525
                VectorQueryRectangle {
1✔
526
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
527
                        .unwrap(),
1✔
528
                    time_interval: time_instant,
1✔
529
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
530
                    attributes: ColumnSelection::all(),
1✔
531
                },
1✔
532
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
533
            )
1✔
534
            .await
×
535
            .unwrap()
1✔
536
            .map(Result::unwrap)
1✔
537
            .collect::<Vec<MultiPointCollection>>()
1✔
538
            .await;
29✔
539

540
        assert_eq!(result.len(), 1);
1✔
541

542
        let result = result.remove(0);
1✔
543

1✔
544
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
545
            &MultiPointCollection::from_slices(
1✔
546
                &MultiPoint::many(vec![
1✔
547
                    vec![(-13.95, 20.05)],
1✔
548
                    vec![(-14.05, 20.05)],
1✔
549
                    vec![(-13.95, 19.95)],
1✔
550
                    vec![(-14.05, 19.95)],
1✔
551
                    vec![(-13.95, 19.95), (-14.05, 19.95)],
1✔
552
                ])
1✔
553
                .unwrap(),
1✔
554
                &[time_instant; 5],
1✔
555
                // these values are taken from loading the tiff in QGIS
1✔
556
                &[("ndvi", FeatureData::Int(vec![54, 55, 51, 55, 51]))],
1✔
557
            )
1✔
558
            .unwrap()
1✔
559
        ));
1✔
560
    }
561

562
    #[tokio::test]
1✔
563
    async fn points_instant() {
1✔
564
        let points = MockFeatureCollectionSource::single(
1✔
565
            MultiPointCollection::from_data(
1✔
566
                MultiPoint::many(vec![
1✔
567
                    (-13.95, 20.05),
1✔
568
                    (-14.05, 20.05),
1✔
569
                    (-13.95, 19.95),
1✔
570
                    (-14.05, 19.95),
1✔
571
                ])
1✔
572
                .unwrap(),
1✔
573
                vec![TimeInterval::new_instant(DateTime::new_utc(2014, 1, 1, 0, 0, 0)).unwrap(); 4],
1✔
574
                Default::default(),
1✔
575
                CacheHint::default(),
1✔
576
            )
1✔
577
            .unwrap(),
1✔
578
        )
1✔
579
        .boxed();
1✔
580

1✔
581
        let mut execution_context = MockExecutionContext::test_default();
1✔
582

1✔
583
        let raster_source = GdalSource {
1✔
584
            params: GdalSourceParameters {
1✔
585
                data: add_ndvi_dataset(&mut execution_context),
1✔
586
            },
1✔
587
        }
1✔
588
        .boxed();
1✔
589

590
        let points = points
1✔
591
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
592
            .await
×
593
            .unwrap()
1✔
594
            .query_processor()
1✔
595
            .unwrap()
1✔
596
            .multi_point()
1✔
597
            .unwrap();
1✔
598

599
        let rasters = raster_source
1✔
600
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
601
            .await
×
602
            .unwrap()
1✔
603
            .query_processor()
1✔
604
            .unwrap();
1✔
605

1✔
606
        let processor = RasterVectorJoinProcessor::new(
1✔
607
            points,
1✔
608
            vec![rasters],
1✔
609
            vec![1],
1✔
610
            vec!["ndvi".to_owned()],
1✔
611
            FeatureAggregationMethod::First,
1✔
612
            false,
1✔
613
        );
1✔
614

615
        let mut result = processor
1✔
616
            .query(
1✔
617
                VectorQueryRectangle {
1✔
618
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
619
                        .unwrap(),
1✔
620
                    time_interval: TimeInterval::new(
1✔
621
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
622
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
623
                    )
1✔
624
                    .unwrap(),
1✔
625
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
626
                    attributes: ColumnSelection::all(),
1✔
627
                },
1✔
628
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
629
            )
1✔
630
            .await
×
631
            .unwrap()
1✔
632
            .map(Result::unwrap)
1✔
633
            .collect::<Vec<MultiPointCollection>>()
1✔
634
            .await;
60✔
635

636
        assert_eq!(result.len(), 1);
1✔
637

638
        let result = result.remove(0);
1✔
639

1✔
640
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
641
            &MultiPointCollection::from_slices(
1✔
642
                &MultiPoint::many(vec![
1✔
643
                    (-13.95, 20.05),
1✔
644
                    (-14.05, 20.05),
1✔
645
                    (-13.95, 19.95),
1✔
646
                    (-14.05, 19.95),
1✔
647
                ])
1✔
648
                .unwrap(),
1✔
649
                &[TimeInterval::new_instant(DateTime::new_utc(2014, 1, 1, 0, 0, 0)).unwrap(); 4],
1✔
650
                // these values are taken from loading the tiff in QGIS
1✔
651
                &[("ndvi", FeatureData::Int(vec![54, 55, 51, 55]))],
1✔
652
            )
1✔
653
            .unwrap()
1✔
654
        ));
1✔
655
    }
656

657
    #[tokio::test]
1✔
658
    async fn raster_instant() {
1✔
659
        let points = MockFeatureCollectionSource::single(
1✔
660
            MultiPointCollection::from_data(
1✔
661
                MultiPoint::many(vec![
1✔
662
                    (-13.95, 20.05),
1✔
663
                    (-14.05, 20.05),
1✔
664
                    (-13.95, 19.95),
1✔
665
                    (-14.05, 19.95),
1✔
666
                ])
1✔
667
                .unwrap(),
1✔
668
                vec![
1✔
669
                    TimeInterval::new(
1✔
670
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
671
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
672
                    )
1✔
673
                    .unwrap();
1✔
674
                    4
1✔
675
                ],
1✔
676
                Default::default(),
1✔
677
                CacheHint::default(),
1✔
678
            )
1✔
679
            .unwrap(),
1✔
680
        )
1✔
681
        .boxed();
1✔
682

1✔
683
        let mut execution_context = MockExecutionContext::test_default();
1✔
684

1✔
685
        let raster_source = GdalSource {
1✔
686
            params: GdalSourceParameters {
1✔
687
                data: add_ndvi_dataset(&mut execution_context),
1✔
688
            },
1✔
689
        }
1✔
690
        .boxed();
1✔
691

692
        let points = points
1✔
693
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
694
            .await
×
695
            .unwrap()
1✔
696
            .query_processor()
1✔
697
            .unwrap()
1✔
698
            .multi_point()
1✔
699
            .unwrap();
1✔
700

701
        let rasters = raster_source
1✔
702
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
703
            .await
×
704
            .unwrap()
1✔
705
            .query_processor()
1✔
706
            .unwrap();
1✔
707

1✔
708
        let processor = RasterVectorJoinProcessor::new(
1✔
709
            points,
1✔
710
            vec![rasters],
1✔
711
            vec![1],
1✔
712
            vec!["ndvi".to_owned()],
1✔
713
            FeatureAggregationMethod::First,
1✔
714
            false,
1✔
715
        );
1✔
716

717
        let mut result = processor
1✔
718
            .query(
1✔
719
                VectorQueryRectangle {
1✔
720
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
721
                        .unwrap(),
1✔
722
                    time_interval: TimeInterval::new_instant(DateTime::new_utc(
1✔
723
                        2014, 1, 1, 0, 0, 0,
1✔
724
                    ))
1✔
725
                    .unwrap(),
1✔
726
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
727
                    attributes: ColumnSelection::all(),
1✔
728
                },
1✔
729
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
730
            )
1✔
731
            .await
×
732
            .unwrap()
1✔
733
            .map(Result::unwrap)
1✔
734
            .collect::<Vec<MultiPointCollection>>()
1✔
735
            .await;
32✔
736

737
        assert_eq!(result.len(), 1);
1✔
738

739
        let result = result.remove(0);
1✔
740

1✔
741
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
742
            &MultiPointCollection::from_slices(
1✔
743
                &MultiPoint::many(vec![
1✔
744
                    (-13.95, 20.05),
1✔
745
                    (-14.05, 20.05),
1✔
746
                    (-13.95, 19.95),
1✔
747
                    (-14.05, 19.95),
1✔
748
                ])
1✔
749
                .unwrap(),
1✔
750
                &[TimeInterval::new(
1✔
751
                    DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
752
                    DateTime::new_utc(2014, 2, 1, 0, 0, 0),
1✔
753
                )
1✔
754
                .unwrap(); 4],
1✔
755
                // these values are taken from loading the tiff in QGIS
1✔
756
                &[("ndvi", FeatureData::Int(vec![54, 55, 51, 55]))],
1✔
757
            )
1✔
758
            .unwrap()
1✔
759
        ));
1✔
760
    }
761

762
    #[allow(clippy::too_many_lines)]
763
    #[tokio::test]
1✔
764
    async fn both_ranges() {
1✔
765
        let points = MockFeatureCollectionSource::single(
1✔
766
            MultiPointCollection::from_data(
1✔
767
                MultiPoint::many(vec![
1✔
768
                    (-13.95, 20.05),
1✔
769
                    (-14.05, 20.05),
1✔
770
                    (-13.95, 19.95),
1✔
771
                    (-14.05, 19.95),
1✔
772
                ])
1✔
773
                .unwrap(),
1✔
774
                vec![
1✔
775
                    TimeInterval::new(
1✔
776
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
777
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
778
                    )
1✔
779
                    .unwrap();
1✔
780
                    4
1✔
781
                ],
1✔
782
                Default::default(),
1✔
783
                CacheHint::default(),
1✔
784
            )
1✔
785
            .unwrap(),
1✔
786
        )
1✔
787
        .boxed();
1✔
788

1✔
789
        let mut execution_context = MockExecutionContext::test_default();
1✔
790

1✔
791
        let raster_source = GdalSource {
1✔
792
            params: GdalSourceParameters {
1✔
793
                data: add_ndvi_dataset(&mut execution_context),
1✔
794
            },
1✔
795
        }
1✔
796
        .boxed();
1✔
797

798
        let points = points
1✔
799
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
800
            .await
×
801
            .unwrap()
1✔
802
            .query_processor()
1✔
803
            .unwrap()
1✔
804
            .multi_point()
1✔
805
            .unwrap();
1✔
806

807
        let rasters = raster_source
1✔
808
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
809
            .await
×
810
            .unwrap()
1✔
811
            .query_processor()
1✔
812
            .unwrap();
1✔
813

1✔
814
        let processor = RasterVectorJoinProcessor::new(
1✔
815
            points,
1✔
816
            vec![rasters],
1✔
817
            vec![1],
1✔
818
            vec!["ndvi".to_owned()],
1✔
819
            FeatureAggregationMethod::First,
1✔
820
            false,
1✔
821
        );
1✔
822

823
        let mut result = processor
1✔
824
            .query(
1✔
825
                VectorQueryRectangle {
1✔
826
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
827
                        .unwrap(),
1✔
828
                    time_interval: TimeInterval::new(
1✔
829
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
830
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
831
                    )
1✔
832
                    .unwrap(),
1✔
833
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
834
                    attributes: ColumnSelection::all(),
1✔
835
                },
1✔
836
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
837
            )
1✔
838
            .await
×
839
            .unwrap()
1✔
840
            .map(Result::unwrap)
1✔
841
            .collect::<Vec<MultiPointCollection>>()
1✔
842
            .await;
63✔
843

844
        assert_eq!(result.len(), 1);
1✔
845

846
        let result = result.remove(0);
1✔
847

1✔
848
        let t1 = TimeInterval::new(
1✔
849
            DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
850
            DateTime::new_utc(2014, 2, 1, 0, 0, 0),
1✔
851
        )
1✔
852
        .unwrap();
1✔
853
        let t2 = TimeInterval::new(
1✔
854
            DateTime::new_utc(2014, 2, 1, 0, 0, 0),
1✔
855
            DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
856
        )
1✔
857
        .unwrap();
1✔
858
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
859
            &MultiPointCollection::from_slices(
1✔
860
                &MultiPoint::many(vec![
1✔
861
                    (-13.95, 20.05),
1✔
862
                    (-14.05, 20.05),
1✔
863
                    (-13.95, 19.95),
1✔
864
                    (-14.05, 19.95),
1✔
865
                    (-13.95, 20.05),
1✔
866
                    (-14.05, 20.05),
1✔
867
                    (-13.95, 19.95),
1✔
868
                    (-14.05, 19.95),
1✔
869
                ])
1✔
870
                .unwrap(),
1✔
871
                &[t1, t1, t1, t1, t2, t2, t2, t2],
1✔
872
                // these values are taken from loading the tiff in QGIS
1✔
873
                &[(
1✔
874
                    "ndvi",
1✔
875
                    FeatureData::Int(vec![54, 55, 51, 55, 52, 55, 50, 53])
1✔
876
                )],
1✔
877
            )
1✔
878
            .unwrap()
1✔
879
        ));
1✔
880
    }
881

882
    #[tokio::test]
1✔
883
    #[allow(clippy::float_cmp)]
884
    #[allow(clippy::too_many_lines)]
885
    async fn extract_raster_values_two_spatial_tiles_per_time_step_mean() {
1✔
886
        let raster_tile_a_0 = RasterTile2D::new_with_tile_info(
1✔
887
            TimeInterval::new(0, 10).unwrap(),
1✔
888
            TileInformation {
1✔
889
                global_geo_transform: TestDefault::test_default(),
1✔
890
                global_tile_position: [0, 0].into(),
1✔
891
                tile_size_in_pixels: [3, 2].into(),
1✔
892
            },
1✔
893
            0,
1✔
894
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
895
                .unwrap()
1✔
896
                .into(),
1✔
897
            CacheHint::default(),
1✔
898
        );
1✔
899
        let raster_tile_a_1 = RasterTile2D::new_with_tile_info(
1✔
900
            TimeInterval::new(0, 10).unwrap(),
1✔
901
            TileInformation {
1✔
902
                global_geo_transform: TestDefault::test_default(),
1✔
903
                global_tile_position: [0, 1].into(),
1✔
904
                tile_size_in_pixels: [3, 2].into(),
1✔
905
            },
1✔
906
            0,
1✔
907
            Grid2D::new([3, 2].into(), vec![60, 50, 40, 30, 20, 10])
1✔
908
                .unwrap()
1✔
909
                .into(),
1✔
910
            CacheHint::default(),
1✔
911
        );
1✔
912
        let raster_tile_b_0 = RasterTile2D::new_with_tile_info(
1✔
913
            TimeInterval::new(10, 20).unwrap(),
1✔
914
            TileInformation {
1✔
915
                global_geo_transform: TestDefault::test_default(),
1✔
916
                global_tile_position: [0, 0].into(),
1✔
917
                tile_size_in_pixels: [3, 2].into(),
1✔
918
            },
1✔
919
            0,
1✔
920
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
921
                .unwrap()
1✔
922
                .into(),
1✔
923
            CacheHint::default(),
1✔
924
        );
1✔
925
        let raster_tile_b_1 = RasterTile2D::new_with_tile_info(
1✔
926
            TimeInterval::new(10, 20).unwrap(),
1✔
927
            TileInformation {
1✔
928
                global_geo_transform: TestDefault::test_default(),
1✔
929
                global_tile_position: [0, 1].into(),
1✔
930
                tile_size_in_pixels: [3, 2].into(),
1✔
931
            },
1✔
932
            0,
1✔
933
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
934
                .unwrap()
1✔
935
                .into(),
1✔
936
            CacheHint::default(),
1✔
937
        );
1✔
938

1✔
939
        let raster_source = MockRasterSource {
1✔
940
            params: MockRasterSourceParams {
1✔
941
                data: vec![
1✔
942
                    raster_tile_a_0,
1✔
943
                    raster_tile_a_1,
1✔
944
                    raster_tile_b_0,
1✔
945
                    raster_tile_b_1,
1✔
946
                ],
1✔
947
                result_descriptor: RasterResultDescriptor {
1✔
948
                    data_type: RasterDataType::U8,
1✔
949
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
950
                    time: None,
1✔
951
                    bbox: None,
1✔
952
                    resolution: None,
1✔
953
                    bands: RasterBandDescriptors::new_single_band(),
1✔
954
                },
1✔
955
            },
1✔
956
        }
1✔
957
        .boxed();
1✔
958

1✔
959
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
960
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
961
        );
1✔
962

963
        let raster = raster_source
1✔
964
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
965
            .await
×
966
            .unwrap()
1✔
967
            .query_processor()
1✔
968
            .unwrap();
1✔
969

1✔
970
        let points = MultiPointCollection::from_data(
1✔
971
            MultiPoint::many(vec![
1✔
972
                vec![(0.0, 0.0), (2.0, 0.0)],
1✔
973
                vec![(1.0, 0.0), (3.0, 0.0)],
1✔
974
            ])
1✔
975
            .unwrap(),
1✔
976
            vec![TimeInterval::default(); 2],
1✔
977
            Default::default(),
1✔
978
            CacheHint::default(),
1✔
979
        )
1✔
980
        .unwrap();
1✔
981

1✔
982
        let points = MockFeatureCollectionSource::single(points).boxed();
1✔
983

984
        let points = points
1✔
985
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
986
            .await
×
987
            .unwrap()
1✔
988
            .query_processor()
1✔
989
            .unwrap()
1✔
990
            .multi_point()
1✔
991
            .unwrap();
1✔
992

1✔
993
        let processor = RasterVectorJoinProcessor::new(
1✔
994
            points,
1✔
995
            vec![raster],
1✔
996
            vec![1],
1✔
997
            vec!["foo".to_owned()],
1✔
998
            FeatureAggregationMethod::Mean,
1✔
999
            false,
1✔
1000
        );
1✔
1001

1002
        let mut result = processor
1✔
1003
            .query(
1✔
1004
                VectorQueryRectangle {
1✔
1005
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1006
                        .unwrap(),
1✔
1007
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1008
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1009
                    attributes: ColumnSelection::all(),
1✔
1010
                },
1✔
1011
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1012
            )
1✔
1013
            .await
×
1014
            .unwrap()
1✔
1015
            .map(Result::unwrap)
1✔
1016
            .collect::<Vec<MultiPointCollection>>()
1✔
1017
            .await;
×
1018

1019
        assert_eq!(result.len(), 1);
1✔
1020

1021
        let result = result.remove(0);
1✔
1022

1✔
1023
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1024
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1025

1✔
1026
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
1027
            &MultiPointCollection::from_slices(
1✔
1028
                &MultiPoint::many(vec![
1✔
1029
                    vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1030
                    vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1031
                    vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1032
                    vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1033
                ])
1✔
1034
                .unwrap(),
1✔
1035
                &[t1, t1, t2, t2],
1✔
1036
                &[(
1✔
1037
                    "foo",
1✔
1038
                    FeatureData::Float(vec![
1✔
1039
                        (6. + 60.) / 2.,
1✔
1040
                        (5. + 50.) / 2.,
1✔
1041
                        (1. + 10.) / 2.,
1✔
1042
                        (2. + 20.) / 2.
1✔
1043
                    ])
1✔
1044
                )],
1✔
1045
            )
1✔
1046
            .unwrap()
1✔
1047
        ));
1✔
1048
    }
1049

1050
    #[tokio::test]
1✔
1051
    #[allow(clippy::float_cmp)]
1052
    #[allow(clippy::too_many_lines)]
1053
    async fn polygons() {
1✔
1054
        let raster_tile_a_0 = RasterTile2D::new_with_tile_info(
1✔
1055
            TimeInterval::new(0, 10).unwrap(),
1✔
1056
            TileInformation {
1✔
1057
                global_geo_transform: TestDefault::test_default(),
1✔
1058
                global_tile_position: [0, 0].into(),
1✔
1059
                tile_size_in_pixels: [3, 2].into(),
1✔
1060
            },
1✔
1061
            0,
1✔
1062
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
1063
                .unwrap()
1✔
1064
                .into(),
1✔
1065
            CacheHint::default(),
1✔
1066
        );
1✔
1067
        let raster_tile_a_1 = RasterTile2D::new_with_tile_info(
1✔
1068
            TimeInterval::new(0, 10).unwrap(),
1✔
1069
            TileInformation {
1✔
1070
                global_geo_transform: TestDefault::test_default(),
1✔
1071
                global_tile_position: [0, 1].into(),
1✔
1072
                tile_size_in_pixels: [3, 2].into(),
1✔
1073
            },
1✔
1074
            0,
1✔
1075
            Grid2D::new([3, 2].into(), vec![60, 50, 40, 30, 20, 10])
1✔
1076
                .unwrap()
1✔
1077
                .into(),
1✔
1078
            CacheHint::default(),
1✔
1079
        );
1✔
1080
        let raster_tile_a_2 = RasterTile2D::new_with_tile_info(
1✔
1081
            TimeInterval::new(0, 10).unwrap(),
1✔
1082
            TileInformation {
1✔
1083
                global_geo_transform: TestDefault::test_default(),
1✔
1084
                global_tile_position: [0, 2].into(),
1✔
1085
                tile_size_in_pixels: [3, 2].into(),
1✔
1086
            },
1✔
1087
            0,
1✔
1088
            Grid2D::new([3, 2].into(), vec![600, 500, 400, 300, 200, 100])
1✔
1089
                .unwrap()
1✔
1090
                .into(),
1✔
1091
            CacheHint::default(),
1✔
1092
        );
1✔
1093
        let raster_tile_b_0 = RasterTile2D::new_with_tile_info(
1✔
1094
            TimeInterval::new(10, 20).unwrap(),
1✔
1095
            TileInformation {
1✔
1096
                global_geo_transform: TestDefault::test_default(),
1✔
1097
                global_tile_position: [0, 0].into(),
1✔
1098
                tile_size_in_pixels: [3, 2].into(),
1✔
1099
            },
1✔
1100
            0,
1✔
1101
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1102
                .unwrap()
1✔
1103
                .into(),
1✔
1104
            CacheHint::default(),
1✔
1105
        );
1✔
1106
        let raster_tile_b_1 = RasterTile2D::new_with_tile_info(
1✔
1107
            TimeInterval::new(10, 20).unwrap(),
1✔
1108
            TileInformation {
1✔
1109
                global_geo_transform: TestDefault::test_default(),
1✔
1110
                global_tile_position: [0, 1].into(),
1✔
1111
                tile_size_in_pixels: [3, 2].into(),
1✔
1112
            },
1✔
1113
            0,
1✔
1114
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
1115
                .unwrap()
1✔
1116
                .into(),
1✔
1117
            CacheHint::default(),
1✔
1118
        );
1✔
1119

1✔
1120
        let raster_tile_b_2 = RasterTile2D::new_with_tile_info(
1✔
1121
            TimeInterval::new(10, 20).unwrap(),
1✔
1122
            TileInformation {
1✔
1123
                global_geo_transform: TestDefault::test_default(),
1✔
1124
                global_tile_position: [0, 2].into(),
1✔
1125
                tile_size_in_pixels: [3, 2].into(),
1✔
1126
            },
1✔
1127
            0,
1✔
1128
            Grid2D::new([3, 2].into(), vec![100, 200, 300, 400, 500, 600])
1✔
1129
                .unwrap()
1✔
1130
                .into(),
1✔
1131
            CacheHint::default(),
1✔
1132
        );
1✔
1133

1✔
1134
        let raster_source = MockRasterSource {
1✔
1135
            params: MockRasterSourceParams {
1✔
1136
                data: vec![
1✔
1137
                    raster_tile_a_0,
1✔
1138
                    raster_tile_a_1,
1✔
1139
                    raster_tile_a_2,
1✔
1140
                    raster_tile_b_0,
1✔
1141
                    raster_tile_b_1,
1✔
1142
                    raster_tile_b_2,
1✔
1143
                ],
1✔
1144
                result_descriptor: RasterResultDescriptor {
1✔
1145
                    data_type: RasterDataType::U16,
1✔
1146
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1147
                    time: None,
1✔
1148
                    bbox: None,
1✔
1149
                    resolution: None,
1✔
1150
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1151
                },
1✔
1152
            },
1✔
1153
        }
1✔
1154
        .boxed();
1✔
1155

1✔
1156
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1157
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1158
        );
1✔
1159

1160
        let raster = raster_source
1✔
1161
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1162
            .await
×
1163
            .unwrap()
1✔
1164
            .query_processor()
1✔
1165
            .unwrap();
1✔
1166

1✔
1167
        let polygons = MultiPolygonCollection::from_data(
1✔
1168
            vec![MultiPolygon::new(vec![vec![vec![
1✔
1169
                (0.5, -0.5).into(),
1✔
1170
                (4., -1.).into(),
1✔
1171
                (0.5, -2.5).into(),
1✔
1172
                (0.5, -0.5).into(),
1✔
1173
            ]]])
1✔
1174
            .unwrap()],
1✔
1175
            vec![TimeInterval::default(); 1],
1✔
1176
            Default::default(),
1✔
1177
            CacheHint::default(),
1✔
1178
        )
1✔
1179
        .unwrap();
1✔
1180

1✔
1181
        let polygons = MockFeatureCollectionSource::single(polygons).boxed();
1✔
1182

1183
        let points = polygons
1✔
1184
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1185
            .await
×
1186
            .unwrap()
1✔
1187
            .query_processor()
1✔
1188
            .unwrap()
1✔
1189
            .multi_polygon()
1✔
1190
            .unwrap();
1✔
1191

1✔
1192
        let processor = RasterVectorJoinProcessor::new(
1✔
1193
            points,
1✔
1194
            vec![raster],
1✔
1195
            vec![1],
1✔
1196
            vec!["foo".to_owned()],
1✔
1197
            FeatureAggregationMethod::Mean,
1✔
1198
            false,
1✔
1199
        );
1✔
1200

1201
        let mut result = processor
1✔
1202
            .query(
1✔
1203
                VectorQueryRectangle {
1✔
1204
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1205
                        .unwrap(),
1✔
1206
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1207
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1208
                    attributes: ColumnSelection::all(),
1✔
1209
                },
1✔
1210
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1211
            )
1✔
1212
            .await
×
1213
            .unwrap()
1✔
1214
            .map(Result::unwrap)
1✔
1215
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1216
            .await;
×
1217

1218
        assert_eq!(result.len(), 1);
1✔
1219

1220
        let result = result.remove(0);
1✔
1221

1✔
1222
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1223
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1224

1✔
1225
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
1226
            &MultiPolygonCollection::from_slices(
1✔
1227
                &[
1✔
1228
                    MultiPolygon::new(vec![vec![vec![
1✔
1229
                        (0.5, -0.5).into(),
1✔
1230
                        (4., -1.).into(),
1✔
1231
                        (0.5, -2.5).into(),
1✔
1232
                        (0.5, -0.5).into(),
1✔
1233
                    ]]])
1✔
1234
                    .unwrap(),
1✔
1235
                    MultiPolygon::new(vec![vec![vec![
1✔
1236
                        (0.5, -0.5).into(),
1✔
1237
                        (4., -1.).into(),
1✔
1238
                        (0.5, -2.5).into(),
1✔
1239
                        (0.5, -0.5).into(),
1✔
1240
                    ]]])
1✔
1241
                    .unwrap()
1✔
1242
                ],
1✔
1243
                &[t1, t2],
1✔
1244
                &[(
1✔
1245
                    "foo",
1✔
1246
                    FeatureData::Float(vec![
1✔
1247
                        (3. + 1. + 40. + 30. + 400.) / 5.,
1✔
1248
                        (4. + 6. + 30. + 40. + 300.) / 5.
1✔
1249
                    ])
1✔
1250
                )],
1✔
1251
            )
1✔
1252
            .unwrap()
1✔
1253
        ));
1✔
1254
    }
1255

1256
    #[tokio::test]
1✔
1257
    #[allow(clippy::float_cmp)]
1258
    #[allow(clippy::too_many_lines)]
1259
    async fn polygons_multi_band() {
1✔
1260
        let raster_tile_a_0_band_0 = RasterTile2D::new_with_tile_info(
1✔
1261
            TimeInterval::new(0, 10).unwrap(),
1✔
1262
            TileInformation {
1✔
1263
                global_geo_transform: TestDefault::test_default(),
1✔
1264
                global_tile_position: [0, 0].into(),
1✔
1265
                tile_size_in_pixels: [3, 2].into(),
1✔
1266
            },
1✔
1267
            0,
1✔
1268
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
1269
                .unwrap()
1✔
1270
                .into(),
1✔
1271
            CacheHint::default(),
1✔
1272
        );
1✔
1273
        let raster_tile_a_0_band_1 = RasterTile2D::new_with_tile_info(
1✔
1274
            TimeInterval::new(0, 10).unwrap(),
1✔
1275
            TileInformation {
1✔
1276
                global_geo_transform: TestDefault::test_default(),
1✔
1277
                global_tile_position: [0, 0].into(),
1✔
1278
                tile_size_in_pixels: [3, 2].into(),
1✔
1279
            },
1✔
1280
            1,
1✔
1281
            Grid2D::new([3, 2].into(), vec![255, 254, 253, 251, 250, 249])
1✔
1282
                .unwrap()
1✔
1283
                .into(),
1✔
1284
            CacheHint::default(),
1✔
1285
        );
1✔
1286

1✔
1287
        let raster_tile_a_1_band_0 = RasterTile2D::new_with_tile_info(
1✔
1288
            TimeInterval::new(0, 10).unwrap(),
1✔
1289
            TileInformation {
1✔
1290
                global_geo_transform: TestDefault::test_default(),
1✔
1291
                global_tile_position: [0, 1].into(),
1✔
1292
                tile_size_in_pixels: [3, 2].into(),
1✔
1293
            },
1✔
1294
            0,
1✔
1295
            Grid2D::new([3, 2].into(), vec![60, 50, 40, 30, 20, 10])
1✔
1296
                .unwrap()
1✔
1297
                .into(),
1✔
1298
            CacheHint::default(),
1✔
1299
        );
1✔
1300
        let raster_tile_a_1_band_1 = RasterTile2D::new_with_tile_info(
1✔
1301
            TimeInterval::new(0, 10).unwrap(),
1✔
1302
            TileInformation {
1✔
1303
                global_geo_transform: TestDefault::test_default(),
1✔
1304
                global_tile_position: [0, 1].into(),
1✔
1305
                tile_size_in_pixels: [3, 2].into(),
1✔
1306
            },
1✔
1307
            1,
1✔
1308
            Grid2D::new([3, 2].into(), vec![160, 150, 140, 130, 120, 110])
1✔
1309
                .unwrap()
1✔
1310
                .into(),
1✔
1311
            CacheHint::default(),
1✔
1312
        );
1✔
1313

1✔
1314
        let raster_tile_a_2_band_0 = RasterTile2D::new_with_tile_info(
1✔
1315
            TimeInterval::new(0, 10).unwrap(),
1✔
1316
            TileInformation {
1✔
1317
                global_geo_transform: TestDefault::test_default(),
1✔
1318
                global_tile_position: [0, 2].into(),
1✔
1319
                tile_size_in_pixels: [3, 2].into(),
1✔
1320
            },
1✔
1321
            0,
1✔
1322
            Grid2D::new([3, 2].into(), vec![600, 500, 400, 300, 200, 100])
1✔
1323
                .unwrap()
1✔
1324
                .into(),
1✔
1325
            CacheHint::default(),
1✔
1326
        );
1✔
1327
        let raster_tile_a_2_band_1 = RasterTile2D::new_with_tile_info(
1✔
1328
            TimeInterval::new(0, 10).unwrap(),
1✔
1329
            TileInformation {
1✔
1330
                global_geo_transform: TestDefault::test_default(),
1✔
1331
                global_tile_position: [0, 2].into(),
1✔
1332
                tile_size_in_pixels: [3, 2].into(),
1✔
1333
            },
1✔
1334
            1,
1✔
1335
            Grid2D::new([3, 2].into(), vec![610, 510, 410, 310, 210, 110])
1✔
1336
                .unwrap()
1✔
1337
                .into(),
1✔
1338
            CacheHint::default(),
1✔
1339
        );
1✔
1340

1✔
1341
        let raster_tile_b_0_band_0 = RasterTile2D::new_with_tile_info(
1✔
1342
            TimeInterval::new(10, 20).unwrap(),
1✔
1343
            TileInformation {
1✔
1344
                global_geo_transform: TestDefault::test_default(),
1✔
1345
                global_tile_position: [0, 0].into(),
1✔
1346
                tile_size_in_pixels: [3, 2].into(),
1✔
1347
            },
1✔
1348
            0,
1✔
1349
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1350
                .unwrap()
1✔
1351
                .into(),
1✔
1352
            CacheHint::default(),
1✔
1353
        );
1✔
1354
        let raster_tile_b_0_band_1 = RasterTile2D::new_with_tile_info(
1✔
1355
            TimeInterval::new(10, 20).unwrap(),
1✔
1356
            TileInformation {
1✔
1357
                global_geo_transform: TestDefault::test_default(),
1✔
1358
                global_tile_position: [0, 0].into(),
1✔
1359
                tile_size_in_pixels: [3, 2].into(),
1✔
1360
            },
1✔
1361
            1,
1✔
1362
            Grid2D::new([3, 2].into(), vec![11, 22, 33, 44, 55, 66])
1✔
1363
                .unwrap()
1✔
1364
                .into(),
1✔
1365
            CacheHint::default(),
1✔
1366
        );
1✔
1367
        let raster_tile_b_1_band_0 = RasterTile2D::new_with_tile_info(
1✔
1368
            TimeInterval::new(10, 20).unwrap(),
1✔
1369
            TileInformation {
1✔
1370
                global_geo_transform: TestDefault::test_default(),
1✔
1371
                global_tile_position: [0, 1].into(),
1✔
1372
                tile_size_in_pixels: [3, 2].into(),
1✔
1373
            },
1✔
1374
            0,
1✔
1375
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
1376
                .unwrap()
1✔
1377
                .into(),
1✔
1378
            CacheHint::default(),
1✔
1379
        );
1✔
1380
        let raster_tile_b_1_band_1 = RasterTile2D::new_with_tile_info(
1✔
1381
            TimeInterval::new(10, 20).unwrap(),
1✔
1382
            TileInformation {
1✔
1383
                global_geo_transform: TestDefault::test_default(),
1✔
1384
                global_tile_position: [0, 1].into(),
1✔
1385
                tile_size_in_pixels: [3, 2].into(),
1✔
1386
            },
1✔
1387
            1,
1✔
1388
            Grid2D::new([3, 2].into(), vec![100, 220, 300, 400, 500, 600])
1✔
1389
                .unwrap()
1✔
1390
                .into(),
1✔
1391
            CacheHint::default(),
1✔
1392
        );
1✔
1393

1✔
1394
        let raster_tile_b_2_band_0 = RasterTile2D::new_with_tile_info(
1✔
1395
            TimeInterval::new(10, 20).unwrap(),
1✔
1396
            TileInformation {
1✔
1397
                global_geo_transform: TestDefault::test_default(),
1✔
1398
                global_tile_position: [0, 2].into(),
1✔
1399
                tile_size_in_pixels: [3, 2].into(),
1✔
1400
            },
1✔
1401
            0,
1✔
1402
            Grid2D::new([3, 2].into(), vec![100, 200, 300, 400, 500, 600])
1✔
1403
                .unwrap()
1✔
1404
                .into(),
1✔
1405
            CacheHint::default(),
1✔
1406
        );
1✔
1407
        let raster_tile_b_2_band_1 = RasterTile2D::new_with_tile_info(
1✔
1408
            TimeInterval::new(10, 20).unwrap(),
1✔
1409
            TileInformation {
1✔
1410
                global_geo_transform: TestDefault::test_default(),
1✔
1411
                global_tile_position: [0, 2].into(),
1✔
1412
                tile_size_in_pixels: [3, 2].into(),
1✔
1413
            },
1✔
1414
            1,
1✔
1415
            Grid2D::new([3, 2].into(), vec![101, 201, 301, 401, 501, 601])
1✔
1416
                .unwrap()
1✔
1417
                .into(),
1✔
1418
            CacheHint::default(),
1✔
1419
        );
1✔
1420

1✔
1421
        let raster_source = MockRasterSource {
1✔
1422
            params: MockRasterSourceParams {
1✔
1423
                data: vec![
1✔
1424
                    raster_tile_a_0_band_0,
1✔
1425
                    raster_tile_a_0_band_1,
1✔
1426
                    raster_tile_a_1_band_0,
1✔
1427
                    raster_tile_a_1_band_1,
1✔
1428
                    raster_tile_a_2_band_0,
1✔
1429
                    raster_tile_a_2_band_1,
1✔
1430
                    raster_tile_b_0_band_0,
1✔
1431
                    raster_tile_b_0_band_1,
1✔
1432
                    raster_tile_b_1_band_0,
1✔
1433
                    raster_tile_b_1_band_1,
1✔
1434
                    raster_tile_b_2_band_0,
1✔
1435
                    raster_tile_b_2_band_1,
1✔
1436
                ],
1✔
1437
                result_descriptor: RasterResultDescriptor {
1✔
1438
                    data_type: RasterDataType::U16,
1✔
1439
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1440
                    time: None,
1✔
1441
                    bbox: None,
1✔
1442
                    resolution: None,
1✔
1443
                    bands: RasterBandDescriptors::new(vec![
1✔
1444
                        RasterBandDescriptor::new_unitless("band_0".into()),
1✔
1445
                        RasterBandDescriptor::new_unitless("band_1".into()),
1✔
1446
                    ])
1✔
1447
                    .unwrap(),
1✔
1448
                },
1✔
1449
            },
1✔
1450
        }
1✔
1451
        .boxed();
1✔
1452

1✔
1453
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1454
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1455
        );
1✔
1456

1457
        let raster = raster_source
1✔
1458
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
NEW
1459
            .await
×
1460
            .unwrap()
1✔
1461
            .query_processor()
1✔
1462
            .unwrap();
1✔
1463

1✔
1464
        let polygons = MultiPolygonCollection::from_data(
1✔
1465
            vec![MultiPolygon::new(vec![vec![vec![
1✔
1466
                (0.5, -0.5).into(),
1✔
1467
                (4., -1.).into(),
1✔
1468
                (0.5, -2.5).into(),
1✔
1469
                (0.5, -0.5).into(),
1✔
1470
            ]]])
1✔
1471
            .unwrap()],
1✔
1472
            vec![TimeInterval::default(); 1],
1✔
1473
            Default::default(),
1✔
1474
            CacheHint::default(),
1✔
1475
        )
1✔
1476
        .unwrap();
1✔
1477

1✔
1478
        let polygons = MockFeatureCollectionSource::single(polygons).boxed();
1✔
1479

1480
        let points = polygons
1✔
1481
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
NEW
1482
            .await
×
1483
            .unwrap()
1✔
1484
            .query_processor()
1✔
1485
            .unwrap()
1✔
1486
            .multi_polygon()
1✔
1487
            .unwrap();
1✔
1488

1✔
1489
        let processor = RasterVectorJoinProcessor::new(
1✔
1490
            points,
1✔
1491
            vec![raster],
1✔
1492
            vec![2],
1✔
1493
            vec!["foo".to_owned()],
1✔
1494
            FeatureAggregationMethod::Mean,
1✔
1495
            false,
1✔
1496
        );
1✔
1497

1498
        let mut result = processor
1✔
1499
            .query(
1✔
1500
                VectorQueryRectangle {
1✔
1501
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1502
                        .unwrap(),
1✔
1503
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1504
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1505
                    attributes: ColumnSelection::all(),
1✔
1506
                },
1✔
1507
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1508
            )
1✔
NEW
1509
            .await
×
1510
            .unwrap()
1✔
1511
            .map(Result::unwrap)
1✔
1512
            .collect::<Vec<MultiPolygonCollection>>()
1✔
NEW
1513
            .await;
×
1514

1515
        assert_eq!(result.len(), 1);
1✔
1516

1517
        let result = result.remove(0);
1✔
1518

1✔
1519
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1520
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1521

1✔
1522
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
1523
            &MultiPolygonCollection::from_slices(
1✔
1524
                &[
1✔
1525
                    MultiPolygon::new(vec![vec![vec![
1✔
1526
                        (0.5, -0.5).into(),
1✔
1527
                        (4., -1.).into(),
1✔
1528
                        (0.5, -2.5).into(),
1✔
1529
                        (0.5, -0.5).into(),
1✔
1530
                    ]]])
1✔
1531
                    .unwrap(),
1✔
1532
                    MultiPolygon::new(vec![vec![vec![
1✔
1533
                        (0.5, -0.5).into(),
1✔
1534
                        (4., -1.).into(),
1✔
1535
                        (0.5, -2.5).into(),
1✔
1536
                        (0.5, -0.5).into(),
1✔
1537
                    ]]])
1✔
1538
                    .unwrap()
1✔
1539
                ],
1✔
1540
                &[t1, t2],
1✔
1541
                &[
1✔
1542
                    (
1✔
1543
                        "foo",
1✔
1544
                        FeatureData::Float(vec![
1✔
1545
                            (3. + 1. + 40. + 30. + 400.) / 5.,
1✔
1546
                            (4. + 6. + 30. + 40. + 300.) / 5.
1✔
1547
                        ])
1✔
1548
                    ),
1✔
1549
                    (
1✔
1550
                        "foo_1",
1✔
1551
                        FeatureData::Float(vec![
1✔
1552
                            (251. + 249. + 140. + 130. + 410.) / 5.,
1✔
1553
                            (44. + 66. + 300. + 400. + 301.) / 5.
1✔
1554
                        ])
1✔
1555
                    )
1✔
1556
                ],
1✔
1557
            )
1✔
1558
            .unwrap()
1✔
1559
        ));
1✔
1560
    }
1561
}
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

© 2025 Coveralls, Inc