tp_lib_core/crs/
transform.rs

1//! CRS transformation utilities using proj4rs library
2//!
3//! This module uses proj4rs, a pure Rust implementation of PROJ.4,
4//! with crs-definitions for EPSG code lookup.
5
6use crate::errors::ProjectionError;
7use geo::Point;
8use proj4rs::proj::Proj;
9
10/// Wrapper around proj4rs for coordinate reference system transformations
11///
12/// Uses proj4rs (pure Rust PROJ.4 implementation) with no system dependencies.
13/// EPSG codes are resolved using the crs-definitions crate.
14pub struct CrsTransformer {
15    source_crs: String,
16    target_crs: String,
17    from_proj: Proj,
18    to_proj: Proj,
19    source_is_geographic: bool,
20    target_is_geographic: bool,
21}
22
23impl CrsTransformer {
24    /// Create a new CRS transformer
25    ///
26    /// # Arguments
27    /// * `source_crs` - Source CRS as EPSG code (e.g., "EPSG:4326") or PROJ string
28    /// * `target_crs` - Target CRS as EPSG code (e.g., "EPSG:31370") or PROJ string
29    pub fn new(source_crs: String, target_crs: String) -> Result<Self, ProjectionError> {
30        // Convert EPSG codes to PROJ strings
31        let source_proj_str = Self::epsg_to_proj_string(&source_crs)?;
32        let target_proj_str = Self::epsg_to_proj_string(&target_crs)?;
33
34        let from_proj = Proj::from_proj_string(&source_proj_str).map_err(|e| {
35            ProjectionError::InvalidCrs(format!(
36                "Failed to create source projection from {}: {:?}",
37                source_crs, e
38            ))
39        })?;
40
41        let to_proj = Proj::from_proj_string(&target_proj_str).map_err(|e| {
42            ProjectionError::InvalidCrs(format!(
43                "Failed to create target projection from {}: {:?}",
44                target_crs, e
45            ))
46        })?;
47
48        // Detect if projections are geographic (longlat)
49        let source_is_geographic = source_proj_str.contains("+proj=longlat");
50        let target_is_geographic = target_proj_str.contains("+proj=longlat");
51
52        Ok(Self {
53            source_crs,
54            target_crs,
55            from_proj,
56            to_proj,
57            source_is_geographic,
58            target_is_geographic,
59        })
60    }
61
62    fn epsg_to_proj_string(epsg: &str) -> Result<String, ProjectionError> {
63        // Handle EPSG:xxxx format
64        let code = if epsg.starts_with("EPSG:") {
65            epsg.strip_prefix("EPSG:")
66                .and_then(|s| s.parse::<u16>().ok())
67        } else {
68            epsg.parse::<u16>().ok()
69        };
70
71        if let Some(code) = code {
72            // Use crs-definitions to get PROJ string
73            crs_definitions::from_code(code)
74                .ok_or_else(|| ProjectionError::InvalidCrs(format!("Unknown EPSG code: {}", epsg)))
75                .map(|def| def.proj4.to_string())
76        } else {
77            // If not EPSG code, assume it's already a PROJ string
78            Ok(epsg.to_string())
79        }
80    }
81
82    /// Transform a point from source CRS to target CRS
83    ///
84    /// Handles automatic radian/degree conversion for geographic coordinate systems.
85    pub fn transform(&self, point: Point<f64>) -> Result<Point<f64>, ProjectionError> {
86        let mut coord = (point.x(), point.y(), 0.0);
87
88        // Convert input from degrees to radians if source is geographic
89        if self.source_is_geographic {
90            coord.0 = coord.0.to_radians();
91            coord.1 = coord.1.to_radians();
92        }
93
94        // Perform transformation
95        proj4rs::transform::transform(&self.from_proj, &self.to_proj, &mut coord).map_err(|e| {
96            ProjectionError::TransformFailed(format!(
97                "proj4rs transformation failed ({} -> {}): {:?}",
98                self.source_crs, self.target_crs, e
99            ))
100        })?;
101
102        // Convert output from radians to degrees if target is geographic
103        if self.target_is_geographic {
104            coord.0 = coord.0.to_degrees();
105            coord.1 = coord.1.to_degrees();
106        }
107
108        Ok(Point::new(coord.0, coord.1))
109    }
110}