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

geo-engine / geoengine / 22961910495

11 Mar 2026 04:00PM UTC coverage: 88.133% (-0.06%) from 88.19%
22961910495

push

github

web-flow
feat: Operators in OpenAPI (#1116)

* build: use correct vergen-gitcl version

* feat: GdalSource as openapi

* feat: MockPointSource OpenAPI

* feat: expression openapi

* feat: RasterVectorJoin openapi

* fmt

* empty plot operator type

* fix: enhance expression and raster/vector join operators with new source structures

* fix: add Expression operator and support for RasterVectorJoin conversion

* fix: update ColumnNames enum to use structured values and enhance OpenAPI documentation

* fix: add schema annotation to output_band in ExpressionParameters to prevent null values

* fix: update OpenAPI documentation and schema annotations for Expression and GdalSource operators

* Refactor API structure and remove deprecated files

- Deleted the README.md and lib.rs files from the api module as they are no longer needed.
- Updated services/Cargo.toml to remove the geoengine-api dependency.
- Refactored the API documentation to point to the new processingGraphs endpoint.
- Introduced a new processing_graphs module with operators for raster and vector processing.
- Added parameters and processing logic for Expression and RasterVectorJoin operators.
- Implemented GdalSource and MockPointSource for data input handling.
- Enhanced serialization and deserialization for new data types and structures.

* refactor: remove 4th impl of coordinate2d

* refactor: update Workflow usage to Legacy variant across multiple modules

* build: add justfile for build, lint, run, and test commands

* refactor: rename TypedOperator to LegacyTypedOperator and update references in API and workflows

* refactor: update visit_schema and visit_response functions to include already_visited parameter for recursion prevention

* build: update dependencies to latest versions in Cargo.lock

429 of 561 new or added lines in 25 files covered. (76.47%)

3 existing lines in 2 files now uncovered.

112722 of 127900 relevant lines covered (88.13%)

506349.16 hits per line

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

48.58
/services/src/api/model/processing_graphs/parameters.rs
1
use crate::api::model::{
2
    datatypes::Coordinate2D,
3
    processing_graphs::{RasterOperator, VectorOperator},
4
};
5
use anyhow::Context;
6
use geoengine_macros::type_tag;
7
use serde::{Deserialize, Serialize, Serializer};
8
use std::collections::BTreeMap;
9
use utoipa::ToSchema;
10

11
/// A raster data type.
12
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
13
#[serde(rename_all = "camelCase")]
14
pub enum RasterDataType {
15
    U8,
16
    U16,
17
    U32,
18
    U64,
19
    I8,
20
    I16,
21
    I32,
22
    I64,
23
    F32,
24
    F64,
25
}
26

27
impl From<geoengine_datatypes::raster::RasterDataType> for RasterDataType {
28
    fn from(value: geoengine_datatypes::raster::RasterDataType) -> Self {
1✔
29
        match value {
1✔
NEW
30
            geoengine_datatypes::raster::RasterDataType::U8 => Self::U8,
×
NEW
31
            geoengine_datatypes::raster::RasterDataType::U16 => Self::U16,
×
NEW
32
            geoengine_datatypes::raster::RasterDataType::U32 => Self::U32,
×
NEW
33
            geoengine_datatypes::raster::RasterDataType::U64 => Self::U64,
×
NEW
34
            geoengine_datatypes::raster::RasterDataType::I8 => Self::I8,
×
NEW
35
            geoengine_datatypes::raster::RasterDataType::I16 => Self::I16,
×
NEW
36
            geoengine_datatypes::raster::RasterDataType::I32 => Self::I32,
×
NEW
37
            geoengine_datatypes::raster::RasterDataType::I64 => Self::I64,
×
38
            geoengine_datatypes::raster::RasterDataType::F32 => Self::F32,
1✔
NEW
39
            geoengine_datatypes::raster::RasterDataType::F64 => Self::F64,
×
40
        }
41
    }
1✔
42
}
43

44
impl From<RasterDataType> for geoengine_datatypes::raster::RasterDataType {
45
    fn from(value: RasterDataType) -> Self {
2✔
46
        match value {
2✔
NEW
47
            RasterDataType::U8 => Self::U8,
×
NEW
48
            RasterDataType::U16 => Self::U16,
×
NEW
49
            RasterDataType::U32 => Self::U32,
×
NEW
50
            RasterDataType::U64 => Self::U64,
×
NEW
51
            RasterDataType::I8 => Self::I8,
×
NEW
52
            RasterDataType::I16 => Self::I16,
×
NEW
53
            RasterDataType::I32 => Self::I32,
×
NEW
54
            RasterDataType::I64 => Self::I64,
×
55
            RasterDataType::F32 => Self::F32,
2✔
NEW
56
            RasterDataType::F64 => Self::F64,
×
57
        }
58
    }
2✔
59
}
60

61
/// Measurement information for a raster band.
62
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
63
#[serde(rename_all = "camelCase", untagged)]
64
#[schema(discriminator = "type")]
65
pub enum Measurement {
66
    Unitless(UnitlessMeasurement),
67
    Continuous(ContinuousMeasurement),
68
    Classification(ClassificationMeasurement),
69
}
70

71
impl From<geoengine_datatypes::primitives::Measurement> for Measurement {
72
    fn from(value: geoengine_datatypes::primitives::Measurement) -> Self {
1✔
73
        match value {
1✔
74
            geoengine_datatypes::primitives::Measurement::Unitless => {
75
                Self::Unitless(UnitlessMeasurement {
1✔
76
                    r#type: Default::default(),
1✔
77
                })
1✔
78
            }
NEW
79
            geoengine_datatypes::primitives::Measurement::Continuous(cm) => {
×
NEW
80
                Self::Continuous(cm.into())
×
81
            }
NEW
82
            geoengine_datatypes::primitives::Measurement::Classification(cm) => {
×
NEW
83
                Self::Classification(cm.into())
×
84
            }
85
        }
86
    }
1✔
87
}
88

89
impl From<Measurement> for geoengine_datatypes::primitives::Measurement {
90
    fn from(value: Measurement) -> Self {
1✔
91
        match value {
1✔
92
            Measurement::Unitless(_) => Self::Unitless,
1✔
NEW
93
            Measurement::Continuous(cm) => Self::Continuous(cm.into()),
×
NEW
94
            Measurement::Classification(cm) => Self::Classification(cm.into()),
×
95
        }
96
    }
1✔
97
}
98

99
/// A measurement without a unit.
100
#[type_tag(value = "unitless")]
101
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema, Default)]
102
pub struct UnitlessMeasurement {}
103

104
/// A continuous measurement, e.g., "temperature".
105
/// It may have an optional unit, e.g., "°C" for degrees Celsius.
106
#[type_tag(value = "continuous")]
107
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
108
pub struct ContinuousMeasurement {
109
    pub measurement: String,
110
    pub unit: Option<String>,
111
}
112

113
impl From<geoengine_datatypes::primitives::ContinuousMeasurement> for ContinuousMeasurement {
NEW
114
    fn from(value: geoengine_datatypes::primitives::ContinuousMeasurement) -> Self {
×
NEW
115
        ContinuousMeasurement {
×
NEW
116
            r#type: Default::default(),
×
NEW
117
            measurement: value.measurement,
×
NEW
118
            unit: value.unit,
×
NEW
119
        }
×
NEW
120
    }
×
121
}
122

123
impl From<ContinuousMeasurement> for geoengine_datatypes::primitives::ContinuousMeasurement {
NEW
124
    fn from(value: ContinuousMeasurement) -> Self {
×
NEW
125
        Self {
×
NEW
126
            measurement: value.measurement,
×
NEW
127
            unit: value.unit,
×
NEW
128
        }
×
NEW
129
    }
×
130
}
131

132
impl From<geoengine_datatypes::primitives::ClassificationMeasurement>
133
    for ClassificationMeasurement
134
{
NEW
135
    fn from(value: geoengine_datatypes::primitives::ClassificationMeasurement) -> Self {
×
NEW
136
        let mut classes = BTreeMap::new();
×
NEW
137
        for (k, v) in value.classes {
×
NEW
138
            classes.insert(k, v);
×
NEW
139
        }
×
NEW
140
        ClassificationMeasurement {
×
NEW
141
            r#type: Default::default(),
×
NEW
142
            measurement: value.measurement,
×
NEW
143
            classes,
×
NEW
144
        }
×
NEW
145
    }
×
146
}
147

148
impl From<ClassificationMeasurement>
149
    for geoengine_datatypes::primitives::ClassificationMeasurement
150
{
NEW
151
    fn from(value: ClassificationMeasurement) -> Self {
×
NEW
152
        geoengine_datatypes::primitives::ClassificationMeasurement {
×
NEW
153
            measurement: value.measurement,
×
NEW
154
            classes: value.classes,
×
NEW
155
        }
×
NEW
156
    }
×
157
}
158

159
/// A classification measurement.
160
/// It contains a mapping from class IDs to class names.
161
#[type_tag(value = "classification")]
162
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
163
pub struct ClassificationMeasurement {
164
    pub measurement: String,
165
    #[serde(serialize_with = "serialize_classes")]
166
    #[serde(deserialize_with = "deserialize_classes")]
167
    pub classes: BTreeMap<u8, String>,
168
}
169

NEW
170
fn serialize_classes<S>(classes: &BTreeMap<u8, String>, serializer: S) -> Result<S::Ok, S::Error>
×
NEW
171
where
×
NEW
172
    S: Serializer,
×
173
{
174
    use serde::ser::SerializeMap;
175

NEW
176
    let mut map = serializer.serialize_map(Some(classes.len()))?;
×
NEW
177
    for (k, v) in classes {
×
NEW
178
        map.serialize_entry(&k.to_string(), v)?;
×
179
    }
NEW
180
    map.end()
×
NEW
181
}
×
182

NEW
183
fn deserialize_classes<'de, D>(deserializer: D) -> Result<BTreeMap<u8, String>, D::Error>
×
NEW
184
where
×
NEW
185
    D: serde::de::Deserializer<'de>,
×
186
{
187
    use serde::de::{MapAccess, Visitor};
188
    use std::fmt;
189

190
    struct ClassesVisitor;
191

192
    impl<'de> Visitor<'de> for ClassesVisitor {
193
        type Value = BTreeMap<u8, String>;
194

NEW
195
        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
×
NEW
196
            formatter.write_str("a map with numeric string keys")
×
NEW
197
        }
×
198

NEW
199
        fn visit_map<M>(self, mut access: M) -> Result<Self::Value, M::Error>
×
NEW
200
        where
×
NEW
201
            M: MapAccess<'de>,
×
202
        {
NEW
203
            let mut map = BTreeMap::new();
×
NEW
204
            while let Some((key, value)) = access.next_entry::<String, String>()? {
×
NEW
205
                let k = key.parse::<u8>().map_err(serde::de::Error::custom)?;
×
NEW
206
                map.insert(k, value);
×
207
            }
NEW
208
            Ok(map)
×
NEW
209
        }
×
210
    }
211

NEW
212
    deserializer.deserialize_map(ClassesVisitor)
×
NEW
213
}
×
214

215
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, ToSchema)]
216
#[serde(rename_all = "camelCase")]
217
pub struct RasterBandDescriptor {
218
    pub name: String,
219
    pub measurement: Measurement,
220
}
221

222
impl From<geoengine_operators::engine::RasterBandDescriptor> for RasterBandDescriptor {
223
    fn from(value: geoengine_operators::engine::RasterBandDescriptor) -> Self {
1✔
224
        Self {
1✔
225
            name: value.name,
1✔
226
            measurement: value.measurement.into(),
1✔
227
        }
1✔
228
    }
1✔
229
}
230

231
impl From<RasterBandDescriptor> for geoengine_operators::engine::RasterBandDescriptor {
232
    fn from(value: RasterBandDescriptor) -> Self {
1✔
233
        Self {
1✔
234
            name: value.name,
1✔
235
            measurement: value.measurement.into(),
1✔
236
        }
1✔
237
    }
1✔
238
}
239

240
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
241
#[serde(rename_all = "camelCase", tag = "type")]
242
pub enum ColumnNames {
243
    #[schema(title = "Default")]
244
    Default,
245
    #[schema(title = "Suffix")]
246
    Suffix { values: Vec<String> },
247
    #[schema(title = "Names")]
248
    Names { values: Vec<String> },
249
}
250

251
impl From<geoengine_operators::processing::ColumnNames> for ColumnNames {
NEW
252
    fn from(value: geoengine_operators::processing::ColumnNames) -> Self {
×
NEW
253
        match value {
×
NEW
254
            geoengine_operators::processing::ColumnNames::Default => ColumnNames::Default,
×
NEW
255
            geoengine_operators::processing::ColumnNames::Suffix(v) => {
×
NEW
256
                ColumnNames::Suffix { values: v }
×
257
            }
NEW
258
            geoengine_operators::processing::ColumnNames::Names(v) => {
×
NEW
259
                ColumnNames::Names { values: v }
×
260
            }
261
        }
NEW
262
    }
×
263
}
264

265
impl From<ColumnNames> for geoengine_operators::processing::ColumnNames {
266
    fn from(value: ColumnNames) -> Self {
1✔
267
        match value {
1✔
NEW
268
            ColumnNames::Default => geoengine_operators::processing::ColumnNames::Default,
×
NEW
269
            ColumnNames::Suffix { values } => {
×
NEW
270
                geoengine_operators::processing::ColumnNames::Suffix(values)
×
271
            }
272
            ColumnNames::Names { values } => {
1✔
273
                geoengine_operators::processing::ColumnNames::Names(values)
1✔
274
            }
275
        }
276
    }
1✔
277
}
278

279
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
280
#[serde(rename_all = "camelCase")]
281
pub enum FeatureAggregationMethod {
282
    First,
283
    Mean,
284
}
285

286
impl From<geoengine_operators::processing::FeatureAggregationMethod> for FeatureAggregationMethod {
NEW
287
    fn from(value: geoengine_operators::processing::FeatureAggregationMethod) -> Self {
×
NEW
288
        match value {
×
289
            geoengine_operators::processing::FeatureAggregationMethod::First => {
NEW
290
                FeatureAggregationMethod::First
×
291
            }
292
            geoengine_operators::processing::FeatureAggregationMethod::Mean => {
NEW
293
                FeatureAggregationMethod::Mean
×
294
            }
295
        }
NEW
296
    }
×
297
}
298

299
impl From<FeatureAggregationMethod> for geoengine_operators::processing::FeatureAggregationMethod {
300
    fn from(value: FeatureAggregationMethod) -> Self {
1✔
301
        match value {
1✔
302
            FeatureAggregationMethod::First => {
303
                geoengine_operators::processing::FeatureAggregationMethod::First
1✔
304
            }
305
            FeatureAggregationMethod::Mean => {
NEW
306
                geoengine_operators::processing::FeatureAggregationMethod::Mean
×
307
            }
308
        }
309
    }
1✔
310
}
311

312
#[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize, ToSchema)]
313
#[serde(rename_all = "camelCase")]
314
pub enum TemporalAggregationMethod {
315
    None,
316
    First,
317
    Mean,
318
}
319

320
impl From<geoengine_operators::processing::TemporalAggregationMethod>
321
    for TemporalAggregationMethod
322
{
NEW
323
    fn from(value: geoengine_operators::processing::TemporalAggregationMethod) -> Self {
×
NEW
324
        match value {
×
325
            geoengine_operators::processing::TemporalAggregationMethod::None => {
NEW
326
                TemporalAggregationMethod::None
×
327
            }
328
            geoengine_operators::processing::TemporalAggregationMethod::First => {
NEW
329
                TemporalAggregationMethod::First
×
330
            }
331
            geoengine_operators::processing::TemporalAggregationMethod::Mean => {
NEW
332
                TemporalAggregationMethod::Mean
×
333
            }
334
        }
NEW
335
    }
×
336
}
337

338
impl From<TemporalAggregationMethod>
339
    for geoengine_operators::processing::TemporalAggregationMethod
340
{
341
    fn from(value: TemporalAggregationMethod) -> Self {
1✔
342
        match value {
1✔
343
            TemporalAggregationMethod::None => {
NEW
344
                geoengine_operators::processing::TemporalAggregationMethod::None
×
345
            }
346
            TemporalAggregationMethod::First => {
NEW
347
                geoengine_operators::processing::TemporalAggregationMethod::First
×
348
            }
349
            TemporalAggregationMethod::Mean => {
350
                geoengine_operators::processing::TemporalAggregationMethod::Mean
1✔
351
            }
352
        }
353
    }
1✔
354
}
355

356
/// Spatial bounds derivation options for the [`MockPointSource`].
357
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
358
#[serde(rename_all = "camelCase", untagged)]
359
#[schema(discriminator = "type")]
360
pub enum SpatialBoundsDerive {
361
    Derive(SpatialBoundsDeriveDerive),
362
    Bounds(SpatialBoundsDeriveBounds),
363
    None(SpatialBoundsDeriveNone),
364
}
365

366
impl Default for SpatialBoundsDerive {
NEW
367
    fn default() -> Self {
×
NEW
368
        SpatialBoundsDerive::None(SpatialBoundsDeriveNone::default())
×
NEW
369
    }
×
370
}
371

372
#[type_tag(value = "derive")]
373
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
374
pub struct SpatialBoundsDeriveDerive {}
375

376
#[type_tag(value = "bounds")]
377
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
378
pub struct SpatialBoundsDeriveBounds {
379
    #[serde(flatten)]
380
    pub bounding_box: BoundingBox2D,
381
}
382

383
#[type_tag(value = "none")]
384
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
385
pub struct SpatialBoundsDeriveNone {}
386

387
impl TryFrom<SpatialBoundsDerive> for geoengine_operators::mock::SpatialBoundsDerive {
388
    type Error = anyhow::Error;
389
    fn try_from(value: SpatialBoundsDerive) -> Result<Self, Self::Error> {
5✔
390
        Ok(match value {
5✔
391
            SpatialBoundsDerive::Derive(_) => {
392
                geoengine_operators::mock::SpatialBoundsDerive::Derive
4✔
393
            }
NEW
394
            SpatialBoundsDerive::Bounds(bounds) => {
×
395
                geoengine_operators::mock::SpatialBoundsDerive::Bounds(
NEW
396
                    bounds.bounding_box.try_into()?,
×
397
                )
398
            }
399
            SpatialBoundsDerive::None(_) => geoengine_operators::mock::SpatialBoundsDerive::None,
1✔
400
        })
401
    }
5✔
402
}
403

404
/// A bounding box that includes all border points.
405
/// Note: may degenerate to a point!
406
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
407
#[serde(rename_all = "camelCase")]
408
pub struct BoundingBox2D {
409
    lower_left_coordinate: Coordinate2D,
410
    upper_right_coordinate: Coordinate2D,
411
}
412

413
impl TryFrom<BoundingBox2D> for geoengine_datatypes::primitives::BoundingBox2D {
414
    type Error = anyhow::Error;
415
    fn try_from(value: BoundingBox2D) -> Result<Self, Self::Error> {
1✔
416
        geoengine_datatypes::primitives::BoundingBox2D::new(
1✔
417
            value.lower_left_coordinate.into(),
1✔
418
            value.upper_right_coordinate.into(),
1✔
419
        )
420
        .context("invalid bounding box")
1✔
421
    }
1✔
422
}
423

424
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
425
#[schema(no_recursion)]
426
#[serde(rename_all = "camelCase")]
427
pub struct SingleRasterSource {
428
    pub raster: RasterOperator,
429
}
430

431
impl TryFrom<SingleRasterSource> for geoengine_operators::engine::SingleRasterSource {
432
    type Error = anyhow::Error;
433

434
    fn try_from(value: SingleRasterSource) -> Result<Self, Self::Error> {
1✔
435
        Ok(Self {
436
            raster: value.raster.try_into()?,
1✔
437
        })
438
    }
1✔
439
}
440

441
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema)]
442
#[schema(no_recursion)]
443
#[serde(rename_all = "camelCase")]
444
pub struct SingleVectorMultipleRasterSources {
445
    pub vector: VectorOperator,
446
    pub rasters: Vec<RasterOperator>,
447
}
448

449
impl TryFrom<SingleVectorMultipleRasterSources>
450
    for geoengine_operators::engine::SingleVectorMultipleRasterSources
451
{
452
    type Error = anyhow::Error;
453

454
    fn try_from(value: SingleVectorMultipleRasterSources) -> Result<Self, Self::Error> {
1✔
455
        Ok(Self {
456
            vector: value.vector.try_into()?,
1✔
457
            rasters: value
1✔
458
                .rasters
1✔
459
                .into_iter()
1✔
460
                .map(std::convert::TryInto::try_into)
1✔
461
                .collect::<Result<_, _>>()?,
1✔
462
        })
463
    }
1✔
464
}
465

466
#[cfg(test)]
467
mod tests {
468
    #![allow(clippy::float_cmp)] // ok for tests
469

470
    use geoengine_datatypes::primitives::AxisAlignedRectangle;
471

472
    use super::*;
473

474
    #[test]
475
    fn it_converts_coordinates() {
1✔
476
        let dt = geoengine_datatypes::primitives::Coordinate2D { x: 1.5, y: -2.25 };
1✔
477

478
        let api: Coordinate2D = dt.into();
1✔
479
        assert_eq!(api.x, 1.5);
1✔
480
        assert_eq!(api.y, -2.25);
1✔
481

482
        let back: geoengine_datatypes::primitives::Coordinate2D = api.into();
1✔
483
        assert_eq!(back.x, 1.5);
1✔
484
        assert_eq!(back.y, -2.25);
1✔
485
    }
1✔
486

487
    #[test]
488
    fn it_converts_raster_data_types() {
1✔
489
        use geoengine_datatypes::raster::RasterDataType as Dt;
490

491
        let dt = Dt::F32;
1✔
492
        let api: RasterDataType = dt.into();
1✔
493
        assert_eq!(api, RasterDataType::F32);
1✔
494

495
        let back: geoengine_datatypes::raster::RasterDataType = api.into();
1✔
496
        assert_eq!(back, Dt::F32);
1✔
497
    }
1✔
498

499
    #[test]
500
    fn it_converts_raster_band_descriptors() {
1✔
501
        use geoengine_datatypes::primitives::Measurement;
502
        use geoengine_operators::engine::RasterBandDescriptor as OpsDesc;
503

504
        let ops = OpsDesc {
1✔
505
            name: "band 0".into(),
1✔
506
            measurement: Measurement::Unitless,
1✔
507
        };
1✔
508

509
        let api: RasterBandDescriptor = ops.clone().into();
1✔
510
        assert_eq!(api.name, "band 0");
1✔
511

512
        let back: geoengine_operators::engine::RasterBandDescriptor = api.into();
1✔
513
        assert_eq!(back, ops);
1✔
514
    }
1✔
515

516
    #[test]
517
    fn it_converts_bounding_boxes() {
1✔
518
        let api_bbox = BoundingBox2D {
1✔
519
            lower_left_coordinate: Coordinate2D { x: 1.0, y: 2.0 },
1✔
520
            upper_right_coordinate: Coordinate2D { x: 3.0, y: 4.0 },
1✔
521
        };
1✔
522

523
        let dt_bbox: geoengine_datatypes::primitives::BoundingBox2D =
1✔
524
            api_bbox.try_into().expect("it should convert");
1✔
525

526
        assert_eq!(
1✔
527
            dt_bbox.upper_left(),
1✔
528
            geoengine_datatypes::primitives::Coordinate2D { x: 1.0, y: 4.0 }
529
        );
530
        assert_eq!(
1✔
531
            dt_bbox.lower_right(),
1✔
532
            geoengine_datatypes::primitives::Coordinate2D { x: 3.0, y: 2.0 }
533
        );
534
    }
1✔
535
}
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