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

geo-engine / geoengine / 7006568925

27 Nov 2023 02:07PM UTC coverage: 89.651% (+0.2%) from 89.498%
7006568925

push

github

web-flow
Merge pull request #888 from geo-engine/raster_stacks

raster stacking

4032 of 4274 new or added lines in 107 files covered. (94.34%)

12 existing lines in 8 files now uncovered.

113020 of 126066 relevant lines covered (89.65%)

59901.79 hits per line

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

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

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

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

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

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

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

11✔
60
        match self.sources.source {
11✔
61
            MultiRasterOrVectorOperator::Raster(rasters) => {
7✔
62
                ensure!( self.params.column_names.is_empty() || self.params.column_names.len() == rasters.len(),
7✔
63
                    error::InvalidOperatorSpec {
1✔
64
                        reason: "Statistics on raster data must either contain a name/alias for every input ('column_names' parameter) or no names at all."
1✔
65
                            .to_string(),
1✔
66
                });
1✔
67

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

76
                let rasters = futures::future::try_join_all(
6✔
77
                    rasters
6✔
78
                        .into_iter()
6✔
79
                        .enumerate()
6✔
80
                        .map(|(i, op)| op.initialize(path.clone_and_append(i as u8), context)),
6✔
81
                )
6✔
82
                .await?;
×
83

84
                let in_descriptors = rasters
6✔
85
                    .iter()
6✔
86
                    .map(InitializedRasterOperator::result_descriptor)
6✔
87
                    .collect::<Vec<_>>();
6✔
88

6✔
89
                // TODO: implement multi-band functionality and remove this check
6✔
90
                ensure!(
6✔
91
                    in_descriptors.iter().all(|r| r.bands.len() == 1),
6✔
NEW
92
                    crate::error::OperatorDoesNotSupportMultiBandsSourcesYet {
×
NEW
93
                        operator: Statistics::TYPE_NAME,
×
NEW
94
                    }
×
95
                );
96

97
                if rasters.len() > 1 {
6✔
98
                    let srs = in_descriptors[0].spatial_reference;
2✔
99
                    ensure!(
2✔
100
                        in_descriptors.iter().all(|d| d.spatial_reference == srs),
4✔
101
                        error::AllSourcesMustHaveSameSpatialReference
×
102
                    );
103
                }
4✔
104

105
                let time = time_interval_extent(in_descriptors.iter().map(|d| d.time));
6✔
106
                let bbox = partitions_extent(in_descriptors.iter().map(|d| d.bbox));
6✔
107

6✔
108
                let initialized_operator = InitializedStatistics::new(
6✔
109
                    name,
6✔
110
                    PlotResultDescriptor {
6✔
111
                        spatial_reference: rasters.get(0).map_or_else(
6✔
112
                            || SpatialReferenceOption::Unreferenced,
6✔
113
                            |r| r.result_descriptor().spatial_reference,
6✔
114
                        ),
6✔
115
                        time,
6✔
116
                        bbox: bbox
6✔
117
                            .and_then(|p| BoundingBox2D::new(p.lower_left(), p.upper_right()).ok()),
6✔
118
                    },
6✔
119
                    output_names,
6✔
120
                    rasters,
6✔
121
                );
6✔
122

6✔
123
                Ok(initialized_operator.boxed())
6✔
124
            }
125
            MultiRasterOrVectorOperator::Vector(vector_source) => {
4✔
126
                let initialized_vector = vector_source
4✔
127
                    .initialize(path.clone_and_append(0), context)
4✔
128
                    .await?;
4✔
129

130
                let in_descriptor = initialized_vector.result_descriptor();
3✔
131

132
                let column_names = if self.params.column_names.is_empty() {
3✔
133
                    in_descriptor
1✔
134
                        .columns
1✔
135
                        .clone()
1✔
136
                        .into_iter()
1✔
137
                        .filter(|(_, info)| info.data_type.is_numeric())
2✔
138
                        .map(|(name, _)| name)
2✔
139
                        .collect()
1✔
140
                } else {
141
                    for cn in &self.params.column_names {
5✔
142
                        match in_descriptor.column_data_type(cn.as_str()) {
3✔
143
                            Some(column) if !column.is_numeric() => {
3✔
144
                                return Err(Error::InvalidOperatorSpec {
×
145
                                    reason: format!("Column '{cn}' is not numeric."),
×
146
                                });
×
147
                            }
148
                            Some(_) => {
3✔
149
                                // OK
3✔
150
                            }
3✔
151
                            None => {
152
                                return Err(Error::ColumnDoesNotExist {
×
153
                                    column: cn.to_string(),
×
154
                                });
×
155
                            }
156
                        }
157
                    }
158
                    self.params.column_names.clone()
2✔
159
                };
160

161
                let initialized_operator = InitializedStatistics::new(
3✔
162
                    name,
3✔
163
                    PlotResultDescriptor {
3✔
164
                        spatial_reference: in_descriptor.spatial_reference,
3✔
165
                        time: in_descriptor.time,
3✔
166
                        bbox: in_descriptor.bbox,
3✔
167
                    },
3✔
168
                    column_names,
3✔
169
                    initialized_vector,
3✔
170
                );
3✔
171

3✔
172
                Ok(initialized_operator.boxed())
3✔
173
            }
174
        }
175
    }
22✔
176

177
    span_fn!(Statistics);
×
178
}
179

180
/// The initialization of `Statistics`
181
pub struct InitializedStatistics<Op> {
182
    name: CanonicOperatorName,
183
    result_descriptor: PlotResultDescriptor,
184
    column_names: Vec<String>,
185
    source: Op,
186
}
187

188
impl<Op> InitializedStatistics<Op> {
189
    pub fn new(
9✔
190
        name: CanonicOperatorName,
9✔
191
        result_descriptor: PlotResultDescriptor,
9✔
192
        column_names: Vec<String>,
9✔
193
        source: Op,
9✔
194
    ) -> Self {
9✔
195
        Self {
9✔
196
            name,
9✔
197
            result_descriptor,
9✔
198
            column_names,
9✔
199
            source,
9✔
200
        }
9✔
201
    }
9✔
202
}
203

204
impl InitializedPlotOperator for InitializedStatistics<Box<dyn InitializedVectorOperator>> {
205
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
206
        &self.result_descriptor
×
207
    }
×
208

209
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
3✔
210
        Ok(TypedPlotQueryProcessor::JsonPlain(
3✔
211
            StatisticsVectorQueryProcessor {
3✔
212
                vector: self.source.query_processor()?,
3✔
213
                column_names: self.column_names.clone(),
3✔
214
            }
3✔
215
            .boxed(),
3✔
216
        ))
217
    }
3✔
218

219
    fn canonic_name(&self) -> CanonicOperatorName {
×
220
        self.name.clone()
×
221
    }
×
222
}
223

224
impl InitializedPlotOperator for InitializedStatistics<Vec<Box<dyn InitializedRasterOperator>>> {
225
    fn result_descriptor(&self) -> &PlotResultDescriptor {
2✔
226
        &self.result_descriptor
2✔
227
    }
2✔
228

229
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
5✔
230
        Ok(TypedPlotQueryProcessor::JsonPlain(
5✔
231
            StatisticsRasterQueryProcessor {
5✔
232
                rasters: self
5✔
233
                    .source
5✔
234
                    .iter()
5✔
235
                    .map(InitializedRasterOperator::query_processor)
5✔
236
                    .collect::<Result<Vec<_>>>()?,
5✔
237
                column_names: self.column_names.clone(),
5✔
238
            }
5✔
239
            .boxed(),
5✔
240
        ))
241
    }
5✔
242

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

248
/// A query processor that calculates the statistics about its vector input.
249
pub struct StatisticsVectorQueryProcessor {
250
    vector: TypedVectorQueryProcessor,
251
    column_names: Vec<String>,
252
}
253

254
#[async_trait]
255
impl PlotQueryProcessor for StatisticsVectorQueryProcessor {
256
    type OutputFormat = serde_json::Value;
257

258
    fn plot_type(&self) -> &'static str {
×
259
        STATISTICS_OPERATOR_NAME
×
260
    }
×
261

262
    async fn plot_query<'a>(
3✔
263
        &'a self,
3✔
264
        query: PlotQueryRectangle,
3✔
265
        ctx: &'a dyn QueryContext,
3✔
266
    ) -> Result<Self::OutputFormat> {
3✔
267
        let mut number_statistics: HashMap<String, NumberStatistics> = self
3✔
268
            .column_names
3✔
269
            .iter()
3✔
270
            .map(|column| (column.clone(), NumberStatistics::default()))
5✔
271
            .collect();
3✔
272

273
        call_on_generic_vector_processor!(&self.vector, processor => {
3✔
274
            let mut query = processor.query(query.into(), ctx).await?;
3✔
275

276
            while let Some(collection) = query.next().await {
6✔
277
                let collection = collection?;
3✔
278

279
                for (column, stats) in &mut number_statistics {
8✔
280
                    match collection.data(column) {
5✔
281
                        Ok(data) => data.float_options_iter().for_each(
5✔
282
                            | value | {
35✔
283
                                match value {
35✔
284
                                    Some(v) => stats.add(v),
25✔
285
                                    None => stats.add_no_data()
10✔
286
                                }
287
                            }
35✔
288
                        ),
289
                        Err(_) => stats.add_no_data_batch(collection.len())
×
290
                    }
291
                }
292
            }
293
        });
294

295
        let output: HashMap<String, StatisticsOutput> = number_statistics
3✔
296
            .iter()
3✔
297
            .map(|(column, number_statistics)| {
5✔
298
                (column.clone(), StatisticsOutput::from(number_statistics))
5✔
299
            })
5✔
300
            .collect();
3✔
301
        serde_json::to_value(output).map_err(Into::into)
3✔
302
    }
6✔
303
}
304

305
/// A query processor that calculates the statistics about its raster inputs.
306
pub struct StatisticsRasterQueryProcessor {
307
    rasters: Vec<TypedRasterQueryProcessor>,
308
    column_names: Vec<String>,
309
}
310

311
#[async_trait]
312
impl PlotQueryProcessor for StatisticsRasterQueryProcessor {
313
    type OutputFormat = serde_json::Value;
314

315
    fn plot_type(&self) -> &'static str {
1✔
316
        STATISTICS_OPERATOR_NAME
1✔
317
    }
1✔
318

319
    async fn plot_query<'a>(
5✔
320
        &'a self,
5✔
321
        query: PlotQueryRectangle,
5✔
322
        ctx: &'a dyn QueryContext,
5✔
323
    ) -> Result<Self::OutputFormat> {
5✔
324
        let mut queries = Vec::with_capacity(self.rasters.len());
5✔
325
        let q: RasterQueryRectangle =
5✔
326
            RasterQueryRectangle::from_qrect_and_bands(&query, BandSelection::first());
5✔
327
        for (i, raster_processor) in self.rasters.iter().enumerate() {
6✔
328
            queries.push(
329
                call_on_generic_raster_processor!(raster_processor, processor => {
6✔
330
                    processor.query(q.clone(), ctx).await?
6✔
331
                             .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))
55,209✔
332
                             .boxed()
6✔
333
                }),
334
            );
335
        }
336

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

5✔
339
        select_all(queries)
5✔
340
            .try_fold(
5✔
341
                number_statistics,
5✔
342
                |number_statistics: Vec<NumberStatistics>, enumerated_raster_tile| async move {
55,209✔
343
                    let mut number_statistics = number_statistics;
55,209✔
344
                    let (i, raster_tile) = enumerated_raster_tile;
55,209✔
345
                    match raster_tile.grid_array {
55,209✔
346
                        GridOrEmpty::Grid(g) => process_raster(
6✔
347
                            &mut number_statistics[i],
6✔
348
                            g.masked_element_deref_iterator(),
6✔
349
                        ),
6✔
350
                        GridOrEmpty::Empty(n) => {
55,203✔
351
                            number_statistics[i].add_no_data_batch(n.number_of_elements());
55,203✔
352
                        }
55,203✔
353
                    }
354

355
                    Ok(number_statistics)
55,209✔
356
                },
55,209✔
357
            )
5✔
358
            .map(|number_statistics| {
5✔
359
                let output: HashMap<String, StatisticsOutput> = number_statistics?
5✔
360
                    .iter()
5✔
361
                    .enumerate()
5✔
362
                    .map(|(i, stat)| (self.column_names[i].clone(), StatisticsOutput::from(stat)))
6✔
363
                    .collect();
5✔
364
                serde_json::to_value(output).map_err(Into::into)
5✔
365
            })
5✔
366
            .await
36,464✔
367
    }
10✔
368
}
369

370
fn process_raster<I>(number_statistics: &mut NumberStatistics, data: I)
6✔
371
where
6✔
372
    I: Iterator<Item = Option<f64>>,
6✔
373
{
6✔
374
    for value_option in data {
42✔
375
        if let Some(value) = value_option {
36✔
376
            number_statistics.add(value);
36✔
377
        } else {
36✔
378
            number_statistics.add_no_data();
×
379
        }
×
380
    }
381
}
6✔
382

383
/// The statistics summary output type for each raster input/vector input column
384
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
11✔
385
#[serde(rename_all = "camelCase")]
386
struct StatisticsOutput {
387
    pub value_count: usize,
388
    pub valid_count: usize,
389
    pub min: f64,
390
    pub max: f64,
391
    pub mean: f64,
392
    pub stddev: f64,
393
}
394

395
impl From<&NumberStatistics> for StatisticsOutput {
396
    fn from(number_statistics: &NumberStatistics) -> Self {
11✔
397
        Self {
11✔
398
            value_count: number_statistics.count() + number_statistics.nan_count(),
11✔
399
            valid_count: number_statistics.count(),
11✔
400
            min: number_statistics.min(),
11✔
401
            max: number_statistics.max(),
11✔
402
            mean: number_statistics.mean(),
11✔
403
            stddev: number_statistics.std_dev(),
11✔
404
        }
11✔
405
    }
11✔
406
}
407

408
#[cfg(test)]
409
mod tests {
410
    use geoengine_datatypes::collections::DataCollection;
411
    use geoengine_datatypes::primitives::{CacheHint, PlotSeriesSelection};
412
    use geoengine_datatypes::util::test::TestDefault;
413
    use serde_json::json;
414

415
    use super::*;
416
    use crate::engine::{
417
        ChunkByteSize, MockExecutionContext, MockQueryContext, RasterOperator,
418
        RasterResultDescriptor,
419
    };
420
    use crate::engine::{RasterBandDescriptors, VectorOperator};
421
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
422
    use crate::util::input::MultiRasterOrVectorOperator::Raster;
423
    use geoengine_datatypes::primitives::{
424
        BoundingBox2D, FeatureData, NoGeometry, SpatialResolution, TimeInterval,
425
    };
426
    use geoengine_datatypes::raster::{
427
        Grid2D, RasterDataType, RasterTile2D, TileInformation, TilingSpecification,
428
    };
429
    use geoengine_datatypes::spatial_reference::SpatialReference;
430

431
    #[test]
1✔
432
    fn serialization() {
1✔
433
        let statistics = Statistics {
1✔
434
            params: StatisticsParams {
1✔
435
                column_names: vec![],
1✔
436
            },
1✔
437
            sources: MultipleRasterOrSingleVectorSource {
1✔
438
                source: Raster(vec![]),
1✔
439
            },
1✔
440
        };
1✔
441

1✔
442
        let serialized = json!({
1✔
443
            "type": "Statistics",
1✔
444
            "params": {},
1✔
445
            "sources": {
1✔
446
                "source": [],
1✔
447
            },
1✔
448
        })
1✔
449
        .to_string();
1✔
450

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

1✔
453
        assert_eq!(deserialized.params, statistics.params);
1✔
454
    }
1✔
455

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

1✔
464
        let statistics = Statistics {
1✔
465
            params: StatisticsParams {
1✔
466
                column_names: vec![],
1✔
467
            },
1✔
468
            sources: vec![].into(),
1✔
469
        };
1✔
470

1✔
471
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
472

473
        let statistics = statistics
1✔
474
            .boxed()
1✔
475
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
476
            .await
×
477
            .unwrap();
1✔
478

1✔
479
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
480

481
        let result = processor
1✔
482
            .plot_query(
1✔
483
                PlotQueryRectangle {
1✔
484
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
485
                        .unwrap(),
1✔
486
                    time_interval: TimeInterval::default(),
1✔
487
                    spatial_resolution: SpatialResolution::one(),
1✔
488
                    attributes: PlotSeriesSelection::all(),
1✔
489
                },
1✔
490
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
491
            )
1✔
492
            .await
×
493
            .unwrap();
1✔
494

1✔
495
        assert_eq!(result.to_string(), json!({}).to_string());
1✔
496
    }
497

498
    #[tokio::test]
1✔
499
    async fn single_raster_implicit_name() {
1✔
500
        let tile_size_in_pixels = [3, 2].into();
1✔
501
        let tiling_specification = TilingSpecification {
1✔
502
            origin_coordinate: [0.0, 0.0].into(),
1✔
503
            tile_size_in_pixels,
1✔
504
        };
1✔
505

1✔
506
        let raster_source = MockRasterSource {
1✔
507
            params: MockRasterSourceParams {
1✔
508
                data: vec![RasterTile2D::new_with_tile_info(
1✔
509
                    TimeInterval::default(),
1✔
510
                    TileInformation {
1✔
511
                        global_geo_transform: TestDefault::test_default(),
1✔
512
                        global_tile_position: [0, 0].into(),
1✔
513
                        tile_size_in_pixels,
1✔
514
                    },
1✔
515
                    0,
1✔
516
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
517
                        .unwrap()
1✔
518
                        .into(),
1✔
519
                    CacheHint::default(),
1✔
520
                )],
1✔
521
                result_descriptor: RasterResultDescriptor {
1✔
522
                    data_type: RasterDataType::U8,
1✔
523
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
524
                    time: None,
1✔
525
                    bbox: None,
1✔
526
                    resolution: None,
1✔
527
                    bands: RasterBandDescriptors::new_single_band(),
1✔
528
                },
1✔
529
            },
1✔
530
        }
1✔
531
        .boxed();
1✔
532

1✔
533
        let statistics = Statistics {
1✔
534
            params: StatisticsParams {
1✔
535
                column_names: vec![],
1✔
536
            },
1✔
537
            sources: vec![raster_source].into(),
1✔
538
        };
1✔
539

1✔
540
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
541

542
        let statistics = statistics
1✔
543
            .boxed()
1✔
544
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
545
            .await
×
546
            .unwrap();
1✔
547

1✔
548
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
549

550
        let result = processor
1✔
551
            .plot_query(
1✔
552
                PlotQueryRectangle {
1✔
553
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
554
                        .unwrap(),
1✔
555
                    time_interval: TimeInterval::default(),
1✔
556
                    spatial_resolution: SpatialResolution::one(),
1✔
557
                    attributes: PlotSeriesSelection::all(),
1✔
558
                },
1✔
559
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
560
            )
1✔
561
            .await
15,905✔
562
            .unwrap();
1✔
563

1✔
564
        assert_eq!(
1✔
565
            result.to_string(),
1✔
566
            json!({
1✔
567
                "Raster-1": {
1✔
568
                    "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✔
569
                    "validCount": 6,
1✔
570
                    "min": 1.0,
1✔
571
                    "max": 6.0,
1✔
572
                    "mean": 3.5,
1✔
573
                    "stddev": 1.707_825_127_659_933,
1✔
574
                }
1✔
575
            })
1✔
576
            .to_string()
1✔
577
        );
1✔
578
    }
579

580
    #[tokio::test]
1✔
581
    #[allow(clippy::too_many_lines)]
582
    async fn two_rasters_implicit_names() {
1✔
583
        let tile_size_in_pixels = [3, 2].into();
1✔
584
        let tiling_specification = TilingSpecification {
1✔
585
            origin_coordinate: [0.0, 0.0].into(),
1✔
586
            tile_size_in_pixels,
1✔
587
        };
1✔
588

1✔
589
        let raster_source = vec![
1✔
590
            MockRasterSource {
1✔
591
                params: MockRasterSourceParams {
1✔
592
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
593
                        TimeInterval::default(),
1✔
594
                        TileInformation {
1✔
595
                            global_geo_transform: TestDefault::test_default(),
1✔
596
                            global_tile_position: [0, 0].into(),
1✔
597
                            tile_size_in_pixels,
1✔
598
                        },
1✔
599
                        0,
1✔
600
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
601
                            .unwrap()
1✔
602
                            .into(),
1✔
603
                        CacheHint::default(),
1✔
604
                    )],
1✔
605
                    result_descriptor: RasterResultDescriptor {
1✔
606
                        data_type: RasterDataType::U8,
1✔
607
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
608
                        time: None,
1✔
609
                        bbox: None,
1✔
610
                        resolution: None,
1✔
611
                        bands: RasterBandDescriptors::new_single_band(),
1✔
612
                    },
1✔
613
                },
1✔
614
            }
1✔
615
            .boxed(),
1✔
616
            MockRasterSource {
1✔
617
                params: MockRasterSourceParams {
1✔
618
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
619
                        TimeInterval::default(),
1✔
620
                        TileInformation {
1✔
621
                            global_geo_transform: TestDefault::test_default(),
1✔
622
                            global_tile_position: [0, 0].into(),
1✔
623
                            tile_size_in_pixels,
1✔
624
                        },
1✔
625
                        0,
1✔
626
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
627
                            .unwrap()
1✔
628
                            .into(),
1✔
629
                        CacheHint::default(),
1✔
630
                    )],
1✔
631
                    result_descriptor: RasterResultDescriptor {
1✔
632
                        data_type: RasterDataType::U8,
1✔
633
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
634
                        time: None,
1✔
635
                        bbox: None,
1✔
636
                        resolution: None,
1✔
637
                        bands: RasterBandDescriptors::new_single_band(),
1✔
638
                    },
1✔
639
                },
1✔
640
            }
1✔
641
            .boxed(),
1✔
642
        ];
1✔
643

1✔
644
        let statistics = Statistics {
1✔
645
            params: StatisticsParams {
1✔
646
                column_names: vec![],
1✔
647
            },
1✔
648
            sources: raster_source.into(),
1✔
649
        };
1✔
650

1✔
651
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
652

653
        let statistics = statistics
1✔
654
            .boxed()
1✔
655
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
656
            .await
×
657
            .unwrap();
1✔
658

1✔
659
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
660

661
        let result = processor
1✔
662
            .plot_query(
1✔
663
                PlotQueryRectangle {
1✔
664
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
665
                        .unwrap(),
1✔
666
                    time_interval: TimeInterval::default(),
1✔
667
                    spatial_resolution: SpatialResolution::one(),
1✔
668
                    attributes: PlotSeriesSelection::all(),
1✔
669
                },
1✔
670
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
671
            )
1✔
672
            .await
8,624✔
673
            .unwrap();
1✔
674

1✔
675
        assert_eq!(
1✔
676
            result.to_string(),
1✔
677
            json!({
1✔
678
                "Raster-1": {
1✔
679
                    "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✔
680
                    "validCount": 6,
1✔
681
                    "min": 1.0,
1✔
682
                    "max": 6.0,
1✔
683
                    "mean": 3.5,
1✔
684
                    "stddev": 1.707_825_127_659_933
1✔
685
                },
1✔
686
                "Raster-2": {
1✔
687
                    "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✔
688
                    "validCount": 6,
1✔
689
                    "min": 7.0,
1✔
690
                    "max": 12.0,
1✔
691
                    "mean": 9.5,
1✔
692
                    "stddev": 1.707_825_127_659_933
1✔
693
                },
1✔
694
            })
1✔
695
            .to_string()
1✔
696
        );
1✔
697
    }
698

699
    #[tokio::test]
1✔
700
    #[allow(clippy::too_many_lines)]
701
    async fn two_rasters_explicit_names() {
1✔
702
        let tile_size_in_pixels = [3, 2].into();
1✔
703
        let tiling_specification = TilingSpecification {
1✔
704
            origin_coordinate: [0.0, 0.0].into(),
1✔
705
            tile_size_in_pixels,
1✔
706
        };
1✔
707

1✔
708
        let raster_source = vec![
1✔
709
            MockRasterSource {
1✔
710
                params: MockRasterSourceParams {
1✔
711
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
712
                        TimeInterval::default(),
1✔
713
                        TileInformation {
1✔
714
                            global_geo_transform: TestDefault::test_default(),
1✔
715
                            global_tile_position: [0, 0].into(),
1✔
716
                            tile_size_in_pixels,
1✔
717
                        },
1✔
718
                        0,
1✔
719
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
720
                            .unwrap()
1✔
721
                            .into(),
1✔
722
                        CacheHint::default(),
1✔
723
                    )],
1✔
724
                    result_descriptor: RasterResultDescriptor {
1✔
725
                        data_type: RasterDataType::U8,
1✔
726
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
727
                        time: None,
1✔
728
                        bbox: None,
1✔
729
                        resolution: None,
1✔
730
                        bands: RasterBandDescriptors::new_single_band(),
1✔
731
                    },
1✔
732
                },
1✔
733
            }
1✔
734
            .boxed(),
1✔
735
            MockRasterSource {
1✔
736
                params: MockRasterSourceParams {
1✔
737
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
738
                        TimeInterval::default(),
1✔
739
                        TileInformation {
1✔
740
                            global_geo_transform: TestDefault::test_default(),
1✔
741
                            global_tile_position: [0, 0].into(),
1✔
742
                            tile_size_in_pixels,
1✔
743
                        },
1✔
744
                        0,
1✔
745
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
746
                            .unwrap()
1✔
747
                            .into(),
1✔
748
                        CacheHint::default(),
1✔
749
                    )],
1✔
750
                    result_descriptor: RasterResultDescriptor {
1✔
751
                        data_type: RasterDataType::U8,
1✔
752
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
753
                        time: None,
1✔
754
                        bbox: None,
1✔
755
                        resolution: None,
1✔
756
                        bands: RasterBandDescriptors::new_single_band(),
1✔
757
                    },
1✔
758
                },
1✔
759
            }
1✔
760
            .boxed(),
1✔
761
        ];
1✔
762

1✔
763
        let statistics = Statistics {
1✔
764
            params: StatisticsParams {
1✔
765
                column_names: vec!["A".to_string(), "B".to_string()],
1✔
766
            },
1✔
767
            sources: raster_source.into(),
1✔
768
        };
1✔
769

1✔
770
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
771

772
        let statistics = statistics
1✔
773
            .boxed()
1✔
774
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
775
            .await
×
776
            .unwrap();
1✔
777

1✔
778
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
779

780
        let result = processor
1✔
781
            .plot_query(
1✔
782
                PlotQueryRectangle {
1✔
783
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
784
                        .unwrap(),
1✔
785
                    time_interval: TimeInterval::default(),
1✔
786
                    spatial_resolution: SpatialResolution::one(),
1✔
787
                    attributes: PlotSeriesSelection::all(),
1✔
788
                },
1✔
789
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
790
            )
1✔
791
            .await
11,927✔
792
            .unwrap();
1✔
793

1✔
794
        assert_eq!(
1✔
795
            result.to_string(),
1✔
796
            json!({
1✔
797
                "A": {
1✔
798
                    "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✔
799
                    "validCount": 6,
1✔
800
                    "min": 1.0,
1✔
801
                    "max": 6.0,
1✔
802
                    "mean": 3.5,
1✔
803
                    "stddev": 1.707_825_127_659_933
1✔
804
                },
1✔
805
                "B": {
1✔
806
                    "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✔
807
                    "validCount": 6,
1✔
808
                    "min": 7.0,
1✔
809
                    "max": 12.0,
1✔
810
                    "mean": 9.5,
1✔
811
                    "stddev": 1.707_825_127_659_933
1✔
812
                },
1✔
813
            })
1✔
814
            .to_string()
1✔
815
        );
1✔
816
    }
817

818
    #[tokio::test]
1✔
819
    async fn two_rasters_explicit_names_incomplete() {
1✔
820
        let tile_size_in_pixels = [3, 2].into();
1✔
821
        let tiling_specification = TilingSpecification {
1✔
822
            origin_coordinate: [0.0, 0.0].into(),
1✔
823
            tile_size_in_pixels,
1✔
824
        };
1✔
825

1✔
826
        let raster_source = vec![
1✔
827
            MockRasterSource {
1✔
828
                params: MockRasterSourceParams {
1✔
829
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
830
                        TimeInterval::default(),
1✔
831
                        TileInformation {
1✔
832
                            global_geo_transform: TestDefault::test_default(),
1✔
833
                            global_tile_position: [0, 0].into(),
1✔
834
                            tile_size_in_pixels,
1✔
835
                        },
1✔
836
                        0,
1✔
837
                        Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
1✔
838
                            .unwrap()
1✔
839
                            .into(),
1✔
840
                        CacheHint::default(),
1✔
841
                    )],
1✔
842
                    result_descriptor: RasterResultDescriptor {
1✔
843
                        data_type: RasterDataType::U8,
1✔
844
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
845
                        time: None,
1✔
846
                        bbox: None,
1✔
847
                        resolution: None,
1✔
848
                        bands: RasterBandDescriptors::new_single_band(),
1✔
849
                    },
1✔
850
                },
1✔
851
            }
1✔
852
            .boxed(),
1✔
853
            MockRasterSource {
1✔
854
                params: MockRasterSourceParams {
1✔
855
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
856
                        TimeInterval::default(),
1✔
857
                        TileInformation {
1✔
858
                            global_geo_transform: TestDefault::test_default(),
1✔
859
                            global_tile_position: [0, 0].into(),
1✔
860
                            tile_size_in_pixels,
1✔
861
                        },
1✔
862
                        0,
1✔
863
                        Grid2D::new([3, 2].into(), vec![7, 8, 9, 10, 11, 12])
1✔
864
                            .unwrap()
1✔
865
                            .into(),
1✔
866
                        CacheHint::default(),
1✔
867
                    )],
1✔
868
                    result_descriptor: RasterResultDescriptor {
1✔
869
                        data_type: RasterDataType::U8,
1✔
870
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
871
                        time: None,
1✔
872
                        bbox: None,
1✔
873
                        resolution: None,
1✔
874
                        bands: RasterBandDescriptors::new_single_band(),
1✔
875
                    },
1✔
876
                },
1✔
877
            }
1✔
878
            .boxed(),
1✔
879
        ];
1✔
880

1✔
881
        let statistics = Statistics {
1✔
882
            params: StatisticsParams {
1✔
883
                column_names: vec!["A".to_string()],
1✔
884
            },
1✔
885
            sources: raster_source.into(),
1✔
886
        };
1✔
887

1✔
888
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
889

890
        let statistics = statistics
1✔
891
            .boxed()
1✔
892
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
893
            .await;
×
894

895
        assert!(
1✔
896
            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✔
897
        );
898
    }
899

900
    #[tokio::test]
1✔
901
    async fn vector_no_column() {
1✔
902
        let tile_size_in_pixels = [3, 2].into();
1✔
903
        let tiling_specification = TilingSpecification {
1✔
904
            origin_coordinate: [0.0, 0.0].into(),
1✔
905
            tile_size_in_pixels,
1✔
906
        };
1✔
907

1✔
908
        let vector_source =
1✔
909
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
910
                &[] as &[NoGeometry],
1✔
911
                &[TimeInterval::default(); 7],
1✔
912
                &[
1✔
913
                    (
1✔
914
                        "foo",
1✔
915
                        FeatureData::NullableFloat(vec![
1✔
916
                            Some(1.0),
1✔
917
                            None,
1✔
918
                            Some(3.0),
1✔
919
                            None,
1✔
920
                            Some(f64::NAN),
1✔
921
                            Some(6.0),
1✔
922
                            Some(f64::NAN),
1✔
923
                        ]),
1✔
924
                    ),
1✔
925
                    (
1✔
926
                        "bar",
1✔
927
                        FeatureData::NullableFloat(vec![
1✔
928
                            Some(1.0),
1✔
929
                            Some(2.0),
1✔
930
                            None,
1✔
931
                            None,
1✔
932
                            Some(5.0),
1✔
933
                            Some(f64::NAN),
1✔
934
                            Some(f64::NAN),
1✔
935
                        ]),
1✔
936
                    ),
1✔
937
                ],
1✔
938
            )
1✔
939
            .unwrap()])
1✔
940
            .boxed();
1✔
941

1✔
942
        let statistics = Statistics {
1✔
943
            params: StatisticsParams {
1✔
944
                column_names: vec![],
1✔
945
            },
1✔
946
            sources: vector_source.into(),
1✔
947
        };
1✔
948

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

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

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

959
        let result = processor
1✔
960
            .plot_query(
1✔
961
                PlotQueryRectangle {
1✔
962
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
963
                        .unwrap(),
1✔
964
                    time_interval: TimeInterval::default(),
1✔
965
                    spatial_resolution: SpatialResolution::one(),
1✔
966
                    attributes: PlotSeriesSelection::all(),
1✔
967
                },
1✔
968
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
969
            )
1✔
970
            .await
×
971
            .unwrap();
1✔
972

1✔
973
        assert_eq!(
1✔
974
            result.to_string(),
1✔
975
            json!({
1✔
976
                "foo": {
1✔
977
                    "valueCount": 7,
1✔
978
                    "validCount": 3,
1✔
979
                    "min": 1.0,
1✔
980
                    "max": 6.0,
1✔
981
                    "mean": 3.333_333_333_333_333,
1✔
982
                    "stddev": 2.054_804_667_656_325_6
1✔
983
                },
1✔
984
                "bar": {
1✔
985
                    "valueCount": 7,
1✔
986
                    "validCount": 3,
1✔
987
                    "min": 1.0,
1✔
988
                    "max": 5.0,
1✔
989
                    "mean": 2.666_666_666_666_667,
1✔
990
                    "stddev": 1.699_673_171_197_595
1✔
991
                },
1✔
992
            })
1✔
993
            .to_string()
1✔
994
        );
1✔
995
    }
996

997
    #[tokio::test]
1✔
998
    async fn vector_single_column() {
1✔
999
        let tile_size_in_pixels = [3, 2].into();
1✔
1000
        let tiling_specification = TilingSpecification {
1✔
1001
            origin_coordinate: [0.0, 0.0].into(),
1✔
1002
            tile_size_in_pixels,
1✔
1003
        };
1✔
1004

1✔
1005
        let vector_source =
1✔
1006
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1007
                &[] as &[NoGeometry],
1✔
1008
                &[TimeInterval::default(); 7],
1✔
1009
                &[
1✔
1010
                    (
1✔
1011
                        "foo",
1✔
1012
                        FeatureData::NullableFloat(vec![
1✔
1013
                            Some(1.0),
1✔
1014
                            None,
1✔
1015
                            Some(3.0),
1✔
1016
                            None,
1✔
1017
                            Some(f64::NAN),
1✔
1018
                            Some(6.0),
1✔
1019
                            Some(f64::NAN),
1✔
1020
                        ]),
1✔
1021
                    ),
1✔
1022
                    (
1✔
1023
                        "bar",
1✔
1024
                        FeatureData::NullableFloat(vec![
1✔
1025
                            Some(1.0),
1✔
1026
                            Some(2.0),
1✔
1027
                            None,
1✔
1028
                            None,
1✔
1029
                            Some(5.0),
1✔
1030
                            Some(f64::NAN),
1✔
1031
                            Some(f64::NAN),
1✔
1032
                        ]),
1✔
1033
                    ),
1✔
1034
                ],
1✔
1035
            )
1✔
1036
            .unwrap()])
1✔
1037
            .boxed();
1✔
1038

1✔
1039
        let statistics = Statistics {
1✔
1040
            params: StatisticsParams {
1✔
1041
                column_names: vec!["foo".to_string()],
1✔
1042
            },
1✔
1043
            sources: vector_source.into(),
1✔
1044
        };
1✔
1045

1✔
1046
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1047

1048
        let statistics = statistics
1✔
1049
            .boxed()
1✔
1050
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1051
            .await
×
1052
            .unwrap();
1✔
1053

1✔
1054
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1055

1056
        let result = processor
1✔
1057
            .plot_query(
1✔
1058
                PlotQueryRectangle {
1✔
1059
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1060
                        .unwrap(),
1✔
1061
                    time_interval: TimeInterval::default(),
1✔
1062
                    spatial_resolution: SpatialResolution::one(),
1✔
1063
                    attributes: PlotSeriesSelection::all(),
1✔
1064
                },
1✔
1065
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1066
            )
1✔
1067
            .await
×
1068
            .unwrap();
1✔
1069

1✔
1070
        assert_eq!(
1✔
1071
            result.to_string(),
1✔
1072
            json!({
1✔
1073
                "foo": {
1✔
1074
                    "valueCount": 7,
1✔
1075
                    "validCount": 3,
1✔
1076
                    "min": 1.0,
1✔
1077
                    "max": 6.0,
1✔
1078
                    "mean": 3.333_333_333_333_333,
1✔
1079
                    "stddev": 2.054_804_667_656_325_6
1✔
1080
                },
1✔
1081
            })
1✔
1082
            .to_string()
1✔
1083
        );
1✔
1084
    }
1085

1086
    #[tokio::test]
1✔
1087
    async fn vector_two_columns() {
1✔
1088
        let tile_size_in_pixels = [3, 2].into();
1✔
1089
        let tiling_specification = TilingSpecification {
1✔
1090
            origin_coordinate: [0.0, 0.0].into(),
1✔
1091
            tile_size_in_pixels,
1✔
1092
        };
1✔
1093

1✔
1094
        let vector_source =
1✔
1095
            MockFeatureCollectionSource::multiple(vec![DataCollection::from_slices(
1✔
1096
                &[] as &[NoGeometry],
1✔
1097
                &[TimeInterval::default(); 7],
1✔
1098
                &[
1✔
1099
                    (
1✔
1100
                        "foo",
1✔
1101
                        FeatureData::NullableFloat(vec![
1✔
1102
                            Some(1.0),
1✔
1103
                            None,
1✔
1104
                            Some(3.0),
1✔
1105
                            None,
1✔
1106
                            Some(f64::NAN),
1✔
1107
                            Some(6.0),
1✔
1108
                            Some(f64::NAN),
1✔
1109
                        ]),
1✔
1110
                    ),
1✔
1111
                    (
1✔
1112
                        "bar",
1✔
1113
                        FeatureData::NullableFloat(vec![
1✔
1114
                            Some(1.0),
1✔
1115
                            Some(2.0),
1✔
1116
                            None,
1✔
1117
                            None,
1✔
1118
                            Some(5.0),
1✔
1119
                            Some(f64::NAN),
1✔
1120
                            Some(f64::NAN),
1✔
1121
                        ]),
1✔
1122
                    ),
1✔
1123
                ],
1✔
1124
            )
1✔
1125
            .unwrap()])
1✔
1126
            .boxed();
1✔
1127

1✔
1128
        let statistics = Statistics {
1✔
1129
            params: StatisticsParams {
1✔
1130
                column_names: vec!["foo".to_string(), "bar".to_string()],
1✔
1131
            },
1✔
1132
            sources: vector_source.into(),
1✔
1133
        };
1✔
1134

1✔
1135
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1136

1137
        let statistics = statistics
1✔
1138
            .boxed()
1✔
1139
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1140
            .await
×
1141
            .unwrap();
1✔
1142

1✔
1143
        let processor = statistics.query_processor().unwrap().json_plain().unwrap();
1✔
1144

1145
        let result = processor
1✔
1146
            .plot_query(
1✔
1147
                PlotQueryRectangle {
1✔
1148
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1149
                        .unwrap(),
1✔
1150
                    time_interval: TimeInterval::default(),
1✔
1151
                    spatial_resolution: SpatialResolution::one(),
1✔
1152
                    attributes: PlotSeriesSelection::all(),
1✔
1153
                },
1✔
1154
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1155
            )
1✔
1156
            .await
×
1157
            .unwrap();
1✔
1158

1✔
1159
        assert_eq!(
1✔
1160
            result.to_string(),
1✔
1161
            json!({
1✔
1162
                "foo": {
1✔
1163
                    "valueCount": 7,
1✔
1164
                    "validCount": 3,
1✔
1165
                    "min": 1.0,
1✔
1166
                    "max": 6.0,
1✔
1167
                    "mean": 3.333_333_333_333_333,
1✔
1168
                    "stddev": 2.054_804_667_656_325_6
1✔
1169
                },
1✔
1170
                "bar": {
1✔
1171
                    "valueCount": 7,
1✔
1172
                    "validCount": 3,
1✔
1173
                    "min": 1.0,
1✔
1174
                    "max": 5.0,
1✔
1175
                    "mean": 2.666_666_666_666_667,
1✔
1176
                    "stddev": 1.699_673_171_197_595
1✔
1177
                },
1✔
1178
            })
1✔
1179
            .to_string()
1✔
1180
        );
1✔
1181
    }
1182
}
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