use crate::errors::ProjectionError;
use geo::Point;
use proj4rs::proj::Proj;
pub struct CrsTransformer {
source_crs: String,
target_crs: String,
from_proj: Proj,
to_proj: Proj,
source_is_geographic: bool,
target_is_geographic: bool,
}
impl CrsTransformer {
pub fn new(source_crs: String, target_crs: String) -> Result<Self, ProjectionError> {
let source_proj_str = Self::epsg_to_proj_string(&source_crs)?;
let target_proj_str = Self::epsg_to_proj_string(&target_crs)?;
let from_proj = Proj::from_proj_string(&source_proj_str).map_err(|e| {
ProjectionError::InvalidCrs(format!(
"Failed to create source projection from {}: {:?}",
source_crs, e
))
})?;
let to_proj = Proj::from_proj_string(&target_proj_str).map_err(|e| {
ProjectionError::InvalidCrs(format!(
"Failed to create target projection from {}: {:?}",
target_crs, e
))
})?;
let source_is_geographic = source_proj_str.contains("+proj=longlat");
let target_is_geographic = target_proj_str.contains("+proj=longlat");
Ok(Self {
source_crs,
target_crs,
from_proj,
to_proj,
source_is_geographic,
target_is_geographic,
})
}
fn epsg_to_proj_string(epsg: &str) -> Result<String, ProjectionError> {
let code = if epsg.starts_with("EPSG:") {
epsg.strip_prefix("EPSG:")
.and_then(|s| s.parse::<u16>().ok())
} else {
epsg.parse::<u16>().ok()
};
if let Some(code) = code {
crs_definitions::from_code(code)
.ok_or_else(|| ProjectionError::InvalidCrs(format!("Unknown EPSG code: {}", epsg)))
.map(|def| def.proj4.to_string())
} else {
Ok(epsg.to_string())
}
}
pub fn transform(&self, point: Point<f64>) -> Result<Point<f64>, ProjectionError> {
let mut coord = (point.x(), point.y(), 0.0);
if self.source_is_geographic {
coord.0 = coord.0.to_radians();
coord.1 = coord.1.to_radians();
}
proj4rs::transform::transform(&self.from_proj, &self.to_proj, &mut coord).map_err(|e| {
ProjectionError::TransformFailed(format!(
"proj4rs transformation failed ({} -> {}): {:?}",
self.source_crs, self.target_crs, e
))
})?;
if self.target_is_geographic {
coord.0 = coord.0.to_degrees();
coord.1 = coord.1.to_degrees();
}
Ok(Point::new(coord.0, coord.1))
}
}