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

geo-engine / geoengine / 16410182728

21 Jul 2025 06:33AM UTC coverage: 88.748% (+0.01%) from 88.738%
16410182728

push

github

web-flow
feat(operators): skip empty tiles and merge masks in onnx; remove trace/debug in release mode (#1061)

* wip

* remove more trace and debug calls on release builds

* rename tracing removed macros

* add mask merge and tile skip params to onnx operator

* fmt

* rust 1.88

* clippy auto-fixes

* manual clippy fixes

* update deps

* cargo update

* update onnx

* cargo fmt

* update sqlfluff

* update lock

* adapt to main changes

* adapt array4 usage in onnnx

* add docs

* replace log with tracing

* rework nodata handling

* rework nodata handling #2

* set features and rename variable

* move ml model no data handling to model metadata

* refactor ml model api types

* delegate MlModelName serde to impl type

* move migration sql into own file

* tagged or untagged ???

* lint sql

* cargo.toml

* use next()

* event_anabled

* don't serialize operator type

* remove todo

* debug tracing

* openapi camel case

---------

Co-authored-by: Christian Beilschmidt <christian.beilschmidt@geoengine.de>

419 of 578 new or added lines in 44 files covered. (72.49%)

15 existing lines in 7 files now uncovered.

111437 of 125565 relevant lines covered (88.75%)

80307.98 hits per line

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

97.3
/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::{BoxStream, once as once_stream};
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, VectorResultDescriptor,
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, RasterInput};
33

34
pub struct RasterVectorJoinProcessor<G> {
35
    collection: Box<dyn VectorQueryProcessor<VectorType = FeatureCollection<G>>>,
36
    result_descriptor: VectorResultDescriptor,
37
    raster_inputs: Vec<RasterInput>,
38
    aggregation_method: FeatureAggregationMethod,
39
    ignore_no_data: bool,
40
}
41

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

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

85
        stream
7✔
86
            .try_flatten()
7✔
87
            .merge_chunks(ctx.chunk_byte_size().into())
7✔
88
            .boxed()
7✔
89
    }
7✔
90

91
    #[allow(clippy::too_many_arguments)]
92
    async fn process_collection_chunk<'a>(
7✔
93
        collection: FeatureCollection<G>,
7✔
94
        raster_processor: &'a TypedRasterQueryProcessor,
7✔
95
        column_names: &'a [String],
7✔
96
        query: VectorQueryRectangle,
7✔
97
        ctx: &'a dyn QueryContext,
7✔
98
        aggregation_method: FeatureAggregationMethod,
7✔
99
        ignore_no_data: bool,
7✔
100
    ) -> Result<BoxStream<'a, Result<FeatureCollection<G>>>> {
7✔
101
        if collection.is_empty() {
7✔
NEW
102
            tracing::debug!(
×
103
                "input collection is empty, returning empty collection, skipping raster query"
×
104
            );
105

106
            return Self::collection_with_new_null_columns(
×
107
                &collection,
×
108
                column_names,
×
109
                raster_processor.raster_data_type().into(),
×
110
            );
111
        }
7✔
112

113
        let bbox = collection
7✔
114
            .bbox()
7✔
115
            .and_then(|bbox| bbox.intersection(&query.spatial_bounds));
7✔
116

117
        let time = collection
7✔
118
            .time_bounds()
7✔
119
            .and_then(|time| time.intersect(&query.time_interval));
7✔
120

121
        // TODO: also intersect with raster spatial / time bounds
122

123
        let (Some(_spatial_bounds), Some(_time_interval)) = (bbox, time) else {
7✔
NEW
124
            tracing::debug!(
×
125
                "spatial or temporal intersection is empty, returning the same collection, skipping raster query"
×
126
            );
127

128
            return Self::collection_with_new_null_columns(
×
129
                &collection,
×
130
                column_names,
×
131
                raster_processor.raster_data_type().into(),
×
132
            );
133
        };
134

135
        let query = RasterQueryRectangle::from_qrect_and_bands(
7✔
136
            &query,
7✔
137
            BandSelection::first_n(column_names.len() as u32),
7✔
138
        );
139

140
        call_on_generic_raster_processor!(raster_processor, raster_processor => {
7✔
141
            Self::process_typed_collection_chunk(
4✔
142
                collection,
4✔
143
                raster_processor,
4✔
144
                column_names,
4✔
145
                query,
4✔
146
                ctx,
4✔
147
                aggregation_method,
4✔
148
                ignore_no_data,
4✔
149
            )
150
            .await
×
151
        })
152
    }
7✔
153

154
    fn collection_with_new_null_columns<'a>(
×
155
        collection: &FeatureCollection<G>,
×
156
        column_names: &'a [String],
×
157
        feature_data_type: FeatureDataType,
×
158
    ) -> Result<BoxStream<'a, Result<FeatureCollection<G>>>> {
×
159
        let feature_data = (0..column_names.len())
×
160
            .map(|_| feature_data_type.null_feature_data(collection.len()))
×
161
            .collect::<Vec<_>>();
×
162

163
        let columns = column_names
×
164
            .iter()
×
165
            .map(String::as_str)
×
166
            .zip(feature_data)
×
167
            .collect::<Vec<_>>();
×
168

169
        let collection = collection.add_columns(&columns)?;
×
170

171
        let collection_stream = once_stream(async move { Ok(collection) }).boxed();
×
172
        Ok(collection_stream)
×
173
    }
×
174

175
    #[allow(clippy::too_many_arguments)]
176
    async fn process_typed_collection_chunk<'a, P: Pixel>(
7✔
177
        collection: FeatureCollection<G>,
7✔
178
        raster_processor: &'a dyn RasterQueryProcessor<RasterType = P>,
7✔
179
        column_names: &'a [String],
7✔
180
        query: RasterQueryRectangle,
7✔
181
        ctx: &'a dyn QueryContext,
7✔
182
        aggregation_method: FeatureAggregationMethod,
7✔
183
        ignore_no_data: bool,
7✔
184
    ) -> Result<BoxStream<'a, Result<FeatureCollection<G>>>> {
7✔
185
        let raster_query = raster_processor.raster_query(query, ctx).await?;
7✔
186

187
        let collection = Arc::new(collection);
7✔
188

189
        let collection_stream = raster_query
7✔
190
            .time_multi_fold(
7✔
191
                move || {
12✔
192
                    Ok(VectorRasterJoiner::new(
12✔
193
                        column_names.len() as u32,
12✔
194
                        aggregation_method,
12✔
195
                        ignore_no_data,
12✔
196
                    ))
12✔
197
                },
12✔
198
                move |accum, raster| {
240✔
199
                    let collection = collection.clone();
240✔
200
                    async move {
240✔
201
                        let accum = accum?;
240✔
202
                        let raster = raster?;
240✔
203
                        accum.extract_raster_values(&collection, &raster)
240✔
204
                    }
240✔
205
                },
240✔
206
            )
207
            .map(move |accum| accum?.into_collection(column_names));
12✔
208

209
        Ok(collection_stream.boxed())
7✔
210
    }
7✔
211
}
212

213
struct JoinerState<G, C> {
214
    covered_pixels: C,
215
    feature_pixels: Option<Vec<Vec<GridIdx2D>>>,
216
    current_tile: GridIdx2D,
217
    current_band_idx: u32,
218
    aggregators: Vec<TypedAggregator>, // one aggregator per band
219
    g: PhantomData<G>,
220
}
221

222
struct VectorRasterJoiner<G, C> {
223
    state: Option<JoinerState<G, C>>,
224
    num_bands: u32,
225
    aggregation_method: FeatureAggregationMethod,
226
    ignore_no_data: bool,
227
    cache_hint: CacheHint,
228
}
229

230
impl<G, C> VectorRasterJoiner<G, C>
231
where
232
    G: Geometry + ArrowTyped + 'static,
233
    C: CoveredPixels<G>,
234
    FeatureCollection<G>: PixelCoverCreator<G, C = C>,
235
{
236
    fn new(
12✔
237
        num_bands: u32,
12✔
238
        aggregation_method: FeatureAggregationMethod,
12✔
239
        ignore_no_data: bool,
12✔
240
    ) -> Self {
12✔
241
        // TODO: is it possible to do the initialization here?
242

243
        Self {
12✔
244
            state: None,
12✔
245
            num_bands,
12✔
246
            aggregation_method,
12✔
247
            ignore_no_data,
12✔
248
            cache_hint: CacheHint::max_duration(),
12✔
249
        }
12✔
250
    }
12✔
251

252
    fn initialize<P: Pixel>(
12✔
253
        &mut self,
12✔
254
        collection: &FeatureCollection<G>,
12✔
255
        raster_time: &TimeInterval,
12✔
256
    ) -> Result<()> {
12✔
257
        // TODO: could be paralellized
258

259
        let (indexes, time_intervals): (Vec<_>, Vec<_>) = collection
12✔
260
            .time_intervals()
12✔
261
            .iter()
12✔
262
            .enumerate()
12✔
263
            .filter_map(|(i, time)| {
33✔
264
                time.intersect(raster_time)
33✔
265
                    .map(|time_intersection| (i, time_intersection))
33✔
266
            })
33✔
267
            .unzip();
12✔
268

269
        let mut valid = vec![false; collection.len()];
12✔
270
        for i in indexes {
41✔
271
            valid[i] = true;
29✔
272
        }
29✔
273

274
        let collection = collection.filter(valid)?;
12✔
275
        let collection = collection.replace_time(&time_intervals)?;
12✔
276

277
        self.state = Some(JoinerState::<G, C> {
12✔
278
            aggregators: (0..self.num_bands)
12✔
279
                .map(|_| {
14✔
280
                    create_feature_aggregator::<P>(
14✔
281
                        collection.len(),
14✔
282
                        self.aggregation_method,
14✔
283
                        self.ignore_no_data,
14✔
284
                    )
285
                })
14✔
286
                .collect(),
12✔
287
            covered_pixels: collection.create_covered_pixels(),
12✔
288
            feature_pixels: None,
12✔
289
            current_tile: [0, 0].into(),
12✔
290
            current_band_idx: 0,
291
            g: Default::default(),
12✔
292
        });
293

294
        Ok(())
12✔
295
    }
12✔
296

297
    fn extract_raster_values<P: Pixel>(
240✔
298
        mut self,
240✔
299
        initial_collection: &FeatureCollection<G>,
240✔
300
        raster: &RasterTile2D<P>,
240✔
301
    ) -> Result<Self> {
240✔
302
        let state = loop {
240✔
303
            if let Some(state) = &mut self.state {
252✔
304
                break state;
240✔
305
            }
12✔
306

307
            self.initialize::<P>(initial_collection, &raster.time)?;
12✔
308
        };
309
        let collection = &state.covered_pixels.collection_ref();
240✔
310
        let aggregator = &mut state.aggregators[raster.band as usize];
240✔
311
        let covered_pixels = &state.covered_pixels;
240✔
312

313
        if state.feature_pixels.is_some() && raster.tile_position == state.current_tile {
240✔
314
            // same tile as before, but a different band. We can re-use the covered pixels
12✔
315
            state.current_band_idx = raster.band;
12✔
316
            // state
12✔
317
            //     .feature_pixels
12✔
318
            //     .expect("feature_pixels should exist because we checked it above")
12✔
319
        } else {
12✔
320
            // first or new tile, we need to calculcate the covered pixels
321
            state.current_tile = raster.tile_position;
228✔
322
            state.current_band_idx = raster.band;
228✔
323

324
            state.feature_pixels = Some(
228✔
325
                (0..collection.len())
228✔
326
                    .map(|feature_index| covered_pixels.covered_pixels(feature_index, raster))
720✔
327
                    .collect::<Vec<_>>(),
228✔
328
            );
329
        }
330

331
        for (feature_index, feature_pixels) in state
732✔
332
            .feature_pixels
240✔
333
            .as_ref()
240✔
334
            .expect("should exist because it was calculated before")
240✔
335
            .iter()
240✔
336
            .enumerate()
240✔
337
        {
338
            for grid_idx in feature_pixels {
792✔
339
                let Ok(value) = raster.get_at_grid_index(*grid_idx) else {
60✔
340
                    continue; // not found in this raster tile
×
341
                };
342

343
                if let Some(data) = value {
60✔
344
                    aggregator.add_value(feature_index, data, 1);
60✔
345
                } else {
60✔
346
                    aggregator.add_null(feature_index);
×
347
                }
×
348
            }
349
        }
350

351
        self.cache_hint.merge_with(&raster.cache_hint);
240✔
352

353
        Ok(self)
240✔
354
    }
240✔
355

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

361
        let columns = new_column_names
12✔
362
            .iter()
12✔
363
            .map(String::as_str)
12✔
364
            .zip(
12✔
365
                state
12✔
366
                    .aggregators
12✔
367
                    .into_iter()
12✔
368
                    .map(TypedAggregator::into_data),
12✔
369
            )
370
            .collect::<Vec<_>>();
12✔
371

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

374
        new_collection.cache_hint = self.cache_hint;
12✔
375

376
        Ok(new_collection)
12✔
377
    }
12✔
378
}
379

380
#[async_trait]
381
impl<G> QueryProcessor for RasterVectorJoinProcessor<G>
382
where
383
    G: Geometry + ArrowTyped + 'static,
384
    FeatureCollection<G>: GeometryCollection + PixelCoverCreator<G>,
385
{
386
    type Output = FeatureCollection<G>;
387
    type SpatialBounds = BoundingBox2D;
388
    type Selection = ColumnSelection;
389
    type ResultDescription = VectorResultDescriptor;
390

391
    async fn _query<'a>(
392
        &'a self,
393
        query: VectorQueryRectangle,
394
        ctx: &'a dyn QueryContext,
395
    ) -> Result<BoxStream<'a, Result<Self::Output>>> {
14✔
396
        let mut stream = self.collection.query(query.clone(), ctx).await?;
7✔
397

398
        // TODO: adjust raster bands to the vector attribute selection in the query once we support it
399
        for raster_input in &self.raster_inputs {
14✔
400
            tracing::debug!(
7✔
401
                "processing raster for new columns {:?}",
×
402
                raster_input.column_names
403
            );
404
            // TODO: spawn task
405
            stream = Self::process_collections(
7✔
406
                stream,
7✔
407
                &raster_input.processor,
7✔
408
                &raster_input.column_names,
7✔
409
                query.clone(),
7✔
410
                ctx,
7✔
411
                self.aggregation_method,
7✔
412
                self.ignore_no_data,
7✔
413
            );
414
        }
415

416
        Ok(stream)
7✔
417
    }
14✔
418

419
    fn result_descriptor(&self) -> &VectorResultDescriptor {
7✔
420
        &self.result_descriptor
7✔
421
    }
7✔
422
}
423

424
#[cfg(test)]
425
mod tests {
426
    use super::*;
427

428
    use crate::engine::{
429
        ChunkByteSize, MockExecutionContext, MockQueryContext, QueryProcessor,
430
        RasterBandDescriptor, RasterBandDescriptors, RasterOperator, RasterResultDescriptor,
431
        VectorColumnInfo, VectorOperator, WorkflowOperatorPath,
432
    };
433
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
434
    use crate::source::{GdalSource, GdalSourceParameters};
435
    use crate::util::gdal::add_ndvi_dataset;
436
    use geoengine_datatypes::collections::{
437
        ChunksEqualIgnoringCacheHint, MultiPointCollection, MultiPolygonCollection, VectorDataType,
438
    };
439
    use geoengine_datatypes::primitives::SpatialResolution;
440
    use geoengine_datatypes::primitives::{BoundingBox2D, DateTime, FeatureData, MultiPolygon};
441
    use geoengine_datatypes::primitives::{CacheHint, Measurement};
442
    use geoengine_datatypes::primitives::{MultiPoint, TimeInterval};
443
    use geoengine_datatypes::raster::{
444
        Grid2D, RasterDataType, TileInformation, TilingSpecification,
445
    };
446
    use geoengine_datatypes::spatial_reference::{SpatialReference, SpatialReferenceOption};
447
    use geoengine_datatypes::util::test::TestDefault;
448

449
    #[tokio::test]
450
    async fn both_instant() {
1✔
451
        let time_instant =
1✔
452
            TimeInterval::new_instant(DateTime::new_utc(2014, 1, 1, 0, 0, 0)).unwrap();
1✔
453

454
        let points = MockFeatureCollectionSource::single(
1✔
455
            MultiPointCollection::from_data(
1✔
456
                MultiPoint::many(vec![
1✔
457
                    vec![(-13.95, 20.05)],
1✔
458
                    vec![(-14.05, 20.05)],
1✔
459
                    vec![(-13.95, 19.95)],
1✔
460
                    vec![(-14.05, 19.95)],
1✔
461
                    vec![(-13.95, 19.95), (-14.05, 19.95)],
1✔
462
                ])
463
                .unwrap(),
1✔
464
                vec![time_instant; 5],
1✔
465
                Default::default(),
1✔
466
                CacheHint::default(),
1✔
467
            )
468
            .unwrap(),
1✔
469
        )
470
        .boxed();
1✔
471

472
        let mut execution_context = MockExecutionContext::test_default();
1✔
473

474
        let raster_source = GdalSource {
1✔
475
            params: GdalSourceParameters {
1✔
476
                data: add_ndvi_dataset(&mut execution_context),
1✔
477
            },
1✔
478
        }
1✔
479
        .boxed();
1✔
480

481
        let points = points
1✔
482
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
483
            .await
1✔
484
            .unwrap()
1✔
485
            .query_processor()
1✔
486
            .unwrap()
1✔
487
            .multi_point()
1✔
488
            .unwrap();
1✔
489

490
        let rasters = raster_source
1✔
491
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
492
            .await
1✔
493
            .unwrap()
1✔
494
            .query_processor()
1✔
495
            .unwrap();
1✔
496

497
        let processor = RasterVectorJoinProcessor::new(
1✔
498
            points,
1✔
499
            VectorResultDescriptor {
1✔
500
                data_type: VectorDataType::MultiPoint,
1✔
501
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
502
                columns: [(
1✔
503
                    "ndvi".to_string(),
1✔
504
                    VectorColumnInfo {
1✔
505
                        data_type: FeatureDataType::Int,
1✔
506
                        measurement: Measurement::Unitless,
1✔
507
                    },
1✔
508
                )]
1✔
509
                .into_iter()
1✔
510
                .collect(),
1✔
511
                time: None,
1✔
512
                bbox: None,
1✔
513
            },
1✔
514
            vec![RasterInput {
1✔
515
                processor: rasters,
1✔
516
                column_names: vec!["ndvi".to_owned()],
1✔
517
            }],
1✔
518
            FeatureAggregationMethod::First,
1✔
519
            false,
520
        );
521

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

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

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

543
        assert!(
1✔
544
            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
        );
1✔
561
    }
1✔
562

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

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

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

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

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

607
        let processor = RasterVectorJoinProcessor::new(
1✔
608
            points,
1✔
609
            VectorResultDescriptor {
1✔
610
                data_type: VectorDataType::MultiPoint,
1✔
611
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
612
                columns: [(
1✔
613
                    "ndvi".to_string(),
1✔
614
                    VectorColumnInfo {
1✔
615
                        data_type: FeatureDataType::Int,
1✔
616
                        measurement: Measurement::Unitless,
1✔
617
                    },
1✔
618
                )]
1✔
619
                .into_iter()
1✔
620
                .collect(),
1✔
621
                time: None,
1✔
622
                bbox: None,
1✔
623
            },
1✔
624
            vec![RasterInput {
1✔
625
                processor: rasters,
1✔
626
                column_names: vec!["ndvi".to_owned()],
1✔
627
            }],
1✔
628
            FeatureAggregationMethod::First,
1✔
629
            false,
630
        );
631

632
        let mut result = processor
1✔
633
            .query(
1✔
634
                VectorQueryRectangle {
1✔
635
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
636
                        .unwrap(),
1✔
637
                    time_interval: TimeInterval::new(
1✔
638
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
639
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
640
                    )
1✔
641
                    .unwrap(),
1✔
642
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
643
                    attributes: ColumnSelection::all(),
1✔
644
                },
1✔
645
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
646
            )
1✔
647
            .await
1✔
648
            .unwrap()
1✔
649
            .map(Result::unwrap)
1✔
650
            .collect::<Vec<MultiPointCollection>>()
1✔
651
            .await;
1✔
652

653
        assert_eq!(result.len(), 1);
1✔
654

655
        let result = result.remove(0);
1✔
656

657
        assert!(
1✔
658
            result.chunks_equal_ignoring_cache_hint(
1✔
659
                &MultiPointCollection::from_slices(
1✔
660
                    &MultiPoint::many(vec![
1✔
661
                        (-13.95, 20.05),
1✔
662
                        (-14.05, 20.05),
1✔
663
                        (-13.95, 19.95),
1✔
664
                        (-14.05, 19.95),
1✔
665
                    ])
1✔
666
                    .unwrap(),
1✔
667
                    &[TimeInterval::new_instant(DateTime::new_utc(2014, 1, 1, 0, 0, 0)).unwrap();
1✔
668
                        4],
1✔
669
                    // these values are taken from loading the tiff in QGIS
1✔
670
                    &[("ndvi", FeatureData::Int(vec![54, 55, 51, 55]))],
1✔
671
                )
1✔
672
                .unwrap()
1✔
673
            )
1✔
674
        );
1✔
675
    }
1✔
676

677
    #[tokio::test]
678
    #[allow(clippy::too_many_lines)]
679
    async fn raster_instant() {
1✔
680
        let points = MockFeatureCollectionSource::single(
1✔
681
            MultiPointCollection::from_data(
1✔
682
                MultiPoint::many(vec![
1✔
683
                    (-13.95, 20.05),
1✔
684
                    (-14.05, 20.05),
1✔
685
                    (-13.95, 19.95),
1✔
686
                    (-14.05, 19.95),
1✔
687
                ])
688
                .unwrap(),
1✔
689
                vec![
1✔
690
                    TimeInterval::new(
1✔
691
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
692
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
693
                    )
694
                    .unwrap();
1✔
695
                    4
696
                ],
697
                Default::default(),
1✔
698
                CacheHint::default(),
1✔
699
            )
700
            .unwrap(),
1✔
701
        )
702
        .boxed();
1✔
703

704
        let mut execution_context = MockExecutionContext::test_default();
1✔
705

706
        let raster_source = GdalSource {
1✔
707
            params: GdalSourceParameters {
1✔
708
                data: add_ndvi_dataset(&mut execution_context),
1✔
709
            },
1✔
710
        }
1✔
711
        .boxed();
1✔
712

713
        let points = points
1✔
714
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
715
            .await
1✔
716
            .unwrap()
1✔
717
            .query_processor()
1✔
718
            .unwrap()
1✔
719
            .multi_point()
1✔
720
            .unwrap();
1✔
721

722
        let rasters = raster_source
1✔
723
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
724
            .await
1✔
725
            .unwrap()
1✔
726
            .query_processor()
1✔
727
            .unwrap();
1✔
728

729
        let processor = RasterVectorJoinProcessor::new(
1✔
730
            points,
1✔
731
            VectorResultDescriptor {
1✔
732
                data_type: VectorDataType::MultiPoint,
1✔
733
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
734
                columns: [(
1✔
735
                    "ndvi".to_string(),
1✔
736
                    VectorColumnInfo {
1✔
737
                        data_type: FeatureDataType::Int,
1✔
738
                        measurement: Measurement::Unitless,
1✔
739
                    },
1✔
740
                )]
1✔
741
                .into_iter()
1✔
742
                .collect(),
1✔
743
                time: None,
1✔
744
                bbox: None,
1✔
745
            },
1✔
746
            vec![RasterInput {
1✔
747
                processor: rasters,
1✔
748
                column_names: vec!["ndvi".to_owned()],
1✔
749
            }],
1✔
750
            FeatureAggregationMethod::First,
1✔
751
            false,
752
        );
753

754
        let mut result = processor
1✔
755
            .query(
1✔
756
                VectorQueryRectangle {
1✔
757
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
758
                        .unwrap(),
1✔
759
                    time_interval: TimeInterval::new_instant(DateTime::new_utc(
1✔
760
                        2014, 1, 1, 0, 0, 0,
1✔
761
                    ))
1✔
762
                    .unwrap(),
1✔
763
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
764
                    attributes: ColumnSelection::all(),
1✔
765
                },
1✔
766
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
767
            )
1✔
768
            .await
1✔
769
            .unwrap()
1✔
770
            .map(Result::unwrap)
1✔
771
            .collect::<Vec<MultiPointCollection>>()
1✔
772
            .await;
1✔
773

774
        assert_eq!(result.len(), 1);
1✔
775

776
        let result = result.remove(0);
1✔
777

778
        assert!(
1✔
779
            result.chunks_equal_ignoring_cache_hint(
1✔
780
                &MultiPointCollection::from_slices(
1✔
781
                    &MultiPoint::many(vec![
1✔
782
                        (-13.95, 20.05),
1✔
783
                        (-14.05, 20.05),
1✔
784
                        (-13.95, 19.95),
1✔
785
                        (-14.05, 19.95),
1✔
786
                    ])
1✔
787
                    .unwrap(),
1✔
788
                    &[TimeInterval::new(
1✔
789
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
790
                        DateTime::new_utc(2014, 2, 1, 0, 0, 0),
1✔
791
                    )
1✔
792
                    .unwrap(); 4],
1✔
793
                    // these values are taken from loading the tiff in QGIS
1✔
794
                    &[("ndvi", FeatureData::Int(vec![54, 55, 51, 55]))],
1✔
795
                )
1✔
796
                .unwrap()
1✔
797
            )
1✔
798
        );
1✔
799
    }
1✔
800

801
    #[allow(clippy::too_many_lines)]
802
    #[tokio::test]
803
    async fn both_ranges() {
1✔
804
        let points = MockFeatureCollectionSource::single(
1✔
805
            MultiPointCollection::from_data(
1✔
806
                MultiPoint::many(vec![
1✔
807
                    (-13.95, 20.05),
1✔
808
                    (-14.05, 20.05),
1✔
809
                    (-13.95, 19.95),
1✔
810
                    (-14.05, 19.95),
1✔
811
                ])
812
                .unwrap(),
1✔
813
                vec![
1✔
814
                    TimeInterval::new(
1✔
815
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
816
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
817
                    )
818
                    .unwrap();
1✔
819
                    4
820
                ],
821
                Default::default(),
1✔
822
                CacheHint::default(),
1✔
823
            )
824
            .unwrap(),
1✔
825
        )
826
        .boxed();
1✔
827

828
        let mut execution_context = MockExecutionContext::test_default();
1✔
829

830
        let raster_source = GdalSource {
1✔
831
            params: GdalSourceParameters {
1✔
832
                data: add_ndvi_dataset(&mut execution_context),
1✔
833
            },
1✔
834
        }
1✔
835
        .boxed();
1✔
836

837
        let points = points
1✔
838
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
839
            .await
1✔
840
            .unwrap()
1✔
841
            .query_processor()
1✔
842
            .unwrap()
1✔
843
            .multi_point()
1✔
844
            .unwrap();
1✔
845

846
        let rasters = raster_source
1✔
847
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
848
            .await
1✔
849
            .unwrap()
1✔
850
            .query_processor()
1✔
851
            .unwrap();
1✔
852

853
        let processor = RasterVectorJoinProcessor::new(
1✔
854
            points,
1✔
855
            VectorResultDescriptor {
1✔
856
                data_type: VectorDataType::MultiPoint,
1✔
857
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
858
                columns: [(
1✔
859
                    "ndvi".to_string(),
1✔
860
                    VectorColumnInfo {
1✔
861
                        data_type: FeatureDataType::Int,
1✔
862
                        measurement: Measurement::Unitless,
1✔
863
                    },
1✔
864
                )]
1✔
865
                .into_iter()
1✔
866
                .collect(),
1✔
867
                time: None,
1✔
868
                bbox: None,
1✔
869
            },
1✔
870
            vec![RasterInput {
1✔
871
                processor: rasters,
1✔
872
                column_names: vec!["ndvi".to_owned()],
1✔
873
            }],
1✔
874
            FeatureAggregationMethod::First,
1✔
875
            false,
876
        );
877

878
        let mut result = processor
1✔
879
            .query(
1✔
880
                VectorQueryRectangle {
1✔
881
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
882
                        .unwrap(),
1✔
883
                    time_interval: TimeInterval::new(
1✔
884
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
885
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
886
                    )
1✔
887
                    .unwrap(),
1✔
888
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
889
                    attributes: ColumnSelection::all(),
1✔
890
                },
1✔
891
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
892
            )
1✔
893
            .await
1✔
894
            .unwrap()
1✔
895
            .map(Result::unwrap)
1✔
896
            .collect::<Vec<MultiPointCollection>>()
1✔
897
            .await;
1✔
898

899
        assert_eq!(result.len(), 1);
1✔
900

901
        let result = result.remove(0);
1✔
902

903
        let t1 = TimeInterval::new(
1✔
904
            DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
905
            DateTime::new_utc(2014, 2, 1, 0, 0, 0),
1✔
906
        )
907
        .unwrap();
1✔
908
        let t2 = TimeInterval::new(
1✔
909
            DateTime::new_utc(2014, 2, 1, 0, 0, 0),
1✔
910
            DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
911
        )
912
        .unwrap();
1✔
913
        assert!(
1✔
914
            result.chunks_equal_ignoring_cache_hint(
1✔
915
                &MultiPointCollection::from_slices(
1✔
916
                    &MultiPoint::many(vec![
1✔
917
                        (-13.95, 20.05),
1✔
918
                        (-14.05, 20.05),
1✔
919
                        (-13.95, 19.95),
1✔
920
                        (-14.05, 19.95),
1✔
921
                        (-13.95, 20.05),
1✔
922
                        (-14.05, 20.05),
1✔
923
                        (-13.95, 19.95),
1✔
924
                        (-14.05, 19.95),
1✔
925
                    ])
1✔
926
                    .unwrap(),
1✔
927
                    &[t1, t1, t1, t1, t2, t2, t2, t2],
1✔
928
                    // these values are taken from loading the tiff in QGIS
1✔
929
                    &[(
1✔
930
                        "ndvi",
1✔
931
                        FeatureData::Int(vec![54, 55, 51, 55, 52, 55, 50, 53])
1✔
932
                    )],
1✔
933
                )
1✔
934
                .unwrap()
1✔
935
            )
1✔
936
        );
1✔
937
    }
1✔
938

939
    #[tokio::test]
940
    #[allow(clippy::float_cmp)]
941
    #[allow(clippy::too_many_lines)]
942
    async fn extract_raster_values_two_spatial_tiles_per_time_step_mean() {
1✔
943
        let raster_tile_a_0 = RasterTile2D::new_with_tile_info(
1✔
944
            TimeInterval::new(0, 10).unwrap(),
1✔
945
            TileInformation {
1✔
946
                global_geo_transform: TestDefault::test_default(),
1✔
947
                global_tile_position: [0, 0].into(),
1✔
948
                tile_size_in_pixels: [3, 2].into(),
1✔
949
            },
1✔
950
            0,
951
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
952
                .unwrap()
1✔
953
                .into(),
1✔
954
            CacheHint::default(),
1✔
955
        );
956
        let raster_tile_a_1 = RasterTile2D::new_with_tile_info(
1✔
957
            TimeInterval::new(0, 10).unwrap(),
1✔
958
            TileInformation {
1✔
959
                global_geo_transform: TestDefault::test_default(),
1✔
960
                global_tile_position: [0, 1].into(),
1✔
961
                tile_size_in_pixels: [3, 2].into(),
1✔
962
            },
1✔
963
            0,
964
            Grid2D::new([3, 2].into(), vec![60, 50, 40, 30, 20, 10])
1✔
965
                .unwrap()
1✔
966
                .into(),
1✔
967
            CacheHint::default(),
1✔
968
        );
969
        let raster_tile_b_0 = RasterTile2D::new_with_tile_info(
1✔
970
            TimeInterval::new(10, 20).unwrap(),
1✔
971
            TileInformation {
1✔
972
                global_geo_transform: TestDefault::test_default(),
1✔
973
                global_tile_position: [0, 0].into(),
1✔
974
                tile_size_in_pixels: [3, 2].into(),
1✔
975
            },
1✔
976
            0,
977
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
978
                .unwrap()
1✔
979
                .into(),
1✔
980
            CacheHint::default(),
1✔
981
        );
982
        let raster_tile_b_1 = RasterTile2D::new_with_tile_info(
1✔
983
            TimeInterval::new(10, 20).unwrap(),
1✔
984
            TileInformation {
1✔
985
                global_geo_transform: TestDefault::test_default(),
1✔
986
                global_tile_position: [0, 1].into(),
1✔
987
                tile_size_in_pixels: [3, 2].into(),
1✔
988
            },
1✔
989
            0,
990
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
991
                .unwrap()
1✔
992
                .into(),
1✔
993
            CacheHint::default(),
1✔
994
        );
995

996
        let raster_source = MockRasterSource {
1✔
997
            params: MockRasterSourceParams {
1✔
998
                data: vec![
1✔
999
                    raster_tile_a_0,
1✔
1000
                    raster_tile_a_1,
1✔
1001
                    raster_tile_b_0,
1✔
1002
                    raster_tile_b_1,
1✔
1003
                ],
1✔
1004
                result_descriptor: RasterResultDescriptor {
1✔
1005
                    data_type: RasterDataType::U8,
1✔
1006
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1007
                    time: None,
1✔
1008
                    bbox: None,
1✔
1009
                    resolution: None,
1✔
1010
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1011
                },
1✔
1012
            },
1✔
1013
        }
1✔
1014
        .boxed();
1✔
1015

1016
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1017
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1018
        );
1019

1020
        let raster = raster_source
1✔
1021
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1022
            .await
1✔
1023
            .unwrap()
1✔
1024
            .query_processor()
1✔
1025
            .unwrap();
1✔
1026

1027
        let points = MultiPointCollection::from_data(
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
            ])
1032
            .unwrap(),
1✔
1033
            vec![TimeInterval::default(); 2],
1✔
1034
            Default::default(),
1✔
1035
            CacheHint::default(),
1✔
1036
        )
1037
        .unwrap();
1✔
1038

1039
        let points = MockFeatureCollectionSource::single(points).boxed();
1✔
1040

1041
        let points = points
1✔
1042
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1043
            .await
1✔
1044
            .unwrap()
1✔
1045
            .query_processor()
1✔
1046
            .unwrap()
1✔
1047
            .multi_point()
1✔
1048
            .unwrap();
1✔
1049

1050
        let processor = RasterVectorJoinProcessor::new(
1✔
1051
            points,
1✔
1052
            VectorResultDescriptor {
1✔
1053
                data_type: VectorDataType::MultiPoint,
1✔
1054
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
1055
                columns: [(
1✔
1056
                    "ndvi".to_string(),
1✔
1057
                    VectorColumnInfo {
1✔
1058
                        data_type: FeatureDataType::Int,
1✔
1059
                        measurement: Measurement::Unitless,
1✔
1060
                    },
1✔
1061
                )]
1✔
1062
                .into_iter()
1✔
1063
                .collect(),
1✔
1064
                time: None,
1✔
1065
                bbox: None,
1✔
1066
            },
1✔
1067
            vec![RasterInput {
1✔
1068
                processor: raster,
1✔
1069
                column_names: vec!["ndvi".to_owned()],
1✔
1070
            }],
1✔
1071
            FeatureAggregationMethod::Mean,
1✔
1072
            false,
1073
        );
1074

1075
        let mut result = processor
1✔
1076
            .query(
1✔
1077
                VectorQueryRectangle {
1✔
1078
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1079
                        .unwrap(),
1✔
1080
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1081
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1082
                    attributes: ColumnSelection::all(),
1✔
1083
                },
1✔
1084
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1085
            )
1✔
1086
            .await
1✔
1087
            .unwrap()
1✔
1088
            .map(Result::unwrap)
1✔
1089
            .collect::<Vec<MultiPointCollection>>()
1✔
1090
            .await;
1✔
1091

1092
        assert_eq!(result.len(), 1);
1✔
1093

1094
        let result = result.remove(0);
1✔
1095

1096
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1097
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1098

1099
        assert!(
1✔
1100
            result.chunks_equal_ignoring_cache_hint(
1✔
1101
                &MultiPointCollection::from_slices(
1✔
1102
                    &MultiPoint::many(vec![
1✔
1103
                        vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1104
                        vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1105
                        vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1106
                        vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1107
                    ])
1✔
1108
                    .unwrap(),
1✔
1109
                    &[t1, t1, t2, t2],
1✔
1110
                    &[(
1✔
1111
                        "ndvi",
1✔
1112
                        FeatureData::Float(vec![
1✔
1113
                            f64::midpoint(6., 60.),
1✔
1114
                            f64::midpoint(5., 50.),
1✔
1115
                            f64::midpoint(1., 10.),
1✔
1116
                            f64::midpoint(2., 20.)
1✔
1117
                        ])
1✔
1118
                    )],
1✔
1119
                )
1✔
1120
                .unwrap()
1✔
1121
            )
1✔
1122
        );
1✔
1123
    }
1✔
1124

1125
    #[tokio::test]
1126
    #[allow(clippy::float_cmp)]
1127
    #[allow(clippy::too_many_lines)]
1128
    async fn polygons() {
1✔
1129
        let raster_tile_a_0 = RasterTile2D::new_with_tile_info(
1✔
1130
            TimeInterval::new(0, 10).unwrap(),
1✔
1131
            TileInformation {
1✔
1132
                global_geo_transform: TestDefault::test_default(),
1✔
1133
                global_tile_position: [0, 0].into(),
1✔
1134
                tile_size_in_pixels: [3, 2].into(),
1✔
1135
            },
1✔
1136
            0,
1137
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
1138
                .unwrap()
1✔
1139
                .into(),
1✔
1140
            CacheHint::default(),
1✔
1141
        );
1142
        let raster_tile_a_1 = RasterTile2D::new_with_tile_info(
1✔
1143
            TimeInterval::new(0, 10).unwrap(),
1✔
1144
            TileInformation {
1✔
1145
                global_geo_transform: TestDefault::test_default(),
1✔
1146
                global_tile_position: [0, 1].into(),
1✔
1147
                tile_size_in_pixels: [3, 2].into(),
1✔
1148
            },
1✔
1149
            0,
1150
            Grid2D::new([3, 2].into(), vec![60, 50, 40, 30, 20, 10])
1✔
1151
                .unwrap()
1✔
1152
                .into(),
1✔
1153
            CacheHint::default(),
1✔
1154
        );
1155
        let raster_tile_a_2 = RasterTile2D::new_with_tile_info(
1✔
1156
            TimeInterval::new(0, 10).unwrap(),
1✔
1157
            TileInformation {
1✔
1158
                global_geo_transform: TestDefault::test_default(),
1✔
1159
                global_tile_position: [0, 2].into(),
1✔
1160
                tile_size_in_pixels: [3, 2].into(),
1✔
1161
            },
1✔
1162
            0,
1163
            Grid2D::new([3, 2].into(), vec![600, 500, 400, 300, 200, 100])
1✔
1164
                .unwrap()
1✔
1165
                .into(),
1✔
1166
            CacheHint::default(),
1✔
1167
        );
1168
        let raster_tile_b_0 = RasterTile2D::new_with_tile_info(
1✔
1169
            TimeInterval::new(10, 20).unwrap(),
1✔
1170
            TileInformation {
1✔
1171
                global_geo_transform: TestDefault::test_default(),
1✔
1172
                global_tile_position: [0, 0].into(),
1✔
1173
                tile_size_in_pixels: [3, 2].into(),
1✔
1174
            },
1✔
1175
            0,
1176
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1177
                .unwrap()
1✔
1178
                .into(),
1✔
1179
            CacheHint::default(),
1✔
1180
        );
1181
        let raster_tile_b_1 = RasterTile2D::new_with_tile_info(
1✔
1182
            TimeInterval::new(10, 20).unwrap(),
1✔
1183
            TileInformation {
1✔
1184
                global_geo_transform: TestDefault::test_default(),
1✔
1185
                global_tile_position: [0, 1].into(),
1✔
1186
                tile_size_in_pixels: [3, 2].into(),
1✔
1187
            },
1✔
1188
            0,
1189
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
1190
                .unwrap()
1✔
1191
                .into(),
1✔
1192
            CacheHint::default(),
1✔
1193
        );
1194

1195
        let raster_tile_b_2 = RasterTile2D::new_with_tile_info(
1✔
1196
            TimeInterval::new(10, 20).unwrap(),
1✔
1197
            TileInformation {
1✔
1198
                global_geo_transform: TestDefault::test_default(),
1✔
1199
                global_tile_position: [0, 2].into(),
1✔
1200
                tile_size_in_pixels: [3, 2].into(),
1✔
1201
            },
1✔
1202
            0,
1203
            Grid2D::new([3, 2].into(), vec![100, 200, 300, 400, 500, 600])
1✔
1204
                .unwrap()
1✔
1205
                .into(),
1✔
1206
            CacheHint::default(),
1✔
1207
        );
1208

1209
        let raster_source = MockRasterSource {
1✔
1210
            params: MockRasterSourceParams {
1✔
1211
                data: vec![
1✔
1212
                    raster_tile_a_0,
1✔
1213
                    raster_tile_a_1,
1✔
1214
                    raster_tile_a_2,
1✔
1215
                    raster_tile_b_0,
1✔
1216
                    raster_tile_b_1,
1✔
1217
                    raster_tile_b_2,
1✔
1218
                ],
1✔
1219
                result_descriptor: RasterResultDescriptor {
1✔
1220
                    data_type: RasterDataType::U16,
1✔
1221
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1222
                    time: None,
1✔
1223
                    bbox: None,
1✔
1224
                    resolution: None,
1✔
1225
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1226
                },
1✔
1227
            },
1✔
1228
        }
1✔
1229
        .boxed();
1✔
1230

1231
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1232
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1233
        );
1234

1235
        let raster = raster_source
1✔
1236
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1237
            .await
1✔
1238
            .unwrap()
1✔
1239
            .query_processor()
1✔
1240
            .unwrap();
1✔
1241

1242
        let polygons = MultiPolygonCollection::from_data(
1✔
1243
            vec![
1✔
1244
                MultiPolygon::new(vec![vec![vec![
1✔
1245
                    (0.5, -0.5).into(),
1✔
1246
                    (4., -1.).into(),
1✔
1247
                    (0.5, -2.5).into(),
1✔
1248
                    (0.5, -0.5).into(),
1✔
1249
                ]]])
1250
                .unwrap(),
1✔
1251
            ],
1252
            vec![TimeInterval::default(); 1],
1✔
1253
            Default::default(),
1✔
1254
            CacheHint::default(),
1✔
1255
        )
1256
        .unwrap();
1✔
1257

1258
        let polygons = MockFeatureCollectionSource::single(polygons).boxed();
1✔
1259

1260
        let points = polygons
1✔
1261
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1262
            .await
1✔
1263
            .unwrap()
1✔
1264
            .query_processor()
1✔
1265
            .unwrap()
1✔
1266
            .multi_polygon()
1✔
1267
            .unwrap();
1✔
1268

1269
        let processor = RasterVectorJoinProcessor::new(
1✔
1270
            points,
1✔
1271
            VectorResultDescriptor {
1✔
1272
                data_type: VectorDataType::MultiPoint,
1✔
1273
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
1274
                columns: [(
1✔
1275
                    "ndvi".to_string(),
1✔
1276
                    VectorColumnInfo {
1✔
1277
                        data_type: FeatureDataType::Int,
1✔
1278
                        measurement: Measurement::Unitless,
1✔
1279
                    },
1✔
1280
                )]
1✔
1281
                .into_iter()
1✔
1282
                .collect(),
1✔
1283
                time: None,
1✔
1284
                bbox: None,
1✔
1285
            },
1✔
1286
            vec![RasterInput {
1✔
1287
                processor: raster,
1✔
1288
                column_names: vec!["ndvi".to_owned()],
1✔
1289
            }],
1✔
1290
            FeatureAggregationMethod::Mean,
1✔
1291
            false,
1292
        );
1293

1294
        let mut result = processor
1✔
1295
            .query(
1✔
1296
                VectorQueryRectangle {
1✔
1297
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1298
                        .unwrap(),
1✔
1299
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1300
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1301
                    attributes: ColumnSelection::all(),
1✔
1302
                },
1✔
1303
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1304
            )
1✔
1305
            .await
1✔
1306
            .unwrap()
1✔
1307
            .map(Result::unwrap)
1✔
1308
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1309
            .await;
1✔
1310

1311
        assert_eq!(result.len(), 1);
1✔
1312

1313
        let result = result.remove(0);
1✔
1314

1315
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1316
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1317

1318
        assert!(
1✔
1319
            result.chunks_equal_ignoring_cache_hint(
1✔
1320
                &MultiPolygonCollection::from_slices(
1✔
1321
                    &[
1✔
1322
                        MultiPolygon::new(vec![vec![vec![
1✔
1323
                            (0.5, -0.5).into(),
1✔
1324
                            (4., -1.).into(),
1✔
1325
                            (0.5, -2.5).into(),
1✔
1326
                            (0.5, -0.5).into(),
1✔
1327
                        ]]])
1✔
1328
                        .unwrap(),
1✔
1329
                        MultiPolygon::new(vec![vec![vec![
1✔
1330
                            (0.5, -0.5).into(),
1✔
1331
                            (4., -1.).into(),
1✔
1332
                            (0.5, -2.5).into(),
1✔
1333
                            (0.5, -0.5).into(),
1✔
1334
                        ]]])
1✔
1335
                        .unwrap()
1✔
1336
                    ],
1✔
1337
                    &[t1, t2],
1✔
1338
                    &[(
1✔
1339
                        "ndvi",
1✔
1340
                        FeatureData::Float(vec![
1✔
1341
                            (3. + 1. + 40. + 30. + 400.) / 5.,
1✔
1342
                            (4. + 6. + 30. + 40. + 300.) / 5.
1✔
1343
                        ])
1✔
1344
                    )],
1✔
1345
                )
1✔
1346
                .unwrap()
1✔
1347
            )
1✔
1348
        );
1✔
1349
    }
1✔
1350

1351
    #[tokio::test]
1352
    #[allow(clippy::float_cmp)]
1353
    #[allow(clippy::too_many_lines)]
1354
    async fn polygons_multi_band() {
1✔
1355
        let raster_tile_a_0_band_0 = RasterTile2D::new_with_tile_info(
1✔
1356
            TimeInterval::new(0, 10).unwrap(),
1✔
1357
            TileInformation {
1✔
1358
                global_geo_transform: TestDefault::test_default(),
1✔
1359
                global_tile_position: [0, 0].into(),
1✔
1360
                tile_size_in_pixels: [3, 2].into(),
1✔
1361
            },
1✔
1362
            0,
1363
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
1364
                .unwrap()
1✔
1365
                .into(),
1✔
1366
            CacheHint::default(),
1✔
1367
        );
1368
        let raster_tile_a_0_band_1 = RasterTile2D::new_with_tile_info(
1✔
1369
            TimeInterval::new(0, 10).unwrap(),
1✔
1370
            TileInformation {
1✔
1371
                global_geo_transform: TestDefault::test_default(),
1✔
1372
                global_tile_position: [0, 0].into(),
1✔
1373
                tile_size_in_pixels: [3, 2].into(),
1✔
1374
            },
1✔
1375
            1,
1376
            Grid2D::new([3, 2].into(), vec![255, 254, 253, 251, 250, 249])
1✔
1377
                .unwrap()
1✔
1378
                .into(),
1✔
1379
            CacheHint::default(),
1✔
1380
        );
1381

1382
        let raster_tile_a_1_band_0 = RasterTile2D::new_with_tile_info(
1✔
1383
            TimeInterval::new(0, 10).unwrap(),
1✔
1384
            TileInformation {
1✔
1385
                global_geo_transform: TestDefault::test_default(),
1✔
1386
                global_tile_position: [0, 1].into(),
1✔
1387
                tile_size_in_pixels: [3, 2].into(),
1✔
1388
            },
1✔
1389
            0,
1390
            Grid2D::new([3, 2].into(), vec![60, 50, 40, 30, 20, 10])
1✔
1391
                .unwrap()
1✔
1392
                .into(),
1✔
1393
            CacheHint::default(),
1✔
1394
        );
1395
        let raster_tile_a_1_band_1 = RasterTile2D::new_with_tile_info(
1✔
1396
            TimeInterval::new(0, 10).unwrap(),
1✔
1397
            TileInformation {
1✔
1398
                global_geo_transform: TestDefault::test_default(),
1✔
1399
                global_tile_position: [0, 1].into(),
1✔
1400
                tile_size_in_pixels: [3, 2].into(),
1✔
1401
            },
1✔
1402
            1,
1403
            Grid2D::new([3, 2].into(), vec![160, 150, 140, 130, 120, 110])
1✔
1404
                .unwrap()
1✔
1405
                .into(),
1✔
1406
            CacheHint::default(),
1✔
1407
        );
1408

1409
        let raster_tile_a_2_band_0 = RasterTile2D::new_with_tile_info(
1✔
1410
            TimeInterval::new(0, 10).unwrap(),
1✔
1411
            TileInformation {
1✔
1412
                global_geo_transform: TestDefault::test_default(),
1✔
1413
                global_tile_position: [0, 2].into(),
1✔
1414
                tile_size_in_pixels: [3, 2].into(),
1✔
1415
            },
1✔
1416
            0,
1417
            Grid2D::new([3, 2].into(), vec![600, 500, 400, 300, 200, 100])
1✔
1418
                .unwrap()
1✔
1419
                .into(),
1✔
1420
            CacheHint::default(),
1✔
1421
        );
1422
        let raster_tile_a_2_band_1 = RasterTile2D::new_with_tile_info(
1✔
1423
            TimeInterval::new(0, 10).unwrap(),
1✔
1424
            TileInformation {
1✔
1425
                global_geo_transform: TestDefault::test_default(),
1✔
1426
                global_tile_position: [0, 2].into(),
1✔
1427
                tile_size_in_pixels: [3, 2].into(),
1✔
1428
            },
1✔
1429
            1,
1430
            Grid2D::new([3, 2].into(), vec![610, 510, 410, 310, 210, 110])
1✔
1431
                .unwrap()
1✔
1432
                .into(),
1✔
1433
            CacheHint::default(),
1✔
1434
        );
1435

1436
        let raster_tile_b_0_band_0 = RasterTile2D::new_with_tile_info(
1✔
1437
            TimeInterval::new(10, 20).unwrap(),
1✔
1438
            TileInformation {
1✔
1439
                global_geo_transform: TestDefault::test_default(),
1✔
1440
                global_tile_position: [0, 0].into(),
1✔
1441
                tile_size_in_pixels: [3, 2].into(),
1✔
1442
            },
1✔
1443
            0,
1444
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1445
                .unwrap()
1✔
1446
                .into(),
1✔
1447
            CacheHint::default(),
1✔
1448
        );
1449
        let raster_tile_b_0_band_1 = RasterTile2D::new_with_tile_info(
1✔
1450
            TimeInterval::new(10, 20).unwrap(),
1✔
1451
            TileInformation {
1✔
1452
                global_geo_transform: TestDefault::test_default(),
1✔
1453
                global_tile_position: [0, 0].into(),
1✔
1454
                tile_size_in_pixels: [3, 2].into(),
1✔
1455
            },
1✔
1456
            1,
1457
            Grid2D::new([3, 2].into(), vec![11, 22, 33, 44, 55, 66])
1✔
1458
                .unwrap()
1✔
1459
                .into(),
1✔
1460
            CacheHint::default(),
1✔
1461
        );
1462
        let raster_tile_b_1_band_0 = RasterTile2D::new_with_tile_info(
1✔
1463
            TimeInterval::new(10, 20).unwrap(),
1✔
1464
            TileInformation {
1✔
1465
                global_geo_transform: TestDefault::test_default(),
1✔
1466
                global_tile_position: [0, 1].into(),
1✔
1467
                tile_size_in_pixels: [3, 2].into(),
1✔
1468
            },
1✔
1469
            0,
1470
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
1471
                .unwrap()
1✔
1472
                .into(),
1✔
1473
            CacheHint::default(),
1✔
1474
        );
1475
        let raster_tile_b_1_band_1 = RasterTile2D::new_with_tile_info(
1✔
1476
            TimeInterval::new(10, 20).unwrap(),
1✔
1477
            TileInformation {
1✔
1478
                global_geo_transform: TestDefault::test_default(),
1✔
1479
                global_tile_position: [0, 1].into(),
1✔
1480
                tile_size_in_pixels: [3, 2].into(),
1✔
1481
            },
1✔
1482
            1,
1483
            Grid2D::new([3, 2].into(), vec![100, 220, 300, 400, 500, 600])
1✔
1484
                .unwrap()
1✔
1485
                .into(),
1✔
1486
            CacheHint::default(),
1✔
1487
        );
1488

1489
        let raster_tile_b_2_band_0 = RasterTile2D::new_with_tile_info(
1✔
1490
            TimeInterval::new(10, 20).unwrap(),
1✔
1491
            TileInformation {
1✔
1492
                global_geo_transform: TestDefault::test_default(),
1✔
1493
                global_tile_position: [0, 2].into(),
1✔
1494
                tile_size_in_pixels: [3, 2].into(),
1✔
1495
            },
1✔
1496
            0,
1497
            Grid2D::new([3, 2].into(), vec![100, 200, 300, 400, 500, 600])
1✔
1498
                .unwrap()
1✔
1499
                .into(),
1✔
1500
            CacheHint::default(),
1✔
1501
        );
1502
        let raster_tile_b_2_band_1 = RasterTile2D::new_with_tile_info(
1✔
1503
            TimeInterval::new(10, 20).unwrap(),
1✔
1504
            TileInformation {
1✔
1505
                global_geo_transform: TestDefault::test_default(),
1✔
1506
                global_tile_position: [0, 2].into(),
1✔
1507
                tile_size_in_pixels: [3, 2].into(),
1✔
1508
            },
1✔
1509
            1,
1510
            Grid2D::new([3, 2].into(), vec![101, 201, 301, 401, 501, 601])
1✔
1511
                .unwrap()
1✔
1512
                .into(),
1✔
1513
            CacheHint::default(),
1✔
1514
        );
1515

1516
        let raster_source = MockRasterSource {
1✔
1517
            params: MockRasterSourceParams {
1✔
1518
                data: vec![
1✔
1519
                    raster_tile_a_0_band_0,
1✔
1520
                    raster_tile_a_0_band_1,
1✔
1521
                    raster_tile_a_1_band_0,
1✔
1522
                    raster_tile_a_1_band_1,
1✔
1523
                    raster_tile_a_2_band_0,
1✔
1524
                    raster_tile_a_2_band_1,
1✔
1525
                    raster_tile_b_0_band_0,
1✔
1526
                    raster_tile_b_0_band_1,
1✔
1527
                    raster_tile_b_1_band_0,
1✔
1528
                    raster_tile_b_1_band_1,
1✔
1529
                    raster_tile_b_2_band_0,
1✔
1530
                    raster_tile_b_2_band_1,
1✔
1531
                ],
1✔
1532
                result_descriptor: RasterResultDescriptor {
1✔
1533
                    data_type: RasterDataType::U16,
1✔
1534
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1535
                    time: None,
1✔
1536
                    bbox: None,
1✔
1537
                    resolution: None,
1✔
1538
                    bands: RasterBandDescriptors::new(vec![
1✔
1539
                        RasterBandDescriptor::new_unitless("band_0".into()),
1✔
1540
                        RasterBandDescriptor::new_unitless("band_1".into()),
1✔
1541
                    ])
1✔
1542
                    .unwrap(),
1✔
1543
                },
1✔
1544
            },
1✔
1545
        }
1✔
1546
        .boxed();
1✔
1547

1548
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1549
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1550
        );
1551

1552
        let raster = raster_source
1✔
1553
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1554
            .await
1✔
1555
            .unwrap()
1✔
1556
            .query_processor()
1✔
1557
            .unwrap();
1✔
1558

1559
        let polygons = MultiPolygonCollection::from_data(
1✔
1560
            vec![
1✔
1561
                MultiPolygon::new(vec![vec![vec![
1✔
1562
                    (0.5, -0.5).into(),
1✔
1563
                    (4., -1.).into(),
1✔
1564
                    (0.5, -2.5).into(),
1✔
1565
                    (0.5, -0.5).into(),
1✔
1566
                ]]])
1567
                .unwrap(),
1✔
1568
            ],
1569
            vec![TimeInterval::default(); 1],
1✔
1570
            Default::default(),
1✔
1571
            CacheHint::default(),
1✔
1572
        )
1573
        .unwrap();
1✔
1574

1575
        let polygons = MockFeatureCollectionSource::single(polygons).boxed();
1✔
1576

1577
        let points = polygons
1✔
1578
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1579
            .await
1✔
1580
            .unwrap()
1✔
1581
            .query_processor()
1✔
1582
            .unwrap()
1✔
1583
            .multi_polygon()
1✔
1584
            .unwrap();
1✔
1585

1586
        let processor = RasterVectorJoinProcessor::new(
1✔
1587
            points,
1✔
1588
            VectorResultDescriptor {
1✔
1589
                data_type: VectorDataType::MultiPoint,
1✔
1590
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
1591
                columns: [(
1✔
1592
                    "ndvi".to_string(),
1✔
1593
                    VectorColumnInfo {
1✔
1594
                        data_type: FeatureDataType::Int,
1✔
1595
                        measurement: Measurement::Unitless,
1✔
1596
                    },
1✔
1597
                )]
1✔
1598
                .into_iter()
1✔
1599
                .collect(),
1✔
1600
                time: None,
1✔
1601
                bbox: None,
1✔
1602
            },
1✔
1603
            vec![RasterInput {
1✔
1604
                processor: raster,
1✔
1605
                column_names: vec!["foo".to_owned(), "foo_1".to_owned()],
1✔
1606
            }],
1✔
1607
            FeatureAggregationMethod::Mean,
1✔
1608
            false,
1609
        );
1610

1611
        let mut result = processor
1✔
1612
            .query(
1✔
1613
                VectorQueryRectangle {
1✔
1614
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1615
                        .unwrap(),
1✔
1616
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1617
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1618
                    attributes: ColumnSelection::all(),
1✔
1619
                },
1✔
1620
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1621
            )
1✔
1622
            .await
1✔
1623
            .unwrap()
1✔
1624
            .map(Result::unwrap)
1✔
1625
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1626
            .await;
1✔
1627

1628
        assert_eq!(result.len(), 1);
1✔
1629

1630
        let result = result.remove(0);
1✔
1631

1632
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1633
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1634

1635
        assert!(
1✔
1636
            result.chunks_equal_ignoring_cache_hint(
1✔
1637
                &MultiPolygonCollection::from_slices(
1✔
1638
                    &[
1✔
1639
                        MultiPolygon::new(vec![vec![vec![
1✔
1640
                            (0.5, -0.5).into(),
1✔
1641
                            (4., -1.).into(),
1✔
1642
                            (0.5, -2.5).into(),
1✔
1643
                            (0.5, -0.5).into(),
1✔
1644
                        ]]])
1✔
1645
                        .unwrap(),
1✔
1646
                        MultiPolygon::new(vec![vec![vec![
1✔
1647
                            (0.5, -0.5).into(),
1✔
1648
                            (4., -1.).into(),
1✔
1649
                            (0.5, -2.5).into(),
1✔
1650
                            (0.5, -0.5).into(),
1✔
1651
                        ]]])
1✔
1652
                        .unwrap()
1✔
1653
                    ],
1✔
1654
                    &[t1, t2],
1✔
1655
                    &[
1✔
1656
                        (
1✔
1657
                            "foo",
1✔
1658
                            FeatureData::Float(vec![
1✔
1659
                                (3. + 1. + 40. + 30. + 400.) / 5.,
1✔
1660
                                (4. + 6. + 30. + 40. + 300.) / 5.
1✔
1661
                            ])
1✔
1662
                        ),
1✔
1663
                        (
1✔
1664
                            "foo_1",
1✔
1665
                            FeatureData::Float(vec![
1✔
1666
                                (251. + 249. + 140. + 130. + 410.) / 5.,
1✔
1667
                                (44. + 66. + 300. + 400. + 301.) / 5.
1✔
1668
                            ])
1✔
1669
                        )
1✔
1670
                    ],
1✔
1671
                )
1✔
1672
                .unwrap()
1✔
1673
            )
1✔
1674
        );
1✔
1675
    }
1✔
1676
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc