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

geo-engine / geoengine / 5006008836

pending completion
5006008836

push

github

GitHub
Merge #785 #787

936 of 936 new or added lines in 50 files covered. (100.0%)

96010 of 107707 relevant lines covered (89.14%)

72676.46 hits per line

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

96.13
/operators/src/plot/statistics.rs
1
use crate::engine::{
2
    CanonicOperatorName, ExecutionContext, InitializedPlotOperator, InitializedRasterOperator,
3
    InitializedVectorOperator, MultipleRasterOrSingleVectorSource, Operator, OperatorName,
4
    PlotOperator, PlotQueryProcessor, PlotResultDescriptor, QueryContext, QueryProcessor,
5
    TypedPlotQueryProcessor, TypedRasterQueryProcessor, TypedVectorQueryProcessor,
6
    WorkflowOperatorPath,
7
};
8
use crate::error;
9
use crate::error::Error;
10
use crate::util::input::MultiRasterOrVectorOperator;
11
use crate::util::number_statistics::NumberStatistics;
12
use crate::util::Result;
13
use async_trait::async_trait;
14
use futures::stream::select_all;
15
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
16
use geoengine_datatypes::collections::FeatureCollectionInfos;
17
use geoengine_datatypes::primitives::{
18
    partitions_extent, time_interval_extent, AxisAlignedRectangle, BoundingBox2D,
19
    PlotQueryRectangle, VectorQueryRectangle,
20
};
21
use geoengine_datatypes::raster::ConvertDataTypeParallel;
22
use geoengine_datatypes::raster::{GridOrEmpty, GridSize};
23
use geoengine_datatypes::spatial_reference::SpatialReferenceOption;
24
use serde::{Deserialize, Serialize};
25
use snafu::ensure;
26
use std::collections::HashMap;
27

28
pub const STATISTICS_OPERATOR_NAME: &str = "Statistics";
29

30
/// A plot that outputs basic statistics about its inputs
31
///
32
/// Does currently not use a weighted computations, so it assumes equally weighted
33
/// time steps in the sources.
34
pub type Statistics = Operator<StatisticsParams, MultipleRasterOrSingleVectorSource>;
35

36
impl OperatorName for Statistics {
37
    const TYPE_NAME: &'static str = "Statistics";
38
}
39

40
/// The parameter spec for `Statistics`
41
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25✔
42
#[serde(rename_all = "camelCase")]
43
pub struct StatisticsParams {
44
    /// Names of the (numeric) attributes to compute the statistics on.
45
    #[serde(default)]
46
    pub column_names: Vec<String>,
47
}
48

49
#[typetag::serde]
3✔
50
#[async_trait]
51
impl PlotOperator for Statistics {
52
    async fn _initialize(
11✔
53
        self: Box<Self>,
11✔
54
        path: WorkflowOperatorPath,
11✔
55
        context: &dyn ExecutionContext,
11✔
56
    ) -> Result<Box<dyn InitializedPlotOperator>> {
11✔
57
        let name = CanonicOperatorName::from(&self);
11✔
58

11✔
59
        match self.sources.source {
11✔
60
            MultiRasterOrVectorOperator::Raster(rasters) => {
7✔
61
                ensure!( self.params.column_names.is_empty() || self.params.column_names.len() == rasters.len(),
7✔
62
                    error::InvalidOperatorSpec {
1✔
63
                        reason: "Statistics on raster data must either contain a name/alias for every input ('column_names' parameter) or no names at all."
1✔
64
                            .to_string(),
1✔
65
                });
1✔
66

67
                let output_names = if self.params.column_names.is_empty() {
6✔
68
                    (1..=rasters.len())
5✔
69
                        .map(|i| format!("Raster-{i}"))
5✔
70
                        .collect::<Vec<_>>()
5✔
71
                } else {
72
                    self.params.column_names.clone()
1✔
73
                };
74

75
                let rasters = futures::future::try_join_all(
6✔
76
                    rasters
6✔
77
                        .into_iter()
6✔
78
                        .enumerate()
6✔
79
                        .map(|(i, op)| op.initialize(path.clone_and_append(i as u8), context)),
6✔
80
                )
6✔
81
                .await?;
×
82

83
                let in_descriptors = rasters
6✔
84
                    .iter()
6✔
85
                    .map(InitializedRasterOperator::result_descriptor)
6✔
86
                    .collect::<Vec<_>>();
6✔
87

6✔
88
                if rasters.len() > 1 {
6✔
89
                    let srs = in_descriptors[0].spatial_reference;
2✔
90
                    ensure!(
2✔
91
                        in_descriptors.iter().all(|d| d.spatial_reference == srs),
4✔
92
                        error::AllSourcesMustHaveSameSpatialReference
×
93
                    );
94
                }
4✔
95

96
                let time = time_interval_extent(in_descriptors.iter().map(|d| d.time));
6✔
97
                let bbox = partitions_extent(in_descriptors.iter().map(|d| d.bbox));
6✔
98

6✔
99
                let initialized_operator = InitializedStatistics::new(
6✔
100
                    name,
6✔
101
                    PlotResultDescriptor {
6✔
102
                        spatial_reference: rasters.get(0).map_or_else(
6✔
103
                            || SpatialReferenceOption::Unreferenced,
6✔
104
                            |r| r.result_descriptor().spatial_reference,
6✔
105
                        ),
6✔
106
                        time,
6✔
107
                        bbox: bbox
6✔
108
                            .and_then(|p| BoundingBox2D::new(p.lower_left(), p.upper_right()).ok()),
6✔
109
                    },
6✔
110
                    output_names,
6✔
111
                    rasters,
6✔
112
                );
6✔
113

6✔
114
                Ok(initialized_operator.boxed())
6✔
115
            }
116
            MultiRasterOrVectorOperator::Vector(vector_source) => {
4✔
117
                let initialized_vector = vector_source
4✔
118
                    .initialize(path.clone_and_append(0), context)
4✔
119
                    .await?;
1✔
120

121
                let in_descriptor = initialized_vector.result_descriptor();
3✔
122

123
                let column_names = if self.params.column_names.is_empty() {
3✔
124
                    in_descriptor
1✔
125
                        .columns
1✔
126
                        .clone()
1✔
127
                        .into_iter()
1✔
128
                        .filter(|(_, info)| info.data_type.is_numeric())
2✔
129
                        .map(|(name, _)| name)
2✔
130
                        .collect()
1✔
131
                } else {
132
                    for cn in &self.params.column_names {
5✔
133
                        match in_descriptor.column_data_type(cn.as_str()) {
3✔
134
                            Some(column) if !column.is_numeric() => {
3✔
135
                                return Err(Error::InvalidOperatorSpec {
×
136
                                    reason: format!("Column '{cn}' is not numeric."),
×
137
                                });
×
138
                            }
139
                            Some(_) => {
3✔
140
                                // OK
3✔
141
                            }
3✔
142
                            None => {
143
                                return Err(Error::ColumnDoesNotExist {
×
144
                                    column: cn.to_string(),
×
145
                                });
×
146
                            }
147
                        }
148
                    }
149
                    self.params.column_names.clone()
2✔
150
                };
151

152
                let initialized_operator = InitializedStatistics::new(
3✔
153
                    name,
3✔
154
                    PlotResultDescriptor {
3✔
155
                        spatial_reference: in_descriptor.spatial_reference,
3✔
156
                        time: in_descriptor.time,
3✔
157
                        bbox: in_descriptor.bbox,
3✔
158
                    },
3✔
159
                    column_names,
3✔
160
                    initialized_vector,
3✔
161
                );
3✔
162

3✔
163
                Ok(initialized_operator.boxed())
3✔
164
            }
165
        }
166
    }
22✔
167

168
    span_fn!(Statistics);
×
169
}
170

171
/// The initialization of `Statistics`
172
pub struct InitializedStatistics<Op> {
173
    name: CanonicOperatorName,
174
    result_descriptor: PlotResultDescriptor,
175
    column_names: Vec<String>,
176
    source: Op,
177
}
178

179
impl<Op> InitializedStatistics<Op> {
180
    pub fn new(
9✔
181
        name: CanonicOperatorName,
9✔
182
        result_descriptor: PlotResultDescriptor,
9✔
183
        column_names: Vec<String>,
9✔
184
        source: Op,
9✔
185
    ) -> Self {
9✔
186
        Self {
9✔
187
            name,
9✔
188
            result_descriptor,
9✔
189
            column_names,
9✔
190
            source,
9✔
191
        }
9✔
192
    }
9✔
193
}
194

195
impl InitializedPlotOperator for InitializedStatistics<Box<dyn InitializedVectorOperator>> {
196
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
197
        &self.result_descriptor
×
198
    }
×
199

200
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
3✔
201
        Ok(TypedPlotQueryProcessor::JsonPlain(
3✔
202
            StatisticsVectorQueryProcessor {
3✔
203
                vector: self.source.query_processor()?,
3✔
204
                column_names: self.column_names.clone(),
3✔
205
            }
3✔
206
            .boxed(),
3✔
207
        ))
208
    }
3✔
209

210
    fn canonic_name(&self) -> CanonicOperatorName {
×
211
        self.name.clone()
×
212
    }
×
213
}
214

215
impl InitializedPlotOperator for InitializedStatistics<Vec<Box<dyn InitializedRasterOperator>>> {
216
    fn result_descriptor(&self) -> &PlotResultDescriptor {
2✔
217
        &self.result_descriptor
2✔
218
    }
2✔
219

220
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
5✔
221
        Ok(TypedPlotQueryProcessor::JsonPlain(
5✔
222
            StatisticsRasterQueryProcessor {
5✔
223
                rasters: self
5✔
224
                    .source
5✔
225
                    .iter()
5✔
226
                    .map(InitializedRasterOperator::query_processor)
5✔
227
                    .collect::<Result<Vec<_>>>()?,
5✔
228
                column_names: self.column_names.clone(),
5✔
229
            }
5✔
230
            .boxed(),
5✔
231
        ))
232
    }
5✔
233

234
    fn canonic_name(&self) -> CanonicOperatorName {
×
235
        self.name.clone()
×
236
    }
×
237
}
238

239
/// A query processor that calculates the statistics about its vector input.
240
pub struct StatisticsVectorQueryProcessor {
241
    vector: TypedVectorQueryProcessor,
242
    column_names: Vec<String>,
243
}
244

245
#[async_trait]
246
impl PlotQueryProcessor for StatisticsVectorQueryProcessor {
247
    type OutputFormat = serde_json::Value;
248

249
    fn plot_type(&self) -> &'static str {
×
250
        STATISTICS_OPERATOR_NAME
×
251
    }
×
252

253
    async fn plot_query<'a>(
3✔
254
        &'a self,
3✔
255
        query: PlotQueryRectangle,
3✔
256
        ctx: &'a dyn QueryContext,
3✔
257
    ) -> Result<Self::OutputFormat> {
3✔
258
        let mut number_statistics: HashMap<String, NumberStatistics> = self
3✔
259
            .column_names
3✔
260
            .iter()
3✔
261
            .map(|column| (column.clone(), NumberStatistics::default()))
5✔
262
            .collect();
3✔
263

264
        call_on_generic_vector_processor!(&self.vector, processor => {
3✔
265
            let mut query = processor.query(query, ctx).await?;
3✔
266

267
            while let Some(collection) = query.next().await {
6✔
268
                let collection = collection?;
3✔
269

270
                for (column, stats) in &mut number_statistics {
8✔
271
                    match collection.data(column) {
5✔
272
                        Ok(data) => data.float_options_iter().for_each(
5✔
273
                            | value | {
35✔
274
                                match value {
35✔
275
                                    Some(v) => stats.add(v),
25✔
276
                                    None => stats.add_no_data()
10✔
277
                                }
278
                            }
35✔
279
                        ),
5✔
280
                        Err(_) => stats.add_no_data_batch(collection.len())
×
281
                    }
282
                }
283
            }
284
        });
285

286
        let output: HashMap<String, StatisticsOutput> = number_statistics
3✔
287
            .iter()
3✔
288
            .map(|(column, number_statistics)| {
5✔
289
                (column.clone(), StatisticsOutput::from(number_statistics))
5✔
290
            })
5✔
291
            .collect();
3✔
292
        serde_json::to_value(output).map_err(Into::into)
3✔
293
    }
6✔
294
}
295

296
/// A query processor that calculates the statistics about its raster inputs.
297
pub struct StatisticsRasterQueryProcessor {
298
    rasters: Vec<TypedRasterQueryProcessor>,
299
    column_names: Vec<String>,
300
}
301

302
#[async_trait]
303
impl PlotQueryProcessor for StatisticsRasterQueryProcessor {
304
    type OutputFormat = serde_json::Value;
305

306
    fn plot_type(&self) -> &'static str {
1✔
307
        STATISTICS_OPERATOR_NAME
1✔
308
    }
1✔
309

310
    async fn plot_query<'a>(
5✔
311
        &'a self,
5✔
312
        query: VectorQueryRectangle,
5✔
313
        ctx: &'a dyn QueryContext,
5✔
314
    ) -> Result<Self::OutputFormat> {
5✔
315
        let mut queries = Vec::with_capacity(self.rasters.len());
5✔
316
        let q = query.into();
5✔
317
        for (i, raster_processor) in self.rasters.iter().enumerate() {
6✔
318
            queries.push(
319
                call_on_generic_raster_processor!(raster_processor, processor => {
6✔
320
                    processor.query(q, ctx).await?
6✔
321
                             .and_then(move |tile| crate::util::spawn_blocking_with_thread_pool(ctx.thread_pool().clone(), move || (i, tile.convert_data_type_parallel()) ).map_err(Into::into))
55,209✔
322
                             .boxed()
6✔
323
                }),
324
            );
325
        }
326

327
        let number_statistics = vec![NumberStatistics::default(); self.rasters.len()];
5✔
328

5✔
329
        select_all(queries)
5✔
330
            .fold(
5✔
331
                Ok(number_statistics),
5✔
332
                |number_statistics: Result<Vec<NumberStatistics>>, enumerated_raster_tile| async move {
55,209✔
333
                    let mut number_statistics = number_statistics?;
55,209✔
334
                    let (i, raster_tile) = enumerated_raster_tile?;
55,209✔
335
                    match raster_tile.grid_array {
55,209✔
336
                        GridOrEmpty::Grid(g) => process_raster(&mut number_statistics[i], g.masked_element_deref_iterator()),
6✔
337
                        GridOrEmpty::Empty(n) => number_statistics[i].add_no_data_batch(n.number_of_elements())
55,203✔
338
                    }
339

340
                    Ok(number_statistics)
55,209✔
341
                },
55,209✔
342
            )
5✔
343
            .map(|number_statistics| {
5✔
344
                let output: HashMap<String, StatisticsOutput> = number_statistics?.iter().enumerate().map(|(i, stat)| (self.column_names[i].clone(), StatisticsOutput::from(stat))).collect();
6✔
345
                serde_json::to_value(output).map_err(Into::into)
5✔
346
            })
5✔
347
            .await
30,127✔
348
    }
10✔
349
}
350

351
fn process_raster<I>(number_statistics: &mut NumberStatistics, data: I)
6✔
352
where
6✔
353
    I: Iterator<Item = Option<f64>>,
6✔
354
{
6✔
355
    for value_option in data {
42✔
356
        if let Some(value) = value_option {
36✔
357
            number_statistics.add(value);
36✔
358
        } else {
36✔
359
            number_statistics.add_no_data();
×
360
        }
×
361
    }
362
}
6✔
363

364
/// The statistics summary output type for each raster input/vector input column
365
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
11✔
366
#[serde(rename_all = "camelCase")]
367
struct StatisticsOutput {
368
    pub value_count: usize,
369
    pub valid_count: usize,
370
    pub min: f64,
371
    pub max: f64,
372
    pub mean: f64,
373
    pub stddev: f64,
374
}
375

376
impl From<&NumberStatistics> for StatisticsOutput {
377
    fn from(number_statistics: &NumberStatistics) -> Self {
11✔
378
        Self {
11✔
379
            value_count: number_statistics.count() + number_statistics.nan_count(),
11✔
380
            valid_count: number_statistics.count(),
11✔
381
            min: number_statistics.min(),
11✔
382
            max: number_statistics.max(),
11✔
383
            mean: number_statistics.mean(),
11✔
384
            stddev: number_statistics.std_dev(),
11✔
385
        }
11✔
386
    }
11✔
387
}
388

389
#[cfg(test)]
390
mod tests {
391
    use geoengine_datatypes::collections::DataCollection;
392
    use geoengine_datatypes::util::test::TestDefault;
393
    use serde_json::json;
394

395
    use super::*;
396
    use crate::engine::VectorOperator;
397
    use crate::engine::{
398
        ChunkByteSize, MockExecutionContext, MockQueryContext, RasterOperator,
399
        RasterResultDescriptor,
400
    };
401
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
402
    use crate::util::input::MultiRasterOrVectorOperator::Raster;
403
    use geoengine_datatypes::primitives::{
404
        BoundingBox2D, FeatureData, Measurement, NoGeometry, SpatialResolution, TimeInterval,
405
    };
406
    use geoengine_datatypes::raster::{
407
        Grid2D, RasterDataType, RasterTile2D, TileInformation, TilingSpecification,
408
    };
409
    use geoengine_datatypes::spatial_reference::SpatialReference;
410

411
    #[test]
1✔
412
    fn serialization() {
1✔
413
        let statistics = Statistics {
1✔
414
            params: StatisticsParams {
1✔
415
                column_names: vec![],
1✔
416
            },
1✔
417
            sources: MultipleRasterOrSingleVectorSource {
1✔
418
                source: Raster(vec![]),
1✔
419
            },
1✔
420
        };
1✔
421

1✔
422
        let serialized = json!({
1✔
423
            "type": "Statistics",
1✔
424
            "params": {},
1✔
425
            "sources": {
1✔
426
                "source": [],
1✔
427
            },
1✔
428
        })
1✔
429
        .to_string();
1✔
430

1✔
431
        let deserialized: Statistics = serde_json::from_str(&serialized).unwrap();
1✔
432

1✔
433
        assert_eq!(deserialized.params, statistics.params);
1✔
434
    }
1✔
435

436
    #[tokio::test]
1✔
437
    async fn empty_raster_input() {
1✔
438
        let tile_size_in_pixels = [3, 2].into();
1✔
439
        let tiling_specification = TilingSpecification {
1✔
440
            origin_coordinate: [0.0, 0.0].into(),
1✔
441
            tile_size_in_pixels,
1✔
442
        };
1✔
443

1✔
444
        let statistics = Statistics {
1✔
445
            params: StatisticsParams {
1✔
446
                column_names: vec![],
1✔
447
            },
1✔
448
            sources: vec![].into(),
1✔
449
        };
1✔
450

1✔
451
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
452

453
        let statistics = statistics
1✔
454
            .boxed()
1✔
455
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
456
            .await
×
457
            .unwrap();
1✔
458

1✔
459
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
460

461
        let result = processor
1✔
462
            .plot_query(
1✔
463
                VectorQueryRectangle {
1✔
464
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
465
                        .unwrap(),
1✔
466
                    time_interval: TimeInterval::default(),
1✔
467
                    spatial_resolution: SpatialResolution::one(),
1✔
468
                },
1✔
469
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
470
            )
1✔
471
            .await
×
472
            .unwrap();
1✔
473

1✔
474
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
475
    }
476

477
    #[tokio::test]
1✔
478
    async fn single_raster_implicit_name() {
1✔
479
        let tile_size_in_pixels = [3, 2].into();
1✔
480
        let tiling_specification = TilingSpecification {
1✔
481
            origin_coordinate: [0.0, 0.0].into(),
1✔
482
            tile_size_in_pixels,
1✔
483
        };
1✔
484

1✔
485
        let raster_source = MockRasterSource {
1✔
486
            params: MockRasterSourceParams {
1✔
487
                data: vec![RasterTile2D::new_with_tile_info(
1✔
488
                    TimeInterval::default(),
1✔
489
                    TileInformation {
1✔
490
                        global_geo_transform: TestDefault::test_default(),
1✔
491
                        global_tile_position: [0, 0].into(),
1✔
492
                        tile_size_in_pixels,
1✔
493
                    },
1✔
494
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
495
                        .unwrap()
1✔
496
                        .into(),
1✔
497
                )],
1✔
498
                result_descriptor: RasterResultDescriptor {
1✔
499
                    data_type: RasterDataType::U8,
1✔
500
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
501
                    measurement: Measurement::Unitless,
1✔
502
                    time: None,
1✔
503
                    bbox: None,
1✔
504
                    resolution: None,
1✔
505
                },
1✔
506
            },
1✔
507
        }
1✔
508
        .boxed();
1✔
509

1✔
510
        let statistics = Statistics {
1✔
511
            params: StatisticsParams {
1✔
512
                column_names: vec![],
1✔
513
            },
1✔
514
            sources: vec![raster_source].into(),
1✔
515
        };
1✔
516

1✔
517
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
518

519
        let statistics = statistics
1✔
520
            .boxed()
1✔
521
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
522
            .await
×
523
            .unwrap();
1✔
524

1✔
525
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
526

527
        let result = processor
1✔
528
            .plot_query(
1✔
529
                VectorQueryRectangle {
1✔
530
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
531
                        .unwrap(),
1✔
532
                    time_interval: TimeInterval::default(),
1✔
533
                    spatial_resolution: SpatialResolution::one(),
1✔
534
                },
1✔
535
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
536
            )
1✔
537
            .await
13,564✔
538
            .unwrap();
1✔
539

1✔
540
        assert_eq!(
1✔
541
            result.to_string(),
1✔
542
            json!({
1✔
543
                "Raster-1": {
1✔
544
                    "valueCount": 66_246, // 362*183 Note: this is caused by the inclusive nature of the bounding box. Since the right and lower bounds are included this wraps to a new row/column of tiles. In this test the tiles are 3x2 pixels in size.
1✔
545
                    "validCount": 6,
1✔
546
                    "min": 1.0,
1✔
547
                    "max": 6.0,
1✔
548
                    "mean": 3.5,
1✔
549
                    "stddev": 1.707_825_127_659_933,
1✔
550
                }
1✔
551
            })
1✔
552
            .to_string()
1✔
553
        );
1✔
554
    }
555

556
    #[tokio::test]
1✔
557
    #[allow(clippy::too_many_lines)]
558
    async fn two_rasters_implicit_names() {
1✔
559
        let tile_size_in_pixels = [3, 2].into();
1✔
560
        let tiling_specification = TilingSpecification {
1✔
561
            origin_coordinate: [0.0, 0.0].into(),
1✔
562
            tile_size_in_pixels,
1✔
563
        };
1✔
564

1✔
565
        let raster_source = vec![
1✔
566
            MockRasterSource {
1✔
567
                params: MockRasterSourceParams {
1✔
568
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
569
                        TimeInterval::default(),
1✔
570
                        TileInformation {
1✔
571
                            global_geo_transform: TestDefault::test_default(),
1✔
572
                            global_tile_position: [0, 0].into(),
1✔
573
                            tile_size_in_pixels,
1✔
574
                        },
1✔
575
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
576
                            .unwrap()
1✔
577
                            .into(),
1✔
578
                    )],
1✔
579
                    result_descriptor: RasterResultDescriptor {
1✔
580
                        data_type: RasterDataType::U8,
1✔
581
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
582
                        measurement: Measurement::Unitless,
1✔
583
                        time: None,
1✔
584
                        bbox: None,
1✔
585
                        resolution: None,
1✔
586
                    },
1✔
587
                },
1✔
588
            }
1✔
589
            .boxed(),
1✔
590
            MockRasterSource {
1✔
591
                params: MockRasterSourceParams {
1✔
592
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
593
                        TimeInterval::default(),
1✔
594
                        TileInformation {
1✔
595
                            global_geo_transform: TestDefault::test_default(),
1✔
596
                            global_tile_position: [0, 0].into(),
1✔
597
                            tile_size_in_pixels,
1✔
598
                        },
1✔
599
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
600
                            .unwrap()
1✔
601
                            .into(),
1✔
602
                    )],
1✔
603
                    result_descriptor: RasterResultDescriptor {
1✔
604
                        data_type: RasterDataType::U8,
1✔
605
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
606
                        measurement: Measurement::Unitless,
1✔
607
                        time: None,
1✔
608
                        bbox: None,
1✔
609
                        resolution: None,
1✔
610
                    },
1✔
611
                },
1✔
612
            }
1✔
613
            .boxed(),
1✔
614
        ];
1✔
615

1✔
616
        let statistics = Statistics {
1✔
617
            params: StatisticsParams {
1✔
618
                column_names: vec![],
1✔
619
            },
1✔
620
            sources: raster_source.into(),
1✔
621
        };
1✔
622

1✔
623
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
624

625
        let statistics = statistics
1✔
626
            .boxed()
1✔
627
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
628
            .await
×
629
            .unwrap();
1✔
630

1✔
631
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
632

633
        let result = processor
1✔
634
            .plot_query(
1✔
635
                VectorQueryRectangle {
1✔
636
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
637
                        .unwrap(),
1✔
638
                    time_interval: TimeInterval::default(),
1✔
639
                    spatial_resolution: SpatialResolution::one(),
1✔
640
                },
1✔
641
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
642
            )
1✔
643
            .await
8,524✔
644
            .unwrap();
1✔
645

1✔
646
        assert_eq!(
1✔
647
            result.to_string(),
1✔
648
            json!({
1✔
649
                "Raster-1": {
1✔
650
                    "valueCount": 66_246, // 362*183 Note: this is caused by the inclusive nature of the bounding box. Since the right and lower bounds are included this wraps to a new row/column of tiles. In this test the tiles are 3x2 pixels in size.
1✔
651
                    "validCount": 6,
1✔
652
                    "min": 1.0,
1✔
653
                    "max": 6.0,
1✔
654
                    "mean": 3.5,
1✔
655
                    "stddev": 1.707_825_127_659_933
1✔
656
                },
1✔
657
                "Raster-2": {
1✔
658
                    "valueCount": 66_246, // 362*183 Note: this is caused by the inclusive nature of the bounding box. Since the right and lower bounds are included this wraps to a new row/column of tiles. In this test the tiles are 3x2 pixels in size.
1✔
659
                    "validCount": 6,
1✔
660
                    "min": 7.0,
1✔
661
                    "max": 12.0,
1✔
662
                    "mean": 9.5,
1✔
663
                    "stddev": 1.707_825_127_659_933
1✔
664
                },
1✔
665
            })
1✔
666
            .to_string()
1✔
667
        );
1✔
668
    }
669

670
    #[tokio::test]
1✔
671
    #[allow(clippy::too_many_lines)]
672
    async fn two_rasters_explicit_names() {
1✔
673
        let tile_size_in_pixels = [3, 2].into();
1✔
674
        let tiling_specification = TilingSpecification {
1✔
675
            origin_coordinate: [0.0, 0.0].into(),
1✔
676
            tile_size_in_pixels,
1✔
677
        };
1✔
678

1✔
679
        let raster_source = vec![
1✔
680
            MockRasterSource {
1✔
681
                params: MockRasterSourceParams {
1✔
682
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
683
                        TimeInterval::default(),
1✔
684
                        TileInformation {
1✔
685
                            global_geo_transform: TestDefault::test_default(),
1✔
686
                            global_tile_position: [0, 0].into(),
1✔
687
                            tile_size_in_pixels,
1✔
688
                        },
1✔
689
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
690
                            .unwrap()
1✔
691
                            .into(),
1✔
692
                    )],
1✔
693
                    result_descriptor: RasterResultDescriptor {
1✔
694
                        data_type: RasterDataType::U8,
1✔
695
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
696
                        measurement: Measurement::Unitless,
1✔
697
                        time: None,
1✔
698
                        bbox: None,
1✔
699
                        resolution: None,
1✔
700
                    },
1✔
701
                },
1✔
702
            }
1✔
703
            .boxed(),
1✔
704
            MockRasterSource {
1✔
705
                params: MockRasterSourceParams {
1✔
706
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
707
                        TimeInterval::default(),
1✔
708
                        TileInformation {
1✔
709
                            global_geo_transform: TestDefault::test_default(),
1✔
710
                            global_tile_position: [0, 0].into(),
1✔
711
                            tile_size_in_pixels,
1✔
712
                        },
1✔
713
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
714
                            .unwrap()
1✔
715
                            .into(),
1✔
716
                    )],
1✔
717
                    result_descriptor: RasterResultDescriptor {
1✔
718
                        data_type: RasterDataType::U8,
1✔
719
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
720
                        measurement: Measurement::Unitless,
1✔
721
                        time: None,
1✔
722
                        bbox: None,
1✔
723
                        resolution: None,
1✔
724
                    },
1✔
725
                },
1✔
726
            }
1✔
727
            .boxed(),
1✔
728
        ];
1✔
729

1✔
730
        let statistics = Statistics {
1✔
731
            params: StatisticsParams {
1✔
732
                column_names: vec!["A".to_string(), "B".to_string()],
1✔
733
            },
1✔
734
            sources: raster_source.into(),
1✔
735
        };
1✔
736

1✔
737
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
738

739
        let statistics = statistics
1✔
740
            .boxed()
1✔
741
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
742
            .await
×
743
            .unwrap();
1✔
744

1✔
745
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
746

747
        let result = processor
1✔
748
            .plot_query(
1✔
749
                VectorQueryRectangle {
1✔
750
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
751
                        .unwrap(),
1✔
752
                    time_interval: TimeInterval::default(),
1✔
753
                    spatial_resolution: SpatialResolution::one(),
1✔
754
                },
1✔
755
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
756
            )
1✔
757
            .await
8,031✔
758
            .unwrap();
1✔
759

1✔
760
        assert_eq!(
1✔
761
            result.to_string(),
1✔
762
            json!({
1✔
763
                "A": {
1✔
764
                    "valueCount": 66_246, // 362*183 Note: this is caused by the inclusive nature of the bounding box. Since the right and lower bounds are included this wraps to a new row/column of tiles. In this test the tiles are 3x2 pixels in size.
1✔
765
                    "validCount": 6,
1✔
766
                    "min": 1.0,
1✔
767
                    "max": 6.0,
1✔
768
                    "mean": 3.5,
1✔
769
                    "stddev": 1.707_825_127_659_933
1✔
770
                },
1✔
771
                "B": {
1✔
772
                    "valueCount": 66_246, // 362*183 Note: this is caused by the inclusive nature of the bounding box. Since the right and lower bounds are included this wraps to a new row/column of tiles. In this test the tiles are 3x2 pixels in size.
1✔
773
                    "validCount": 6,
1✔
774
                    "min": 7.0,
1✔
775
                    "max": 12.0,
1✔
776
                    "mean": 9.5,
1✔
777
                    "stddev": 1.707_825_127_659_933
1✔
778
                },
1✔
779
            })
1✔
780
            .to_string()
1✔
781
        );
1✔
782
    }
783

784
    #[tokio::test]
1✔
785
    async fn two_rasters_explicit_names_incomplete() {
1✔
786
        let tile_size_in_pixels = [3, 2].into();
1✔
787
        let tiling_specification = TilingSpecification {
1✔
788
            origin_coordinate: [0.0, 0.0].into(),
1✔
789
            tile_size_in_pixels,
1✔
790
        };
1✔
791

1✔
792
        let raster_source = vec![
1✔
793
            MockRasterSource {
1✔
794
                params: MockRasterSourceParams {
1✔
795
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
796
                        TimeInterval::default(),
1✔
797
                        TileInformation {
1✔
798
                            global_geo_transform: TestDefault::test_default(),
1✔
799
                            global_tile_position: [0, 0].into(),
1✔
800
                            tile_size_in_pixels,
1✔
801
                        },
1✔
802
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
803
                            .unwrap()
1✔
804
                            .into(),
1✔
805
                    )],
1✔
806
                    result_descriptor: RasterResultDescriptor {
1✔
807
                        data_type: RasterDataType::U8,
1✔
808
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
809
                        measurement: Measurement::Unitless,
1✔
810
                        time: None,
1✔
811
                        bbox: None,
1✔
812
                        resolution: None,
1✔
813
                    },
1✔
814
                },
1✔
815
            }
1✔
816
            .boxed(),
1✔
817
            MockRasterSource {
1✔
818
                params: MockRasterSourceParams {
1✔
819
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
820
                        TimeInterval::default(),
1✔
821
                        TileInformation {
1✔
822
                            global_geo_transform: TestDefault::test_default(),
1✔
823
                            global_tile_position: [0, 0].into(),
1✔
824
                            tile_size_in_pixels,
1✔
825
                        },
1✔
826
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
827
                            .unwrap()
1✔
828
                            .into(),
1✔
829
                    )],
1✔
830
                    result_descriptor: RasterResultDescriptor {
1✔
831
                        data_type: RasterDataType::U8,
1✔
832
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
833
                        measurement: Measurement::Unitless,
1✔
834
                        time: None,
1✔
835
                        bbox: None,
1✔
836
                        resolution: None,
1✔
837
                    },
1✔
838
                },
1✔
839
            }
1✔
840
            .boxed(),
1✔
841
        ];
1✔
842

1✔
843
        let statistics = Statistics {
1✔
844
            params: StatisticsParams {
1✔
845
                column_names: vec!["A".to_string()],
1✔
846
            },
1✔
847
            sources: raster_source.into(),
1✔
848
        };
1✔
849

1✔
850
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
851

852
        let statistics = statistics
1✔
853
            .boxed()
1✔
854
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
855
            .await;
×
856

857
        assert!(
1✔
858
            matches!(statistics, Err(error::Error::InvalidOperatorSpec{reason}) if reason == *"Statistics on raster data must either contain a name/alias for every input ('column_names' parameter) or no names at all.")
1✔
859
        );
860
    }
861

862
    #[tokio::test]
1✔
863
    async fn vector_no_column() {
1✔
864
        let tile_size_in_pixels = [3, 2].into();
1✔
865
        let tiling_specification = TilingSpecification {
1✔
866
            origin_coordinate: [0.0, 0.0].into(),
1✔
867
            tile_size_in_pixels,
1✔
868
        };
1✔
869

1✔
870
        let vector_source =
1✔
871
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
872
                &[] as &[NoGeometry],
1✔
873
                &[TimeInterval::default(); 7],
1✔
874
                &[
1✔
875
                    (
1✔
876
                        "foo",
1✔
877
                        FeatureData::NullableFloat(vec![
1✔
878
                            Some(1.0),
1✔
879
                            None,
1✔
880
                            Some(3.0),
1✔
881
                            None,
1✔
882
                            Some(f64::NAN),
1✔
883
                            Some(6.0),
1✔
884
                            Some(f64::NAN),
1✔
885
                        ]),
1✔
886
                    ),
1✔
887
                    (
1✔
888
                        "bar",
1✔
889
                        FeatureData::NullableFloat(vec![
1✔
890
                            Some(1.0),
1✔
891
                            Some(2.0),
1✔
892
                            None,
1✔
893
                            None,
1✔
894
                            Some(5.0),
1✔
895
                            Some(f64::NAN),
1✔
896
                            Some(f64::NAN),
1✔
897
                        ]),
1✔
898
                    ),
1✔
899
                ],
1✔
900
            )
1✔
901
            .unwrap()])
1✔
902
            .boxed();
1✔
903

1✔
904
        let statistics = Statistics {
1✔
905
            params: StatisticsParams {
1✔
906
                column_names: vec![],
1✔
907
            },
1✔
908
            sources: vector_source.into(),
1✔
909
        };
1✔
910

1✔
911
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
912

913
        let statistics = statistics
1✔
914
            .boxed()
1✔
915
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
916
            .await
×
917
            .unwrap();
1✔
918

1✔
919
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
920

921
        let result = processor
1✔
922
            .plot_query(
1✔
923
                VectorQueryRectangle {
1✔
924
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
925
                        .unwrap(),
1✔
926
                    time_interval: TimeInterval::default(),
1✔
927
                    spatial_resolution: SpatialResolution::one(),
1✔
928
                },
1✔
929
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
930
            )
1✔
931
            .await
×
932
            .unwrap();
1✔
933

1✔
934
        assert_eq!(
1✔
935
            result.to_string(),
1✔
936
            json!({
1✔
937
                "foo": {
1✔
938
                    "valueCount": 7,
1✔
939
                    "validCount": 3,
1✔
940
                    "min": 1.0,
1✔
941
                    "max": 6.0,
1✔
942
                    "mean": 3.333_333_333_333_333,
1✔
943
                    "stddev": 2.054_804_667_656_325_6
1✔
944
                },
1✔
945
                "bar": {
1✔
946
                    "valueCount": 7,
1✔
947
                    "validCount": 3,
1✔
948
                    "min": 1.0,
1✔
949
                    "max": 5.0,
1✔
950
                    "mean": 2.666_666_666_666_667,
1✔
951
                    "stddev": 1.699_673_171_197_595
1✔
952
                },
1✔
953
            })
1✔
954
            .to_string()
1✔
955
        );
1✔
956
    }
957

958
    #[tokio::test]
1✔
959
    async fn vector_single_column() {
1✔
960
        let tile_size_in_pixels = [3, 2].into();
1✔
961
        let tiling_specification = TilingSpecification {
1✔
962
            origin_coordinate: [0.0, 0.0].into(),
1✔
963
            tile_size_in_pixels,
1✔
964
        };
1✔
965

1✔
966
        let vector_source =
1✔
967
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
968
                &[] as &[NoGeometry],
1✔
969
                &[TimeInterval::default(); 7],
1✔
970
                &[
1✔
971
                    (
1✔
972
                        "foo",
1✔
973
                        FeatureData::NullableFloat(vec![
1✔
974
                            Some(1.0),
1✔
975
                            None,
1✔
976
                            Some(3.0),
1✔
977
                            None,
1✔
978
                            Some(f64::NAN),
1✔
979
                            Some(6.0),
1✔
980
                            Some(f64::NAN),
1✔
981
                        ]),
1✔
982
                    ),
1✔
983
                    (
1✔
984
                        "bar",
1✔
985
                        FeatureData::NullableFloat(vec![
1✔
986
                            Some(1.0),
1✔
987
                            Some(2.0),
1✔
988
                            None,
1✔
989
                            None,
1✔
990
                            Some(5.0),
1✔
991
                            Some(f64::NAN),
1✔
992
                            Some(f64::NAN),
1✔
993
                        ]),
1✔
994
                    ),
1✔
995
                ],
1✔
996
            )
1✔
997
            .unwrap()])
1✔
998
            .boxed();
1✔
999

1✔
1000
        let statistics = Statistics {
1✔
1001
            params: StatisticsParams {
1✔
1002
                column_names: vec!["foo".to_string()],
1✔
1003
            },
1✔
1004
            sources: vector_source.into(),
1✔
1005
        };
1✔
1006

1✔
1007
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1008

1009
        let statistics = statistics
1✔
1010
            .boxed()
1✔
1011
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1012
            .await
×
1013
            .unwrap();
1✔
1014

1✔
1015
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1016

1017
        let result = processor
1✔
1018
            .plot_query(
1✔
1019
                VectorQueryRectangle {
1✔
1020
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1021
                        .unwrap(),
1✔
1022
                    time_interval: TimeInterval::default(),
1✔
1023
                    spatial_resolution: SpatialResolution::one(),
1✔
1024
                },
1✔
1025
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1026
            )
1✔
1027
            .await
×
1028
            .unwrap();
1✔
1029

1✔
1030
        assert_eq!(
1✔
1031
            result.to_string(),
1✔
1032
            json!({
1✔
1033
                "foo": {
1✔
1034
                    "valueCount": 7,
1✔
1035
                    "validCount": 3,
1✔
1036
                    "min": 1.0,
1✔
1037
                    "max": 6.0,
1✔
1038
                    "mean": 3.333_333_333_333_333,
1✔
1039
                    "stddev": 2.054_804_667_656_325_6
1✔
1040
                },
1✔
1041
            })
1✔
1042
            .to_string()
1✔
1043
        );
1✔
1044
    }
1045

1046
    #[tokio::test]
1✔
1047
    async fn vector_two_columns() {
1✔
1048
        let tile_size_in_pixels = [3, 2].into();
1✔
1049
        let tiling_specification = TilingSpecification {
1✔
1050
            origin_coordinate: [0.0, 0.0].into(),
1✔
1051
            tile_size_in_pixels,
1✔
1052
        };
1✔
1053

1✔
1054
        let vector_source =
1✔
1055
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1056
                &[] as &[NoGeometry],
1✔
1057
                &[TimeInterval::default(); 7],
1✔
1058
                &[
1✔
1059
                    (
1✔
1060
                        "foo",
1✔
1061
                        FeatureData::NullableFloat(vec![
1✔
1062
                            Some(1.0),
1✔
1063
                            None,
1✔
1064
                            Some(3.0),
1✔
1065
                            None,
1✔
1066
                            Some(f64::NAN),
1✔
1067
                            Some(6.0),
1✔
1068
                            Some(f64::NAN),
1✔
1069
                        ]),
1✔
1070
                    ),
1✔
1071
                    (
1✔
1072
                        "bar",
1✔
1073
                        FeatureData::NullableFloat(vec![
1✔
1074
                            Some(1.0),
1✔
1075
                            Some(2.0),
1✔
1076
                            None,
1✔
1077
                            None,
1✔
1078
                            Some(5.0),
1✔
1079
                            Some(f64::NAN),
1✔
1080
                            Some(f64::NAN),
1✔
1081
                        ]),
1✔
1082
                    ),
1✔
1083
                ],
1✔
1084
            )
1✔
1085
            .unwrap()])
1✔
1086
            .boxed();
1✔
1087

1✔
1088
        let statistics = Statistics {
1✔
1089
            params: StatisticsParams {
1✔
1090
                column_names: vec!["foo".to_string(), "bar".to_string()],
1✔
1091
            },
1✔
1092
            sources: vector_source.into(),
1✔
1093
        };
1✔
1094

1✔
1095
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1096

1097
        let statistics = statistics
1✔
1098
            .boxed()
1✔
1099
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1100
            .await
×
1101
            .unwrap();
1✔
1102

1✔
1103
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1104

1105
        let result = processor
1✔
1106
            .plot_query(
1✔
1107
                VectorQueryRectangle {
1✔
1108
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1109
                        .unwrap(),
1✔
1110
                    time_interval: TimeInterval::default(),
1✔
1111
                    spatial_resolution: SpatialResolution::one(),
1✔
1112
                },
1✔
1113
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1114
            )
1✔
1115
            .await
×
1116
            .unwrap();
1✔
1117

1✔
1118
        assert_eq!(
1✔
1119
            result.to_string(),
1✔
1120
            json!({
1✔
1121
                "foo": {
1✔
1122
                    "valueCount": 7,
1✔
1123
                    "validCount": 3,
1✔
1124
                    "min": 1.0,
1✔
1125
                    "max": 6.0,
1✔
1126
                    "mean": 3.333_333_333_333_333,
1✔
1127
                    "stddev": 2.054_804_667_656_325_6
1✔
1128
                },
1✔
1129
                "bar": {
1✔
1130
                    "valueCount": 7,
1✔
1131
                    "validCount": 3,
1✔
1132
                    "min": 1.0,
1✔
1133
                    "max": 5.0,
1✔
1134
                    "mean": 2.666_666_666_666_667,
1✔
1135
                    "stddev": 1.699_673_171_197_595
1✔
1136
                },
1✔
1137
            })
1✔
1138
            .to_string()
1✔
1139
        );
1✔
1140
    }
1141
}
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