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

geo-engine / geoengine / 3929938005

pending completion
3929938005

push

github

GitHub
Merge #713

84930 of 96741 relevant lines covered (87.79%)

79640.1 hits per line

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

96.91
/operators/src/plot/statistics.rs
1
use crate::engine::{
2
    CreateSpan, ExecutionContext, InitializedPlotOperator, InitializedRasterOperator,
3
    InitializedVectorOperator, MultipleRasterOrSingleVectorSource, Operator, OperatorName,
4
    PlotOperator, PlotQueryProcessor, PlotResultDescriptor, QueryContext, QueryProcessor,
5
    TypedPlotQueryProcessor, TypedRasterQueryProcessor, TypedVectorQueryProcessor,
6
};
7
use crate::error;
8
use crate::error::Error;
9
use crate::util::input::MultiRasterOrVectorOperator::{Raster, Vector};
10
use crate::util::number_statistics::NumberStatistics;
11
use crate::util::Result;
12
use async_trait::async_trait;
13
use futures::future::join_all;
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
use tracing::{span, Level};
28

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

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

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

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

50
#[typetag::serde]
2✔
51
#[async_trait]
52
impl PlotOperator for Statistics {
53
    async fn _initialize(
10✔
54
        self: Box<Self>,
10✔
55
        context: &dyn ExecutionContext,
10✔
56
    ) -> Result<Box<dyn InitializedPlotOperator>> {
10✔
57
        match self.sources.source {
10✔
58
            Raster(rasters) => {
7✔
59
                ensure!( self.params.column_names.is_empty() || self.params.column_names.len() == rasters.len(),
7✔
60
                    error::InvalidOperatorSpec {
1✔
61
                        reason: "Statistics on raster data must either contain a name/alias for every input ('column_names' parameter) or no names at all."
1✔
62
                            .to_string(),
1✔
63
                });
1✔
64

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

73
                let rasters = join_all(rasters.into_iter().map(|s| s.initialize(context))).await;
6✔
74
                let rasters = rasters.into_iter().collect::<Result<Vec<_>>>()?;
6✔
75

76
                let in_descriptors = rasters
6✔
77
                    .iter()
6✔
78
                    .map(InitializedRasterOperator::result_descriptor)
6✔
79
                    .collect::<Vec<_>>();
6✔
80

6✔
81
                if rasters.len() > 1 {
6✔
82
                    let srs = in_descriptors[0].spatial_reference;
2✔
83
                    ensure!(
2✔
84
                        in_descriptors.iter().all(|d| d.spatial_reference == srs),
4✔
85
                        error::AllSourcesMustHaveSameSpatialReference
×
86
                    );
87
                }
4✔
88

89
                let time = time_interval_extent(in_descriptors.iter().map(|d| d.time));
6✔
90
                let bbox = partitions_extent(in_descriptors.iter().map(|d| d.bbox));
6✔
91

6✔
92
                let initialized_operator = InitializedStatistics::new(
6✔
93
                    PlotResultDescriptor {
6✔
94
                        spatial_reference: rasters.get(0).map_or_else(
6✔
95
                            || SpatialReferenceOption::Unreferenced,
6✔
96
                            |r| r.result_descriptor().spatial_reference,
6✔
97
                        ),
6✔
98
                        time,
6✔
99
                        bbox: bbox
6✔
100
                            .and_then(|p| BoundingBox2D::new(p.lower_left(), p.upper_right()).ok()),
6✔
101
                    },
6✔
102
                    output_names,
6✔
103
                    rasters,
6✔
104
                );
6✔
105

6✔
106
                Ok(initialized_operator.boxed())
6✔
107
            }
108
            Vector(vector) => {
3✔
109
                let initialized_vector = vector.initialize(context).await?;
3✔
110
                let in_descriptor = initialized_vector.result_descriptor();
3✔
111

112
                let column_names = if self.params.column_names.is_empty() {
3✔
113
                    in_descriptor
1✔
114
                        .columns
1✔
115
                        .clone()
1✔
116
                        .into_iter()
1✔
117
                        .filter(|(_, info)| info.data_type.is_numeric())
2✔
118
                        .map(|(name, _)| name)
2✔
119
                        .collect()
1✔
120
                } else {
121
                    for cn in &self.params.column_names {
5✔
122
                        match in_descriptor.column_data_type(cn.as_str()) {
3✔
123
                            Some(column) if !column.is_numeric() => {
3✔
124
                                return Err(Error::InvalidOperatorSpec {
×
125
                                    reason: format!("Column '{cn}' is not numeric."),
×
126
                                });
×
127
                            }
128
                            Some(_) => {
3✔
129
                                // OK
3✔
130
                            }
3✔
131
                            None => {
132
                                return Err(Error::ColumnDoesNotExist {
×
133
                                    column: cn.to_string(),
×
134
                                });
×
135
                            }
136
                        }
137
                    }
138
                    self.params.column_names.clone()
2✔
139
                };
140

141
                let initialized_operator = InitializedStatistics::new(
3✔
142
                    PlotResultDescriptor {
3✔
143
                        spatial_reference: in_descriptor.spatial_reference,
3✔
144
                        time: in_descriptor.time,
3✔
145
                        bbox: in_descriptor.bbox,
3✔
146
                    },
3✔
147
                    column_names,
3✔
148
                    initialized_vector,
3✔
149
                );
3✔
150

3✔
151
                Ok(initialized_operator.boxed())
3✔
152
            }
153
        }
154
    }
20✔
155

156
    span_fn!(Statistics);
×
157
}
158

159
/// The initialization of `Statistics`
160
pub struct InitializedStatistics<Op> {
161
    result_descriptor: PlotResultDescriptor,
162
    column_names: Vec<String>,
163
    source: Op,
164
}
165

166
impl<Op> InitializedStatistics<Op> {
167
    pub fn new(
9✔
168
        result_descriptor: PlotResultDescriptor,
9✔
169
        column_names: Vec<String>,
9✔
170
        source: Op,
9✔
171
    ) -> Self {
9✔
172
        Self {
9✔
173
            result_descriptor,
9✔
174
            column_names,
9✔
175
            source,
9✔
176
        }
9✔
177
    }
9✔
178
}
179

180
impl InitializedPlotOperator for InitializedStatistics<Box<dyn InitializedVectorOperator>> {
181
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
182
        &self.result_descriptor
×
183
    }
×
184

185
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
3✔
186
        Ok(TypedPlotQueryProcessor::JsonPlain(
3✔
187
            StatisticsVectorQueryProcessor {
3✔
188
                vector: self.source.query_processor()?,
3✔
189
                column_names: self.column_names.clone(),
3✔
190
            }
3✔
191
            .boxed(),
3✔
192
        ))
193
    }
3✔
194
}
195

196
impl InitializedPlotOperator for InitializedStatistics<Vec<Box<dyn InitializedRasterOperator>>> {
197
    fn result_descriptor(&self) -> &PlotResultDescriptor {
2✔
198
        &self.result_descriptor
2✔
199
    }
2✔
200

201
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
5✔
202
        Ok(TypedPlotQueryProcessor::JsonPlain(
5✔
203
            StatisticsRasterQueryProcessor {
5✔
204
                rasters: self
5✔
205
                    .source
5✔
206
                    .iter()
5✔
207
                    .map(InitializedRasterOperator::query_processor)
5✔
208
                    .collect::<Result<Vec<_>>>()?,
5✔
209
                column_names: self.column_names.clone(),
5✔
210
            }
5✔
211
            .boxed(),
5✔
212
        ))
213
    }
5✔
214
}
215

216
/// A query processor that calculates the statistics about its vector input.
217
pub struct StatisticsVectorQueryProcessor {
218
    vector: TypedVectorQueryProcessor,
219
    column_names: Vec<String>,
220
}
221

222
#[async_trait]
223
impl PlotQueryProcessor for StatisticsVectorQueryProcessor {
224
    type OutputFormat = serde_json::Value;
225

226
    fn plot_type(&self) -> &'static str {
×
227
        STATISTICS_OPERATOR_NAME
×
228
    }
×
229

230
    async fn plot_query<'a>(
3✔
231
        &'a self,
3✔
232
        query: PlotQueryRectangle,
3✔
233
        ctx: &'a dyn QueryContext,
3✔
234
    ) -> Result<Self::OutputFormat> {
3✔
235
        let mut number_statistics: HashMap<String, NumberStatistics> = self
3✔
236
            .column_names
3✔
237
            .iter()
3✔
238
            .map(|column| (column.clone(), NumberStatistics::default()))
5✔
239
            .collect();
3✔
240

241
        call_on_generic_vector_processor!(&self.vector, processor => {
3✔
242
            let mut query = processor.query(query, ctx).await?;
3✔
243

244
            while let Some(collection) = query.next().await {
6✔
245
                let collection = collection?;
3✔
246

247
                for (column, stats) in &mut number_statistics {
8✔
248
                    match collection.data(column) {
5✔
249
                        Ok(data) => data.float_options_iter().for_each(
5✔
250
                            | value | {
35✔
251
                                match value {
35✔
252
                                    Some(v) => stats.add(v),
25✔
253
                                    None => stats.add_no_data()
10✔
254
                                }
255
                            }
35✔
256
                        ),
5✔
257
                        Err(_) => stats.add_no_data_batch(collection.len())
×
258
                    }
259
                }
260
            }
261
        });
262

263
        let output: HashMap<String, StatisticsOutput> = number_statistics
3✔
264
            .iter()
3✔
265
            .map(|(column, number_statistics)| {
5✔
266
                (column.clone(), StatisticsOutput::from(number_statistics))
5✔
267
            })
5✔
268
            .collect();
3✔
269
        serde_json::to_value(output).map_err(Into::into)
3✔
270
    }
6✔
271
}
272

273
/// A query processor that calculates the statistics about its raster inputs.
274
pub struct StatisticsRasterQueryProcessor {
275
    rasters: Vec<TypedRasterQueryProcessor>,
276
    column_names: Vec<String>,
277
}
278

279
#[async_trait]
280
impl PlotQueryProcessor for StatisticsRasterQueryProcessor {
281
    type OutputFormat = serde_json::Value;
282

283
    fn plot_type(&self) -> &'static str {
1✔
284
        STATISTICS_OPERATOR_NAME
1✔
285
    }
1✔
286

287
    async fn plot_query<'a>(
5✔
288
        &'a self,
5✔
289
        query: VectorQueryRectangle,
5✔
290
        ctx: &'a dyn QueryContext,
5✔
291
    ) -> Result<Self::OutputFormat> {
5✔
292
        let mut queries = Vec::with_capacity(self.rasters.len());
5✔
293
        for (i, raster_processor) in self.rasters.iter().enumerate() {
6✔
294
            queries.push(
295
                call_on_generic_raster_processor!(raster_processor, processor => {
6✔
296
                    processor.query(query.into(), ctx).await?
6✔
297
                             .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))
54,001✔
298
                             .boxed()
6✔
299
                }),
300
            );
301
        }
302

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

5✔
305
        select_all(queries)
5✔
306
            .fold(
5✔
307
                Ok(number_statistics),
5✔
308
                |number_statistics: Result<Vec<NumberStatistics>>, enumerated_raster_tile| async move {
54,001✔
309
                    let mut number_statistics = number_statistics?;
54,001✔
310
                    let (i, raster_tile) = enumerated_raster_tile?;
54,001✔
311
                    match raster_tile.grid_array {
54,001✔
312
                        GridOrEmpty::Grid(g) => process_raster(&mut number_statistics[i], g.masked_element_deref_iterator()),
6✔
313
                        GridOrEmpty::Empty(n) => number_statistics[i].add_no_data_batch(n.number_of_elements())
53,995✔
314
                    }
315

316
                    Ok(number_statistics)
54,001✔
317
                },
54,001✔
318
            )
5✔
319
            .map(|number_statistics| {
5✔
320
                let output: HashMap<String, StatisticsOutput> = number_statistics?.iter().enumerate().map(|(i, stat)| (self.column_names[i].clone(), StatisticsOutput::from(stat))).collect();
6✔
321
                serde_json::to_value(output).map_err(Into::into)
5✔
322
            })
5✔
323
            .await
30,833✔
324
    }
10✔
325
}
326

327
fn process_raster<I>(number_statistics: &mut NumberStatistics, data: I)
6✔
328
where
6✔
329
    I: Iterator<Item = Option<f64>>,
6✔
330
{
6✔
331
    for value_option in data {
42✔
332
        if let Some(value) = value_option {
36✔
333
            number_statistics.add(value);
36✔
334
        } else {
36✔
335
            number_statistics.add_no_data();
×
336
        }
×
337
    }
338
}
6✔
339

340
/// The statistics summary output type for each raster input/vector input column
341
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
11✔
342
#[serde(rename_all = "camelCase")]
343
struct StatisticsOutput {
344
    pub value_count: usize,
345
    pub valid_count: usize,
346
    pub min: f64,
347
    pub max: f64,
348
    pub mean: f64,
349
    pub stddev: f64,
350
}
351

352
impl From<&NumberStatistics> for StatisticsOutput {
353
    fn from(number_statistics: &NumberStatistics) -> Self {
11✔
354
        Self {
11✔
355
            value_count: number_statistics.count() + number_statistics.nan_count(),
11✔
356
            valid_count: number_statistics.count(),
11✔
357
            min: number_statistics.min(),
11✔
358
            max: number_statistics.max(),
11✔
359
            mean: number_statistics.mean(),
11✔
360
            stddev: number_statistics.std_dev(),
11✔
361
        }
11✔
362
    }
11✔
363
}
364

365
#[cfg(test)]
366
mod tests {
367
    use geoengine_datatypes::collections::DataCollection;
368
    use geoengine_datatypes::util::test::TestDefault;
369
    use serde_json::json;
370

371
    use super::*;
372
    use crate::engine::VectorOperator;
373
    use crate::engine::{
374
        ChunkByteSize, MockExecutionContext, MockQueryContext, RasterOperator,
375
        RasterResultDescriptor,
376
    };
377
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
378
    use crate::util::input::MultiRasterOrVectorOperator::Raster;
379
    use geoengine_datatypes::primitives::{
380
        BoundingBox2D, FeatureData, Measurement, NoGeometry, SpatialResolution, TimeInterval,
381
    };
382
    use geoengine_datatypes::raster::{
383
        Grid2D, RasterDataType, RasterTile2D, TileInformation, TilingSpecification,
384
    };
385
    use geoengine_datatypes::spatial_reference::SpatialReference;
386

387
    #[test]
1✔
388
    fn serialization() {
1✔
389
        let statistics = Statistics {
1✔
390
            params: StatisticsParams {
1✔
391
                column_names: vec![],
1✔
392
            },
1✔
393
            sources: MultipleRasterOrSingleVectorSource {
1✔
394
                source: Raster(vec![]),
1✔
395
            },
1✔
396
        };
1✔
397

1✔
398
        let serialized = json!({
1✔
399
            "type": "Statistics",
1✔
400
            "params": {},
1✔
401
            "sources": {
1✔
402
                "source": [],
1✔
403
            },
1✔
404
        })
1✔
405
        .to_string();
1✔
406

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

1✔
409
        assert_eq!(deserialized.params, statistics.params);
1✔
410
    }
1✔
411

412
    #[tokio::test]
1✔
413
    async fn empty_raster_input() {
1✔
414
        let tile_size_in_pixels = [3, 2].into();
1✔
415
        let tiling_specification = TilingSpecification {
1✔
416
            origin_coordinate: [0.0, 0.0].into(),
1✔
417
            tile_size_in_pixels,
1✔
418
        };
1✔
419

1✔
420
        let statistics = Statistics {
1✔
421
            params: StatisticsParams {
1✔
422
                column_names: vec![],
1✔
423
            },
1✔
424
            sources: vec![].into(),
1✔
425
        };
1✔
426

1✔
427
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
428

429
        let statistics = statistics
1✔
430
            .boxed()
1✔
431
            .initialize(&execution_context)
1✔
432
            .await
×
433
            .unwrap();
1✔
434

1✔
435
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
436

437
        let result = processor
1✔
438
            .plot_query(
1✔
439
                VectorQueryRectangle {
1✔
440
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
441
                        .unwrap(),
1✔
442
                    time_interval: TimeInterval::default(),
1✔
443
                    spatial_resolution: SpatialResolution::one(),
1✔
444
                },
1✔
445
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
446
            )
1✔
447
            .await
×
448
            .unwrap();
1✔
449

1✔
450
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
451
    }
452

453
    #[tokio::test]
1✔
454
    async fn single_raster_implicit_name() {
1✔
455
        let tile_size_in_pixels = [3, 2].into();
1✔
456
        let tiling_specification = TilingSpecification {
1✔
457
            origin_coordinate: [0.0, 0.0].into(),
1✔
458
            tile_size_in_pixels,
1✔
459
        };
1✔
460

1✔
461
        let raster_source = MockRasterSource {
1✔
462
            params: MockRasterSourceParams {
1✔
463
                data: vec![RasterTile2D::new_with_tile_info(
1✔
464
                    TimeInterval::default(),
1✔
465
                    TileInformation {
1✔
466
                        global_geo_transform: TestDefault::test_default(),
1✔
467
                        global_tile_position: [0, 0].into(),
1✔
468
                        tile_size_in_pixels,
1✔
469
                    },
1✔
470
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
471
                        .unwrap()
1✔
472
                        .into(),
1✔
473
                )],
1✔
474
                result_descriptor: RasterResultDescriptor {
1✔
475
                    data_type: RasterDataType::U8,
1✔
476
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
477
                    measurement: Measurement::Unitless,
1✔
478
                    time: None,
1✔
479
                    bbox: None,
1✔
480
                    resolution: None,
1✔
481
                },
1✔
482
            },
1✔
483
        }
1✔
484
        .boxed();
1✔
485

1✔
486
        let statistics = Statistics {
1✔
487
            params: StatisticsParams {
1✔
488
                column_names: vec![],
1✔
489
            },
1✔
490
            sources: vec![raster_source].into(),
1✔
491
        };
1✔
492

1✔
493
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
494

495
        let statistics = statistics
1✔
496
            .boxed()
1✔
497
            .initialize(&execution_context)
1✔
498
            .await
×
499
            .unwrap();
1✔
500

1✔
501
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
502

503
        let result = processor
1✔
504
            .plot_query(
1✔
505
                VectorQueryRectangle {
1✔
506
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
507
                        .unwrap(),
1✔
508
                    time_interval: TimeInterval::default(),
1✔
509
                    spatial_resolution: SpatialResolution::one(),
1✔
510
                },
1✔
511
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
512
            )
1✔
513
            .await
12,691✔
514
            .unwrap();
1✔
515

1✔
516
        assert_eq!(
1✔
517
            result.to_string(),
1✔
518
            json!({
1✔
519
                "Raster-1": {
1✔
520
                    "valueCount": 64_800, // 360*180
1✔
521
                    "validCount": 6,
1✔
522
                    "min": 1.0,
1✔
523
                    "max": 6.0,
1✔
524
                    "mean": 3.5,
1✔
525
                    "stddev": 1.707_825_127_659_933,
1✔
526
                }
1✔
527
            })
1✔
528
            .to_string()
1✔
529
        );
1✔
530
    }
531

532
    #[tokio::test]
1✔
533
    #[allow(clippy::too_many_lines)]
534
    async fn two_rasters_implicit_names() {
1✔
535
        let tile_size_in_pixels = [3, 2].into();
1✔
536
        let tiling_specification = TilingSpecification {
1✔
537
            origin_coordinate: [0.0, 0.0].into(),
1✔
538
            tile_size_in_pixels,
1✔
539
        };
1✔
540

1✔
541
        let raster_source = vec![
1✔
542
            MockRasterSource {
1✔
543
                params: MockRasterSourceParams {
1✔
544
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
545
                        TimeInterval::default(),
1✔
546
                        TileInformation {
1✔
547
                            global_geo_transform: TestDefault::test_default(),
1✔
548
                            global_tile_position: [0, 0].into(),
1✔
549
                            tile_size_in_pixels,
1✔
550
                        },
1✔
551
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
552
                            .unwrap()
1✔
553
                            .into(),
1✔
554
                    )],
1✔
555
                    result_descriptor: RasterResultDescriptor {
1✔
556
                        data_type: RasterDataType::U8,
1✔
557
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
558
                        measurement: Measurement::Unitless,
1✔
559
                        time: None,
1✔
560
                        bbox: None,
1✔
561
                        resolution: None,
1✔
562
                    },
1✔
563
                },
1✔
564
            }
1✔
565
            .boxed(),
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![7, 8, 9, 10, 11, 12])
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
        ];
1✔
591

1✔
592
        let statistics = Statistics {
1✔
593
            params: StatisticsParams {
1✔
594
                column_names: vec![],
1✔
595
            },
1✔
596
            sources: raster_source.into(),
1✔
597
        };
1✔
598

1✔
599
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
600

601
        let statistics = statistics
1✔
602
            .boxed()
1✔
603
            .initialize(&execution_context)
1✔
604
            .await
×
605
            .unwrap();
1✔
606

1✔
607
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
608

609
        let result = processor
1✔
610
            .plot_query(
1✔
611
                VectorQueryRectangle {
1✔
612
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
613
                        .unwrap(),
1✔
614
                    time_interval: TimeInterval::default(),
1✔
615
                    spatial_resolution: SpatialResolution::one(),
1✔
616
                },
1✔
617
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
618
            )
1✔
619
            .await
8,693✔
620
            .unwrap();
1✔
621

1✔
622
        assert_eq!(
1✔
623
            result.to_string(),
1✔
624
            json!({
1✔
625
                "Raster-1": {
1✔
626
                    "valueCount": 64_800, // 360*180
1✔
627
                    "validCount": 6,
1✔
628
                    "min": 1.0,
1✔
629
                    "max": 6.0,
1✔
630
                    "mean": 3.5,
1✔
631
                    "stddev": 1.707_825_127_659_933
1✔
632
                },
1✔
633
                "Raster-2": {
1✔
634
                    "valueCount": 64_800, // 360*180
1✔
635
                    "validCount": 6,
1✔
636
                    "min": 7.0,
1✔
637
                    "max": 12.0,
1✔
638
                    "mean": 9.5,
1✔
639
                    "stddev": 1.707_825_127_659_933
1✔
640
                },
1✔
641
            })
1✔
642
            .to_string()
1✔
643
        );
1✔
644
    }
645

646
    #[tokio::test]
1✔
647
    #[allow(clippy::too_many_lines)]
648
    async fn two_rasters_explicit_names() {
1✔
649
        let tile_size_in_pixels = [3, 2].into();
1✔
650
        let tiling_specification = TilingSpecification {
1✔
651
            origin_coordinate: [0.0, 0.0].into(),
1✔
652
            tile_size_in_pixels,
1✔
653
        };
1✔
654

1✔
655
        let raster_source = vec![
1✔
656
            MockRasterSource {
1✔
657
                params: MockRasterSourceParams {
1✔
658
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
659
                        TimeInterval::default(),
1✔
660
                        TileInformation {
1✔
661
                            global_geo_transform: TestDefault::test_default(),
1✔
662
                            global_tile_position: [0, 0].into(),
1✔
663
                            tile_size_in_pixels,
1✔
664
                        },
1✔
665
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
666
                            .unwrap()
1✔
667
                            .into(),
1✔
668
                    )],
1✔
669
                    result_descriptor: RasterResultDescriptor {
1✔
670
                        data_type: RasterDataType::U8,
1✔
671
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
672
                        measurement: Measurement::Unitless,
1✔
673
                        time: None,
1✔
674
                        bbox: None,
1✔
675
                        resolution: None,
1✔
676
                    },
1✔
677
                },
1✔
678
            }
1✔
679
            .boxed(),
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![7, 8, 9, 10, 11, 12])
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
        ];
1✔
705

1✔
706
        let statistics = Statistics {
1✔
707
            params: StatisticsParams {
1✔
708
                column_names: vec!["A".to_string(), "B".to_string()],
1✔
709
            },
1✔
710
            sources: raster_source.into(),
1✔
711
        };
1✔
712

1✔
713
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
714

715
        let statistics = statistics
1✔
716
            .boxed()
1✔
717
            .initialize(&execution_context)
1✔
718
            .await
×
719
            .unwrap();
1✔
720

1✔
721
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
722

723
        let result = processor
1✔
724
            .plot_query(
1✔
725
                VectorQueryRectangle {
1✔
726
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
727
                        .unwrap(),
1✔
728
                    time_interval: TimeInterval::default(),
1✔
729
                    spatial_resolution: SpatialResolution::one(),
1✔
730
                },
1✔
731
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
732
            )
1✔
733
            .await
9,447✔
734
            .unwrap();
1✔
735

1✔
736
        assert_eq!(
1✔
737
            result.to_string(),
1✔
738
            json!({
1✔
739
                "A": {
1✔
740
                    "valueCount": 64_800, // 360*180
1✔
741
                    "validCount": 6,
1✔
742
                    "min": 1.0,
1✔
743
                    "max": 6.0,
1✔
744
                    "mean": 3.5,
1✔
745
                    "stddev": 1.707_825_127_659_933
1✔
746
                },
1✔
747
                "B": {
1✔
748
                    "valueCount": 64_800, // 360*180
1✔
749
                    "validCount": 6,
1✔
750
                    "min": 7.0,
1✔
751
                    "max": 12.0,
1✔
752
                    "mean": 9.5,
1✔
753
                    "stddev": 1.707_825_127_659_933
1✔
754
                },
1✔
755
            })
1✔
756
            .to_string()
1✔
757
        );
1✔
758
    }
759

760
    #[tokio::test]
1✔
761
    async fn two_rasters_explicit_names_incomplete() {
1✔
762
        let tile_size_in_pixels = [3, 2].into();
1✔
763
        let tiling_specification = TilingSpecification {
1✔
764
            origin_coordinate: [0.0, 0.0].into(),
1✔
765
            tile_size_in_pixels,
1✔
766
        };
1✔
767

1✔
768
        let raster_source = vec![
1✔
769
            MockRasterSource {
1✔
770
                params: MockRasterSourceParams {
1✔
771
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
772
                        TimeInterval::default(),
1✔
773
                        TileInformation {
1✔
774
                            global_geo_transform: TestDefault::test_default(),
1✔
775
                            global_tile_position: [0, 0].into(),
1✔
776
                            tile_size_in_pixels,
1✔
777
                        },
1✔
778
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
779
                            .unwrap()
1✔
780
                            .into(),
1✔
781
                    )],
1✔
782
                    result_descriptor: RasterResultDescriptor {
1✔
783
                        data_type: RasterDataType::U8,
1✔
784
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
785
                        measurement: Measurement::Unitless,
1✔
786
                        time: None,
1✔
787
                        bbox: None,
1✔
788
                        resolution: None,
1✔
789
                    },
1✔
790
                },
1✔
791
            }
1✔
792
            .boxed(),
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![7, 8, 9, 10, 11, 12])
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
        ];
1✔
818

1✔
819
        let statistics = Statistics {
1✔
820
            params: StatisticsParams {
1✔
821
                column_names: vec!["A".to_string()],
1✔
822
            },
1✔
823
            sources: raster_source.into(),
1✔
824
        };
1✔
825

1✔
826
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
827

828
        let statistics = statistics.boxed().initialize(&execution_context).await;
1✔
829

830
        assert!(
1✔
831
            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✔
832
        );
833
    }
834

835
    #[tokio::test]
1✔
836
    async fn vector_no_column() {
1✔
837
        let tile_size_in_pixels = [3, 2].into();
1✔
838
        let tiling_specification = TilingSpecification {
1✔
839
            origin_coordinate: [0.0, 0.0].into(),
1✔
840
            tile_size_in_pixels,
1✔
841
        };
1✔
842

1✔
843
        let vector_source =
1✔
844
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
845
                &[] as &[NoGeometry],
1✔
846
                &[TimeInterval::default(); 7],
1✔
847
                &[
1✔
848
                    (
1✔
849
                        "foo",
1✔
850
                        FeatureData::NullableFloat(vec![
1✔
851
                            Some(1.0),
1✔
852
                            None,
1✔
853
                            Some(3.0),
1✔
854
                            None,
1✔
855
                            Some(f64::NAN),
1✔
856
                            Some(6.0),
1✔
857
                            Some(f64::NAN),
1✔
858
                        ]),
1✔
859
                    ),
1✔
860
                    (
1✔
861
                        "bar",
1✔
862
                        FeatureData::NullableFloat(vec![
1✔
863
                            Some(1.0),
1✔
864
                            Some(2.0),
1✔
865
                            None,
1✔
866
                            None,
1✔
867
                            Some(5.0),
1✔
868
                            Some(f64::NAN),
1✔
869
                            Some(f64::NAN),
1✔
870
                        ]),
1✔
871
                    ),
1✔
872
                ],
1✔
873
            )
1✔
874
            .unwrap()])
1✔
875
            .boxed();
1✔
876

1✔
877
        let statistics = Statistics {
1✔
878
            params: StatisticsParams {
1✔
879
                column_names: vec![],
1✔
880
            },
1✔
881
            sources: vector_source.into(),
1✔
882
        };
1✔
883

1✔
884
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
885

886
        let statistics = statistics
1✔
887
            .boxed()
1✔
888
            .initialize(&execution_context)
1✔
889
            .await
×
890
            .unwrap();
1✔
891

1✔
892
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
893

894
        let result = processor
1✔
895
            .plot_query(
1✔
896
                VectorQueryRectangle {
1✔
897
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
898
                        .unwrap(),
1✔
899
                    time_interval: TimeInterval::default(),
1✔
900
                    spatial_resolution: SpatialResolution::one(),
1✔
901
                },
1✔
902
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
903
            )
1✔
904
            .await
×
905
            .unwrap();
1✔
906

1✔
907
        assert_eq!(
1✔
908
            result.to_string(),
1✔
909
            json!({
1✔
910
                "foo": {
1✔
911
                    "valueCount": 7,
1✔
912
                    "validCount": 3,
1✔
913
                    "min": 1.0,
1✔
914
                    "max": 6.0,
1✔
915
                    "mean": 3.333_333_333_333_333,
1✔
916
                    "stddev": 2.054_804_667_656_325_6
1✔
917
                },
1✔
918
                "bar": {
1✔
919
                    "valueCount": 7,
1✔
920
                    "validCount": 3,
1✔
921
                    "min": 1.0,
1✔
922
                    "max": 5.0,
1✔
923
                    "mean": 2.666_666_666_666_667,
1✔
924
                    "stddev": 1.699_673_171_197_595
1✔
925
                },
1✔
926
            })
1✔
927
            .to_string()
1✔
928
        );
1✔
929
    }
930

931
    #[tokio::test]
1✔
932
    async fn vector_single_column() {
1✔
933
        let tile_size_in_pixels = [3, 2].into();
1✔
934
        let tiling_specification = TilingSpecification {
1✔
935
            origin_coordinate: [0.0, 0.0].into(),
1✔
936
            tile_size_in_pixels,
1✔
937
        };
1✔
938

1✔
939
        let vector_source =
1✔
940
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
941
                &[] as &[NoGeometry],
1✔
942
                &[TimeInterval::default(); 7],
1✔
943
                &[
1✔
944
                    (
1✔
945
                        "foo",
1✔
946
                        FeatureData::NullableFloat(vec![
1✔
947
                            Some(1.0),
1✔
948
                            None,
1✔
949
                            Some(3.0),
1✔
950
                            None,
1✔
951
                            Some(f64::NAN),
1✔
952
                            Some(6.0),
1✔
953
                            Some(f64::NAN),
1✔
954
                        ]),
1✔
955
                    ),
1✔
956
                    (
1✔
957
                        "bar",
1✔
958
                        FeatureData::NullableFloat(vec![
1✔
959
                            Some(1.0),
1✔
960
                            Some(2.0),
1✔
961
                            None,
1✔
962
                            None,
1✔
963
                            Some(5.0),
1✔
964
                            Some(f64::NAN),
1✔
965
                            Some(f64::NAN),
1✔
966
                        ]),
1✔
967
                    ),
1✔
968
                ],
1✔
969
            )
1✔
970
            .unwrap()])
1✔
971
            .boxed();
1✔
972

1✔
973
        let statistics = Statistics {
1✔
974
            params: StatisticsParams {
1✔
975
                column_names: vec!["foo".to_string()],
1✔
976
            },
1✔
977
            sources: vector_source.into(),
1✔
978
        };
1✔
979

1✔
980
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
981

982
        let statistics = statistics
1✔
983
            .boxed()
1✔
984
            .initialize(&execution_context)
1✔
985
            .await
×
986
            .unwrap();
1✔
987

1✔
988
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
989

990
        let result = processor
1✔
991
            .plot_query(
1✔
992
                VectorQueryRectangle {
1✔
993
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
994
                        .unwrap(),
1✔
995
                    time_interval: TimeInterval::default(),
1✔
996
                    spatial_resolution: SpatialResolution::one(),
1✔
997
                },
1✔
998
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
999
            )
1✔
1000
            .await
×
1001
            .unwrap();
1✔
1002

1✔
1003
        assert_eq!(
1✔
1004
            result.to_string(),
1✔
1005
            json!({
1✔
1006
                "foo": {
1✔
1007
                    "valueCount": 7,
1✔
1008
                    "validCount": 3,
1✔
1009
                    "min": 1.0,
1✔
1010
                    "max": 6.0,
1✔
1011
                    "mean": 3.333_333_333_333_333,
1✔
1012
                    "stddev": 2.054_804_667_656_325_6
1✔
1013
                },
1✔
1014
            })
1✔
1015
            .to_string()
1✔
1016
        );
1✔
1017
    }
1018

1019
    #[tokio::test]
1✔
1020
    async fn vector_two_columns() {
1✔
1021
        let tile_size_in_pixels = [3, 2].into();
1✔
1022
        let tiling_specification = TilingSpecification {
1✔
1023
            origin_coordinate: [0.0, 0.0].into(),
1✔
1024
            tile_size_in_pixels,
1✔
1025
        };
1✔
1026

1✔
1027
        let vector_source =
1✔
1028
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1029
                &[] as &[NoGeometry],
1✔
1030
                &[TimeInterval::default(); 7],
1✔
1031
                &[
1✔
1032
                    (
1✔
1033
                        "foo",
1✔
1034
                        FeatureData::NullableFloat(vec![
1✔
1035
                            Some(1.0),
1✔
1036
                            None,
1✔
1037
                            Some(3.0),
1✔
1038
                            None,
1✔
1039
                            Some(f64::NAN),
1✔
1040
                            Some(6.0),
1✔
1041
                            Some(f64::NAN),
1✔
1042
                        ]),
1✔
1043
                    ),
1✔
1044
                    (
1✔
1045
                        "bar",
1✔
1046
                        FeatureData::NullableFloat(vec![
1✔
1047
                            Some(1.0),
1✔
1048
                            Some(2.0),
1✔
1049
                            None,
1✔
1050
                            None,
1✔
1051
                            Some(5.0),
1✔
1052
                            Some(f64::NAN),
1✔
1053
                            Some(f64::NAN),
1✔
1054
                        ]),
1✔
1055
                    ),
1✔
1056
                ],
1✔
1057
            )
1✔
1058
            .unwrap()])
1✔
1059
            .boxed();
1✔
1060

1✔
1061
        let statistics = Statistics {
1✔
1062
            params: StatisticsParams {
1✔
1063
                column_names: vec!["foo".to_string(), "bar".to_string()],
1✔
1064
            },
1✔
1065
            sources: vector_source.into(),
1✔
1066
        };
1✔
1067

1✔
1068
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1069

1070
        let statistics = statistics
1✔
1071
            .boxed()
1✔
1072
            .initialize(&execution_context)
1✔
1073
            .await
×
1074
            .unwrap();
1✔
1075

1✔
1076
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1077

1078
        let result = processor
1✔
1079
            .plot_query(
1✔
1080
                VectorQueryRectangle {
1✔
1081
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1082
                        .unwrap(),
1✔
1083
                    time_interval: TimeInterval::default(),
1✔
1084
                    spatial_resolution: SpatialResolution::one(),
1✔
1085
                },
1✔
1086
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1087
            )
1✔
1088
            .await
×
1089
            .unwrap();
1✔
1090

1✔
1091
        assert_eq!(
1✔
1092
            result.to_string(),
1✔
1093
            json!({
1✔
1094
                "foo": {
1✔
1095
                    "valueCount": 7,
1✔
1096
                    "validCount": 3,
1✔
1097
                    "min": 1.0,
1✔
1098
                    "max": 6.0,
1✔
1099
                    "mean": 3.333_333_333_333_333,
1✔
1100
                    "stddev": 2.054_804_667_656_325_6
1✔
1101
                },
1✔
1102
                "bar": {
1✔
1103
                    "valueCount": 7,
1✔
1104
                    "validCount": 3,
1✔
1105
                    "min": 1.0,
1✔
1106
                    "max": 5.0,
1✔
1107
                    "mean": 2.666_666_666_666_667,
1✔
1108
                    "stddev": 1.699_673_171_197_595
1✔
1109
                },
1✔
1110
            })
1✔
1111
            .to_string()
1✔
1112
        );
1✔
1113
    }
1114
}
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