Skip to main content

proj_core/
crs.rs

1use crate::datum::Datum;
2use crate::error::{Error, Result};
3
4/// A coordinate system's projected linear unit.
5///
6/// The stored value is the conversion factor from one native unit to meters.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct LinearUnit {
9    meters_per_unit: f64,
10}
11
12impl LinearUnit {
13    /// Metre-based projected coordinates.
14    pub const fn metre() -> Self {
15        Self {
16            meters_per_unit: 1.0,
17        }
18    }
19
20    /// Alias for [`LinearUnit::metre`].
21    pub const fn meter() -> Self {
22        Self::metre()
23    }
24
25    /// Kilometer-based projected coordinates.
26    pub const fn kilometre() -> Self {
27        Self {
28            meters_per_unit: 1000.0,
29        }
30    }
31
32    /// Alias for [`LinearUnit::kilometre`].
33    pub const fn kilometer() -> Self {
34        Self::kilometre()
35    }
36
37    /// International foot-based projected coordinates.
38    pub const fn foot() -> Self {
39        Self {
40            meters_per_unit: 0.3048,
41        }
42    }
43
44    /// US survey foot-based projected coordinates.
45    pub const fn us_survey_foot() -> Self {
46        Self {
47            meters_per_unit: 0.3048006096012192,
48        }
49    }
50
51    /// Construct a custom projected linear unit from its meter conversion factor.
52    pub fn from_meters_per_unit(meters_per_unit: f64) -> Result<Self> {
53        if !meters_per_unit.is_finite() || meters_per_unit <= 0.0 {
54            return Err(Error::InvalidDefinition(
55                "linear unit conversion factor must be a finite positive number".into(),
56            ));
57        }
58
59        Ok(Self { meters_per_unit })
60    }
61
62    /// Return the number of meters represented by one native projected unit.
63    pub const fn meters_per_unit(self) -> f64 {
64        self.meters_per_unit
65    }
66
67    /// Convert a native projected coordinate value into meters.
68    pub const fn to_meters(self, value: f64) -> f64 {
69        value * self.meters_per_unit
70    }
71
72    /// Convert a meter value into the native projected unit.
73    pub const fn from_meters(self, value: f64) -> f64 {
74        value / self.meters_per_unit
75    }
76}
77
78/// A Coordinate Reference System definition.
79#[derive(Debug, Clone, Copy)]
80pub enum CrsDef {
81    /// Geographic CRS (lon/lat in degrees).
82    Geographic(GeographicCrsDef),
83    /// Projected CRS (easting/northing in the CRS's native linear unit).
84    Projected(ProjectedCrsDef),
85}
86
87impl CrsDef {
88    /// Get the datum for this CRS.
89    pub fn datum(&self) -> &Datum {
90        match self {
91            CrsDef::Geographic(g) => g.datum(),
92            CrsDef::Projected(p) => p.datum(),
93        }
94    }
95
96    /// Get the EPSG code for this CRS.
97    pub fn epsg(&self) -> u32 {
98        match self {
99            CrsDef::Geographic(g) => g.epsg(),
100            CrsDef::Projected(p) => p.epsg(),
101        }
102    }
103
104    /// Get the CRS name.
105    pub fn name(&self) -> &str {
106        match self {
107            CrsDef::Geographic(g) => g.name(),
108            CrsDef::Projected(p) => p.name(),
109        }
110    }
111
112    /// Returns true if this is a geographic CRS.
113    pub fn is_geographic(&self) -> bool {
114        matches!(self, CrsDef::Geographic(_))
115    }
116
117    /// Returns true if this is a projected CRS.
118    pub fn is_projected(&self) -> bool {
119        matches!(self, CrsDef::Projected(_))
120    }
121
122    /// Returns the geographic CRS EPSG code used for operation selection, when known.
123    pub fn base_geographic_crs_epsg(&self) -> Option<u32> {
124        match self {
125            CrsDef::Geographic(g) if g.epsg() != 0 => Some(g.epsg()),
126            CrsDef::Projected(p) if p.base_geographic_crs_epsg() != 0 => {
127                Some(p.base_geographic_crs_epsg())
128            }
129            _ => None,
130        }
131    }
132
133    /// Returns true when two CRS definitions map to the same internal semantics.
134    pub fn semantically_equivalent(&self, other: &Self) -> bool {
135        match (self, other) {
136            (CrsDef::Geographic(a), CrsDef::Geographic(b)) => a.datum().same_datum(b.datum()),
137            (CrsDef::Projected(a), CrsDef::Projected(b)) => {
138                a.datum().same_datum(b.datum())
139                    && approx_eq(a.linear_unit_to_meter(), b.linear_unit_to_meter())
140                    && projection_methods_equivalent(&a.method(), &b.method())
141            }
142            _ => false,
143        }
144    }
145}
146
147/// Definition of a geographic CRS (longitude, latitude in degrees).
148#[derive(Debug, Clone, Copy)]
149pub struct GeographicCrsDef {
150    epsg: u32,
151    datum: Datum,
152    name: &'static str,
153}
154
155impl GeographicCrsDef {
156    pub const fn new(epsg: u32, datum: Datum, name: &'static str) -> Self {
157        Self { epsg, datum, name }
158    }
159
160    pub const fn epsg(&self) -> u32 {
161        self.epsg
162    }
163
164    pub const fn datum(&self) -> &Datum {
165        &self.datum
166    }
167
168    pub const fn name(&self) -> &'static str {
169        self.name
170    }
171}
172
173/// Definition of a projected CRS (easting, northing in the CRS's native linear unit).
174#[derive(Debug, Clone, Copy)]
175pub struct ProjectedCrsDef {
176    epsg: u32,
177    base_geographic_crs_epsg: u32,
178    datum: Datum,
179    method: ProjectionMethod,
180    linear_unit: LinearUnit,
181    name: &'static str,
182}
183
184impl ProjectedCrsDef {
185    pub const fn new(
186        epsg: u32,
187        datum: Datum,
188        method: ProjectionMethod,
189        linear_unit: LinearUnit,
190        name: &'static str,
191    ) -> Self {
192        Self::new_with_base_geographic_crs(epsg, 0, datum, method, linear_unit, name)
193    }
194
195    pub const fn new_with_base_geographic_crs(
196        epsg: u32,
197        base_geographic_crs_epsg: u32,
198        datum: Datum,
199        method: ProjectionMethod,
200        linear_unit: LinearUnit,
201        name: &'static str,
202    ) -> Self {
203        Self {
204            epsg,
205            base_geographic_crs_epsg,
206            datum,
207            method,
208            linear_unit,
209            name,
210        }
211    }
212
213    pub const fn epsg(&self) -> u32 {
214        self.epsg
215    }
216
217    pub const fn datum(&self) -> &Datum {
218        &self.datum
219    }
220
221    pub const fn base_geographic_crs_epsg(&self) -> u32 {
222        self.base_geographic_crs_epsg
223    }
224
225    pub const fn method(&self) -> ProjectionMethod {
226        self.method
227    }
228
229    pub const fn linear_unit(&self) -> LinearUnit {
230        self.linear_unit
231    }
232
233    pub const fn linear_unit_to_meter(&self) -> f64 {
234        self.linear_unit.meters_per_unit()
235    }
236
237    pub const fn name(&self) -> &'static str {
238        self.name
239    }
240}
241
242/// All supported projection methods with their parameters.
243///
244/// Angle parameters are stored in **degrees**. Conversion to radians happens
245/// at projection construction time (once), not per-transform.
246#[derive(Debug, Clone, Copy, PartialEq)]
247pub enum ProjectionMethod {
248    /// Web Mercator (EPSG:3857) — spherical Mercator on WGS84 semi-major axis.
249    WebMercator,
250
251    /// Transverse Mercator (includes UTM zones).
252    TransverseMercator {
253        /// Central meridian (degrees).
254        lon0: f64,
255        /// Latitude of origin (degrees).
256        lat0: f64,
257        /// Scale factor on central meridian.
258        k0: f64,
259        /// False easting (meters).
260        false_easting: f64,
261        /// False northing (meters).
262        false_northing: f64,
263    },
264
265    /// Polar Stereographic.
266    PolarStereographic {
267        /// Central meridian / straight vertical longitude (degrees).
268        lon0: f64,
269        /// Latitude of true scale (degrees). Determines the hemisphere.
270        lat_ts: f64,
271        /// Scale factor (used when lat_ts = ±90°, otherwise derived from lat_ts).
272        k0: f64,
273        /// False easting (meters).
274        false_easting: f64,
275        /// False northing (meters).
276        false_northing: f64,
277    },
278
279    /// Lambert Conformal Conic (1SP or 2SP).
280    LambertConformalConic {
281        /// Central meridian (degrees).
282        lon0: f64,
283        /// Latitude of origin (degrees).
284        lat0: f64,
285        /// First standard parallel (degrees).
286        lat1: f64,
287        /// Second standard parallel (degrees). Set equal to lat1 for 1SP variant.
288        lat2: f64,
289        /// False easting (meters).
290        false_easting: f64,
291        /// False northing (meters).
292        false_northing: f64,
293    },
294
295    /// Albers Equal Area Conic.
296    AlbersEqualArea {
297        /// Central meridian (degrees).
298        lon0: f64,
299        /// Latitude of origin (degrees).
300        lat0: f64,
301        /// First standard parallel (degrees).
302        lat1: f64,
303        /// Second standard parallel (degrees).
304        lat2: f64,
305        /// False easting (meters).
306        false_easting: f64,
307        /// False northing (meters).
308        false_northing: f64,
309    },
310
311    /// Standard Mercator (ellipsoidal, distinct from Web Mercator).
312    Mercator {
313        /// Central meridian (degrees).
314        lon0: f64,
315        /// Latitude of true scale (degrees). 0 for 1SP variant.
316        lat_ts: f64,
317        /// Scale factor (for 1SP when lat_ts = 0).
318        k0: f64,
319        /// False easting (meters).
320        false_easting: f64,
321        /// False northing (meters).
322        false_northing: f64,
323    },
324
325    /// Equidistant Cylindrical / Plate Carrée.
326    EquidistantCylindrical {
327        /// Central meridian (degrees).
328        lon0: f64,
329        /// Latitude of true scale (degrees).
330        lat_ts: f64,
331        /// False easting (meters).
332        false_easting: f64,
333        /// False northing (meters).
334        false_northing: f64,
335    },
336}
337
338fn projection_methods_equivalent(a: &ProjectionMethod, b: &ProjectionMethod) -> bool {
339    match (a, b) {
340        (ProjectionMethod::WebMercator, ProjectionMethod::WebMercator) => true,
341        (
342            ProjectionMethod::TransverseMercator {
343                lon0: a_lon0,
344                lat0: a_lat0,
345                k0: a_k0,
346                false_easting: a_false_easting,
347                false_northing: a_false_northing,
348            },
349            ProjectionMethod::TransverseMercator {
350                lon0: b_lon0,
351                lat0: b_lat0,
352                k0: b_k0,
353                false_easting: b_false_easting,
354                false_northing: b_false_northing,
355            },
356        ) => {
357            approx_eq(*a_lon0, *b_lon0)
358                && approx_eq(*a_lat0, *b_lat0)
359                && approx_eq(*a_k0, *b_k0)
360                && approx_eq(*a_false_easting, *b_false_easting)
361                && approx_eq(*a_false_northing, *b_false_northing)
362        }
363        (
364            ProjectionMethod::PolarStereographic {
365                lon0: a_lon0,
366                lat_ts: a_lat_ts,
367                k0: a_k0,
368                false_easting: a_false_easting,
369                false_northing: a_false_northing,
370            },
371            ProjectionMethod::PolarStereographic {
372                lon0: b_lon0,
373                lat_ts: b_lat_ts,
374                k0: b_k0,
375                false_easting: b_false_easting,
376                false_northing: b_false_northing,
377            },
378        ) => {
379            approx_eq(*a_lon0, *b_lon0)
380                && approx_eq(*a_lat_ts, *b_lat_ts)
381                && approx_eq(*a_k0, *b_k0)
382                && approx_eq(*a_false_easting, *b_false_easting)
383                && approx_eq(*a_false_northing, *b_false_northing)
384        }
385        (
386            ProjectionMethod::LambertConformalConic {
387                lon0: a_lon0,
388                lat0: a_lat0,
389                lat1: a_lat1,
390                lat2: a_lat2,
391                false_easting: a_false_easting,
392                false_northing: a_false_northing,
393            },
394            ProjectionMethod::LambertConformalConic {
395                lon0: b_lon0,
396                lat0: b_lat0,
397                lat1: b_lat1,
398                lat2: b_lat2,
399                false_easting: b_false_easting,
400                false_northing: b_false_northing,
401            },
402        ) => {
403            approx_eq(*a_lon0, *b_lon0)
404                && approx_eq(*a_lat0, *b_lat0)
405                && approx_eq(*a_lat1, *b_lat1)
406                && approx_eq(*a_lat2, *b_lat2)
407                && approx_eq(*a_false_easting, *b_false_easting)
408                && approx_eq(*a_false_northing, *b_false_northing)
409        }
410        (
411            ProjectionMethod::AlbersEqualArea {
412                lon0: a_lon0,
413                lat0: a_lat0,
414                lat1: a_lat1,
415                lat2: a_lat2,
416                false_easting: a_false_easting,
417                false_northing: a_false_northing,
418            },
419            ProjectionMethod::AlbersEqualArea {
420                lon0: b_lon0,
421                lat0: b_lat0,
422                lat1: b_lat1,
423                lat2: b_lat2,
424                false_easting: b_false_easting,
425                false_northing: b_false_northing,
426            },
427        ) => {
428            approx_eq(*a_lon0, *b_lon0)
429                && approx_eq(*a_lat0, *b_lat0)
430                && approx_eq(*a_lat1, *b_lat1)
431                && approx_eq(*a_lat2, *b_lat2)
432                && approx_eq(*a_false_easting, *b_false_easting)
433                && approx_eq(*a_false_northing, *b_false_northing)
434        }
435        (
436            ProjectionMethod::Mercator {
437                lon0: a_lon0,
438                lat_ts: a_lat_ts,
439                k0: a_k0,
440                false_easting: a_false_easting,
441                false_northing: a_false_northing,
442            },
443            ProjectionMethod::Mercator {
444                lon0: b_lon0,
445                lat_ts: b_lat_ts,
446                k0: b_k0,
447                false_easting: b_false_easting,
448                false_northing: b_false_northing,
449            },
450        ) => {
451            approx_eq(*a_lon0, *b_lon0)
452                && approx_eq(*a_lat_ts, *b_lat_ts)
453                && approx_eq(*a_k0, *b_k0)
454                && approx_eq(*a_false_easting, *b_false_easting)
455                && approx_eq(*a_false_northing, *b_false_northing)
456        }
457        (
458            ProjectionMethod::EquidistantCylindrical {
459                lon0: a_lon0,
460                lat_ts: a_lat_ts,
461                false_easting: a_false_easting,
462                false_northing: a_false_northing,
463            },
464            ProjectionMethod::EquidistantCylindrical {
465                lon0: b_lon0,
466                lat_ts: b_lat_ts,
467                false_easting: b_false_easting,
468                false_northing: b_false_northing,
469            },
470        ) => {
471            approx_eq(*a_lon0, *b_lon0)
472                && approx_eq(*a_lat_ts, *b_lat_ts)
473                && approx_eq(*a_false_easting, *b_false_easting)
474                && approx_eq(*a_false_northing, *b_false_northing)
475        }
476        _ => false,
477    }
478}
479
480fn approx_eq(a: f64, b: f64) -> bool {
481    (a - b).abs() < 1e-12
482}
483
484#[cfg(test)]
485mod tests {
486    use super::*;
487    use crate::datum;
488
489    #[test]
490    fn geographic_crs_is_geographic() {
491        let crs = CrsDef::Geographic(GeographicCrsDef::new(4326, datum::WGS84, "WGS 84"));
492        assert!(crs.is_geographic());
493        assert!(!crs.is_projected());
494        assert_eq!(crs.epsg(), 4326);
495    }
496
497    #[test]
498    fn projected_crs_is_projected() {
499        let crs = CrsDef::Projected(ProjectedCrsDef::new(
500            3857,
501            datum::WGS84,
502            ProjectionMethod::WebMercator,
503            LinearUnit::metre(),
504            "WGS 84 / Pseudo-Mercator",
505        ));
506        assert!(crs.is_projected());
507        assert!(!crs.is_geographic());
508        assert_eq!(crs.epsg(), 3857);
509    }
510
511    #[test]
512    fn linear_unit_validates_positive_finite_conversion() {
513        assert!(LinearUnit::from_meters_per_unit(0.3048).is_ok());
514        assert!(LinearUnit::from_meters_per_unit(0.0).is_err());
515        assert!(LinearUnit::from_meters_per_unit(f64::NAN).is_err());
516    }
517}