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

geo-engine / geoengine / 11911118784

19 Nov 2024 10:06AM UTC coverage: 90.448% (-0.2%) from 90.687%
11911118784

push

github

web-flow
Merge pull request #994 from geo-engine/workspace-dependencies

use workspace dependencies, update toolchain, use global lock in expression

9 of 11 new or added lines in 6 files covered. (81.82%)

369 existing lines in 74 files now uncovered.

132871 of 146904 relevant lines covered (90.45%)

54798.62 hits per line

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

97.42
/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, 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
            )
7✔
83
        });
7✔
84

7✔
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✔
102
            log::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

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

7✔
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✔
124
            log::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
        );
7✔
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
            )
4✔
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

7✔
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
            )
7✔
207
            .map(move |accum| accum?.into_collection(column_names));
12✔
208

7✔
209
        return 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?
12✔
242

12✔
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
12✔
258

12✔
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

12✔
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
                    )
14✔
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,
12✔
291
            g: Default::default(),
12✔
292
        });
12✔
293

12✔
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

12✔
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

240✔
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 {
228✔
320
            // first or new tile, we need to calculcate the covered pixels
228✔
321
            state.current_tile = raster.tile_position;
228✔
322
            state.current_band_idx = raster.band;
228✔
323

228✔
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
            );
228✔
329
        };
228✔
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

240✔
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
            )
12✔
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

12✔
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>>> {
7✔
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
            log::debug!(
7✔
UNCOV
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
            );
7✔
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

1✔
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
                ])
1✔
463
                .unwrap(),
1✔
464
                vec![time_instant; 5],
1✔
465
                Default::default(),
1✔
466
                CacheHint::default(),
1✔
467
            )
1✔
468
            .unwrap(),
1✔
469
        )
1✔
470
        .boxed();
1✔
471

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

1✔
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

1✔
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

1✔
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

1✔
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,
1✔
520
        );
1✔
521

1✔
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;
32✔
538

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

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

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

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

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

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

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

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

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

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

1✔
651
        assert_eq!(result.len(), 1);
1✔
652

1✔
653
        let result = result.remove(0);
1✔
654

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

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

1✔
699
        let mut execution_context = MockExecutionContext::test_default();
1✔
700

1✔
701
        let raster_source = GdalSource {
1✔
702
            params: GdalSourceParameters {
1✔
703
                data: add_ndvi_dataset(&mut execution_context),
1✔
704
            },
1✔
705
        }
1✔
706
        .boxed();
1✔
707

1✔
708
        let points = points
1✔
709
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
710
            .await
1✔
711
            .unwrap()
1✔
712
            .query_processor()
1✔
713
            .unwrap()
1✔
714
            .multi_point()
1✔
715
            .unwrap();
1✔
716

1✔
717
        let rasters = raster_source
1✔
718
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
719
            .await
1✔
720
            .unwrap()
1✔
721
            .query_processor()
1✔
722
            .unwrap();
1✔
723

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

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

1✔
769
        assert_eq!(result.len(), 1);
1✔
770

1✔
771
        let result = result.remove(0);
1✔
772

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

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

1✔
821
        let mut execution_context = MockExecutionContext::test_default();
1✔
822

1✔
823
        let raster_source = GdalSource {
1✔
824
            params: GdalSourceParameters {
1✔
825
                data: add_ndvi_dataset(&mut execution_context),
1✔
826
            },
1✔
827
        }
1✔
828
        .boxed();
1✔
829

1✔
830
        let points = points
1✔
831
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
832
            .await
1✔
833
            .unwrap()
1✔
834
            .query_processor()
1✔
835
            .unwrap()
1✔
836
            .multi_point()
1✔
837
            .unwrap();
1✔
838

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

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

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

1✔
892
        assert_eq!(result.len(), 1);
1✔
893

1✔
894
        let result = result.remove(0);
1✔
895

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

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

1✔
987
        let raster_source = MockRasterSource {
1✔
988
            params: MockRasterSourceParams {
1✔
989
                data: vec![
1✔
990
                    raster_tile_a_0,
1✔
991
                    raster_tile_a_1,
1✔
992
                    raster_tile_b_0,
1✔
993
                    raster_tile_b_1,
1✔
994
                ],
1✔
995
                result_descriptor: RasterResultDescriptor {
1✔
996
                    data_type: RasterDataType::U8,
1✔
997
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
998
                    time: None,
1✔
999
                    bbox: None,
1✔
1000
                    resolution: None,
1✔
1001
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1002
                },
1✔
1003
            },
1✔
1004
        }
1✔
1005
        .boxed();
1✔
1006

1✔
1007
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1008
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1009
        );
1✔
1010

1✔
1011
        let raster = raster_source
1✔
1012
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1013
            .await
1✔
1014
            .unwrap()
1✔
1015
            .query_processor()
1✔
1016
            .unwrap();
1✔
1017

1✔
1018
        let points = MultiPointCollection::from_data(
1✔
1019
            MultiPoint::many(vec![
1✔
1020
                vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1021
                vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1022
            ])
1✔
1023
            .unwrap(),
1✔
1024
            vec![TimeInterval::default(); 2],
1✔
1025
            Default::default(),
1✔
1026
            CacheHint::default(),
1✔
1027
        )
1✔
1028
        .unwrap();
1✔
1029

1✔
1030
        let points = MockFeatureCollectionSource::single(points).boxed();
1✔
1031

1✔
1032
        let points = points
1✔
1033
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1034
            .await
1✔
1035
            .unwrap()
1✔
1036
            .query_processor()
1✔
1037
            .unwrap()
1✔
1038
            .multi_point()
1✔
1039
            .unwrap();
1✔
1040

1✔
1041
        let processor = RasterVectorJoinProcessor::new(
1✔
1042
            points,
1✔
1043
            VectorResultDescriptor {
1✔
1044
                data_type: VectorDataType::MultiPoint,
1✔
1045
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
1046
                columns: [(
1✔
1047
                    "ndvi".to_string(),
1✔
1048
                    VectorColumnInfo {
1✔
1049
                        data_type: FeatureDataType::Int,
1✔
1050
                        measurement: Measurement::Unitless,
1✔
1051
                    },
1✔
1052
                )]
1✔
1053
                .into_iter()
1✔
1054
                .collect(),
1✔
1055
                time: None,
1✔
1056
                bbox: None,
1✔
1057
            },
1✔
1058
            vec![RasterInput {
1✔
1059
                processor: raster,
1✔
1060
                column_names: vec!["ndvi".to_owned()],
1✔
1061
            }],
1✔
1062
            FeatureAggregationMethod::Mean,
1✔
1063
            false,
1✔
1064
        );
1✔
1065

1✔
1066
        let mut result = processor
1✔
1067
            .query(
1✔
1068
                VectorQueryRectangle {
1✔
1069
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1070
                        .unwrap(),
1✔
1071
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1072
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1073
                    attributes: ColumnSelection::all(),
1✔
1074
                },
1✔
1075
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1076
            )
1✔
1077
            .await
1✔
1078
            .unwrap()
1✔
1079
            .map(Result::unwrap)
1✔
1080
            .collect::<Vec<MultiPointCollection>>()
1✔
1081
            .await;
1✔
1082

1✔
1083
        assert_eq!(result.len(), 1);
1✔
1084

1✔
1085
        let result = result.remove(0);
1✔
1086

1✔
1087
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1088
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1089

1✔
1090
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
1091
            &MultiPointCollection::from_slices(
1✔
1092
                &MultiPoint::many(vec![
1✔
1093
                    vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1094
                    vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1095
                    vec![(0.0, 0.0), (2.0, 0.0)],
1✔
1096
                    vec![(1.0, 0.0), (3.0, 0.0)],
1✔
1097
                ])
1✔
1098
                .unwrap(),
1✔
1099
                &[t1, t1, t2, t2],
1✔
1100
                &[(
1✔
1101
                    "ndvi",
1✔
1102
                    FeatureData::Float(vec![
1✔
1103
                        (6. + 60.) / 2.,
1✔
1104
                        (5. + 50.) / 2.,
1✔
1105
                        (1. + 10.) / 2.,
1✔
1106
                        (2. + 20.) / 2.
1✔
1107
                    ])
1✔
1108
                )],
1✔
1109
            )
1✔
1110
            .unwrap()
1✔
1111
        ));
1✔
1112
    }
1✔
1113

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

1✔
1184
        let raster_tile_b_2 = RasterTile2D::new_with_tile_info(
1✔
1185
            TimeInterval::new(10, 20).unwrap(),
1✔
1186
            TileInformation {
1✔
1187
                global_geo_transform: TestDefault::test_default(),
1✔
1188
                global_tile_position: [0, 2].into(),
1✔
1189
                tile_size_in_pixels: [3, 2].into(),
1✔
1190
            },
1✔
1191
            0,
1✔
1192
            Grid2D::new([3, 2].into(), vec![100, 200, 300, 400, 500, 600])
1✔
1193
                .unwrap()
1✔
1194
                .into(),
1✔
1195
            CacheHint::default(),
1✔
1196
        );
1✔
1197

1✔
1198
        let raster_source = MockRasterSource {
1✔
1199
            params: MockRasterSourceParams {
1✔
1200
                data: vec![
1✔
1201
                    raster_tile_a_0,
1✔
1202
                    raster_tile_a_1,
1✔
1203
                    raster_tile_a_2,
1✔
1204
                    raster_tile_b_0,
1✔
1205
                    raster_tile_b_1,
1✔
1206
                    raster_tile_b_2,
1✔
1207
                ],
1✔
1208
                result_descriptor: RasterResultDescriptor {
1✔
1209
                    data_type: RasterDataType::U16,
1✔
1210
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1211
                    time: None,
1✔
1212
                    bbox: None,
1✔
1213
                    resolution: None,
1✔
1214
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1215
                },
1✔
1216
            },
1✔
1217
        }
1✔
1218
        .boxed();
1✔
1219

1✔
1220
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1221
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1222
        );
1✔
1223

1✔
1224
        let raster = raster_source
1✔
1225
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1226
            .await
1✔
1227
            .unwrap()
1✔
1228
            .query_processor()
1✔
1229
            .unwrap();
1✔
1230

1✔
1231
        let polygons = MultiPolygonCollection::from_data(
1✔
1232
            vec![MultiPolygon::new(vec![vec![vec![
1✔
1233
                (0.5, -0.5).into(),
1✔
1234
                (4., -1.).into(),
1✔
1235
                (0.5, -2.5).into(),
1✔
1236
                (0.5, -0.5).into(),
1✔
1237
            ]]])
1✔
1238
            .unwrap()],
1✔
1239
            vec![TimeInterval::default(); 1],
1✔
1240
            Default::default(),
1✔
1241
            CacheHint::default(),
1✔
1242
        )
1✔
1243
        .unwrap();
1✔
1244

1✔
1245
        let polygons = MockFeatureCollectionSource::single(polygons).boxed();
1✔
1246

1✔
1247
        let points = polygons
1✔
1248
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1249
            .await
1✔
1250
            .unwrap()
1✔
1251
            .query_processor()
1✔
1252
            .unwrap()
1✔
1253
            .multi_polygon()
1✔
1254
            .unwrap();
1✔
1255

1✔
1256
        let processor = RasterVectorJoinProcessor::new(
1✔
1257
            points,
1✔
1258
            VectorResultDescriptor {
1✔
1259
                data_type: VectorDataType::MultiPoint,
1✔
1260
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
1261
                columns: [(
1✔
1262
                    "ndvi".to_string(),
1✔
1263
                    VectorColumnInfo {
1✔
1264
                        data_type: FeatureDataType::Int,
1✔
1265
                        measurement: Measurement::Unitless,
1✔
1266
                    },
1✔
1267
                )]
1✔
1268
                .into_iter()
1✔
1269
                .collect(),
1✔
1270
                time: None,
1✔
1271
                bbox: None,
1✔
1272
            },
1✔
1273
            vec![RasterInput {
1✔
1274
                processor: raster,
1✔
1275
                column_names: vec!["ndvi".to_owned()],
1✔
1276
            }],
1✔
1277
            FeatureAggregationMethod::Mean,
1✔
1278
            false,
1✔
1279
        );
1✔
1280

1✔
1281
        let mut result = processor
1✔
1282
            .query(
1✔
1283
                VectorQueryRectangle {
1✔
1284
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1285
                        .unwrap(),
1✔
1286
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1287
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1288
                    attributes: ColumnSelection::all(),
1✔
1289
                },
1✔
1290
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1291
            )
1✔
1292
            .await
1✔
1293
            .unwrap()
1✔
1294
            .map(Result::unwrap)
1✔
1295
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1296
            .await;
1✔
1297

1✔
1298
        assert_eq!(result.len(), 1);
1✔
1299

1✔
1300
        let result = result.remove(0);
1✔
1301

1✔
1302
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1303
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1304

1✔
1305
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
1306
            &MultiPolygonCollection::from_slices(
1✔
1307
                &[
1✔
1308
                    MultiPolygon::new(vec![vec![vec![
1✔
1309
                        (0.5, -0.5).into(),
1✔
1310
                        (4., -1.).into(),
1✔
1311
                        (0.5, -2.5).into(),
1✔
1312
                        (0.5, -0.5).into(),
1✔
1313
                    ]]])
1✔
1314
                    .unwrap(),
1✔
1315
                    MultiPolygon::new(vec![vec![vec![
1✔
1316
                        (0.5, -0.5).into(),
1✔
1317
                        (4., -1.).into(),
1✔
1318
                        (0.5, -2.5).into(),
1✔
1319
                        (0.5, -0.5).into(),
1✔
1320
                    ]]])
1✔
1321
                    .unwrap()
1✔
1322
                ],
1✔
1323
                &[t1, t2],
1✔
1324
                &[(
1✔
1325
                    "ndvi",
1✔
1326
                    FeatureData::Float(vec![
1✔
1327
                        (3. + 1. + 40. + 30. + 400.) / 5.,
1✔
1328
                        (4. + 6. + 30. + 40. + 300.) / 5.
1✔
1329
                    ])
1✔
1330
                )],
1✔
1331
            )
1✔
1332
            .unwrap()
1✔
1333
        ));
1✔
1334
    }
1✔
1335

1336
    #[tokio::test]
1337
    #[allow(clippy::float_cmp)]
1338
    #[allow(clippy::too_many_lines)]
1339
    async fn polygons_multi_band() {
1✔
1340
        let raster_tile_a_0_band_0 = RasterTile2D::new_with_tile_info(
1✔
1341
            TimeInterval::new(0, 10).unwrap(),
1✔
1342
            TileInformation {
1✔
1343
                global_geo_transform: TestDefault::test_default(),
1✔
1344
                global_tile_position: [0, 0].into(),
1✔
1345
                tile_size_in_pixels: [3, 2].into(),
1✔
1346
            },
1✔
1347
            0,
1✔
1348
            Grid2D::new([3, 2].into(), vec![6, 5, 4, 3, 2, 1])
1✔
1349
                .unwrap()
1✔
1350
                .into(),
1✔
1351
            CacheHint::default(),
1✔
1352
        );
1✔
1353
        let raster_tile_a_0_band_1 = RasterTile2D::new_with_tile_info(
1✔
1354
            TimeInterval::new(0, 10).unwrap(),
1✔
1355
            TileInformation {
1✔
1356
                global_geo_transform: TestDefault::test_default(),
1✔
1357
                global_tile_position: [0, 0].into(),
1✔
1358
                tile_size_in_pixels: [3, 2].into(),
1✔
1359
            },
1✔
1360
            1,
1✔
1361
            Grid2D::new([3, 2].into(), vec![255, 254, 253, 251, 250, 249])
1✔
1362
                .unwrap()
1✔
1363
                .into(),
1✔
1364
            CacheHint::default(),
1✔
1365
        );
1✔
1366

1✔
1367
        let raster_tile_a_1_band_0 = RasterTile2D::new_with_tile_info(
1✔
1368
            TimeInterval::new(0, 10).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![60, 50, 40, 30, 20, 10])
1✔
1376
                .unwrap()
1✔
1377
                .into(),
1✔
1378
            CacheHint::default(),
1✔
1379
        );
1✔
1380
        let raster_tile_a_1_band_1 = RasterTile2D::new_with_tile_info(
1✔
1381
            TimeInterval::new(0, 10).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![160, 150, 140, 130, 120, 110])
1✔
1389
                .unwrap()
1✔
1390
                .into(),
1✔
1391
            CacheHint::default(),
1✔
1392
        );
1✔
1393

1✔
1394
        let raster_tile_a_2_band_0 = RasterTile2D::new_with_tile_info(
1✔
1395
            TimeInterval::new(0, 10).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![600, 500, 400, 300, 200, 100])
1✔
1403
                .unwrap()
1✔
1404
                .into(),
1✔
1405
            CacheHint::default(),
1✔
1406
        );
1✔
1407
        let raster_tile_a_2_band_1 = RasterTile2D::new_with_tile_info(
1✔
1408
            TimeInterval::new(0, 10).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![610, 510, 410, 310, 210, 110])
1✔
1416
                .unwrap()
1✔
1417
                .into(),
1✔
1418
            CacheHint::default(),
1✔
1419
        );
1✔
1420

1✔
1421
        let raster_tile_b_0_band_0 = RasterTile2D::new_with_tile_info(
1✔
1422
            TimeInterval::new(10, 20).unwrap(),
1✔
1423
            TileInformation {
1✔
1424
                global_geo_transform: TestDefault::test_default(),
1✔
1425
                global_tile_position: [0, 0].into(),
1✔
1426
                tile_size_in_pixels: [3, 2].into(),
1✔
1427
            },
1✔
1428
            0,
1✔
1429
            Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1430
                .unwrap()
1✔
1431
                .into(),
1✔
1432
            CacheHint::default(),
1✔
1433
        );
1✔
1434
        let raster_tile_b_0_band_1 = RasterTile2D::new_with_tile_info(
1✔
1435
            TimeInterval::new(10, 20).unwrap(),
1✔
1436
            TileInformation {
1✔
1437
                global_geo_transform: TestDefault::test_default(),
1✔
1438
                global_tile_position: [0, 0].into(),
1✔
1439
                tile_size_in_pixels: [3, 2].into(),
1✔
1440
            },
1✔
1441
            1,
1✔
1442
            Grid2D::new([3, 2].into(), vec![11, 22, 33, 44, 55, 66])
1✔
1443
                .unwrap()
1✔
1444
                .into(),
1✔
1445
            CacheHint::default(),
1✔
1446
        );
1✔
1447
        let raster_tile_b_1_band_0 = RasterTile2D::new_with_tile_info(
1✔
1448
            TimeInterval::new(10, 20).unwrap(),
1✔
1449
            TileInformation {
1✔
1450
                global_geo_transform: TestDefault::test_default(),
1✔
1451
                global_tile_position: [0, 1].into(),
1✔
1452
                tile_size_in_pixels: [3, 2].into(),
1✔
1453
            },
1✔
1454
            0,
1✔
1455
            Grid2D::new([3, 2].into(), vec![10, 20, 30, 40, 50, 60])
1✔
1456
                .unwrap()
1✔
1457
                .into(),
1✔
1458
            CacheHint::default(),
1✔
1459
        );
1✔
1460
        let raster_tile_b_1_band_1 = RasterTile2D::new_with_tile_info(
1✔
1461
            TimeInterval::new(10, 20).unwrap(),
1✔
1462
            TileInformation {
1✔
1463
                global_geo_transform: TestDefault::test_default(),
1✔
1464
                global_tile_position: [0, 1].into(),
1✔
1465
                tile_size_in_pixels: [3, 2].into(),
1✔
1466
            },
1✔
1467
            1,
1✔
1468
            Grid2D::new([3, 2].into(), vec![100, 220, 300, 400, 500, 600])
1✔
1469
                .unwrap()
1✔
1470
                .into(),
1✔
1471
            CacheHint::default(),
1✔
1472
        );
1✔
1473

1✔
1474
        let raster_tile_b_2_band_0 = RasterTile2D::new_with_tile_info(
1✔
1475
            TimeInterval::new(10, 20).unwrap(),
1✔
1476
            TileInformation {
1✔
1477
                global_geo_transform: TestDefault::test_default(),
1✔
1478
                global_tile_position: [0, 2].into(),
1✔
1479
                tile_size_in_pixels: [3, 2].into(),
1✔
1480
            },
1✔
1481
            0,
1✔
1482
            Grid2D::new([3, 2].into(), vec![100, 200, 300, 400, 500, 600])
1✔
1483
                .unwrap()
1✔
1484
                .into(),
1✔
1485
            CacheHint::default(),
1✔
1486
        );
1✔
1487
        let raster_tile_b_2_band_1 = RasterTile2D::new_with_tile_info(
1✔
1488
            TimeInterval::new(10, 20).unwrap(),
1✔
1489
            TileInformation {
1✔
1490
                global_geo_transform: TestDefault::test_default(),
1✔
1491
                global_tile_position: [0, 2].into(),
1✔
1492
                tile_size_in_pixels: [3, 2].into(),
1✔
1493
            },
1✔
1494
            1,
1✔
1495
            Grid2D::new([3, 2].into(), vec![101, 201, 301, 401, 501, 601])
1✔
1496
                .unwrap()
1✔
1497
                .into(),
1✔
1498
            CacheHint::default(),
1✔
1499
        );
1✔
1500

1✔
1501
        let raster_source = MockRasterSource {
1✔
1502
            params: MockRasterSourceParams {
1✔
1503
                data: vec![
1✔
1504
                    raster_tile_a_0_band_0,
1✔
1505
                    raster_tile_a_0_band_1,
1✔
1506
                    raster_tile_a_1_band_0,
1✔
1507
                    raster_tile_a_1_band_1,
1✔
1508
                    raster_tile_a_2_band_0,
1✔
1509
                    raster_tile_a_2_band_1,
1✔
1510
                    raster_tile_b_0_band_0,
1✔
1511
                    raster_tile_b_0_band_1,
1✔
1512
                    raster_tile_b_1_band_0,
1✔
1513
                    raster_tile_b_1_band_1,
1✔
1514
                    raster_tile_b_2_band_0,
1✔
1515
                    raster_tile_b_2_band_1,
1✔
1516
                ],
1✔
1517
                result_descriptor: RasterResultDescriptor {
1✔
1518
                    data_type: RasterDataType::U16,
1✔
1519
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1520
                    time: None,
1✔
1521
                    bbox: None,
1✔
1522
                    resolution: None,
1✔
1523
                    bands: RasterBandDescriptors::new(vec![
1✔
1524
                        RasterBandDescriptor::new_unitless("band_0".into()),
1✔
1525
                        RasterBandDescriptor::new_unitless("band_1".into()),
1✔
1526
                    ])
1✔
1527
                    .unwrap(),
1✔
1528
                },
1✔
1529
            },
1✔
1530
        }
1✔
1531
        .boxed();
1✔
1532

1✔
1533
        let execution_context = MockExecutionContext::new_with_tiling_spec(
1✔
1534
            TilingSpecification::new((0., 0.).into(), [3, 2].into()),
1✔
1535
        );
1✔
1536

1✔
1537
        let raster = raster_source
1✔
1538
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1539
            .await
1✔
1540
            .unwrap()
1✔
1541
            .query_processor()
1✔
1542
            .unwrap();
1✔
1543

1✔
1544
        let polygons = MultiPolygonCollection::from_data(
1✔
1545
            vec![MultiPolygon::new(vec![vec![vec![
1✔
1546
                (0.5, -0.5).into(),
1✔
1547
                (4., -1.).into(),
1✔
1548
                (0.5, -2.5).into(),
1✔
1549
                (0.5, -0.5).into(),
1✔
1550
            ]]])
1✔
1551
            .unwrap()],
1✔
1552
            vec![TimeInterval::default(); 1],
1✔
1553
            Default::default(),
1✔
1554
            CacheHint::default(),
1✔
1555
        )
1✔
1556
        .unwrap();
1✔
1557

1✔
1558
        let polygons = MockFeatureCollectionSource::single(polygons).boxed();
1✔
1559

1✔
1560
        let points = polygons
1✔
1561
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1562
            .await
1✔
1563
            .unwrap()
1✔
1564
            .query_processor()
1✔
1565
            .unwrap()
1✔
1566
            .multi_polygon()
1✔
1567
            .unwrap();
1✔
1568

1✔
1569
        let processor = RasterVectorJoinProcessor::new(
1✔
1570
            points,
1✔
1571
            VectorResultDescriptor {
1✔
1572
                data_type: VectorDataType::MultiPoint,
1✔
1573
                spatial_reference: SpatialReferenceOption::Unreferenced,
1✔
1574
                columns: [(
1✔
1575
                    "ndvi".to_string(),
1✔
1576
                    VectorColumnInfo {
1✔
1577
                        data_type: FeatureDataType::Int,
1✔
1578
                        measurement: Measurement::Unitless,
1✔
1579
                    },
1✔
1580
                )]
1✔
1581
                .into_iter()
1✔
1582
                .collect(),
1✔
1583
                time: None,
1✔
1584
                bbox: None,
1✔
1585
            },
1✔
1586
            vec![RasterInput {
1✔
1587
                processor: raster,
1✔
1588
                column_names: vec!["foo".to_owned(), "foo_1".to_owned()],
1✔
1589
            }],
1✔
1590
            FeatureAggregationMethod::Mean,
1✔
1591
            false,
1✔
1592
        );
1✔
1593

1✔
1594
        let mut result = processor
1✔
1595
            .query(
1✔
1596
                VectorQueryRectangle {
1✔
1597
                    spatial_bounds: BoundingBox2D::new((0.0, -3.0).into(), (4.0, 0.0).into())
1✔
1598
                        .unwrap(),
1✔
1599
                    time_interval: TimeInterval::new_unchecked(0, 20),
1✔
1600
                    spatial_resolution: SpatialResolution::new(1., 1.).unwrap(),
1✔
1601
                    attributes: ColumnSelection::all(),
1✔
1602
                },
1✔
1603
                &MockQueryContext::new(ChunkByteSize::MAX),
1✔
1604
            )
1✔
1605
            .await
1✔
1606
            .unwrap()
1✔
1607
            .map(Result::unwrap)
1✔
1608
            .collect::<Vec<MultiPolygonCollection>>()
1✔
1609
            .await;
1✔
1610

1✔
1611
        assert_eq!(result.len(), 1);
1✔
1612

1✔
1613
        let result = result.remove(0);
1✔
1614

1✔
1615
        let t1 = TimeInterval::new(0, 10).unwrap();
1✔
1616
        let t2 = TimeInterval::new(10, 20).unwrap();
1✔
1617

1✔
1618
        assert!(result.chunks_equal_ignoring_cache_hint(
1✔
1619
            &MultiPolygonCollection::from_slices(
1✔
1620
                &[
1✔
1621
                    MultiPolygon::new(vec![vec![vec![
1✔
1622
                        (0.5, -0.5).into(),
1✔
1623
                        (4., -1.).into(),
1✔
1624
                        (0.5, -2.5).into(),
1✔
1625
                        (0.5, -0.5).into(),
1✔
1626
                    ]]])
1✔
1627
                    .unwrap(),
1✔
1628
                    MultiPolygon::new(vec![vec![vec![
1✔
1629
                        (0.5, -0.5).into(),
1✔
1630
                        (4., -1.).into(),
1✔
1631
                        (0.5, -2.5).into(),
1✔
1632
                        (0.5, -0.5).into(),
1✔
1633
                    ]]])
1✔
1634
                    .unwrap()
1✔
1635
                ],
1✔
1636
                &[t1, t2],
1✔
1637
                &[
1✔
1638
                    (
1✔
1639
                        "foo",
1✔
1640
                        FeatureData::Float(vec![
1✔
1641
                            (3. + 1. + 40. + 30. + 400.) / 5.,
1✔
1642
                            (4. + 6. + 30. + 40. + 300.) / 5.
1✔
1643
                        ])
1✔
1644
                    ),
1✔
1645
                    (
1✔
1646
                        "foo_1",
1✔
1647
                        FeatureData::Float(vec![
1✔
1648
                            (251. + 249. + 140. + 130. + 410.) / 5.,
1✔
1649
                            (44. + 66. + 300. + 400. + 301.) / 5.
1✔
1650
                        ])
1✔
1651
                    )
1✔
1652
                ],
1✔
1653
            )
1✔
1654
            .unwrap()
1✔
1655
        ));
1✔
1656
    }
1✔
1657
}
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