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

geo-engine / geoengine / 8344180126

19 Mar 2024 01:47PM UTC coverage: 90.504% (+0.03%) from 90.473%
8344180126

push

github

web-flow
Merge pull request #942 from geo-engine/ground-truth-schema

Compare schema after migrations to current schema

739 of 761 new or added lines in 3 files covered. (97.11%)

15 existing lines in 8 files now uncovered.

129494 of 143081 relevant lines covered (90.5%)

54026.37 hits per line

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

90.58
/operators/src/processing/raster_vector_join/mod.rs
1
mod aggregated;
2
mod aggregator;
3
mod non_aggregated;
4
mod util;
5

6
use crate::engine::{
7
    CanonicOperatorName, ExecutionContext, InitializedRasterOperator, InitializedVectorOperator,
8
    Operator, OperatorName, SingleVectorMultipleRasterSources, TypedVectorQueryProcessor,
9
    VectorColumnInfo, VectorOperator, VectorQueryProcessor, VectorResultDescriptor,
10
    WorkflowOperatorPath,
11
};
12
use crate::error::{self, Error};
13
use crate::processing::raster_vector_join::non_aggregated::RasterVectorJoinProcessor;
14
use crate::util::Result;
15

16
use crate::processing::raster_vector_join::aggregated::RasterVectorAggregateJoinProcessor;
17
use async_trait::async_trait;
18
use geoengine_datatypes::collections::VectorDataType;
19
use geoengine_datatypes::primitives::FeatureDataType;
20
use geoengine_datatypes::raster::{Pixel, RasterDataType};
21
use serde::{Deserialize, Serialize};
22
use snafu::ensure;
23

24
use self::aggregator::{
25
    Aggregator, FirstValueFloatAggregator, FirstValueIntAggregator, MeanValueAggregator,
26
    TypedAggregator,
27
};
28

29
/// An operator that attaches raster values to vector data
30
pub type RasterVectorJoin = Operator<RasterVectorJoinParams, SingleVectorMultipleRasterSources>;
31

32
impl OperatorName for RasterVectorJoin {
33
    const TYPE_NAME: &'static str = "RasterVectorJoin";
34
}
35

36
const MAX_NUMBER_OF_RASTER_INPUTS: usize = 8;
37

38
/// The parameter spec for `RasterVectorJoin`
39
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25✔
40
#[serde(rename_all = "camelCase")]
41
pub struct RasterVectorJoinParams {
42
    /// Each name reflects the output column of the join result.
43
    /// For each raster input, one name must be defined.
44
    pub names: Vec<String>,
45

46
    /// Specifies which method is used for aggregating values for a feature
47
    pub feature_aggregation: FeatureAggregationMethod,
48

49
    /// Whether NO DATA values should be ignored in aggregating the joined feature data
50
    /// `false` by default
51
    #[serde(default)]
52
    pub feature_aggregation_ignore_no_data: bool,
53

54
    /// Specifies which method is used for aggregating values over time
55
    pub temporal_aggregation: TemporalAggregationMethod,
56

57
    /// Whether NO DATA values should be ignored in aggregating the joined temporal data
58
    /// `false` by default
59
    #[serde(default)]
60
    pub temporal_aggregation_ignore_no_data: bool,
61
}
62

63
/// How to aggregate the values for the geometries inside a feature e.g.
64
/// the mean of all the raster values corresponding to the individual
65
/// points inside a `MultiPoint` feature.
66
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Copy)]
8✔
67
#[serde(rename_all = "camelCase")]
68
pub enum FeatureAggregationMethod {
69
    First,
70
    Mean,
71
}
72

73
/// How to aggregate the values over time
74
/// If there are multiple rasters valid during the validity of a feature
75
/// the featuer is either split into multiple (None-aggregation) or the
76
/// values are aggreagated
77
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Copy)]
8✔
78
#[serde(rename_all = "camelCase")]
79
pub enum TemporalAggregationMethod {
80
    None,
81
    First,
82
    Mean,
83
}
84

85
#[allow(clippy::too_many_lines)]
86
#[typetag::serde]
2✔
87
#[async_trait]
88
impl VectorOperator for RasterVectorJoin {
89
    async fn _initialize(
6✔
90
        mut self: Box<Self>,
6✔
91
        path: WorkflowOperatorPath,
6✔
92
        context: &dyn ExecutionContext,
6✔
93
    ) -> Result<Box<dyn InitializedVectorOperator>> {
6✔
94
        ensure!(
6✔
95
            (1..=MAX_NUMBER_OF_RASTER_INPUTS).contains(&self.sources.rasters.len()),
6✔
96
            error::InvalidNumberOfRasterInputs {
×
97
                expected: 1..MAX_NUMBER_OF_RASTER_INPUTS,
×
98
                found: self.sources.rasters.len()
×
99
            }
×
100
        );
101
        ensure!(
6✔
102
            self.sources.rasters.len() == self.params.names.len(),
6✔
103
            error::InvalidOperatorSpec {
×
104
                reason: "`rasters` must be of equal length as `names`"
×
105
            }
×
106
        );
107

108
        let name = CanonicOperatorName::from(&self);
6✔
109

110
        let vector_source = self
6✔
111
            .sources
6✔
112
            .vector
6✔
113
            .initialize(path.clone_and_append(0), context)
6✔
UNCOV
114
            .await?;
×
115

116
        let vector_rd = vector_source.result_descriptor();
6✔
117

6✔
118
        ensure!(
6✔
119
            vector_rd.data_type != VectorDataType::Data,
6✔
120
            error::InvalidType {
×
121
                expected: format!(
×
122
                    "{}, {} or {}",
×
123
                    VectorDataType::MultiPoint,
×
124
                    VectorDataType::MultiLineString,
×
125
                    VectorDataType::MultiPolygon
×
126
                ),
×
127
                found: VectorDataType::Data.to_string()
×
128
            },
×
129
        );
130

131
        let raster_sources = futures::future::try_join_all(
6✔
132
            self.sources
6✔
133
                .rasters
6✔
134
                .into_iter()
6✔
135
                .enumerate()
6✔
136
                .map(|(i, op)| op.initialize(path.clone_and_append(i as u8 + 1), context)),
7✔
137
        )
6✔
UNCOV
138
        .await?;
×
139

140
        let source_descriptors = raster_sources
6✔
141
            .iter()
6✔
142
            .map(InitializedRasterOperator::result_descriptor)
6✔
143
            .collect::<Vec<_>>();
6✔
144

6✔
145
        let spatial_reference = vector_rd.spatial_reference;
6✔
146

147
        for other_spatial_reference in source_descriptors
7✔
148
            .iter()
6✔
149
            .map(|source_descriptor| source_descriptor.spatial_reference)
7✔
150
        {
151
            ensure!(
7✔
152
                spatial_reference == other_spatial_reference,
7✔
153
                crate::error::InvalidSpatialReference {
1✔
154
                    expected: spatial_reference,
1✔
155
                    found: other_spatial_reference,
1✔
156
                }
1✔
157
            );
158
        }
159

160
        let raster_sources_bands = source_descriptors
5✔
161
            .iter()
5✔
162
            .map(|rd| rd.bands.count())
6✔
163
            .collect::<Vec<_>>();
5✔
164

5✔
165
        let params = self.params;
5✔
166

5✔
167
        let result_descriptor = vector_rd.map_columns(|columns| {
5✔
168
            let mut columns = columns.clone();
5✔
169
            for ((i, new_column_name), source_descriptor) in
6✔
170
                params.names.iter().enumerate().zip(&source_descriptors)
5✔
171
            {
172
                let feature_data_type = match params.temporal_aggregation {
6✔
173
                    TemporalAggregationMethod::First | TemporalAggregationMethod::None => {
174
                        match raster_sources[i].result_descriptor().data_type {
4✔
175
                            RasterDataType::U8
176
                            | RasterDataType::U16
177
                            | RasterDataType::U32
178
                            | RasterDataType::U64
179
                            | RasterDataType::I8
180
                            | RasterDataType::I16
181
                            | RasterDataType::I32
182
                            | RasterDataType::I64 => FeatureDataType::Int,
4✔
183
                            RasterDataType::F32 | RasterDataType::F64 => FeatureDataType::Float,
×
184
                        }
185
                    }
186
                    TemporalAggregationMethod::Mean => FeatureDataType::Float,
2✔
187
                };
188

189
                for (i, band) in source_descriptor.bands.iter().enumerate() {
9✔
190
                    let column_name = if i == 0 {
9✔
191
                        new_column_name.clone()
6✔
192
                    } else {
193
                        format!("{new_column_name}_{i}")
3✔
194
                    };
195

196
                    columns.insert(
9✔
197
                        column_name,
9✔
198
                        VectorColumnInfo {
9✔
199
                            data_type: feature_data_type,
9✔
200
                            measurement: band.measurement.clone(),
9✔
201
                        },
9✔
202
                    );
9✔
203
                }
204
            }
205
            columns
5✔
206
        });
5✔
207

5✔
208
        Ok(InitializedRasterVectorJoin {
5✔
209
            name,
5✔
210
            result_descriptor,
5✔
211
            vector_source,
5✔
212
            raster_sources,
5✔
213
            raster_sources_bands,
5✔
214
            state: params,
5✔
215
        }
5✔
216
        .boxed())
5✔
217
    }
18✔
218

219
    span_fn!(RasterVectorJoin);
×
220
}
221

222
pub struct InitializedRasterVectorJoin {
223
    name: CanonicOperatorName,
224
    result_descriptor: VectorResultDescriptor,
225
    vector_source: Box<dyn InitializedVectorOperator>,
226
    raster_sources: Vec<Box<dyn InitializedRasterOperator>>,
227
    raster_sources_bands: Vec<u32>,
228
    state: RasterVectorJoinParams,
229
}
230

231
impl InitializedVectorOperator for InitializedRasterVectorJoin {
232
    fn result_descriptor(&self) -> &VectorResultDescriptor {
2✔
233
        &self.result_descriptor
2✔
234
    }
2✔
235

236
    fn query_processor(&self) -> Result<TypedVectorQueryProcessor> {
4✔
237
        let typed_raster_processors = self
4✔
238
            .raster_sources
4✔
239
            .iter()
4✔
240
            .map(InitializedRasterOperator::query_processor)
4✔
241
            .collect::<Result<Vec<_>>>()?;
4✔
242

243
        Ok(match self.vector_source.query_processor()? {
4✔
244
            TypedVectorQueryProcessor::Data(_) => unreachable!(),
×
245
            TypedVectorQueryProcessor::MultiPoint(points) => {
4✔
246
                TypedVectorQueryProcessor::MultiPoint(match self.state.temporal_aggregation {
4✔
247
                    TemporalAggregationMethod::None => RasterVectorJoinProcessor::new(
×
248
                        points,
×
249
                        self.result_descriptor.clone(),
×
250
                        typed_raster_processors,
×
251
                        self.raster_sources_bands.clone(),
×
252
                        self.state.names.clone(),
×
253
                        self.state.feature_aggregation,
×
254
                        self.state.feature_aggregation_ignore_no_data,
×
255
                    )
×
256
                    .boxed(),
×
257
                    TemporalAggregationMethod::First | TemporalAggregationMethod::Mean => {
258
                        RasterVectorAggregateJoinProcessor::new(
4✔
259
                            points,
4✔
260
                            self.result_descriptor.clone(),
4✔
261
                            typed_raster_processors,
4✔
262
                            self.raster_sources_bands.clone(),
4✔
263
                            self.state.names.clone(),
4✔
264
                            self.state.feature_aggregation,
4✔
265
                            self.state.feature_aggregation_ignore_no_data,
4✔
266
                            self.state.temporal_aggregation,
4✔
267
                            self.state.temporal_aggregation_ignore_no_data,
4✔
268
                        )
4✔
269
                        .boxed()
4✔
270
                    }
271
                })
272
            }
273
            TypedVectorQueryProcessor::MultiPolygon(polygons) => {
×
274
                TypedVectorQueryProcessor::MultiPolygon(match self.state.temporal_aggregation {
×
275
                    TemporalAggregationMethod::None => RasterVectorJoinProcessor::new(
×
276
                        polygons,
×
277
                        self.result_descriptor.clone(),
×
278
                        typed_raster_processors,
×
279
                        self.raster_sources_bands.clone(),
×
280
                        self.state.names.clone(),
×
281
                        self.state.feature_aggregation,
×
282
                        self.state.feature_aggregation_ignore_no_data,
×
283
                    )
×
284
                    .boxed(),
×
285
                    TemporalAggregationMethod::First | TemporalAggregationMethod::Mean => {
286
                        RasterVectorAggregateJoinProcessor::new(
×
287
                            polygons,
×
288
                            self.result_descriptor.clone(),
×
289
                            typed_raster_processors,
×
290
                            self.raster_sources_bands.clone(),
×
291
                            self.state.names.clone(),
×
292
                            self.state.feature_aggregation,
×
293
                            self.state.feature_aggregation_ignore_no_data,
×
294
                            self.state.temporal_aggregation,
×
295
                            self.state.temporal_aggregation_ignore_no_data,
×
296
                        )
×
297
                        .boxed()
×
298
                    }
299
                })
300
            }
301
            TypedVectorQueryProcessor::MultiLineString(_) => return Err(Error::NotYetImplemented),
×
302
        })
303
    }
4✔
304

305
    fn canonic_name(&self) -> CanonicOperatorName {
×
306
        self.name.clone()
×
307
    }
×
308
}
309

310
pub fn create_feature_aggregator<P: Pixel>(
31✔
311
    number_of_features: usize,
31✔
312
    aggregation: FeatureAggregationMethod,
31✔
313
    ignore_no_data: bool,
31✔
314
) -> TypedAggregator {
31✔
315
    match aggregation {
31✔
316
        FeatureAggregationMethod::First => match P::TYPE {
16✔
317
            RasterDataType::U8
318
            | RasterDataType::U16
319
            | RasterDataType::U32
320
            | RasterDataType::U64
321
            | RasterDataType::I8
322
            | RasterDataType::I16
323
            | RasterDataType::I32
324
            | RasterDataType::I64 => {
325
                FirstValueIntAggregator::new(number_of_features, ignore_no_data).into_typed()
16✔
326
            }
327
            RasterDataType::F32 | RasterDataType::F64 => {
328
                FirstValueFloatAggregator::new(number_of_features, ignore_no_data).into_typed()
×
329
            }
330
        },
331
        FeatureAggregationMethod::Mean => {
332
            MeanValueAggregator::new(number_of_features, ignore_no_data).into_typed()
15✔
333
        }
334
    }
335
}
31✔
336

337
#[cfg(test)]
338
mod tests {
339
    use super::*;
340
    use std::str::FromStr;
341

342
    use crate::engine::{
343
        ChunkByteSize, MockExecutionContext, MockQueryContext, QueryProcessor,
344
        RasterBandDescriptor, RasterBandDescriptors, RasterOperator, RasterResultDescriptor,
345
    };
346
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
347
    use crate::source::{GdalSource, GdalSourceParameters};
348
    use crate::util::gdal::add_ndvi_dataset;
349
    use futures::StreamExt;
350
    use geoengine_datatypes::collections::{FeatureCollectionInfos, MultiPointCollection};
351
    use geoengine_datatypes::dataset::NamedData;
352
    use geoengine_datatypes::primitives::{
353
        BoundingBox2D, ColumnSelection, DataRef, DateTime, FeatureDataRef, MultiPoint,
354
        SpatialResolution, TimeInterval, VectorQueryRectangle,
355
    };
356
    use geoengine_datatypes::primitives::{CacheHint, Measurement};
357
    use geoengine_datatypes::raster::RasterTile2D;
358
    use geoengine_datatypes::spatial_reference::SpatialReference;
359
    use geoengine_datatypes::util::{gdal::hide_gdal_errors, test::TestDefault};
360
    use serde_json::json;
361

362
    #[test]
1✔
363
    fn serialization() {
1✔
364
        let raster_vector_join = RasterVectorJoin {
1✔
365
            params: RasterVectorJoinParams {
1✔
366
                names: ["foo", "bar"].iter().copied().map(str::to_string).collect(),
1✔
367
                feature_aggregation: FeatureAggregationMethod::First,
1✔
368
                feature_aggregation_ignore_no_data: false,
1✔
369
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
370
                temporal_aggregation_ignore_no_data: false,
1✔
371
            },
1✔
372
            sources: SingleVectorMultipleRasterSources {
1✔
373
                vector: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![]).boxed(),
1✔
374
                rasters: vec![],
1✔
375
            },
1✔
376
        };
1✔
377

1✔
378
        let serialized = json!({
1✔
379
            "type": "RasterVectorJoin",
1✔
380
            "params": {
1✔
381
                "names": ["foo", "bar"],
1✔
382
                "featureAggregation": "first",
1✔
383
                "temporalAggregation": "mean",
1✔
384
            },
1✔
385
            "sources": {
1✔
386
                "vector": {
1✔
387
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
388
                    "params": {
1✔
389
                        "collections": [],
1✔
390
                        "spatialReference": "EPSG:4326",
1✔
391
                        "measurements": {},
1✔
392
                    }
1✔
393
                },
1✔
394
                "rasters": [],
1✔
395
            },
1✔
396
        })
1✔
397
        .to_string();
1✔
398

1✔
399
        let deserialized: RasterVectorJoin = serde_json::from_str(&serialized).unwrap();
1✔
400

1✔
401
        assert_eq!(deserialized.params, raster_vector_join.params);
1✔
402
    }
1✔
403

404
    fn ndvi_source(name: NamedData) -> Box<dyn RasterOperator> {
4✔
405
        let gdal_source = GdalSource {
4✔
406
            params: GdalSourceParameters { data: name },
4✔
407
        };
4✔
408

4✔
409
        gdal_source.boxed()
4✔
410
    }
4✔
411

412
    #[tokio::test]
1✔
413
    async fn ndvi_time_point() {
1✔
414
        let point_source = MockFeatureCollectionSource::single(
1✔
415
            MultiPointCollection::from_data(
1✔
416
                MultiPoint::many(vec![
1✔
417
                    (-13.95, 20.05),
1✔
418
                    (-14.05, 20.05),
1✔
419
                    (-13.95, 19.95),
1✔
420
                    (-14.05, 19.95),
1✔
421
                ])
1✔
422
                .unwrap(),
1✔
423
                vec![
1✔
424
                    TimeInterval::new(
1✔
425
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
426
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
427
                    )
1✔
428
                    .unwrap();
1✔
429
                    4
1✔
430
                ],
1✔
431
                Default::default(),
1✔
432
                CacheHint::default(),
1✔
433
            )
1✔
434
            .unwrap(),
1✔
435
        )
1✔
436
        .boxed();
1✔
437

1✔
438
        let mut exe_ctc = MockExecutionContext::test_default();
1✔
439
        let ndvi_id = add_ndvi_dataset(&mut exe_ctc);
1✔
440

1✔
441
        let operator = RasterVectorJoin {
1✔
442
            params: RasterVectorJoinParams {
1✔
443
                names: vec!["ndvi".to_string()],
1✔
444
                feature_aggregation: FeatureAggregationMethod::First,
1✔
445
                feature_aggregation_ignore_no_data: false,
1✔
446
                temporal_aggregation: TemporalAggregationMethod::First,
1✔
447
                temporal_aggregation_ignore_no_data: false,
1✔
448
            },
1✔
449
            sources: SingleVectorMultipleRasterSources {
1✔
450
                vector: point_source,
1✔
451
                rasters: vec![ndvi_source(ndvi_id.clone())],
1✔
452
            },
1✔
453
        };
1✔
454

1✔
455
        let operator = operator
1✔
456
            .boxed()
1✔
457
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctc)
1✔
458
            .await
1✔
459
            .unwrap();
1✔
460

1✔
461
        let query_processor = operator.query_processor().unwrap().multi_point().unwrap();
1✔
462

1✔
463
        let result = query_processor
1✔
464
            .query(
1✔
465
                VectorQueryRectangle {
1✔
466
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
467
                        .unwrap(),
1✔
468
                    time_interval: TimeInterval::default(),
1✔
469
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
470
                    attributes: ColumnSelection::all(),
1✔
471
                },
1✔
472
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
473
            )
1✔
474
            .await
1✔
475
            .unwrap()
1✔
476
            .map(Result::unwrap)
1✔
477
            .collect::<Vec<MultiPointCollection>>()
1✔
478
            .await;
33✔
479

1✔
480
        assert_eq!(result.len(), 1);
1✔
481

1✔
482
        let FeatureDataRef::Int(data) = result[0].data("ndvi").unwrap() else {
1✔
483
            unreachable!();
1✔
484
        };
1✔
485

1✔
486
        // these values are taken from loading the tiff in QGIS
1✔
487
        assert_eq!(data.as_ref(), &[54, 55, 51, 55]);
1✔
488
    }
1✔
489

490
    #[tokio::test]
1✔
491
    #[allow(clippy::float_cmp)]
492
    async fn ndvi_time_range() {
1✔
493
        let point_source = MockFeatureCollectionSource::single(
1✔
494
            MultiPointCollection::from_data(
1✔
495
                MultiPoint::many(vec![
1✔
496
                    (-13.95, 20.05),
1✔
497
                    (-14.05, 20.05),
1✔
498
                    (-13.95, 19.95),
1✔
499
                    (-14.05, 19.95),
1✔
500
                ])
1✔
501
                .unwrap(),
1✔
502
                vec![
1✔
503
                    TimeInterval::new(
1✔
504
                        DateTime::new_utc(2014, 1, 1, 0, 0, 0),
1✔
505
                        DateTime::new_utc(2014, 3, 1, 0, 0, 0),
1✔
506
                    )
1✔
507
                    .unwrap();
1✔
508
                    4
1✔
509
                ],
1✔
510
                Default::default(),
1✔
511
                CacheHint::default(),
1✔
512
            )
1✔
513
            .unwrap(),
1✔
514
        )
1✔
515
        .boxed();
1✔
516

1✔
517
        let mut exe_ctc = MockExecutionContext::test_default();
1✔
518
        let ndvi_id = add_ndvi_dataset(&mut exe_ctc);
1✔
519

1✔
520
        let operator = RasterVectorJoin {
1✔
521
            params: RasterVectorJoinParams {
1✔
522
                names: vec!["ndvi".to_string()],
1✔
523
                feature_aggregation: FeatureAggregationMethod::First,
1✔
524
                feature_aggregation_ignore_no_data: false,
1✔
525
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
526
                temporal_aggregation_ignore_no_data: false,
1✔
527
            },
1✔
528
            sources: SingleVectorMultipleRasterSources {
1✔
529
                vector: point_source,
1✔
530
                rasters: vec![ndvi_source(ndvi_id.clone())],
1✔
531
            },
1✔
532
        };
1✔
533

1✔
534
        let operator = operator
1✔
535
            .boxed()
1✔
536
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctc)
1✔
537
            .await
1✔
538
            .unwrap();
1✔
539

1✔
540
        let query_processor = operator.query_processor().unwrap().multi_point().unwrap();
1✔
541

1✔
542
        let result = query_processor
1✔
543
            .query(
1✔
544
                VectorQueryRectangle {
1✔
545
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
546
                        .unwrap(),
1✔
547
                    time_interval: TimeInterval::default(),
1✔
548
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
549
                    attributes: ColumnSelection::all(),
1✔
550
                },
1✔
551
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
552
            )
1✔
553
            .await
1✔
554
            .unwrap()
1✔
555
            .map(Result::unwrap)
1✔
556
            .collect::<Vec<MultiPointCollection>>()
1✔
557
            .await;
64✔
558

1✔
559
        assert_eq!(result.len(), 1);
1✔
560

1✔
561
        let FeatureDataRef::Float(data) = result[0].data("ndvi").unwrap() else {
1✔
562
            unreachable!();
1✔
563
        };
1✔
564

1✔
565
        // these values are taken from loading the tiff in QGIS
1✔
566
        assert_eq!(
1✔
567
            data.as_ref(),
1✔
568
            &[
1✔
569
                (54. + 52.) / 2.,
1✔
570
                (55. + 55.) / 2.,
1✔
571
                (51. + 50.) / 2.,
1✔
572
                (55. + 53.) / 2.,
1✔
573
            ]
1✔
574
        );
1✔
575
    }
1✔
576

577
    #[tokio::test]
1✔
578
    #[allow(clippy::float_cmp)]
579
    async fn ndvi_with_default_time() {
1✔
580
        hide_gdal_errors();
1✔
581

1✔
582
        let point_source = MockFeatureCollectionSource::single(
1✔
583
            MultiPointCollection::from_data(
1✔
584
                MultiPoint::many(vec![
1✔
585
                    (-13.95, 20.05),
1✔
586
                    (-14.05, 20.05),
1✔
587
                    (-13.95, 19.95),
1✔
588
                    (-14.05, 19.95),
1✔
589
                ])
1✔
590
                .unwrap(),
1✔
591
                vec![TimeInterval::default(); 4],
1✔
592
                Default::default(),
1✔
593
                CacheHint::default(),
1✔
594
            )
1✔
595
            .unwrap(),
1✔
596
        )
1✔
597
        .boxed();
1✔
598

1✔
599
        let mut exe_ctc = MockExecutionContext::test_default();
1✔
600
        let ndvi_id = add_ndvi_dataset(&mut exe_ctc);
1✔
601

1✔
602
        let operator = RasterVectorJoin {
1✔
603
            params: RasterVectorJoinParams {
1✔
604
                names: vec!["ndvi".to_string()],
1✔
605
                feature_aggregation: FeatureAggregationMethod::First,
1✔
606
                feature_aggregation_ignore_no_data: false,
1✔
607
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
608
                temporal_aggregation_ignore_no_data: false,
1✔
609
            },
1✔
610
            sources: SingleVectorMultipleRasterSources {
1✔
611
                vector: point_source,
1✔
612
                rasters: vec![ndvi_source(ndvi_id.clone())],
1✔
613
            },
1✔
614
        };
1✔
615

1✔
616
        let operator = operator
1✔
617
            .boxed()
1✔
618
            .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctc)
1✔
619
            .await
1✔
620
            .unwrap();
1✔
621

1✔
622
        let query_processor = operator.query_processor().unwrap().multi_point().unwrap();
1✔
623

1✔
624
        let result = query_processor
1✔
625
            .query(
1✔
626
                VectorQueryRectangle {
1✔
627
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
628
                        .unwrap(),
1✔
629
                    time_interval: TimeInterval::default(),
1✔
630
                    spatial_resolution: SpatialResolution::new(0.1, 0.1).unwrap(),
1✔
631
                    attributes: ColumnSelection::all(),
1✔
632
                },
1✔
633
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
634
            )
1✔
635
            .await
1✔
636
            .unwrap()
1✔
637
            .map(Result::unwrap)
1✔
638
            .collect::<Vec<MultiPointCollection>>()
1✔
639
            .await;
4✔
640

1✔
641
        assert_eq!(result.len(), 1);
1✔
642

1✔
643
        let FeatureDataRef::Float(data) = result[0].data("ndvi").unwrap() else {
1✔
644
            unreachable!();
1✔
645
        };
1✔
646

1✔
647
        assert_eq!(data.as_ref(), &[0., 0., 0., 0.]);
1✔
648

1✔
649
        assert_eq!(data.nulls(), vec![true, true, true, true]);
1✔
650
    }
1✔
651

652
    #[tokio::test]
1✔
653
    async fn it_checks_sref() {
1✔
654
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
655
            vec![MultiPointCollection::from_data(
1✔
656
                MultiPoint::many(vec![
1✔
657
                    (-13.95, 20.05),
1✔
658
                    (-14.05, 20.05),
1✔
659
                    (-13.95, 19.95),
1✔
660
                    (-14.05, 19.95),
1✔
661
                ])
1✔
662
                .unwrap(),
1✔
663
                vec![TimeInterval::default(); 4],
1✔
664
                Default::default(),
1✔
665
                CacheHint::default(),
1✔
666
            )
1✔
667
            .unwrap()],
1✔
668
            SpatialReference::from_str("EPSG:3857").unwrap(),
1✔
669
        )
1✔
670
        .boxed();
1✔
671

1✔
672
        let mut exe_ctc = MockExecutionContext::test_default();
1✔
673
        let ndvi_id = add_ndvi_dataset(&mut exe_ctc);
1✔
674

1✔
675
        let operator = RasterVectorJoin {
1✔
676
            params: RasterVectorJoinParams {
1✔
677
                names: vec!["ndvi".to_string()],
1✔
678
                feature_aggregation: FeatureAggregationMethod::First,
1✔
679
                feature_aggregation_ignore_no_data: false,
1✔
680
                temporal_aggregation: TemporalAggregationMethod::Mean,
1✔
681
                temporal_aggregation_ignore_no_data: false,
1✔
682
            },
1✔
683
            sources: SingleVectorMultipleRasterSources {
1✔
684
                vector: point_source,
1✔
685
                rasters: vec![ndvi_source(ndvi_id.clone())],
1✔
686
            },
1✔
687
        }
1✔
688
        .boxed()
1✔
689
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctc)
1✔
690
        .await;
1✔
691

1✔
692
        assert!(matches!(
1✔
693
            operator,
1✔
694
            Err(Error::InvalidSpatialReference {
1✔
695
                expected: _,
1✔
696
                found: _,
1✔
697
            })
1✔
698
        ));
1✔
699
    }
1✔
700

701
    #[tokio::test]
1✔
702
    #[allow(clippy::too_many_lines)]
703
    async fn it_includes_bands_in_result_descriptor() {
1✔
704
        let point_source = MockFeatureCollectionSource::with_collections_and_sref(
1✔
705
            vec![MultiPointCollection::from_data(
1✔
706
                MultiPoint::many(vec![
1✔
707
                    (-13.95, 20.05),
1✔
708
                    (-14.05, 20.05),
1✔
709
                    (-13.95, 19.95),
1✔
710
                    (-14.05, 19.95),
1✔
711
                ])
1✔
712
                .unwrap(),
1✔
713
                vec![TimeInterval::default(); 4],
1✔
714
                Default::default(),
1✔
715
                CacheHint::default(),
1✔
716
            )
1✔
717
            .unwrap()],
1✔
718
            SpatialReference::from_str("EPSG:4326").unwrap(),
1✔
719
        )
1✔
720
        .boxed();
1✔
721

1✔
722
        let raster_source = MockRasterSource {
1✔
723
            params: MockRasterSourceParams {
1✔
724
                data: Vec::<RasterTile2D<u8>>::new(),
1✔
725
                result_descriptor: RasterResultDescriptor {
1✔
726
                    data_type: RasterDataType::U8,
1✔
727
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
728
                    time: None,
1✔
729
                    bbox: None,
1✔
730
                    resolution: None,
1✔
731
                    bands: RasterBandDescriptors::new(vec![
1✔
732
                        RasterBandDescriptor::new_unitless("band_0".into()),
1✔
733
                        RasterBandDescriptor::new_unitless("band_1".into()),
1✔
734
                    ])
1✔
735
                    .unwrap(),
1✔
736
                },
1✔
737
            },
1✔
738
        }
1✔
739
        .boxed();
1✔
740

1✔
741
        let raster_source2 = MockRasterSource {
1✔
742
            params: MockRasterSourceParams {
1✔
743
                data: Vec::<RasterTile2D<u8>>::new(),
1✔
744
                result_descriptor: RasterResultDescriptor {
1✔
745
                    data_type: RasterDataType::U8,
1✔
746
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
747
                    time: None,
1✔
748
                    bbox: None,
1✔
749
                    resolution: None,
1✔
750
                    bands: RasterBandDescriptors::new(vec![
1✔
751
                        RasterBandDescriptor::new_unitless("band_0".into()),
1✔
752
                        RasterBandDescriptor::new_unitless("band_1".into()),
1✔
753
                        RasterBandDescriptor::new_unitless("band_2".into()),
1✔
754
                    ])
1✔
755
                    .unwrap(),
1✔
756
                },
1✔
757
            },
1✔
758
        }
1✔
759
        .boxed();
1✔
760

1✔
761
        let exe_ctc = MockExecutionContext::test_default();
1✔
762

1✔
763
        let join = RasterVectorJoin {
1✔
764
            params: RasterVectorJoinParams {
1✔
765
                names: vec!["s0".to_string(), "s1".to_string()],
1✔
766
                feature_aggregation: FeatureAggregationMethod::First,
1✔
767
                feature_aggregation_ignore_no_data: false,
1✔
768
                temporal_aggregation: TemporalAggregationMethod::None,
1✔
769
                temporal_aggregation_ignore_no_data: false,
1✔
770
            },
1✔
771
            sources: SingleVectorMultipleRasterSources {
1✔
772
                vector: point_source,
1✔
773
                rasters: vec![raster_source, raster_source2],
1✔
774
            },
1✔
775
        }
1✔
776
        .boxed()
1✔
777
        .initialize(WorkflowOperatorPath::initialize_root(), &exe_ctc)
1✔
778
        .await
1✔
779
        .unwrap();
1✔
780

1✔
781
        assert_eq!(
1✔
782
            join.result_descriptor(),
1✔
783
            &VectorResultDescriptor {
1✔
784
                data_type: VectorDataType::MultiPoint,
1✔
785
                spatial_reference: SpatialReference::epsg_4326().into(),
1✔
786
                columns: [
1✔
787
                    (
1✔
788
                        "s0".to_string(),
1✔
789
                        VectorColumnInfo {
1✔
790
                            data_type: FeatureDataType::Int,
1✔
791
                            measurement: Measurement::Unitless
1✔
792
                        }
1✔
793
                    ),
1✔
794
                    (
1✔
795
                        "s0_1".to_string(),
1✔
796
                        VectorColumnInfo {
1✔
797
                            data_type: FeatureDataType::Int,
1✔
798
                            measurement: Measurement::Unitless
1✔
799
                        }
1✔
800
                    ),
1✔
801
                    (
1✔
802
                        "s1".to_string(),
1✔
803
                        VectorColumnInfo {
1✔
804
                            data_type: FeatureDataType::Int,
1✔
805
                            measurement: Measurement::Unitless
1✔
806
                        }
1✔
807
                    ),
1✔
808
                    (
1✔
809
                        "s1_1".to_string(),
1✔
810
                        VectorColumnInfo {
1✔
811
                            data_type: FeatureDataType::Int,
1✔
812
                            measurement: Measurement::Unitless
1✔
813
                        }
1✔
814
                    ),
1✔
815
                    (
1✔
816
                        "s1_2".to_string(),
1✔
817
                        VectorColumnInfo {
1✔
818
                            data_type: FeatureDataType::Int,
1✔
819
                            measurement: Measurement::Unitless
1✔
820
                        }
1✔
821
                    )
1✔
822
                ]
1✔
823
                .into_iter()
1✔
824
                .collect(),
1✔
825
                time: None,
1✔
826
                bbox: None
1✔
827
            }
1✔
828
        );
1✔
829
    }
1✔
830
}
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