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

geo-engine / geoengine / 11911118784

19 Nov 2024 10:06AM UTC coverage: 90.448% (-0.2%) from 90.687%
11911118784

push

github

web-flow
Merge pull request #994 from geo-engine/workspace-dependencies

use workspace dependencies, update toolchain, use global lock in expression

9 of 11 new or added lines in 6 files covered. (81.82%)

369 existing lines in 74 files now uncovered.

132871 of 146904 relevant lines covered (90.45%)

54798.62 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

199
    span_fn!(Statistics);
200
}
201

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

485
    pub fn update(&mut self, sample: T) -> Result<(), StatisticsError> {
22✔
486
        match self {
22✔
487
            Self::Unitialized(quantile) => {
4✔
488
                // initial sample must be finite, if the current sample is not, stay uninitialized
4✔
489
                if f64::is_finite(sample.as_()) {
4✔
490
                    *self =
491
                        Self::Initialized(SafePSquareQuantileEstimator::new(*quantile, sample)?);
4✔
492
                }
×
493
            }
494
            Self::Initialized(estimator) => estimator.update(sample),
18✔
495
        }
496

497
        Ok(())
22✔
498
    }
22✔
499
}
500

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

514
#[derive(Debug, Clone, Serialize, Deserialize)]
×
515
struct PercentileOutput {
516
    percentile: f64,
517
    value: f64,
518
}
519

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

542
#[cfg(test)]
543
mod tests {
544
    use geoengine_datatypes::collections::DataCollection;
545
    use geoengine_datatypes::primitives::{CacheHint, PlotSeriesSelection};
546
    use geoengine_datatypes::util::test::TestDefault;
547
    use serde_json::json;
548

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

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

1✔
577
        let serialized = json!({
1✔
578
            "type": "Statistics",
1✔
579
            "params": {},
1✔
580
            "sources": {
1✔
581
                "source": [],
1✔
582
            },
1✔
583
        })
1✔
584
        .to_string();
1✔
585

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

1✔
588
        assert_eq!(deserialized.params, statistics.params);
1✔
589
    }
1✔
590

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

1✔
599
        let statistics = Statistics {
1✔
600
            params: StatisticsParams {
1✔
601
                column_names: vec![],
1✔
602
                percentiles: vec![],
1✔
603
            },
1✔
604
            sources: vec![].into(),
1✔
605
        };
1✔
606

1✔
607
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
608

1✔
609
        let statistics = statistics
1✔
610
            .boxed()
1✔
611
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
612
            .await
1✔
613
            .unwrap();
1✔
614

1✔
615
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
616

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

1✔
631
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
632
    }
1✔
633

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

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

1✔
669
        let statistics = Statistics {
1✔
670
            params: StatisticsParams {
1✔
671
                column_names: vec![],
1✔
672
                percentiles: vec![],
1✔
673
            },
1✔
674
            sources: vec![raster_source].into(),
1✔
675
        };
1✔
676

1✔
677
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
678

1✔
679
        let statistics = statistics
1✔
680
            .boxed()
1✔
681
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
682
            .await
1✔
683
            .unwrap();
1✔
684

1✔
685
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
686

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

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

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

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

1✔
782
        let statistics = Statistics {
1✔
783
            params: StatisticsParams {
1✔
784
                column_names: vec![],
1✔
785
                percentiles: vec![],
1✔
786
            },
1✔
787
            sources: raster_source.into(),
1✔
788
        };
1✔
789

1✔
790
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
791

1✔
792
        let statistics = statistics
1✔
793
            .boxed()
1✔
794
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
795
            .await
1✔
796
            .unwrap();
1✔
797

1✔
798
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
799

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1031
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1032

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

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

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

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

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

1✔
1093
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1094

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

1✔
1101
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1102

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1✔
1399
        assert_eq!(
1✔
1400
            result.to_string(),
1✔
1401
            json!({
1✔
1402
                "Raster-1": {
1✔
1403
                    "valueCount": 66_246, // 362*183 Note: this is caused by the inclusive nature of the bounding box. Since the right and lower bounds are included this wraps to a new row/column of tiles. In this test the tiles are 3x2 pixels in size.
1✔
1404
                    "validCount": 6,
1✔
1405
                    "min": 1.0,
1✔
1406
                    "max": 6.0,
1✔
1407
                    "mean": 3.5,
1✔
1408
                    "stddev": 1.707_825_127_659_933,
1✔
1409
                    "percentiles": [
1✔
1410
                        {"percentile": 0.25, "value": 3.0},
1✔
1411
                        {"percentile": 0.75, "value": 3.0},
1✔
1412
                    ],
1✔
1413
                }
1✔
1414
            })
1✔
1415
            .to_string()
1✔
1416
        );
1✔
1417
    }
1✔
1418

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

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

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

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

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

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

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

1✔
1493
        assert_eq!(
1✔
1494
            result.to_string(),
1✔
1495
            json!({
1✔
1496
                "foo": {
1✔
1497
                    "valueCount": 7,
1✔
1498
                    "validCount": 3,
1✔
1499
                    "min": 1.0,
1✔
1500
                    "max": 6.0,
1✔
1501
                    "mean": 3.333_333_333_333_333,
1✔
1502
                    "stddev": 2.054_804_667_656_325_6,
1✔
1503
                    "percentiles": [
1✔
1504
                        {"percentile": 0.25, "value": 1.0},
1✔
1505
                        {"percentile": 0.75, "value": 6.0},
1✔
1506
                    ],
1✔
1507
                },
1✔
1508
            })
1✔
1509
            .to_string()
1✔
1510
        );
1✔
1511
    }
1✔
1512
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc