1#[cfg(feature = "std")]
15pub mod azimuthal;
16#[cfg(feature = "std")]
17pub mod conic;
18#[cfg(feature = "std")]
19pub mod cylindrical;
20#[cfg(feature = "std")]
21pub mod pseudocylindrical;
22
23#[cfg(feature = "std")]
24use crate::crs::Crs;
25use crate::error::{Error, Result};
26#[cfg(not(feature = "std"))]
27use alloc::format;
28use core::fmt;
29
30#[cfg(feature = "std")]
32pub use azimuthal::{AzimuthalEquidistant, Gnomonic, LambertAzimuthalEqualArea};
33#[cfg(feature = "std")]
34pub use conic::{EquidistantConic, LambertConformalConic};
35#[cfg(feature = "std")]
36pub use cylindrical::{CassineSoldner, GaussKruger, TransverseMercator};
37#[cfg(feature = "std")]
38pub use pseudocylindrical::{EckertIV, EckertVI, Mollweide, Robinson, Sinusoidal};
39
40#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Coordinate {
43 pub x: f64,
45 pub y: f64,
47}
48
49impl Coordinate {
50 pub fn new(x: f64, y: f64) -> Self {
52 Self { x, y }
53 }
54
55 pub fn from_lon_lat(lon: f64, lat: f64) -> Self {
57 Self::new(lon, lat)
58 }
59
60 pub fn lon(&self) -> f64 {
62 self.x
63 }
64
65 pub fn lat(&self) -> f64 {
67 self.y
68 }
69
70 pub fn validate_geographic(&self) -> Result<()> {
72 if !(-180.0..=180.0).contains(&self.x) {
73 return Err(Error::coordinate_out_of_bounds(self.x, self.y));
74 }
75 if !(-90.0..=90.0).contains(&self.y) {
76 return Err(Error::coordinate_out_of_bounds(self.x, self.y));
77 }
78 Ok(())
79 }
80
81 pub fn is_valid(&self) -> bool {
83 self.x.is_finite() && self.y.is_finite()
84 }
85}
86
87impl fmt::Display for Coordinate {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 write!(f, "({}, {})", self.x, self.y)
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct Coordinate3D {
96 pub x: f64,
98 pub y: f64,
100 pub z: f64,
102}
103
104impl Coordinate3D {
105 pub fn new(x: f64, y: f64, z: f64) -> Self {
107 Self { x, y, z }
108 }
109
110 pub fn to_2d(&self) -> Coordinate {
112 Coordinate::new(self.x, self.y)
113 }
114
115 pub fn is_valid(&self) -> bool {
117 self.x.is_finite() && self.y.is_finite() && self.z.is_finite()
118 }
119}
120
121impl From<Coordinate> for Coordinate3D {
122 fn from(coord: Coordinate) -> Self {
123 Self::new(coord.x, coord.y, 0.0)
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq)]
129pub struct BoundingBox {
130 pub min_x: f64,
132 pub min_y: f64,
134 pub max_x: f64,
136 pub max_y: f64,
138}
139
140impl BoundingBox {
141 pub fn new(min_x: f64, min_y: f64, max_x: f64, max_y: f64) -> Result<Self> {
147 if min_x > max_x {
148 return Err(Error::invalid_bounding_box(format!(
149 "min_x ({}) > max_x ({})",
150 min_x, max_x
151 )));
152 }
153 if min_y > max_y {
154 return Err(Error::invalid_bounding_box(format!(
155 "min_y ({}) > max_y ({})",
156 min_y, max_y
157 )));
158 }
159
160 Ok(Self {
161 min_x,
162 min_y,
163 max_x,
164 max_y,
165 })
166 }
167
168 pub fn from_coordinates(c1: Coordinate, c2: Coordinate) -> Result<Self> {
170 let min_x = c1.x.min(c2.x);
171 let min_y = c1.y.min(c2.y);
172 let max_x = c1.x.max(c2.x);
173 let max_y = c1.y.max(c2.y);
174 Self::new(min_x, min_y, max_x, max_y)
175 }
176
177 pub fn width(&self) -> f64 {
179 self.max_x - self.min_x
180 }
181
182 pub fn height(&self) -> f64 {
184 self.max_y - self.min_y
185 }
186
187 pub fn center(&self) -> Coordinate {
189 Coordinate::new(
190 (self.min_x + self.max_x) / 2.0,
191 (self.min_y + self.max_y) / 2.0,
192 )
193 }
194
195 pub fn corners(&self) -> [Coordinate; 4] {
197 [
198 Coordinate::new(self.min_x, self.min_y),
199 Coordinate::new(self.max_x, self.min_y),
200 Coordinate::new(self.max_x, self.max_y),
201 Coordinate::new(self.min_x, self.max_y),
202 ]
203 }
204
205 pub fn contains(&self, coord: &Coordinate) -> bool {
207 coord.x >= self.min_x
208 && coord.x <= self.max_x
209 && coord.y >= self.min_y
210 && coord.y <= self.max_y
211 }
212
213 pub fn expand_to_include(&mut self, coord: &Coordinate) {
215 self.min_x = self.min_x.min(coord.x);
216 self.min_y = self.min_y.min(coord.y);
217 self.max_x = self.max_x.max(coord.x);
218 self.max_y = self.max_y.max(coord.y);
219 }
220}
221
222#[cfg(feature = "std")]
224pub struct Transformer {
225 source_crs: Crs,
226 target_crs: Crs,
227 proj: Option<proj4rs::Proj>,
228}
229
230#[cfg(feature = "std")]
231impl Transformer {
232 pub fn new(source_crs: Crs, target_crs: Crs) -> Result<Self> {
243 let proj = if source_crs.is_equivalent(&target_crs) {
245 None
246 } else {
247 let source_proj_str = source_crs.to_proj_string()?;
249 let target_proj_str = target_crs.to_proj_string()?;
250
251 let _source_proj = proj4rs::Proj::from_proj_string(&source_proj_str)
252 .map_err(|e| Error::projection_init_error(format!("Source CRS: {:?}", e)))?;
253
254 let target_proj = proj4rs::Proj::from_proj_string(&target_proj_str)
255 .map_err(|e| Error::projection_init_error(format!("Target CRS: {:?}", e)))?;
256
257 Some(target_proj)
259 };
260
261 Ok(Self {
262 source_crs,
263 target_crs,
264 proj,
265 })
266 }
267
268 pub fn from_epsg(source_epsg: u32, target_epsg: u32) -> Result<Self> {
279 let source_crs = Crs::from_epsg(source_epsg)?;
280 let target_crs = Crs::from_epsg(target_epsg)?;
281 Self::new(source_crs, target_crs)
282 }
283
284 pub fn source_crs(&self) -> &Crs {
286 &self.source_crs
287 }
288
289 pub fn target_crs(&self) -> &Crs {
291 &self.target_crs
292 }
293
294 pub fn transform(&self, coord: &Coordinate) -> Result<Coordinate> {
304 if self.proj.is_none() {
306 return Ok(*coord);
307 }
308
309 if !coord.is_valid() {
311 return Err(Error::invalid_coordinate(
312 "Coordinate contains non-finite values",
313 ));
314 }
315
316 self.transform_impl(coord)
318 }
319
320 pub fn transform_3d(&self, coord: &Coordinate3D) -> Result<Coordinate3D> {
322 if self.proj.is_none() {
323 return Ok(*coord);
324 }
325
326 if !coord.is_valid() {
327 return Err(Error::invalid_coordinate(
328 "Coordinate contains non-finite values",
329 ));
330 }
331
332 let coord_2d = coord.to_2d();
334 let transformed_2d = self.transform_impl(&coord_2d)?;
335
336 Ok(Coordinate3D::new(
338 transformed_2d.x,
339 transformed_2d.y,
340 coord.z,
341 ))
342 }
343
344 pub fn transform_batch(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
356 coords.iter().map(|c| self.transform(c)).collect()
357 }
358
359 pub fn transform_bbox(&self, bbox: &BoundingBox) -> Result<BoundingBox> {
371 if self.proj.is_none() {
372 return Ok(*bbox);
373 }
374
375 let corners = bbox.corners();
377 let transformed_corners = self.transform_batch(&corners)?;
378
379 let mut min_x = f64::INFINITY;
381 let mut min_y = f64::INFINITY;
382 let mut max_x = f64::NEG_INFINITY;
383 let mut max_y = f64::NEG_INFINITY;
384
385 for corner in &transformed_corners {
386 min_x = min_x.min(corner.x);
387 min_y = min_y.min(corner.y);
388 max_x = max_x.max(corner.x);
389 max_y = max_y.max(corner.y);
390 }
391
392 BoundingBox::new(min_x, min_y, max_x, max_y)
393 }
394
395 fn transform_impl(&self, coord: &Coordinate) -> Result<Coordinate> {
397 let source_proj_str = self.source_crs.to_proj_string()?;
398 let target_proj_str = self.target_crs.to_proj_string()?;
399
400 let source_proj = proj4rs::Proj::from_proj_string(&source_proj_str)
401 .map_err(|e| Error::from_proj4rs(format!("{:?}", e)))?;
402
403 let target_proj = proj4rs::Proj::from_proj_string(&target_proj_str)
404 .map_err(|e| Error::from_proj4rs(format!("{:?}", e)))?;
405
406 let mut x = coord.x;
408 let mut y = coord.y;
409
410 if self.source_crs.is_geographic() {
411 x = x.to_radians();
412 y = y.to_radians();
413 }
414
415 let mut points = [(x, y)];
417 proj4rs::transform::transform(&source_proj, &target_proj, &mut points[..])
418 .map_err(|e| Error::transformation_error(format!("{:?}", e)))?;
419
420 let (mut result_x, mut result_y) = points[0];
421
422 if self.target_crs.is_geographic() {
424 result_x = result_x.to_degrees();
425 result_y = result_y.to_degrees();
426 }
427
428 let transformed = Coordinate::new(result_x, result_y);
429
430 if !transformed.is_valid() {
431 return Err(Error::transformation_error(
432 "Transformation resulted in non-finite values",
433 ));
434 }
435
436 Ok(transformed)
437 }
438}
439
440#[cfg(feature = "std")]
442pub fn transform_coordinate(
453 coord: &Coordinate,
454 source_crs: &Crs,
455 target_crs: &Crs,
456) -> Result<Coordinate> {
457 let transformer = Transformer::new(source_crs.clone(), target_crs.clone())?;
458 transformer.transform(coord)
459}
460
461#[cfg(feature = "std")]
463pub fn transform_epsg(
474 coord: &Coordinate,
475 source_epsg: u32,
476 target_epsg: u32,
477) -> Result<Coordinate> {
478 let transformer = Transformer::from_epsg(source_epsg, target_epsg)?;
479 transformer.transform(coord)
480}
481
482#[cfg(test)]
483#[allow(clippy::expect_used)]
484mod tests {
485 use super::*;
486 use approx::assert_relative_eq;
487
488 #[test]
489 fn test_coordinate_creation() {
490 let coord = Coordinate::new(10.0, 20.0);
491 assert_eq!(coord.x, 10.0);
492 assert_eq!(coord.y, 20.0);
493 }
494
495 #[test]
496 fn test_coordinate_from_lon_lat() {
497 let coord = Coordinate::from_lon_lat(-122.4194, 37.7749);
498 assert_eq!(coord.lon(), -122.4194);
499 assert_eq!(coord.lat(), 37.7749);
500 }
501
502 #[test]
503 fn test_coordinate_validation() {
504 let valid = Coordinate::new(0.0, 0.0);
505 assert!(valid.validate_geographic().is_ok());
506
507 let invalid_lon = Coordinate::new(200.0, 0.0);
508 assert!(invalid_lon.validate_geographic().is_err());
509
510 let invalid_lat = Coordinate::new(0.0, 100.0);
511 assert!(invalid_lat.validate_geographic().is_err());
512 }
513
514 #[test]
515 fn test_coordinate_is_valid() {
516 let valid = Coordinate::new(1.0, 2.0);
517 assert!(valid.is_valid());
518
519 let invalid = Coordinate::new(f64::NAN, 2.0);
520 assert!(!invalid.is_valid());
521
522 let infinite = Coordinate::new(f64::INFINITY, 2.0);
523 assert!(!infinite.is_valid());
524 }
525
526 #[test]
527 fn test_coordinate3d() {
528 let coord = Coordinate3D::new(1.0, 2.0, 3.0);
529 assert_eq!(coord.x, 1.0);
530 assert_eq!(coord.y, 2.0);
531 assert_eq!(coord.z, 3.0);
532
533 let coord_2d = coord.to_2d();
534 assert_eq!(coord_2d.x, 1.0);
535 assert_eq!(coord_2d.y, 2.0);
536 }
537
538 #[test]
539 fn test_bounding_box() {
540 let bbox = BoundingBox::new(0.0, 0.0, 10.0, 20.0);
541 assert!(bbox.is_ok());
542
543 let bbox = bbox.expect("should be valid");
544 assert_eq!(bbox.width(), 10.0);
545 assert_eq!(bbox.height(), 20.0);
546
547 let center = bbox.center();
548 assert_eq!(center.x, 5.0);
549 assert_eq!(center.y, 10.0);
550 }
551
552 #[test]
553 fn test_bounding_box_invalid() {
554 let result = BoundingBox::new(10.0, 0.0, 0.0, 20.0);
555 assert!(result.is_err());
556
557 let result = BoundingBox::new(0.0, 20.0, 10.0, 0.0);
558 assert!(result.is_err());
559 }
560
561 #[test]
562 fn test_bounding_box_contains() {
563 let bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
564
565 assert!(bbox.contains(&Coordinate::new(5.0, 5.0)));
566 assert!(bbox.contains(&Coordinate::new(0.0, 0.0)));
567 assert!(bbox.contains(&Coordinate::new(10.0, 10.0)));
568 assert!(!bbox.contains(&Coordinate::new(-1.0, 5.0)));
569 assert!(!bbox.contains(&Coordinate::new(5.0, 11.0)));
570 }
571
572 #[test]
573 fn test_bounding_box_expand() {
574 let mut bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
575
576 bbox.expand_to_include(&Coordinate::new(15.0, 5.0));
577 assert_eq!(bbox.max_x, 15.0);
578
579 bbox.expand_to_include(&Coordinate::new(5.0, -5.0));
580 assert_eq!(bbox.min_y, -5.0);
581 }
582
583 #[test]
584 fn test_transformer_same_crs() {
585 let wgs84 = Crs::wgs84();
586 let transformer = Transformer::new(wgs84.clone(), wgs84.clone());
587 assert!(transformer.is_ok());
588
589 let transformer = transformer.expect("should create transformer");
590 let coord = Coordinate::new(10.0, 20.0);
591 let result = transformer.transform(&coord);
592 assert!(result.is_ok());
593
594 let result = result.expect("should transform");
595 assert_eq!(result, coord);
596 }
597
598 #[test]
599 fn test_transformer_wgs84_to_web_mercator() {
600 let transformer = Transformer::from_epsg(4326, 3857);
601 assert!(transformer.is_ok());
602
603 let transformer = transformer.expect("should create transformer");
604
605 let london = Coordinate::from_lon_lat(0.0, 51.5);
607 let result = transformer.transform(&london);
608 assert!(result.is_ok());
609
610 let result = result.expect("should transform");
611 assert_relative_eq!(result.x, 0.0, epsilon = 1.0);
614 assert!(result.y > 6_000_000.0 && result.y < 7_000_000.0);
616 }
617
618 #[test]
619 fn test_transform_batch() {
620 let transformer = Transformer::from_epsg(4326, 4326).expect("same CRS");
621
622 let coords = vec![
623 Coordinate::new(0.0, 0.0),
624 Coordinate::new(10.0, 10.0),
625 Coordinate::new(20.0, 20.0),
626 ];
627
628 let result = transformer.transform_batch(&coords);
629 assert!(result.is_ok());
630
631 let result = result.expect("should transform");
632 assert_eq!(result.len(), 3);
633 assert_eq!(result[0], coords[0]);
634 assert_eq!(result[1], coords[1]);
635 assert_eq!(result[2], coords[2]);
636 }
637
638 #[test]
639 fn test_transform_bbox() {
640 let transformer = Transformer::from_epsg(4326, 4326).expect("same CRS");
641
642 let bbox = BoundingBox::new(0.0, 0.0, 10.0, 10.0).expect("valid bbox");
643 let result = transformer.transform_bbox(&bbox);
644 assert!(result.is_ok());
645
646 let result = result.expect("should transform");
647 assert_eq!(result, bbox);
648 }
649
650 #[test]
651 fn test_convenience_functions() {
652 let wgs84 = Crs::wgs84();
653 let coord = Coordinate::new(0.0, 0.0);
654
655 let result = transform_coordinate(&coord, &wgs84, &wgs84);
656 assert!(result.is_ok());
657 assert_eq!(result.expect("should transform"), coord);
658
659 let result = transform_epsg(&coord, 4326, 4326);
660 assert!(result.is_ok());
661 assert_eq!(result.expect("should transform"), coord);
662 }
663
664 #[test]
665 fn test_transform_invalid_coordinate() {
666 let transformer = Transformer::from_epsg(4326, 3857).expect("should create");
667
668 let invalid = Coordinate::new(f64::NAN, 0.0);
669 let result = transformer.transform(&invalid);
670 assert!(result.is_err());
671 }
672}