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

geo-engine / geoengine / 28518206162

01 Jul 2026 12:40PM UTC coverage: 87.774% (+0.1%) from 87.639%
28518206162

push

github

web-flow
feat: add OGC API Tiles (#1197)

* feat: implement OGC API endpoints and integrate with existing services

* feat: add OGC API conformance endpoint and support fixed session ID for testing

* feat: implement OGC API collections endpoint and error handling

* feat: add OGC API Tile Matrix Set endpoints and error handling

* feat: tiles impl

* refactor: generated code

* fix: tests

* first version that runs with openlayers

* works for wgs84 and original resolution

* tests for web mercator

* feat: Enhance OGC API Tile Matrix Set Implementation

- Refactor tile matrix set handling to include ordered axes based on spatial reference.
- Introduce functions to calculate the number of zoom levels and tiles at specific zoom levels.
- Update tests to validate new functionality and ensure correct tile matrix definitions for EPSG:4326 and EPSG:3857.
- Add helper function to add file definitions to datasets and return layer IDs.
- Introduce NDVI symbology and colorizer functions for raster layers.
- Update NDVI dataset definition with corrected spatial grid bounds and pixel sizes.
- Add example OpenLayers implementation for displaying OGC API Map Tiles.
- Include new test data for NDVI tiles in both EPSG:4326 and EPSG:3857.

* fix: after-merge fixes

* fix: update openapi spec

* fix: duplicate enum

* fix: /ogc/ogc path

* fix: update test assertions for data usage and WMS requests

* fix: enhance meters_per_unit and uses_meters methods for harcoded epsgs

* feat: enhance OGC API error handling and add time interval checks

* feat: implement TMS blob URL handling and enhance tile loading for OGC layers

* feat: update tile endpoint and enhance map configurations for WGS84 and Web Mercator

* feat: refactor session ID handling and improve OGC API documentation

* feat: add OGC API Map Tile Layer component and integrate into main viewer

* docs: remove redundant comment about fixed session ID in Postgres user DB

* feat: enhance tile loading and configuration f... (continued)

1992 of 2132 new or added lines in 14 files covered. (93.43%)

121666 of 138613 relevant lines covered (87.77%)

472247.01 hits per line

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

90.95
/geoengine/datatypes/src/spatial_reference.rs
1
use crate::{
2
    error::{self, BoxedResultExt},
3
    operations::reproject::{CoordinateProjection, CoordinateProjector, Reproject},
4
    primitives::AxisAlignedRectangle,
5
    util::Result,
6
};
7
use gdal::spatial_ref::SpatialRef;
8

9
use postgres_types::private::BytesMut;
10

11
use postgres_types::{FromSql, IsNull, ToSql, Type};
12
use proj::Proj;
13
use proj_sys::{
14
    proj_context_create, proj_context_destroy, proj_create, proj_destroy,
15
    proj_ellipsoid_get_parameters, proj_get_ellipsoid,
16
};
17
use serde::de::Visitor;
18
use serde::{Deserialize, Deserializer, Serialize, Serializer};
19
use std::ffi::CString;
20

21
use snafu::Error;
22
use snafu::ResultExt;
23
use std::str::FromStr;
24
use std::{convert::TryFrom, fmt::Formatter};
25

26
/// A spatial reference authority that is part of a spatial reference definition
27
#[derive(
28
    Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, ToSql, FromSql,
×
29
)]
30
#[serde(rename_all = "SCREAMING-KEBAB-CASE")]
31
pub enum SpatialReferenceAuthority {
32
    Epsg,
33
    SrOrg,
34
    Iau2000,
35
    Esri,
36
}
37

38
impl std::fmt::Display for SpatialReferenceAuthority {
39
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2,735✔
40
        write!(
2,735✔
41
            f,
2,735✔
42
            "{}",
43
            match self {
2,735✔
44
                SpatialReferenceAuthority::Epsg => "EPSG",
2,724✔
45
                SpatialReferenceAuthority::SrOrg => "SR-ORG",
3✔
46
                SpatialReferenceAuthority::Iau2000 => "IAU2000",
4✔
47
                SpatialReferenceAuthority::Esri => "ESRI",
4✔
48
            }
49
        )
50
    }
2,735✔
51
}
52

53
/// A spatial reference consists of an authority and a code
54
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, ToSql, FromSql)]
×
55
pub struct SpatialReference {
56
    authority: SpatialReferenceAuthority,
57
    code: u32,
58
}
59

60
impl SpatialReference {
61
    pub fn new(authority: SpatialReferenceAuthority, code: u32) -> Self {
2,120✔
62
        Self { authority, code }
2,120✔
63
    }
2,120✔
64

65
    pub fn authority(&self) -> &SpatialReferenceAuthority {
103✔
66
        &self.authority
103✔
67
    }
103✔
68

69
    pub fn code(self) -> u32 {
103✔
70
        self.code
103✔
71
    }
103✔
72

73
    /// the WGS 84 spatial reference system
74
    pub fn epsg_4326() -> Self {
728✔
75
        Self::new(SpatialReferenceAuthority::Epsg, 4326)
728✔
76
    }
728✔
77

78
    pub fn web_mercator() -> Self {
6✔
79
        Self::new(SpatialReferenceAuthority::Epsg, 3857)
6✔
80
    }
6✔
81

82
    pub fn proj_string(self) -> Result<String> {
1,723✔
83
        match self.authority {
2✔
84
            SpatialReferenceAuthority::Epsg | SpatialReferenceAuthority::Iau2000 | SpatialReferenceAuthority::Esri => {
85
                Ok(format!("{}:{}", self.authority, self.code))
1,721✔
86
            }
87
            // poor-mans integration of Meteosat Second Generation 
88
            SpatialReferenceAuthority::SrOrg if self.code == 81 => Ok("+proj=geos +lon_0=0 +h=35785831 +x_0=0 +y_0=0 +ellps=WGS84 +units=m +no_defs +type=crs".to_owned()),
2✔
89
            SpatialReferenceAuthority::SrOrg => {
90
                Err(error::Error::ProjStringUnresolvable { spatial_ref: self })
1✔
91
                //TODO: we might need to look them up somehow! Best solution would be a registry where we can store user definexd srs strings.
92
            }
93
        }
94
    }
1,723✔
95

96
    /// Return the area of use in EPSG:4326 projection
97
    pub fn area_of_use<A: AxisAlignedRectangle>(self) -> Result<A> {
134✔
98
        let proj_string = self.proj_string()?;
134✔
99

100
        let proj = Proj::new(&proj_string).map_err(|_| error::Error::InvalidProjDefinition {
134✔
101
            proj_definition: proj_string.clone(),
×
102
        })?;
×
103
        let area = proj
134✔
104
            .area_of_use()
134✔
105
            .context(error::ProjInternal)?
134✔
106
            .0
107
            .ok_or(error::Error::NoAreaOfUseDefined { proj_string })?;
134✔
108
        A::from_min_max(
134✔
109
            (area.west, area.south).into(),
134✔
110
            (area.east, area.north).into(),
134✔
111
        )
112
    }
134✔
113

114
    /// Return the area of use in current projection
115
    pub fn area_of_use_projected<A: AxisAlignedRectangle>(self) -> Result<A> {
34✔
116
        if self == Self::epsg_4326() {
34✔
117
            return self.area_of_use();
29✔
118
        }
5✔
119
        let p = CoordinateProjector::from_known_srs(Self::epsg_4326(), self)?;
5✔
120
        self.area_of_use::<A>()?.reproject(&p)
5✔
121
    }
34✔
122

123
    /// Return the srs-string "authority:code"
124
    #[allow(clippy::trivially_copy_pass_by_ref)]
125
    pub fn srs_string(&self) -> String {
30✔
126
        format!("{}:{}", self.authority, self.code)
30✔
127
    }
30✔
128

129
    /// Compute the bounding box of this spatial reference that is also valid in the `other` spatial reference. Might be None.
130
    #[allow(clippy::trivially_copy_pass_by_ref)]
131
    pub fn area_of_use_intersection<T>(&self, other: &SpatialReference) -> Result<Option<T>>
26✔
132
    where
26✔
133
        T: AxisAlignedRectangle,
26✔
134
    {
135
        // generate a projector which transforms wgs84 into the projection we want to produce.
136
        let valid_bounds_proj =
26✔
137
            CoordinateProjector::from_known_srs(SpatialReference::epsg_4326(), *self)?;
26✔
138

139
        // transform the bounds of the input srs (coordinates are in wgs84) into the output projection.
140
        // TODO check if  there is a better / smarter way to check if the coordinates are valid.
141
        let area_out = self.area_of_use::<T>()?;
26✔
142
        let area_other = other.area_of_use::<T>()?;
26✔
143

144
        area_out
26✔
145
            .intersection(&area_other)
26✔
146
            .map(|x| x.reproject(&valid_bounds_proj))
26✔
147
            .transpose()
26✔
148
    }
26✔
149

150
    /// Computes the equatorial radius of the ellipsoid associated with this spatial reference in meters.
151
    /// This is a helper function for calculating the meters per unit for projections that use degrees as their unit of measurement.
152
    pub fn meters_per_unit(self) -> Result<f64> {
5✔
153
        // hardcode some projections that are commonly
154
        match (self.authority, self.code) {
5✔
155
            (SpatialReferenceAuthority::Epsg, 4326) => return Ok(111_319.490_8), // WGS 84
2✔
156
            (SpatialReferenceAuthority::Epsg, 3857 /* Web Mercator */ |  25832 /* ETRS89 / UTM zone 32N */)  => {
157
                return Ok(1.0);
2✔
158
            },
159
            _ => {}
1✔
160
        }
161

162
        if self.uses_meters()? {
1✔
NEW
163
            return Ok(1.0);
×
164
        }
1✔
165

166
        let proj_string = CString::new(self.proj_string()?).boxed_context(error::ProjInternal2)?;
1✔
167
        let mut meters_per_degree = None;
1✔
168

169
        unsafe {
170
            // 1. Initialize the PROJ context and instantiate the CRS
171
            let ctx = proj_context_create();
1✔
172
            let crs = proj_create(ctx, proj_string.as_ptr());
1✔
173

174
            if crs.is_null() {
1✔
NEW
175
                proj_context_destroy(ctx);
×
NEW
176
                return Err(error::Error::ProjStringUnresolvable { spatial_ref: self });
×
177
            }
1✔
178

179
            // 2. Fetch the underlying ellipsoid object from the CRS
180
            let ellipsoid = proj_get_ellipsoid(ctx, crs);
1✔
181

182
            if ellipsoid.is_null() {
1✔
NEW
183
                proj_destroy(crs);
×
NEW
184
                proj_context_destroy(ctx);
×
NEW
185
                return Err(error::Error::ProjStringUnresolvable { spatial_ref: self });
×
186
            }
1✔
187

188
            let mut semi_major: f64 = 0.0;
1✔
189
            let mut semi_minor: f64 = 0.0;
1✔
190
            let mut is_semi_minor_computed: i32 = 0;
1✔
191
            let mut inv_flattening: f64 = 0.0;
1✔
192

193
            // 3. Extract the semi-major axis (Equatorial Radius)
194
            let success = proj_ellipsoid_get_parameters(
1✔
195
                ctx,
1✔
196
                ellipsoid,
1✔
197
                &raw mut semi_major,
1✔
198
                &raw mut semi_minor,
1✔
199
                &raw mut is_semi_minor_computed,
1✔
200
                &raw mut inv_flattening,
1✔
201
            );
202

203
            if success == 1 {
1✔
204
                // 4. Calculate the Equatorial Perimeter divided by 360 degrees
1✔
205
                // WGS84 Semi-major axis (semi_major) = 6378137.0 meters
1✔
206
                let equatorial_perimeter = semi_major * 2.0 * std::f64::consts::PI;
1✔
207
                meters_per_degree = Some(equatorial_perimeter / 360.0);
1✔
208
            }
1✔
209

210
            // Clean up the main context, CRS and ellipsoid structures
211
            proj_destroy(ellipsoid);
1✔
212
            proj_destroy(crs);
1✔
213
            proj_context_destroy(ctx);
1✔
214
        }
215

216
        if let Some(meters) = meters_per_degree {
1✔
217
            Ok(meters)
1✔
218
        } else {
NEW
219
            Err(error::Error::ProjStringUnresolvable { spatial_ref: self })
×
220
        }
221
    }
5✔
222

223
    /// Checks if the spatial reference uses meters as its unit of measurement.
224
    /// This is a heuristic check and may not be 100% accurate for all projections.
225
    pub fn uses_meters(self) -> Result<bool> {
5✔
226
        let proj_string = self.proj_string()?;
5✔
227

228
        if proj_string.contains("+units=m") {
5✔
NEW
229
            return Ok(true);
×
230
        }
5✔
231

232
        let proj = Proj::new_known_crs("EPSG:4326", &proj_string, None).map_err(|_| {
5✔
NEW
233
            error::Error::InvalidProjDefinition {
×
NEW
234
                proj_definition: proj_string.clone(),
×
NEW
235
            }
×
NEW
236
        })?;
×
237

238
        // Using 500,000 Easting (UTM Center) and 100,000 Northing (just North of the Equator/Origin)
239
        let (Ok(coord0), Ok(coord1)) = (
5✔
240
            proj.project((500_000.0, 100_000.0), true),
5✔
241
            proj.project((500_001.0, 100_000.0), true),
5✔
242
        ) else {
243
            // If the projection cannot handle these coordinates, it's likely not in meters
NEW
244
            return Ok(false);
×
245
        };
246

247
        // If it handles meters, moving 1 meter changes the output degrees by a microscopic amount
248
        let delta = f64::abs(coord1.0 - coord0.0);
5✔
249
        Ok(delta < 0.1)
5✔
250
    }
5✔
251
}
252

253
impl std::fmt::Display for SpatialReference {
254
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
976✔
255
        write!(f, "{}:{}", self.authority, self.code)
976✔
256
    }
976✔
257
}
258

259
impl Serialize for SpatialReference {
260
    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
45✔
261
    where
45✔
262
        S: Serializer,
45✔
263
    {
264
        serializer.serialize_str(&self.to_string())
45✔
265
    }
45✔
266
}
267

268
/// Helper struct for deserializing a `SpatialReferencce`
269
struct SpatialReferenceDeserializeVisitor;
270

271
impl Visitor<'_> for SpatialReferenceDeserializeVisitor {
272
    type Value = SpatialReference;
273

274
    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
×
275
        formatter.write_str("a spatial reference in the form authority:code")
×
276
    }
×
277

278
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
25✔
279
    where
25✔
280
        E: serde::de::Error,
25✔
281
    {
282
        v.parse().map_err(serde::de::Error::custom)
25✔
283
    }
25✔
284
}
285

286
impl<'de> Deserialize<'de> for SpatialReference {
287
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
25✔
288
    where
25✔
289
        D: Deserializer<'de>,
25✔
290
    {
291
        deserializer.deserialize_str(SpatialReferenceDeserializeVisitor)
25✔
292
    }
25✔
293
}
294

295
impl FromStr for SpatialReferenceAuthority {
296
    type Err = error::Error;
297

298
    fn from_str(s: &str) -> Result<Self, Self::Err> {
1,211✔
299
        Ok(match s {
1,211✔
300
            "EPSG" => SpatialReferenceAuthority::Epsg,
1,211✔
301
            "SR-ORG" => SpatialReferenceAuthority::SrOrg,
202✔
302
            "IAU2000" => SpatialReferenceAuthority::Iau2000,
200✔
303
            "ESRI" => SpatialReferenceAuthority::Esri,
199✔
304
            _ => {
305
                return Err(error::Error::InvalidSpatialReferenceString {
2✔
306
                    spatial_reference_string: s.into(),
2✔
307
                });
2✔
308
            }
309
        })
310
    }
1,211✔
311
}
312

313
impl FromStr for SpatialReference {
314
    type Err = error::Error;
315

316
    fn from_str(s: &str) -> Result<Self, Self::Err> {
133✔
317
        let mut split = s.split(':');
133✔
318

319
        match (split.next(), split.next(), split.next()) {
133✔
320
            (Some(authority), Some(code), None) => Ok(Self::new(
133✔
321
                authority.parse()?,
133✔
322
                code.parse::<u32>().context(error::ParseU32)?,
131✔
323
            )),
324
            _ => Err(error::Error::InvalidSpatialReferenceString {
×
325
                spatial_reference_string: s.into(),
×
326
            }),
×
327
        }
328
    }
133✔
329
}
330

331
impl TryFrom<SpatialRef> for SpatialReference {
332
    type Error = error::Error;
333

334
    fn try_from(value: SpatialRef) -> Result<Self, Self::Error> {
1,198✔
335
        let auth_name = value.auth_name().map_or_else(|| value.authority(), Ok)?;
1,198✔
336
        Ok(SpatialReference::new(
1,078✔
337
            SpatialReferenceAuthority::from_str(&auth_name)?,
1,078✔
338
            value.auth_code()? as u32,
1,078✔
339
        ))
340
    }
1,198✔
341
}
342

343
impl TryFrom<SpatialReference> for SpatialRef {
344
    type Error = error::Error;
345

346
    fn try_from(value: SpatialReference) -> Result<Self, Self::Error> {
37✔
347
        if value.authority == SpatialReferenceAuthority::Epsg {
37✔
348
            return SpatialRef::from_epsg(value.code).context(error::Gdal);
37✔
349
        }
×
350

351
        // TODO: support other projections reliably
352

353
        SpatialRef::from_proj4(&value.proj_string()?).context(error::Gdal)
×
354
    }
37✔
355
}
356

357
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
358
pub enum SpatialReferenceOption {
359
    SpatialReference(SpatialReference),
360
    Unreferenced,
361
}
362

363
impl SpatialReferenceOption {
364
    pub fn is_spatial_ref(self) -> bool {
6✔
365
        match self {
6✔
366
            SpatialReferenceOption::SpatialReference(_) => true,
4✔
367
            SpatialReferenceOption::Unreferenced => false,
2✔
368
        }
369
    }
6✔
370

371
    pub fn is_unreferenced(self) -> bool {
4✔
372
        !self.is_spatial_ref()
4✔
373
    }
4✔
374

375
    pub fn as_option(self) -> Option<SpatialReference> {
30✔
376
        self.into()
30✔
377
    }
30✔
378
}
379

380
impl ToSql for SpatialReferenceOption {
381
    fn to_sql(&self, ty: &Type, out: &mut BytesMut) -> Result<IsNull, Box<dyn Error + Sync + Send>>
521✔
382
    where
521✔
383
        Self: Sized,
521✔
384
    {
385
        match self {
521✔
386
            SpatialReferenceOption::SpatialReference(sref) => sref.to_sql(ty, out),
415✔
387
            SpatialReferenceOption::Unreferenced => Ok(IsNull::Yes),
106✔
388
        }
389
    }
521✔
390

391
    fn accepts(ty: &Type) -> bool
247✔
392
    where
247✔
393
        Self: Sized,
247✔
394
    {
395
        <SpatialReference as ToSql>::accepts(ty)
247✔
396
    }
247✔
397

398
    fn to_sql_checked(
×
399
        &self,
×
400
        ty: &Type,
×
401
        out: &mut BytesMut,
×
402
    ) -> Result<IsNull, Box<dyn Error + Sync + Send>> {
×
403
        match self {
×
404
            SpatialReferenceOption::SpatialReference(sref) => sref.to_sql_checked(ty, out),
×
405
            SpatialReferenceOption::Unreferenced => Ok(IsNull::Yes),
×
406
        }
407
    }
×
408
}
409

410
impl<'a> FromSql<'a> for SpatialReferenceOption {
411
    fn from_sql(ty: &Type, raw: &'a [u8]) -> Result<Self, Box<dyn Error + Sync + Send>> {
159✔
412
        Ok(SpatialReferenceOption::SpatialReference(
413
            SpatialReference::from_sql(ty, raw)?,
159✔
414
        ))
415
    }
159✔
416

417
    fn from_sql_null(_: &Type) -> Result<Self, Box<dyn Error + Sync + Send>> {
53✔
418
        Ok(SpatialReferenceOption::Unreferenced)
53✔
419
    }
53✔
420

421
    fn accepts(ty: &Type) -> bool {
2,611✔
422
        <SpatialReference as FromSql>::accepts(ty)
2,611✔
423
    }
2,611✔
424
}
425

426
impl std::fmt::Display for SpatialReferenceOption {
427
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
642✔
428
        match self {
642✔
429
            SpatialReferenceOption::SpatialReference(p) => write!(f, "{p}"),
636✔
430
            SpatialReferenceOption::Unreferenced => Ok(()),
6✔
431
        }
432
    }
642✔
433
}
434

435
impl From<SpatialReference> for SpatialReferenceOption {
436
    fn from(spatial_reference: SpatialReference) -> Self {
1,121✔
437
        Self::SpatialReference(spatial_reference)
1,121✔
438
    }
1,121✔
439
}
440

441
impl From<Option<SpatialReference>> for SpatialReferenceOption {
442
    fn from(option: Option<SpatialReference>) -> Self {
5✔
443
        match option {
5✔
444
            Some(p) => SpatialReferenceOption::SpatialReference(p),
4✔
445
            None => SpatialReferenceOption::Unreferenced,
1✔
446
        }
447
    }
5✔
448
}
449

450
impl Serialize for SpatialReferenceOption {
451
    fn serialize<S>(&self, serializer: S) -> Result<<S as Serializer>::Ok, <S as Serializer>::Error>
634✔
452
    where
634✔
453
        S: Serializer,
634✔
454
    {
455
        serializer.serialize_str(&self.to_string())
634✔
456
    }
634✔
457
}
458

459
impl From<SpatialReferenceOption> for Option<SpatialReference> {
460
    fn from(s_ref: SpatialReferenceOption) -> Self {
72✔
461
        match s_ref {
72✔
462
            SpatialReferenceOption::SpatialReference(s) => Some(s),
71✔
463
            SpatialReferenceOption::Unreferenced => None,
1✔
464
        }
465
    }
72✔
466
}
467

468
/// Helper struct for deserializing a `SpatialReferenceOption`
469
struct SpatialReferenceOptionDeserializeVisitor;
470

471
impl Visitor<'_> for SpatialReferenceOptionDeserializeVisitor {
472
    type Value = SpatialReferenceOption;
473

474
    fn expecting(&self, formatter: &mut Formatter) -> std::fmt::Result {
×
475
        formatter.write_str("a spatial reference in the form authority:code")
×
476
    }
×
477

478
    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
52✔
479
    where
52✔
480
        E: serde::de::Error,
52✔
481
    {
482
        if v.is_empty() {
52✔
483
            return Ok(SpatialReferenceOption::Unreferenced);
7✔
484
        }
45✔
485

486
        let spatial_reference: SpatialReference = v.parse().map_err(serde::de::Error::custom)?;
45✔
487

488
        Ok(spatial_reference.into())
44✔
489
    }
52✔
490
}
491

492
impl<'de> Deserialize<'de> for SpatialReferenceOption {
493
    fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
52✔
494
    where
52✔
495
        D: Deserializer<'de>,
52✔
496
    {
497
        deserializer.deserialize_str(SpatialReferenceOptionDeserializeVisitor)
52✔
498
    }
52✔
499
}
500

501
#[cfg(test)]
502
mod tests {
503
    use super::*;
504
    use core::f64;
505
    use std::convert::TryInto;
506

507
    #[test]
508
    fn display() {
1✔
509
        assert_eq!(SpatialReferenceAuthority::Epsg.to_string(), "EPSG");
1✔
510
        assert_eq!(SpatialReferenceAuthority::SrOrg.to_string(), "SR-ORG");
1✔
511
        assert_eq!(SpatialReferenceAuthority::Iau2000.to_string(), "IAU2000");
1✔
512
        assert_eq!(SpatialReferenceAuthority::Esri.to_string(), "ESRI");
1✔
513

514
        assert_eq!(
1✔
515
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 4326).to_string(),
1✔
516
            "EPSG:4326"
517
        );
518
        assert_eq!(
1✔
519
            SpatialReference::new(SpatialReferenceAuthority::SrOrg, 1).to_string(),
1✔
520
            "SR-ORG:1"
521
        );
522
        assert_eq!(
1✔
523
            SpatialReference::new(SpatialReferenceAuthority::Iau2000, 4711).to_string(),
1✔
524
            "IAU2000:4711"
525
        );
526
        assert_eq!(
1✔
527
            SpatialReference::new(SpatialReferenceAuthority::Esri, 42).to_string(),
1✔
528
            "ESRI:42"
529
        );
530
    }
1✔
531

532
    #[test]
533
    fn serialize_json() {
1✔
534
        assert_eq!(
1✔
535
            serde_json::to_string(&SpatialReference::new(
1✔
536
                SpatialReferenceAuthority::Epsg,
1✔
537
                4326
1✔
538
            ))
1✔
539
            .unwrap(),
1✔
540
            "\"EPSG:4326\""
541
        );
542
        assert_eq!(
1✔
543
            serde_json::to_string(&SpatialReference::new(SpatialReferenceAuthority::SrOrg, 1))
1✔
544
                .unwrap(),
1✔
545
            "\"SR-ORG:1\""
546
        );
547
        assert_eq!(
1✔
548
            serde_json::to_string(&SpatialReference::new(
1✔
549
                SpatialReferenceAuthority::Iau2000,
1✔
550
                4711
1✔
551
            ))
1✔
552
            .unwrap(),
1✔
553
            "\"IAU2000:4711\""
554
        );
555
        assert_eq!(
1✔
556
            serde_json::to_string(&SpatialReference::new(SpatialReferenceAuthority::Esri, 42))
1✔
557
                .unwrap(),
1✔
558
            "\"ESRI:42\""
559
        );
560
    }
1✔
561

562
    #[test]
563
    fn deserialize_json() {
1✔
564
        assert_eq!(
1✔
565
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 4326),
1✔
566
            serde_json::from_str("\"EPSG:4326\"").unwrap()
1✔
567
        );
568
        assert_eq!(
1✔
569
            SpatialReference::new(SpatialReferenceAuthority::SrOrg, 1),
1✔
570
            serde_json::from_str("\"SR-ORG:1\"").unwrap()
1✔
571
        );
572
        assert_eq!(
1✔
573
            SpatialReference::new(SpatialReferenceAuthority::Iau2000, 4711),
1✔
574
            serde_json::from_str("\"IAU2000:4711\"").unwrap()
1✔
575
        );
576
        assert_eq!(
1✔
577
            SpatialReference::new(SpatialReferenceAuthority::Esri, 42),
1✔
578
            serde_json::from_str("\"ESRI:42\"").unwrap()
1✔
579
        );
580

581
        assert!(serde_json::from_str::<SpatialReference>("\"foo:bar\"").is_err());
1✔
582
    }
1✔
583

584
    #[test]
585
    fn spatial_reference_option_serde() {
1✔
586
        assert_eq!(
1✔
587
            serde_json::to_string(&SpatialReferenceOption::SpatialReference(
1✔
588
                SpatialReference::new(SpatialReferenceAuthority::Epsg, 4326)
1✔
589
            ))
1✔
590
            .unwrap(),
1✔
591
            "\"EPSG:4326\""
592
        );
593

594
        assert_eq!(
1✔
595
            serde_json::to_string(&SpatialReferenceOption::Unreferenced).unwrap(),
1✔
596
            "\"\""
597
        );
598

599
        assert_eq!(
1✔
600
            SpatialReferenceOption::SpatialReference(SpatialReference::new(
1✔
601
                SpatialReferenceAuthority::Epsg,
1✔
602
                4326
1✔
603
            )),
1✔
604
            serde_json::from_str("\"EPSG:4326\"").unwrap()
1✔
605
        );
606

607
        assert_eq!(
1✔
608
            SpatialReferenceOption::Unreferenced,
609
            serde_json::from_str("\"\"").unwrap()
1✔
610
        );
611

612
        assert!(serde_json::from_str::<SpatialReferenceOption>("\"foo:bar\"").is_err());
1✔
613
    }
1✔
614

615
    #[test]
616
    fn is_spatial_ref() {
1✔
617
        let s_ref = SpatialReferenceOption::from(SpatialReference::epsg_4326());
1✔
618
        assert!(s_ref.is_spatial_ref());
1✔
619
        assert!(!s_ref.is_unreferenced());
1✔
620
    }
1✔
621

622
    #[test]
623
    fn is_unreferenced() {
1✔
624
        let s_ref = SpatialReferenceOption::Unreferenced;
1✔
625
        assert!(s_ref.is_unreferenced());
1✔
626
        assert!(!s_ref.is_spatial_ref());
1✔
627
    }
1✔
628

629
    #[test]
630
    fn from_option_some() {
1✔
631
        let s_ref: SpatialReferenceOption = Some(SpatialReference::epsg_4326()).into();
1✔
632
        assert_eq!(
1✔
633
            s_ref,
634
            SpatialReferenceOption::SpatialReference(SpatialReference::epsg_4326())
1✔
635
        );
636
    }
1✔
637

638
    #[test]
639
    fn from_option_none() {
1✔
640
        let s_ref: SpatialReferenceOption = None.into();
1✔
641
        assert_eq!(s_ref, SpatialReferenceOption::Unreferenced);
1✔
642
    }
1✔
643

644
    #[test]
645
    fn into_option_some() {
1✔
646
        let s_ref: Option<SpatialReference> =
1✔
647
            SpatialReferenceOption::SpatialReference(SpatialReference::epsg_4326()).into();
1✔
648
        assert_eq!(s_ref, Some(SpatialReference::epsg_4326()));
1✔
649
    }
1✔
650

651
    #[test]
652
    fn into_option_none() {
1✔
653
        let s_ref: Option<SpatialReference> = SpatialReferenceOption::Unreferenced.into();
1✔
654
        assert_eq!(s_ref, None);
1✔
655
    }
1✔
656

657
    #[test]
658
    fn proj_string() {
1✔
659
        assert_eq!(
1✔
660
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 4326)
1✔
661
                .proj_string()
1✔
662
                .unwrap(),
1✔
663
            "EPSG:4326"
664
        );
665
        assert_eq!(
1✔
666
            SpatialReference::new(SpatialReferenceAuthority::SrOrg, 81)
1✔
667
                .proj_string()
1✔
668
                .unwrap(),
1✔
669
            "+proj=geos +lon_0=0 +h=35785831 +x_0=0 +y_0=0 +ellps=WGS84 +units=m +no_defs +type=crs"
670
        );
671
        assert_eq!(
1✔
672
            SpatialReference::new(SpatialReferenceAuthority::Iau2000, 4711)
1✔
673
                .proj_string()
1✔
674
                .unwrap(),
1✔
675
            "IAU2000:4711"
676
        );
677
        assert_eq!(
1✔
678
            SpatialReference::new(SpatialReferenceAuthority::Esri, 42)
1✔
679
                .proj_string()
1✔
680
                .unwrap(),
1✔
681
            "ESRI:42"
682
        );
683
        assert!(
1✔
684
            SpatialReference::new(SpatialReferenceAuthority::SrOrg, 1)
1✔
685
                .proj_string()
1✔
686
                .is_err()
1✔
687
        );
688
    }
1✔
689

690
    #[test]
691
    fn spatial_reference_to_gdal_spatial_ref_epsg() {
1✔
692
        let spatial_reference = SpatialReference::epsg_4326();
1✔
693
        let gdal_sref: SpatialRef = spatial_reference.try_into().unwrap();
1✔
694

695
        assert_eq!(gdal_sref.auth_name().unwrap(), "EPSG");
1✔
696
        assert_eq!(gdal_sref.auth_code().unwrap(), 4326);
1✔
697
    }
1✔
698

699
    #[test]
700
    fn it_knows_if_it_is_in_meters() {
1✔
701
        for (epsg, should_be_in_meters) in &[
4✔
702
            (4326, false), // WGS 84
1✔
703
            (3857, true),  // Web Mercator
1✔
704
            (25832, true), // ETRS89 / UTM zone 32N
1✔
705
            (4258, false), // ETRS89
1✔
706
        ] {
1✔
707
            let spatial_ref = SpatialReference::new(SpatialReferenceAuthority::Epsg, *epsg);
4✔
708
            assert_eq!(
4✔
709
                spatial_ref.uses_meters().unwrap(),
4✔
710
                *should_be_in_meters,
711
                "EPSG:{epsg} should be in meters: {should_be_in_meters}",
712
            );
713
        }
714
    }
1✔
715

716
    #[test]
717
    fn it_calculates_the_perimeter_in_meters() {
1✔
718
        use float_cmp::assert_approx_eq;
719

720
        let wgs84 = SpatialReference::new(SpatialReferenceAuthority::Epsg, 4326);
1✔
721
        let web_mercator = SpatialReference::new(SpatialReferenceAuthority::Epsg, 3857);
1✔
722

723
        // cf. <https://docs.ogc.org/is/17-083r4/17-083r4.html#6-1-1-1-%C2%A0-tile-matrix-in-a-two-dimensional-space>
724
        assert_approx_eq!(
1✔
725
            f64,
726
            wgs84.meters_per_unit().unwrap(),
1✔
727
            111_319.490_8,
728
            epsilon = 0.000_1
729
        );
730

731
        assert_approx_eq!(f64, web_mercator.meters_per_unit().unwrap(), 1.0);
1✔
732

733
        assert_approx_eq!(
1✔
734
            f64,
735
            SpatialReference::new(SpatialReferenceAuthority::Epsg, 4258) // ETRS89
1✔
736
                .meters_per_unit()
1✔
737
                .unwrap(),
1✔
738
            111_319.490_8,
739
            epsilon = 0.000_1
740
        );
741
    }
1✔
742
}
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