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 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 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#[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#[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#[derive(Debug, Clone, Copy, PartialEq)]
247pub enum ProjectionMethod {
248 WebMercator,
250
251 TransverseMercator {
253 lon0: f64,
255 lat0: f64,
257 k0: f64,
259 false_easting: f64,
261 false_northing: f64,
263 },
264
265 PolarStereographic {
267 lon0: f64,
269 lat_ts: f64,
271 k0: f64,
273 false_easting: f64,
275 false_northing: f64,
277 },
278
279 LambertConformalConic {
281 lon0: f64,
283 lat0: f64,
285 lat1: f64,
287 lat2: f64,
289 false_easting: f64,
291 false_northing: f64,
293 },
294
295 AlbersEqualArea {
297 lon0: f64,
299 lat0: f64,
301 lat1: f64,
303 lat2: f64,
305 false_easting: f64,
307 false_northing: f64,
309 },
310
311 Mercator {
313 lon0: f64,
315 lat_ts: f64,
317 k0: f64,
319 false_easting: f64,
321 false_northing: f64,
323 },
324
325 EquidistantCylindrical {
327 lon0: f64,
329 lat_ts: f64,
331 false_easting: f64,
333 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}