oxigdal_algorithms/vector/
transform.rs1use 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 {
66 source_crs: String,
67 target_crs: String,
68}
69
70impl CrsTransformer {
71 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 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 pub fn from_common(source: CommonCrs, target: CommonCrs) -> Result<Self> {
105 Self::new(source.epsg_code(), target.epsg_code())
106 }
107
108 pub fn transform_coordinate(&self, coord: &Coordinate) -> Result<Coordinate> {
122 if self.source_crs == self.target_crs {
124 return Ok(*coord);
125 }
126
127 if self.source_crs == "EPSG:4326" && self.target_crs == "EPSG:3857" {
129 return self.wgs84_to_web_mercator(coord);
130 }
131
132 if self.source_crs == "EPSG:3857" && self.target_crs == "EPSG:4326" {
134 return self.web_mercator_to_wgs84(coord);
135 }
136
137 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 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 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 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 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 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 fn wgs84_to_web_mercator(&self, coord: &Coordinate) -> Result<Coordinate> {
233 const EARTH_RADIUS: f64 = 6_378_137.0;
234
235 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 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 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 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
279pub 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
299pub 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
309pub 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
315pub 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 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 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 assert!(transformed.x < 0.0);
401 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 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 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 let invalid = Coordinate::new_2d(0.0, 95.0);
519 let result = t.transform_coordinate(&invalid);
520 assert!(result.is_err());
521
522 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}