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
123/// Definition of a geographic CRS (longitude, latitude in degrees).
124#[derive(Debug, Clone, Copy)]
125pub struct GeographicCrsDef {
126    epsg: u32,
127    datum: Datum,
128    name: &'static str,
129}
130
131impl GeographicCrsDef {
132    pub const fn new(epsg: u32, datum: Datum, name: &'static str) -> Self {
133        Self { epsg, datum, name }
134    }
135
136    pub const fn epsg(&self) -> u32 {
137        self.epsg
138    }
139
140    pub const fn datum(&self) -> &Datum {
141        &self.datum
142    }
143
144    pub const fn name(&self) -> &'static str {
145        self.name
146    }
147}
148
149/// Definition of a projected CRS (easting, northing in the CRS's native linear unit).
150#[derive(Debug, Clone, Copy)]
151pub struct ProjectedCrsDef {
152    epsg: u32,
153    datum: Datum,
154    method: ProjectionMethod,
155    linear_unit: LinearUnit,
156    name: &'static str,
157}
158
159impl ProjectedCrsDef {
160    pub const fn new(
161        epsg: u32,
162        datum: Datum,
163        method: ProjectionMethod,
164        linear_unit: LinearUnit,
165        name: &'static str,
166    ) -> Self {
167        Self {
168            epsg,
169            datum,
170            method,
171            linear_unit,
172            name,
173        }
174    }
175
176    pub const fn epsg(&self) -> u32 {
177        self.epsg
178    }
179
180    pub const fn datum(&self) -> &Datum {
181        &self.datum
182    }
183
184    pub const fn method(&self) -> ProjectionMethod {
185        self.method
186    }
187
188    pub const fn linear_unit(&self) -> LinearUnit {
189        self.linear_unit
190    }
191
192    pub const fn linear_unit_to_meter(&self) -> f64 {
193        self.linear_unit.meters_per_unit()
194    }
195
196    pub const fn name(&self) -> &'static str {
197        self.name
198    }
199}
200
201/// All supported projection methods with their parameters.
202///
203/// Angle parameters are stored in **degrees**. Conversion to radians happens
204/// at projection construction time (once), not per-transform.
205#[derive(Debug, Clone, Copy)]
206pub enum ProjectionMethod {
207    /// Web Mercator (EPSG:3857) — spherical Mercator on WGS84 semi-major axis.
208    WebMercator,
209
210    /// Transverse Mercator (includes UTM zones).
211    TransverseMercator {
212        /// Central meridian (degrees).
213        lon0: f64,
214        /// Latitude of origin (degrees).
215        lat0: f64,
216        /// Scale factor on central meridian.
217        k0: f64,
218        /// False easting (meters).
219        false_easting: f64,
220        /// False northing (meters).
221        false_northing: f64,
222    },
223
224    /// Polar Stereographic.
225    PolarStereographic {
226        /// Central meridian / straight vertical longitude (degrees).
227        lon0: f64,
228        /// Latitude of true scale (degrees). Determines the hemisphere.
229        lat_ts: f64,
230        /// Scale factor (used when lat_ts = ±90°, otherwise derived from lat_ts).
231        k0: f64,
232        /// False easting (meters).
233        false_easting: f64,
234        /// False northing (meters).
235        false_northing: f64,
236    },
237
238    /// Lambert Conformal Conic (1SP or 2SP).
239    LambertConformalConic {
240        /// Central meridian (degrees).
241        lon0: f64,
242        /// Latitude of origin (degrees).
243        lat0: f64,
244        /// First standard parallel (degrees).
245        lat1: f64,
246        /// Second standard parallel (degrees). Set equal to lat1 for 1SP variant.
247        lat2: f64,
248        /// False easting (meters).
249        false_easting: f64,
250        /// False northing (meters).
251        false_northing: f64,
252    },
253
254    /// Albers Equal Area Conic.
255    AlbersEqualArea {
256        /// Central meridian (degrees).
257        lon0: f64,
258        /// Latitude of origin (degrees).
259        lat0: f64,
260        /// First standard parallel (degrees).
261        lat1: f64,
262        /// Second standard parallel (degrees).
263        lat2: f64,
264        /// False easting (meters).
265        false_easting: f64,
266        /// False northing (meters).
267        false_northing: f64,
268    },
269
270    /// Standard Mercator (ellipsoidal, distinct from Web Mercator).
271    Mercator {
272        /// Central meridian (degrees).
273        lon0: f64,
274        /// Latitude of true scale (degrees). 0 for 1SP variant.
275        lat_ts: f64,
276        /// Scale factor (for 1SP when lat_ts = 0).
277        k0: f64,
278        /// False easting (meters).
279        false_easting: f64,
280        /// False northing (meters).
281        false_northing: f64,
282    },
283
284    /// Equidistant Cylindrical / Plate Carrée.
285    EquidistantCylindrical {
286        /// Central meridian (degrees).
287        lon0: f64,
288        /// Latitude of true scale (degrees).
289        lat_ts: f64,
290        /// False easting (meters).
291        false_easting: f64,
292        /// False northing (meters).
293        false_northing: f64,
294    },
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::datum;
301
302    #[test]
303    fn geographic_crs_is_geographic() {
304        let crs = CrsDef::Geographic(GeographicCrsDef::new(4326, datum::WGS84, "WGS 84"));
305        assert!(crs.is_geographic());
306        assert!(!crs.is_projected());
307        assert_eq!(crs.epsg(), 4326);
308    }
309
310    #[test]
311    fn projected_crs_is_projected() {
312        let crs = CrsDef::Projected(ProjectedCrsDef::new(
313            3857,
314            datum::WGS84,
315            ProjectionMethod::WebMercator,
316            LinearUnit::metre(),
317            "WGS 84 / Pseudo-Mercator",
318        ));
319        assert!(crs.is_projected());
320        assert!(!crs.is_geographic());
321        assert_eq!(crs.epsg(), 3857);
322    }
323
324    #[test]
325    fn linear_unit_validates_positive_finite_conversion() {
326        assert!(LinearUnit::from_meters_per_unit(0.3048).is_ok());
327        assert!(LinearUnit::from_meters_per_unit(0.0).is_err());
328        assert!(LinearUnit::from_meters_per_unit(f64::NAN).is_err());
329    }
330}