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

geo-engine / geoengine / 19065440894

04 Nov 2025 10:21AM UTC coverage: 88.783%. First build
19065440894

Pull #1083

github

web-flow
Merge 577088df6 into 3d9be4869
Pull Request #1083: feat: new gdal source workflow optimization

5714 of 6540 new or added lines in 71 files covered. (87.37%)

115349 of 129923 relevant lines covered (88.78%)

499710.35 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 {
171
                                    column: cn.to_string(),
172
                                });
173
                            }
174
                        }
175
                    }
176
                    self.params.column_names.clone()
177
                };
178

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

195
                Ok(initialized_operator.boxed())
196
            }
197
        }
198
    }
13✔
199

200
    span_fn!(Statistics);
201
}
202

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

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

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

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

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

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

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

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

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

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

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

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

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

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

356
        let query = query.select_attributes(ColumnSelection::all());
357

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

361
            while let Some(collection) = query.next().await {
362
                let collection = collection?;
363

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

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

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

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

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

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

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

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

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

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

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

438
                    let (i, raster_tile) = enumerated_raster_tile;
66,247✔
439

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

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

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

481
    Ok(())
7✔
482
}
7✔
483

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

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

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

507
        Ok(())
72✔
508
    }
72✔
509

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

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

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

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

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

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

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

556
        Ok(())
22✔
557
    }
22✔
558
}
559

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

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

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

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

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

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

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

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

646
        assert_eq!(deserialized.params, statistics.params);
1✔
647
    }
1✔
648

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

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

664
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
665

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

672
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
673

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

686
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
687
    }
1✔
688

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

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

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

732
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
733

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

740
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
741

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

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

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

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

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

836
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
837

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

844
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
845

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

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

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

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

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

948
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
949

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

956
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
957

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

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

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

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

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

1059
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1060

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

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

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

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

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

1121
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1122

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

1129
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1130

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

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

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

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

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

1218
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1219

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

1226
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1227

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

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

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

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

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

1307
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1308

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

1315
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1316

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

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

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

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

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

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

1400
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1401

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

1408
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1409

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

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

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

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

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

1492
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1493

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

1500
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1501

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

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