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

geo-engine / geoengine / 18554766227

16 Oct 2025 08:12AM UTC coverage: 88.843% (+0.3%) from 88.543%
18554766227

push

github

web-flow
build: update dependencies (#1081)

* update sqlfluff

* clippy autofix

* manual clippy fixes

* removal of unused code

* update deps

* upgrade packages

* enable cargo lints

* make sqlfluff happy

* fix chrono parsin error

* clippy

* byte_size

* fix image cmp with tiffs

* remove debug

177 of 205 new or added lines in 38 files covered. (86.34%)

41 existing lines in 20 files now uncovered.

106415 of 119779 relevant lines covered (88.84%)

84190.21 hits per line

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

96.3
/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::Result;
12
use crate::util::input::RasterOrVectorOperator;
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, BandSelection, BoundingBox2D, DataRef, FeatureDataRef, FeatureDataType,
20
    Geometry, Measurement, PlotQueryRectangle, RasterQueryRectangle,
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;
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)]
45
#[serde(rename_all = "camelCase")]
46
pub struct HistogramParams {
47
    /// Name of the (numeric) vector attribute or raster band to compute the histogram on.
48
    pub attribute_name: 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)]
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)]
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(
90
        self: Box<Self>,
91
        path: WorkflowOperatorPath,
92
        context: &dyn ExecutionContext,
93
    ) -> Result<Box<dyn InitializedPlotOperator>> {
11✔
94
        let name = CanonicOperatorName::from(&self);
95

96
        Ok(match self.sources.source {
97
            RasterOrVectorOperator::Raster(raster_source) => {
98
                let raster_source = raster_source
99
                    .initialize(path.clone_and_append(0), context)
100
                    .await?;
101

102
                let in_desc = raster_source.result_descriptor();
103

104
                ensure!(
105
                    in_desc
106
                        .bands
107
                        .iter()
108
                        .any(|b| b.name == self.params.attribute_name),
5✔
109
                    error::InvalidOperatorSpec {
110
                        reason: "Band with given `attribute_name` does not exist".to_string(),
111
                    }
112
                );
113

114
                InitializedHistogram::new(
115
                    name,
116
                    PlotResultDescriptor {
117
                        spatial_reference: in_desc.spatial_reference,
118
                        time: in_desc.time,
119
                        // converting `SpatialPartition2D` to `BoundingBox2D` is ok here, because is makes the covered area only larger
120
                        bbox: in_desc
121
                            .bbox
UNCOV
122
                            .and_then(|p| BoundingBox2D::new(p.lower_left(), p.upper_right()).ok()),
×
123
                    },
124
                    self.params,
125
                    raster_source,
126
                )
127
                .boxed()
128
            }
129
            RasterOrVectorOperator::Vector(vector_source) => {
130
                let column_name = &self.params.attribute_name;
131
                let vector_source = vector_source
132
                    .initialize(path.clone_and_append(0), context)
133
                    .await?;
134

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

160
                let in_desc = vector_source.result_descriptor().clone();
161

162
                InitializedHistogram::new(name, in_desc.into(), self.params, vector_source).boxed()
163
            }
164
        })
165
    }
11✔
166

167
    span_fn!(Histogram);
168
}
169

170
/// The initialization of `Histogram`
171
pub struct InitializedHistogram<Op> {
172
    name: CanonicOperatorName,
173
    result_descriptor: PlotResultDescriptor,
174
    metadata: HistogramMetadataOptions,
175
    source: Op,
176
    interactive: bool,
177
    attribute_name: String,
178
}
179

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

193
        let (number_of_buckets, max_number_of_buckets) = match params.buckets {
9✔
194
            HistogramBuckets::Number {
195
                value: number_of_buckets,
3✔
196
            } => (Some(number_of_buckets as usize), None),
3✔
197
            HistogramBuckets::SquareRootChoiceRule {
198
                max_number_of_buckets,
6✔
199
            } => (None, Some(max_number_of_buckets as usize)),
6✔
200
        };
201

202
        Self {
9✔
203
            name,
9✔
204
            result_descriptor,
9✔
205
            metadata: HistogramMetadataOptions {
9✔
206
                number_of_buckets,
9✔
207
                max_number_of_buckets,
9✔
208
                min,
9✔
209
                max,
9✔
210
            },
9✔
211
            source,
9✔
212
            interactive: params.interactive,
9✔
213
            attribute_name: params.attribute_name,
9✔
214
        }
9✔
215
    }
9✔
216
}
217

218
impl InitializedPlotOperator for InitializedHistogram<Box<dyn InitializedRasterOperator>> {
219
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
5✔
220
        let (band_idx, band) = self
5✔
221
            .source
5✔
222
            .result_descriptor()
5✔
223
            .bands
5✔
224
            .iter().enumerate()
5✔
225
            .find(|(_, b)| b.name == self.attribute_name).expect("the band with the attribute name should exist in the source because it was checked during initialization of the operator.");
5✔
226

227
        let processor = HistogramRasterQueryProcessor {
5✔
228
            input: self.source.query_processor()?,
5✔
229
            band_idx: band_idx as u32,
5✔
230
            measurement: band.measurement.clone(),
5✔
231
            metadata: self.metadata,
5✔
232
            interactive: self.interactive,
5✔
233
        };
234

235
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
5✔
236
    }
5✔
237

238
    fn result_descriptor(&self) -> &PlotResultDescriptor {
1✔
239
        &self.result_descriptor
1✔
240
    }
1✔
241

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

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

262
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
4✔
263
    }
4✔
264

265
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
266
        &self.result_descriptor
×
267
    }
×
268

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

274
/// A query processor that calculates the Histogram about its raster inputs.
275
pub struct HistogramRasterQueryProcessor {
276
    input: TypedRasterQueryProcessor,
277
    band_idx: u32,
278
    measurement: Measurement,
279
    metadata: HistogramMetadataOptions,
280
    interactive: bool,
281
}
282

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

292
#[async_trait]
293
impl PlotQueryProcessor for HistogramRasterQueryProcessor {
294
    type OutputFormat = PlotData;
295

296
    fn plot_type(&self) -> &'static str {
1✔
297
        HISTOGRAM_OPERATOR_NAME
1✔
298
    }
1✔
299

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

313
                self.process(histogram_metadata, query, ctx).await
4✔
314
            })
10✔
315
            .await
316
    }
5✔
317
}
318

319
#[async_trait]
320
impl PlotQueryProcessor for HistogramVectorQueryProcessor {
321
    type OutputFormat = PlotData;
322

323
    fn plot_type(&self) -> &'static str {
×
324
        HISTOGRAM_OPERATOR_NAME
×
325
    }
×
326

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

340
                self.process(histogram_metadata, query, ctx).await
3✔
341
            })
8✔
342
            .await
343
    }
4✔
344
}
345

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

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

367
            Ok(metadata.merge_with(computed_metadata.into()))
3✔
368
        }
3✔
369

370
        if let Ok(metadata) = HistogramMetadata::try_from(self.metadata) {
5✔
371
            return Ok(metadata);
2✔
372
        }
3✔
373

374
        // TODO: compute only number of buckets if possible
375

376
        call_on_generic_raster_processor!(&self.input, processor => {
3✔
377
            process_metadata(processor.query(RasterQueryRectangle::from_qrect_and_bands(&query, BandSelection::new_single(self.band_idx)), ctx).await?, self.metadata).await
1✔
378
        })
379
    }
5✔
380

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

396
        call_on_generic_raster_processor!(&self.input, processor => {
4✔
397
            let mut query = processor.query(RasterQueryRectangle::from_qrect_and_bands(&query, BandSelection::new_single(self.band_idx)), ctx).await?;
×
398

399
            while let Some(tile) = query.next().await {
×
400

401

402
                match tile?.grid_array {
×
403
                    geoengine_datatypes::raster::GridOrEmpty::Grid(g) => histogram.add_raster_data(g.masked_element_deref_iterator()),
×
404
                    geoengine_datatypes::raster::GridOrEmpty::Empty(n) => histogram.add_nodata_batch(n.number_of_elements() as u64) // TODO: why u64?
×
405
                }
406
            }
407
        });
408

409
        let chart = histogram.to_vega_embeddable(self.interactive)?;
4✔
410

411
        Ok(chart)
4✔
412
    }
4✔
413

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

422
        let chart = histogram.to_vega_embeddable(self.interactive)?;
1✔
423

424
        Ok(chart)
1✔
425
    }
1✔
426
}
427

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

445
            while let Some(collection) = input.next().await {
6✔
446
                let collection = collection?;
3✔
447

448
                let feature_data = collection.data(column_name).expect("check in param");
3✔
449
                computed_metadata.add_vector_batch(feature_data);
3✔
450
            }
451

452
            Ok(metadata.merge_with(computed_metadata.into()))
3✔
453
        }
3✔
454

455
        if let Ok(metadata) = HistogramMetadata::try_from(self.metadata) {
4✔
456
            return Ok(metadata);
1✔
457
        }
3✔
458

459
        // TODO: compute only number of buckets if possible
460

461
        call_on_generic_vector_processor!(&self.input, processor => {
3✔
462
            process_metadata(processor.query(query.into(), ctx).await?, &self.column_name, self.metadata).await
3✔
463
        })
464
    }
4✔
465

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

481
        call_on_generic_vector_processor!(&self.input, processor => {
3✔
482
            let mut query = processor.query(query.into(), ctx).await?;
3✔
483

484
            while let Some(collection) = query.next().await {
7✔
485
                let collection = collection?;
4✔
486

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

489
                histogram.add_feature_data(feature_data)?;
4✔
490
            }
491
        });
492

493
        let chart = histogram.to_vega_embeddable(self.interactive)?;
3✔
494

495
        Ok(chart)
3✔
496
    }
3✔
497

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

506
        let chart = histogram.to_vega_embeddable(self.interactive)?;
1✔
507

508
        Ok(chart)
1✔
509
    }
1✔
510
}
511

512
#[derive(Debug, Copy, Clone, PartialEq)]
513
struct HistogramMetadata {
514
    pub number_of_buckets: usize,
515
    pub min: f64,
516
    pub max: f64,
517
}
518

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

528
    fn has_invalid_parameters(&self) -> bool {
9✔
529
        self.number_of_buckets == 0 || self.min > self.max
9✔
530
    }
9✔
531
}
532

533
#[derive(Debug, Copy, Clone, PartialEq)]
534
struct HistogramMetadataOptions {
535
    pub number_of_buckets: Option<usize>,
536
    pub max_number_of_buckets: Option<usize>,
537
    pub min: Option<f64>,
538
    pub max: Option<f64>,
539
}
540

541
impl TryFrom<HistogramMetadataOptions> for HistogramMetadata {
542
    type Error = ();
543

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

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

566
        HistogramMetadata {
6✔
567
            number_of_buckets,
6✔
568
            min: self.min.unwrap_or(metadata.min),
6✔
569
            max: self.max.unwrap_or(metadata.max),
6✔
570
        }
6✔
571
    }
6✔
572
}
573

574
#[derive(Debug, Copy, Clone, PartialEq)]
575
struct HistogramMetadataInProgress {
576
    pub n: usize,
577
    pub min: f64,
578
    pub max: f64,
579
}
580

581
impl Default for HistogramMetadataInProgress {
582
    fn default() -> Self {
6✔
583
        Self {
6✔
584
            n: 0,
6✔
585
            min: f64::MAX,
6✔
586
            max: f64::MIN,
6✔
587
        }
6✔
588
    }
6✔
589
}
590

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

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

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

635
    #[inline]
636
    fn update_minmax(&mut self, value: f64) {
17✔
637
        self.min = f64::min(self.min, value);
17✔
638
        self.max = f64::max(self.max, value);
17✔
639
    }
17✔
640
}
641

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

652
#[cfg(test)]
653
mod tests {
654
    use super::*;
655

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

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

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

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

730
        assert_eq!(deserialized.params, histogram.params);
1✔
731
    }
1✔
732

733
    #[test]
734
    fn serialization_alt() {
1✔
735
        let histogram = Histogram {
1✔
736
            params: HistogramParams {
1✔
737
                attribute_name: "band".to_string(),
1✔
738
                bounds: HistogramBounds::Data(Default::default()),
1✔
739
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
740
                    max_number_of_buckets: 100,
1✔
741
                },
1✔
742
                interactive: false,
1✔
743
            },
1✔
744
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
745
                .boxed()
1✔
746
                .into(),
1✔
747
        };
1✔
748

749
        let serialized = json!({
1✔
750
            "type": "Histogram",
1✔
751
            "params": {
1✔
752
                "attributeName": "band",
1✔
753
                "bounds": "data",
1✔
754
                "buckets": {
1✔
755
                    "type": "squareRootChoiceRule",
1✔
756
                    "maxNumberOfBuckets": 100,
1✔
757
                },
758
            },
759
            "sources": {
1✔
760
                "source": {
1✔
761
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
762
                    "params": {
1✔
763
                        "collections": [],
1✔
764
                        "spatialReference": "EPSG:4326",
1✔
765
                        "measurements": {},
1✔
766
                    }
767
                }
768
            }
769
        })
770
        .to_string();
1✔
771

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

774
        assert_eq!(deserialized.params, histogram.params);
1✔
775
    }
1✔
776

777
    #[tokio::test]
778
    async fn column_name_for_raster_source() {
1✔
779
        let histogram = Histogram {
1✔
780
            params: HistogramParams {
1✔
781
                attribute_name: "foo".to_string(),
1✔
782
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
783
                buckets: HistogramBuckets::Number { value: 3 },
1✔
784
                interactive: false,
1✔
785
            },
1✔
786
            sources: mock_raster_source().into(),
1✔
787
        };
1✔
788

789
        let execution_context = MockExecutionContext::test_default();
1✔
790

791
        assert!(
1✔
792
            histogram
1✔
793
                .boxed()
1✔
794
                .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
795
                .await
1✔
796
                .is_err()
1✔
797
        );
1✔
798
    }
1✔
799

800
    fn mock_raster_source() -> Box<dyn RasterOperator> {
3✔
801
        MockRasterSource {
3✔
802
            params: MockRasterSourceParams {
3✔
803
                data: vec![RasterTile2D::new_with_tile_info(
3✔
804
                    TimeInterval::default(),
3✔
805
                    TileInformation {
3✔
806
                        global_geo_transform: TestDefault::test_default(),
3✔
807
                        global_tile_position: [0, 0].into(),
3✔
808
                        tile_size_in_pixels: [3, 2].into(),
3✔
809
                    },
3✔
810
                    0,
3✔
811
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
3✔
812
                        .unwrap()
3✔
813
                        .into(),
3✔
814
                    CacheHint::default(),
3✔
815
                )],
3✔
816
                result_descriptor: RasterResultDescriptor {
3✔
817
                    data_type: RasterDataType::U8,
3✔
818
                    spatial_reference: SpatialReference::epsg_4326().into(),
3✔
819
                    time: None,
3✔
820
                    bbox: None,
3✔
821
                    resolution: None,
3✔
822
                    bands: RasterBandDescriptors::new_single_band(),
3✔
823
                },
3✔
824
            },
3✔
825
        }
3✔
826
        .boxed()
3✔
827
    }
3✔
828

829
    #[tokio::test]
830
    async fn simple_raster() {
1✔
831
        let tile_size_in_pixels = [3, 2].into();
1✔
832
        let tiling_specification = TilingSpecification {
1✔
833
            origin_coordinate: [0.0, 0.0].into(),
1✔
834
            tile_size_in_pixels,
1✔
835
        };
1✔
836
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
837

838
        let histogram = Histogram {
1✔
839
            params: HistogramParams {
1✔
840
                attribute_name: "band".to_string(),
1✔
841
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
842
                buckets: HistogramBuckets::Number { value: 3 },
1✔
843
                interactive: false,
1✔
844
            },
1✔
845
            sources: mock_raster_source().into(),
1✔
846
        };
1✔
847

848
        let query_processor = histogram
1✔
849
            .boxed()
1✔
850
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
851
            .await
1✔
852
            .unwrap()
1✔
853
            .query_processor()
1✔
854
            .unwrap()
1✔
855
            .json_vega()
1✔
856
            .unwrap();
1✔
857

858
        let result = query_processor
1✔
859
            .plot_query(
1✔
860
                PlotQueryRectangle {
1✔
861
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
862
                    time_interval: TimeInterval::default(),
1✔
863
                    spatial_resolution: SpatialResolution::one(),
1✔
864
                    attributes: PlotSeriesSelection::all(),
1✔
865
                },
1✔
866
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
867
            )
1✔
868
            .await
1✔
869
            .unwrap();
1✔
870

871
        assert_eq!(
1✔
872
            result,
1✔
873
            geoengine_datatypes::plots::Histogram::builder(3, 0., 8., Measurement::Unitless)
1✔
874
                .counts(vec![2, 3, 1])
1✔
875
                .build()
1✔
876
                .unwrap()
1✔
877
                .to_vega_embeddable(false)
1✔
878
                .unwrap()
1✔
879
        );
1✔
880
    }
1✔
881

882
    #[tokio::test]
883
    async fn simple_raster_without_spec() {
1✔
884
        let tile_size_in_pixels = [3, 2].into();
1✔
885
        let tiling_specification = TilingSpecification {
1✔
886
            origin_coordinate: [0.0, 0.0].into(),
1✔
887
            tile_size_in_pixels,
1✔
888
        };
1✔
889
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
890

891
        let histogram = Histogram {
1✔
892
            params: HistogramParams {
1✔
893
                attribute_name: "band".to_string(),
1✔
894
                bounds: HistogramBounds::Data(Default::default()),
1✔
895
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
896
                    max_number_of_buckets: 100,
1✔
897
                },
1✔
898
                interactive: false,
1✔
899
            },
1✔
900
            sources: mock_raster_source().into(),
1✔
901
        };
1✔
902

903
        let query_processor = histogram
1✔
904
            .boxed()
1✔
905
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
906
            .await
1✔
907
            .unwrap()
1✔
908
            .query_processor()
1✔
909
            .unwrap()
1✔
910
            .json_vega()
1✔
911
            .unwrap();
1✔
912

913
        let result = query_processor
1✔
914
            .plot_query(
1✔
915
                PlotQueryRectangle {
1✔
916
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
917
                    time_interval: TimeInterval::default(),
1✔
918
                    spatial_resolution: SpatialResolution::one(),
1✔
919
                    attributes: PlotSeriesSelection::all(),
1✔
920
                },
1✔
921
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
922
            )
1✔
923
            .await
1✔
924
            .unwrap();
1✔
925

926
        assert_eq!(
1✔
927
            result,
1✔
928
            geoengine_datatypes::plots::Histogram::builder(2, 1., 6., Measurement::Unitless)
1✔
929
                .counts(vec![3, 3])
1✔
930
                .build()
1✔
931
                .unwrap()
1✔
932
                .to_vega_embeddable(false)
1✔
933
                .unwrap()
1✔
934
        );
1✔
935
    }
1✔
936

937
    #[tokio::test]
938
    async fn vector_data() {
1✔
939
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
940
            DataCollection::from_slices(
1✔
941
                &[] as &[NoGeometry],
1✔
942
                &[TimeInterval::default(); 8],
1✔
943
                &[("foo", FeatureData::Int(vec![1, 1, 2, 2, 3, 3, 4, 4]))],
1✔
944
            )
945
            .unwrap(),
1✔
946
            DataCollection::from_slices(
1✔
947
                &[] as &[NoGeometry],
1✔
948
                &[TimeInterval::default(); 4],
1✔
949
                &[("foo", FeatureData::Int(vec![5, 6, 7, 8]))],
1✔
950
            )
951
            .unwrap(),
1✔
952
        ])
953
        .boxed();
1✔
954

955
        let histogram = Histogram {
1✔
956
            params: HistogramParams {
1✔
957
                attribute_name: "foo".to_string(),
1✔
958
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
959
                buckets: HistogramBuckets::Number { value: 3 },
1✔
960
                interactive: true,
1✔
961
            },
1✔
962
            sources: vector_source.into(),
1✔
963
        };
1✔
964

965
        let execution_context = MockExecutionContext::test_default();
1✔
966

967
        let query_processor = histogram
1✔
968
            .boxed()
1✔
969
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
970
            .await
1✔
971
            .unwrap()
1✔
972
            .query_processor()
1✔
973
            .unwrap()
1✔
974
            .json_vega()
1✔
975
            .unwrap();
1✔
976

977
        let result = query_processor
1✔
978
            .plot_query(
1✔
979
                PlotQueryRectangle {
1✔
980
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
981
                        .unwrap(),
1✔
982
                    time_interval: TimeInterval::default(),
1✔
983
                    spatial_resolution: SpatialResolution::one(),
1✔
984
                    attributes: PlotSeriesSelection::all(),
1✔
985
                },
1✔
986
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
987
            )
1✔
988
            .await
1✔
989
            .unwrap();
1✔
990

991
        assert_eq!(
1✔
992
            result,
1✔
993
            geoengine_datatypes::plots::Histogram::builder(3, 0., 8., Measurement::Unitless)
1✔
994
                .counts(vec![4, 5, 3])
1✔
995
                .build()
1✔
996
                .unwrap()
1✔
997
                .to_vega_embeddable(true)
1✔
998
                .unwrap()
1✔
999
        );
1✔
1000
    }
1✔
1001

1002
    #[tokio::test]
1003
    async fn vector_data_with_nulls() {
1✔
1004
        let vector_source = MockFeatureCollectionSource::single(
1✔
1005
            DataCollection::from_slices(
1✔
1006
                &[] as &[NoGeometry],
1✔
1007
                &[TimeInterval::default(); 6],
1✔
1008
                &[(
1✔
1009
                    "foo",
1✔
1010
                    FeatureData::NullableFloat(vec![
1✔
1011
                        Some(1.),
1✔
1012
                        Some(2.),
1✔
1013
                        None,
1✔
1014
                        Some(4.),
1✔
1015
                        None,
1✔
1016
                        Some(5.),
1✔
1017
                    ]),
1✔
1018
                )],
1✔
1019
            )
1020
            .unwrap(),
1✔
1021
        )
1022
        .boxed();
1✔
1023

1024
        let histogram = Histogram {
1✔
1025
            params: HistogramParams {
1✔
1026
                attribute_name: "foo".to_string(),
1✔
1027
                bounds: HistogramBounds::Data(Default::default()),
1✔
1028
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1029
                    max_number_of_buckets: 100,
1✔
1030
                },
1✔
1031
                interactive: false,
1✔
1032
            },
1✔
1033
            sources: vector_source.into(),
1✔
1034
        };
1✔
1035

1036
        let execution_context = MockExecutionContext::test_default();
1✔
1037

1038
        let query_processor = histogram
1✔
1039
            .boxed()
1✔
1040
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1041
            .await
1✔
1042
            .unwrap()
1✔
1043
            .query_processor()
1✔
1044
            .unwrap()
1✔
1045
            .json_vega()
1✔
1046
            .unwrap();
1✔
1047

1048
        let result = query_processor
1✔
1049
            .plot_query(
1✔
1050
                PlotQueryRectangle {
1✔
1051
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1052
                        .unwrap(),
1✔
1053
                    time_interval: TimeInterval::default(),
1✔
1054
                    spatial_resolution: SpatialResolution::one(),
1✔
1055
                    attributes: PlotSeriesSelection::all(),
1✔
1056
                },
1✔
1057
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1058
            )
1✔
1059
            .await
1✔
1060
            .unwrap();
1✔
1061

1062
        assert_eq!(
1✔
1063
            result,
1✔
1064
            geoengine_datatypes::plots::Histogram::builder(2, 1., 5., Measurement::Unitless)
1✔
1065
                .counts(vec![2, 2])
1✔
1066
                .build()
1✔
1067
                .unwrap()
1✔
1068
                .to_vega_embeddable(false)
1✔
1069
                .unwrap()
1✔
1070
        );
1✔
1071
    }
1✔
1072

1073
    #[tokio::test]
1074
    #[allow(clippy::too_many_lines)]
1075
    async fn text_attribute() {
1✔
1076
        let dataset_id = DatasetId::new();
1✔
1077
        let dataset_name = NamedData::with_system_name("ne_10m_ports");
1✔
1078

1079
        let workflow = serde_json::json!({
1✔
1080
            "type": "Histogram",
1✔
1081
            "params": {
1✔
1082
                "attributeName": "featurecla",
1✔
1083
                "bounds": "data",
1✔
1084
                "buckets": {
1✔
1085
                    "type": "squareRootChoiceRule",
1✔
1086
                    "maxNumberOfBuckets": 100,
1✔
1087
                }
1088
            },
1089
            "sources": {
1✔
1090
                "source": {
1✔
1091
                    "type": "OgrSource",
1✔
1092
                    "params": {
1✔
1093
                        "data": dataset_name.clone(),
1✔
1094
                        "attributeProjection": null
1✔
1095
                    },
1096
                }
1097
            }
1098
        });
1099
        let histogram: Histogram = serde_json::from_value(workflow).unwrap();
1✔
1100

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

1184
        if let Err(Error::InvalidOperatorSpec { reason }) = histogram
1✔
1185
            .boxed()
1✔
1186
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1187
            .await
1✔
1188
        {
1✔
1189
            assert_eq!(reason, "column `featurecla` must be numerical");
1✔
1190
        } else {
1✔
1191
            panic!("we currently don't support text features, but this went through");
1✔
1192
        }
1✔
1193
    }
1✔
1194

1195
    #[tokio::test]
1196
    async fn no_data_raster() {
1✔
1197
        let tile_size_in_pixels = [3, 2].into();
1✔
1198
        let tiling_specification = TilingSpecification {
1✔
1199
            origin_coordinate: [0.0, 0.0].into(),
1✔
1200
            tile_size_in_pixels,
1✔
1201
        };
1✔
1202
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1203
        let histogram = Histogram {
1✔
1204
            params: HistogramParams {
1✔
1205
                attribute_name: "band".to_string(),
1✔
1206
                bounds: HistogramBounds::Data(Data),
1✔
1207
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1208
                    max_number_of_buckets: 100,
1✔
1209
                },
1✔
1210
                interactive: false,
1✔
1211
            },
1✔
1212
            sources: MockRasterSource {
1✔
1213
                params: MockRasterSourceParams {
1✔
1214
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1215
                        TimeInterval::default(),
1✔
1216
                        TileInformation {
1✔
1217
                            global_geo_transform: TestDefault::test_default(),
1✔
1218
                            global_tile_position: [0, 0].into(),
1✔
1219
                            tile_size_in_pixels,
1✔
1220
                        },
1✔
1221
                        0,
1✔
1222
                        EmptyGrid2D::<u8>::new(tile_size_in_pixels).into(),
1✔
1223
                        CacheHint::default(),
1✔
1224
                    )],
1✔
1225
                    result_descriptor: RasterResultDescriptor {
1✔
1226
                        data_type: RasterDataType::U8,
1✔
1227
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1228
                        time: None,
1✔
1229
                        bbox: None,
1✔
1230
                        resolution: None,
1✔
1231
                        bands: RasterBandDescriptors::new_single_band(),
1✔
1232
                    },
1✔
1233
                },
1✔
1234
            }
1✔
1235
            .boxed()
1✔
1236
            .into(),
1✔
1237
        };
1✔
1238

1239
        let query_processor = histogram
1✔
1240
            .boxed()
1✔
1241
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1242
            .await
1✔
1243
            .unwrap()
1✔
1244
            .query_processor()
1✔
1245
            .unwrap()
1✔
1246
            .json_vega()
1✔
1247
            .unwrap();
1✔
1248

1249
        let result = query_processor
1✔
1250
            .plot_query(
1✔
1251
                PlotQueryRectangle {
1✔
1252
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
1253
                    time_interval: TimeInterval::default(),
1✔
1254
                    spatial_resolution: SpatialResolution::one(),
1✔
1255
                    attributes: PlotSeriesSelection::all(),
1✔
1256
                },
1✔
1257
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1258
            )
1✔
1259
            .await
1✔
1260
            .unwrap();
1✔
1261

1262
        assert_eq!(
1✔
1263
            result,
1✔
1264
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., Measurement::Unitless)
1✔
1265
                .build()
1✔
1266
                .unwrap()
1✔
1267
                .to_vega_embeddable(false)
1✔
1268
                .unwrap()
1✔
1269
        );
1✔
1270
    }
1✔
1271

1272
    #[tokio::test]
1273
    async fn empty_feature_collection() {
1✔
1274
        let vector_source = MockFeatureCollectionSource::single(
1✔
1275
            DataCollection::from_slices(
1✔
1276
                &[] as &[NoGeometry],
1✔
1277
                &[] as &[TimeInterval],
1✔
1278
                &[("foo", FeatureData::Float(vec![]))],
1✔
1279
            )
1280
            .unwrap(),
1✔
1281
        )
1282
        .boxed();
1✔
1283

1284
        let histogram = Histogram {
1✔
1285
            params: HistogramParams {
1✔
1286
                attribute_name: "foo".to_string(),
1✔
1287
                bounds: HistogramBounds::Data(Default::default()),
1✔
1288
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1289
                    max_number_of_buckets: 100,
1✔
1290
                },
1✔
1291
                interactive: false,
1✔
1292
            },
1✔
1293
            sources: vector_source.into(),
1✔
1294
        };
1✔
1295

1296
        let execution_context = MockExecutionContext::test_default();
1✔
1297

1298
        let query_processor = histogram
1✔
1299
            .boxed()
1✔
1300
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1301
            .await
1✔
1302
            .unwrap()
1✔
1303
            .query_processor()
1✔
1304
            .unwrap()
1✔
1305
            .json_vega()
1✔
1306
            .unwrap();
1✔
1307

1308
        let result = query_processor
1✔
1309
            .plot_query(
1✔
1310
                PlotQueryRectangle {
1✔
1311
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1312
                        .unwrap(),
1✔
1313
                    time_interval: TimeInterval::default(),
1✔
1314
                    spatial_resolution: SpatialResolution::one(),
1✔
1315
                    attributes: PlotSeriesSelection::all(),
1✔
1316
                },
1✔
1317
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1318
            )
1✔
1319
            .await
1✔
1320
            .unwrap();
1✔
1321

1322
        assert_eq!(
1✔
1323
            result,
1✔
1324
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., Measurement::Unitless)
1✔
1325
                .build()
1✔
1326
                .unwrap()
1✔
1327
                .to_vega_embeddable(false)
1✔
1328
                .unwrap()
1✔
1329
        );
1✔
1330
    }
1✔
1331

1332
    #[tokio::test]
1333
    async fn feature_collection_with_one_feature() {
1✔
1334
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
1335
            vec![
1✔
1336
                DataCollection::from_slices(
1✔
1337
                    &[] as &[NoGeometry],
1✔
1338
                    &[TimeInterval::default()],
1✔
1339
                    &[("foo", FeatureData::Float(vec![5.0]))],
1✔
1340
                )
1341
                .unwrap(),
1✔
1342
            ],
1343
            [(
1✔
1344
                "foo".to_string(),
1✔
1345
                Measurement::continuous("bar".to_string(), None),
1✔
1346
            )]
1✔
1347
            .into_iter()
1✔
1348
            .collect(),
1✔
1349
        )
1350
        .boxed();
1✔
1351

1352
        let histogram = Histogram {
1✔
1353
            params: HistogramParams {
1✔
1354
                attribute_name: "foo".to_string(),
1✔
1355
                bounds: HistogramBounds::Data(Default::default()),
1✔
1356
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1357
                    max_number_of_buckets: 100,
1✔
1358
                },
1✔
1359
                interactive: false,
1✔
1360
            },
1✔
1361
            sources: vector_source.into(),
1✔
1362
        };
1✔
1363

1364
        let execution_context = MockExecutionContext::test_default();
1✔
1365

1366
        let query_processor = histogram
1✔
1367
            .boxed()
1✔
1368
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1369
            .await
1✔
1370
            .unwrap()
1✔
1371
            .query_processor()
1✔
1372
            .unwrap()
1✔
1373
            .json_vega()
1✔
1374
            .unwrap();
1✔
1375

1376
        let result = query_processor
1✔
1377
            .plot_query(
1✔
1378
                PlotQueryRectangle {
1✔
1379
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
1380
                        .unwrap(),
1✔
1381
                    time_interval: TimeInterval::default(),
1✔
1382
                    spatial_resolution: SpatialResolution::one(),
1✔
1383
                    attributes: PlotSeriesSelection::all(),
1✔
1384
                },
1✔
1385
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1386
            )
1✔
1387
            .await
1✔
1388
            .unwrap();
1✔
1389

1390
        assert_eq!(
1✔
1391
            result,
1✔
1392
            geoengine_datatypes::plots::Histogram::builder(
1✔
1393
                1,
1✔
1394
                5.,
1✔
1395
                5.,
1✔
1396
                Measurement::continuous("bar".to_string(), None)
1✔
1397
            )
1✔
1398
            .counts(vec![1])
1✔
1399
            .build()
1✔
1400
            .unwrap()
1✔
1401
            .to_vega_embeddable(false)
1✔
1402
            .unwrap()
1✔
1403
        );
1✔
1404
    }
1✔
1405

1406
    #[tokio::test]
1407
    async fn single_value_raster_stream() {
1✔
1408
        let tile_size_in_pixels = [3, 2].into();
1✔
1409
        let tiling_specification = TilingSpecification {
1✔
1410
            origin_coordinate: [0.0, 0.0].into(),
1✔
1411
            tile_size_in_pixels,
1✔
1412
        };
1✔
1413
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1414
        let histogram = Histogram {
1✔
1415
            params: HistogramParams {
1✔
1416
                attribute_name: "band".to_string(),
1✔
1417
                bounds: HistogramBounds::Data(Data),
1✔
1418
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1419
                    max_number_of_buckets: 100,
1✔
1420
                },
1✔
1421
                interactive: false,
1✔
1422
            },
1✔
1423
            sources: MockRasterSource {
1✔
1424
                params: MockRasterSourceParams {
1✔
1425
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1426
                        TimeInterval::default(),
1✔
1427
                        TileInformation {
1✔
1428
                            global_geo_transform: TestDefault::test_default(),
1✔
1429
                            global_tile_position: [0, 0].into(),
1✔
1430
                            tile_size_in_pixels,
1✔
1431
                        },
1✔
1432
                        0,
1✔
1433
                        Grid2D::new(tile_size_in_pixels, vec![4; 6]).unwrap().into(),
1✔
1434
                        CacheHint::default(),
1✔
1435
                    )],
1✔
1436
                    result_descriptor: RasterResultDescriptor {
1✔
1437
                        data_type: RasterDataType::U8,
1✔
1438
                        spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1439
                        time: None,
1✔
1440
                        bbox: None,
1✔
1441
                        resolution: None,
1✔
1442
                        bands: RasterBandDescriptors::new_single_band(),
1✔
1443
                    },
1✔
1444
                },
1✔
1445
            }
1✔
1446
            .boxed()
1✔
1447
            .into(),
1✔
1448
        };
1✔
1449

1450
        let query_processor = histogram
1✔
1451
            .boxed()
1✔
1452
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1453
            .await
1✔
1454
            .unwrap()
1✔
1455
            .query_processor()
1✔
1456
            .unwrap()
1✔
1457
            .json_vega()
1✔
1458
            .unwrap();
1✔
1459

1460
        let result = query_processor
1✔
1461
            .plot_query(
1✔
1462
                PlotQueryRectangle {
1✔
1463
                    spatial_bounds: BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
1464
                    time_interval: TimeInterval::new_instant(DateTime::new_utc(
1✔
1465
                        2013, 12, 1, 12, 0, 0,
1✔
1466
                    ))
1✔
1467
                    .unwrap(),
1✔
1468
                    spatial_resolution: SpatialResolution::one(),
1✔
1469
                    attributes: PlotSeriesSelection::all(),
1✔
1470
                },
1✔
1471
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
1472
            )
1✔
1473
            .await
1✔
1474
            .unwrap();
1✔
1475

1476
        assert_eq!(
1✔
1477
            result,
1✔
1478
            geoengine_datatypes::plots::Histogram::builder(1, 4., 4., Measurement::Unitless)
1✔
1479
                .counts(vec![6])
1✔
1480
                .build()
1✔
1481
                .unwrap()
1✔
1482
                .to_vega_embeddable(false)
1✔
1483
                .unwrap()
1✔
1484
        );
1✔
1485
    }
1✔
1486
}
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