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

geo-engine / geoengine / 9364358145

04 Jun 2024 09:08AM UTC coverage: 90.582% (+0.01%) from 90.571%
9364358145

push

github

web-flow
Merge pull request #958 from geo-engine/percentile_statistics

add percentiles to statistics operator

315 of 325 new or added lines in 6 files covered. (96.92%)

16 existing lines in 12 files now uncovered.

131329 of 144984 relevant lines covered (90.58%)

53359.25 hits per line

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

97.09
/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::statistics::{SafePSquareQuantileEstimator, StatisticsError};
13
use crate::util::Result;
14
use async_trait::async_trait;
15
use futures::stream::select_all;
16
use futures::{FutureExt, StreamExt, TryFutureExt, TryStreamExt};
17
use geoengine_datatypes::collections::FeatureCollectionInfos;
18
use geoengine_datatypes::primitives::{
19
    partitions_extent, time_interval_extent, AxisAlignedRectangle, BandSelection, BoundingBox2D,
20
    PlotQueryRectangle, RasterQueryRectangle,
21
};
22
use geoengine_datatypes::raster::ConvertDataTypeParallel;
23
use geoengine_datatypes::raster::{GridOrEmpty, GridSize};
24
use geoengine_datatypes::spatial_reference::SpatialReferenceOption;
25
use num_traits::AsPrimitive;
26
use ordered_float::NotNan;
27
use serde::{Deserialize, Serialize};
28
use snafu::ensure;
29
use std::collections::HashMap;
30

31
pub const STATISTICS_OPERATOR_NAME: &str = "Statistics";
32

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

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

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

54
#[typetag::serde]
6✔
55
#[async_trait]
56
#[allow(clippy::too_many_lines)]
57
impl PlotOperator for Statistics {
58
    async fn _initialize(
13✔
59
        self: Box<Self>,
13✔
60
        path: WorkflowOperatorPath,
13✔
61
        context: &dyn ExecutionContext,
13✔
62
    ) -> Result<Box<dyn InitializedPlotOperator>> {
13✔
63
        let name = CanonicOperatorName::from(&self);
13✔
64

13✔
65
        ensure!(
13✔
66
            self.params.percentiles.len() <= 8,
13✔
NEW
67
            error::InvalidOperatorSpec {
×
NEW
68
                reason: "Only up to 8 percentiles can be computed at the same time.".to_string(),
×
NEW
69
            }
×
70
        );
71

72
        match self.sources.source {
13✔
73
            MultiRasterOrVectorOperator::Raster(rasters) => {
8✔
74
                ensure!( self.params.column_names.is_empty() || self.params.column_names.len() == rasters.len(),
8✔
75
                    error::InvalidOperatorSpec {
1✔
76
                        reason: "Statistics on raster data must either contain a name/alias for every input ('column_names' parameter) or no names at all."
1✔
77
                            .to_string(),
1✔
78
                });
1✔
79

80
                let output_names = if self.params.column_names.is_empty() {
7✔
81
                    (1..=rasters.len())
6✔
82
                        .map(|i| format!("Raster-{i}"))
6✔
83
                        .collect::<Vec<_>>()
6✔
84
                } else {
85
                    self.params.column_names.clone()
1✔
86
                };
87

88
                let rasters = futures::future::try_join_all(
7✔
89
                    rasters
7✔
90
                        .into_iter()
7✔
91
                        .enumerate()
7✔
92
                        .map(|(i, op)| op.initialize(path.clone_and_append(i as u8), context)),
7✔
93
                )
7✔
94
                .await?;
×
95

96
                let in_descriptors = rasters
7✔
97
                    .iter()
7✔
98
                    .map(InitializedRasterOperator::result_descriptor)
7✔
99
                    .collect::<Vec<_>>();
7✔
100

7✔
101
                // TODO: implement multi-band functionality and remove this check
7✔
102
                ensure!(
7✔
103
                    in_descriptors.iter().all(|r| r.bands.len() == 1),
7✔
104
                    crate::error::OperatorDoesNotSupportMultiBandsSourcesYet {
×
105
                        operator: Statistics::TYPE_NAME,
×
106
                    }
×
107
                );
108

109
                if rasters.len() > 1 {
7✔
110
                    let srs = in_descriptors[0].spatial_reference;
2✔
111
                    ensure!(
2✔
112
                        in_descriptors.iter().all(|d| d.spatial_reference == srs),
4✔
113
                        error::AllSourcesMustHaveSameSpatialReference
×
114
                    );
115
                }
5✔
116

117
                let time = time_interval_extent(in_descriptors.iter().map(|d| d.time));
7✔
118
                let bbox = partitions_extent(in_descriptors.iter().map(|d| d.bbox));
7✔
119

7✔
120
                let initialized_operator = InitializedStatistics::new(
7✔
121
                    name,
7✔
122
                    PlotResultDescriptor {
7✔
123
                        spatial_reference: rasters.first().map_or_else(
7✔
124
                            || SpatialReferenceOption::Unreferenced,
7✔
125
                            |r| r.result_descriptor().spatial_reference,
7✔
126
                        ),
7✔
127
                        time,
7✔
128
                        bbox: bbox
7✔
129
                            .and_then(|p| BoundingBox2D::new(p.lower_left(), p.upper_right()).ok()),
7✔
130
                    },
7✔
131
                    output_names,
7✔
132
                    self.params
7✔
133
                        .percentiles
7✔
134
                        .iter()
7✔
135
                        .map(|p| p.into_inner())
7✔
136
                        .collect(),
7✔
137
                    rasters,
7✔
138
                );
7✔
139

7✔
140
                Ok(initialized_operator.boxed())
7✔
141
            }
142
            MultiRasterOrVectorOperator::Vector(vector_source) => {
5✔
143
                let initialized_vector = vector_source
5✔
144
                    .initialize(path.clone_and_append(0), context)
5✔
145
                    .await?;
4✔
146

147
                let in_descriptor = initialized_vector.result_descriptor();
4✔
148

149
                let column_names = if self.params.column_names.is_empty() {
4✔
150
                    in_descriptor
1✔
151
                        .columns
1✔
152
                        .clone()
1✔
153
                        .into_iter()
1✔
154
                        .filter(|(_, info)| info.data_type.is_numeric())
2✔
155
                        .map(|(name, _)| name)
2✔
156
                        .collect()
1✔
157
                } else {
158
                    for cn in &self.params.column_names {
7✔
159
                        match in_descriptor.column_data_type(cn.as_str()) {
4✔
160
                            Some(column) if !column.is_numeric() => {
4✔
161
                                return Err(Error::InvalidOperatorSpec {
×
162
                                    reason: format!("Column '{cn}' is not numeric."),
×
163
                                });
×
164
                            }
165
                            Some(_) => {
4✔
166
                                // OK
4✔
167
                            }
4✔
168
                            None => {
169
                                return Err(Error::ColumnDoesNotExist {
×
170
                                    column: cn.to_string(),
×
171
                                });
×
172
                            }
173
                        }
174
                    }
175
                    self.params.column_names.clone()
3✔
176
                };
177

178
                let initialized_operator = InitializedStatistics::new(
4✔
179
                    name,
4✔
180
                    PlotResultDescriptor {
4✔
181
                        spatial_reference: in_descriptor.spatial_reference,
4✔
182
                        time: in_descriptor.time,
4✔
183
                        bbox: in_descriptor.bbox,
4✔
184
                    },
4✔
185
                    column_names,
4✔
186
                    self.params
4✔
187
                        .percentiles
4✔
188
                        .iter()
4✔
189
                        .map(|p| p.into_inner())
4✔
190
                        .collect(),
4✔
191
                    initialized_vector,
4✔
192
                );
4✔
193

4✔
194
                Ok(initialized_operator.boxed())
4✔
195
            }
196
        }
197
    }
39✔
198

199
    span_fn!(Statistics);
×
200
}
201

202
/// The initialization of `Statistics`
203
pub struct InitializedStatistics<Op> {
204
    name: CanonicOperatorName,
205
    result_descriptor: PlotResultDescriptor,
206
    column_names: Vec<String>,
207
    percentiles: Vec<f64>,
208
    source: Op,
209
}
210

211
impl<Op> InitializedStatistics<Op> {
212
    pub fn new(
11✔
213
        name: CanonicOperatorName,
11✔
214
        result_descriptor: PlotResultDescriptor,
11✔
215
        column_names: Vec<String>,
11✔
216
        percentiles: Vec<f64>,
11✔
217
        source: Op,
11✔
218
    ) -> Self {
11✔
219
        Self {
11✔
220
            name,
11✔
221
            result_descriptor,
11✔
222
            column_names,
11✔
223
            percentiles,
11✔
224
            source,
11✔
225
        }
11✔
226
    }
11✔
227
}
228

229
impl InitializedPlotOperator for InitializedStatistics<Box<dyn InitializedVectorOperator>> {
230
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
231
        &self.result_descriptor
×
232
    }
×
233

234
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
4✔
235
        Ok(TypedPlotQueryProcessor::JsonPlain(
4✔
236
            StatisticsVectorQueryProcessor {
4✔
237
                vector: self.source.query_processor()?,
4✔
238
                column_names: self.column_names.clone(),
4✔
239
                percentiles: self.percentiles.clone(),
4✔
240
            }
4✔
241
            .boxed(),
4✔
242
        ))
243
    }
4✔
244

245
    fn canonic_name(&self) -> CanonicOperatorName {
×
246
        self.name.clone()
×
247
    }
×
248
}
249

250
impl InitializedPlotOperator for InitializedStatistics<Vec<Box<dyn InitializedRasterOperator>>> {
251
    fn result_descriptor(&self) -> &PlotResultDescriptor {
2✔
252
        &self.result_descriptor
2✔
253
    }
2✔
254

255
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
6✔
256
        Ok(TypedPlotQueryProcessor::JsonPlain(
6✔
257
            StatisticsRasterQueryProcessor {
6✔
258
                rasters: self
6✔
259
                    .source
6✔
260
                    .iter()
6✔
261
                    .map(InitializedRasterOperator::query_processor)
6✔
262
                    .collect::<Result<Vec<_>>>()?,
6✔
263
                column_names: self.column_names.clone(),
6✔
264
                percentiles: self.percentiles.clone(),
6✔
265
            }
6✔
266
            .boxed(),
6✔
267
        ))
268
    }
6✔
269

270
    fn canonic_name(&self) -> CanonicOperatorName {
×
271
        self.name.clone()
×
272
    }
×
273
}
274

275
/// A query processor that calculates the statistics about its vector input.
276
pub struct StatisticsVectorQueryProcessor {
277
    vector: TypedVectorQueryProcessor,
278
    column_names: Vec<String>,
279
    percentiles: Vec<f64>,
280
}
281

282
#[async_trait]
283
impl PlotQueryProcessor for StatisticsVectorQueryProcessor {
284
    type OutputFormat = serde_json::Value;
285

286
    fn plot_type(&self) -> &'static str {
×
287
        STATISTICS_OPERATOR_NAME
×
288
    }
×
289

290
    async fn plot_query<'a>(
4✔
291
        &'a self,
4✔
292
        query: PlotQueryRectangle,
4✔
293
        ctx: &'a dyn QueryContext,
4✔
294
    ) -> Result<Self::OutputFormat> {
4✔
295
        let mut statistics: HashMap<String, StatisticsAggregator<f64>> = self
4✔
296
            .column_names
4✔
297
            .iter()
4✔
298
            .map(|column| {
6✔
299
                (
6✔
300
                    column.clone(),
6✔
301
                    StatisticsAggregator::with_percentiles(&self.percentiles),
6✔
302
                )
6✔
303
            })
6✔
304
            .collect();
4✔
305

306
        call_on_generic_vector_processor!(&self.vector, processor => {
4✔
307
            let mut query = processor.query(query.into(), ctx).await?;
4✔
308

309
            while let Some(collection) = query.next().await {
8✔
310
                let collection = collection?;
4✔
311

312
                for (column, stats) in &mut statistics {
10✔
313
                    match collection.data(column) {
6✔
314
                        Ok(data) => for value in data.float_options_iter(){
42✔
315
                                match value {
42✔
316
                                    Some(v) => stats.add(v)?,
30✔
317
                                    None => stats.add_no_data()
12✔
318
                                }
319

320
                            },
UNCOV
321
                        Err(_) => stats.add_no_data_batch(collection.len())
×
322
                    }
323
                }
324
            }
325
        });
326

327
        let output: HashMap<String, StatisticsOutput> = statistics
4✔
328
            .iter()
4✔
329
            .map(|(column, number_statistics)| {
6✔
330
                (column.clone(), StatisticsOutput::from(number_statistics))
6✔
331
            })
6✔
332
            .collect();
4✔
333
        serde_json::to_value(output).map_err(Into::into)
4✔
334
    }
12✔
335
}
336

337
/// A query processor that calculates the statistics about its raster inputs.
338
pub struct StatisticsRasterQueryProcessor {
339
    rasters: Vec<TypedRasterQueryProcessor>,
340
    column_names: Vec<String>,
341
    percentiles: Vec<f64>,
342
}
343

344
#[async_trait]
345
impl PlotQueryProcessor for StatisticsRasterQueryProcessor {
346
    type OutputFormat = serde_json::Value;
347

348
    fn plot_type(&self) -> &'static str {
1✔
349
        STATISTICS_OPERATOR_NAME
1✔
350
    }
1✔
351

352
    async fn plot_query<'a>(
6✔
353
        &'a self,
6✔
354
        query: PlotQueryRectangle,
6✔
355
        ctx: &'a dyn QueryContext,
6✔
356
    ) -> Result<Self::OutputFormat> {
6✔
357
        let mut queries = Vec::with_capacity(self.rasters.len());
6✔
358
        let q: RasterQueryRectangle =
6✔
359
            RasterQueryRectangle::from_qrect_and_bands(&query, BandSelection::first());
6✔
360
        for (i, raster_processor) in self.rasters.iter().enumerate() {
7✔
361
            queries.push(
7✔
362
                call_on_generic_raster_processor!(raster_processor, processor => {
7✔
363
                    processor.query(q.clone(), ctx).await?
7✔
364
                             .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))
66,250✔
365
                             .boxed()
7✔
366
                }),
367
            );
368
        }
369

370
        let statistics =
6✔
371
            vec![StatisticsAggregator::with_percentiles(&self.percentiles); self.rasters.len()];
6✔
372

6✔
373
        select_all(queries)
6✔
374
            .try_fold(
6✔
375
                statistics,
6✔
376
                |statistics: Vec<StatisticsAggregator<f64>>, enumerated_raster_tile| async move {
66,250✔
377
                    let mut statistics = statistics;
66,250✔
378

66,250✔
379
                    let (i, raster_tile) = enumerated_raster_tile;
66,250✔
380

66,250✔
381
                    match raster_tile.grid_array {
66,250✔
382
                        GridOrEmpty::Grid(g) => {
7✔
383
                            process_raster(&mut statistics[i], g.masked_element_deref_iterator())?;
7✔
384
                        }
385
                        GridOrEmpty::Empty(n) => {
66,243✔
386
                            statistics[i]
66,243✔
387
                                .number_statistics
66,243✔
388
                                .add_no_data_batch(n.number_of_elements());
66,243✔
389
                        }
66,243✔
390
                    }
391

392
                    Ok(statistics)
66,250✔
393
                },
132,500✔
394
            )
6✔
395
            .map(|number_statistics| {
6✔
396
                let output: HashMap<String, StatisticsOutput> = number_statistics?
6✔
397
                    .iter()
6✔
398
                    .enumerate()
6✔
399
                    .map(|(i, stat)| (self.column_names[i].clone(), StatisticsOutput::from(stat)))
7✔
400
                    .collect();
6✔
401
                serde_json::to_value(output).map_err(Into::into)
6✔
402
            })
6✔
403
            .await
19,940✔
404
    }
18✔
405
}
406

407
fn process_raster<I>(
7✔
408
    statistics: &mut StatisticsAggregator<f64>,
7✔
409
    data: I,
7✔
410
) -> Result<(), StatisticsError>
7✔
411
where
7✔
412
    I: Iterator<Item = Option<f64>>,
7✔
413
{
7✔
414
    for value_option in data {
49✔
415
        if let Some(value) = value_option {
42✔
416
            statistics.add(value)?;
42✔
UNCOV
417
        } else {
×
NEW
418
            statistics.add_no_data();
×
419
        }
×
420
    }
421

422
    Ok(())
7✔
423
}
7✔
424

425
#[derive(Debug, Default, Clone)]
2✔
426
struct StatisticsAggregator<T: AsPrimitive<f64>> {
427
    number_statistics: NumberStatistics,
428
    percentile_estimators: Vec<PercentileEstimator<T>>,
429
}
430

431
impl<T: AsPrimitive<f64>> StatisticsAggregator<T> {
432
    fn with_percentiles(percentiles: &[f64]) -> Self {
12✔
433
        Self {
12✔
434
            number_statistics: NumberStatistics::default(),
12✔
435
            percentile_estimators: percentiles
12✔
436
                .iter()
12✔
437
                .map(|p| PercentileEstimator::new(*p))
12✔
438
                .collect(),
12✔
439
        }
12✔
440
    }
12✔
441

442
    fn add(&mut self, value: T) -> Result<(), StatisticsError> {
72✔
443
        self.number_statistics.add(value);
72✔
444
        for estimator in &mut self.percentile_estimators {
94✔
445
            estimator.update(value)?;
22✔
446
        }
447

448
        Ok(())
72✔
449
    }
72✔
450

451
    fn add_no_data(&mut self) {
12✔
452
        self.number_statistics.add_no_data();
12✔
453
    }
12✔
454

NEW
455
    fn add_no_data_batch(&mut self, batch_size: usize) {
×
NEW
456
        self.number_statistics.add_no_data_batch(batch_size);
×
NEW
457
    }
×
458
}
459

NEW
460
#[derive(Debug, Clone)]
×
461
enum PercentileEstimator<T: AsPrimitive<f64>> {
462
    Unitialized(f64),
463
    Initialized(SafePSquareQuantileEstimator<T>),
464
}
465

466
impl<T: AsPrimitive<f64>> PercentileEstimator<T> {
467
    pub fn new(quantile: f64) -> Self {
4✔
468
        Self::Unitialized(quantile)
4✔
469
    }
4✔
470

471
    pub fn percentile_estimate(&self) -> Option<f64> {
4✔
472
        match self {
4✔
NEW
473
            Self::Unitialized(_) => None,
×
474
            Self::Initialized(estimator) => Some(estimator.quantile_estimate()),
4✔
475
        }
476
    }
4✔
477

478
    pub fn percentile_arg(&self) -> f64 {
4✔
479
        match self {
4✔
NEW
480
            Self::Unitialized(quantile) => *quantile,
×
481
            Self::Initialized(estimator) => estimator.quantile_arg(),
4✔
482
        }
483
    }
4✔
484

485
    pub fn update(&mut self, sample: T) -> Result<(), StatisticsError> {
22✔
486
        match self {
22✔
487
            Self::Unitialized(quantile) => {
4✔
488
                *self = Self::Initialized(SafePSquareQuantileEstimator::new(*quantile, sample)?);
4✔
489
            }
490
            Self::Initialized(estimator) => estimator.update(sample),
18✔
491
        }
492

493
        Ok(())
22✔
494
    }
22✔
495
}
496

497
/// The statistics summary output type for each raster input/vector input column
498
#[derive(Debug, Clone, Serialize, Deserialize)]
13✔
499
#[serde(rename_all = "camelCase")]
500
struct StatisticsOutput {
501
    pub value_count: usize,
502
    pub valid_count: usize,
503
    pub min: f64,
504
    pub max: f64,
505
    pub mean: f64,
506
    pub stddev: f64,
507
    pub percentiles: Vec<PercentileOutput>,
508
}
509

510
#[derive(Debug, Clone, Serialize, Deserialize)]
4✔
511
struct PercentileOutput {
512
    percentile: f64,
513
    value: f64,
514
}
515

516
impl From<&StatisticsAggregator<f64>> for StatisticsOutput {
517
    fn from(statistics: &StatisticsAggregator<f64>) -> Self {
13✔
518
        let number_statistics = statistics.number_statistics;
13✔
519
        Self {
13✔
520
            value_count: number_statistics.count() + number_statistics.nan_count(),
13✔
521
            valid_count: number_statistics.count(),
13✔
522
            min: number_statistics.min(),
13✔
523
            max: number_statistics.max(),
13✔
524
            mean: number_statistics.mean(),
13✔
525
            stddev: number_statistics.std_dev(),
13✔
526
            percentiles: statistics
13✔
527
                .percentile_estimators
13✔
528
                .iter()
13✔
529
                .map(|estimator| PercentileOutput {
13✔
530
                    percentile: estimator.percentile_arg(),
4✔
531
                    value: estimator.percentile_estimate().unwrap_or(f64::NAN),
4✔
532
                })
13✔
533
                .collect(),
13✔
534
        }
13✔
535
    }
13✔
536
}
537

538
#[cfg(test)]
539
mod tests {
540
    use geoengine_datatypes::collections::DataCollection;
541
    use geoengine_datatypes::primitives::{CacheHint, PlotSeriesSelection};
542
    use geoengine_datatypes::util::test::TestDefault;
543
    use serde_json::json;
544

545
    use super::*;
546
    use crate::engine::{
547
        ChunkByteSize, MockExecutionContext, MockQueryContext, RasterOperator,
548
        RasterResultDescriptor,
549
    };
550
    use crate::engine::{RasterBandDescriptors, VectorOperator};
551
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
552
    use crate::util::input::MultiRasterOrVectorOperator::Raster;
553
    use geoengine_datatypes::primitives::{
554
        BoundingBox2D, FeatureData, NoGeometry, SpatialResolution, TimeInterval,
555
    };
556
    use geoengine_datatypes::raster::{
557
        Grid2D, RasterDataType, RasterTile2D, TileInformation, TilingSpecification,
558
    };
559
    use geoengine_datatypes::spatial_reference::SpatialReference;
560

561
    #[test]
1✔
562
    fn serialization() {
1✔
563
        let statistics = Statistics {
1✔
564
            params: StatisticsParams {
1✔
565
                column_names: vec![],
1✔
566
                percentiles: vec![],
1✔
567
            },
1✔
568
            sources: MultipleRasterOrSingleVectorSource {
1✔
569
                source: Raster(vec![]),
1✔
570
            },
1✔
571
        };
1✔
572

1✔
573
        let serialized = json!({
1✔
574
            "type": "Statistics",
1✔
575
            "params": {},
1✔
576
            "sources": {
1✔
577
                "source": [],
1✔
578
            },
1✔
579
        })
1✔
580
        .to_string();
1✔
581

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

1✔
584
        assert_eq!(deserialized.params, statistics.params);
1✔
585
    }
1✔
586

587
    #[tokio::test]
1✔
588
    async fn empty_raster_input() {
1✔
589
        let tile_size_in_pixels = [3, 2].into();
1✔
590
        let tiling_specification = TilingSpecification {
1✔
591
            origin_coordinate: [0.0, 0.0].into(),
1✔
592
            tile_size_in_pixels,
1✔
593
        };
1✔
594

1✔
595
        let statistics = Statistics {
1✔
596
            params: StatisticsParams {
1✔
597
                column_names: vec![],
1✔
598
                percentiles: vec![],
1✔
599
            },
1✔
600
            sources: vec![].into(),
1✔
601
        };
1✔
602

1✔
603
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
604

1✔
605
        let statistics = statistics
1✔
606
            .boxed()
1✔
607
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
608
            .await
1✔
609
            .unwrap();
1✔
610

1✔
611
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
612

1✔
613
        let result = processor
1✔
614
            .plot_query(
1✔
615
                PlotQueryRectangle {
1✔
616
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
617
                        .unwrap(),
1✔
618
                    time_interval: TimeInterval::default(),
1✔
619
                    spatial_resolution: SpatialResolution::one(),
1✔
620
                    attributes: PlotSeriesSelection::all(),
1✔
621
                },
1✔
622
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
623
            )
1✔
624
            .await
1✔
625
            .unwrap();
1✔
626

1✔
627
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
628
    }
1✔
629

630
    #[tokio::test]
1✔
631
    async fn single_raster_implicit_name() {
1✔
632
        let tile_size_in_pixels = [3, 2].into();
1✔
633
        let tiling_specification = TilingSpecification {
1✔
634
            origin_coordinate: [0.0, 0.0].into(),
1✔
635
            tile_size_in_pixels,
1✔
636
        };
1✔
637

1✔
638
        let raster_source = MockRasterSource {
1✔
639
            params: MockRasterSourceParams {
1✔
640
                data: vec![RasterTile2D::new_with_tile_info(
1✔
641
                    TimeInterval::default(),
1✔
642
                    TileInformation {
1✔
643
                        global_geo_transform: TestDefault::test_default(),
1✔
644
                        global_tile_position: [0, 0].into(),
1✔
645
                        tile_size_in_pixels,
1✔
646
                    },
1✔
647
                    0,
1✔
648
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
649
                        .unwrap()
1✔
650
                        .into(),
1✔
651
                    CacheHint::default(),
1✔
652
                )],
1✔
653
                result_descriptor: RasterResultDescriptor {
1✔
654
                    data_type: RasterDataType::U8,
1✔
655
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
656
                    time: None,
1✔
657
                    bbox: None,
1✔
658
                    resolution: None,
1✔
659
                    bands: RasterBandDescriptors::new_single_band(),
1✔
660
                },
1✔
661
            },
1✔
662
        }
1✔
663
        .boxed();
1✔
664

1✔
665
        let statistics = Statistics {
1✔
666
            params: StatisticsParams {
1✔
667
                column_names: vec![],
1✔
668
                percentiles: vec![],
1✔
669
            },
1✔
670
            sources: vec![raster_source].into(),
1✔
671
        };
1✔
672

1✔
673
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
674

1✔
675
        let statistics = statistics
1✔
676
            .boxed()
1✔
677
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
678
            .await
1✔
679
            .unwrap();
1✔
680

1✔
681
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
682

1✔
683
        let result = processor
1✔
684
            .plot_query(
1✔
685
                PlotQueryRectangle {
1✔
686
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
687
                        .unwrap(),
1✔
688
                    time_interval: TimeInterval::default(),
1✔
689
                    spatial_resolution: SpatialResolution::one(),
1✔
690
                    attributes: PlotSeriesSelection::all(),
1✔
691
                },
1✔
692
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
693
            )
1✔
694
            .await
1,648✔
695
            .unwrap();
1✔
696

1✔
697
        assert_eq!(
1✔
698
            result.to_string(),
1✔
699
            json!({
1✔
700
                "Raster-1": {
1✔
701
                    "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✔
702
                    "validCount": 6,
1✔
703
                    "min": 1.0,
1✔
704
                    "max": 6.0,
1✔
705
                    "mean": 3.5,
1✔
706
                    "stddev": 1.707_825_127_659_933,
1✔
707
                    "percentiles": [],
1✔
708
                }
1✔
709
            })
1✔
710
            .to_string()
1✔
711
        );
1✔
712
    }
1✔
713

714
    #[tokio::test]
1✔
715
    #[allow(clippy::too_many_lines)]
716
    async fn two_rasters_implicit_names() {
1✔
717
        let tile_size_in_pixels = [3, 2].into();
1✔
718
        let tiling_specification = TilingSpecification {
1✔
719
            origin_coordinate: [0.0, 0.0].into(),
1✔
720
            tile_size_in_pixels,
1✔
721
        };
1✔
722

1✔
723
        let raster_source = vec![
1✔
724
            MockRasterSource {
1✔
725
                params: MockRasterSourceParams {
1✔
726
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
727
                        TimeInterval::default(),
1✔
728
                        TileInformation {
1✔
729
                            global_geo_transform: TestDefault::test_default(),
1✔
730
                            global_tile_position: [0, 0].into(),
1✔
731
                            tile_size_in_pixels,
1✔
732
                        },
1✔
733
                        0,
1✔
734
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
735
                            .unwrap()
1✔
736
                            .into(),
1✔
737
                        CacheHint::default(),
1✔
738
                    )],
1✔
739
                    result_descriptor: RasterResultDescriptor {
1✔
740
                        data_type: RasterDataType::U8,
1✔
741
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
742
                        time: None,
1✔
743
                        bbox: None,
1✔
744
                        resolution: None,
1✔
745
                        bands: RasterBandDescriptors::new_single_band(),
1✔
746
                    },
1✔
747
                },
1✔
748
            }
1✔
749
            .boxed(),
1✔
750
            MockRasterSource {
1✔
751
                params: MockRasterSourceParams {
1✔
752
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
753
                        TimeInterval::default(),
1✔
754
                        TileInformation {
1✔
755
                            global_geo_transform: TestDefault::test_default(),
1✔
756
                            global_tile_position: [0, 0].into(),
1✔
757
                            tile_size_in_pixels,
1✔
758
                        },
1✔
759
                        0,
1✔
760
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
761
                            .unwrap()
1✔
762
                            .into(),
1✔
763
                        CacheHint::default(),
1✔
764
                    )],
1✔
765
                    result_descriptor: RasterResultDescriptor {
1✔
766
                        data_type: RasterDataType::U8,
1✔
767
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
768
                        time: None,
1✔
769
                        bbox: None,
1✔
770
                        resolution: None,
1✔
771
                        bands: RasterBandDescriptors::new_single_band(),
1✔
772
                    },
1✔
773
                },
1✔
774
            }
1✔
775
            .boxed(),
1✔
776
        ];
1✔
777

1✔
778
        let statistics = Statistics {
1✔
779
            params: StatisticsParams {
1✔
780
                column_names: vec![],
1✔
781
                percentiles: vec![],
1✔
782
            },
1✔
783
            sources: raster_source.into(),
1✔
784
        };
1✔
785

1✔
786
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
787

1✔
788
        let statistics = statistics
1✔
789
            .boxed()
1✔
790
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
791
            .await
1✔
792
            .unwrap();
1✔
793

1✔
794
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
795

1✔
796
        let result = processor
1✔
797
            .plot_query(
1✔
798
                PlotQueryRectangle {
1✔
799
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
800
                        .unwrap(),
1✔
801
                    time_interval: TimeInterval::default(),
1✔
802
                    spatial_resolution: SpatialResolution::one(),
1✔
803
                    attributes: PlotSeriesSelection::all(),
1✔
804
                },
1✔
805
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
806
            )
1✔
807
            .await
4,801✔
808
            .unwrap();
1✔
809

1✔
810
        assert_eq!(
1✔
811
            result.to_string(),
1✔
812
            json!({
1✔
813
                "Raster-1": {
1✔
814
                    "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✔
815
                    "validCount": 6,
1✔
816
                    "min": 1.0,
1✔
817
                    "max": 6.0,
1✔
818
                    "mean": 3.5,
1✔
819
                    "stddev": 1.707_825_127_659_933,
1✔
820
                    "percentiles": [],
1✔
821
                },
1✔
822
                "Raster-2": {
1✔
823
                    "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✔
824
                    "validCount": 6,
1✔
825
                    "min": 7.0,
1✔
826
                    "max": 12.0,
1✔
827
                    "mean": 9.5,
1✔
828
                    "stddev": 1.707_825_127_659_933,
1✔
829
                    "percentiles": [],
1✔
830
                },
1✔
831
            })
1✔
832
            .to_string()
1✔
833
        );
1✔
834
    }
1✔
835

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

1✔
845
        let raster_source = vec![
1✔
846
            MockRasterSource {
1✔
847
                params: MockRasterSourceParams {
1✔
848
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
849
                        TimeInterval::default(),
1✔
850
                        TileInformation {
1✔
851
                            global_geo_transform: TestDefault::test_default(),
1✔
852
                            global_tile_position: [0, 0].into(),
1✔
853
                            tile_size_in_pixels,
1✔
854
                        },
1✔
855
                        0,
1✔
856
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
857
                            .unwrap()
1✔
858
                            .into(),
1✔
859
                        CacheHint::default(),
1✔
860
                    )],
1✔
861
                    result_descriptor: RasterResultDescriptor {
1✔
862
                        data_type: RasterDataType::U8,
1✔
863
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
864
                        time: None,
1✔
865
                        bbox: None,
1✔
866
                        resolution: None,
1✔
867
                        bands: RasterBandDescriptors::new_single_band(),
1✔
868
                    },
1✔
869
                },
1✔
870
            }
1✔
871
            .boxed(),
1✔
872
            MockRasterSource {
1✔
873
                params: MockRasterSourceParams {
1✔
874
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
875
                        TimeInterval::default(),
1✔
876
                        TileInformation {
1✔
877
                            global_geo_transform: TestDefault::test_default(),
1✔
878
                            global_tile_position: [0, 0].into(),
1✔
879
                            tile_size_in_pixels,
1✔
880
                        },
1✔
881
                        0,
1✔
882
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
883
                            .unwrap()
1✔
884
                            .into(),
1✔
885
                        CacheHint::default(),
1✔
886
                    )],
1✔
887
                    result_descriptor: RasterResultDescriptor {
1✔
888
                        data_type: RasterDataType::U8,
1✔
889
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
890
                        time: None,
1✔
891
                        bbox: None,
1✔
892
                        resolution: None,
1✔
893
                        bands: RasterBandDescriptors::new_single_band(),
1✔
894
                    },
1✔
895
                },
1✔
896
            }
1✔
897
            .boxed(),
1✔
898
        ];
1✔
899

1✔
900
        let statistics = Statistics {
1✔
901
            params: StatisticsParams {
1✔
902
                column_names: vec!["A".to_string(), "B".to_string()],
1✔
903
                percentiles: vec![],
1✔
904
            },
1✔
905
            sources: raster_source.into(),
1✔
906
        };
1✔
907

1✔
908
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
909

1✔
910
        let statistics = statistics
1✔
911
            .boxed()
1✔
912
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
913
            .await
1✔
914
            .unwrap();
1✔
915

1✔
916
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
917

1✔
918
        let result = processor
1✔
919
            .plot_query(
1✔
920
                PlotQueryRectangle {
1✔
921
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
922
                        .unwrap(),
1✔
923
                    time_interval: TimeInterval::default(),
1✔
924
                    spatial_resolution: SpatialResolution::one(),
1✔
925
                    attributes: PlotSeriesSelection::all(),
1✔
926
                },
1✔
927
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
928
            )
1✔
929
            .await
11,648✔
930
            .unwrap();
1✔
931

1✔
932
        assert_eq!(
1✔
933
            result.to_string(),
1✔
934
            json!({
1✔
935
                "A": {
1✔
936
                    "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✔
937
                    "validCount": 6,
1✔
938
                    "min": 1.0,
1✔
939
                    "max": 6.0,
1✔
940
                    "mean": 3.5,
1✔
941
                    "stddev": 1.707_825_127_659_933,
1✔
942
                    "percentiles": [],
1✔
943
                },
1✔
944
                "B": {
1✔
945
                    "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✔
946
                    "validCount": 6,
1✔
947
                    "min": 7.0,
1✔
948
                    "max": 12.0,
1✔
949
                    "mean": 9.5,
1✔
950
                    "stddev": 1.707_825_127_659_933,
1✔
951
                    "percentiles": [],
1✔
952
                },
1✔
953
            })
1✔
954
            .to_string()
1✔
955
        );
1✔
956
    }
1✔
957

958
    #[tokio::test]
1✔
959
    async fn two_rasters_explicit_names_incomplete() {
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 raster_source = vec![
1✔
967
            MockRasterSource {
1✔
968
                params: MockRasterSourceParams {
1✔
969
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
970
                        TimeInterval::default(),
1✔
971
                        TileInformation {
1✔
972
                            global_geo_transform: TestDefault::test_default(),
1✔
973
                            global_tile_position: [0, 0].into(),
1✔
974
                            tile_size_in_pixels,
1✔
975
                        },
1✔
976
                        0,
1✔
977
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
978
                            .unwrap()
1✔
979
                            .into(),
1✔
980
                        CacheHint::default(),
1✔
981
                    )],
1✔
982
                    result_descriptor: RasterResultDescriptor {
1✔
983
                        data_type: RasterDataType::U8,
1✔
984
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
985
                        time: None,
1✔
986
                        bbox: None,
1✔
987
                        resolution: None,
1✔
988
                        bands: RasterBandDescriptors::new_single_band(),
1✔
989
                    },
1✔
990
                },
1✔
991
            }
1✔
992
            .boxed(),
1✔
993
            MockRasterSource {
1✔
994
                params: MockRasterSourceParams {
1✔
995
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
996
                        TimeInterval::default(),
1✔
997
                        TileInformation {
1✔
998
                            global_geo_transform: TestDefault::test_default(),
1✔
999
                            global_tile_position: [0, 0].into(),
1✔
1000
                            tile_size_in_pixels,
1✔
1001
                        },
1✔
1002
                        0,
1✔
1003
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
1004
                            .unwrap()
1✔
1005
                            .into(),
1✔
1006
                        CacheHint::default(),
1✔
1007
                    )],
1✔
1008
                    result_descriptor: RasterResultDescriptor {
1✔
1009
                        data_type: RasterDataType::U8,
1✔
1010
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1011
                        time: None,
1✔
1012
                        bbox: None,
1✔
1013
                        resolution: None,
1✔
1014
                        bands: RasterBandDescriptors::new_single_band(),
1✔
1015
                    },
1✔
1016
                },
1✔
1017
            }
1✔
1018
            .boxed(),
1✔
1019
        ];
1✔
1020

1✔
1021
        let statistics = Statistics {
1✔
1022
            params: StatisticsParams {
1✔
1023
                column_names: vec!["A".to_string()],
1✔
1024
                percentiles: vec![],
1✔
1025
            },
1✔
1026
            sources: raster_source.into(),
1✔
1027
        };
1✔
1028

1✔
1029
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1030

1✔
1031
        let statistics = statistics
1✔
1032
            .boxed()
1✔
1033
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1034
            .await;
1✔
1035

1✔
1036
        assert!(
1✔
1037
            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✔
1038
        );
1✔
1039
    }
1✔
1040

1041
    #[tokio::test]
1✔
1042
    async fn vector_no_column() {
1✔
1043
        let tile_size_in_pixels = [3, 2].into();
1✔
1044
        let tiling_specification = TilingSpecification {
1✔
1045
            origin_coordinate: [0.0, 0.0].into(),
1✔
1046
            tile_size_in_pixels,
1✔
1047
        };
1✔
1048

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

1✔
1083
        let statistics = Statistics {
1✔
1084
            params: StatisticsParams {
1✔
1085
                column_names: vec![],
1✔
1086
                percentiles: vec![],
1✔
1087
            },
1✔
1088
            sources: vector_source.into(),
1✔
1089
        };
1✔
1090

1✔
1091
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1092

1✔
1093
        let statistics = statistics
1✔
1094
            .boxed()
1✔
1095
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1096
            .await
1✔
1097
            .unwrap();
1✔
1098

1✔
1099
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1100

1✔
1101
        let result = processor
1✔
1102
            .plot_query(
1✔
1103
                PlotQueryRectangle {
1✔
1104
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1105
                        .unwrap(),
1✔
1106
                    time_interval: TimeInterval::default(),
1✔
1107
                    spatial_resolution: SpatialResolution::one(),
1✔
1108
                    attributes: PlotSeriesSelection::all(),
1✔
1109
                },
1✔
1110
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1111
            )
1✔
1112
            .await
1✔
1113
            .unwrap();
1✔
1114

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

1141
    #[tokio::test]
1✔
1142
    async fn vector_single_column() {
1✔
1143
        let tile_size_in_pixels = [3, 2].into();
1✔
1144
        let tiling_specification = TilingSpecification {
1✔
1145
            origin_coordinate: [0.0, 0.0].into(),
1✔
1146
            tile_size_in_pixels,
1✔
1147
        };
1✔
1148

1✔
1149
        let vector_source =
1✔
1150
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1151
                &[] as &[NoGeometry],
1✔
1152
                &[TimeInterval::default(); 7],
1✔
1153
                &[
1✔
1154
                    (
1✔
1155
                        "foo",
1✔
1156
                        FeatureData::NullableFloat(vec![
1✔
1157
                            Some(1.0),
1✔
1158
                            None,
1✔
1159
                            Some(3.0),
1✔
1160
                            None,
1✔
1161
                            Some(f64::NAN),
1✔
1162
                            Some(6.0),
1✔
1163
                            Some(f64::NAN),
1✔
1164
                        ]),
1✔
1165
                    ),
1✔
1166
                    (
1✔
1167
                        "bar",
1✔
1168
                        FeatureData::NullableFloat(vec![
1✔
1169
                            Some(1.0),
1✔
1170
                            Some(2.0),
1✔
1171
                            None,
1✔
1172
                            None,
1✔
1173
                            Some(5.0),
1✔
1174
                            Some(f64::NAN),
1✔
1175
                            Some(f64::NAN),
1✔
1176
                        ]),
1✔
1177
                    ),
1✔
1178
                ],
1✔
1179
            )
1✔
1180
            .unwrap()])
1✔
1181
            .boxed();
1✔
1182

1✔
1183
        let statistics = Statistics {
1✔
1184
            params: StatisticsParams {
1✔
1185
                column_names: vec!["foo".to_string()],
1✔
1186
                percentiles: vec![],
1✔
1187
            },
1✔
1188
            sources: vector_source.into(),
1✔
1189
        };
1✔
1190

1✔
1191
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1192

1✔
1193
        let statistics = statistics
1✔
1194
            .boxed()
1✔
1195
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1196
            .await
1✔
1197
            .unwrap();
1✔
1198

1✔
1199
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1200

1✔
1201
        let result = processor
1✔
1202
            .plot_query(
1✔
1203
                PlotQueryRectangle {
1✔
1204
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1205
                        .unwrap(),
1✔
1206
                    time_interval: TimeInterval::default(),
1✔
1207
                    spatial_resolution: SpatialResolution::one(),
1✔
1208
                    attributes: PlotSeriesSelection::all(),
1✔
1209
                },
1✔
1210
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1211
            )
1✔
1212
            .await
1✔
1213
            .unwrap();
1✔
1214

1✔
1215
        assert_eq!(
1✔
1216
            result.to_string(),
1✔
1217
            json!({
1✔
1218
                "foo": {
1✔
1219
                    "valueCount": 7,
1✔
1220
                    "validCount": 3,
1✔
1221
                    "min": 1.0,
1✔
1222
                    "max": 6.0,
1✔
1223
                    "mean": 3.333_333_333_333_333,
1✔
1224
                    "stddev": 2.054_804_667_656_325_6,
1✔
1225
                    "percentiles": [],
1✔
1226
                },
1✔
1227
            })
1✔
1228
            .to_string()
1✔
1229
        );
1✔
1230
    }
1✔
1231

1232
    #[tokio::test]
1✔
1233
    async fn vector_two_columns() {
1✔
1234
        let tile_size_in_pixels = [3, 2].into();
1✔
1235
        let tiling_specification = TilingSpecification {
1✔
1236
            origin_coordinate: [0.0, 0.0].into(),
1✔
1237
            tile_size_in_pixels,
1✔
1238
        };
1✔
1239

1✔
1240
        let vector_source =
1✔
1241
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1242
                &[] as &[NoGeometry],
1✔
1243
                &[TimeInterval::default(); 7],
1✔
1244
                &[
1✔
1245
                    (
1✔
1246
                        "foo",
1✔
1247
                        FeatureData::NullableFloat(vec![
1✔
1248
                            Some(1.0),
1✔
1249
                            None,
1✔
1250
                            Some(3.0),
1✔
1251
                            None,
1✔
1252
                            Some(f64::NAN),
1✔
1253
                            Some(6.0),
1✔
1254
                            Some(f64::NAN),
1✔
1255
                        ]),
1✔
1256
                    ),
1✔
1257
                    (
1✔
1258
                        "bar",
1✔
1259
                        FeatureData::NullableFloat(vec![
1✔
1260
                            Some(1.0),
1✔
1261
                            Some(2.0),
1✔
1262
                            None,
1✔
1263
                            None,
1✔
1264
                            Some(5.0),
1✔
1265
                            Some(f64::NAN),
1✔
1266
                            Some(f64::NAN),
1✔
1267
                        ]),
1✔
1268
                    ),
1✔
1269
                ],
1✔
1270
            )
1✔
1271
            .unwrap()])
1✔
1272
            .boxed();
1✔
1273

1✔
1274
        let statistics = Statistics {
1✔
1275
            params: StatisticsParams {
1✔
1276
                column_names: vec!["foo".to_string(), "bar".to_string()],
1✔
1277
                percentiles: vec![],
1✔
1278
            },
1✔
1279
            sources: vector_source.into(),
1✔
1280
        };
1✔
1281

1✔
1282
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1283

1✔
1284
        let statistics = statistics
1✔
1285
            .boxed()
1✔
1286
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1287
            .await
1✔
1288
            .unwrap();
1✔
1289

1✔
1290
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1291

1✔
1292
        let result = processor
1✔
1293
            .plot_query(
1✔
1294
                PlotQueryRectangle {
1✔
1295
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1296
                        .unwrap(),
1✔
1297
                    time_interval: TimeInterval::default(),
1✔
1298
                    spatial_resolution: SpatialResolution::one(),
1✔
1299
                    attributes: PlotSeriesSelection::all(),
1✔
1300
                },
1✔
1301
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1302
            )
1✔
1303
            .await
1✔
1304
            .unwrap();
1✔
1305

1✔
1306
        assert_eq!(
1✔
1307
            result.to_string(),
1✔
1308
            json!({
1✔
1309
                "foo": {
1✔
1310
                    "valueCount": 7,
1✔
1311
                    "validCount": 3,
1✔
1312
                    "min": 1.0,
1✔
1313
                    "max": 6.0,
1✔
1314
                    "mean": 3.333_333_333_333_333,
1✔
1315
                    "stddev": 2.054_804_667_656_325_6,
1✔
1316
                    "percentiles": [],
1✔
1317
                },
1✔
1318
                "bar": {
1✔
1319
                    "valueCount": 7,
1✔
1320
                    "validCount": 3,
1✔
1321
                    "min": 1.0,
1✔
1322
                    "max": 5.0,
1✔
1323
                    "mean": 2.666_666_666_666_667,
1✔
1324
                    "stddev": 1.699_673_171_197_595,
1✔
1325
                    "percentiles": [],
1✔
1326
                },
1✔
1327
            })
1✔
1328
            .to_string()
1✔
1329
        );
1✔
1330
    }
1✔
1331

1332
    #[tokio::test]
1✔
1333
    async fn raster_percentile() {
1✔
1334
        let tile_size_in_pixels = [3, 2].into();
1✔
1335
        let tiling_specification = TilingSpecification {
1✔
1336
            origin_coordinate: [0.0, 0.0].into(),
1✔
1337
            tile_size_in_pixels,
1✔
1338
        };
1✔
1339

1✔
1340
        let raster_source = MockRasterSource {
1✔
1341
            params: MockRasterSourceParams {
1✔
1342
                data: vec![RasterTile2D::new_with_tile_info(
1✔
1343
                    TimeInterval::default(),
1✔
1344
                    TileInformation {
1✔
1345
                        global_geo_transform: TestDefault::test_default(),
1✔
1346
                        global_tile_position: [0, 0].into(),
1✔
1347
                        tile_size_in_pixels,
1✔
1348
                    },
1✔
1349
                    0,
1✔
1350
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1351
                        .unwrap()
1✔
1352
                        .into(),
1✔
1353
                    CacheHint::default(),
1✔
1354
                )],
1✔
1355
                result_descriptor: RasterResultDescriptor {
1✔
1356
                    data_type: RasterDataType::U8,
1✔
1357
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1358
                    time: None,
1✔
1359
                    bbox: None,
1✔
1360
                    resolution: None,
1✔
1361
                    bands: RasterBandDescriptors::new_single_band(),
1✔
1362
                },
1✔
1363
            },
1✔
1364
        }
1✔
1365
        .boxed();
1✔
1366

1✔
1367
        let statistics = Statistics {
1✔
1368
            params: StatisticsParams {
1✔
1369
                column_names: vec![],
1✔
1370
                percentiles: vec![NotNan::new(0.25).unwrap(), NotNan::new(0.75).unwrap()],
1✔
1371
            },
1✔
1372
            sources: vec![raster_source].into(),
1✔
1373
        };
1✔
1374

1✔
1375
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1376

1✔
1377
        let statistics = statistics
1✔
1378
            .boxed()
1✔
1379
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1380
            .await
1✔
1381
            .unwrap();
1✔
1382

1✔
1383
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1384

1✔
1385
        let result = processor
1✔
1386
            .plot_query(
1✔
1387
                PlotQueryRectangle {
1✔
1388
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1389
                        .unwrap(),
1✔
1390
                    time_interval: TimeInterval::default(),
1✔
1391
                    spatial_resolution: SpatialResolution::one(),
1✔
1392
                    attributes: PlotSeriesSelection::all(),
1✔
1393
                },
1✔
1394
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1395
            )
1✔
1396
            .await
1,835✔
1397
            .unwrap();
1✔
1398

1✔
1399
        assert_eq!(
1✔
1400
            result.to_string(),
1✔
1401
            json!({
1✔
1402
                "Raster-1": {
1✔
1403
                    "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✔
1404
                    "validCount": 6,
1✔
1405
                    "min": 1.0,
1✔
1406
                    "max": 6.0,
1✔
1407
                    "mean": 3.5,
1✔
1408
                    "stddev": 1.707_825_127_659_933,
1✔
1409
                    "percentiles": [
1✔
1410
                        {"percentile": 0.25, "value": 3.0},
1✔
1411
                        {"percentile": 0.75, "value": 3.0},
1✔
1412
                    ],
1✔
1413
                }
1✔
1414
            })
1✔
1415
            .to_string()
1✔
1416
        );
1✔
1417
    }
1✔
1418

1419
    #[tokio::test]
1✔
1420
    async fn vector_percentiles() {
1✔
1421
        let tile_size_in_pixels = [3, 2].into();
1✔
1422
        let tiling_specification = TilingSpecification {
1✔
1423
            origin_coordinate: [0.0, 0.0].into(),
1✔
1424
            tile_size_in_pixels,
1✔
1425
        };
1✔
1426

1✔
1427
        let vector_source =
1✔
1428
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1429
                &[] as &[NoGeometry],
1✔
1430
                &[TimeInterval::default(); 7],
1✔
1431
                &[
1✔
1432
                    (
1✔
1433
                        "foo",
1✔
1434
                        FeatureData::NullableFloat(vec![
1✔
1435
                            Some(1.0),
1✔
1436
                            None,
1✔
1437
                            Some(3.0),
1✔
1438
                            None,
1✔
1439
                            Some(f64::NAN),
1✔
1440
                            Some(6.0),
1✔
1441
                            Some(f64::NAN),
1✔
1442
                        ]),
1✔
1443
                    ),
1✔
1444
                    (
1✔
1445
                        "bar",
1✔
1446
                        FeatureData::NullableFloat(vec![
1✔
1447
                            Some(1.0),
1✔
1448
                            Some(2.0),
1✔
1449
                            None,
1✔
1450
                            None,
1✔
1451
                            Some(5.0),
1✔
1452
                            Some(f64::NAN),
1✔
1453
                            Some(f64::NAN),
1✔
1454
                        ]),
1✔
1455
                    ),
1✔
1456
                ],
1✔
1457
            )
1✔
1458
            .unwrap()])
1✔
1459
            .boxed();
1✔
1460

1✔
1461
        let statistics = Statistics {
1✔
1462
            params: StatisticsParams {
1✔
1463
                column_names: vec!["foo".to_string()],
1✔
1464
                percentiles: vec![NotNan::new(0.25).unwrap(), NotNan::new(0.75).unwrap()],
1✔
1465
            },
1✔
1466
            sources: vector_source.into(),
1✔
1467
        };
1✔
1468

1✔
1469
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1470

1✔
1471
        let statistics = statistics
1✔
1472
            .boxed()
1✔
1473
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1474
            .await
1✔
1475
            .unwrap();
1✔
1476

1✔
1477
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1478

1✔
1479
        let result = processor
1✔
1480
            .plot_query(
1✔
1481
                PlotQueryRectangle {
1✔
1482
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1483
                        .unwrap(),
1✔
1484
                    time_interval: TimeInterval::default(),
1✔
1485
                    spatial_resolution: SpatialResolution::one(),
1✔
1486
                    attributes: PlotSeriesSelection::all(),
1✔
1487
                },
1✔
1488
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1489
            )
1✔
1490
            .await
1✔
1491
            .unwrap();
1✔
1492

1✔
1493
        assert_eq!(
1✔
1494
            result.to_string(),
1✔
1495
            json!({
1✔
1496
                "foo": {
1✔
1497
                    "valueCount": 7,
1✔
1498
                    "validCount": 3,
1✔
1499
                    "min": 1.0,
1✔
1500
                    "max": 6.0,
1✔
1501
                    "mean": 3.333_333_333_333_333,
1✔
1502
                    "stddev": 2.054_804_667_656_325_6,
1✔
1503
                    "percentiles": [
1✔
1504
                        {"percentile": 0.25, "value": 1.0},
1✔
1505
                        {"percentile": 0.75, "value": 6.0},
1✔
1506
                    ],
1✔
1507
                },
1✔
1508
            })
1✔
1509
            .to_string()
1✔
1510
        );
1✔
1511
    }
1✔
1512
}
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