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

geo-engine / geoengine / 19241805651

10 Nov 2025 06:22PM UTC coverage: 88.832%. First build
19241805651

Pull #1083

github

web-flow
Merge dac631b93 into 113de40ca
Pull Request #1083: feat: new gdal source workflow optimization

6213 of 7052 new or added lines in 71 files covered. (88.1%)

116189 of 130797 relevant lines covered (88.83%)

496404.71 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

148
                let in_descriptor = initialized_vector.result_descriptor();
149

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

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

193
                Ok(initialized_operator.boxed())
194
            }
195
        }
196
    }
13✔
197

198
    span_fn!(Statistics);
199
}
200

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

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

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

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

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

NEW
248
    fn optimize(
×
NEW
249
        &self,
×
NEW
250
        target_resolution: SpatialResolution,
×
NEW
251
    ) -> Result<Box<dyn PlotOperator>, OptimizationError> {
×
252
        Ok(Statistics {
NEW
253
            params: StatisticsParams {
×
NEW
254
                column_names: self.column_names.clone(),
×
NEW
255
                percentiles: self
×
NEW
256
                    .percentiles
×
NEW
257
                    .iter()
×
NEW
258
                    .copied()
×
NEW
259
                    .map(NotNan::<f64>::new)
×
NEW
260
                    .collect::<Result<Vec<_>,_>>().expect("percentiles should be not nan because they are NotNan<f64> during initialization"),
×
NEW
261
            },
×
262
            sources: MultipleRasterOrSingleVectorSource {
263
                source: MultiRasterOrVectorOperator::Vector(
NEW
264
                    self.source.optimize(target_resolution)?,
×
265
                ),
266
            },
267
        }
NEW
268
        .boxed())
×
NEW
269
    }
×
270
}
271

272
impl InitializedPlotOperator for InitializedStatistics<Vec<Box<dyn InitializedRasterOperator>>> {
273
    fn result_descriptor(&self) -> &PlotResultDescriptor {
2✔
274
        &self.result_descriptor
2✔
275
    }
2✔
276

277
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
6✔
278
        Ok(TypedPlotQueryProcessor::JsonPlain(
279
            StatisticsRasterQueryProcessor {
280
                rasters: self
6✔
281
                    .source
6✔
282
                    .iter()
6✔
283
                    .map(InitializedRasterOperator::query_processor)
6✔
284
                    .collect::<Result<Vec<_>>>()?,
6✔
285
                column_names: self.column_names.clone(),
6✔
286
                percentiles: self.percentiles.clone(),
6✔
287
            }
288
            .boxed(),
6✔
289
        ))
290
    }
6✔
291

292
    fn canonic_name(&self) -> CanonicOperatorName {
×
293
        self.name.clone()
×
294
    }
×
295

NEW
296
    fn optimize(
×
NEW
297
        &self,
×
NEW
298
        target_resolution: SpatialResolution,
×
NEW
299
    ) -> Result<Box<dyn PlotOperator>, OptimizationError> {
×
300
        Ok(Statistics {
NEW
301
            params: StatisticsParams {
×
NEW
302
                column_names: self.column_names.clone(),
×
NEW
303
                percentiles: self
×
NEW
304
                    .percentiles
×
NEW
305
                    .iter()
×
NEW
306
                    .copied()
×
NEW
307
                    .map(NotNan::<f64>::new)
×
NEW
308
                    .collect::<Result<Vec<_>,_>>().expect("percentiles should be not nan because they are NotNan<f64> during initialization"),
×
NEW
309
            },
×
310
            sources: MultipleRasterOrSingleVectorSource {
311
                source: MultiRasterOrVectorOperator::Raster(
NEW
312
                    self.source
×
NEW
313
                        .iter()
×
NEW
314
                        .map(|s| s.optimize(target_resolution))
×
NEW
315
                        .collect::<Result<Vec<_>, OptimizationError>>()?,
×
316
                ),
317
            },
318
        }
NEW
319
        .boxed())
×
NEW
320
    }
×
321
}
322

323
/// A query processor that calculates the statistics about its vector input.
324
pub struct StatisticsVectorQueryProcessor {
325
    vector: TypedVectorQueryProcessor,
326
    column_names: Vec<String>,
327
    percentiles: Vec<f64>,
328
}
329

330
#[async_trait]
331
impl PlotQueryProcessor for StatisticsVectorQueryProcessor {
332
    type OutputFormat = serde_json::Value;
333

334
    fn plot_type(&self) -> &'static str {
×
335
        STATISTICS_OPERATOR_NAME
×
336
    }
×
337

338
    async fn plot_query<'a>(
339
        &'a self,
340
        query: PlotQueryRectangle,
341
        ctx: &'a dyn QueryContext,
342
    ) -> Result<Self::OutputFormat> {
4✔
343
        let mut statistics: HashMap<String, StatisticsAggregator<f64>> = self
344
            .column_names
345
            .iter()
346
            .map(|column| {
6✔
347
                (
6✔
348
                    column.clone(),
6✔
349
                    StatisticsAggregator::with_percentiles(&self.percentiles),
6✔
350
                )
6✔
351
            })
6✔
352
            .collect();
353

354
        let query = query.select_attributes(ColumnSelection::all());
355

356
        call_on_generic_vector_processor!(&self.vector, processor => {
357
            let mut query = processor.query(query, ctx).await?;
358

359
            while let Some(collection) = query.next().await {
360
                let collection = collection?;
361

362
                for (column, stats) in &mut statistics {
363
                    match collection.data(column) {
364
                        Ok(data) => for value in data.float_options_iter(){
365
                                match value {
366
                                    Some(v) => stats.add(v)?,
367
                                    None => stats.add_no_data()
368
                                }
369

370
                            },
371
                        Err(_) => stats.add_no_data_batch(collection.len())
372
                    }
373
                }
374
            }
375
        });
376

377
        let output: HashMap<String, StatisticsOutput> = statistics
378
            .iter()
379
            .map(|(column, number_statistics)| {
6✔
380
                (column.clone(), StatisticsOutput::from(number_statistics))
6✔
381
            })
6✔
382
            .collect();
383
        serde_json::to_value(output).map_err(Into::into)
384
    }
4✔
385
}
386

387
/// A query processor that calculates the statistics about its raster inputs.
388
pub struct StatisticsRasterQueryProcessor {
389
    rasters: Vec<TypedRasterQueryProcessor>,
390
    column_names: Vec<String>,
391
    percentiles: Vec<f64>,
392
}
393

394
#[async_trait]
395
impl PlotQueryProcessor for StatisticsRasterQueryProcessor {
396
    type OutputFormat = serde_json::Value;
397

398
    fn plot_type(&self) -> &'static str {
1✔
399
        STATISTICS_OPERATOR_NAME
1✔
400
    }
1✔
401

402
    async fn plot_query<'a>(
403
        &'a self,
404
        query: PlotQueryRectangle,
405
        ctx: &'a dyn QueryContext,
406
    ) -> Result<Self::OutputFormat> {
6✔
407
        let mut queries = Vec::with_capacity(self.rasters.len());
408
        for (i, raster_processor) in self.rasters.iter().enumerate() {
409
            let rd = raster_processor.result_descriptor();
410

411
            let raster_query_rect = RasterQueryRectangle::from_bounds_and_geo_transform(
412
                &query,
413
                BandSelection::first(),
414
                rd.tiling_grid_definition(ctx.tiling_specification())
415
                    .tiling_geo_transform(),
416
            );
417

418
            queries.push(
419
                call_on_generic_raster_processor!(raster_processor, processor => {
420
                    processor.query(raster_query_rect.clone(), ctx).await? // TODO: avoid cloning query?
421
                             .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,247✔
422
                             .boxed()
423
                }),
424
            );
425
        }
426

427
        let statistics =
428
            vec![StatisticsAggregator::with_percentiles(&self.percentiles); self.rasters.len()];
429

430
        select_all(queries)
431
            .try_fold(
432
                statistics,
433
                |statistics: Vec<StatisticsAggregator<f64>>, enumerated_raster_tile| async move {
66,247✔
434
                    let mut statistics = statistics;
66,247✔
435

436
                    let (i, raster_tile) = enumerated_raster_tile;
66,247✔
437

438
                    match raster_tile.grid_array {
66,247✔
439
                        GridOrEmpty::Grid(g) => {
7✔
440
                            process_raster(&mut statistics[i], g.masked_element_deref_iterator())?;
7✔
441
                        }
442
                        GridOrEmpty::Empty(n) => {
66,240✔
443
                            statistics[i]
66,240✔
444
                                .number_statistics
66,240✔
445
                                .add_no_data_batch(n.number_of_elements());
66,240✔
446
                        }
66,240✔
447
                    }
448

449
                    Ok(statistics)
66,247✔
450
                },
132,494✔
451
            )
452
            .map(|number_statistics| {
6✔
453
                let output: HashMap<String, StatisticsOutput> = number_statistics?
6✔
454
                    .iter()
6✔
455
                    .enumerate()
6✔
456
                    .map(|(i, stat)| (self.column_names[i].clone(), StatisticsOutput::from(stat)))
7✔
457
                    .collect();
6✔
458
                serde_json::to_value(output).map_err(Into::into)
6✔
459
            })
6✔
460
            .await
461
    }
6✔
462
}
463

464
fn process_raster<I>(
7✔
465
    statistics: &mut StatisticsAggregator<f64>,
7✔
466
    data: I,
7✔
467
) -> Result<(), StatisticsError>
7✔
468
where
7✔
469
    I: Iterator<Item = Option<f64>>,
7✔
470
{
471
    for value_option in data {
49✔
472
        if let Some(value) = value_option {
42✔
473
            statistics.add(value)?;
42✔
474
        } else {
×
475
            statistics.add_no_data();
×
476
        }
×
477
    }
478

479
    Ok(())
7✔
480
}
7✔
481

482
#[derive(Debug, Default, Clone)]
483
struct StatisticsAggregator<T: AsPrimitive<f64>> {
484
    number_statistics: NumberStatistics,
485
    percentile_estimators: Vec<PercentileEstimator<T>>,
486
}
487

488
impl<T: AsPrimitive<f64>> StatisticsAggregator<T> {
489
    fn with_percentiles(percentiles: &[f64]) -> Self {
12✔
490
        Self {
491
            number_statistics: NumberStatistics::default(),
12✔
492
            percentile_estimators: percentiles
12✔
493
                .iter()
12✔
494
                .map(|p| PercentileEstimator::new(*p))
12✔
495
                .collect(),
12✔
496
        }
497
    }
12✔
498

499
    fn add(&mut self, value: T) -> Result<(), StatisticsError> {
72✔
500
        self.number_statistics.add(value);
72✔
501
        for estimator in &mut self.percentile_estimators {
94✔
502
            estimator.update(value)?;
22✔
503
        }
504

505
        Ok(())
72✔
506
    }
72✔
507

508
    fn add_no_data(&mut self) {
12✔
509
        self.number_statistics.add_no_data();
12✔
510
    }
12✔
511

512
    fn add_no_data_batch(&mut self, batch_size: usize) {
×
513
        self.number_statistics.add_no_data_batch(batch_size);
×
514
    }
×
515
}
516

517
#[derive(Debug, Clone)]
518
enum PercentileEstimator<T: AsPrimitive<f64>> {
519
    Unitialized(f64),
520
    Initialized(SafePSquareQuantileEstimator<T>),
521
}
522

523
impl<T: AsPrimitive<f64>> PercentileEstimator<T> {
524
    pub fn new(quantile: f64) -> Self {
4✔
525
        Self::Unitialized(quantile)
4✔
526
    }
4✔
527

528
    pub fn percentile_estimate(&self) -> Option<f64> {
4✔
529
        match self {
4✔
530
            Self::Unitialized(_) => None,
×
531
            Self::Initialized(estimator) => Some(estimator.quantile_estimate()),
4✔
532
        }
533
    }
4✔
534

535
    pub fn percentile_arg(&self) -> f64 {
4✔
536
        match self {
4✔
537
            Self::Unitialized(quantile) => *quantile,
×
538
            Self::Initialized(estimator) => estimator.quantile_arg(),
4✔
539
        }
540
    }
4✔
541

542
    pub fn update(&mut self, sample: T) -> Result<(), StatisticsError> {
22✔
543
        match self {
22✔
544
            Self::Unitialized(quantile) => {
4✔
545
                // initial sample must be finite, if the current sample is not, stay uninitialized
546
                if f64::is_finite(sample.as_()) {
4✔
547
                    *self =
548
                        Self::Initialized(SafePSquareQuantileEstimator::new(*quantile, sample)?);
4✔
549
                }
×
550
            }
551
            Self::Initialized(estimator) => estimator.update(sample),
18✔
552
        }
553

554
        Ok(())
22✔
555
    }
22✔
556
}
557

558
/// The statistics summary output type for each raster input/vector input column
559
#[derive(Debug, Clone, Serialize, Deserialize)]
560
#[serde(rename_all = "camelCase")]
561
struct StatisticsOutput {
562
    pub value_count: usize,
563
    pub valid_count: usize,
564
    pub min: f64,
565
    pub max: f64,
566
    pub mean: f64,
567
    pub stddev: f64,
568
    pub percentiles: Vec<PercentileOutput>,
569
}
570

571
#[derive(Debug, Clone, Serialize, Deserialize)]
572
struct PercentileOutput {
573
    percentile: f64,
574
    value: f64,
575
}
576

577
impl From<&StatisticsAggregator<f64>> for StatisticsOutput {
578
    fn from(statistics: &StatisticsAggregator<f64>) -> Self {
13✔
579
        let number_statistics = statistics.number_statistics;
13✔
580
        Self {
581
            value_count: number_statistics.count() + number_statistics.nan_count(),
13✔
582
            valid_count: number_statistics.count(),
13✔
583
            min: number_statistics.min(),
13✔
584
            max: number_statistics.max(),
13✔
585
            mean: number_statistics.mean(),
13✔
586
            stddev: number_statistics.std_dev(),
13✔
587
            percentiles: statistics
13✔
588
                .percentile_estimators
13✔
589
                .iter()
13✔
590
                .map(|estimator| PercentileOutput {
13✔
591
                    percentile: estimator.percentile_arg(),
4✔
592
                    value: estimator.percentile_estimate().unwrap_or(f64::NAN),
4✔
593
                })
4✔
594
                .collect(),
13✔
595
        }
596
    }
13✔
597
}
598

599
#[cfg(test)]
600
mod tests {
601
    use geoengine_datatypes::collections::DataCollection;
602
    use geoengine_datatypes::primitives::{CacheHint, Coordinate2D, PlotSeriesSelection};
603
    use geoengine_datatypes::util::test::TestDefault;
604
    use serde_json::json;
605

606
    use super::*;
607
    use crate::engine::{
608
        ChunkByteSize, MockExecutionContext, RasterOperator, RasterResultDescriptor,
609
        SpatialGridDescriptor, TimeDescriptor,
610
    };
611
    use crate::engine::{RasterBandDescriptors, VectorOperator};
612
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
613
    use crate::util::input::MultiRasterOrVectorOperator::Raster;
614
    use geoengine_datatypes::primitives::{BoundingBox2D, FeatureData, NoGeometry, TimeInterval};
615
    use geoengine_datatypes::raster::{
616
        BoundedGrid, GeoTransform, Grid2D, GridBoundingBox2D, GridShape2D, RasterDataType,
617
        RasterTile2D, TileInformation, TilingSpecification,
618
    };
619
    use geoengine_datatypes::spatial_reference::SpatialReference;
620

621
    #[test]
622
    fn serialization() {
1✔
623
        let statistics = Statistics {
1✔
624
            params: StatisticsParams {
1✔
625
                column_names: vec![],
1✔
626
                percentiles: vec![],
1✔
627
            },
1✔
628
            sources: MultipleRasterOrSingleVectorSource {
1✔
629
                source: Raster(vec![]),
1✔
630
            },
1✔
631
        };
1✔
632

633
        let serialized = json!({
1✔
634
            "type": "Statistics",
1✔
635
            "params": {},
1✔
636
            "sources": {
1✔
637
                "source": [],
1✔
638
            },
639
        })
640
        .to_string();
1✔
641

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

644
        assert_eq!(deserialized.params, statistics.params);
1✔
645
    }
1✔
646

647
    #[tokio::test]
648
    async fn empty_raster_input() {
1✔
649
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
650
        let tiling_specification = TilingSpecification {
1✔
651
            tile_size_in_pixels,
1✔
652
        };
1✔
653

654
        let statistics = Statistics {
1✔
655
            params: StatisticsParams {
1✔
656
                column_names: vec![],
1✔
657
                percentiles: vec![],
1✔
658
            },
1✔
659
            sources: vec![].into(),
1✔
660
        };
1✔
661

662
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
663

664
        let statistics = statistics
1✔
665
            .boxed()
1✔
666
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
667
            .await
1✔
668
            .unwrap();
1✔
669

670
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
671

672
        let result = processor
1✔
673
            .plot_query(
1✔
674
                PlotQueryRectangle::new(
1✔
675
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
676
                    TimeInterval::default(),
1✔
677
                    PlotSeriesSelection::all(),
1✔
678
                ),
1✔
679
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
680
            )
1✔
681
            .await
1✔
682
            .unwrap();
1✔
683

684
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
685
    }
1✔
686

687
    #[tokio::test]
688
    async fn single_raster_implicit_name() {
1✔
689
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
690
        let result_descriptor = RasterResultDescriptor {
1✔
691
            data_type: RasterDataType::U8,
1✔
692
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
693
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
694
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
695
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
696
                tile_size_in_pixels.bounding_box(),
1✔
697
            ),
1✔
698
            bands: RasterBandDescriptors::new_single_band(),
1✔
699
        };
1✔
700
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
701

702
        let raster_source = MockRasterSource {
1✔
703
            params: MockRasterSourceParams {
1✔
704
                data: vec![RasterTile2D::new_with_tile_info(
1✔
705
                    TimeInterval::default(),
1✔
706
                    TileInformation {
1✔
707
                        global_geo_transform: TestDefault::test_default(),
1✔
708
                        global_tile_position: [0, 0].into(),
1✔
709
                        tile_size_in_pixels,
1✔
710
                    },
1✔
711
                    0,
1✔
712
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
713
                        .unwrap()
1✔
714
                        .into(),
1✔
715
                    CacheHint::default(),
1✔
716
                )],
1✔
717
                result_descriptor,
1✔
718
            },
1✔
719
        }
1✔
720
        .boxed();
1✔
721

722
        let statistics = Statistics {
1✔
723
            params: StatisticsParams {
1✔
724
                column_names: vec![],
1✔
725
                percentiles: vec![],
1✔
726
            },
1✔
727
            sources: vec![raster_source].into(),
1✔
728
        };
1✔
729

730
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
731

732
        let statistics = statistics
1✔
733
            .boxed()
1✔
734
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
735
            .await
1✔
736
            .unwrap();
1✔
737

738
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
739

740
        let result = processor
1✔
741
            .plot_query(
1✔
742
                PlotQueryRectangle::new(
1✔
743
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
744
                    TimeInterval::default(),
1✔
745
                    PlotSeriesSelection::all(),
1✔
746
                ),
1✔
747
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
748
            )
1✔
749
            .await
1✔
750
            .unwrap();
1✔
751

752
        assert_eq!(
1✔
753
            result.to_string(),
1✔
754
            json!({
1✔
755
                "Raster-1": {
1✔
756
                    "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✔
757
                    "validCount": 6,
1✔
758
                    "min": 1.0,
1✔
759
                    "max": 6.0,
1✔
760
                    "mean": 3.5,
1✔
761
                    "stddev": 1.707_825_127_659_933,
1✔
762
                    "percentiles": [],
1✔
763
                }
1✔
764
            })
1✔
765
            .to_string()
1✔
766
        );
1✔
767
    }
1✔
768

769
    #[tokio::test]
770
    #[allow(clippy::too_many_lines)]
771
    async fn two_rasters_implicit_names() {
1✔
772
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
773
        let result_descriptor = RasterResultDescriptor {
1✔
774
            data_type: RasterDataType::U8,
1✔
775
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
776
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
777
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
778
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
779
                tile_size_in_pixels.bounding_box(),
1✔
780
            ),
1✔
781
            bands: RasterBandDescriptors::new_single_band(),
1✔
782
        };
1✔
783
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
784

785
        let raster_source = vec![
1✔
786
            MockRasterSource {
1✔
787
                params: MockRasterSourceParams {
1✔
788
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
789
                        TimeInterval::default(),
1✔
790
                        TileInformation {
1✔
791
                            global_geo_transform: TestDefault::test_default(),
1✔
792
                            global_tile_position: [0, 0].into(),
1✔
793
                            tile_size_in_pixels,
1✔
794
                        },
1✔
795
                        0,
1✔
796
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
797
                            .unwrap()
1✔
798
                            .into(),
1✔
799
                        CacheHint::default(),
1✔
800
                    )],
1✔
801
                    result_descriptor: result_descriptor.clone(),
1✔
802
                },
1✔
803
            }
1✔
804
            .boxed(),
1✔
805
            MockRasterSource {
1✔
806
                params: MockRasterSourceParams {
1✔
807
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
808
                        TimeInterval::default(),
1✔
809
                        TileInformation {
1✔
810
                            global_geo_transform: TestDefault::test_default(),
1✔
811
                            global_tile_position: [0, 0].into(),
1✔
812
                            tile_size_in_pixels,
1✔
813
                        },
1✔
814
                        0,
1✔
815
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
816
                            .unwrap()
1✔
817
                            .into(),
1✔
818
                        CacheHint::default(),
1✔
819
                    )],
1✔
820
                    result_descriptor,
1✔
821
                },
1✔
822
            }
1✔
823
            .boxed(),
1✔
824
        ];
825

826
        let statistics = Statistics {
1✔
827
            params: StatisticsParams {
1✔
828
                column_names: vec![],
1✔
829
                percentiles: vec![],
1✔
830
            },
1✔
831
            sources: raster_source.into(),
1✔
832
        };
1✔
833

834
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
835

836
        let statistics = statistics
1✔
837
            .boxed()
1✔
838
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
839
            .await
1✔
840
            .unwrap();
1✔
841

842
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
843

844
        let result = processor
1✔
845
            .plot_query(
1✔
846
                PlotQueryRectangle::new(
1✔
847
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
848
                    TimeInterval::default(),
1✔
849
                    PlotSeriesSelection::all(),
1✔
850
                ),
1✔
851
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
852
            )
1✔
853
            .await
1✔
854
            .unwrap();
1✔
855

856
        assert_eq!(
1✔
857
            result,
1✔
858
            json!({
1✔
859
                "Raster-1": {
1✔
860
                    "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✔
861
                    "validCount": 6,
1✔
862
                    "min": 1.0,
1✔
863
                    "max": 6.0,
1✔
864
                    "mean": 3.5,
1✔
865
                    "stddev": 1.707_825_127_659_933,
1✔
866
                    "percentiles": [],
1✔
867
                },
1✔
868
                "Raster-2": {
1✔
869
                    "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✔
870
                    "validCount": 6,
1✔
871
                    "min": 7.0,
1✔
872
                    "max": 12.0,
1✔
873
                    "mean": 9.5,
1✔
874
                    "stddev": 1.707_825_127_659_933,
1✔
875
                    "percentiles": [],
1✔
876
                },
1✔
877
            })
1✔
878
        );
1✔
879
    }
1✔
880

881
    #[tokio::test]
882
    #[allow(clippy::too_many_lines)]
883
    async fn two_rasters_explicit_names() {
1✔
884
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
885
        let result_descriptor = RasterResultDescriptor {
1✔
886
            data_type: RasterDataType::U8,
1✔
887
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
888
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
889
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
890
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
891
                tile_size_in_pixels.bounding_box(),
1✔
892
            ),
1✔
893
            bands: RasterBandDescriptors::new_single_band(),
1✔
894
        };
1✔
895
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
896

897
        let raster_source = vec![
1✔
898
            MockRasterSource {
1✔
899
                params: MockRasterSourceParams {
1✔
900
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
901
                        TimeInterval::default(),
1✔
902
                        TileInformation {
1✔
903
                            global_geo_transform: TestDefault::test_default(),
1✔
904
                            global_tile_position: [0, 0].into(),
1✔
905
                            tile_size_in_pixels,
1✔
906
                        },
1✔
907
                        0,
1✔
908
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
909
                            .unwrap()
1✔
910
                            .into(),
1✔
911
                        CacheHint::default(),
1✔
912
                    )],
1✔
913
                    result_descriptor: result_descriptor.clone(),
1✔
914
                },
1✔
915
            }
1✔
916
            .boxed(),
1✔
917
            MockRasterSource {
1✔
918
                params: MockRasterSourceParams {
1✔
919
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
920
                        TimeInterval::default(),
1✔
921
                        TileInformation {
1✔
922
                            global_geo_transform: TestDefault::test_default(),
1✔
923
                            global_tile_position: [0, 0].into(),
1✔
924
                            tile_size_in_pixels,
1✔
925
                        },
1✔
926
                        0,
1✔
927
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
928
                            .unwrap()
1✔
929
                            .into(),
1✔
930
                        CacheHint::default(),
1✔
931
                    )],
1✔
932
                    result_descriptor,
1✔
933
                },
1✔
934
            }
1✔
935
            .boxed(),
1✔
936
        ];
937

938
        let statistics = Statistics {
1✔
939
            params: StatisticsParams {
1✔
940
                column_names: vec!["A".to_string(), "B".to_string()],
1✔
941
                percentiles: vec![],
1✔
942
            },
1✔
943
            sources: raster_source.into(),
1✔
944
        };
1✔
945

946
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
947

948
        let statistics = statistics
1✔
949
            .boxed()
1✔
950
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
951
            .await
1✔
952
            .unwrap();
1✔
953

954
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
955

956
        let result = processor
1✔
957
            .plot_query(
1✔
958
                PlotQueryRectangle::new(
1✔
959
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
960
                    TimeInterval::default(),
1✔
961
                    PlotSeriesSelection::all(),
1✔
962
                ),
1✔
963
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
964
            )
1✔
965
            .await
1✔
966
            .unwrap();
1✔
967

968
        assert_eq!(
1✔
969
            result,
1✔
970
            json!({
1✔
971
                "A": {
1✔
972
                    "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✔
973
                    "validCount": 6,
1✔
974
                    "min": 1.0,
1✔
975
                    "max": 6.0,
1✔
976
                    "mean": 3.5,
1✔
977
                    "stddev": 1.707_825_127_659_933,
1✔
978
                    "percentiles": [],
1✔
979
                },
1✔
980
                "B": {
1✔
981
                    "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✔
982
                    "validCount": 6,
1✔
983
                    "min": 7.0,
1✔
984
                    "max": 12.0,
1✔
985
                    "mean": 9.5,
1✔
986
                    "stddev": 1.707_825_127_659_933,
1✔
987
                    "percentiles": [],
1✔
988
                },
1✔
989
            })
1✔
990
        );
1✔
991
    }
1✔
992

993
    #[tokio::test]
994
    async fn two_rasters_explicit_names_incomplete() {
1✔
995
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
996
        let result_descriptor = RasterResultDescriptor {
1✔
997
            data_type: RasterDataType::U8,
1✔
998
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
999
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
1000
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1001
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
1002
                tile_size_in_pixels.bounding_box(),
1✔
1003
            ),
1✔
1004
            bands: RasterBandDescriptors::new_single_band(),
1✔
1005
        };
1✔
1006
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
1007

1008
        let raster_source = vec![
1✔
1009
            MockRasterSource {
1✔
1010
                params: MockRasterSourceParams {
1✔
1011
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1012
                        TimeInterval::default(),
1✔
1013
                        TileInformation {
1✔
1014
                            global_geo_transform: TestDefault::test_default(),
1✔
1015
                            global_tile_position: [0, 0].into(),
1✔
1016
                            tile_size_in_pixels,
1✔
1017
                        },
1✔
1018
                        0,
1✔
1019
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1020
                            .unwrap()
1✔
1021
                            .into(),
1✔
1022
                        CacheHint::default(),
1✔
1023
                    )],
1✔
1024
                    result_descriptor: result_descriptor.clone(),
1✔
1025
                },
1✔
1026
            }
1✔
1027
            .boxed(),
1✔
1028
            MockRasterSource {
1✔
1029
                params: MockRasterSourceParams {
1✔
1030
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1031
                        TimeInterval::default(),
1✔
1032
                        TileInformation {
1✔
1033
                            global_geo_transform: TestDefault::test_default(),
1✔
1034
                            global_tile_position: [0, 0].into(),
1✔
1035
                            tile_size_in_pixels,
1✔
1036
                        },
1✔
1037
                        0,
1✔
1038
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
1039
                            .unwrap()
1✔
1040
                            .into(),
1✔
1041
                        CacheHint::default(),
1✔
1042
                    )],
1✔
1043
                    result_descriptor,
1✔
1044
                },
1✔
1045
            }
1✔
1046
            .boxed(),
1✔
1047
        ];
1048

1049
        let statistics = Statistics {
1✔
1050
            params: StatisticsParams {
1✔
1051
                column_names: vec!["A".to_string()],
1✔
1052
                percentiles: vec![],
1✔
1053
            },
1✔
1054
            sources: raster_source.into(),
1✔
1055
        };
1✔
1056

1057
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1058

1059
        let statistics = statistics
1✔
1060
            .boxed()
1✔
1061
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1062
            .await;
1✔
1063

1064
        assert!(
1✔
1065
            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✔
1066
        );
1✔
1067
    }
1✔
1068

1069
    #[tokio::test]
1070
    async fn vector_no_column() {
1✔
1071
        let tile_size_in_pixels = [3, 2].into();
1✔
1072
        let tiling_specification = TilingSpecification {
1✔
1073
            tile_size_in_pixels,
1✔
1074
        };
1✔
1075

1076
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
1077
            DataCollection::from_slices(
1✔
1078
                &[] as &[NoGeometry],
1✔
1079
                &[TimeInterval::default(); 7],
1✔
1080
                &[
1✔
1081
                    (
1✔
1082
                        "foo",
1✔
1083
                        FeatureData::NullableFloat(vec![
1✔
1084
                            Some(1.0),
1✔
1085
                            None,
1✔
1086
                            Some(3.0),
1✔
1087
                            None,
1✔
1088
                            Some(f64::NAN),
1✔
1089
                            Some(6.0),
1✔
1090
                            Some(f64::NAN),
1✔
1091
                        ]),
1✔
1092
                    ),
1✔
1093
                    (
1✔
1094
                        "bar",
1✔
1095
                        FeatureData::NullableFloat(vec![
1✔
1096
                            Some(1.0),
1✔
1097
                            Some(2.0),
1✔
1098
                            None,
1✔
1099
                            None,
1✔
1100
                            Some(5.0),
1✔
1101
                            Some(f64::NAN),
1✔
1102
                            Some(f64::NAN),
1✔
1103
                        ]),
1✔
1104
                    ),
1✔
1105
                ],
1✔
1106
            )
1107
            .unwrap(),
1✔
1108
        ])
1109
        .boxed();
1✔
1110

1111
        let statistics = Statistics {
1✔
1112
            params: StatisticsParams {
1✔
1113
                column_names: vec![],
1✔
1114
                percentiles: vec![],
1✔
1115
            },
1✔
1116
            sources: vector_source.into(),
1✔
1117
        };
1✔
1118

1119
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1120

1121
        let statistics = statistics
1✔
1122
            .boxed()
1✔
1123
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1124
            .await
1✔
1125
            .unwrap();
1✔
1126

1127
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1128

1129
        let result = processor
1✔
1130
            .plot_query(
1✔
1131
                PlotQueryRectangle::new(
1✔
1132
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1133
                    TimeInterval::default(),
1✔
1134
                    PlotSeriesSelection::all(),
1✔
1135
                ),
1✔
1136
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1137
            )
1✔
1138
            .await
1✔
1139
            .unwrap();
1✔
1140

1141
        assert_eq!(
1✔
1142
            result,
1✔
1143
            json!({
1✔
1144
                "foo": {
1✔
1145
                    "valueCount": 7,
1✔
1146
                    "validCount": 3,
1✔
1147
                    "min": 1.0,
1✔
1148
                    "max": 6.0,
1✔
1149
                    "mean": 3.333_333_333_333_333,
1✔
1150
                    "stddev": 2.054_804_667_656_325_6,
1✔
1151
                    "percentiles": [],
1✔
1152
                },
1✔
1153
                "bar": {
1✔
1154
                    "valueCount": 7,
1✔
1155
                    "validCount": 3,
1✔
1156
                    "min": 1.0,
1✔
1157
                    "max": 5.0,
1✔
1158
                    "mean": 2.666_666_666_666_667,
1✔
1159
                    "stddev": 1.699_673_171_197_595,
1✔
1160
                    "percentiles": [],
1✔
1161
                },
1✔
1162
            })
1✔
1163
        );
1✔
1164
    }
1✔
1165

1166
    #[tokio::test]
1167
    async fn vector_single_column() {
1✔
1168
        let tile_size_in_pixels = [3, 2].into();
1✔
1169
        let tiling_specification = TilingSpecification {
1✔
1170
            tile_size_in_pixels,
1✔
1171
        };
1✔
1172

1173
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
1174
            DataCollection::from_slices(
1✔
1175
                &[] as &[NoGeometry],
1✔
1176
                &[TimeInterval::default(); 7],
1✔
1177
                &[
1✔
1178
                    (
1✔
1179
                        "foo",
1✔
1180
                        FeatureData::NullableFloat(vec![
1✔
1181
                            Some(1.0),
1✔
1182
                            None,
1✔
1183
                            Some(3.0),
1✔
1184
                            None,
1✔
1185
                            Some(f64::NAN),
1✔
1186
                            Some(6.0),
1✔
1187
                            Some(f64::NAN),
1✔
1188
                        ]),
1✔
1189
                    ),
1✔
1190
                    (
1✔
1191
                        "bar",
1✔
1192
                        FeatureData::NullableFloat(vec![
1✔
1193
                            Some(1.0),
1✔
1194
                            Some(2.0),
1✔
1195
                            None,
1✔
1196
                            None,
1✔
1197
                            Some(5.0),
1✔
1198
                            Some(f64::NAN),
1✔
1199
                            Some(f64::NAN),
1✔
1200
                        ]),
1✔
1201
                    ),
1✔
1202
                ],
1✔
1203
            )
1204
            .unwrap(),
1✔
1205
        ])
1206
        .boxed();
1✔
1207

1208
        let statistics = Statistics {
1✔
1209
            params: StatisticsParams {
1✔
1210
                column_names: vec!["foo".to_string()],
1✔
1211
                percentiles: vec![],
1✔
1212
            },
1✔
1213
            sources: vector_source.into(),
1✔
1214
        };
1✔
1215

1216
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1217

1218
        let statistics = statistics
1✔
1219
            .boxed()
1✔
1220
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1221
            .await
1✔
1222
            .unwrap();
1✔
1223

1224
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1225

1226
        let result = processor
1✔
1227
            .plot_query(
1✔
1228
                PlotQueryRectangle::new(
1✔
1229
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1230
                    TimeInterval::default(),
1✔
1231
                    PlotSeriesSelection::all(),
1✔
1232
                ),
1✔
1233
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1234
            )
1✔
1235
            .await
1✔
1236
            .unwrap();
1✔
1237

1238
        assert_eq!(
1✔
1239
            result.to_string(),
1✔
1240
            json!({
1✔
1241
                "foo": {
1✔
1242
                    "valueCount": 7,
1✔
1243
                    "validCount": 3,
1✔
1244
                    "min": 1.0,
1✔
1245
                    "max": 6.0,
1✔
1246
                    "mean": 3.333_333_333_333_333,
1✔
1247
                    "stddev": 2.054_804_667_656_325_6,
1✔
1248
                    "percentiles": [],
1✔
1249
                },
1✔
1250
            })
1✔
1251
            .to_string()
1✔
1252
        );
1✔
1253
    }
1✔
1254

1255
    #[tokio::test]
1256
    async fn vector_two_columns() {
1✔
1257
        let tile_size_in_pixels = [3, 2].into();
1✔
1258
        let tiling_specification = TilingSpecification {
1✔
1259
            tile_size_in_pixels,
1✔
1260
        };
1✔
1261

1262
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
1263
            DataCollection::from_slices(
1✔
1264
                &[] as &[NoGeometry],
1✔
1265
                &[TimeInterval::default(); 7],
1✔
1266
                &[
1✔
1267
                    (
1✔
1268
                        "foo",
1✔
1269
                        FeatureData::NullableFloat(vec![
1✔
1270
                            Some(1.0),
1✔
1271
                            None,
1✔
1272
                            Some(3.0),
1✔
1273
                            None,
1✔
1274
                            Some(f64::NAN),
1✔
1275
                            Some(6.0),
1✔
1276
                            Some(f64::NAN),
1✔
1277
                        ]),
1✔
1278
                    ),
1✔
1279
                    (
1✔
1280
                        "bar",
1✔
1281
                        FeatureData::NullableFloat(vec![
1✔
1282
                            Some(1.0),
1✔
1283
                            Some(2.0),
1✔
1284
                            None,
1✔
1285
                            None,
1✔
1286
                            Some(5.0),
1✔
1287
                            Some(f64::NAN),
1✔
1288
                            Some(f64::NAN),
1✔
1289
                        ]),
1✔
1290
                    ),
1✔
1291
                ],
1✔
1292
            )
1293
            .unwrap(),
1✔
1294
        ])
1295
        .boxed();
1✔
1296

1297
        let statistics = Statistics {
1✔
1298
            params: StatisticsParams {
1✔
1299
                column_names: vec!["foo".to_string(), "bar".to_string()],
1✔
1300
                percentiles: vec![],
1✔
1301
            },
1✔
1302
            sources: vector_source.into(),
1✔
1303
        };
1✔
1304

1305
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1306

1307
        let statistics = statistics
1✔
1308
            .boxed()
1✔
1309
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1310
            .await
1✔
1311
            .unwrap();
1✔
1312

1313
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1314

1315
        let result = processor
1✔
1316
            .plot_query(
1✔
1317
                PlotQueryRectangle::new(
1✔
1318
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1319
                    TimeInterval::default(),
1✔
1320
                    PlotSeriesSelection::all(),
1✔
1321
                ),
1✔
1322
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1323
            )
1✔
1324
            .await
1✔
1325
            .unwrap();
1✔
1326

1327
        assert_eq!(
1✔
1328
            result,
1✔
1329
            json!({
1✔
1330
                "foo": {
1✔
1331
                    "valueCount": 7,
1✔
1332
                    "validCount": 3,
1✔
1333
                    "min": 1.0,
1✔
1334
                    "max": 6.0,
1✔
1335
                    "mean": 3.333_333_333_333_333,
1✔
1336
                    "stddev": 2.054_804_667_656_325_6,
1✔
1337
                    "percentiles": [],
1✔
1338
                },
1✔
1339
                "bar": {
1✔
1340
                    "valueCount": 7,
1✔
1341
                    "validCount": 3,
1✔
1342
                    "min": 1.0,
1✔
1343
                    "max": 5.0,
1✔
1344
                    "mean": 2.666_666_666_666_667,
1✔
1345
                    "stddev": 1.699_673_171_197_595,
1✔
1346
                    "percentiles": [],
1✔
1347
                },
1✔
1348
            })
1✔
1349
        );
1✔
1350
    }
1✔
1351

1352
    #[tokio::test]
1353
    async fn raster_percentile() {
1✔
1354
        let tile_size_in_pixels = [3, 2].into();
1✔
1355
        let tiling_specification = TilingSpecification {
1✔
1356
            tile_size_in_pixels,
1✔
1357
        };
1✔
1358

1359
        let result_descriptor = RasterResultDescriptor {
1✔
1360
            data_type: RasterDataType::U8,
1✔
1361
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1362
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
1363
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1364
                TestDefault::test_default(),
1✔
1365
                GridBoundingBox2D::new_min_max(-90, 89, -180, 179).unwrap(),
1✔
1366
            ),
1✔
1367
            bands: RasterBandDescriptors::new_single_band(),
1✔
1368
        };
1✔
1369

1370
        let raster_source = MockRasterSource {
1✔
1371
            params: MockRasterSourceParams {
1✔
1372
                data: vec![RasterTile2D::new_with_tile_info(
1✔
1373
                    TimeInterval::default(),
1✔
1374
                    TileInformation {
1✔
1375
                        global_geo_transform: TestDefault::test_default(),
1✔
1376
                        global_tile_position: [0, 0].into(),
1✔
1377
                        tile_size_in_pixels,
1✔
1378
                    },
1✔
1379
                    0,
1✔
1380
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
1381
                        .unwrap()
1✔
1382
                        .into(),
1✔
1383
                    CacheHint::default(),
1✔
1384
                )],
1✔
1385
                result_descriptor,
1✔
1386
            },
1✔
1387
        }
1✔
1388
        .boxed();
1✔
1389

1390
        let statistics = Statistics {
1✔
1391
            params: StatisticsParams {
1✔
1392
                column_names: vec![],
1✔
1393
                percentiles: vec![NotNan::new(0.25).unwrap(), NotNan::new(0.75).unwrap()],
1✔
1394
            },
1✔
1395
            sources: vec![raster_source].into(),
1✔
1396
        };
1✔
1397

1398
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1399

1400
        let statistics = statistics
1✔
1401
            .boxed()
1✔
1402
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1403
            .await
1✔
1404
            .unwrap();
1✔
1405

1406
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1407

1408
        let result = processor
1✔
1409
            .plot_query(
1✔
1410
                PlotQueryRectangle::new(
1✔
1411
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1412
                    TimeInterval::default(),
1✔
1413
                    PlotSeriesSelection::all(),
1✔
1414
                ),
1✔
1415
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1416
            )
1✔
1417
            .await
1✔
1418
            .unwrap();
1✔
1419

1420
        assert_eq!(
1✔
1421
            result.to_string(),
1✔
1422
            json!({
1✔
1423
                "Raster-1": {
1✔
1424
                    "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✔
1425
                    "validCount": 6,
1✔
1426
                    "min": 1.0,
1✔
1427
                    "max": 6.0,
1✔
1428
                    "mean": 3.5,
1✔
1429
                    "stddev": 1.707_825_127_659_933,
1✔
1430
                    "percentiles": [
1✔
1431
                        {"percentile": 0.25, "value": 3.0},
1✔
1432
                        {"percentile": 0.75, "value": 3.0},
1✔
1433
                    ],
1✔
1434
                }
1✔
1435
            })
1✔
1436
            .to_string()
1✔
1437
        );
1✔
1438
    }
1✔
1439

1440
    #[tokio::test]
1441
    async fn vector_percentiles() {
1✔
1442
        let tile_size_in_pixels = [3, 2].into();
1✔
1443
        let tiling_specification = TilingSpecification {
1✔
1444
            tile_size_in_pixels,
1✔
1445
        };
1✔
1446

1447
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
1448
            DataCollection::from_slices(
1✔
1449
                &[] as &[NoGeometry],
1✔
1450
                &[TimeInterval::default(); 7],
1✔
1451
                &[
1✔
1452
                    (
1✔
1453
                        "foo",
1✔
1454
                        FeatureData::NullableFloat(vec![
1✔
1455
                            Some(1.0),
1✔
1456
                            None,
1✔
1457
                            Some(3.0),
1✔
1458
                            None,
1✔
1459
                            Some(f64::NAN),
1✔
1460
                            Some(6.0),
1✔
1461
                            Some(f64::NAN),
1✔
1462
                        ]),
1✔
1463
                    ),
1✔
1464
                    (
1✔
1465
                        "bar",
1✔
1466
                        FeatureData::NullableFloat(vec![
1✔
1467
                            Some(1.0),
1✔
1468
                            Some(2.0),
1✔
1469
                            None,
1✔
1470
                            None,
1✔
1471
                            Some(5.0),
1✔
1472
                            Some(f64::NAN),
1✔
1473
                            Some(f64::NAN),
1✔
1474
                        ]),
1✔
1475
                    ),
1✔
1476
                ],
1✔
1477
            )
1478
            .unwrap(),
1✔
1479
        ])
1480
        .boxed();
1✔
1481

1482
        let statistics = Statistics {
1✔
1483
            params: StatisticsParams {
1✔
1484
                column_names: vec!["foo".to_string()],
1✔
1485
                percentiles: vec![NotNan::new(0.25).unwrap(), NotNan::new(0.75).unwrap()],
1✔
1486
            },
1✔
1487
            sources: vector_source.into(),
1✔
1488
        };
1✔
1489

1490
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1491

1492
        let statistics = statistics
1✔
1493
            .boxed()
1✔
1494
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1495
            .await
1✔
1496
            .unwrap();
1✔
1497

1498
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1499

1500
        let result = processor
1✔
1501
            .plot_query(
1✔
1502
                PlotQueryRectangle::new(
1✔
1503
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1504
                    TimeInterval::default(),
1✔
1505
                    PlotSeriesSelection::all(),
1✔
1506
                ),
1✔
1507
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1508
            )
1✔
1509
            .await
1✔
1510
            .unwrap();
1✔
1511

1512
        assert_eq!(
1✔
1513
            result.to_string(),
1✔
1514
            json!({
1✔
1515
                "foo": {
1✔
1516
                    "valueCount": 7,
1✔
1517
                    "validCount": 3,
1✔
1518
                    "min": 1.0,
1✔
1519
                    "max": 6.0,
1✔
1520
                    "mean": 3.333_333_333_333_333,
1✔
1521
                    "stddev": 2.054_804_667_656_325_6,
1✔
1522
                    "percentiles": [
1✔
1523
                        {"percentile": 0.25, "value": 1.0},
1✔
1524
                        {"percentile": 0.75, "value": 6.0},
1✔
1525
                    ],
1✔
1526
                },
1✔
1527
            })
1✔
1528
            .to_string()
1✔
1529
        );
1✔
1530
    }
1✔
1531
}
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