1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum CommonCrs {
36 Wgs84,
38 WebMercator,
40 Utm { zone: u8, north: bool },
42}
43
44impl CommonCrs {
45 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
61pub struct CrsTransformer {
68 source_crs: String,
69 target_crs: String,
70 #[cfg(feature = "crs-transform")]
71 proj_transformer: Option<oxigdal_proj::Transformer>,
72}
73
74impl CrsTransformer {
75 pub fn new(source_crs: impl Into<String>, target_crs: impl Into<String>) -> Result<Self> {
90 let source = source_crs.into();
91 let target = target_crs.into();
92
93 if source.is_empty() || target.is_empty() {
95 return Err(AlgorithmError::InvalidParameter {
96 parameter: "crs",
97 message: "CRS definition cannot be empty".to_string(),
98 });
99 }
100
101 #[cfg(feature = "crs-transform")]
102 let proj_transformer = Self::build_proj_transformer(&source, &target);
103
104 Ok(Self {
105 source_crs: source,
106 target_crs: target,
107 #[cfg(feature = "crs-transform")]
108 proj_transformer,
109 })
110 }
111
112 #[cfg(feature = "crs-transform")]
118 fn build_proj_transformer(source: &str, target: &str) -> Option<oxigdal_proj::Transformer> {
119 let src_crs = Self::parse_crs_string(source).ok()?;
120 let tgt_crs = Self::parse_crs_string(target).ok()?;
121 match oxigdal_proj::Transformer::new(src_crs, tgt_crs) {
122 Ok(t) => Some(t.with_strict(false)),
123 Err(e) => {
124 tracing::debug!(
125 "oxigdal-proj: could not initialise transformer {} → {}: {}",
126 source,
127 target,
128 e
129 );
130 None
131 }
132 }
133 }
134
135 #[cfg(feature = "crs-transform")]
142 fn parse_crs_string(s: &str) -> core::result::Result<oxigdal_proj::Crs, oxigdal_proj::Error> {
143 let upper = s.trim().to_uppercase();
144 if let Some(code_str) = upper.strip_prefix("EPSG:") {
145 let code = code_str
146 .trim()
147 .parse::<u32>()
148 .map_err(|_| oxigdal_proj::Error::invalid_epsg_code(0))?;
149 oxigdal_proj::Crs::from_epsg(code)
150 } else if upper.starts_with("+PROJ=") || upper.starts_with("+proj=") {
151 oxigdal_proj::Crs::from_proj(s)
152 } else {
153 oxigdal_proj::Crs::from_wkt(s)
154 }
155 }
156
157 pub fn from_common(source: CommonCrs, target: CommonCrs) -> Result<Self> {
159 Self::new(source.epsg_code(), target.epsg_code())
160 }
161
162 pub fn transform_coordinate(&self, coord: &Coordinate) -> Result<Coordinate> {
176 if self.source_crs == self.target_crs {
178 return Ok(*coord);
179 }
180
181 #[cfg(feature = "crs-transform")]
186 if let Some(ref t) = self.proj_transformer {
187 return Self::transform_via_proj(t, coord);
188 }
189
190 if self.source_crs == "EPSG:4326" && self.target_crs == "EPSG:3857" {
194 return self.wgs84_to_web_mercator(coord);
195 }
196
197 if self.source_crs == "EPSG:3857" && self.target_crs == "EPSG:4326" {
199 return self.web_mercator_to_wgs84(coord);
200 }
201
202 Err(AlgorithmError::UnsupportedOperation {
204 operation: format!(
205 "Coordinate transformation from {} to {} (requires crs-transform feature)",
206 self.source_crs, self.target_crs
207 ),
208 })
209 }
210
211 #[cfg(feature = "crs-transform")]
219 fn transform_via_proj(t: &oxigdal_proj::Transformer, coord: &Coordinate) -> Result<Coordinate> {
220 let proj_coord = oxigdal_proj::Coordinate::new(coord.x, coord.y);
221 match t.transform(&proj_coord) {
222 Ok(out) => Ok(Coordinate::new_2d(out.x, out.y)),
223 Err(oxigdal_proj::Error::OutOfAreaOfUse { lon, lat, .. }) => {
224 tracing::debug!(
225 "oxigdal-proj: point ({}, {}) is outside area of use — returning unchanged",
226 lon,
227 lat
228 );
229 Ok(*coord)
230 }
231 Err(e) => Err(AlgorithmError::UnsupportedOperation {
232 operation: format!("Coordinate transformation failed: {}", e),
233 }),
234 }
235 }
236
237 pub fn transform_coordinates(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
239 coords
240 .iter()
241 .map(|c| self.transform_coordinate(c))
242 .collect()
243 }
244
245 pub fn transform_point(&self, point: &Point) -> Result<Point> {
247 let transformed = self.transform_coordinate(&point.coord)?;
248 Ok(Point::from_coord(transformed))
249 }
250
251 pub fn transform_linestring(&self, linestring: &LineString) -> Result<LineString> {
253 let coords = self.transform_coordinates(&linestring.coords)?;
254 LineString::new(coords).map_err(|e| AlgorithmError::GeometryError {
255 message: format!("Failed to create transformed linestring: {}", e),
256 })
257 }
258
259 pub fn transform_polygon(&self, polygon: &Polygon) -> Result<Polygon> {
261 let exterior_coords = self.transform_coordinates(&polygon.exterior.coords)?;
262 let exterior =
263 LineString::new(exterior_coords).map_err(|e| AlgorithmError::GeometryError {
264 message: format!("Failed to create transformed exterior ring: {}", e),
265 })?;
266
267 let mut interiors = Vec::new();
268 for hole in &polygon.interiors {
269 let hole_coords = self.transform_coordinates(&hole.coords)?;
270 let hole_ring =
271 LineString::new(hole_coords).map_err(|e| AlgorithmError::GeometryError {
272 message: format!("Failed to create transformed interior ring: {}", e),
273 })?;
274 interiors.push(hole_ring);
275 }
276
277 Polygon::new(exterior, interiors).map_err(|e| AlgorithmError::GeometryError {
278 message: format!("Failed to create transformed polygon: {}", e),
279 })
280 }
281
282 pub fn transform_geometry(&self, geometry: &Geometry) -> Result<Geometry> {
284 match geometry {
285 Geometry::Point(p) => Ok(Geometry::Point(self.transform_point(p)?)),
286 Geometry::LineString(ls) => Ok(Geometry::LineString(self.transform_linestring(ls)?)),
287 Geometry::Polygon(poly) => Ok(Geometry::Polygon(self.transform_polygon(poly)?)),
288 Geometry::MultiPoint(mp) => {
289 let mut points = Vec::new();
290 for point in &mp.points {
291 points.push(self.transform_point(point)?);
292 }
293 Ok(Geometry::MultiPoint(MultiPoint { points }))
294 }
295 Geometry::MultiLineString(mls) => {
296 let mut line_strings = Vec::new();
297 for ls in &mls.line_strings {
298 line_strings.push(self.transform_linestring(ls)?);
299 }
300 Ok(Geometry::MultiLineString(MultiLineString { line_strings }))
301 }
302 Geometry::MultiPolygon(mp) => {
303 let mut polygons = Vec::new();
304 for poly in &mp.polygons {
305 polygons.push(self.transform_polygon(poly)?);
306 }
307 Ok(Geometry::MultiPolygon(MultiPolygon { polygons }))
308 }
309 Geometry::GeometryCollection(gc) => {
310 let mut geometries = Vec::new();
311 for geom in &gc.geometries {
312 geometries.push(self.transform_geometry(geom)?);
313 }
314 Ok(Geometry::GeometryCollection(GeometryCollection {
315 geometries,
316 }))
317 }
318 }
319 }
320
321 fn wgs84_to_web_mercator(&self, coord: &Coordinate) -> Result<Coordinate> {
323 const EARTH_RADIUS: f64 = 6_378_137.0;
324
325 if !(-90.0..=90.0).contains(&coord.y) {
327 return Err(AlgorithmError::InvalidParameter {
328 parameter: "latitude",
329 message: format!("Latitude {} is out of range [-90, 90]", coord.y),
330 });
331 }
332
333 if coord.y.abs() > 85.0511 {
335 return Err(AlgorithmError::InvalidParameter {
336 parameter: "latitude",
337 message: format!(
338 "Latitude {} is too close to poles for Web Mercator (max ±85.0511°)",
339 coord.y
340 ),
341 });
342 }
343
344 let lon_rad = coord.x.to_radians();
345 let lat_rad = coord.y.to_radians();
346
347 let x = EARTH_RADIUS * lon_rad;
348 let y = EARTH_RADIUS * ((std::f64::consts::PI / 4.0 + lat_rad / 2.0).tan().ln());
349
350 Ok(Coordinate::new_2d(x, y))
351 }
352
353 fn web_mercator_to_wgs84(&self, coord: &Coordinate) -> Result<Coordinate> {
355 const EARTH_RADIUS: f64 = 6_378_137.0;
356
357 let lon = (coord.x / EARTH_RADIUS).to_degrees();
358 let lat =
359 (2.0 * (coord.y / EARTH_RADIUS).exp().atan() - std::f64::consts::PI / 2.0).to_degrees();
360
361 let lon = lon.clamp(-180.0, 180.0);
363 let lat = lat.clamp(-90.0, 90.0);
364
365 Ok(Coordinate::new_2d(lon, lat))
366 }
367}
368
369pub fn transform_point(point: &Point, source_crs: &str, target_crs: &str) -> Result<Point> {
385 let transformer = CrsTransformer::new(source_crs, target_crs)?;
386 transformer.transform_point(point)
387}
388
389pub fn transform_linestring(
391 linestring: &LineString,
392 source_crs: &str,
393 target_crs: &str,
394) -> Result<LineString> {
395 let transformer = CrsTransformer::new(source_crs, target_crs)?;
396 transformer.transform_linestring(linestring)
397}
398
399pub fn transform_polygon(polygon: &Polygon, source_crs: &str, target_crs: &str) -> Result<Polygon> {
401 let transformer = CrsTransformer::new(source_crs, target_crs)?;
402 transformer.transform_polygon(polygon)
403}
404
405pub fn transform_geometry(
407 geometry: &Geometry,
408 source_crs: &str,
409 target_crs: &str,
410) -> Result<Geometry> {
411 let transformer = CrsTransformer::new(source_crs, target_crs)?;
412 transformer.transform_geometry(geometry)
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn test_common_crs_epsg_codes() {
421 assert_eq!(CommonCrs::Wgs84.epsg_code(), "EPSG:4326");
422 assert_eq!(CommonCrs::WebMercator.epsg_code(), "EPSG:3857");
423 assert_eq!(
424 CommonCrs::Utm {
425 zone: 10,
426 north: true
427 }
428 .epsg_code(),
429 "EPSG:32610"
430 );
431 assert_eq!(
432 CommonCrs::Utm {
433 zone: 33,
434 north: false
435 }
436 .epsg_code(),
437 "EPSG:32733"
438 );
439 }
440
441 #[test]
442 fn test_transformer_creation() {
443 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
444 assert!(transformer.is_ok());
445
446 let empty = CrsTransformer::new("", "EPSG:3857");
447 assert!(empty.is_err());
448 }
449
450 #[test]
451 fn test_identity_transformation() {
452 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:4326");
453 assert!(transformer.is_ok());
454
455 if let Ok(t) = transformer {
456 let coord = Coordinate::new_2d(10.0, 20.0);
457 let result = t.transform_coordinate(&coord);
458 assert!(result.is_ok());
459
460 if let Ok(transformed) = result {
461 assert!((transformed.x - 10.0).abs() < f64::EPSILON);
462 assert!((transformed.y - 20.0).abs() < f64::EPSILON);
463 }
464 }
465 }
466
467 #[test]
468 fn test_wgs84_to_web_mercator() {
469 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
470 assert!(transformer.is_ok());
471
472 if let Ok(t) = transformer {
473 let origin = Coordinate::new_2d(0.0, 0.0);
475 let result = t.transform_coordinate(&origin);
476 assert!(result.is_ok());
477
478 if let Ok(transformed) = result {
479 assert!(transformed.x.abs() < 1.0);
480 assert!(transformed.y.abs() < 1.0);
481 }
482
483 let sf = Coordinate::new_2d(-122.4194, 37.7749);
485 let result = t.transform_coordinate(&sf);
486 assert!(result.is_ok());
487
488 if let Ok(transformed) = result {
489 assert!(transformed.x < 0.0);
491 assert!(transformed.y > 0.0);
493 }
494 }
495 }
496
497 #[test]
498 fn test_web_mercator_to_wgs84() {
499 let transformer = CrsTransformer::new("EPSG:3857", "EPSG:4326");
500 assert!(transformer.is_ok());
501
502 if let Ok(t) = transformer {
503 let origin = Coordinate::new_2d(0.0, 0.0);
505 let result = t.transform_coordinate(&origin);
506 assert!(result.is_ok());
507
508 if let Ok(transformed) = result {
509 assert!(transformed.x.abs() < 1e-6);
510 assert!(transformed.y.abs() < 1e-6);
511 }
512 }
513 }
514
515 #[test]
516 fn test_round_trip_transformation() {
517 let to_merc = CrsTransformer::new("EPSG:4326", "EPSG:3857");
518 let to_wgs = CrsTransformer::new("EPSG:3857", "EPSG:4326");
519
520 assert!(to_merc.is_ok());
521 assert!(to_wgs.is_ok());
522
523 if let (Ok(t1), Ok(t2)) = (to_merc, to_wgs) {
524 let original = Coordinate::new_2d(-122.4194, 37.7749);
525
526 let merc = t1.transform_coordinate(&original);
527 assert!(merc.is_ok());
528
529 if let Ok(m) = merc {
530 let back = t2.transform_coordinate(&m);
531 assert!(back.is_ok());
532
533 if let Ok(b) = back {
534 assert!((b.x - original.x).abs() < 1e-6);
536 assert!((b.y - original.y).abs() < 1e-6);
537 }
538 }
539 }
540 }
541
542 #[test]
543 fn test_transform_point() {
544 let point = Point::new(-122.4194, 37.7749);
545 let result = transform_point(&point, "EPSG:4326", "EPSG:3857");
546 assert!(result.is_ok());
547
548 if let Ok(transformed) = result {
549 assert!(transformed.coord.x < 0.0);
550 assert!(transformed.coord.y > 0.0);
551 }
552 }
553
554 #[test]
555 fn test_transform_linestring() {
556 let coords = vec![
557 Coordinate::new_2d(0.0, 0.0),
558 Coordinate::new_2d(1.0, 1.0),
559 Coordinate::new_2d(2.0, 2.0),
560 ];
561 let linestring = LineString::new(coords);
562 assert!(linestring.is_ok());
563
564 if let Ok(ls) = linestring {
565 let result = transform_linestring(&ls, "EPSG:4326", "EPSG:3857");
566 assert!(result.is_ok());
567
568 if let Ok(transformed) = result {
569 assert_eq!(transformed.coords.len(), 3);
570 }
571 }
572 }
573
574 #[test]
575 fn test_transform_polygon() {
576 let coords = vec![
577 Coordinate::new_2d(0.0, 0.0),
578 Coordinate::new_2d(4.0, 0.0),
579 Coordinate::new_2d(4.0, 4.0),
580 Coordinate::new_2d(0.0, 4.0),
581 Coordinate::new_2d(0.0, 0.0),
582 ];
583 let exterior = LineString::new(coords);
584 assert!(exterior.is_ok());
585
586 if let Ok(ext) = exterior {
587 let polygon = Polygon::new(ext, vec![]);
588 assert!(polygon.is_ok());
589
590 if let Ok(poly) = polygon {
591 let result = transform_polygon(&poly, "EPSG:4326", "EPSG:3857");
592 assert!(result.is_ok());
593
594 if let Ok(transformed) = result {
595 assert_eq!(transformed.exterior.coords.len(), 5);
596 }
597 }
598 }
599 }
600
601 #[test]
602 fn test_invalid_latitude() {
603 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
604 assert!(transformer.is_ok());
605
606 if let Ok(t) = transformer {
607 let invalid = Coordinate::new_2d(0.0, 95.0);
610 let result = t.transform_coordinate(&invalid);
611 assert!(result.is_err(), "lat=95 must be rejected");
612
613 #[cfg(not(feature = "crs-transform"))]
619 {
620 let near_pole = Coordinate::new_2d(0.0, 89.0);
621 let result = t.transform_coordinate(&near_pole);
622 assert!(
623 result.is_err(),
624 "lat=89 must be rejected by hardcoded fallback"
625 );
626 }
627 }
628 }
629
630 #[test]
631 fn test_batch_transformation() {
632 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
633 assert!(transformer.is_ok());
634
635 if let Ok(t) = transformer {
636 let coords = vec![
637 Coordinate::new_2d(0.0, 0.0),
638 Coordinate::new_2d(1.0, 1.0),
639 Coordinate::new_2d(-1.0, -1.0),
640 ];
641
642 let result = t.transform_coordinates(&coords);
643 assert!(result.is_ok());
644
645 if let Ok(transformed) = result {
646 assert_eq!(transformed.len(), 3);
647 }
648 }
649 }
650
651 #[test]
652 fn test_from_common_crs() {
653 let transformer = CrsTransformer::from_common(CommonCrs::Wgs84, CommonCrs::WebMercator);
654 assert!(transformer.is_ok());
655
656 if let Ok(t) = transformer {
657 assert_eq!(t.source_crs, "EPSG:4326");
658 assert_eq!(t.target_crs, "EPSG:3857");
659 }
660 }
661
662 #[test]
669 fn test_crs_transformer_wgs84_identity_passthrough() {
670 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:4326")
671 .expect("identity transformer must construct");
672
673 let input = Coordinate::new_2d(13.4050, 52.5200); let output = transformer
675 .transform_coordinate(&input)
676 .expect("identity transform must succeed");
677
678 assert!(
679 (output.x - input.x).abs() < f64::EPSILON,
680 "x must be unchanged: {} != {}",
681 output.x,
682 input.x
683 );
684 assert!(
685 (output.y - input.y).abs() < f64::EPSILON,
686 "y must be unchanged: {} != {}",
687 output.y,
688 input.y
689 );
690 }
691
692 #[test]
695 fn test_crs_transformer_polygon_preserves_vertex_count() {
696 let coords = vec![
697 Coordinate::new_2d(-10.0, -10.0),
698 Coordinate::new_2d(10.0, -10.0),
699 Coordinate::new_2d(10.0, 10.0),
700 Coordinate::new_2d(-10.0, 10.0),
701 Coordinate::new_2d(-10.0, -10.0), ];
703 let exterior = LineString::new(coords).expect("linestring must construct");
704 let polygon = Polygon::new(exterior, vec![]).expect("polygon must construct");
705
706 let transformer =
708 CrsTransformer::new("EPSG:4326", "EPSG:3857").expect("transformer must construct");
709 let result = transformer
710 .transform_polygon(&polygon)
711 .expect("polygon transform must succeed");
712
713 assert_eq!(
714 result.exterior.coords.len(),
715 5,
716 "transformed polygon must retain 5 vertices"
717 );
718 }
719
720 #[test]
724 fn test_crs_transformer_unknown_epsg_falls_back_gracefully() {
725 let result = CrsTransformer::new("EPSG:4326", "EPSG:32637");
729 assert!(
731 result.is_ok(),
732 "CrsTransformer::new must not fail for any non-empty CRS string"
733 );
734
735 let transformer = result.expect("already asserted Ok above");
736 let coord = Coordinate::new_2d(37.0, 55.0);
737 let transform_result = transformer.transform_coordinate(&coord);
738
739 if let Err(ref e) = transform_result {
742 assert!(
743 matches!(e, AlgorithmError::UnsupportedOperation { .. }),
744 "unexpected error variant: {:?}",
745 e
746 );
747 }
748 if let Ok(ref c) = transform_result {
750 assert!(c.x.is_finite() && c.y.is_finite(), "output must be finite");
751 }
752 }
753
754 #[test]
757 fn test_crs_transformer_wgs84_to_webmercator_known_point() {
758 let transformer =
759 CrsTransformer::new("EPSG:4326", "EPSG:3857").expect("transformer must construct");
760
761 let origin = Coordinate::new_2d(0.0, 0.0);
762 let result = transformer
763 .transform_coordinate(&origin)
764 .expect("transform of origin must succeed");
765
766 assert!(
767 result.x.abs() < 1.0,
768 "Web Mercator X at lon=0 must be ~0, got {}",
769 result.x
770 );
771 assert!(
772 result.y.abs() < 1.0,
773 "Web Mercator Y at lat=0 must be ~0, got {}",
774 result.y
775 );
776 }
777}