diff --git a/Cargo.toml b/Cargo.toml index 71f7770c..60b5d5bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,8 @@ pkg_config = [ "proj-sys/pkg_config" ] network = ["reqwest", "proj-sys/network"] [dev-dependencies] -approx = "0.3" +approx = "0.4" +geo-types = { version = "0.7", features = ["approx"] } [package.metadata.docs.rs] features = [ "proj-sys/nobuild", "network", "geo-types" ] diff --git a/proj-sys/build.rs b/proj-sys/build.rs index d8ac4df1..076f2e47 100644 --- a/proj-sys/build.rs +++ b/proj-sys/build.rs @@ -99,7 +99,7 @@ fn build_from_source() -> Result> config.define("BUILD_PROJSYNC", "OFF"); config.define("ENABLE_CURL", "OFF"); - let enable_tiff = cfg!(feature="network"); + let enable_tiff = cfg!(feature = "network"); if enable_tiff { eprintln!("enabling tiff support"); config.define("ENABLE_TIFF", "ON"); diff --git a/src/geo_types.rs b/src/geo_types.rs index 92570437..60f64756 100644 --- a/src/geo_types.rs +++ b/src/geo_types.rs @@ -1,3 +1,6 @@ +use crate::{Proj, ProjError, Transform}; +use geo_types::Geometry; + ///```rust /// # use approx::assert_relative_eq; /// extern crate proj; @@ -51,3 +54,342 @@ impl crate::Coord for geo_types::Point { Self::new(x, y) } } + +impl Transform for geo_types::Geometry +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + match self { + Geometry::Point(g) => g.transform(proj), + Geometry::Line(g) => g.transform(proj), + Geometry::LineString(g) => g.transform(proj), + Geometry::Polygon(g) => g.transform(proj), + Geometry::MultiPoint(g) => g.transform(proj), + Geometry::MultiLineString(g) => g.transform(proj), + Geometry::MultiPolygon(g) => g.transform(proj), + Geometry::GeometryCollection(g) => g.transform(proj), + Geometry::Rect(g) => g.transform(proj), + Geometry::Triangle(g) => g.transform(proj), + } + } +} + +impl Transform for geo_types::Coordinate +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = *self; + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + *self = proj.convert(*self)?; + Ok(()) + } +} + +impl Transform for geo_types::Point +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = *self; + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + self.0.transform(proj) + } +} + +impl Transform for geo_types::Line +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = *self; + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + self.start.transform(proj)?; + self.end.transform(proj)?; + Ok(()) + } +} + +impl Transform for geo_types::LineString +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + proj.convert_array(&mut self.0)?; + Ok(()) + } +} + +impl Transform for geo_types::Polygon +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + let mut exterior_result = Ok(()); + self.exterior_mut(|exterior| { + exterior_result = exterior.transform(proj); + }); + exterior_result?; + + let mut interiors_result = Ok(()); + self.interiors_mut(|interiors| { + interiors_result = interiors + .iter_mut() + .try_for_each(|interior| interior.transform(proj)) + }); + interiors_result?; + + Ok(()) + } +} + +impl Transform for geo_types::MultiPoint +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + proj.convert_array(&mut self.0)?; + Ok(()) + } +} + +impl Transform for geo_types::MultiLineString +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + for line_string in &mut self.0 { + line_string.transform(proj)?; + } + Ok(()) + } +} + +impl Transform for geo_types::MultiPolygon +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + for polygon in &mut self.0 { + polygon.transform(proj)?; + } + Ok(()) + } +} + +impl Transform for geo_types::GeometryCollection +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = self.clone(); + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + for geometry in &mut self.0 { + geometry.transform(proj)?; + } + Ok(()) + } +} + +impl Transform for geo_types::Rect +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = *self; + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + let a = self.min(); + let b = self.max(); + let new = geo_types::Rect::new(proj.convert(a)?, proj.convert(b)?); + *self = new; + Ok(()) + } +} + +impl Transform for geo_types::Triangle +where + T: crate::proj::CoordinateType, +{ + type Output = Self; + + fn transformed(&self, proj: &Proj) -> Result { + let mut output = *self; + output.transform(proj)?; + Ok(output) + } + + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError> { + self.0.transform(proj)?; + self.1.transform(proj)?; + self.2.transform(proj)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use geo_types::{point, MultiPoint, Rect}; + + #[test] + fn test_point() { + let mut subject = point!(x: 4760096.421921f64, y: 3744293.729449f64); + subject + .transform_crs_to_crs("EPSG:2230", "EPSG:26946") + .unwrap(); + let expected = point!(x: 1450880.29f64, y: 1141263.01f64); + assert_relative_eq!(subject, expected, epsilon = 0.2); + } + + #[test] + fn test_rect() { + let mut subject = { + let point_a = point!(x: 4760096.421921f64, y: 3744293.729449f64); + let point_b = point!(x: 4760196.421921f64, y: 3744393.729449f64); + Rect::new(point_a, point_b) + }; + + subject + .transform_crs_to_crs("EPSG:2230", "EPSG:26946") + .unwrap(); + let expected = { + let point_a = point!(x: 1450880.2910605022, y: 1141263.0111604782); + let point_b = point!(x: 1450910.771121464, y: 1141293.4912214363); + Rect::new(point_a, point_b) + }; + assert_relative_eq!(subject, expected, epsilon = 0.2); + } + + #[test] + fn test_multi_point() { + let mut subject = { + let point_a = point!(x: 4760096.421921f64, y: 3744293.729449f64); + let point_b = point!(x: 4760196.421921f64, y: 3744393.729449f64); + MultiPoint(vec![point_a, point_b]) + }; + + subject + .transform_crs_to_crs("EPSG:2230", "EPSG:26946") + .unwrap(); + let expected = { + let point_a = point!(x: 1450880.2910605022, y: 1141263.0111604782); + let point_b = point!(x: 1450910.771121464, y: 1141293.4912214363); + MultiPoint(vec![point_a, point_b]) + }; + assert_relative_eq!(subject, expected, epsilon = 0.2); + } + + #[test] + fn test_geometry_collection() { + let mut subject = { + let multi_point = { + let point_a = point!(x: 4760096.421921f64, y: 3744293.729449f64); + let point_b = point!(x: 4760196.421921f64, y: 3744393.729449f64); + MultiPoint(vec![point_a, point_b]) + }; + let rect = { + let point_a = point!(x: 4760096.421921f64, y: 3744293.729449f64); + let point_b = point!(x: 4760196.421921f64, y: 3744393.729449f64); + Rect::new(point_a, point_b) + }; + geo_types::GeometryCollection(vec![Geometry::from(multi_point), Geometry::from(rect)]) + }; + + subject + .transform_crs_to_crs("EPSG:2230", "EPSG:26946") + .unwrap(); + let expected = { + let multi_point = { + let point_a = point!(x: 1450880.2910605022, y: 1141263.0111604782); + let point_b = point!(x: 1450910.771121464, y: 1141293.4912214363); + MultiPoint(vec![point_a, point_b]) + }; + let rect = { + let point_a = point!(x: 1450880.2910605022, y: 1141263.0111604782); + let point_b = point!(x: 1450910.771121464, y: 1141293.4912214363); + Rect::new(point_a, point_b) + }; + geo_types::GeometryCollection(vec![Geometry::from(multi_point), Geometry::from(rect)]) + }; + assert_relative_eq!(subject, expected, epsilon = 0.2); + } +} diff --git a/src/lib.rs b/src/lib.rs index bf1bf9c8..2dcc0bf1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -211,6 +211,8 @@ mod geo_types; extern crate approx; mod proj; +mod transform; +pub use transform::{Transform, TransformError}; pub use crate::proj::Area; pub use crate::proj::Coord; diff --git a/src/proj.rs b/src/proj.rs index 8705ec10..fe529292 100644 --- a/src/proj.rs +++ b/src/proj.rs @@ -11,8 +11,7 @@ use proj_sys::{ PJ_DIRECTION_PJ_INV, PJ_INFO, PJ_LPZT, PJ_XYZT, }; use std::{ - convert, - ffi, + convert, ffi, fmt::{self, Debug}, str, }; diff --git a/src/transform.rs b/src/transform.rs new file mode 100644 index 00000000..5691f86a --- /dev/null +++ b/src/transform.rs @@ -0,0 +1,142 @@ +use std::{error::Error, fmt}; + +use crate::{Proj, ProjError}; + +/// Transform a geometry using PROJ. +pub trait Transform { + type Output; + + /// Transform a Geometry by mutating it in place. + /// + #[cfg_attr(feature = "geo-types", doc = r##" +# Examples + +Transform a geometry using a PROJ string definition: + +``` +use geo_types; +use proj::{Proj, Transform}; + # use approx::assert_relative_eq; + +let mut point = geo_types::point!(x: -36.508f32, y: -54.2815f32); +let proj = Proj::new("+proj=axisswap +order=2,1,3,4").expect("invalid proj string"); + point.transform(&proj); + +assert_relative_eq!( + point, + geo_types::point!(x: -54.2815f32, y: -36.508f32) +); +``` +"##)] + fn transform(&mut self, proj: &Proj) -> Result<(), ProjError>; + + /// Immutable flavor of [`Transform::transform`], which allocates a new geometry. + /// + #[cfg_attr(feature = "geo-types", doc = r##" +# Examples + +Transform a geometry using a PROJ string definition: + +``` +use geo_types; +use proj::{Proj, Transform}; +# use approx::assert_relative_eq; + +let point = geo_types::point!(x: -36.508f32, y: -54.2815f32); +let proj = Proj::new("+proj=axisswap +order=2,1,3,4").expect("invalid proj string"); + +assert_relative_eq!( + point.transformed(&proj).unwrap(), + geo_types::point!(x: -54.2815f32, y: -36.508f32) +); + +// original `point` is untouched +assert_relative_eq!( + point, + geo_types::point!(x: -36.508f32, y: -54.2815f32) +); +``` +"##)] + fn transformed(&self, proj: &Proj) -> Result; + + /// Transform a geometry from one CRS to another CRS by modifying it in place. + /// + #[cfg_attr(feature = "geo-types", doc = r##" +# Examples + +``` +# use approx::assert_relative_eq; +use proj::Transform; +use geo_types::{point, Point}; + +let mut point: Point = point!(x: -36.508f32, y: -54.2815f32); +point.transform_crs_to_crs("EPSG:4326", "EPSG:3857").unwrap(); + +assert_relative_eq!(point, point!(x: -4064052.0f32, y: -7223650.5f32)); +``` +"##)] + fn transform_crs_to_crs( + &mut self, + source_crs: &str, + target_crs: &str, + ) -> Result<(), TransformError> { + let proj = Proj::new_known_crs(source_crs, target_crs, None)?; + Ok(self.transform(&proj)?) + } + + /// Immutable flavor of [`Transform::transform_crs_to_crs`], which allocates a new geometry. + /// + #[cfg_attr(feature = "geo-types", doc = r##" +# Examples + +``` +# use approx::assert_relative_eq; +use proj::Transform; +use geo_types::{point, Point}; + +let mut point: Point = point!(x: -36.508f32, y: -54.2815f32); + +assert_relative_eq!( + point.transformed_crs_to_crs("EPSG:4326", "EPSG:3857").unwrap(), + point!(x: -4064052.0f32, y: -7223650.5f32) +); +``` +"##)] + fn transformed_crs_to_crs( + &self, + source_crs: &str, + target_crs: &str, + ) -> Result { + let proj = Proj::new_known_crs(source_crs, target_crs, None)?; + Ok(self.transformed(&proj)?) + } +} + +#[derive(Debug)] +pub enum TransformError { + ProjCreateError(crate::ProjCreateError), + ProjError(crate::ProjError), +} + +impl From for TransformError { + fn from(e: crate::ProjError) -> Self { + TransformError::ProjError(e) + } +} + +impl From for TransformError { + fn from(e: crate::ProjCreateError) -> Self { + TransformError::ProjCreateError(e) + } +} + +impl fmt::Display for TransformError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + TransformError::ProjCreateError(err) => err.fmt(f), + TransformError::ProjError(err) => err.fmt(f), + } + } +} + +impl Error for TransformError {}