Skip to main content

oxigdal_proj/transform/
mod.rs

1//! Coordinate transformation operations.
2//!
3//! This module provides coordinate transformation capabilities between different CRS
4//! using the proj4rs library for pure Rust implementations, as well as native pure-Rust
5//! implementations of many map projections.
6//!
7//! # Module Structure
8//!
9//! - `cylindrical`   — Cylindrical projections (Mercator, Transverse Mercator, Cassini, etc.)
10//! - `pseudocylindrical` — Pseudo-cylindrical projections (Sinusoidal, Mollweide, Robinson, Eckert IV/VI)
11//! - `conic`         — Conic projections (Lambert Conic, Equidistant Conic, Albers)
12//! - `azimuthal`     — Azimuthal projections (Lambert Azimuthal Equal Area, Azimuthal Equidistant, Gnomonic)
13
14#[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// Re-export projection types for easy access (std only — require transcendental float math)
31#[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/// A 2D coordinate (x, y) or (longitude, latitude).
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Coordinate {
43    /// X coordinate (or longitude in geographic CRS)
44    pub x: f64,
45    /// Y coordinate (or latitude in geographic CRS)
46    pub y: f64,
47}
48
49impl Coordinate {
50    /// Creates a new coordinate.
51    pub fn new(x: f64, y: f64) -> Self {
52        Self { x, y }
53    }
54
55    /// Creates a coordinate from longitude and latitude (in degrees).
56    pub fn from_lon_lat(lon: f64, lat: f64) -> Self {
57        Self::new(lon, lat)
58    }
59
60    /// Returns the longitude (assumes geographic CRS).
61    pub fn lon(&self) -> f64 {
62        self.x
63    }
64
65    /// Returns the latitude (assumes geographic CRS).
66    pub fn lat(&self) -> f64 {
67        self.y
68    }
69
70    /// Validates that the coordinate is within valid bounds for a geographic CRS.
71    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    /// Checks if the coordinate contains valid (finite) values.
82    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/// A 3D coordinate (x, y, z).
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct Coordinate3D {
96    /// X coordinate
97    pub x: f64,
98    /// Y coordinate
99    pub y: f64,
100    /// Z coordinate (elevation/height)
101    pub z: f64,
102}
103
104impl Coordinate3D {
105    /// Creates a new 3D coordinate.
106    pub fn new(x: f64, y: f64, z: f64) -> Self {
107        Self { x, y, z }
108    }
109
110    /// Converts to 2D coordinate (drops Z).
111    pub fn to_2d(&self) -> Coordinate {
112        Coordinate::new(self.x, self.y)
113    }
114
115    /// Checks if the coordinate contains valid (finite) values.
116    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/// A bounding box defined by minimum and maximum coordinates.
128#[derive(Debug, Clone, Copy, PartialEq)]
129pub struct BoundingBox {
130    /// Minimum X coordinate
131    pub min_x: f64,
132    /// Minimum Y coordinate
133    pub min_y: f64,
134    /// Maximum X coordinate
135    pub max_x: f64,
136    /// Maximum Y coordinate
137    pub max_y: f64,
138}
139
140impl BoundingBox {
141    /// Creates a new bounding box.
142    ///
143    /// # Errors
144    ///
145    /// Returns an error if min > max for any dimension.
146    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    /// Creates a bounding box from two coordinates.
169    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    /// Returns the width of the bounding box.
178    pub fn width(&self) -> f64 {
179        self.max_x - self.min_x
180    }
181
182    /// Returns the height of the bounding box.
183    pub fn height(&self) -> f64 {
184        self.max_y - self.min_y
185    }
186
187    /// Returns the center coordinate of the bounding box.
188    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    /// Returns the four corner coordinates.
196    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    /// Checks if a coordinate is within the bounding box.
206    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    /// Expands the bounding box to include a coordinate.
214    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/// Coordinate transformer that handles transformations between CRS.
223#[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    /// Creates a new transformer.
233    ///
234    /// # Arguments
235    ///
236    /// * `source_crs` - Source coordinate reference system
237    /// * `target_crs` - Target coordinate reference system
238    ///
239    /// # Errors
240    ///
241    /// Returns an error if the transformation cannot be initialized.
242    pub fn new(source_crs: Crs, target_crs: Crs) -> Result<Self> {
243        // Check if CRS are the same (no transformation needed)
244        let proj = if source_crs.is_equivalent(&target_crs) {
245            None
246        } else {
247            // Initialize proj4rs transformation
248            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            // We'll store the target proj for now, and use proj4rs::transform later
258            Some(target_proj)
259        };
260
261        Ok(Self {
262            source_crs,
263            target_crs,
264            proj,
265        })
266    }
267
268    /// Creates a transformer from EPSG codes.
269    ///
270    /// # Arguments
271    ///
272    /// * `source_epsg` - Source EPSG code
273    /// * `target_epsg` - Target EPSG code
274    ///
275    /// # Errors
276    ///
277    /// Returns an error if the EPSG codes are invalid or transformation cannot be initialized.
278    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    /// Returns the source CRS.
285    pub fn source_crs(&self) -> &Crs {
286        &self.source_crs
287    }
288
289    /// Returns the target CRS.
290    pub fn target_crs(&self) -> &Crs {
291        &self.target_crs
292    }
293
294    /// Transforms a single coordinate.
295    ///
296    /// # Arguments
297    ///
298    /// * `coord` - Input coordinate in source CRS
299    ///
300    /// # Errors
301    ///
302    /// Returns an error if the transformation fails.
303    pub fn transform(&self, coord: &Coordinate) -> Result<Coordinate> {
304        // If no transformation needed, return as-is
305        if self.proj.is_none() {
306            return Ok(*coord);
307        }
308
309        // Validate input
310        if !coord.is_valid() {
311            return Err(Error::invalid_coordinate(
312                "Coordinate contains non-finite values",
313            ));
314        }
315
316        // Perform transformation using proj4rs
317        self.transform_impl(coord)
318    }
319
320    /// Transforms a 3D coordinate.
321    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        // Transform 2D part
333        let coord_2d = coord.to_2d();
334        let transformed_2d = self.transform_impl(&coord_2d)?;
335
336        // Keep Z coordinate (proper 3D transformation would require more complex logic)
337        Ok(Coordinate3D::new(
338            transformed_2d.x,
339            transformed_2d.y,
340            coord.z,
341        ))
342    }
343
344    /// Transforms multiple coordinates in batch.
345    ///
346    /// This is more efficient than transforming one-by-one for large datasets.
347    ///
348    /// # Arguments
349    ///
350    /// * `coords` - Input coordinates in source CRS
351    ///
352    /// # Errors
353    ///
354    /// Returns an error if any transformation fails.
355    pub fn transform_batch(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
356        coords.iter().map(|c| self.transform(c)).collect()
357    }
358
359    /// Transforms a bounding box.
360    ///
361    /// This transforms all four corners and creates a new bounding box from the results.
362    ///
363    /// # Arguments
364    ///
365    /// * `bbox` - Input bounding box in source CRS
366    ///
367    /// # Errors
368    ///
369    /// Returns an error if the transformation fails.
370    pub fn transform_bbox(&self, bbox: &BoundingBox) -> Result<BoundingBox> {
371        if self.proj.is_none() {
372            return Ok(*bbox);
373        }
374
375        // Transform all four corners
376        let corners = bbox.corners();
377        let transformed_corners = self.transform_batch(&corners)?;
378
379        // Find new bounds
380        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    /// Internal implementation of coordinate transformation using proj4rs.
396    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        // Convert to radians if source is geographic
407        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        // Perform transformation using a mutable array (proj4rs requires slice)
416        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        // Convert from radians if target is geographic
423        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/// Transforms a coordinate from one CRS to another (convenience function).
441#[cfg(feature = "std")]
442///
443/// # Arguments
444///
445/// * `coord` - Input coordinate
446/// * `source_crs` - Source CRS
447/// * `target_crs` - Target CRS
448///
449/// # Errors
450///
451/// Returns an error if the transformation fails.
452pub 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/// Transforms coordinates from one EPSG code to another (convenience function).
462#[cfg(feature = "std")]
463///
464/// # Arguments
465///
466/// * `coord` - Input coordinate
467/// * `source_epsg` - Source EPSG code
468/// * `target_epsg` - Target EPSG code
469///
470/// # Errors
471///
472/// Returns an error if the transformation fails.
473pub 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        // Transform London coordinates (0.0, 51.5)
606        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        // Web Mercator should give us meters from equator
612        // X should be close to 0 (prime meridian)
613        assert_relative_eq!(result.x, 0.0, epsilon = 1.0);
614        // Y should be positive (northern hemisphere)
615        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}