1use crate::datum::Datum;
2use crate::error::{Error, Result};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct LinearUnit {
9 meters_per_unit: f64,
10}
11
12impl LinearUnit {
13 pub const fn metre() -> Self {
15 Self {
16 meters_per_unit: 1.0,
17 }
18 }
19
20 pub const fn meter() -> Self {
22 Self::metre()
23 }
24
25 pub const fn kilometre() -> Self {
27 Self {
28 meters_per_unit: 1000.0,
29 }
30 }
31
32 pub const fn kilometer() -> Self {
34 Self::kilometre()
35 }
36
37 pub const fn foot() -> Self {
39 Self {
40 meters_per_unit: 0.3048,
41 }
42 }
43
44 pub const fn us_survey_foot() -> Self {
46 Self {
47 meters_per_unit: 0.3048006096012192,
48 }
49 }
50
51 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 pub const fn meters_per_unit(self) -> f64 {
64 self.meters_per_unit
65 }
66
67 pub const fn to_meters(self, value: f64) -> f64 {
69 value * self.meters_per_unit
70 }
71
72 pub const fn from_meters(self, value: f64) -> f64 {
74 value / self.meters_per_unit
75 }
76}
77
78#[derive(Debug, Clone, Copy)]
80pub enum CrsDef {
81 Geographic(GeographicCrsDef),
83 Projected(ProjectedCrsDef),
85}
86
87impl CrsDef {
88 pub fn datum(&self) -> &Datum {
90 match self {
91 CrsDef::Geographic(g) => g.datum(),
92 CrsDef::Projected(p) => p.datum(),
93 }
94 }
95
96 pub fn epsg(&self) -> u32 {
98 match self {
99 CrsDef::Geographic(g) => g.epsg(),
100 CrsDef::Projected(p) => p.epsg(),
101 }
102 }
103
104 pub fn name(&self) -> &str {
106 match self {
107 CrsDef::Geographic(g) => g.name(),
108 CrsDef::Projected(p) => p.name(),
109 }
110 }
111
112 pub fn is_geographic(&self) -> bool {
114 matches!(self, CrsDef::Geographic(_))
115 }
116
117 pub fn is_projected(&self) -> bool {
119 matches!(self, CrsDef::Projected(_))
120 }
121}
122
123#[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#[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#[derive(Debug, Clone, Copy)]
206pub enum ProjectionMethod {
207 WebMercator,
209
210 TransverseMercator {
212 lon0: f64,
214 lat0: f64,
216 k0: f64,
218 false_easting: f64,
220 false_northing: f64,
222 },
223
224 PolarStereographic {
226 lon0: f64,
228 lat_ts: f64,
230 k0: f64,
232 false_easting: f64,
234 false_northing: f64,
236 },
237
238 LambertConformalConic {
240 lon0: f64,
242 lat0: f64,
244 lat1: f64,
246 lat2: f64,
248 false_easting: f64,
250 false_northing: f64,
252 },
253
254 AlbersEqualArea {
256 lon0: f64,
258 lat0: f64,
260 lat1: f64,
262 lat2: f64,
264 false_easting: f64,
266 false_northing: f64,
268 },
269
270 Mercator {
272 lon0: f64,
274 lat_ts: f64,
276 k0: f64,
278 false_easting: f64,
280 false_northing: f64,
282 },
283
284 EquidistantCylindrical {
286 lon0: f64,
288 lat_ts: f64,
290 false_easting: f64,
292 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}