use crate::Result;
use geo::{CoordNum, coord};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum YAxisDirection
{
North,
South,
}
#[derive(Debug, Clone)]
pub struct Projection
{
pub(crate) proj: proj4rs::proj::Proj,
pub(crate) unit: Unit,
}
#[derive(Debug, Clone)]
pub enum Unit
{
Radian,
Degree,
Meter,
}
impl Projection
{
pub fn wgs84() -> Self
{
Self::from_proj_string(
"+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs",
Unit::Degree,
)
.expect("Internal error, invalid WGS84 string.")
}
pub fn web_mercator() -> Self
{
Self::from_proj_string(
"+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=m +nadgrids=@null +wktext +no_defs +type=crs"
, Unit::Meter
)
.expect("Internal error, invalid web mercator string.")
}
pub fn from_proj_string(proj: &str, unit: Unit) -> crate::Result<Self>
{
Ok(Self {
proj: proj4rs::proj::Proj::from_proj_string(proj)?,
unit,
})
}
pub(crate) fn y_axis_direction(&self) -> YAxisDirection
{
if self.proj.axis()[1] == b'n'
{
YAxisDirection::North
}
else
{
YAxisDirection::South
}
}
}
impl PartialEq for &Projection
{
fn eq(&self, other: &Self) -> bool
{
self.proj.projname() == other.proj.projname()
}
}
impl Eq for &Projection {}
pub trait Transformable: Sized
{
fn transform(&self, from: &Projection, to: &Projection) -> Result<Self>;
}
impl<T> Transformable for geo::Coord<T>
where
T: num_traits::Float + num_traits::FloatConst + CoordNum,
(T, T): proj4rs::transform::Transform,
{
fn transform(&self, from: &Projection, to: &Projection) -> Result<Self>
{
let mut coord = self.x_y();
if matches!(from.unit, Unit::Degree)
{
coord.0 = coord.0 * T::PI() / T::from(180.0).unwrap();
coord.1 = coord.1 * T::PI() / T::from(180.0).unwrap();
}
proj4rs::transform::transform(&from.proj, &to.proj, &mut coord)?;
if matches!(to.unit, Unit::Degree)
{
coord.0 = coord.0 * T::from(180.0).unwrap() / T::PI();
coord.1 = coord.1 * T::from(180.0).unwrap() / T::PI();
}
Ok(coord! { x: coord.0, y: coord.1})
}
}
impl<T> Transformable for geo::Rect<T>
where
T: num_traits::Float + num_traits::FloatConst + CoordNum,
(T, T): proj4rs::transform::Transform,
{
fn transform(&self, from: &Projection, to: &Projection) -> Result<Self>
{
let min = transform(from, to, &self.min())?;
let max = transform(from, to, &self.max())?;
Ok(Self::new(min, max))
}
}
pub fn transform<T: Transformable>(from: &Projection, to: &Projection, value: &T) -> Result<T>
{
value.transform(from, to)
}
#[cfg(test)]
mod tests
{
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_projection_creation()
{
let wgs = Projection::wgs84();
let web = Projection::web_mercator();
assert_eq!(wgs.y_axis_direction(), YAxisDirection::North);
assert_eq!(web.y_axis_direction(), YAxisDirection::North);
assert_eq!(&wgs, &Projection::wgs84());
assert_eq!(&web, &Projection::web_mercator());
}
#[test]
fn test_coord_transform_wgs_to_webmercator()
{
let wgs = Projection::wgs84();
let web = Projection::web_mercator();
let paris = geo::coord! { x: 2.3522, y: 48.8566 };
let paris_web = paris.transform(&wgs, &web).unwrap();
assert_relative_eq!(paris_web.x, 261845.0, epsilon = 100.0);
assert_relative_eq!(paris_web.y, 6250560.0, epsilon = 100.0);
let paris_wgs_back = paris_web.transform(&web, &wgs).unwrap();
assert_relative_eq!(paris_wgs_back.x, paris.x, epsilon = 1e-5);
assert_relative_eq!(paris_wgs_back.y, paris.y, epsilon = 1e-5);
}
#[test]
fn test_rect_transform_wgs_to_webmercator()
{
let wgs = Projection::wgs84();
let web = Projection::web_mercator();
let rect_wgs = geo::Rect::new(
geo::coord! { x: 2.3, y: 48.85 },
geo::coord! { x: 2.4, y: 48.87 },
);
let rect_web = rect_wgs.transform(&wgs, &web).unwrap();
assert!(rect_web.min().x > 200_000.0 && rect_web.max().x < 300_000.0);
assert!(rect_web.min().y > 6_200_000.0 && rect_web.max().y < 6_300_000.0);
let rect_wgs_back = rect_web.transform(&web, &wgs).unwrap();
assert_relative_eq!(rect_wgs_back.min().x, rect_wgs.min().x, epsilon = 1e-5);
assert_relative_eq!(rect_wgs_back.min().y, rect_wgs.min().y, epsilon = 1e-5);
assert_relative_eq!(rect_wgs_back.max().x, rect_wgs.max().x, epsilon = 1e-5);
assert_relative_eq!(rect_wgs_back.max().y, rect_wgs.max().y, epsilon = 1e-5);
}
}