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
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
74pub enum OutOfAreaPolicy {
75 #[default]
80 PassThrough,
81 Error,
83}
84
85pub struct CrsTransformer {
99 source_crs: String,
100 target_crs: String,
101 out_of_area_policy: OutOfAreaPolicy,
102 #[cfg(feature = "crs-transform")]
103 proj_transformer: Option<oxigdal_proj::Transformer>,
104}
105
106impl CrsTransformer {
107 pub fn new(source_crs: impl Into<String>, target_crs: impl Into<String>) -> Result<Self> {
122 let source = source_crs.into();
123 let target = target_crs.into();
124
125 if source.is_empty() || target.is_empty() {
127 return Err(AlgorithmError::InvalidParameter {
128 parameter: "crs",
129 message: "CRS definition cannot be empty".to_string(),
130 });
131 }
132
133 #[cfg(feature = "crs-transform")]
136 let proj_transformer = Self::build_proj_transformer(&source, &target, false);
137
138 Ok(Self {
139 source_crs: source,
140 target_crs: target,
141 out_of_area_policy: OutOfAreaPolicy::default(),
142 #[cfg(feature = "crs-transform")]
143 proj_transformer,
144 })
145 }
146
147 pub fn with_out_of_area_policy(mut self, policy: OutOfAreaPolicy) -> Self {
155 self.out_of_area_policy = policy;
156 #[cfg(feature = "crs-transform")]
157 {
158 let strict = matches!(policy, OutOfAreaPolicy::Error);
159 self.proj_transformer =
160 Self::build_proj_transformer(&self.source_crs, &self.target_crs, strict);
161 }
162 self
163 }
164
165 pub fn out_of_area_policy(&self) -> OutOfAreaPolicy {
167 self.out_of_area_policy
168 }
169
170 #[cfg(feature = "crs-transform")]
176 fn build_proj_transformer(
177 source: &str,
178 target: &str,
179 strict: bool,
180 ) -> Option<oxigdal_proj::Transformer> {
181 let src_crs = Self::parse_crs_string(source).ok()?;
182 let tgt_crs = Self::parse_crs_string(target).ok()?;
183 match oxigdal_proj::Transformer::new(src_crs, tgt_crs) {
184 Ok(t) => Some(t.with_strict(strict)),
185 Err(e) => {
186 tracing::debug!(
187 "oxigdal-proj: could not initialise transformer {} → {}: {}",
188 source,
189 target,
190 e
191 );
192 None
193 }
194 }
195 }
196
197 #[cfg(feature = "crs-transform")]
204 fn parse_crs_string(s: &str) -> core::result::Result<oxigdal_proj::Crs, oxigdal_proj::Error> {
205 let upper = s.trim().to_uppercase();
206 if let Some(code_str) = upper.strip_prefix("EPSG:") {
207 let code = code_str
208 .trim()
209 .parse::<u32>()
210 .map_err(|_| oxigdal_proj::Error::invalid_epsg_code(0))?;
211 oxigdal_proj::Crs::from_epsg(code)
212 } else if upper.starts_with("+PROJ=") || upper.starts_with("+proj=") {
213 oxigdal_proj::Crs::from_proj(s)
214 } else {
215 oxigdal_proj::Crs::from_wkt(s)
216 }
217 }
218
219 pub fn from_common(source: CommonCrs, target: CommonCrs) -> Result<Self> {
221 Self::new(source.epsg_code(), target.epsg_code())
222 }
223
224 pub fn transform_coordinate(&self, coord: &Coordinate) -> Result<Coordinate> {
250 self.transform_coordinate_checked(coord).map(|(c, _)| c)
251 }
252
253 fn transform_coordinate_checked(&self, coord: &Coordinate) -> Result<(Coordinate, bool)> {
261 if self.source_crs == self.target_crs {
263 return Ok((*coord, false));
264 }
265
266 #[cfg(feature = "crs-transform")]
271 if let Some(ref t) = self.proj_transformer {
272 return self.transform_via_proj(t, coord);
273 }
274
275 if self.source_crs == "EPSG:4326" && self.target_crs == "EPSG:3857" {
279 return self.wgs84_to_web_mercator(coord).map(|c| (c, false));
280 }
281
282 if self.source_crs == "EPSG:3857" && self.target_crs == "EPSG:4326" {
284 return self.web_mercator_to_wgs84(coord).map(|c| (c, false));
285 }
286
287 Err(AlgorithmError::UnsupportedOperation {
289 operation: format!(
290 "Coordinate transformation from {} to {} (requires crs-transform feature)",
291 self.source_crs, self.target_crs
292 ),
293 })
294 }
295
296 #[cfg(feature = "crs-transform")]
305 fn transform_via_proj(
306 &self,
307 t: &oxigdal_proj::Transformer,
308 coord: &Coordinate,
309 ) -> Result<(Coordinate, bool)> {
310 let proj_coord = oxigdal_proj::Coordinate::new(coord.x, coord.y);
311 match t.transform(&proj_coord) {
312 Ok(out) => Ok((Coordinate::new_2d(out.x, out.y), false)),
313 Err(oxigdal_proj::Error::OutOfAreaOfUse { lon, lat, .. }) => {
314 match self.out_of_area_policy {
315 OutOfAreaPolicy::Error => Err(AlgorithmError::ComputationError(format!(
316 "coordinate ({lon}, {lat}) is outside the source CRS's area of use"
317 ))),
318 OutOfAreaPolicy::PassThrough => {
319 tracing::debug!(
320 "oxigdal-proj: point ({}, {}) is outside area of use — returning unchanged",
321 lon,
322 lat
323 );
324 Ok((*coord, true))
325 }
326 }
327 }
328 Err(e) => Err(AlgorithmError::UnsupportedOperation {
329 operation: format!("Coordinate transformation failed: {}", e),
330 }),
331 }
332 }
333
334 pub fn transform_coordinates(&self, coords: &[Coordinate]) -> Result<Vec<Coordinate>> {
336 coords
337 .iter()
338 .map(|c| self.transform_coordinate(c))
339 .collect()
340 }
341
342 pub fn transform_coordinates_reporting(
356 &self,
357 coords: &[Coordinate],
358 ) -> Result<(Vec<Coordinate>, Vec<usize>)> {
359 let mut out = Vec::with_capacity(coords.len());
360 let mut out_of_area = Vec::new();
361 for (i, c) in coords.iter().enumerate() {
362 let (transformed, was_out_of_area) = self.transform_coordinate_checked(c)?;
363 out.push(transformed);
364 if was_out_of_area {
365 out_of_area.push(i);
366 }
367 }
368 Ok((out, out_of_area))
369 }
370
371 pub fn transform_point(&self, point: &Point) -> Result<Point> {
373 let transformed = self.transform_coordinate(&point.coord)?;
374 Ok(Point::from_coord(transformed))
375 }
376
377 pub fn transform_linestring(&self, linestring: &LineString) -> Result<LineString> {
379 let coords = self.transform_coordinates(&linestring.coords)?;
380 LineString::new(coords).map_err(|e| AlgorithmError::GeometryError {
381 message: format!("Failed to create transformed linestring: {}", e),
382 })
383 }
384
385 pub fn transform_polygon(&self, polygon: &Polygon) -> Result<Polygon> {
387 let exterior_coords = self.transform_coordinates(&polygon.exterior.coords)?;
388 let exterior =
389 LineString::new(exterior_coords).map_err(|e| AlgorithmError::GeometryError {
390 message: format!("Failed to create transformed exterior ring: {}", e),
391 })?;
392
393 let mut interiors = Vec::new();
394 for hole in &polygon.interiors {
395 let hole_coords = self.transform_coordinates(&hole.coords)?;
396 let hole_ring =
397 LineString::new(hole_coords).map_err(|e| AlgorithmError::GeometryError {
398 message: format!("Failed to create transformed interior ring: {}", e),
399 })?;
400 interiors.push(hole_ring);
401 }
402
403 Polygon::new(exterior, interiors).map_err(|e| AlgorithmError::GeometryError {
404 message: format!("Failed to create transformed polygon: {}", e),
405 })
406 }
407
408 pub fn transform_geometry(&self, geometry: &Geometry) -> Result<Geometry> {
410 match geometry {
411 Geometry::Point(p) => Ok(Geometry::Point(self.transform_point(p)?)),
412 Geometry::LineString(ls) => Ok(Geometry::LineString(self.transform_linestring(ls)?)),
413 Geometry::Polygon(poly) => Ok(Geometry::Polygon(self.transform_polygon(poly)?)),
414 Geometry::MultiPoint(mp) => {
415 let mut points = Vec::new();
416 for point in &mp.points {
417 points.push(self.transform_point(point)?);
418 }
419 Ok(Geometry::MultiPoint(MultiPoint { points }))
420 }
421 Geometry::MultiLineString(mls) => {
422 let mut line_strings = Vec::new();
423 for ls in &mls.line_strings {
424 line_strings.push(self.transform_linestring(ls)?);
425 }
426 Ok(Geometry::MultiLineString(MultiLineString { line_strings }))
427 }
428 Geometry::MultiPolygon(mp) => {
429 let mut polygons = Vec::new();
430 for poly in &mp.polygons {
431 polygons.push(self.transform_polygon(poly)?);
432 }
433 Ok(Geometry::MultiPolygon(MultiPolygon { polygons }))
434 }
435 Geometry::GeometryCollection(gc) => {
436 let mut geometries = Vec::new();
437 for geom in &gc.geometries {
438 geometries.push(self.transform_geometry(geom)?);
439 }
440 Ok(Geometry::GeometryCollection(GeometryCollection {
441 geometries,
442 }))
443 }
444 }
445 }
446
447 fn wgs84_to_web_mercator(&self, coord: &Coordinate) -> Result<Coordinate> {
449 const EARTH_RADIUS: f64 = 6_378_137.0;
450
451 if !(-90.0..=90.0).contains(&coord.y) {
453 return Err(AlgorithmError::InvalidParameter {
454 parameter: "latitude",
455 message: format!("Latitude {} is out of range [-90, 90]", coord.y),
456 });
457 }
458
459 if coord.y.abs() > 85.0511 {
461 return Err(AlgorithmError::InvalidParameter {
462 parameter: "latitude",
463 message: format!(
464 "Latitude {} is too close to poles for Web Mercator (max ±85.0511°)",
465 coord.y
466 ),
467 });
468 }
469
470 let lon_rad = coord.x.to_radians();
471 let lat_rad = coord.y.to_radians();
472
473 let x = EARTH_RADIUS * lon_rad;
474 let y = EARTH_RADIUS * ((std::f64::consts::PI / 4.0 + lat_rad / 2.0).tan().ln());
475
476 Ok(Coordinate::new_2d(x, y))
477 }
478
479 fn web_mercator_to_wgs84(&self, coord: &Coordinate) -> Result<Coordinate> {
481 const EARTH_RADIUS: f64 = 6_378_137.0;
482
483 let lon = (coord.x / EARTH_RADIUS).to_degrees();
484 let lat =
485 (2.0 * (coord.y / EARTH_RADIUS).exp().atan() - std::f64::consts::PI / 2.0).to_degrees();
486
487 let lon = lon.clamp(-180.0, 180.0);
489 let lat = lat.clamp(-90.0, 90.0);
490
491 Ok(Coordinate::new_2d(lon, lat))
492 }
493}
494
495pub fn transform_point(point: &Point, source_crs: &str, target_crs: &str) -> Result<Point> {
511 let transformer = CrsTransformer::new(source_crs, target_crs)?;
512 transformer.transform_point(point)
513}
514
515pub fn transform_linestring(
517 linestring: &LineString,
518 source_crs: &str,
519 target_crs: &str,
520) -> Result<LineString> {
521 let transformer = CrsTransformer::new(source_crs, target_crs)?;
522 transformer.transform_linestring(linestring)
523}
524
525pub fn transform_polygon(polygon: &Polygon, source_crs: &str, target_crs: &str) -> Result<Polygon> {
527 let transformer = CrsTransformer::new(source_crs, target_crs)?;
528 transformer.transform_polygon(polygon)
529}
530
531pub fn transform_geometry(
533 geometry: &Geometry,
534 source_crs: &str,
535 target_crs: &str,
536) -> Result<Geometry> {
537 let transformer = CrsTransformer::new(source_crs, target_crs)?;
538 transformer.transform_geometry(geometry)
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 #[test]
546 fn test_common_crs_epsg_codes() {
547 assert_eq!(CommonCrs::Wgs84.epsg_code(), "EPSG:4326");
548 assert_eq!(CommonCrs::WebMercator.epsg_code(), "EPSG:3857");
549 assert_eq!(
550 CommonCrs::Utm {
551 zone: 10,
552 north: true
553 }
554 .epsg_code(),
555 "EPSG:32610"
556 );
557 assert_eq!(
558 CommonCrs::Utm {
559 zone: 33,
560 north: false
561 }
562 .epsg_code(),
563 "EPSG:32733"
564 );
565 }
566
567 #[test]
568 fn test_transformer_creation() {
569 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
570 assert!(transformer.is_ok());
571
572 let empty = CrsTransformer::new("", "EPSG:3857");
573 assert!(empty.is_err());
574 }
575
576 #[test]
577 fn test_identity_transformation() {
578 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:4326");
579 assert!(transformer.is_ok());
580
581 if let Ok(t) = transformer {
582 let coord = Coordinate::new_2d(10.0, 20.0);
583 let result = t.transform_coordinate(&coord);
584 assert!(result.is_ok());
585
586 if let Ok(transformed) = result {
587 assert!((transformed.x - 10.0).abs() < f64::EPSILON);
588 assert!((transformed.y - 20.0).abs() < f64::EPSILON);
589 }
590 }
591 }
592
593 #[test]
594 fn test_wgs84_to_web_mercator() {
595 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
596 assert!(transformer.is_ok());
597
598 if let Ok(t) = transformer {
599 let origin = Coordinate::new_2d(0.0, 0.0);
601 let result = t.transform_coordinate(&origin);
602 assert!(result.is_ok());
603
604 if let Ok(transformed) = result {
605 assert!(transformed.x.abs() < 1.0);
606 assert!(transformed.y.abs() < 1.0);
607 }
608
609 let sf = Coordinate::new_2d(-122.4194, 37.7749);
611 let result = t.transform_coordinate(&sf);
612 assert!(result.is_ok());
613
614 if let Ok(transformed) = result {
615 assert!(transformed.x < 0.0);
617 assert!(transformed.y > 0.0);
619 }
620 }
621 }
622
623 #[test]
624 fn test_web_mercator_to_wgs84() {
625 let transformer = CrsTransformer::new("EPSG:3857", "EPSG:4326");
626 assert!(transformer.is_ok());
627
628 if let Ok(t) = transformer {
629 let origin = Coordinate::new_2d(0.0, 0.0);
631 let result = t.transform_coordinate(&origin);
632 assert!(result.is_ok());
633
634 if let Ok(transformed) = result {
635 assert!(transformed.x.abs() < 1e-6);
636 assert!(transformed.y.abs() < 1e-6);
637 }
638 }
639 }
640
641 #[test]
642 fn test_round_trip_transformation() {
643 let to_merc = CrsTransformer::new("EPSG:4326", "EPSG:3857");
644 let to_wgs = CrsTransformer::new("EPSG:3857", "EPSG:4326");
645
646 assert!(to_merc.is_ok());
647 assert!(to_wgs.is_ok());
648
649 if let (Ok(t1), Ok(t2)) = (to_merc, to_wgs) {
650 let original = Coordinate::new_2d(-122.4194, 37.7749);
651
652 let merc = t1.transform_coordinate(&original);
653 assert!(merc.is_ok());
654
655 if let Ok(m) = merc {
656 let back = t2.transform_coordinate(&m);
657 assert!(back.is_ok());
658
659 if let Ok(b) = back {
660 assert!((b.x - original.x).abs() < 1e-6);
662 assert!((b.y - original.y).abs() < 1e-6);
663 }
664 }
665 }
666 }
667
668 #[test]
669 fn test_transform_point() {
670 let point = Point::new(-122.4194, 37.7749);
671 let result = transform_point(&point, "EPSG:4326", "EPSG:3857");
672 assert!(result.is_ok());
673
674 if let Ok(transformed) = result {
675 assert!(transformed.coord.x < 0.0);
676 assert!(transformed.coord.y > 0.0);
677 }
678 }
679
680 #[test]
681 fn test_transform_linestring() {
682 let coords = vec![
683 Coordinate::new_2d(0.0, 0.0),
684 Coordinate::new_2d(1.0, 1.0),
685 Coordinate::new_2d(2.0, 2.0),
686 ];
687 let linestring = LineString::new(coords);
688 assert!(linestring.is_ok());
689
690 if let Ok(ls) = linestring {
691 let result = transform_linestring(&ls, "EPSG:4326", "EPSG:3857");
692 assert!(result.is_ok());
693
694 if let Ok(transformed) = result {
695 assert_eq!(transformed.coords.len(), 3);
696 }
697 }
698 }
699
700 #[test]
701 fn test_transform_polygon() {
702 let coords = vec![
703 Coordinate::new_2d(0.0, 0.0),
704 Coordinate::new_2d(4.0, 0.0),
705 Coordinate::new_2d(4.0, 4.0),
706 Coordinate::new_2d(0.0, 4.0),
707 Coordinate::new_2d(0.0, 0.0),
708 ];
709 let exterior = LineString::new(coords);
710 assert!(exterior.is_ok());
711
712 if let Ok(ext) = exterior {
713 let polygon = Polygon::new(ext, vec![]);
714 assert!(polygon.is_ok());
715
716 if let Ok(poly) = polygon {
717 let result = transform_polygon(&poly, "EPSG:4326", "EPSG:3857");
718 assert!(result.is_ok());
719
720 if let Ok(transformed) = result {
721 assert_eq!(transformed.exterior.coords.len(), 5);
722 }
723 }
724 }
725 }
726
727 #[test]
728 fn test_invalid_latitude() {
729 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
730 assert!(transformer.is_ok());
731
732 if let Ok(t) = transformer {
733 let invalid = Coordinate::new_2d(0.0, 95.0);
736 let result = t.transform_coordinate(&invalid);
737 assert!(result.is_err(), "lat=95 must be rejected");
738
739 #[cfg(not(feature = "crs-transform"))]
745 {
746 let near_pole = Coordinate::new_2d(0.0, 89.0);
747 let result = t.transform_coordinate(&near_pole);
748 assert!(
749 result.is_err(),
750 "lat=89 must be rejected by hardcoded fallback"
751 );
752 }
753 }
754 }
755
756 #[test]
757 fn test_batch_transformation() {
758 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:3857");
759 assert!(transformer.is_ok());
760
761 if let Ok(t) = transformer {
762 let coords = vec![
763 Coordinate::new_2d(0.0, 0.0),
764 Coordinate::new_2d(1.0, 1.0),
765 Coordinate::new_2d(-1.0, -1.0),
766 ];
767
768 let result = t.transform_coordinates(&coords);
769 assert!(result.is_ok());
770
771 if let Ok(transformed) = result {
772 assert_eq!(transformed.len(), 3);
773 }
774 }
775 }
776
777 #[test]
778 fn test_from_common_crs() {
779 let transformer = CrsTransformer::from_common(CommonCrs::Wgs84, CommonCrs::WebMercator);
780 assert!(transformer.is_ok());
781
782 if let Ok(t) = transformer {
783 assert_eq!(t.source_crs, "EPSG:4326");
784 assert_eq!(t.target_crs, "EPSG:3857");
785 }
786 }
787
788 #[test]
795 fn test_crs_transformer_wgs84_identity_passthrough() {
796 let transformer = CrsTransformer::new("EPSG:4326", "EPSG:4326")
797 .expect("identity transformer must construct");
798
799 let input = Coordinate::new_2d(13.4050, 52.5200); let output = transformer
801 .transform_coordinate(&input)
802 .expect("identity transform must succeed");
803
804 assert!(
805 (output.x - input.x).abs() < f64::EPSILON,
806 "x must be unchanged: {} != {}",
807 output.x,
808 input.x
809 );
810 assert!(
811 (output.y - input.y).abs() < f64::EPSILON,
812 "y must be unchanged: {} != {}",
813 output.y,
814 input.y
815 );
816 }
817
818 #[test]
821 fn test_crs_transformer_polygon_preserves_vertex_count() {
822 let coords = vec![
823 Coordinate::new_2d(-10.0, -10.0),
824 Coordinate::new_2d(10.0, -10.0),
825 Coordinate::new_2d(10.0, 10.0),
826 Coordinate::new_2d(-10.0, 10.0),
827 Coordinate::new_2d(-10.0, -10.0), ];
829 let exterior = LineString::new(coords).expect("linestring must construct");
830 let polygon = Polygon::new(exterior, vec![]).expect("polygon must construct");
831
832 let transformer =
834 CrsTransformer::new("EPSG:4326", "EPSG:3857").expect("transformer must construct");
835 let result = transformer
836 .transform_polygon(&polygon)
837 .expect("polygon transform must succeed");
838
839 assert_eq!(
840 result.exterior.coords.len(),
841 5,
842 "transformed polygon must retain 5 vertices"
843 );
844 }
845
846 #[test]
850 fn test_crs_transformer_unknown_epsg_falls_back_gracefully() {
851 let result = CrsTransformer::new("EPSG:4326", "EPSG:32637");
855 assert!(
857 result.is_ok(),
858 "CrsTransformer::new must not fail for any non-empty CRS string"
859 );
860
861 let transformer = result.expect("already asserted Ok above");
862 let coord = Coordinate::new_2d(37.0, 55.0);
863 let transform_result = transformer.transform_coordinate(&coord);
864
865 if let Err(ref e) = transform_result {
868 assert!(
869 matches!(e, AlgorithmError::UnsupportedOperation { .. }),
870 "unexpected error variant: {:?}",
871 e
872 );
873 }
874 if let Ok(ref c) = transform_result {
876 assert!(c.x.is_finite() && c.y.is_finite(), "output must be finite");
877 }
878 }
879
880 #[test]
883 fn test_crs_transformer_wgs84_to_webmercator_known_point() {
884 let transformer =
885 CrsTransformer::new("EPSG:4326", "EPSG:3857").expect("transformer must construct");
886
887 let origin = Coordinate::new_2d(0.0, 0.0);
888 let result = transformer
889 .transform_coordinate(&origin)
890 .expect("transform of origin must succeed");
891
892 assert!(
893 result.x.abs() < 1.0,
894 "Web Mercator X at lon=0 must be ~0, got {}",
895 result.x
896 );
897 assert!(
898 result.y.abs() < 1.0,
899 "Web Mercator Y at lat=0 must be ~0, got {}",
900 result.y
901 );
902 }
903
904 #[test]
905 fn test_out_of_area_policy_default_and_builder() {
906 let t = CrsTransformer::new("EPSG:4326", "EPSG:3857").expect("must construct");
907 assert_eq!(
908 t.out_of_area_policy(),
909 OutOfAreaPolicy::PassThrough,
910 "default policy must be PassThrough for backward compatibility"
911 );
912
913 let t = t.with_out_of_area_policy(OutOfAreaPolicy::Error);
914 assert_eq!(t.out_of_area_policy(), OutOfAreaPolicy::Error);
915
916 let origin = Coordinate::new_2d(0.0, 0.0);
918 let out = t
919 .transform_coordinate(&origin)
920 .expect("origin is in area of use for WGS84→WebMercator");
921 assert!(out.x.abs() < 1.0 && out.y.abs() < 1.0);
922 }
923
924 #[test]
925 fn test_transform_coordinates_reporting_in_area() {
926 let t = CrsTransformer::new("EPSG:4326", "EPSG:3857").expect("must construct");
927 let coords = vec![
928 Coordinate::new_2d(0.0, 0.0),
929 Coordinate::new_2d(10.0, 10.0),
930 Coordinate::new_2d(-20.0, 30.0),
931 ];
932 let (out, out_of_area) = t
933 .transform_coordinates_reporting(&coords)
934 .expect("reporting transform must succeed");
935
936 assert_eq!(out.len(), 3, "all coordinates must be transformed");
937 for c in &out {
938 assert!(c.x.is_finite() && c.y.is_finite());
939 }
940 assert!(
942 out_of_area.is_empty(),
943 "no in-area points should be reported, got {out_of_area:?}"
944 );
945 }
946
947 #[test]
948 fn test_transform_coordinates_reporting_identity() {
949 let t = CrsTransformer::new("EPSG:4326", "EPSG:4326").expect("must construct");
951 let coords = vec![Coordinate::new_2d(200.0, 95.0)]; let (out, out_of_area) = t
953 .transform_coordinates_reporting(&coords)
954 .expect("identity reporting must succeed");
955 assert_eq!(out.len(), 1);
956 assert!(out_of_area.is_empty());
957 assert!((out[0].x - 200.0).abs() < f64::EPSILON);
958 }
959}