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

geo-engine / geoengine / 5006008836

pending completion
5006008836

push

github

GitHub
Merge #785 #787

936 of 936 new or added lines in 50 files covered. (100.0%)

96010 of 107707 relevant lines covered (89.14%)

72676.46 hits per line

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

94.58
/operators/src/plot/histogram.rs
1
use crate::engine::{
2
    CanonicOperatorName, ExecutionContext, InitializedPlotOperator, InitializedRasterOperator,
3
    InitializedVectorOperator, Operator, OperatorName, PlotOperator, PlotQueryProcessor,
4
    PlotResultDescriptor, QueryContext, SingleRasterOrVectorSource, TypedPlotQueryProcessor,
5
    TypedRasterQueryProcessor, TypedVectorQueryProcessor,
6
};
7
use crate::engine::{QueryProcessor, WorkflowOperatorPath};
8
use crate::error;
9
use crate::error::Error;
10
use crate::string_token;
11
use crate::util::input::RasterOrVectorOperator;
12
use crate::util::Result;
13
use async_trait::async_trait;
14
use float_cmp::approx_eq;
15
use futures::stream::BoxStream;
16
use futures::{StreamExt, TryFutureExt};
17
use geoengine_datatypes::plots::{Plot, PlotData};
18
use geoengine_datatypes::primitives::{
19
    AxisAlignedRectangle, BoundingBox2D, DataRef, FeatureDataRef, FeatureDataType, Geometry,
20
    Measurement, VectorQueryRectangle,
21
};
22
use geoengine_datatypes::raster::{Pixel, RasterTile2D};
23
use geoengine_datatypes::{
24
    collections::{FeatureCollection, FeatureCollectionInfos},
25
    raster::GridSize,
26
};
27
use serde::{Deserialize, Serialize};
28
use snafu::{ensure, OptionExt};
29
use std::convert::TryFrom;
30

31
pub const HISTOGRAM_OPERATOR_NAME: &str = "Histogram";
32

33
/// A histogram plot about either a raster or a vector input.
34
///
35
/// For vector inputs, it calculates the histogram on one of its attributes.
36
///
37
pub type Histogram = Operator<HistogramParams, SingleRasterOrVectorSource>;
38

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

43
/// The parameter spec for `Histogram`
44
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
20✔
45
#[serde(rename_all = "camelCase")]
46
pub struct HistogramParams {
47
    /// Name of the (numeric) attribute to compute the histogram on. Ignored for operation on rasters.
48
    pub column_name: Option<String>,
49
    /// The bounds (min/max) of the histogram.
50
    pub bounds: HistogramBounds,
51
    /// Specify the number of buckets or how it should be derived.
52
    pub buckets: HistogramBuckets,
53
    /// Whether to create an interactive output (`false` by default)
54
    #[serde(default)]
55
    pub interactive: bool,
56
}
57

58
/// Options for how to derive the histogram's number of buckets.
59
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15✔
60
#[serde(rename_all = "camelCase", tag = "type")]
61
pub enum HistogramBuckets {
62
    #[serde(rename_all = "camelCase")]
63
    Number { value: u8 },
64
    #[serde(rename_all = "camelCase")]
65
    SquareRootChoiceRule {
66
        #[serde(default = "default_max_number_of_buckets")]
67
        max_number_of_buckets: u8,
68
    },
69
}
70

71
fn default_max_number_of_buckets() -> u8 {
×
72
    100
×
73
}
×
74

75
string_token!(Data, "data");
76

77
/// Let the bounds either be computed or given.
78
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
12✔
79
#[serde(untagged)]
80
pub enum HistogramBounds {
81
    Data(Data),
82
    Values { min: f64, max: f64 },
83
    // TODO: use bounds in measurement if they are available
84
}
85

86
#[typetag::serde]
×
87
#[async_trait]
88
impl PlotOperator for Histogram {
89
    async fn _initialize(
11✔
90
        self: Box<Self>,
11✔
91
        path: WorkflowOperatorPath,
11✔
92
        context: &dyn ExecutionContext,
11✔
93
    ) -> Result<Box<dyn InitializedPlotOperator>> {
11✔
94
        let name = CanonicOperatorName::from(&self);
11✔
95

11✔
96
        Ok(match self.sources.source {
11✔
97
            RasterOrVectorOperator::Raster(raster_source) => {
6✔
98
                ensure!(
6✔
99
                    self.params.column_name.is_none(),
6✔
100
                    error::InvalidOperatorSpec {
1✔
101
                        reason: "Histogram on raster input must not have `columnName` field set"
1✔
102
                            .to_string(),
1✔
103
                    }
1✔
104
                );
105

106
                let raster_source = raster_source
5✔
107
                    .initialize(path.clone_and_append(0), context)
5✔
108
                    .await?;
×
109

110
                let in_desc = raster_source.result_descriptor();
5✔
111
                InitializedHistogram::new(
5✔
112
                    name,
5✔
113
                    PlotResultDescriptor {
5✔
114
                        spatial_reference: in_desc.spatial_reference,
5✔
115
                        time: in_desc.time,
5✔
116
                        // converting `SpatialPartition2D` to `BoundingBox2D` is ok here, because is makes the covered area only larger
5✔
117
                        bbox: in_desc
5✔
118
                            .bbox
5✔
119
                            .and_then(|p| BoundingBox2D::new(p.lower_left(), p.upper_right()).ok()),
5✔
120
                    },
5✔
121
                    self.params,
5✔
122
                    raster_source,
5✔
123
                )
5✔
124
                .boxed()
5✔
125
            }
126
            RasterOrVectorOperator::Vector(vector_source) => {
5✔
127
                let column_name =
5✔
128
                    self.params
5✔
129
                        .column_name
5✔
130
                        .as_ref()
5✔
131
                        .context(error::InvalidOperatorSpec {
5✔
132
                            reason: "Histogram on vector input is missing `columnName` field"
5✔
133
                                .to_string(),
5✔
134
                        })?;
5✔
135

136
                let vector_source = vector_source
5✔
137
                    .initialize(path.clone_and_append(0), context)
5✔
138
                    .await?;
×
139

140
                match vector_source
5✔
141
                    .result_descriptor()
5✔
142
                    .column_data_type(column_name)
5✔
143
                {
144
                    None => {
145
                        return Err(Error::ColumnDoesNotExist {
×
146
                            column: column_name.to_string(),
×
147
                        });
×
148
                    }
149
                    Some(FeatureDataType::Category | FeatureDataType::Text) => {
150
                        // TODO: incorporate category data
151
                        return Err(Error::InvalidOperatorSpec {
1✔
152
                            reason: format!("column `{column_name}` must be numerical"),
1✔
153
                        });
1✔
154
                    }
155
                    Some(
156
                        FeatureDataType::Int
157
                        | FeatureDataType::Float
158
                        | FeatureDataType::Bool
159
                        | FeatureDataType::DateTime,
160
                    ) => {
4✔
161
                        // okay
4✔
162
                    }
4✔
163
                }
4✔
164

4✔
165
                let in_desc = vector_source.result_descriptor().clone();
4✔
166

4✔
167
                InitializedHistogram::new(name, in_desc.into(), self.params, vector_source).boxed()
4✔
168
            }
169
        })
170
    }
22✔
171

172
    span_fn!(Histogram);
×
173
}
174

175
/// The initialization of `Histogram`
176
pub struct InitializedHistogram<Op> {
177
    name: CanonicOperatorName,
178
    result_descriptor: PlotResultDescriptor,
179
    metadata: HistogramMetadataOptions,
180
    source: Op,
181
    interactive: bool,
182
    column_name: Option<String>,
183
}
184

185
impl<Op> InitializedHistogram<Op> {
186
    pub fn new(
9✔
187
        name: CanonicOperatorName,
9✔
188
        result_descriptor: PlotResultDescriptor,
9✔
189
        params: HistogramParams,
9✔
190
        source: Op,
9✔
191
    ) -> Self {
9✔
192
        let (min, max) = if let HistogramBounds::Values { min, max } = params.bounds {
9✔
193
            (Some(min), Some(max))
3✔
194
        } else {
195
            (None, None)
6✔
196
        };
197

198
        let (number_of_buckets, max_number_of_buckets) = match params.buckets {
9✔
199
            HistogramBuckets::Number {
200
                value: number_of_buckets,
3✔
201
            } => (Some(number_of_buckets as usize), None),
3✔
202
            HistogramBuckets::SquareRootChoiceRule {
203
                max_number_of_buckets,
6✔
204
            } => (None, Some(max_number_of_buckets as usize)),
6✔
205
        };
206

207
        Self {
9✔
208
            name,
9✔
209
            result_descriptor,
9✔
210
            metadata: HistogramMetadataOptions {
9✔
211
                number_of_buckets,
9✔
212
                max_number_of_buckets,
9✔
213
                min,
9✔
214
                max,
9✔
215
            },
9✔
216
            source,
9✔
217
            interactive: params.interactive,
9✔
218
            column_name: params.column_name,
9✔
219
        }
9✔
220
    }
9✔
221
}
222

223
impl InitializedPlotOperator for InitializedHistogram<Box<dyn InitializedRasterOperator>> {
224
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
5✔
225
        let processor = HistogramRasterQueryProcessor {
5✔
226
            input: self.source.query_processor()?,
5✔
227
            measurement: self.source.result_descriptor().measurement.clone(),
5✔
228
            metadata: self.metadata,
5✔
229
            interactive: self.interactive,
5✔
230
        };
5✔
231

5✔
232
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
5✔
233
    }
5✔
234

235
    fn result_descriptor(&self) -> &PlotResultDescriptor {
1✔
236
        &self.result_descriptor
1✔
237
    }
1✔
238

239
    fn canonic_name(&self) -> CanonicOperatorName {
×
240
        self.name.clone()
×
241
    }
×
242
}
243

244
impl InitializedPlotOperator for InitializedHistogram<Box<dyn InitializedVectorOperator>> {
245
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
4✔
246
        let processor = HistogramVectorQueryProcessor {
4✔
247
            input: self.source.query_processor()?,
4✔
248
            column_name: self.column_name.clone().unwrap_or_default(),
4✔
249
            measurement: self
4✔
250
                .source
4✔
251
                .result_descriptor()
4✔
252
                .column_measurement(self.column_name.as_deref().unwrap_or_default())
4✔
253
                .cloned()
4✔
254
                .into(),
4✔
255
            metadata: self.metadata,
4✔
256
            interactive: self.interactive,
4✔
257
        };
4✔
258

4✔
259
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
4✔
260
    }
4✔
261

262
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
263
        &self.result_descriptor
×
264
    }
×
265

266
    fn canonic_name(&self) -> CanonicOperatorName {
×
267
        self.name.clone()
×
268
    }
×
269
}
270

271
/// A query processor that calculates the Histogram about its raster inputs.
272
pub struct HistogramRasterQueryProcessor {
273
    input: TypedRasterQueryProcessor,
274
    measurement: Measurement,
275
    metadata: HistogramMetadataOptions,
276
    interactive: bool,
277
}
278

279
/// A query processor that calculates the Histogram about its vector inputs.
280
pub struct HistogramVectorQueryProcessor {
281
    input: TypedVectorQueryProcessor,
282
    column_name: String,
283
    measurement: Measurement,
284
    metadata: HistogramMetadataOptions,
285
    interactive: bool,
286
}
287

288
#[async_trait]
289
impl PlotQueryProcessor for HistogramRasterQueryProcessor {
290
    type OutputFormat = PlotData;
291

292
    fn plot_type(&self) -> &'static str {
1✔
293
        HISTOGRAM_OPERATOR_NAME
1✔
294
    }
1✔
295

296
    async fn plot_query<'p>(
5✔
297
        &'p self,
5✔
298
        query: VectorQueryRectangle,
5✔
299
        ctx: &'p dyn QueryContext,
5✔
300
    ) -> Result<Self::OutputFormat> {
5✔
301
        self.preprocess(query, ctx)
5✔
302
            .and_then(move |mut histogram_metadata| async move {
5✔
303
                histogram_metadata.sanitize();
5✔
304
                if histogram_metadata.has_invalid_parameters() {
5✔
305
                    // early return of empty histogram
306
                    return self.empty_histogram();
1✔
307
                }
4✔
308

4✔
309
                self.process(histogram_metadata, query, ctx).await
4✔
310
            })
5✔
311
            .await
×
312
    }
10✔
313
}
314

315
#[async_trait]
316
impl PlotQueryProcessor for HistogramVectorQueryProcessor {
317
    type OutputFormat = PlotData;
318

319
    fn plot_type(&self) -> &'static str {
×
320
        HISTOGRAM_OPERATOR_NAME
×
321
    }
×
322

323
    async fn plot_query<'p>(
4✔
324
        &'p self,
4✔
325
        query: VectorQueryRectangle,
4✔
326
        ctx: &'p dyn QueryContext,
4✔
327
    ) -> Result<Self::OutputFormat> {
4✔
328
        self.preprocess(query, ctx)
4✔
329
            .and_then(move |mut histogram_metadata| async move {
4✔
330
                histogram_metadata.sanitize();
4✔
331
                if histogram_metadata.has_invalid_parameters() {
4✔
332
                    // early return of empty histogram
333
                    return self.empty_histogram();
1✔
334
                }
3✔
335

3✔
336
                self.process(histogram_metadata, query, ctx).await
3✔
337
            })
4✔
338
            .await
×
339
    }
8✔
340
}
341

342
impl HistogramRasterQueryProcessor {
343
    async fn preprocess<'p>(
5✔
344
        &'p self,
5✔
345
        query: VectorQueryRectangle,
5✔
346
        ctx: &'p dyn QueryContext,
5✔
347
    ) -> Result<HistogramMetadata> {
5✔
348
        async fn process_metadata<T: Pixel>(
3✔
349
            mut input: BoxStream<'_, Result<RasterTile2D<T>>>,
3✔
350
            metadata: HistogramMetadataOptions,
3✔
351
        ) -> Result<HistogramMetadata> {
3✔
352
            let mut computed_metadata = HistogramMetadataInProgress::default();
3✔
353

354
            while let Some(tile) = input.next().await {
15✔
355
                match tile?.grid_array {
12✔
356
                    geoengine_datatypes::raster::GridOrEmpty::Grid(g) => {
2✔
357
                        computed_metadata.add_raster_batch(g.masked_element_deref_iterator());
2✔
358
                    }
2✔
359
                    geoengine_datatypes::raster::GridOrEmpty::Empty(_) => {} // TODO: find out if we really do nothing for empty tiles?
10✔
360
                }
361
            }
362

363
            Ok(metadata.merge_with(computed_metadata.into()))
3✔
364
        }
3✔
365

366
        if let Ok(metadata) = HistogramMetadata::try_from(self.metadata) {
5✔
367
            return Ok(metadata);
2✔
368
        }
3✔
369

3✔
370
        // TODO: compute only number of buckets if possible
3✔
371

3✔
372
        call_on_generic_raster_processor!(&self.input, processor => {
3✔
373
            process_metadata(processor.query(query.into(), ctx).await?, self.metadata).await
3✔
374
        })
375
    }
5✔
376

377
    async fn process<'p>(
4✔
378
        &'p self,
4✔
379
        metadata: HistogramMetadata,
4✔
380
        query: VectorQueryRectangle,
4✔
381
        ctx: &'p dyn QueryContext,
4✔
382
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
4✔
383
        let mut histogram = geoengine_datatypes::plots::Histogram::builder(
4✔
384
            metadata.number_of_buckets,
4✔
385
            metadata.min,
4✔
386
            metadata.max,
4✔
387
            self.measurement.clone(),
4✔
388
        )
4✔
389
        .build()
4✔
390
        .map_err(Error::from)?;
4✔
391

392
        call_on_generic_raster_processor!(&self.input, processor => {
4✔
393
            let mut query = processor.query(query.into(), ctx).await?;
4✔
394

395
            while let Some(tile) = query.next().await {
20✔
396

397

398
                match tile?.grid_array {
16✔
399
                    geoengine_datatypes::raster::GridOrEmpty::Grid(g) => histogram.add_raster_data(g.masked_element_deref_iterator()),
4✔
400
                    geoengine_datatypes::raster::GridOrEmpty::Empty(n) => histogram.add_nodata_batch(n.number_of_elements() as u64) // TODO: why u64?
12✔
401
                }
402
            }
403
        });
404

405
        let chart = histogram.to_vega_embeddable(self.interactive)?;
4✔
406

407
        Ok(chart)
4✔
408
    }
4✔
409

410
    fn empty_histogram(
1✔
411
        &self,
1✔
412
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
1✔
413
        let histogram =
1✔
414
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., self.measurement.clone())
1✔
415
                .build()
1✔
416
                .map_err(Error::from)?;
1✔
417

418
        let chart = histogram.to_vega_embeddable(self.interactive)?;
1✔
419

420
        Ok(chart)
1✔
421
    }
1✔
422
}
423

424
impl HistogramVectorQueryProcessor {
425
    async fn preprocess<'p>(
4✔
426
        &'p self,
4✔
427
        query: VectorQueryRectangle,
4✔
428
        ctx: &'p dyn QueryContext,
4✔
429
    ) -> Result<HistogramMetadata> {
4✔
430
        async fn process_metadata<'m, G>(
3✔
431
            mut input: BoxStream<'m, Result<FeatureCollection<G>>>,
3✔
432
            column_name: &'m str,
3✔
433
            metadata: HistogramMetadataOptions,
3✔
434
        ) -> Result<HistogramMetadata>
3✔
435
        where
3✔
436
            G: Geometry + 'static,
3✔
437
            FeatureCollection<G>: FeatureCollectionInfos,
3✔
438
        {
3✔
439
            let mut computed_metadata = HistogramMetadataInProgress::default();
3✔
440

441
            while let Some(collection) = input.next().await {
6✔
442
                let collection = collection?;
3✔
443

444
                let feature_data = collection.data(column_name).expect("check in param");
3✔
445
                computed_metadata.add_vector_batch(feature_data);
3✔
446
            }
447

448
            Ok(metadata.merge_with(computed_metadata.into()))
3✔
449
        }
3✔
450

451
        if let Ok(metadata) = HistogramMetadata::try_from(self.metadata) {
4✔
452
            return Ok(metadata);
1✔
453
        }
3✔
454

3✔
455
        // TODO: compute only number of buckets if possible
3✔
456

3✔
457
        call_on_generic_vector_processor!(&self.input, processor => {
3✔
458
            process_metadata(processor.query(query, ctx).await?, &self.column_name, self.metadata).await
3✔
459
        })
460
    }
4✔
461

462
    async fn process<'p>(
3✔
463
        &'p self,
3✔
464
        metadata: HistogramMetadata,
3✔
465
        query: VectorQueryRectangle,
3✔
466
        ctx: &'p dyn QueryContext,
3✔
467
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
3✔
468
        let mut histogram = geoengine_datatypes::plots::Histogram::builder(
3✔
469
            metadata.number_of_buckets,
3✔
470
            metadata.min,
3✔
471
            metadata.max,
3✔
472
            self.measurement.clone(),
3✔
473
        )
3✔
474
        .build()
3✔
475
        .map_err(Error::from)?;
3✔
476

477
        call_on_generic_vector_processor!(&self.input, processor => {
3✔
478
            let mut query = processor.query(query, ctx).await?;
3✔
479

480
            while let Some(collection) = query.next().await {
7✔
481
                let collection = collection?;
4✔
482

483
                let feature_data = collection.data(&self.column_name).expect("checked in param");
4✔
484

485
                histogram.add_feature_data(feature_data)?;
4✔
486
            }
487
        });
488

489
        let chart = histogram.to_vega_embeddable(self.interactive)?;
3✔
490

491
        Ok(chart)
3✔
492
    }
3✔
493

494
    fn empty_histogram(
1✔
495
        &self,
1✔
496
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
1✔
497
        let histogram =
1✔
498
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., self.measurement.clone())
1✔
499
                .build()
1✔
500
                .map_err(Error::from)?;
1✔
501

502
        let chart = histogram.to_vega_embeddable(self.interactive)?;
1✔
503

504
        Ok(chart)
1✔
505
    }
1✔
506
}
507

508
#[derive(Debug, Copy, Clone, PartialEq)]
×
509
struct HistogramMetadata {
510
    pub number_of_buckets: usize,
511
    pub min: f64,
512
    pub max: f64,
513
}
514

515
impl HistogramMetadata {
516
    /// Fix invalid configurations if they are fixeable
517
    fn sanitize(&mut self) {
9✔
518
        // prevent the rare case that min=max and you have more than one bucket
9✔
519
        if approx_eq!(f64, self.min, self.max) && self.number_of_buckets > 1 {
9✔
520
            self.number_of_buckets = 1;
1✔
521
        }
8✔
522
    }
9✔
523

524
    fn has_invalid_parameters(&self) -> bool {
9✔
525
        self.number_of_buckets == 0 || self.min > self.max
9✔
526
    }
9✔
527
}
528

529
#[derive(Debug, Copy, Clone, PartialEq)]
×
530
struct HistogramMetadataOptions {
531
    pub number_of_buckets: Option<usize>,
532
    pub max_number_of_buckets: Option<usize>,
533
    pub min: Option<f64>,
534
    pub max: Option<f64>,
535
}
536

537
impl TryFrom<HistogramMetadataOptions> for HistogramMetadata {
538
    type Error = ();
539

540
    fn try_from(options: HistogramMetadataOptions) -> Result<Self, Self::Error> {
541
        match (options.number_of_buckets, options.min, options.max) {
9✔
542
            (Some(number_of_buckets), Some(min), Some(max)) => Ok(Self {
3✔
543
                number_of_buckets,
3✔
544
                min,
3✔
545
                max,
3✔
546
            }),
3✔
547
            _ => Err(()),
6✔
548
        }
549
    }
9✔
550
}
551

552
impl HistogramMetadataOptions {
553
    fn merge_with(self, metadata: HistogramMetadata) -> HistogramMetadata {
6✔
554
        let number_of_buckets = if let Some(number_of_buckets) = self.number_of_buckets {
6✔
555
            number_of_buckets
×
556
        } else if let Some(max_number_of_buckets) = self.max_number_of_buckets {
6✔
557
            metadata.number_of_buckets.min(max_number_of_buckets)
6✔
558
        } else {
559
            metadata.number_of_buckets
×
560
        };
561

562
        HistogramMetadata {
6✔
563
            number_of_buckets,
6✔
564
            min: self.min.unwrap_or(metadata.min),
6✔
565
            max: self.max.unwrap_or(metadata.max),
6✔
566
        }
6✔
567
    }
6✔
568
}
569

570
#[derive(Debug, Copy, Clone, PartialEq)]
×
571
struct HistogramMetadataInProgress {
572
    pub n: usize,
573
    pub min: f64,
574
    pub max: f64,
575
}
576

577
impl Default for HistogramMetadataInProgress {
578
    fn default() -> Self {
6✔
579
        Self {
6✔
580
            n: 0,
6✔
581
            min: f64::MAX,
6✔
582
            max: f64::MIN,
6✔
583
        }
6✔
584
    }
6✔
585
}
586

587
impl HistogramMetadataInProgress {
588
    #[inline]
589
    fn add_raster_batch<T: Pixel, I: Iterator<Item = Option<T>>>(&mut self, values: I) {
2✔
590
        values.for_each(|pixel_option| {
2✔
591
            if let Some(p) = pixel_option {
12✔
592
                self.n += 1;
12✔
593
                self.update_minmax(p.as_());
12✔
594
            }
12✔
595
        });
12✔
596
    }
2✔
597

598
    #[inline]
599
    fn add_vector_batch(&mut self, values: FeatureDataRef) {
3✔
600
        fn add_data_ref<'d, D, T>(metadata: &mut HistogramMetadataInProgress, data_ref: &'d D)
3✔
601
        where
3✔
602
            D: DataRef<'d, T>,
3✔
603
            T: 'static,
3✔
604
        {
3✔
605
            for v in data_ref.float_options_iter().flatten() {
5✔
606
                metadata.n += 1;
5✔
607
                metadata.update_minmax(v);
5✔
608
            }
5✔
609
        }
3✔
610

3✔
611
        match values {
3✔
612
            FeatureDataRef::Int(values) => {
×
613
                add_data_ref(self, &values);
×
614
            }
×
615
            FeatureDataRef::Float(values) => {
3✔
616
                add_data_ref(self, &values);
3✔
617
            }
3✔
618
            FeatureDataRef::Bool(values) => {
×
619
                add_data_ref(self, &values);
×
620
            }
×
621
            FeatureDataRef::DateTime(values) => {
×
622
                add_data_ref(self, &values);
×
623
            }
×
624
            FeatureDataRef::Category(_) | FeatureDataRef::Text(_) => {
×
625
                // do nothing since we don't support them
×
626
                // TODO: fill with live once we support category and text types
×
627
            }
×
628
        }
629
    }
3✔
630

631
    #[inline]
632
    fn update_minmax(&mut self, value: f64) {
17✔
633
        self.min = f64::min(self.min, value);
17✔
634
        self.max = f64::max(self.max, value);
17✔
635
    }
17✔
636
}
637

638
impl From<HistogramMetadataInProgress> for HistogramMetadata {
639
    fn from(metadata: HistogramMetadataInProgress) -> Self {
6✔
640
        Self {
6✔
641
            number_of_buckets: f64::sqrt(metadata.n as f64) as usize,
6✔
642
            min: metadata.min,
6✔
643
            max: metadata.max,
6✔
644
        }
6✔
645
    }
6✔
646
}
647

648
#[cfg(test)]
649
mod tests {
650
    use super::*;
651

652
    use crate::engine::{
653
        ChunkByteSize, MockExecutionContext, MockQueryContext, RasterOperator,
654
        RasterResultDescriptor, StaticMetaData, VectorColumnInfo, VectorOperator,
655
        VectorResultDescriptor,
656
    };
657
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
658
    use crate::source::{
659
        OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceErrorSpec,
660
    };
661
    use crate::test_data;
662
    use geoengine_datatypes::dataset::{DataId, DatasetId};
663
    use geoengine_datatypes::primitives::{
664
        BoundingBox2D, DateTime, FeatureData, NoGeometry, SpatialResolution, TimeInterval,
665
    };
666
    use geoengine_datatypes::raster::{
667
        EmptyGrid2D, Grid2D, RasterDataType, RasterTile2D, TileInformation, TilingSpecification,
668
    };
669
    use geoengine_datatypes::spatial_reference::SpatialReference;
670
    use geoengine_datatypes::util::test::TestDefault;
671
    use geoengine_datatypes::util::Identifier;
672
    use geoengine_datatypes::{
673
        collections::{DataCollection, VectorDataType},
674
        primitives::MultiPoint,
675
    };
676
    use serde_json::json;
677

678
    #[test]
1✔
679
    fn serialization() {
1✔
680
        let histogram = Histogram {
1✔
681
            params: HistogramParams {
1✔
682
                column_name: Some("foobar".to_string()),
1✔
683
                bounds: HistogramBounds::Values {
1✔
684
                    min: 5.0,
1✔
685
                    max: 10.0,
1✔
686
                },
1✔
687
                buckets: HistogramBuckets::Number { value: 15 },
1✔
688
                interactive: false,
1✔
689
            },
1✔
690
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
691
                .boxed()
1✔
692
                .into(),
1✔
693
        };
1✔
694

1✔
695
        let serialized = json!({
1✔
696
            "type": "Histogram",
1✔
697
            "params": {
1✔
698
                "columnName": "foobar",
1✔
699
                "bounds": {
1✔
700
                    "min": 5.0,
1✔
701
                    "max": 10.0,
1✔
702
                },
1✔
703
                "buckets": {
1✔
704
                    "type": "number",
1✔
705
                    "value": 15,
1✔
706
                },
1✔
707
                "interactivity": false,
1✔
708
            },
1✔
709
            "sources": {
1✔
710
                "source": {
1✔
711
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
712
                    "params": {
1✔
713
                        "collections": [],
1✔
714
                        "spatialReference": "EPSG:4326",
1✔
715
                        "measurements": {},
1✔
716
                    }
1✔
717
                }
1✔
718
            }
1✔
719
        })
1✔
720
        .to_string();
1✔
721

1✔
722
        let deserialized: Histogram = serde_json::from_str(&serialized).unwrap();
1✔
723

1✔
724
        assert_eq!(deserialized.params, histogram.params);
1✔
725
    }
1✔
726

727
    #[test]
1✔
728
    fn serialization_alt() {
1✔
729
        let histogram = Histogram {
1✔
730
            params: HistogramParams {
1✔
731
                column_name: None,
1✔
732
                bounds: HistogramBounds::Data(Default::default()),
1✔
733
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
734
                    max_number_of_buckets: 100,
1✔
735
                },
1✔
736
                interactive: false,
1✔
737
            },
1✔
738
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
739
                .boxed()
1✔
740
                .into(),
1✔
741
        };
1✔
742

1✔
743
        let serialized = json!({
1✔
744
            "type": "Histogram",
1✔
745
            "params": {
1✔
746
                "bounds": "data",
1✔
747
                "buckets": {
1✔
748
                    "type": "squareRootChoiceRule",
1✔
749
                    "maxNumberOfBuckets": 100,
1✔
750
                },
1✔
751
            },
1✔
752
            "sources": {
1✔
753
                "source": {
1✔
754
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
755
                    "params": {
1✔
756
                        "collections": [],
1✔
757
                        "spatialReference": "EPSG:4326",
1✔
758
                        "measurements": {},
1✔
759
                    }
1✔
760
                }
1✔
761
            }
1✔
762
        })
1✔
763
        .to_string();
1✔
764

1✔
765
        let deserialized: Histogram = serde_json::from_str(&serialized).unwrap();
1✔
766

1✔
767
        assert_eq!(deserialized.params, histogram.params);
1✔
768
    }
1✔
769

770
    #[tokio::test]
1✔
771
    async fn column_name_for_raster_source() {
1✔
772
        let histogram = Histogram {
1✔
773
            params: HistogramParams {
1✔
774
                column_name: Some("foo".to_string()),
1✔
775
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
776
                buckets: HistogramBuckets::Number { value: 3 },
1✔
777
                interactive: false,
1✔
778
            },
1✔
779
            sources: mock_raster_source().into(),
1✔
780
        };
1✔
781

1✔
782
        let execution_context = MockExecutionContext::test_default();
1✔
783

784
        assert!(histogram
1✔
785
            .boxed()
1✔
786
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
787
            .await
×
788
            .is_err());
1✔
789
    }
790

791
    fn mock_raster_source() -> Box<dyn RasterOperator> {
3✔
792
        MockRasterSource {
3✔
793
            params: MockRasterSourceParams {
3✔
794
                data: vec![RasterTile2D::new_with_tile_info(
3✔
795
                    TimeInterval::default(),
3✔
796
                    TileInformation {
3✔
797
                        global_geo_transform: TestDefault::test_default(),
3✔
798
                        global_tile_position: [0, 0].into(),
3✔
799
                        tile_size_in_pixels: [3, 2].into(),
3✔
800
                    },
3✔
801
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
3✔
802
                        .unwrap()
3✔
803
                        .into(),
3✔
804
                )],
3✔
805
                result_descriptor: RasterResultDescriptor {
3✔
806
                    data_type: RasterDataType::U8,
3✔
807
                    spatial_reference: SpatialReference::epsg_4326().into(),
3✔
808
                    measurement: Measurement::Unitless,
3✔
809
                    time: None,
3✔
810
                    bbox: None,
3✔
811
                    resolution: None,
3✔
812
                },
3✔
813
            },
3✔
814
        }
3✔
815
        .boxed()
3✔
816
    }
3✔
817

818
    #[tokio::test]
1✔
819
    async fn simple_raster() {
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
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
826

1✔
827
        let histogram = Histogram {
1✔
828
            params: HistogramParams {
1✔
829
                column_name: None,
1✔
830
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
831
                buckets: HistogramBuckets::Number { value: 3 },
1✔
832
                interactive: false,
1✔
833
            },
1✔
834
            sources: mock_raster_source().into(),
1✔
835
        };
1✔
836

837
        let query_processor = histogram
1✔
838
            .boxed()
1✔
839
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
840
            .await
×
841
            .unwrap()
1✔
842
            .query_processor()
1✔
843
            .unwrap()
1✔
844
            .json_vega()
1✔
845
            .unwrap();
1✔
846

847
        let result = query_processor
1✔
848
            .plot_query(
1✔
849
                VectorQueryRectangle {
1✔
850
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
851
                    time_interval: TimeInterval::default(),
1✔
852
                    spatial_resolution: SpatialResolution::one(),
1✔
853
                },
1✔
854
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
855
            )
1✔
856
            .await
×
857
            .unwrap();
1✔
858

1✔
859
        assert_eq!(
1✔
860
            result,
1✔
861
            geoengine_datatypes::plots::Histogram::builder(3, 0., 8., Measurement::Unitless)
1✔
862
                .counts(vec![2, 3, 1])
1✔
863
                .build()
1✔
864
                .unwrap()
1✔
865
                .to_vega_embeddable(false)
1✔
866
                .unwrap()
1✔
867
        );
1✔
868
    }
869

870
    #[tokio::test]
1✔
871
    async fn simple_raster_without_spec() {
1✔
872
        let tile_size_in_pixels = [3, 2].into();
1✔
873
        let tiling_specification = TilingSpecification {
1✔
874
            origin_coordinate: [0.0, 0.0].into(),
1✔
875
            tile_size_in_pixels,
1✔
876
        };
1✔
877
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
878

1✔
879
        let histogram = Histogram {
1✔
880
            params: HistogramParams {
1✔
881
                column_name: None,
1✔
882
                bounds: HistogramBounds::Data(Default::default()),
1✔
883
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
884
                    max_number_of_buckets: 100,
1✔
885
                },
1✔
886
                interactive: false,
1✔
887
            },
1✔
888
            sources: mock_raster_source().into(),
1✔
889
        };
1✔
890

891
        let query_processor = histogram
1✔
892
            .boxed()
1✔
893
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
894
            .await
×
895
            .unwrap()
1✔
896
            .query_processor()
1✔
897
            .unwrap()
1✔
898
            .json_vega()
1✔
899
            .unwrap();
1✔
900

901
        let result = query_processor
1✔
902
            .plot_query(
1✔
903
                VectorQueryRectangle {
1✔
904
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
905
                    time_interval: TimeInterval::default(),
1✔
906
                    spatial_resolution: SpatialResolution::one(),
1✔
907
                },
1✔
908
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
909
            )
1✔
910
            .await
×
911
            .unwrap();
1✔
912

1✔
913
        assert_eq!(
1✔
914
            result,
1✔
915
            geoengine_datatypes::plots::Histogram::builder(2, 1., 6., Measurement::Unitless)
1✔
916
                .counts(vec![3, 3])
1✔
917
                .build()
1✔
918
                .unwrap()
1✔
919
                .to_vega_embeddable(false)
1✔
920
                .unwrap()
1✔
921
        );
1✔
922
    }
923

924
    #[tokio::test]
1✔
925
    async fn vector_data() {
1✔
926
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
927
            DataCollection::from_slices(
1✔
928
                &[] as &[NoGeometry],
1✔
929
                &[TimeInterval::default(); 8],
1✔
930
                &[("foo", FeatureData::Int(vec![1, 1, 2, 2, 3, 3, 4, 4]))],
1✔
931
            )
1✔
932
            .unwrap(),
1✔
933
            DataCollection::from_slices(
1✔
934
                &[] as &[NoGeometry],
1✔
935
                &[TimeInterval::default(); 4],
1✔
936
                &[("foo", FeatureData::Int(vec![5, 6, 7, 8]))],
1✔
937
            )
1✔
938
            .unwrap(),
1✔
939
        ])
1✔
940
        .boxed();
1✔
941

1✔
942
        let histogram = Histogram {
1✔
943
            params: HistogramParams {
1✔
944
                column_name: Some("foo".to_string()),
1✔
945
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
946
                buckets: HistogramBuckets::Number { value: 3 },
1✔
947
                interactive: true,
1✔
948
            },
1✔
949
            sources: vector_source.into(),
1✔
950
        };
1✔
951

1✔
952
        let execution_context = MockExecutionContext::test_default();
1✔
953

954
        let query_processor = histogram
1✔
955
            .boxed()
1✔
956
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
957
            .await
×
958
            .unwrap()
1✔
959
            .query_processor()
1✔
960
            .unwrap()
1✔
961
            .json_vega()
1✔
962
            .unwrap();
1✔
963

964
        let result = query_processor
1✔
965
            .plot_query(
1✔
966
                VectorQueryRectangle {
1✔
967
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
968
                        .unwrap(),
1✔
969
                    time_interval: TimeInterval::default(),
1✔
970
                    spatial_resolution: SpatialResolution::one(),
1✔
971
                },
1✔
972
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
973
            )
1✔
974
            .await
×
975
            .unwrap();
1✔
976

1✔
977
        assert_eq!(
1✔
978
            result,
1✔
979
            geoengine_datatypes::plots::Histogram::builder(3, 0., 8., Measurement::Unitless)
1✔
980
                .counts(vec![4, 5, 3])
1✔
981
                .build()
1✔
982
                .unwrap()
1✔
983
                .to_vega_embeddable(true)
1✔
984
                .unwrap()
1✔
985
        );
1✔
986
    }
987

988
    #[tokio::test]
1✔
989
    async fn vector_data_with_nulls() {
1✔
990
        let vector_source = MockFeatureCollectionSource::single(
1✔
991
            DataCollection::from_slices(
1✔
992
                &[] as &[NoGeometry],
1✔
993
                &[TimeInterval::default(); 6],
1✔
994
                &[(
1✔
995
                    "foo",
1✔
996
                    FeatureData::NullableFloat(vec![
1✔
997
                        Some(1.),
1✔
998
                        Some(2.),
1✔
999
                        None,
1✔
1000
                        Some(4.),
1✔
1001
                        None,
1✔
1002
                        Some(5.),
1✔
1003
                    ]),
1✔
1004
                )],
1✔
1005
            )
1✔
1006
            .unwrap(),
1✔
1007
        )
1✔
1008
        .boxed();
1✔
1009

1✔
1010
        let histogram = Histogram {
1✔
1011
            params: HistogramParams {
1✔
1012
                column_name: Some("foo".to_string()),
1✔
1013
                bounds: HistogramBounds::Data(Default::default()),
1✔
1014
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1015
                    max_number_of_buckets: 100,
1✔
1016
                },
1✔
1017
                interactive: false,
1✔
1018
            },
1✔
1019
            sources: vector_source.into(),
1✔
1020
        };
1✔
1021

1✔
1022
        let execution_context = MockExecutionContext::test_default();
1✔
1023

1024
        let query_processor = histogram
1✔
1025
            .boxed()
1✔
1026
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1027
            .await
×
1028
            .unwrap()
1✔
1029
            .query_processor()
1✔
1030
            .unwrap()
1✔
1031
            .json_vega()
1✔
1032
            .unwrap();
1✔
1033

1034
        let result = query_processor
1✔
1035
            .plot_query(
1✔
1036
                VectorQueryRectangle {
1✔
1037
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1038
                        .unwrap(),
1✔
1039
                    time_interval: TimeInterval::default(),
1✔
1040
                    spatial_resolution: SpatialResolution::one(),
1✔
1041
                },
1✔
1042
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1043
            )
1✔
1044
            .await
×
1045
            .unwrap();
1✔
1046

1✔
1047
        assert_eq!(
1✔
1048
            result,
1✔
1049
            geoengine_datatypes::plots::Histogram::builder(2, 1., 5., Measurement::Unitless)
1✔
1050
                .counts(vec![2, 2])
1✔
1051
                .build()
1✔
1052
                .unwrap()
1✔
1053
                .to_vega_embeddable(false)
1✔
1054
                .unwrap()
1✔
1055
        );
1✔
1056
    }
1057

1058
    #[tokio::test]
1✔
1059
    #[allow(clippy::too_many_lines)]
1060
    async fn text_attribute() {
1✔
1061
        let dataset_id = DatasetId::new();
1✔
1062

1✔
1063
        let workflow = serde_json::json!({
1✔
1064
            "type": "Histogram",
1✔
1065
            "params": {
1✔
1066
                "columnName": "featurecla",
1✔
1067
                "bounds": "data",
1✔
1068
                "buckets": {
1✔
1069
                    "type": "squareRootChoiceRule",
1✔
1070
                    "maxNumberOfBuckets": 100,
1✔
1071
                }
1✔
1072
            },
1✔
1073
            "sources": {
1✔
1074
                "source": {
1✔
1075
                    "type": "OgrSource",
1✔
1076
                    "params": {
1✔
1077
                        "data": {
1✔
1078
                            "type": "internal",
1✔
1079
                            "datasetId": dataset_id
1✔
1080
                        },
1✔
1081
                        "attributeProjection": null
1✔
1082
                    },
1✔
1083
                }
1✔
1084
            }
1✔
1085
        });
1✔
1086
        let histogram: Histogram = serde_json::from_value(workflow).unwrap();
1✔
1087

1✔
1088
        let mut execution_context = MockExecutionContext::test_default();
1✔
1089
        execution_context.add_meta_data::<_, _, VectorQueryRectangle>(
1✔
1090
            DataId::Internal { dataset_id },
1✔
1091
            Box::new(StaticMetaData {
1✔
1092
                loading_info: OgrSourceDataset {
1✔
1093
                    file_name: test_data!("vector/data/ne_10m_ports/ne_10m_ports.shp").into(),
1✔
1094
                    layer_name: "ne_10m_ports".to_string(),
1✔
1095
                    data_type: Some(VectorDataType::MultiPoint),
1✔
1096
                    time: OgrSourceDatasetTimeType::None,
1✔
1097
                    default_geometry: None,
1✔
1098
                    columns: Some(OgrSourceColumnSpec {
1✔
1099
                        format_specifics: None,
1✔
1100
                        x: String::new(),
1✔
1101
                        y: None,
1✔
1102
                        int: vec!["natlscale".to_string()],
1✔
1103
                        float: vec!["scalerank".to_string()],
1✔
1104
                        text: vec![
1✔
1105
                            "featurecla".to_string(),
1✔
1106
                            "name".to_string(),
1✔
1107
                            "website".to_string(),
1✔
1108
                        ],
1✔
1109
                        bool: vec![],
1✔
1110
                        datetime: vec![],
1✔
1111
                        rename: None,
1✔
1112
                    }),
1✔
1113
                    force_ogr_time_filter: false,
1✔
1114
                    force_ogr_spatial_filter: false,
1✔
1115
                    on_error: OgrSourceErrorSpec::Ignore,
1✔
1116
                    sql_query: None,
1✔
1117
                    attribute_query: None,
1✔
1118
                },
1✔
1119
                result_descriptor: VectorResultDescriptor {
1✔
1120
                    data_type: VectorDataType::MultiPoint,
1✔
1121
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1122
                    columns: [
1✔
1123
                        (
1✔
1124
                            "natlscale".to_string(),
1✔
1125
                            VectorColumnInfo {
1✔
1126
                                data_type: FeatureDataType::Float,
1✔
1127
                                measurement: Measurement::Unitless,
1✔
1128
                            },
1✔
1129
                        ),
1✔
1130
                        (
1✔
1131
                            "scalerank".to_string(),
1✔
1132
                            VectorColumnInfo {
1✔
1133
                                data_type: FeatureDataType::Int,
1✔
1134
                                measurement: Measurement::Unitless,
1✔
1135
                            },
1✔
1136
                        ),
1✔
1137
                        (
1✔
1138
                            "featurecla".to_string(),
1✔
1139
                            VectorColumnInfo {
1✔
1140
                                data_type: FeatureDataType::Text,
1✔
1141
                                measurement: Measurement::Unitless,
1✔
1142
                            },
1✔
1143
                        ),
1✔
1144
                        (
1✔
1145
                            "name".to_string(),
1✔
1146
                            VectorColumnInfo {
1✔
1147
                                data_type: FeatureDataType::Text,
1✔
1148
                                measurement: Measurement::Unitless,
1✔
1149
                            },
1✔
1150
                        ),
1✔
1151
                        (
1✔
1152
                            "website".to_string(),
1✔
1153
                            VectorColumnInfo {
1✔
1154
                                data_type: FeatureDataType::Text,
1✔
1155
                                measurement: Measurement::Unitless,
1✔
1156
                            },
1✔
1157
                        ),
1✔
1158
                    ]
1✔
1159
                    .iter()
1✔
1160
                    .cloned()
1✔
1161
                    .collect(),
1✔
1162
                    time: None,
1✔
1163
                    bbox: None,
1✔
1164
                },
1✔
1165
                phantom: Default::default(),
1✔
1166
            }),
1✔
1167
        );
1✔
1168

1169
        if let Err(Error::InvalidOperatorSpec { reason }) = histogram
1✔
1170
            .boxed()
1✔
1171
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1172
            .await
×
1173
        {
1174
            assert_eq!(reason, "column `featurecla` must be numerical");
1✔
1175
        } else {
1176
            panic!("we currently don't support text features, but this went through");
×
1177
        }
1178
    }
1179

1180
    #[tokio::test]
1✔
1181
    async fn no_data_raster() {
1✔
1182
        let tile_size_in_pixels = [3, 2].into();
1✔
1183
        let tiling_specification = TilingSpecification {
1✔
1184
            origin_coordinate: [0.0, 0.0].into(),
1✔
1185
            tile_size_in_pixels,
1✔
1186
        };
1✔
1187
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1188
        let histogram = Histogram {
1✔
1189
            params: HistogramParams {
1✔
1190
                column_name: None,
1✔
1191
                bounds: HistogramBounds::Data(Data::default()),
1✔
1192
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1193
                    max_number_of_buckets: 100,
1✔
1194
                },
1✔
1195
                interactive: false,
1✔
1196
            },
1✔
1197
            sources: MockRasterSource {
1✔
1198
                params: MockRasterSourceParams {
1✔
1199
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1200
                        TimeInterval::default(),
1✔
1201
                        TileInformation {
1✔
1202
                            global_geo_transform: TestDefault::test_default(),
1✔
1203
                            global_tile_position: [0, 0].into(),
1✔
1204
                            tile_size_in_pixels,
1✔
1205
                        },
1✔
1206
                        EmptyGrid2D::<u8>::new(tile_size_in_pixels).into(),
1✔
1207
                    )],
1✔
1208
                    result_descriptor: RasterResultDescriptor {
1✔
1209
                        data_type: RasterDataType::U8,
1✔
1210
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1211
                        measurement: Measurement::Unitless,
1✔
1212
                        time: None,
1✔
1213
                        bbox: None,
1✔
1214
                        resolution: None,
1✔
1215
                    },
1✔
1216
                },
1✔
1217
            }
1✔
1218
            .boxed()
1✔
1219
            .into(),
1✔
1220
        };
1✔
1221

1222
        let query_processor = histogram
1✔
1223
            .boxed()
1✔
1224
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1225
            .await
×
1226
            .unwrap()
1✔
1227
            .query_processor()
1✔
1228
            .unwrap()
1✔
1229
            .json_vega()
1✔
1230
            .unwrap();
1✔
1231

1232
        let result = query_processor
1✔
1233
            .plot_query(
1✔
1234
                VectorQueryRectangle {
1✔
1235
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
1236
                    time_interval: TimeInterval::default(),
1✔
1237
                    spatial_resolution: SpatialResolution::one(),
1✔
1238
                },
1✔
1239
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1240
            )
1✔
1241
            .await
×
1242
            .unwrap();
1✔
1243

1✔
1244
        assert_eq!(
1✔
1245
            result,
1✔
1246
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., Measurement::Unitless)
1✔
1247
                .build()
1✔
1248
                .unwrap()
1✔
1249
                .to_vega_embeddable(false)
1✔
1250
                .unwrap()
1✔
1251
        );
1✔
1252
    }
1253

1254
    #[tokio::test]
1✔
1255
    async fn empty_feature_collection() {
1✔
1256
        let vector_source = MockFeatureCollectionSource::single(
1✔
1257
            DataCollection::from_slices(
1✔
1258
                &[] as &[NoGeometry],
1✔
1259
                &[] as &[TimeInterval],
1✔
1260
                &[("foo", FeatureData::Float(vec![]))],
1✔
1261
            )
1✔
1262
            .unwrap(),
1✔
1263
        )
1✔
1264
        .boxed();
1✔
1265

1✔
1266
        let histogram = Histogram {
1✔
1267
            params: HistogramParams {
1✔
1268
                column_name: Some("foo".to_string()),
1✔
1269
                bounds: HistogramBounds::Data(Default::default()),
1✔
1270
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1271
                    max_number_of_buckets: 100,
1✔
1272
                },
1✔
1273
                interactive: false,
1✔
1274
            },
1✔
1275
            sources: vector_source.into(),
1✔
1276
        };
1✔
1277

1✔
1278
        let execution_context = MockExecutionContext::test_default();
1✔
1279

1280
        let query_processor = histogram
1✔
1281
            .boxed()
1✔
1282
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1283
            .await
×
1284
            .unwrap()
1✔
1285
            .query_processor()
1✔
1286
            .unwrap()
1✔
1287
            .json_vega()
1✔
1288
            .unwrap();
1✔
1289

1290
        let result = query_processor
1✔
1291
            .plot_query(
1✔
1292
                VectorQueryRectangle {
1✔
1293
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1294
                        .unwrap(),
1✔
1295
                    time_interval: TimeInterval::default(),
1✔
1296
                    spatial_resolution: SpatialResolution::one(),
1✔
1297
                },
1✔
1298
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1299
            )
1✔
1300
            .await
×
1301
            .unwrap();
1✔
1302

1✔
1303
        assert_eq!(
1✔
1304
            result,
1✔
1305
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., Measurement::Unitless)
1✔
1306
                .build()
1✔
1307
                .unwrap()
1✔
1308
                .to_vega_embeddable(false)
1✔
1309
                .unwrap()
1✔
1310
        );
1✔
1311
    }
1312

1313
    #[tokio::test]
1✔
1314
    async fn feature_collection_with_one_feature() {
1✔
1315
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
1316
            vec![DataCollection::from_slices(
1✔
1317
                &[] as &[NoGeometry],
1✔
1318
                &[TimeInterval::default()],
1✔
1319
                &[("foo", FeatureData::Float(vec![5.0]))],
1✔
1320
            )
1✔
1321
            .unwrap()],
1✔
1322
            [(
1✔
1323
                "foo".to_string(),
1✔
1324
                Measurement::continuous("bar".to_string(), None),
1✔
1325
            )]
1✔
1326
            .into_iter()
1✔
1327
            .collect(),
1✔
1328
        )
1✔
1329
        .boxed();
1✔
1330

1✔
1331
        let histogram = Histogram {
1✔
1332
            params: HistogramParams {
1✔
1333
                column_name: Some("foo".to_string()),
1✔
1334
                bounds: HistogramBounds::Data(Default::default()),
1✔
1335
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1336
                    max_number_of_buckets: 100,
1✔
1337
                },
1✔
1338
                interactive: false,
1✔
1339
            },
1✔
1340
            sources: vector_source.into(),
1✔
1341
        };
1✔
1342

1✔
1343
        let execution_context = MockExecutionContext::test_default();
1✔
1344

1345
        let query_processor = histogram
1✔
1346
            .boxed()
1✔
1347
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1348
            .await
×
1349
            .unwrap()
1✔
1350
            .query_processor()
1✔
1351
            .unwrap()
1✔
1352
            .json_vega()
1✔
1353
            .unwrap();
1✔
1354

1355
        let result = query_processor
1✔
1356
            .plot_query(
1✔
1357
                VectorQueryRectangle {
1✔
1358
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1359
                        .unwrap(),
1✔
1360
                    time_interval: TimeInterval::default(),
1✔
1361
                    spatial_resolution: SpatialResolution::one(),
1✔
1362
                },
1✔
1363
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1364
            )
1✔
1365
            .await
×
1366
            .unwrap();
1✔
1367

1✔
1368
        assert_eq!(
1✔
1369
            result,
1✔
1370
            geoengine_datatypes::plots::Histogram::builder(
1✔
1371
                1,
1✔
1372
                5.,
1✔
1373
                5.,
1✔
1374
                Measurement::continuous("bar".to_string(), None)
1✔
1375
            )
1✔
1376
            .counts(vec![1])
1✔
1377
            .build()
1✔
1378
            .unwrap()
1✔
1379
            .to_vega_embeddable(false)
1✔
1380
            .unwrap()
1✔
1381
        );
1✔
1382
    }
1383

1384
    #[tokio::test]
1✔
1385
    async fn single_value_raster_stream() {
1✔
1386
        let tile_size_in_pixels = [3, 2].into();
1✔
1387
        let tiling_specification = TilingSpecification {
1✔
1388
            origin_coordinate: [0.0, 0.0].into(),
1✔
1389
            tile_size_in_pixels,
1✔
1390
        };
1✔
1391
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1392
        let histogram = Histogram {
1✔
1393
            params: HistogramParams {
1✔
1394
                column_name: None,
1✔
1395
                bounds: HistogramBounds::Data(Data::default()),
1✔
1396
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1397
                    max_number_of_buckets: 100,
1✔
1398
                },
1✔
1399
                interactive: false,
1✔
1400
            },
1✔
1401
            sources: MockRasterSource {
1✔
1402
                params: MockRasterSourceParams {
1✔
1403
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1404
                        TimeInterval::default(),
1✔
1405
                        TileInformation {
1✔
1406
                            global_geo_transform: TestDefault::test_default(),
1✔
1407
                            global_tile_position: [0, 0].into(),
1✔
1408
                            tile_size_in_pixels,
1✔
1409
                        },
1✔
1410
                        Grid2D::new(tile_size_in_pixels, vec![4; 6]).unwrap().into(),
1✔
1411
                    )],
1✔
1412
                    result_descriptor: RasterResultDescriptor {
1✔
1413
                        data_type: RasterDataType::U8,
1✔
1414
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1415
                        measurement: Measurement::Unitless,
1✔
1416
                        time: None,
1✔
1417
                        bbox: None,
1✔
1418
                        resolution: None,
1✔
1419
                    },
1✔
1420
                },
1✔
1421
            }
1✔
1422
            .boxed()
1✔
1423
            .into(),
1✔
1424
        };
1✔
1425

1426
        let query_processor = histogram
1✔
1427
            .boxed()
1✔
1428
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1429
            .await
×
1430
            .unwrap()
1✔
1431
            .query_processor()
1✔
1432
            .unwrap()
1✔
1433
            .json_vega()
1✔
1434
            .unwrap();
1✔
1435

1436
        let result = query_processor
1✔
1437
            .plot_query(
1✔
1438
                VectorQueryRectangle {
1✔
1439
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
1440
                    time_interval: TimeInterval::new_instant(DateTime::new_utc(
1✔
1441
                        2013, 12, 1, 12, 0, 0,
1✔
1442
                    ))
1✔
1443
                    .unwrap(),
1✔
1444
                    spatial_resolution: SpatialResolution::one(),
1✔
1445
                },
1✔
1446
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1447
            )
1✔
1448
            .await
×
1449
            .unwrap();
1✔
1450

1✔
1451
        assert_eq!(
1✔
1452
            result,
1✔
1453
            geoengine_datatypes::plots::Histogram::builder(1, 4., 4., Measurement::Unitless)
1✔
1454
                .counts(vec![6])
1✔
1455
                .build()
1✔
1456
                .unwrap()
1✔
1457
                .to_vega_embeddable(false)
1✔
1458
                .unwrap()
1✔
1459
        );
1✔
1460
    }
1461
}
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