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

geo-engine / geoengine / 12417045631

19 Dec 2024 04:45PM UTC coverage: 90.354% (-0.2%) from 90.512%
12417045631

Pull #998

github

web-flow
Merge 9e7b54661 into 34e12969f
Pull Request #998: quota logging wip

834 of 1211 new or added lines in 66 files covered. (68.87%)

227 existing lines in 20 files now uncovered.

133835 of 148123 relevant lines covered (90.35%)

54353.34 hits per line

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

96.82
/operators/src/plot/pie_chart.rs
1
use crate::engine::{
2
    CanonicOperatorName, ExecutionContext, InitializedPlotOperator, InitializedSources,
3
    InitializedVectorOperator, Operator, OperatorName, PlotOperator, PlotQueryProcessor,
4
    PlotResultDescriptor, QueryContext, TypedPlotQueryProcessor, TypedVectorQueryProcessor,
5
    WorkflowOperatorPath,
6
};
7
use crate::engine::{QueryProcessor, SingleVectorSource};
8
use crate::error::Error;
9
use crate::util::Result;
10
use async_trait::async_trait;
11
use futures::StreamExt;
12
use geoengine_datatypes::collections::FeatureCollectionInfos;
13
use geoengine_datatypes::plots::{Plot, PlotData};
14
use geoengine_datatypes::primitives::{FeatureDataRef, Measurement, PlotQueryRectangle};
15
use serde::{Deserialize, Serialize};
16
use snafu::Snafu;
17
use std::collections::HashMap;
18

19
pub const PIE_CHART_OPERATOR_NAME: &str = "PieChart";
20

21
/// If the number of slices in the result is greater than this, the operator will fail.
22
pub const MAX_NUMBER_OF_SLICES: usize = 32;
23

24
/// A pie chart plot about a column of a vector input.
25
pub type PieChart = Operator<PieChartParams, SingleVectorSource>;
26

27
impl OperatorName for PieChart {
28
    const TYPE_NAME: &'static str = PIE_CHART_OPERATOR_NAME;
29
}
30

31
/// The parameter spec for `PieChart`
32
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
3✔
33
#[serde(tag = "type", rename_all = "camelCase")]
34
pub enum PieChartParams {
35
    /// Count the distinct values of a column
36
    #[serde(rename_all = "camelCase")]
37
    Count {
38
        /// Name of the (numeric) attribute to compute the histogram on. Fails if set for rasters.
39
        column_name: String,
40
        /// Whether to display the pie chart as a normal pie chart or as a donut chart.
41
        /// Defaults to `false`.
42
        #[serde(default)]
43
        donut: bool,
44
    },
45
    // TODO: another useful method would be `Sum` which sums up all values of a column A for a group column B
46
}
47

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

58
        let initialized_sources = self
6✔
59
            .sources
6✔
60
            .initialize_sources(path.clone(), context)
6✔
NEW
61
            .await?;
×
62
        let vector_source = initialized_sources.vector;
6✔
63

6✔
64
        let in_desc = vector_source.result_descriptor().clone();
6✔
65

6✔
66
        match self.params {
6✔
67
            PieChartParams::Count { column_name, donut } => {
6✔
68
                let Some(column_measurement) = in_desc.column_measurement(&column_name) else {
6✔
69
                    return Err(Error::ColumnDoesNotExist {
×
70
                        column: column_name,
×
71
                    });
×
72
                };
73

74
                let mut column_label = column_measurement.to_string();
6✔
75
                if column_label.is_empty() {
6✔
76
                    // in case of `Measurement::Unitless`
2✔
77
                    column_label.clone_from(&column_name);
2✔
78
                }
4✔
79

80
                let class_mapping =
6✔
81
                    if let Measurement::Classification(measurement) = &column_measurement {
6✔
82
                        Some(measurement.classes.clone())
3✔
83
                    } else {
84
                        None
3✔
85
                    };
86

87
                Ok(InitializedCountPieChart::new(
6✔
88
                    name,
6✔
89
                    vector_source,
6✔
90
                    in_desc.into(),
6✔
91
                    column_name.clone(),
6✔
92
                    column_label,
6✔
93
                    class_mapping,
6✔
94
                    donut,
6✔
95
                )
6✔
96
                .boxed())
6✔
97
            }
98
        }
99
    }
12✔
100

101
    span_fn!(PieChart);
102
}
103

104
/// The initialization of `Histogram`
105
pub struct InitializedCountPieChart<Op> {
106
    name: CanonicOperatorName,
107
    source: Op,
108
    result_descriptor: PlotResultDescriptor,
109
    column_name: String,
110
    column_label: String,
111
    class_mapping: Option<HashMap<u8, String>>,
112
    donut: bool,
113
}
114

115
impl<Op> InitializedCountPieChart<Op> {
116
    pub fn new(
6✔
117
        name: CanonicOperatorName,
6✔
118
        source: Op,
6✔
119
        result_descriptor: PlotResultDescriptor,
6✔
120
        column_name: String,
6✔
121
        column_label: String,
6✔
122
        class_mapping: Option<HashMap<u8, String>>,
6✔
123
        donut: bool,
6✔
124
    ) -> Self {
6✔
125
        Self {
6✔
126
            name,
6✔
127
            source,
6✔
128
            result_descriptor,
6✔
129
            column_name,
6✔
130
            column_label,
6✔
131
            class_mapping,
6✔
132
            donut,
6✔
133
        }
6✔
134
    }
6✔
135
}
136

137
impl InitializedPlotOperator for InitializedCountPieChart<Box<dyn InitializedVectorOperator>> {
138
    fn query_processor(&self) -> Result<TypedPlotQueryProcessor> {
6✔
139
        let processor = CountPieChartVectorQueryProcessor {
6✔
140
            input: self.source.query_processor()?,
6✔
141
            column_label: self.column_label.clone(),
6✔
142
            column_name: self.column_name.clone(),
6✔
143
            class_mapping: self.class_mapping.clone(),
6✔
144
            donut: self.donut,
6✔
145
        };
6✔
146

6✔
147
        Ok(TypedPlotQueryProcessor::JsonVega(processor.boxed()))
6✔
148
    }
6✔
149

150
    fn result_descriptor(&self) -> &PlotResultDescriptor {
×
151
        &self.result_descriptor
×
152
    }
×
153

154
    fn canonic_name(&self) -> CanonicOperatorName {
×
155
        self.name.clone()
×
156
    }
×
157
}
158

159
/// A query processor that calculates the Histogram about its vector inputs.
160
pub struct CountPieChartVectorQueryProcessor {
161
    input: TypedVectorQueryProcessor,
162
    column_label: String,
163
    column_name: String,
164
    class_mapping: Option<HashMap<u8, String>>,
165
    donut: bool,
166
}
167

168
#[async_trait]
169
impl PlotQueryProcessor for CountPieChartVectorQueryProcessor {
170
    type OutputFormat = PlotData;
171

172
    fn plot_type(&self) -> &'static str {
×
173
        PIE_CHART_OPERATOR_NAME
×
174
    }
×
175

176
    async fn plot_query<'p>(
177
        &'p self,
178
        query: PlotQueryRectangle,
179
        ctx: &'p dyn QueryContext,
180
    ) -> Result<Self::OutputFormat> {
6✔
181
        self.process(query, ctx).await
50✔
182
    }
12✔
183
}
184

185
/// Creates an iterator over all values as string
186
/// Null-values are empty strings.
187
pub fn feature_data_strings_iter<'f>(
52✔
188
    feature_data: &'f FeatureDataRef,
52✔
189
    class_mapping: Option<&'f HashMap<u8, String>>,
52✔
190
) -> Box<dyn Iterator<Item = String> + 'f> {
52✔
191
    match (feature_data, class_mapping) {
52✔
192
        (FeatureDataRef::Category(feature_data_ref), Some(class_mapping)) => {
×
193
            return Box::new(feature_data_ref.as_ref().iter().map(|v| {
×
194
                class_mapping
×
195
                    .get(v)
×
196
                    .map_or_else(String::new, ToString::to_string)
×
197
            }));
×
198
        }
199
        (FeatureDataRef::Int(feature_data_ref), Some(class_mapping)) => {
2✔
200
            return Box::new(feature_data_ref.as_ref().iter().map(|v| {
12✔
201
                class_mapping
12✔
202
                    .get(&(*v as u8))
12✔
203
                    .map_or_else(String::new, ToString::to_string)
12✔
204
            }));
12✔
205
        }
206
        (FeatureDataRef::Float(feature_data_ref), Some(class_mapping)) => {
2✔
207
            return Box::new(feature_data_ref.as_ref().iter().map(|v| {
2✔
208
                class_mapping
1✔
209
                    .get(&(*v as u8))
1✔
210
                    .map_or_else(String::new, ToString::to_string)
1✔
211
            }));
2✔
212
        }
213
        _ => {
48✔
214
            // no special treatment for other types
48✔
215
        }
48✔
216
    }
48✔
217

48✔
218
    feature_data.strings_iter()
48✔
219
}
52✔
220

221
impl CountPieChartVectorQueryProcessor {
222
    async fn process<'p>(
6✔
223
        &'p self,
6✔
224
        query: PlotQueryRectangle,
6✔
225
        ctx: &'p dyn QueryContext,
6✔
226
    ) -> Result<<CountPieChartVectorQueryProcessor as PlotQueryProcessor>::OutputFormat> {
6✔
227
        let mut slices: HashMap<String, f64> = HashMap::new();
6✔
228

229
        // TODO: parallelize
230

231
        call_on_generic_vector_processor!(&self.input, processor => {
6✔
232
            let mut query = processor.query(query.into(), ctx).await?;
4✔
233

234
            while let Some(collection) = query.next().await {
9✔
235
                let collection = collection?;
5✔
236

237
                let feature_data = collection.data(&self.column_name)?;
5✔
238

239
                let feature_data_strings = feature_data_strings_iter(&feature_data, self.class_mapping.as_ref());
5✔
240

241
                for v in feature_data_strings {
24✔
242
                    if v.is_empty() {
19✔
243
                        continue; // ignore no data
2✔
244
                    }
17✔
245

17✔
246
                    *slices.entry(v).or_insert(0.0) += 1.0;
17✔
247
                }
248

249
                if slices.len() > MAX_NUMBER_OF_SLICES {
5✔
250
                    return Err(PieChartError::TooManySlices.into());
×
251
                }
5✔
252
            }
253
        });
254

255
        // TODO: display NO-DATA count?
256

257
        let bar_chart = geoengine_datatypes::plots::PieChart::new(
5✔
258
            slices.into_iter().collect(),
5✔
259
            self.column_label.clone(),
5✔
260
            self.donut,
5✔
261
        )?;
5✔
262
        let chart = bar_chart.to_vega_embeddable(false)?;
5✔
263

264
        Ok(chart)
5✔
265
    }
6✔
266
}
267

268
#[derive(Debug, Snafu, Clone, PartialEq, Eq)]
1✔
269
#[snafu(
270
    visibility(pub(crate)),
271
    context(suffix(false)), // disables default `Snafu` suffix
272
    module(error),
273
)]
274
pub enum PieChartError {
275
    #[snafu(display(
276
        "The number of slices is too high. Maximum is {}.",
277
        MAX_NUMBER_OF_SLICES
278
    ))]
279
    TooManySlices,
280
}
281

282
#[cfg(test)]
283
mod tests {
284

285
    use super::*;
286

287
    use crate::engine::{
288
        ChunkByteSize, MockExecutionContext, MockQueryContext, StaticMetaData, VectorColumnInfo,
289
        VectorOperator, VectorResultDescriptor,
290
    };
291
    use crate::mock::MockFeatureCollectionSource;
292
    use crate::source::{
293
        AttributeFilter, OgrSource, OgrSourceColumnSpec, OgrSourceDataset,
294
        OgrSourceDatasetTimeType, OgrSourceErrorSpec, OgrSourceParameters,
295
    };
296
    use crate::test_data;
297
    use geoengine_datatypes::dataset::{DataId, DatasetId, NamedData};
298
    use geoengine_datatypes::primitives::{
299
        BoundingBox2D, FeatureData, FeatureDataType, NoGeometry, PlotSeriesSelection,
300
        SpatialResolution, TimeInterval,
301
    };
302
    use geoengine_datatypes::primitives::{CacheTtlSeconds, VectorQueryRectangle};
303
    use geoengine_datatypes::spatial_reference::SpatialReference;
304
    use geoengine_datatypes::util::test::TestDefault;
305
    use geoengine_datatypes::util::Identifier;
306
    use geoengine_datatypes::{
307
        collections::{DataCollection, VectorDataType},
308
        primitives::MultiPoint,
309
    };
310
    use serde_json::json;
311

312
    #[test]
313
    fn serialization() {
1✔
314
        let pie_chart = PieChart {
1✔
315
            params: PieChartParams::Count {
1✔
316
                column_name: "my_column".to_string(),
1✔
317
                donut: false,
1✔
318
            },
1✔
319
            sources: MockFeatureCollectionSource::<MultiPoint>::multiple(vec![])
1✔
320
                .boxed()
1✔
321
                .into(),
1✔
322
        };
1✔
323

1✔
324
        let serialized = json!({
1✔
325
            "type": "PieChart",
1✔
326
            "params": {
1✔
327
                "type": "count",
1✔
328
                "columnName": "my_column",
1✔
329
            },
1✔
330
            "sources": {
1✔
331
                "vector": {
1✔
332
                    "type": "MockFeatureCollectionSourceMultiPoint",
1✔
333
                    "params": {
1✔
334
                        "collections": [],
1✔
335
                        "spatialReference": "EPSG:4326",
1✔
336
                        "measurements": {},
1✔
337
                    }
1✔
338
                }
1✔
339
            }
1✔
340
        })
1✔
341
        .to_string();
1✔
342

1✔
343
        let deserialized: PieChart = serde_json::from_str(&serialized).unwrap();
1✔
344

1✔
345
        assert_eq!(deserialized.params, pie_chart.params);
1✔
346
    }
1✔
347

348
    #[tokio::test]
349
    async fn vector_data_with_classification() {
1✔
350
        let measurement = Measurement::classification(
1✔
351
            "foo".to_string(),
1✔
352
            [
1✔
353
                (1, "A".to_string()),
1✔
354
                (2, "B".to_string()),
1✔
355
                (3, "C".to_string()),
1✔
356
            ]
1✔
357
            .into_iter()
1✔
358
            .collect(),
1✔
359
        );
1✔
360

1✔
361
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
362
            vec![
1✔
363
                DataCollection::from_slices(
1✔
364
                    &[] as &[NoGeometry],
1✔
365
                    &[TimeInterval::default(); 8],
1✔
366
                    &[("foo", FeatureData::Int(vec![1, 1, 2, 2, 3, 3, 1, 2]))],
1✔
367
                )
1✔
368
                .unwrap(),
1✔
369
                DataCollection::from_slices(
1✔
370
                    &[] as &[NoGeometry],
1✔
371
                    &[TimeInterval::default(); 4],
1✔
372
                    &[("foo", FeatureData::Int(vec![1, 1, 2, 3]))],
1✔
373
                )
1✔
374
                .unwrap(),
1✔
375
            ],
1✔
376
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
377
        )
1✔
378
        .boxed();
1✔
379

1✔
380
        let pie_chart = PieChart {
1✔
381
            params: PieChartParams::Count {
1✔
382
                column_name: "foo".to_string(),
1✔
383
                donut: false,
1✔
384
            },
1✔
385
            sources: vector_source.into(),
1✔
386
        };
1✔
387

1✔
388
        let execution_context = MockExecutionContext::test_default();
1✔
389

1✔
390
        let query_processor = pie_chart
1✔
391
            .boxed()
1✔
392
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
393
            .await
1✔
394
            .unwrap()
1✔
395
            .query_processor()
1✔
396
            .unwrap()
1✔
397
            .json_vega()
1✔
398
            .unwrap();
1✔
399

1✔
400
        let result = query_processor
1✔
401
            .plot_query(
1✔
402
                PlotQueryRectangle {
1✔
403
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
404
                        .unwrap(),
1✔
405
                    time_interval: TimeInterval::default(),
1✔
406
                    spatial_resolution: SpatialResolution::one(),
1✔
407
                    attributes: PlotSeriesSelection::all(),
1✔
408
                },
1✔
409
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
410
            )
1✔
411
            .await
1✔
412
            .unwrap();
1✔
413

1✔
414
        assert_eq!(
1✔
415
            result,
1✔
416
            geoengine_datatypes::plots::PieChart::new(
1✔
417
                [
1✔
418
                    ("A".to_string(), 5.),
1✔
419
                    ("B".to_string(), 4.),
1✔
420
                    ("C".to_string(), 3.),
1✔
421
                ]
1✔
422
                .into(),
1✔
423
                "foo".to_string(),
1✔
424
                false,
1✔
425
            )
1✔
426
            .unwrap()
1✔
427
            .to_vega_embeddable(false)
1✔
428
            .unwrap()
1✔
429
        );
1✔
430
    }
1✔
431

432
    #[tokio::test]
433
    async fn vector_data_with_nulls() {
1✔
434
        let measurement = Measurement::continuous("foo".to_string(), None);
1✔
435

1✔
436
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
437
            vec![DataCollection::from_slices(
1✔
438
                &[] as &[NoGeometry],
1✔
439
                &[TimeInterval::default(); 6],
1✔
440
                &[(
1✔
441
                    "foo",
1✔
442
                    FeatureData::NullableFloat(vec![
1✔
443
                        Some(1.),
1✔
444
                        Some(2.),
1✔
445
                        None,
1✔
446
                        Some(1.),
1✔
447
                        None,
1✔
448
                        Some(3.),
1✔
449
                    ]),
1✔
450
                )],
1✔
451
            )
1✔
452
            .unwrap()],
1✔
453
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
454
        )
1✔
455
        .boxed();
1✔
456

1✔
457
        let pie_chart = PieChart {
1✔
458
            params: PieChartParams::Count {
1✔
459
                column_name: "foo".to_string(),
1✔
460
                donut: false,
1✔
461
            },
1✔
462
            sources: vector_source.into(),
1✔
463
        };
1✔
464

1✔
465
        let execution_context = MockExecutionContext::test_default();
1✔
466

1✔
467
        let query_processor = pie_chart
1✔
468
            .boxed()
1✔
469
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
470
            .await
1✔
471
            .unwrap()
1✔
472
            .query_processor()
1✔
473
            .unwrap()
1✔
474
            .json_vega()
1✔
475
            .unwrap();
1✔
476

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

1✔
491
        assert_eq!(
1✔
492
            result,
1✔
493
            geoengine_datatypes::plots::PieChart::new(
1✔
494
                [
1✔
495
                    ("1".to_string(), 2.),
1✔
496
                    ("2".to_string(), 1.),
1✔
497
                    ("3".to_string(), 1.),
1✔
498
                ]
1✔
499
                .into(),
1✔
500
                "foo".to_string(),
1✔
501
                false,
1✔
502
            )
1✔
503
            .unwrap()
1✔
504
            .to_vega_embeddable(false)
1✔
505
            .unwrap()
1✔
506
        );
1✔
507
    }
1✔
508

509
    #[tokio::test]
510
    #[allow(clippy::too_many_lines)]
511
    async fn text_attribute() {
1✔
512
        let dataset_id = DatasetId::new();
1✔
513
        let dataset_name = NamedData::with_system_name("ne_10m_ports");
1✔
514

1✔
515
        let mut execution_context = MockExecutionContext::test_default();
1✔
516
        execution_context.add_meta_data::<_, _, VectorQueryRectangle>(
1✔
517
            DataId::Internal { dataset_id },
1✔
518
            dataset_name.clone(),
1✔
519
            Box::new(StaticMetaData {
1✔
520
                loading_info: OgrSourceDataset {
1✔
521
                    file_name: test_data!("vector/data/ne_10m_ports/ne_10m_ports.shp").into(),
1✔
522
                    layer_name: "ne_10m_ports".to_string(),
1✔
523
                    data_type: Some(VectorDataType::MultiPoint),
1✔
524
                    time: OgrSourceDatasetTimeType::None,
1✔
525
                    default_geometry: None,
1✔
526
                    columns: Some(OgrSourceColumnSpec {
1✔
527
                        format_specifics: None,
1✔
528
                        x: String::new(),
1✔
529
                        y: None,
1✔
530
                        int: vec!["natlscale".to_string()],
1✔
531
                        float: vec!["scalerank".to_string()],
1✔
532
                        text: vec![
1✔
533
                            "featurecla".to_string(),
1✔
534
                            "name".to_string(),
1✔
535
                            "website".to_string(),
1✔
536
                        ],
1✔
537
                        bool: vec![],
1✔
538
                        datetime: vec![],
1✔
539
                        rename: None,
1✔
540
                    }),
1✔
541
                    force_ogr_time_filter: false,
1✔
542
                    force_ogr_spatial_filter: false,
1✔
543
                    on_error: OgrSourceErrorSpec::Ignore,
1✔
544
                    sql_query: None,
1✔
545
                    attribute_query: None,
1✔
546
                    cache_ttl: CacheTtlSeconds::default(),
1✔
547
                },
1✔
548
                result_descriptor: VectorResultDescriptor {
1✔
549
                    data_type: VectorDataType::MultiPoint,
1✔
550
                    spatial_reference: SpatialReference::epsg_4326().into(),
1✔
551
                    columns: [
1✔
552
                        (
1✔
553
                            "natlscale".to_string(),
1✔
554
                            VectorColumnInfo {
1✔
555
                                data_type: FeatureDataType::Float,
1✔
556
                                measurement: Measurement::Unitless,
1✔
557
                            },
1✔
558
                        ),
1✔
559
                        (
1✔
560
                            "scalerank".to_string(),
1✔
561
                            VectorColumnInfo {
1✔
562
                                data_type: FeatureDataType::Int,
1✔
563
                                measurement: Measurement::Unitless,
1✔
564
                            },
1✔
565
                        ),
1✔
566
                        (
1✔
567
                            "featurecla".to_string(),
1✔
568
                            VectorColumnInfo {
1✔
569
                                data_type: FeatureDataType::Text,
1✔
570
                                measurement: Measurement::Unitless,
1✔
571
                            },
1✔
572
                        ),
1✔
573
                        (
1✔
574
                            "name".to_string(),
1✔
575
                            VectorColumnInfo {
1✔
576
                                data_type: FeatureDataType::Text,
1✔
577
                                measurement: Measurement::Unitless,
1✔
578
                            },
1✔
579
                        ),
1✔
580
                        (
1✔
581
                            "website".to_string(),
1✔
582
                            VectorColumnInfo {
1✔
583
                                data_type: FeatureDataType::Text,
1✔
584
                                measurement: Measurement::Unitless,
1✔
585
                            },
1✔
586
                        ),
1✔
587
                    ]
1✔
588
                    .iter()
1✔
589
                    .cloned()
1✔
590
                    .collect(),
1✔
591
                    time: None,
1✔
592
                    bbox: None,
1✔
593
                },
1✔
594
                phantom: Default::default(),
1✔
595
            }),
1✔
596
        );
1✔
597

1✔
598
        let pie_chart = PieChart {
1✔
599
            params: PieChartParams::Count {
1✔
600
                column_name: "name".to_string(),
1✔
601
                donut: false,
1✔
602
            },
1✔
603
            sources: OgrSource {
1✔
604
                params: OgrSourceParameters {
1✔
605
                    data: dataset_name.clone(),
1✔
606
                    attribute_projection: None,
1✔
607
                    attribute_filters: None,
1✔
608
                },
1✔
609
            }
1✔
610
            .boxed()
1✔
611
            .into(),
1✔
612
        };
1✔
613

1✔
614
        let query_processor = pie_chart
1✔
615
            .boxed()
1✔
616
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
617
            .await
1✔
618
            .unwrap()
1✔
619
            .query_processor()
1✔
620
            .unwrap()
1✔
621
            .json_vega()
1✔
622
            .unwrap();
1✔
623

1✔
624
        let result = query_processor
1✔
625
            .plot_query(
1✔
626
                PlotQueryRectangle {
1✔
627
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
628
                        .unwrap(),
1✔
629
                    time_interval: TimeInterval::default(),
1✔
630
                    spatial_resolution: SpatialResolution::one(),
1✔
631
                    attributes: PlotSeriesSelection::all(),
1✔
632
                },
1✔
633
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
634
            )
1✔
635
            .await
34✔
636
            .unwrap_err();
1✔
637

1✔
638
        assert_eq!(
1✔
639
            result.to_string(),
1✔
640
            "PieChart error: The number of slices is too high. Maximum is 32."
1✔
641
        );
1✔
642

1✔
643
        let pie_chart = PieChart {
1✔
644
            params: PieChartParams::Count {
1✔
645
                column_name: "name".to_string(),
1✔
646
                donut: false,
1✔
647
            },
1✔
648
            sources: OgrSource {
1✔
649
                params: OgrSourceParameters {
1✔
650
                    data: dataset_name,
1✔
651
                    attribute_projection: None,
1✔
652
                    attribute_filters: Some(vec![AttributeFilter {
1✔
653
                        attribute: "name".to_string(),
1✔
654
                        ranges: vec![("E".to_string()..="F".to_string()).into()],
1✔
655
                        keep_nulls: false,
1✔
656
                    }]),
1✔
657
                },
1✔
658
            }
1✔
659
            .boxed()
1✔
660
            .into(),
1✔
661
        };
1✔
662

1✔
663
        let query_processor = pie_chart
1✔
664
            .boxed()
1✔
665
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
666
            .await
1✔
667
            .unwrap()
1✔
668
            .query_processor()
1✔
669
            .unwrap()
1✔
670
            .json_vega()
1✔
671
            .unwrap();
1✔
672

1✔
673
        let result = query_processor
1✔
674
            .plot_query(
1✔
675
                PlotQueryRectangle {
1✔
676
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
677
                        .unwrap(),
1✔
678
                    time_interval: TimeInterval::default(),
1✔
679
                    spatial_resolution: SpatialResolution::one(),
1✔
680
                    attributes: PlotSeriesSelection::all(),
1✔
681
                },
1✔
682
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
683
            )
1✔
684
            .await
16✔
685
            .unwrap();
1✔
686

1✔
687
        assert_eq!(
1✔
688
            result,
1✔
689
            geoengine_datatypes::plots::PieChart::new(
1✔
690
                [
1✔
691
                    ("Esquimalt".to_string(), 1.),
1✔
692
                    ("Eckernforde".to_string(), 1.),
1✔
693
                    ("Escanaba".to_string(), 1.),
1✔
694
                    ("Esperance".to_string(), 1.),
1✔
695
                    ("Eden".to_string(), 1.),
1✔
696
                    ("Esmeraldas".to_string(), 1.),
1✔
697
                    ("Europoort".to_string(), 1.),
1✔
698
                    ("Elat".to_string(), 1.),
1✔
699
                    ("Emden".to_string(), 1.),
1✔
700
                    ("Esbjerg".to_string(), 1.),
1✔
701
                    ("Ensenada".to_string(), 1.),
1✔
702
                    ("East London".to_string(), 1.),
1✔
703
                    ("Erie".to_string(), 1.),
1✔
704
                    ("Eureka".to_string(), 1.),
1✔
705
                ]
1✔
706
                .into(),
1✔
707
                "name".to_string(),
1✔
708
                false,
1✔
709
            )
1✔
710
            .unwrap()
1✔
711
            .to_vega_embeddable(false)
1✔
712
            .unwrap()
1✔
713
        );
1✔
714
    }
1✔
715

716
    #[tokio::test]
717
    async fn empty_feature_collection() {
1✔
718
        let measurement = Measurement::classification(
1✔
719
            "foo".to_string(),
1✔
720
            [(1, "A".to_string())].into_iter().collect(),
1✔
721
        );
1✔
722

1✔
723
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
724
            vec![DataCollection::from_slices(
1✔
725
                &[] as &[NoGeometry],
1✔
726
                &[] as &[TimeInterval],
1✔
727
                &[("foo", FeatureData::Float(vec![]))],
1✔
728
            )
1✔
729
            .unwrap()],
1✔
730
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
731
        )
1✔
732
        .boxed();
1✔
733

1✔
734
        let pie_chart = PieChart {
1✔
735
            params: PieChartParams::Count {
1✔
736
                column_name: "foo".to_string(),
1✔
737
                donut: false,
1✔
738
            },
1✔
739
            sources: vector_source.into(),
1✔
740
        };
1✔
741

1✔
742
        let execution_context = MockExecutionContext::test_default();
1✔
743

1✔
744
        let query_processor = pie_chart
1✔
745
            .boxed()
1✔
746
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
747
            .await
1✔
748
            .unwrap()
1✔
749
            .query_processor()
1✔
750
            .unwrap()
1✔
751
            .json_vega()
1✔
752
            .unwrap();
1✔
753

1✔
754
        let result = query_processor
1✔
755
            .plot_query(
1✔
756
                PlotQueryRectangle {
1✔
757
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
758
                        .unwrap(),
1✔
759
                    time_interval: TimeInterval::default(),
1✔
760
                    spatial_resolution: SpatialResolution::one(),
1✔
761
                    attributes: PlotSeriesSelection::all(),
1✔
762
                },
1✔
763
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
764
            )
1✔
765
            .await
1✔
766
            .unwrap();
1✔
767

1✔
768
        assert_eq!(
1✔
769
            result,
1✔
770
            geoengine_datatypes::plots::PieChart::new(
1✔
771
                Default::default(),
1✔
772
                "foo".to_string(),
1✔
773
                false,
1✔
774
            )
1✔
775
            .unwrap()
1✔
776
            .to_vega_embeddable(false)
1✔
777
            .unwrap()
1✔
778
        );
1✔
779
    }
1✔
780

781
    #[tokio::test]
782
    async fn feature_collection_with_one_feature() {
1✔
783
        let measurement = Measurement::classification(
1✔
784
            "foo".to_string(),
1✔
785
            [(5, "A".to_string())].into_iter().collect(),
1✔
786
        );
1✔
787

1✔
788
        let vector_source = MockFeatureCollectionSource::with_collections_and_measurements(
1✔
789
            vec![DataCollection::from_slices(
1✔
790
                &[] as &[NoGeometry],
1✔
791
                &[TimeInterval::default()],
1✔
792
                &[("foo", FeatureData::Float(vec![5.0]))],
1✔
793
            )
1✔
794
            .unwrap()],
1✔
795
            [("foo".to_string(), measurement)].into_iter().collect(),
1✔
796
        )
1✔
797
        .boxed();
1✔
798

1✔
799
        let pie_chart = PieChart {
1✔
800
            params: PieChartParams::Count {
1✔
801
                column_name: "foo".to_string(),
1✔
802
                donut: false,
1✔
803
            },
1✔
804
            sources: vector_source.into(),
1✔
805
        };
1✔
806

1✔
807
        let execution_context = MockExecutionContext::test_default();
1✔
808

1✔
809
        let query_processor = pie_chart
1✔
810
            .boxed()
1✔
811
            .initialize(WorkflowOperatorPath::initialize_root(), &execution_context)
1✔
812
            .await
1✔
813
            .unwrap()
1✔
814
            .query_processor()
1✔
815
            .unwrap()
1✔
816
            .json_vega()
1✔
817
            .unwrap();
1✔
818

1✔
819
        let result = query_processor
1✔
820
            .plot_query(
1✔
821
                PlotQueryRectangle {
1✔
822
                    spatial_bounds: BoundingBox2D::new((-180., -90.).into(), (180., 90.).into())
1✔
823
                        .unwrap(),
1✔
824
                    time_interval: TimeInterval::default(),
1✔
825
                    spatial_resolution: SpatialResolution::one(),
1✔
826
                    attributes: PlotSeriesSelection::all(),
1✔
827
                },
1✔
828
                &MockQueryContext::new(ChunkByteSize::MIN),
1✔
829
            )
1✔
830
            .await
1✔
831
            .unwrap();
1✔
832

1✔
833
        assert_eq!(
1✔
834
            result,
1✔
835
            geoengine_datatypes::plots::PieChart::new(
1✔
836
                [("A".to_string(), 1.),].into(),
1✔
837
                "foo".to_string(),
1✔
838
                false,
1✔
839
            )
1✔
840
            .unwrap()
1✔
841
            .to_vega_embeddable(false)
1✔
842
            .unwrap()
1✔
843
        );
1✔
844
    }
1✔
845
}
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