Skip to main content

oxigdal_algorithms/vector/
transform.rs

1//! Coordinate transformation operations for vector geometries
2//!
3//! This module provides coordinate transformation capabilities for converting
4//! geometries between different coordinate reference systems (CRS).
5//!
6//! # Features
7//!
8//! - **Point Transformation**: Transform individual points between CRS
9//! - **Geometry Transformation**: Transform entire geometries (LineString, Polygon, etc.)
10//! - **Batch Transformation**: Efficiently transform multiple coordinates at once
11//! - **Projection Support**: Support for common projections (WGS84, Web Mercator, UTM, etc.)
12//!
13//! # Examples
14//!
15//! ```
16//! use oxigdal_algorithms::vector::{Point, Coordinate, transform_point};
17//!
18//! // Transform from WGS84 (EPSG:4326) to Web Mercator (EPSG:3857)
19//! let wgs84_point = Point::new(-122.4194, 37.7749); // San Francisco
20//! # // In real usage, you would transform like this:
21//! # // let web_mercator = transform_point(&wgs84_point, "EPSG:4326", "EPSG:3857").unwrap();
22//! ```
23
24use crate::error::{AlgorithmError, Result};
25use oxigdal_core::vector::{
26    Coordinate, Geometry, GeometryCollection, LineString, MultiLineString, MultiPoint,
27    MultiPolygon, Point, Polygon,
28};
29
30#[cfg(feature = "std")]
31use std::vec::Vec;
32
33/// Common coordinate reference systems
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum CommonCrs {
36    /// WGS84 geographic coordinates (latitude/longitude)
37    Wgs84,
38    /// Web Mercator (used by Google Maps, OpenStreetMap)
39    WebMercator,
40    /// UTM Zone (specify zone number and hemisphere)
41    Utm { zone: u8, north: bool },
42}
43
44impl CommonCrs {
45    /// Returns the EPSG code for this CRS
46    pub fn epsg_code(&self) -> String {
47        match self {
48            Self::Wgs84 => "EPSG:4326".to_string(),
49            Self::WebMercator => "EPSG:3857".to_string(),
50            Self::Utm { zone, north } => {
51                if *north {
52                    format!("EPSG:326{:02}", zone)
53                } else {
54                    format!("EPSG:327{:02}", zone)
55                }
56            }
57        }
58    }
59}
60
61/// Transformer for coordinate reference system conversions
62///
63/// This is a placeholder structure. In a full implementation, this would
64/// integrate with oxigdal-proj or proj4rs for actual transformations.
65pub struct CrsTransformer {
66    source_crs: String,
67    target_crs: String,
68}
69
70impl CrsTransformer {
71    /// Creates a new CRS transformer
72    ///
73    /// # Arguments
74    ///
75    /// * `source_crs` - Source CRS (e.g., "EPSG:4326")
76    /// * `target_crs` - Target CRS (e.g., "EPSG:3857")
77    ///
78    /// # Returns
79    ///
80    /// A new transformer
81    ///
82    /// # Errors
83    ///
84    /// Returns error if CRS definitions are invalid
85    pub fn new(source_crs: impl Into<String>, target_crs: impl Into<String>) -> Result<Self> {
86        let source = source_crs.into();
87        let target = target_crs.into();
88
89        // Validate CRS strings
90        if source.is_empty() || target.is_empty() {
91            return Err(AlgorithmError::InvalidParameter {
92                parameter: "crs",
93                message: "CRS definition cannot be empty".to_string(),
94            });
95        }
96
97        Ok(Self {
98            source_crs: source,
99            target_crs: target,
100        })
101    }
102
103    /// Creates a transformer from common CRS types
104    pub fn from_common(source: CommonCrs, target: CommonCrs) -> Result<Self> {
105        Self::new(source.epsg_code(), target.epsg_code())
106    }
107
108    /// Transforms a single coordinate
109    ///
110    /// # Arguments
111    ///
112    /// * `coord` - Input coordinate in source CRS
113    ///
114    /// # Returns
115    ///
116    /// Transformed coordinate in target CRS
117    ///
118    /// # Errors
119    ///
120    /// Returns error if transformation fails
121    pub fn transform_coordinate(&self, coord: &Coordinate) -> Result<Coordinate> {
122        // Special case: Identity transformation
123        if self.source_crs == self.target_crs {
124            return Ok(*coord);
125        }
126
127        // Special case: WGS84 to Web Mercator (common transformation)
128        if self.source_crs == "EPSG:4326" && self.target_crs == "EPSG:3857" {
129            return self.wgs84_to_web_mercator(coord);
130        }
131
132        // Special case: Web Mercator to WGS84
133        if self.source_crs == "EPSG:3857" && self.target_crs == "EPSG:4326" {
134            return self.web_mercator_to_wgs84(coord);
135        }
136
137        // For other transformations, would integrate with oxigdal-proj
138        // For now, return an error indicating unsupported transformation
139        Err(AlgorithmError::UnsupportedOperation {
140            operation: format!(
141                "Coordinate transformation from {} to {} (requires proj integration)",
142                self.source_crs, self.target_crs
143            ),
144        })
145    }
146
147    /// Transforms multiple coordinates efficiently
148    pub fn transform_coordinates(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
149        coords
150            .iter()
151            .map(|c| self.transform_coordinate(c))
152            .collect()
153    }
154
155    /// Transforms a point
156    pub fn transform_point(&self, point: &Point) -> Result<Point> {
157        let transformed = self.transform_coordinate(&point.coord)?;
158        Ok(Point::from_coord(transformed))
159    }
160
161    /// Transforms a linestring
162    pub fn transform_linestring(&self, linestring: &LineString) -> Result<LineString> {
163        let coords = self.transform_coordinates(&linestring.coords)?;
164        LineString::new(coords).map_err(|e| AlgorithmError::GeometryError {
165            message: format!("Failed to create transformed linestring: {}", e),
166        })
167    }
168
169    /// Transforms a polygon
170    pub fn transform_polygon(&self, polygon: &Polygon) -> Result<Polygon> {
171        let exterior_coords = self.transform_coordinates(&polygon.exterior.coords)?;
172        let exterior =
173            LineString::new(exterior_coords).map_err(|e| AlgorithmError::GeometryError {
174                message: format!("Failed to create transformed exterior ring: {}", e),
175            })?;
176
177        let mut interiors = Vec::new();
178        for hole in &polygon.interiors {
179            let hole_coords = self.transform_coordinates(&hole.coords)?;
180            let hole_ring =
181                LineString::new(hole_coords).map_err(|e| AlgorithmError::GeometryError {
182                    message: format!("Failed to create transformed interior ring: {}", e),
183                })?;
184            interiors.push(hole_ring);
185        }
186
187        Polygon::new(exterior, interiors).map_err(|e| AlgorithmError::GeometryError {
188            message: format!("Failed to create transformed polygon: {}", e),
189        })
190    }
191
192    /// Transforms a geometry
193    pub fn transform_geometry(&self, geometry: &Geometry) -> Result<Geometry> {
194        match geometry {
195            Geometry::Point(p) => Ok(Geometry::Point(self.transform_point(p)?)),
196            Geometry::LineString(ls) => Ok(Geometry::LineString(self.transform_linestring(ls)?)),
197            Geometry::Polygon(poly) => Ok(Geometry::Polygon(self.transform_polygon(poly)?)),
198            Geometry::MultiPoint(mp) => {
199                let mut points = Vec::new();
200                for point in &mp.points {
201                    points.push(self.transform_point(point)?);
202                }
203                Ok(Geometry::MultiPoint(MultiPoint { points }))
204            }
205            Geometry::MultiLineString(mls) => {
206                let mut line_strings = Vec::new();
207                for ls in &mls.line_strings {
208                    line_strings.push(self.transform_linestring(ls)?);
209                }
210                Ok(Geometry::MultiLineString(MultiLineString { line_strings }))
211            }
212            Geometry::MultiPolygon(mp) => {
213                let mut polygons = Vec::new();
214                for poly in &mp.polygons {
215                    polygons.push(self.transform_polygon(poly)?);
216                }
217                Ok(Geometry::MultiPolygon(MultiPolygon { polygons }))
218            }
219            Geometry::GeometryCollection(gc) => {
220                let mut geometries = Vec::new();
221                for geom in &gc.geometries {
222                    geometries.push(self.transform_geometry(geom)?);
223                }
224                Ok(Geometry::GeometryCollection(GeometryCollection {
225                    geometries,
226                }))
227            }
228        }
229    }
230
231    /// WGS84 (EPSG:4326) to Web Mercator (EPSG:3857) transformation
232    fn wgs84_to_web_mercator(&self, coord: &Coordinate) -> Result<Coordinate> {
233        const EARTH_RADIUS: f64 = 6_378_137.0;
234
235        // Validate latitude range
236        if !(-90.0..=90.0).contains(&coord.y) {
237            return Err(AlgorithmError::InvalidParameter {
238                parameter: "latitude",
239                message: format!("Latitude {} is out of range [-90, 90]", coord.y),
240            });
241        }
242
243        // Web Mercator doesn't work well near poles
244        if coord.y.abs() > 85.0511 {
245            return Err(AlgorithmError::InvalidParameter {
246                parameter: "latitude",
247                message: format!(
248                    "Latitude {} is too close to poles for Web Mercator (max ±85.0511°)",
249                    coord.y
250                ),
251            });
252        }
253
254        let lon_rad = coord.x.to_radians();
255        let lat_rad = coord.y.to_radians();
256
257        let x = EARTH_RADIUS * lon_rad;
258        let y = EARTH_RADIUS * ((std::f64::consts::PI / 4.0 + lat_rad / 2.0).tan().ln());
259
260        Ok(Coordinate::new_2d(x, y))
261    }
262
263    /// Web Mercator (EPSG:3857) to WGS84 (EPSG:4326) transformation
264    fn web_mercator_to_wgs84(&self, coord: &Coordinate) -> Result<Coordinate> {
265        const EARTH_RADIUS: f64 = 6_378_137.0;
266
267        let lon = (coord.x / EARTH_RADIUS).to_degrees();
268        let lat =
269            (2.0 * (coord.y / EARTH_RADIUS).exp().atan() - std::f64::consts::PI / 2.0).to_degrees();
270
271        // Clamp to valid ranges
272        let lon = lon.clamp(-180.0, 180.0);
273        let lat = lat.clamp(-90.0, 90.0);
274
275        Ok(Coordinate::new_2d(lon, lat))
276    }
277}
278
279/// Transforms a point between coordinate reference systems
280///
281/// # Arguments
282///
283/// * `point` - Input point
284/// * `source_crs` - Source CRS (e.g., "EPSG:4326")
285/// * `target_crs` - Target CRS (e.g., "EPSG:3857")
286///
287/// # Returns
288///
289/// Transformed point
290///
291/// # Errors
292///
293/// Returns error if transformation fails
294pub fn transform_point(point: &Point, source_crs: &str, target_crs: &str) -> Result<Point> {
295    let transformer = CrsTransformer::new(source_crs, target_crs)?;
296    transformer.transform_point(point)
297}
298
299/// Transforms a linestring between coordinate reference systems
300pub fn transform_linestring(
301    linestring: &LineString,
302    source_crs: &str,
303    target_crs: &str,
304) -> Result<LineString> {
305    let transformer = CrsTransformer::new(source_crs, target_crs)?;
306    transformer.transform_linestring(linestring)
307}
308
309/// Transforms a polygon between coordinate reference systems
310pub fn transform_polygon(polygon: &Polygon, source_crs: &str, target_crs: &str) -> Result<Polygon> {
311    let transformer = CrsTransformer::new(source_crs, target_crs)?;
312    transformer.transform_polygon(polygon)
313}
314
315/// Transforms a geometry between coordinate reference systems
316pub fn transform_geometry(
317    geometry: &Geometry,
318    source_crs: &str,
319    target_crs: &str,
320) -> Result<Geometry> {
321    let transformer = CrsTransformer::new(source_crs, target_crs)?;
322    transformer.transform_geometry(geometry)
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    #[test]
330    fn test_common_crs_epsg_codes() {
331        assert_eq!(CommonCrs::Wgs84.epsg_code(), "EPSG:4326");
332        assert_eq!(CommonCrs::WebMercator.epsg_code(), "EPSG:3857");
333        assert_eq!(
334            CommonCrs::Utm {
335                zone: 10,
336                north: true
337            }
338            .epsg_code(),
339            "EPSG:32610"
340        );
341        assert_eq!(
342            CommonCrs::Utm {
343                zone: 33,
344                north: false
345            }
346            .epsg_code(),
347            "EPSG:32733"
348        );
349    }
350
351    #[test]
352    fn test_transformer_creation() {
353        let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
354        assert!(transformer.is_ok());
355
356        let empty = CrsTransformer::new("", "EPSG:3857");
357        assert!(empty.is_err());
358    }
359
360    #[test]
361    fn test_identity_transformation() {
362        let transformer = CrsTransformer::new("EPSG:4326", "EPSG:4326");
363        assert!(transformer.is_ok());
364
365        if let Ok(t) = transformer {
366            let coord = Coordinate::new_2d(10.0, 20.0);
367            let result = t.transform_coordinate(&coord);
368            assert!(result.is_ok());
369
370            if let Ok(transformed) = result {
371                assert!((transformed.x - 10.0).abs() < f64::EPSILON);
372                assert!((transformed.y - 20.0).abs() < f64::EPSILON);
373            }
374        }
375    }
376
377    #[test]
378    fn test_wgs84_to_web_mercator() {
379        let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
380        assert!(transformer.is_ok());
381
382        if let Ok(t) = transformer {
383            // Transform origin (0, 0)
384            let origin = Coordinate::new_2d(0.0, 0.0);
385            let result = t.transform_coordinate(&origin);
386            assert!(result.is_ok());
387
388            if let Ok(transformed) = result {
389                assert!(transformed.x.abs() < 1.0);
390                assert!(transformed.y.abs() < 1.0);
391            }
392
393            // Transform San Francisco
394            let sf = Coordinate::new_2d(-122.4194, 37.7749);
395            let result = t.transform_coordinate(&sf);
396            assert!(result.is_ok());
397
398            if let Ok(transformed) = result {
399                // Web Mercator x should be negative (west of prime meridian)
400                assert!(transformed.x < 0.0);
401                // y should be positive (north of equator)
402                assert!(transformed.y > 0.0);
403            }
404        }
405    }
406
407    #[test]
408    fn test_web_mercator_to_wgs84() {
409        let transformer = CrsTransformer::new("EPSG:3857", "EPSG:4326");
410        assert!(transformer.is_ok());
411
412        if let Ok(t) = transformer {
413            // Transform origin
414            let origin = Coordinate::new_2d(0.0, 0.0);
415            let result = t.transform_coordinate(&origin);
416            assert!(result.is_ok());
417
418            if let Ok(transformed) = result {
419                assert!(transformed.x.abs() < 1e-6);
420                assert!(transformed.y.abs() < 1e-6);
421            }
422        }
423    }
424
425    #[test]
426    fn test_round_trip_transformation() {
427        let to_merc = CrsTransformer::new("EPSG:4326", "EPSG:3857");
428        let to_wgs = CrsTransformer::new("EPSG:3857", "EPSG:4326");
429
430        assert!(to_merc.is_ok());
431        assert!(to_wgs.is_ok());
432
433        if let (Ok(t1), Ok(t2)) = (to_merc, to_wgs) {
434            let original = Coordinate::new_2d(-122.4194, 37.7749);
435
436            let merc = t1.transform_coordinate(&original);
437            assert!(merc.is_ok());
438
439            if let Ok(m) = merc {
440                let back = t2.transform_coordinate(&m);
441                assert!(back.is_ok());
442
443                if let Ok(b) = back {
444                    // Should be close to original (within tolerance)
445                    assert!((b.x - original.x).abs() < 1e-6);
446                    assert!((b.y - original.y).abs() < 1e-6);
447                }
448            }
449        }
450    }
451
452    #[test]
453    fn test_transform_point() {
454        let point = Point::new(-122.4194, 37.7749);
455        let result = transform_point(&point, "EPSG:4326", "EPSG:3857");
456        assert!(result.is_ok());
457
458        if let Ok(transformed) = result {
459            assert!(transformed.coord.x < 0.0);
460            assert!(transformed.coord.y > 0.0);
461        }
462    }
463
464    #[test]
465    fn test_transform_linestring() {
466        let coords = vec![
467            Coordinate::new_2d(0.0, 0.0),
468            Coordinate::new_2d(1.0, 1.0),
469            Coordinate::new_2d(2.0, 2.0),
470        ];
471        let linestring = LineString::new(coords);
472        assert!(linestring.is_ok());
473
474        if let Ok(ls) = linestring {
475            let result = transform_linestring(&ls, "EPSG:4326", "EPSG:3857");
476            assert!(result.is_ok());
477
478            if let Ok(transformed) = result {
479                assert_eq!(transformed.coords.len(), 3);
480            }
481        }
482    }
483
484    #[test]
485    fn test_transform_polygon() {
486        let coords = vec![
487            Coordinate::new_2d(0.0, 0.0),
488            Coordinate::new_2d(4.0, 0.0),
489            Coordinate::new_2d(4.0, 4.0),
490            Coordinate::new_2d(0.0, 4.0),
491            Coordinate::new_2d(0.0, 0.0),
492        ];
493        let exterior = LineString::new(coords);
494        assert!(exterior.is_ok());
495
496        if let Ok(ext) = exterior {
497            let polygon = Polygon::new(ext, vec![]);
498            assert!(polygon.is_ok());
499
500            if let Ok(poly) = polygon {
501                let result = transform_polygon(&poly, "EPSG:4326", "EPSG:3857");
502                assert!(result.is_ok());
503
504                if let Ok(transformed) = result {
505                    assert_eq!(transformed.exterior.coords.len(), 5);
506                }
507            }
508        }
509    }
510
511    #[test]
512    fn test_invalid_latitude() {
513        let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
514        assert!(transformer.is_ok());
515
516        if let Ok(t) = transformer {
517            // Latitude too high
518            let invalid = Coordinate::new_2d(0.0, 95.0);
519            let result = t.transform_coordinate(&invalid);
520            assert!(result.is_err());
521
522            // Latitude near pole (outside Web Mercator range)
523            let near_pole = Coordinate::new_2d(0.0, 89.0);
524            let result = t.transform_coordinate(&near_pole);
525            assert!(result.is_err());
526        }
527    }
528
529    #[test]
530    fn test_batch_transformation() {
531        let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
532        assert!(transformer.is_ok());
533
534        if let Ok(t) = transformer {
535            let coords = vec![
536                Coordinate::new_2d(0.0, 0.0),
537                Coordinate::new_2d(1.0, 1.0),
538                Coordinate::new_2d(-1.0, -1.0),
539            ];
540
541            let result = t.transform_coordinates(&coords);
542            assert!(result.is_ok());
543
544            if let Ok(transformed) = result {
545                assert_eq!(transformed.len(), 3);
546            }
547        }
548    }
549
550    #[test]
551    fn test_from_common_crs() {
552        let transformer = CrsTransformer::from_common(CommonCrs::Wgs84, CommonCrs::WebMercator);
553        assert!(transformer.is_ok());
554
555        if let Ok(t) = transformer {
556            assert_eq!(t.source_crs, "EPSG:4326");
557            assert_eq!(t.target_crs, "EPSG:3857");
558        }
559    }
560}