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

geo-engine / geoengine / 19567405893

21 Nov 2025 10:21AM UTC coverage: 88.567%. First build
19567405893

Pull #1083

github

web-flow
Merge c6cd1b0c9 into ba34db99d
Pull Request #1083: feat: new gdal source workflow optimization

6332 of 7653 new or added lines in 79 files covered. (82.74%)

116676 of 131738 relevant lines covered (88.57%)

492817.76 hits per line

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

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

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

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

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

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

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

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

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

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

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

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

103
                let in_desc = raster_source.result_descriptor();
104

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

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

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

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

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

166
    span_fn!(Histogram);
167
}
168

169
/// The initialization of `Histogram`
170
pub struct InitializedHistogram<Op> {
171
    name: CanonicOperatorName,
172
    params: HistogramParams,
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(
11✔
182
        name: CanonicOperatorName,
11✔
183
        result_descriptor: PlotResultDescriptor,
11✔
184
        params: HistogramParams,
11✔
185
        source: Op,
11✔
186
    ) -> Self {
11✔
187
        let (min, max) = if let HistogramBounds::Values { min, max } = params.bounds {
11✔
188
            (Some(min), Some(max))
5✔
189
        } else {
190
            (None, None)
6✔
191
        };
192

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

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

219
impl InitializedPlotOperator for InitializedHistogram<Box<dyn InitializedRasterOperator>> {
220
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
5✔
221
        let (band_idx, band) = self
5✔
222
            .source
5✔
223
            .result_descriptor()
5✔
224
            .bands
5✔
225
            .iter().enumerate()
5✔
226
            .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✔
227

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

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

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

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

247
    fn optimize(
1✔
248
        &self,
1✔
249
        target_resolution: SpatialResolution,
1✔
250
    ) -> Result<Box<dyn PlotOperator>, OptimizationError> {
1✔
251
        Ok(Histogram {
252
            params: self.params.clone(),
1✔
253
            sources: SingleRasterOrVectorSource {
254
                source: RasterOrVectorOperator::Raster(self.source.optimize(target_resolution)?),
1✔
255
            },
256
        }
257
        .boxed())
1✔
258
    }
1✔
259
}
260

261
impl InitializedPlotOperator for InitializedHistogram<Box<dyn InitializedVectorOperator>> {
262
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
4✔
263
        let processor = HistogramVectorQueryProcessor {
4✔
264
            input: self.source.query_processor()?,
4✔
265
            column_name: self.attribute_name.clone(),
4✔
266
            measurement: self
4✔
267
                .source
4✔
268
                .result_descriptor()
4✔
269
                .column_measurement(&self.attribute_name)
4✔
270
                .cloned()
4✔
271
                .into(),
4✔
272
            metadata: self.metadata,
4✔
273
            interactive: self.interactive,
4✔
274
        };
275

276
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
4✔
277
    }
4✔
278

279
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
280
        &self.result_descriptor
×
281
    }
×
282

283
    fn canonic_name(&self) -> CanonicOperatorName {
×
284
        self.name.clone()
×
285
    }
×
286

NEW
287
    fn optimize(
×
NEW
288
        &self,
×
NEW
289
        target_resolution: SpatialResolution,
×
NEW
290
    ) -> Result<Box<dyn PlotOperator>, OptimizationError> {
×
291
        Ok(Histogram {
NEW
292
            params: self.params.clone(),
×
293
            sources: SingleRasterOrVectorSource {
NEW
294
                source: RasterOrVectorOperator::Vector(self.source.optimize(target_resolution)?),
×
295
            },
296
        }
NEW
297
        .boxed())
×
NEW
298
    }
×
299
}
300

301
/// A query processor that calculates the Histogram about its raster inputs.
302
pub struct HistogramRasterQueryProcessor {
303
    input: TypedRasterQueryProcessor,
304
    band_idx: u32,
305
    measurement: Measurement,
306
    metadata: HistogramMetadataOptions,
307
    interactive: bool,
308
}
309

310
/// A query processor that calculates the Histogram about its vector inputs.
311
pub struct HistogramVectorQueryProcessor {
312
    input: TypedVectorQueryProcessor,
313
    column_name: String,
314
    measurement: Measurement,
315
    metadata: HistogramMetadataOptions,
316
    interactive: bool,
317
}
318

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

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

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

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

346
#[async_trait]
347
impl PlotQueryProcessor for HistogramVectorQueryProcessor {
348
    type OutputFormat = PlotData;
349

350
    fn plot_type(&self) -> &'static str {
×
351
        HISTOGRAM_OPERATOR_NAME
×
352
    }
×
353

354
    async fn plot_query<'p>(
355
        &'p self,
356
        query: PlotQueryRectangle,
357
        ctx: &'p dyn QueryContext,
358
    ) -> Result<Self::OutputFormat> {
4✔
359
        self.preprocess(query.clone(), ctx)
360
            .and_then(move |mut histogram_metadata| async move {
4✔
361
                histogram_metadata.sanitize();
4✔
362
                if histogram_metadata.has_invalid_parameters() {
4✔
363
                    // early return of empty histogram
364
                    return self.empty_histogram();
1✔
365
                }
3✔
366

367
                self.process(histogram_metadata, query, ctx).await
3✔
368
            })
8✔
369
            .await
370
    }
4✔
371
}
372

373
impl HistogramRasterQueryProcessor {
374
    async fn preprocess<'p>(
5✔
375
        &'p self,
5✔
376
        query: PlotQueryRectangle,
5✔
377
        ctx: &'p dyn QueryContext,
5✔
378
    ) -> Result<HistogramMetadata> {
5✔
379
        async fn process_metadata<T: Pixel>(
3✔
380
            mut input: BoxStream<'_, Result<RasterTile2D<T>>>,
3✔
381
            metadata: HistogramMetadataOptions,
3✔
382
        ) -> Result<HistogramMetadata> {
3✔
383
            let mut computed_metadata = HistogramMetadataInProgress::default();
3✔
384

385
            while let Some(tile) = input.next().await {
15✔
386
                match tile?.grid_array {
12✔
387
                    geoengine_datatypes::raster::GridOrEmpty::Grid(g) => {
2✔
388
                        computed_metadata.add_raster_batch(g.masked_element_deref_iterator());
2✔
389
                    }
2✔
390
                    geoengine_datatypes::raster::GridOrEmpty::Empty(_) => {} // TODO: find out if we really do nothing for empty tiles?
10✔
391
                }
392
            }
393

394
            Ok(metadata.merge_with(computed_metadata.into()))
3✔
395
        }
3✔
396

397
        if let Ok(metadata) = HistogramMetadata::try_from(self.metadata) {
5✔
398
            return Ok(metadata);
2✔
399
        }
3✔
400

401
        let rd = self.input.result_descriptor();
3✔
402

403
        // TODO: compute only number of buckets if possible
404
        let raster_query_rect = RasterQueryRectangle::from_bounds_and_geo_transform(
3✔
405
            &query,
3✔
406
            BandSelection::new_single(self.band_idx),
3✔
407
            rd.tiling_grid_definition(ctx.tiling_specification())
3✔
408
                .tiling_geo_transform(),
3✔
409
        );
410

411
        call_on_generic_raster_processor!(&self.input, processor => {
3✔
412
            process_metadata(processor.query(raster_query_rect, ctx).await?, self.metadata).await
1✔
413
        })
414
    }
5✔
415

416
    async fn process<'p>(
4✔
417
        &'p self,
4✔
418
        metadata: HistogramMetadata,
4✔
419
        query: PlotQueryRectangle,
4✔
420
        ctx: &'p dyn QueryContext,
4✔
421
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
4✔
422
        let mut histogram = geoengine_datatypes::plots::Histogram::builder(
4✔
423
            metadata.number_of_buckets,
4✔
424
            metadata.min,
4✔
425
            metadata.max,
4✔
426
            self.measurement.clone(),
4✔
427
        )
428
        .build()
4✔
429
        .map_err(Error::from)?;
4✔
430

431
        let rd = self.input.result_descriptor();
4✔
432

433
        let raster_query_rect = RasterQueryRectangle::from_bounds_and_geo_transform(
4✔
434
            &query,
4✔
435
            BandSelection::new_single(self.band_idx),
4✔
436
            rd.tiling_grid_definition(ctx.tiling_specification())
4✔
437
                .tiling_geo_transform(),
4✔
438
        );
439

440
        call_on_generic_raster_processor!(&self.input, processor => {
4✔
441
            let mut query = processor.query(raster_query_rect, ctx).await?;
×
442

443
            while let Some(tile) = query.next().await {
×
444

445

446
                match tile?.grid_array {
×
447
                    geoengine_datatypes::raster::GridOrEmpty::Grid(g) => histogram.add_raster_data(g.masked_element_deref_iterator()),
×
448
                    geoengine_datatypes::raster::GridOrEmpty::Empty(n) => histogram.add_nodata_batch(n.number_of_elements() as u64) // TODO: why u64?
×
449
                }
450
            }
451
        });
452

453
        let chart = histogram.to_vega_embeddable(self.interactive)?;
4✔
454

455
        Ok(chart)
4✔
456
    }
4✔
457

458
    fn empty_histogram(
1✔
459
        &self,
1✔
460
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
1✔
461
        let histogram =
1✔
462
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., self.measurement.clone())
1✔
463
                .build()
1✔
464
                .map_err(Error::from)?;
1✔
465

466
        let chart = histogram.to_vega_embeddable(self.interactive)?;
1✔
467

468
        Ok(chart)
1✔
469
    }
1✔
470
}
471

472
impl HistogramVectorQueryProcessor {
473
    async fn preprocess<'p>(
4✔
474
        &'p self,
4✔
475
        query: PlotQueryRectangle,
4✔
476
        ctx: &'p dyn QueryContext,
4✔
477
    ) -> Result<HistogramMetadata> {
4✔
478
        async fn process_metadata<'m, G>(
3✔
479
            mut input: BoxStream<'m, Result<FeatureCollection<G>>>,
3✔
480
            column_name: &'m str,
3✔
481
            metadata: HistogramMetadataOptions,
3✔
482
        ) -> Result<HistogramMetadata>
3✔
483
        where
3✔
484
            G: Geometry + 'static,
3✔
485
            FeatureCollection<G>: FeatureCollectionInfos,
3✔
486
        {
3✔
487
            let mut computed_metadata = HistogramMetadataInProgress::default();
3✔
488

489
            while let Some(collection) = input.next().await {
6✔
490
                let collection = collection?;
3✔
491

492
                let feature_data = collection.data(column_name).expect("check in param");
3✔
493
                computed_metadata.add_vector_batch(feature_data);
3✔
494
            }
495

496
            Ok(metadata.merge_with(computed_metadata.into()))
3✔
497
        }
3✔
498

499
        if let Ok(metadata) = HistogramMetadata::try_from(self.metadata) {
4✔
500
            return Ok(metadata);
1✔
501
        }
3✔
502

503
        // TODO: compute only number of buckets if possible
504

505
        let query = query.select_attributes(ColumnSelection::all());
3✔
506

507
        call_on_generic_vector_processor!(&self.input, processor => {
3✔
508
            process_metadata(processor.query(query, ctx).await?, &self.column_name, self.metadata).await
3✔
509
        })
510
    }
4✔
511

512
    async fn process<'p>(
3✔
513
        &'p self,
3✔
514
        metadata: HistogramMetadata,
3✔
515
        query: PlotQueryRectangle,
3✔
516
        ctx: &'p dyn QueryContext,
3✔
517
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
3✔
518
        let mut histogram = geoengine_datatypes::plots::Histogram::builder(
3✔
519
            metadata.number_of_buckets,
3✔
520
            metadata.min,
3✔
521
            metadata.max,
3✔
522
            self.measurement.clone(),
3✔
523
        )
524
        .build()
3✔
525
        .map_err(Error::from)?;
3✔
526

527
        let query = query.select_attributes(ColumnSelection::all());
3✔
528

529
        call_on_generic_vector_processor!(&self.input, processor => {
3✔
530
            let mut query = processor.query(query, ctx).await?;
3✔
531

532
            while let Some(collection) = query.next().await {
7✔
533
                let collection = collection?;
4✔
534

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

537
                histogram.add_feature_data(feature_data)?;
4✔
538
            }
539
        });
540

541
        let chart = histogram.to_vega_embeddable(self.interactive)?;
3✔
542

543
        Ok(chart)
3✔
544
    }
3✔
545

546
    fn empty_histogram(
1✔
547
        &self,
1✔
548
    ) -> Result<<HistogramRasterQueryProcessor as PlotQueryProcessor>::OutputFormat> {
1✔
549
        let histogram =
1✔
550
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., self.measurement.clone())
1✔
551
                .build()
1✔
552
                .map_err(Error::from)?;
1✔
553

554
        let chart = histogram.to_vega_embeddable(self.interactive)?;
1✔
555

556
        Ok(chart)
1✔
557
    }
1✔
558
}
559

560
#[derive(Debug, Copy, Clone, PartialEq)]
561
struct HistogramMetadata {
562
    pub number_of_buckets: usize,
563
    pub min: f64,
564
    pub max: f64,
565
}
566

567
impl HistogramMetadata {
568
    /// Fix invalid configurations if they are fixeable
569
    fn sanitize(&mut self) {
9✔
570
        // prevent the rare case that min=max and you have more than one bucket
571
        if approx_eq!(f64, self.min, self.max) && self.number_of_buckets > 1 {
9✔
572
            self.number_of_buckets = 1;
1✔
573
        }
8✔
574
    }
9✔
575

576
    fn has_invalid_parameters(&self) -> bool {
9✔
577
        self.number_of_buckets == 0 || self.min > self.max
9✔
578
    }
9✔
579
}
580

581
#[derive(Debug, Copy, Clone, PartialEq)]
582
struct HistogramMetadataOptions {
583
    pub number_of_buckets: Option<usize>,
584
    pub max_number_of_buckets: Option<usize>,
585
    pub min: Option<f64>,
586
    pub max: Option<f64>,
587
}
588

589
impl TryFrom<HistogramMetadataOptions> for HistogramMetadata {
590
    type Error = ();
591

592
    fn try_from(options: HistogramMetadataOptions) -> Result<Self, Self::Error> {
9✔
593
        match (options.number_of_buckets, options.min, options.max) {
9✔
594
            (Some(number_of_buckets), Some(min), Some(max)) => Ok(Self {
3✔
595
                number_of_buckets,
3✔
596
                min,
3✔
597
                max,
3✔
598
            }),
3✔
599
            _ => Err(()),
6✔
600
        }
601
    }
9✔
602
}
603

604
impl HistogramMetadataOptions {
605
    fn merge_with(self, metadata: HistogramMetadata) -> HistogramMetadata {
6✔
606
        let number_of_buckets = if let Some(number_of_buckets) = self.number_of_buckets {
6✔
607
            number_of_buckets
×
608
        } else if let Some(max_number_of_buckets) = self.max_number_of_buckets {
6✔
609
            metadata.number_of_buckets.min(max_number_of_buckets)
6✔
610
        } else {
611
            metadata.number_of_buckets
×
612
        };
613

614
        HistogramMetadata {
6✔
615
            number_of_buckets,
6✔
616
            min: self.min.unwrap_or(metadata.min),
6✔
617
            max: self.max.unwrap_or(metadata.max),
6✔
618
        }
6✔
619
    }
6✔
620
}
621

622
#[derive(Debug, Copy, Clone, PartialEq)]
623
struct HistogramMetadataInProgress {
624
    pub n: usize,
625
    pub min: f64,
626
    pub max: f64,
627
}
628

629
impl Default for HistogramMetadataInProgress {
630
    fn default() -> Self {
6✔
631
        Self {
6✔
632
            n: 0,
6✔
633
            min: f64::MAX,
6✔
634
            max: f64::MIN,
6✔
635
        }
6✔
636
    }
6✔
637
}
638

639
impl HistogramMetadataInProgress {
640
    #[inline]
641
    fn add_raster_batch<T: Pixel, I: Iterator<Item = Option<T>>>(&mut self, values: I) {
2✔
642
        values.for_each(|pixel_option| {
12✔
643
            if let Some(p) = pixel_option {
12✔
644
                self.n += 1;
12✔
645
                self.update_minmax(p.as_());
12✔
646
            }
12✔
647
        });
12✔
648
    }
2✔
649

650
    #[inline]
651
    fn add_vector_batch(&mut self, values: FeatureDataRef) {
3✔
652
        fn add_data_ref<'d, D, T>(metadata: &mut HistogramMetadataInProgress, data_ref: &'d D)
3✔
653
        where
3✔
654
            D: DataRef<'d, T>,
3✔
655
            T: 'static,
3✔
656
        {
657
            for v in data_ref.float_options_iter().flatten() {
5✔
658
                metadata.n += 1;
5✔
659
                metadata.update_minmax(v);
5✔
660
            }
5✔
661
        }
3✔
662

663
        match values {
3✔
664
            FeatureDataRef::Int(values) => {
×
665
                add_data_ref(self, &values);
×
666
            }
×
667
            FeatureDataRef::Float(values) => {
3✔
668
                add_data_ref(self, &values);
3✔
669
            }
3✔
670
            FeatureDataRef::Bool(values) => {
×
671
                add_data_ref(self, &values);
×
672
            }
×
673
            FeatureDataRef::DateTime(values) => {
×
674
                add_data_ref(self, &values);
×
675
            }
×
676
            FeatureDataRef::Category(_) | FeatureDataRef::Text(_) => {
×
677
                // do nothing since we don't support them
×
678
                // TODO: fill with live once we support category and text types
×
679
            }
×
680
        }
681
    }
3✔
682

683
    #[inline]
684
    fn update_minmax(&mut self, value: f64) {
17✔
685
        self.min = f64::min(self.min, value);
17✔
686
        self.max = f64::max(self.max, value);
17✔
687
    }
17✔
688
}
689

690
impl From<HistogramMetadataInProgress> for HistogramMetadata {
691
    fn from(metadata: HistogramMetadataInProgress) -> Self {
6✔
692
        Self {
6✔
693
            number_of_buckets: f64::sqrt(metadata.n as f64) as usize,
6✔
694
            min: metadata.min,
6✔
695
            max: metadata.max,
6✔
696
        }
6✔
697
    }
6✔
698
}
699

700
#[cfg(test)]
701
mod tests {
702
    use super::*;
703

704
    use crate::engine::{
705
        ChunkByteSize, MockExecutionContext, RasterBandDescriptors, RasterOperator,
706
        RasterResultDescriptor, SpatialGridDescriptor, StaticMetaData, TimeDescriptor,
707
        VectorColumnInfo, VectorOperator, VectorResultDescriptor,
708
    };
709
    use crate::mock::{MockFeatureCollectionSource, MockRasterSource, MockRasterSourceParams};
710
    use crate::source::{
711
        OgrSourceColumnSpec, OgrSourceDataset, OgrSourceDatasetTimeType, OgrSourceErrorSpec,
712
    };
713
    use crate::test_data;
714
    use geoengine_datatypes::dataset::{DataId, DatasetId, NamedData};
715
    use geoengine_datatypes::primitives::{
716
        BoundingBox2D, Coordinate2D, DateTime, FeatureData, NoGeometry, PlotSeriesSelection,
717
        TimeInterval, VectorQueryRectangle,
718
    };
719
    use geoengine_datatypes::primitives::{CacheHint, CacheTtlSeconds};
720
    use geoengine_datatypes::raster::{
721
        BoundedGrid, EmptyGrid2D, GeoTransform, Grid2D, GridShape2D, RasterDataType, RasterTile2D,
722
        TileInformation, TilingSpecification,
723
    };
724
    use geoengine_datatypes::spatial_reference::SpatialReference;
725
    use geoengine_datatypes::util::Identifier;
726
    use geoengine_datatypes::util::test::TestDefault;
727
    use geoengine_datatypes::{
728
        collections::{DataCollection, VectorDataType},
729
        primitives::MultiPoint,
730
    };
731
    use serde_json::json;
732

733
    #[test]
734
    fn serialization() {
1✔
735
        let histogram = Histogram {
1✔
736
            params: HistogramParams {
1✔
737
                attribute_name: "foobar".to_string(),
1✔
738
                bounds: HistogramBounds::Values {
1✔
739
                    min: 5.0,
1✔
740
                    max: 10.0,
1✔
741
                },
1✔
742
                buckets: HistogramBuckets::Number { value: 15 },
1✔
743
                interactive: false,
1✔
744
            },
1✔
745
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
746
                .boxed()
1✔
747
                .into(),
1✔
748
        };
1✔
749

750
        let serialized = json!({
1✔
751
            "type": "Histogram",
1✔
752
            "params": {
1✔
753
                "attributeName": "foobar",
1✔
754
                "bounds": {
1✔
755
                    "min": 5.0,
1✔
756
                    "max": 10.0,
1✔
757
                },
758
                "buckets": {
1✔
759
                    "type": "number",
1✔
760
                    "value": 15,
1✔
761
                },
762
                "interactivity": false,
1✔
763
            },
764
            "sources": {
1✔
765
                "source": {
1✔
766
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
767
                    "params": {
1✔
768
                        "collections": [],
1✔
769
                        "spatialReference": "EPSG:4326",
1✔
770
                        "measurements": {},
1✔
771
                    }
772
                }
773
            }
774
        })
775
        .to_string();
1✔
776

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

779
        assert_eq!(deserialized.params, histogram.params);
1✔
780
    }
1✔
781

782
    #[test]
783
    fn serialization_alt() {
1✔
784
        let histogram = Histogram {
1✔
785
            params: HistogramParams {
1✔
786
                attribute_name: "band".to_string(),
1✔
787
                bounds: HistogramBounds::Data(Default::default()),
1✔
788
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
789
                    max_number_of_buckets: 100,
1✔
790
                },
1✔
791
                interactive: false,
1✔
792
            },
1✔
793
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
794
                .boxed()
1✔
795
                .into(),
1✔
796
        };
1✔
797

798
        let serialized = json!({
1✔
799
            "type": "Histogram",
1✔
800
            "params": {
1✔
801
                "attributeName": "band",
1✔
802
                "bounds": "data",
1✔
803
                "buckets": {
1✔
804
                    "type": "squareRootChoiceRule",
1✔
805
                    "maxNumberOfBuckets": 100,
1✔
806
                },
807
            },
808
            "sources": {
1✔
809
                "source": {
1✔
810
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
811
                    "params": {
1✔
812
                        "collections": [],
1✔
813
                        "spatialReference": "EPSG:4326",
1✔
814
                        "measurements": {},
1✔
815
                    }
816
                }
817
            }
818
        })
819
        .to_string();
1✔
820

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

823
        assert_eq!(deserialized.params, histogram.params);
1✔
824
    }
1✔
825

826
    #[tokio::test]
827
    async fn column_name_for_raster_source() {
1✔
828
        let histogram = Histogram {
1✔
829
            params: HistogramParams {
1✔
830
                attribute_name: "foo".to_string(),
1✔
831
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
832
                buckets: HistogramBuckets::Number { value: 3 },
1✔
833
                interactive: false,
1✔
834
            },
1✔
835
            sources: mock_raster_source().into(),
1✔
836
        };
1✔
837

838
        let execution_context = MockExecutionContext::test_default();
1✔
839

840
        assert!(
1✔
841
            histogram
1✔
842
                .boxed()
1✔
843
                .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
844
                .await
1✔
845
                .is_err()
1✔
846
        );
1✔
847
    }
1✔
848

849
    fn mock_raster_source() -> Box<dyn RasterOperator> {
3✔
850
        MockRasterSource {
3✔
851
            params: MockRasterSourceParams {
3✔
852
                data: vec![RasterTile2D::new_with_tile_info(
3✔
853
                    TimeInterval::default(),
3✔
854
                    TileInformation {
3✔
855
                        global_geo_transform: TestDefault::test_default(),
3✔
856
                        global_tile_position: [0, 0].into(),
3✔
857
                        tile_size_in_pixels: [3, 2].into(),
3✔
858
                    },
3✔
859
                    0,
3✔
860
                    Grid2D::new([3, 2].into(), vec![1, 2, 3, 4, 5, 6])
3✔
861
                        .unwrap()
3✔
862
                        .into(),
3✔
863
                    CacheHint::default(),
3✔
864
                )],
3✔
865
                result_descriptor: RasterResultDescriptor {
3✔
866
                    data_type: RasterDataType::U8,
3✔
867
                    spatial_reference: SpatialReference::epsg_4326().into(),
3✔
868
                    time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
3✔
869
                    spatial_grid: SpatialGridDescriptor::source_from_parts(
3✔
870
                        GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
3✔
871
                        GridShape2D::new_2d(3, 2).bounding_box(),
3✔
872
                    ),
3✔
873
                    bands: RasterBandDescriptors::new_single_band(),
3✔
874
                },
3✔
875
            },
3✔
876
        }
3✔
877
        .boxed()
3✔
878
    }
3✔
879

880
    #[tokio::test]
881
    async fn simple_raster() {
1✔
882
        let tile_size_in_pixels = [3, 2].into();
1✔
883
        let tiling_specification = TilingSpecification {
1✔
884
            tile_size_in_pixels,
1✔
885
        };
1✔
886
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
887

888
        let histogram = Histogram {
1✔
889
            params: HistogramParams {
1✔
890
                attribute_name: "band".to_string(),
1✔
891
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
892
                buckets: HistogramBuckets::Number { value: 3 },
1✔
893
                interactive: false,
1✔
894
            },
1✔
895
            sources: mock_raster_source().into(),
1✔
896
        };
1✔
897

898
        let query_processor = histogram
1✔
899
            .boxed()
1✔
900
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
901
            .await
1✔
902
            .unwrap()
1✔
903
            .query_processor()
1✔
904
            .unwrap()
1✔
905
            .json_vega()
1✔
906
            .unwrap();
1✔
907

908
        let result = query_processor
1✔
909
            .plot_query(
1✔
910
                PlotQueryRectangle::new(
1✔
911
                    BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
912
                    TimeInterval::default(),
1✔
913
                    PlotSeriesSelection::all(),
1✔
914
                ),
1✔
915
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
916
            )
1✔
917
            .await
1✔
918
            .unwrap();
1✔
919

920
        assert_eq!(
1✔
921
            result,
1✔
922
            geoengine_datatypes::plots::Histogram::builder(3, 0., 8., Measurement::Unitless)
1✔
923
                .counts(vec![2, 3, 1])
1✔
924
                .build()
1✔
925
                .unwrap()
1✔
926
                .to_vega_embeddable(false)
1✔
927
                .unwrap()
1✔
928
        );
1✔
929
    }
1✔
930

931
    #[tokio::test]
932
    async fn simple_raster_without_spec() {
1✔
933
        let tile_size_in_pixels = [3, 2].into();
1✔
934
        let tiling_specification = TilingSpecification {
1✔
935
            tile_size_in_pixels,
1✔
936
        };
1✔
937
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
938

939
        let histogram = Histogram {
1✔
940
            params: HistogramParams {
1✔
941
                attribute_name: "band".to_string(),
1✔
942
                bounds: HistogramBounds::Data(Default::default()),
1✔
943
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
944
                    max_number_of_buckets: 100,
1✔
945
                },
1✔
946
                interactive: false,
1✔
947
            },
1✔
948
            sources: mock_raster_source().into(),
1✔
949
        };
1✔
950

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

961
        let result = query_processor
1✔
962
            .plot_query(
1✔
963
                PlotQueryRectangle::new(
1✔
964
                    BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
965
                    TimeInterval::default(),
1✔
966
                    PlotSeriesSelection::all(),
1✔
967
                ),
1✔
968
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
969
            )
1✔
970
            .await
1✔
971
            .unwrap();
1✔
972

973
        assert_eq!(
1✔
974
            result,
1✔
975
            geoengine_datatypes::plots::Histogram::builder(2, 1., 6., Measurement::Unitless)
1✔
976
                .counts(vec![3, 3])
1✔
977
                .build()
1✔
978
                .unwrap()
1✔
979
                .to_vega_embeddable(false)
1✔
980
                .unwrap()
1✔
981
        );
1✔
982
    }
1✔
983

984
    #[tokio::test]
985
    async fn vector_data() {
1✔
986
        let vector_source = MockFeatureCollectionSource::multiple(vec![
1✔
987
            DataCollection::from_slices(
1✔
988
                &[] as &[NoGeometry],
1✔
989
                &[TimeInterval::default(); 8],
1✔
990
                &[("foo", FeatureData::Int(vec![1, 1, 2, 2, 3, 3, 4, 4]))],
1✔
991
            )
992
            .unwrap(),
1✔
993
            DataCollection::from_slices(
1✔
994
                &[] as &[NoGeometry],
1✔
995
                &[TimeInterval::default(); 4],
1✔
996
                &[("foo", FeatureData::Int(vec![5, 6, 7, 8]))],
1✔
997
            )
998
            .unwrap(),
1✔
999
        ])
1000
        .boxed();
1✔
1001

1002
        let histogram = Histogram {
1✔
1003
            params: HistogramParams {
1✔
1004
                attribute_name: "foo".to_string(),
1✔
1005
                bounds: HistogramBounds::Values { min: 0.0, max: 8.0 },
1✔
1006
                buckets: HistogramBuckets::Number { value: 3 },
1✔
1007
                interactive: true,
1✔
1008
            },
1✔
1009
            sources: vector_source.into(),
1✔
1010
        };
1✔
1011

1012
        let execution_context = MockExecutionContext::test_default();
1✔
1013

1014
        let query_processor = histogram
1✔
1015
            .boxed()
1✔
1016
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1017
            .await
1✔
1018
            .unwrap()
1✔
1019
            .query_processor()
1✔
1020
            .unwrap()
1✔
1021
            .json_vega()
1✔
1022
            .unwrap();
1✔
1023

1024
        let result = query_processor
1✔
1025
            .plot_query(
1✔
1026
                PlotQueryRectangle::new(
1✔
1027
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1028
                    TimeInterval::default(),
1✔
1029
                    PlotSeriesSelection::all(),
1✔
1030
                ),
1✔
1031
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1032
            )
1✔
1033
            .await
1✔
1034
            .unwrap();
1✔
1035

1036
        assert_eq!(
1✔
1037
            result,
1✔
1038
            geoengine_datatypes::plots::Histogram::builder(3, 0., 8., Measurement::Unitless)
1✔
1039
                .counts(vec![4, 5, 3])
1✔
1040
                .build()
1✔
1041
                .unwrap()
1✔
1042
                .to_vega_embeddable(true)
1✔
1043
                .unwrap()
1✔
1044
        );
1✔
1045
    }
1✔
1046

1047
    #[tokio::test]
1048
    async fn vector_data_with_nulls() {
1✔
1049
        let vector_source = MockFeatureCollectionSource::single(
1✔
1050
            DataCollection::from_slices(
1✔
1051
                &[] as &[NoGeometry],
1✔
1052
                &[TimeInterval::default(); 6],
1✔
1053
                &[(
1✔
1054
                    "foo",
1✔
1055
                    FeatureData::NullableFloat(vec![
1✔
1056
                        Some(1.),
1✔
1057
                        Some(2.),
1✔
1058
                        None,
1✔
1059
                        Some(4.),
1✔
1060
                        None,
1✔
1061
                        Some(5.),
1✔
1062
                    ]),
1✔
1063
                )],
1✔
1064
            )
1065
            .unwrap(),
1✔
1066
        )
1067
        .boxed();
1✔
1068

1069
        let histogram = Histogram {
1✔
1070
            params: HistogramParams {
1✔
1071
                attribute_name: "foo".to_string(),
1✔
1072
                bounds: HistogramBounds::Data(Default::default()),
1✔
1073
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1074
                    max_number_of_buckets: 100,
1✔
1075
                },
1✔
1076
                interactive: false,
1✔
1077
            },
1✔
1078
            sources: vector_source.into(),
1✔
1079
        };
1✔
1080

1081
        let execution_context = MockExecutionContext::test_default();
1✔
1082

1083
        let query_processor = histogram
1✔
1084
            .boxed()
1✔
1085
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1086
            .await
1✔
1087
            .unwrap()
1✔
1088
            .query_processor()
1✔
1089
            .unwrap()
1✔
1090
            .json_vega()
1✔
1091
            .unwrap();
1✔
1092

1093
        let result = query_processor
1✔
1094
            .plot_query(
1✔
1095
                PlotQueryRectangle::new(
1✔
1096
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1097
                    TimeInterval::default(),
1✔
1098
                    PlotSeriesSelection::all(),
1✔
1099
                ),
1✔
1100
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1101
            )
1✔
1102
            .await
1✔
1103
            .unwrap();
1✔
1104

1105
        assert_eq!(
1✔
1106
            result,
1✔
1107
            geoengine_datatypes::plots::Histogram::builder(2, 1., 5., Measurement::Unitless)
1✔
1108
                .counts(vec![2, 2])
1✔
1109
                .build()
1✔
1110
                .unwrap()
1✔
1111
                .to_vega_embeddable(false)
1✔
1112
                .unwrap()
1✔
1113
        );
1✔
1114
    }
1✔
1115

1116
    #[tokio::test]
1117
    #[allow(clippy::too_many_lines)]
1118
    async fn text_attribute() {
1✔
1119
        let dataset_id = DatasetId::new();
1✔
1120
        let dataset_name = NamedData::with_system_name("ne_10m_ports");
1✔
1121

1122
        let workflow = serde_json::json!({
1✔
1123
            "type": "Histogram",
1✔
1124
            "params": {
1✔
1125
                "attributeName": "featurecla",
1✔
1126
                "bounds": "data",
1✔
1127
                "buckets": {
1✔
1128
                    "type": "squareRootChoiceRule",
1✔
1129
                    "maxNumberOfBuckets": 100,
1✔
1130
                }
1131
            },
1132
            "sources": {
1✔
1133
                "source": {
1✔
1134
                    "type": "OgrSource",
1✔
1135
                    "params": {
1✔
1136
                        "data": dataset_name.clone(),
1✔
1137
                        "attributeProjection": null
1✔
1138
                    },
1139
                }
1140
            }
1141
        });
1142
        let histogram: Histogram = serde_json::from_value(workflow).unwrap();
1✔
1143

1144
        let mut execution_context = MockExecutionContext::test_default();
1✔
1145
        execution_context.add_meta_data::<_, _, VectorQueryRectangle>(
1✔
1146
            DataId::Internal { dataset_id },
1✔
1147
            dataset_name,
1✔
1148
            Box::new(StaticMetaData {
1✔
1149
                loading_info: OgrSourceDataset {
1✔
1150
                    file_name: test_data!("vector/data/ne_10m_ports/ne_10m_ports.shp").into(),
1✔
1151
                    layer_name: "ne_10m_ports".to_string(),
1✔
1152
                    data_type: Some(VectorDataType::MultiPoint),
1✔
1153
                    time: OgrSourceDatasetTimeType::None,
1✔
1154
                    default_geometry: None,
1✔
1155
                    columns: Some(OgrSourceColumnSpec {
1✔
1156
                        format_specifics: None,
1✔
1157
                        x: String::new(),
1✔
1158
                        y: None,
1✔
1159
                        int: vec!["natlscale".to_string()],
1✔
1160
                        float: vec!["scalerank".to_string()],
1✔
1161
                        text: vec![
1✔
1162
                            "featurecla".to_string(),
1✔
1163
                            "name".to_string(),
1✔
1164
                            "website".to_string(),
1✔
1165
                        ],
1✔
1166
                        bool: vec![],
1✔
1167
                        datetime: vec![],
1✔
1168
                        rename: None,
1✔
1169
                    }),
1✔
1170
                    force_ogr_time_filter: false,
1✔
1171
                    force_ogr_spatial_filter: false,
1✔
1172
                    on_error: OgrSourceErrorSpec::Ignore,
1✔
1173
                    sql_query: None,
1✔
1174
                    attribute_query: None,
1✔
1175
                    cache_ttl: CacheTtlSeconds::default(),
1✔
1176
                },
1✔
1177
                result_descriptor: VectorResultDescriptor {
1✔
1178
                    data_type: VectorDataType::MultiPoint,
1✔
1179
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1180
                    columns: [
1✔
1181
                        (
1✔
1182
                            "natlscale".to_string(),
1✔
1183
                            VectorColumnInfo {
1✔
1184
                                data_type: FeatureDataType::Float,
1✔
1185
                                measurement: Measurement::Unitless,
1✔
1186
                            },
1✔
1187
                        ),
1✔
1188
                        (
1✔
1189
                            "scalerank".to_string(),
1✔
1190
                            VectorColumnInfo {
1✔
1191
                                data_type: FeatureDataType::Int,
1✔
1192
                                measurement: Measurement::Unitless,
1✔
1193
                            },
1✔
1194
                        ),
1✔
1195
                        (
1✔
1196
                            "featurecla".to_string(),
1✔
1197
                            VectorColumnInfo {
1✔
1198
                                data_type: FeatureDataType::Text,
1✔
1199
                                measurement: Measurement::Unitless,
1✔
1200
                            },
1✔
1201
                        ),
1✔
1202
                        (
1✔
1203
                            "name".to_string(),
1✔
1204
                            VectorColumnInfo {
1✔
1205
                                data_type: FeatureDataType::Text,
1✔
1206
                                measurement: Measurement::Unitless,
1✔
1207
                            },
1✔
1208
                        ),
1✔
1209
                        (
1✔
1210
                            "website".to_string(),
1✔
1211
                            VectorColumnInfo {
1✔
1212
                                data_type: FeatureDataType::Text,
1✔
1213
                                measurement: Measurement::Unitless,
1✔
1214
                            },
1✔
1215
                        ),
1✔
1216
                    ]
1✔
1217
                    .iter()
1✔
1218
                    .cloned()
1✔
1219
                    .collect(),
1✔
1220
                    time: None,
1✔
1221
                    bbox: None,
1✔
1222
                },
1✔
1223
                phantom: Default::default(),
1✔
1224
            }),
1✔
1225
        );
1226

1227
        if let Err(Error::InvalidOperatorSpec { reason }) = histogram
1✔
1228
            .boxed()
1✔
1229
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1230
            .await
1✔
1231
        {
1✔
1232
            assert_eq!(reason, "column `featurecla` must be numerical");
1✔
1233
        } else {
1✔
1234
            panic!("we currently don't support text features, but this went through");
1✔
1235
        }
1✔
1236
    }
1✔
1237

1238
    #[tokio::test]
1239
    async fn no_data_raster() {
1✔
1240
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
1241
        let result_descriptor = RasterResultDescriptor {
1✔
1242
            data_type: RasterDataType::U8,
1✔
1243
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1244
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
1245
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1246
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
1247
                tile_size_in_pixels.bounding_box(),
1✔
1248
            ),
1✔
1249
            bands: RasterBandDescriptors::new_single_band(),
1✔
1250
        };
1✔
1251
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
1252
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1253
        let histogram = Histogram {
1✔
1254
            params: HistogramParams {
1✔
1255
                attribute_name: "band".to_string(),
1✔
1256
                bounds: HistogramBounds::Data(Data),
1✔
1257
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1258
                    max_number_of_buckets: 100,
1✔
1259
                },
1✔
1260
                interactive: false,
1✔
1261
            },
1✔
1262
            sources: MockRasterSource {
1✔
1263
                params: MockRasterSourceParams {
1✔
1264
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1265
                        TimeInterval::default(),
1✔
1266
                        TileInformation {
1✔
1267
                            global_geo_transform: TestDefault::test_default(),
1✔
1268
                            global_tile_position: [0, 0].into(),
1✔
1269
                            tile_size_in_pixels,
1✔
1270
                        },
1✔
1271
                        0,
1✔
1272
                        EmptyGrid2D::<u8>::new(tile_size_in_pixels).into(),
1✔
1273
                        CacheHint::default(),
1✔
1274
                    )],
1✔
1275
                    result_descriptor,
1✔
1276
                },
1✔
1277
            }
1✔
1278
            .boxed()
1✔
1279
            .into(),
1✔
1280
        };
1✔
1281

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

1292
        let result = query_processor
1✔
1293
            .plot_query(
1✔
1294
                PlotQueryRectangle::new(
1✔
1295
                    BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
1296
                    TimeInterval::default(),
1✔
1297
                    PlotSeriesSelection::all(),
1✔
1298
                ),
1✔
1299
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1300
            )
1✔
1301
            .await
1✔
1302
            .unwrap();
1✔
1303

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

1314
    #[tokio::test]
1315
    async fn empty_feature_collection() {
1✔
1316
        let vector_source = MockFeatureCollectionSource::single(
1✔
1317
            DataCollection::from_slices(
1✔
1318
                &[] as &[NoGeometry],
1✔
1319
                &[] as &[TimeInterval],
1✔
1320
                &[("foo", FeatureData::Float(vec![]))],
1✔
1321
            )
1322
            .unwrap(),
1✔
1323
        )
1324
        .boxed();
1✔
1325

1326
        let histogram = Histogram {
1✔
1327
            params: HistogramParams {
1✔
1328
                attribute_name: "foo".to_string(),
1✔
1329
                bounds: HistogramBounds::Data(Default::default()),
1✔
1330
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1331
                    max_number_of_buckets: 100,
1✔
1332
                },
1✔
1333
                interactive: false,
1✔
1334
            },
1✔
1335
            sources: vector_source.into(),
1✔
1336
        };
1✔
1337

1338
        let execution_context = MockExecutionContext::test_default();
1✔
1339

1340
        let query_processor = histogram
1✔
1341
            .boxed()
1✔
1342
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1343
            .await
1✔
1344
            .unwrap()
1✔
1345
            .query_processor()
1✔
1346
            .unwrap()
1✔
1347
            .json_vega()
1✔
1348
            .unwrap();
1✔
1349

1350
        let result = query_processor
1✔
1351
            .plot_query(
1✔
1352
                PlotQueryRectangle::new(
1✔
1353
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1354
                    TimeInterval::default(),
1✔
1355
                    PlotSeriesSelection::all(),
1✔
1356
                ),
1✔
1357
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1358
            )
1✔
1359
            .await
1✔
1360
            .unwrap();
1✔
1361

1362
        assert_eq!(
1✔
1363
            result,
1✔
1364
            geoengine_datatypes::plots::Histogram::builder(1, 0., 0., Measurement::Unitless)
1✔
1365
                .build()
1✔
1366
                .unwrap()
1✔
1367
                .to_vega_embeddable(false)
1✔
1368
                .unwrap()
1✔
1369
        );
1✔
1370
    }
1✔
1371

1372
    #[tokio::test]
1373
    async fn feature_collection_with_one_feature() {
1✔
1374
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
1375
            vec![
1✔
1376
                DataCollection::from_slices(
1✔
1377
                    &[] as &[NoGeometry],
1✔
1378
                    &[TimeInterval::default()],
1✔
1379
                    &[("foo", FeatureData::Float(vec![5.0]))],
1✔
1380
                )
1381
                .unwrap(),
1✔
1382
            ],
1383
            [(
1✔
1384
                "foo".to_string(),
1✔
1385
                Measurement::continuous("bar".to_string(), None),
1✔
1386
            )]
1✔
1387
            .into_iter()
1✔
1388
            .collect(),
1✔
1389
        )
1390
        .boxed();
1✔
1391

1392
        let histogram = Histogram {
1✔
1393
            params: HistogramParams {
1✔
1394
                attribute_name: "foo".to_string(),
1✔
1395
                bounds: HistogramBounds::Data(Default::default()),
1✔
1396
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1397
                    max_number_of_buckets: 100,
1✔
1398
                },
1✔
1399
                interactive: false,
1✔
1400
            },
1✔
1401
            sources: vector_source.into(),
1✔
1402
        };
1✔
1403

1404
        let execution_context = MockExecutionContext::test_default();
1✔
1405

1406
        let query_processor = histogram
1✔
1407
            .boxed()
1✔
1408
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1409
            .await
1✔
1410
            .unwrap()
1✔
1411
            .query_processor()
1✔
1412
            .unwrap()
1✔
1413
            .json_vega()
1✔
1414
            .unwrap();
1✔
1415

1416
        let result = query_processor
1✔
1417
            .plot_query(
1✔
1418
                PlotQueryRectangle::new(
1✔
1419
                    BoundingBox2D::new((-180., -90.).into(), (180., 90.).into()).unwrap(),
1✔
1420
                    TimeInterval::default(),
1✔
1421
                    PlotSeriesSelection::all(),
1✔
1422
                ),
1✔
1423
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1424
            )
1✔
1425
            .await
1✔
1426
            .unwrap();
1✔
1427

1428
        assert_eq!(
1✔
1429
            result,
1✔
1430
            geoengine_datatypes::plots::Histogram::builder(
1✔
1431
                1,
1✔
1432
                5.,
1✔
1433
                5.,
1✔
1434
                Measurement::continuous("bar".to_string(), None)
1✔
1435
            )
1✔
1436
            .counts(vec![1])
1✔
1437
            .build()
1✔
1438
            .unwrap()
1✔
1439
            .to_vega_embeddable(false)
1✔
1440
            .unwrap()
1✔
1441
        );
1✔
1442
    }
1✔
1443

1444
    #[tokio::test]
1445
    async fn single_value_raster_stream() {
1✔
1446
        let tile_size_in_pixels = GridShape2D::new_2d(3, 2);
1✔
1447
        let result_descriptor = RasterResultDescriptor {
1✔
1448
            data_type: RasterDataType::U8,
1✔
1449
            spatial_reference: SpatialReference::epsg_4326().into(),
1✔
1450
            time: TimeDescriptor::new_irregular(Some(TimeInterval::default())),
1✔
1451
            spatial_grid: SpatialGridDescriptor::source_from_parts(
1✔
1452
                GeoTransform::new(Coordinate2D::new(0., 0.), 1., -1.),
1✔
1453
                tile_size_in_pixels.bounding_box(),
1✔
1454
            ),
1✔
1455
            bands: RasterBandDescriptors::new_single_band(),
1✔
1456
        };
1✔
1457
        let tiling_specification = TilingSpecification::new(tile_size_in_pixels);
1✔
1458

1459
        let execution_context = MockExecutionContext::new_with_tiling_spec(tiling_specification);
1✔
1460
        let histogram = Histogram {
1✔
1461
            params: HistogramParams {
1✔
1462
                attribute_name: "band".to_string(),
1✔
1463
                bounds: HistogramBounds::Data(Data),
1✔
1464
                buckets: HistogramBuckets::SquareRootChoiceRule {
1✔
1465
                    max_number_of_buckets: 100,
1✔
1466
                },
1✔
1467
                interactive: false,
1✔
1468
            },
1✔
1469
            sources: MockRasterSource {
1✔
1470
                params: MockRasterSourceParams {
1✔
1471
                    data: vec![RasterTile2D::new_with_tile_info(
1✔
1472
                        TimeInterval::default(),
1✔
1473
                        TileInformation {
1✔
1474
                            global_geo_transform: TestDefault::test_default(),
1✔
1475
                            global_tile_position: [0, 0].into(),
1✔
1476
                            tile_size_in_pixels,
1✔
1477
                        },
1✔
1478
                        0,
1✔
1479
                        Grid2D::new(tile_size_in_pixels, vec![4; 6]).unwrap().into(),
1✔
1480
                        CacheHint::default(),
1✔
1481
                    )],
1✔
1482
                    result_descriptor,
1✔
1483
                },
1✔
1484
            }
1✔
1485
            .boxed()
1✔
1486
            .into(),
1✔
1487
        };
1✔
1488

1489
        let query_processor = histogram
1✔
1490
            .boxed()
1✔
1491
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
1492
            .await
1✔
1493
            .unwrap()
1✔
1494
            .query_processor()
1✔
1495
            .unwrap()
1✔
1496
            .json_vega()
1✔
1497
            .unwrap();
1✔
1498

1499
        let result = query_processor
1✔
1500
            .plot_query(
1✔
1501
                PlotQueryRectangle::new(
1✔
1502
                    BoundingBox2D::new((0., -3.).into(), (2., 0.).into()).unwrap(),
1✔
1503
                    TimeInterval::new_instant(DateTime::new_utc(2013, 12, 1, 12, 0, 0)).unwrap(),
1✔
1504
                    PlotSeriesSelection::all(),
1✔
1505
                ),
1✔
1506
                &execution_context.mock_query_context(ChunkByteSize::MIN),
1✔
1507
            )
1✔
1508
            .await
1✔
1509
            .unwrap();
1✔
1510

1511
        assert_eq!(
1✔
1512
            result,
1✔
1513
            geoengine_datatypes::plots::Histogram::builder(1, 4., 4., Measurement::Unitless)
1✔
1514
                .counts(vec![6])
1✔
1515
                .build()
1✔
1516
                .unwrap()
1✔
1517
                .to_vega_embeddable(false)
1✔
1518
                .unwrap()
1✔
1519
        );
1✔
1520
    }
1✔
1521
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc