1use crate::coord::{Coord, Transformable};
2use crate::crs::CrsDef;
3use crate::datum::Datum;
4use crate::error::{Error, Result};
5use crate::geocentric;
6use crate::helmert;
7use crate::projection::{make_projection, ProjectionImpl};
8use crate::registry;
9
10pub struct Transform {
26 pipeline: TransformPipeline,
27}
28
29enum TransformPipeline {
30 Identity,
32 SameDatumForward { forward: Box<dyn ProjectionImpl> },
34 SameDatumInverse { inverse: Box<dyn ProjectionImpl> },
36 SameDatumBoth {
38 inverse: Box<dyn ProjectionImpl>,
39 forward: Box<dyn ProjectionImpl>,
40 },
41 DatumShift {
43 inverse: Option<Box<dyn ProjectionImpl>>,
44 source_datum: Datum,
45 target_datum: Datum,
46 forward: Option<Box<dyn ProjectionImpl>>,
47 },
48}
49
50impl Transform {
51 pub fn new(from_crs: &str, to_crs: &str) -> Result<Self> {
56 let source = registry::lookup_authority_code(from_crs)?;
57 let target = registry::lookup_authority_code(to_crs)?;
58 Self::from_crs_defs(&source, &target)
59 }
60
61 pub fn from_epsg(from: u32, to: u32) -> Result<Self> {
63 let source = registry::lookup_epsg(from)
64 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {from}")))?;
65 let target = registry::lookup_epsg(to)
66 .ok_or_else(|| Error::UnknownCrs(format!("unknown EPSG code: {to}")))?;
67 Self::from_crs_defs(&source, &target)
68 }
69
70 pub fn from_crs_defs(from: &CrsDef, to: &CrsDef) -> Result<Self> {
74 if from.epsg() != 0 && from.epsg() == to.epsg() {
76 return Ok(Self {
77 pipeline: TransformPipeline::Identity,
78 });
79 }
80
81 let source_datum = from.datum();
82 let target_datum = to.datum();
83
84 let same_datum = source_datum.same_datum(target_datum)
85 || (source_datum.is_wgs84_compatible() && target_datum.is_wgs84_compatible());
86
87 let pipeline = if same_datum {
88 match (from, to) {
90 (CrsDef::Geographic(_), CrsDef::Geographic(_)) => TransformPipeline::Identity,
91 (CrsDef::Geographic(_), CrsDef::Projected(p)) => {
92 let forward = make_projection(&p.method, &p.datum)?;
93 TransformPipeline::SameDatumForward { forward }
94 }
95 (CrsDef::Projected(p), CrsDef::Geographic(_)) => {
96 let inverse = make_projection(&p.method, &p.datum)?;
97 TransformPipeline::SameDatumInverse { inverse }
98 }
99 (CrsDef::Projected(p_from), CrsDef::Projected(p_to)) => {
100 let inverse = make_projection(&p_from.method, &p_from.datum)?;
101 let forward = make_projection(&p_to.method, &p_to.datum)?;
102 TransformPipeline::SameDatumBoth { inverse, forward }
103 }
104 }
105 } else {
106 if !source_datum.is_wgs84_compatible() && source_datum.to_wgs84.is_none() {
108 return Err(Error::UnsupportedProjection(format!(
109 "source CRS EPSG:{} has no known datum shift to WGS84",
110 from.epsg()
111 )));
112 }
113 if !target_datum.is_wgs84_compatible() && target_datum.to_wgs84.is_none() {
114 return Err(Error::UnsupportedProjection(format!(
115 "target CRS EPSG:{} has no known datum shift to WGS84",
116 to.epsg()
117 )));
118 }
119
120 let inverse = match from {
121 CrsDef::Projected(p) => Some(make_projection(&p.method, &p.datum)?),
122 CrsDef::Geographic(_) => None,
123 };
124 let forward = match to {
125 CrsDef::Projected(p) => Some(make_projection(&p.method, &p.datum)?),
126 CrsDef::Geographic(_) => None,
127 };
128
129 TransformPipeline::DatumShift {
130 inverse,
131 source_datum: *source_datum,
132 target_datum: *target_datum,
133 forward,
134 }
135 };
136
137 Ok(Self { pipeline })
138 }
139
140 pub fn convert<T: Transformable>(&self, coord: T) -> Result<T> {
150 let c = coord.into_coord();
151 let result = self.convert_coord(c)?;
152 Ok(T::from_coord(result))
153 }
154
155 fn convert_coord(&self, c: Coord) -> Result<Coord> {
157 match &self.pipeline {
158 TransformPipeline::Identity => Ok(c),
159
160 TransformPipeline::SameDatumForward { forward } => {
161 let lon_rad = c.x.to_radians();
163 let lat_rad = c.y.to_radians();
164 let (x, y) = forward.forward(lon_rad, lat_rad)?;
165 Ok(Coord { x, y })
166 }
167
168 TransformPipeline::SameDatumInverse { inverse } => {
169 let (lon_rad, lat_rad) = inverse.inverse(c.x, c.y)?;
171 Ok(Coord {
172 x: lon_rad.to_degrees(),
173 y: lat_rad.to_degrees(),
174 })
175 }
176
177 TransformPipeline::SameDatumBoth { inverse, forward } => {
178 let (lon_rad, lat_rad) = inverse.inverse(c.x, c.y)?;
180 let (x, y) = forward.forward(lon_rad, lat_rad)?;
181 Ok(Coord { x, y })
182 }
183
184 TransformPipeline::DatumShift {
185 inverse,
186 source_datum,
187 target_datum,
188 forward,
189 } => {
190 let (lon_rad, lat_rad) = if let Some(inv) = inverse {
192 inv.inverse(c.x, c.y)?
193 } else {
194 (c.x.to_radians(), c.y.to_radians())
195 };
196
197 let (x, y, z) = geocentric::geodetic_to_geocentric(
199 &source_datum.ellipsoid,
200 lon_rad,
201 lat_rad,
202 0.0,
203 );
204
205 let (x2, y2, z2) = if let Some(params) = &source_datum.to_wgs84 {
207 helmert::helmert_forward(params, x, y, z)
208 } else {
209 (x, y, z) };
211
212 let (x3, y3, z3) = if let Some(params) = &target_datum.to_wgs84 {
214 helmert::helmert_inverse(params, x2, y2, z2)
215 } else {
216 (x2, y2, z2) };
218
219 let (lon_out, lat_out, _h) =
221 geocentric::geocentric_to_geodetic(&target_datum.ellipsoid, x3, y3, z3);
222
223 if let Some(fwd) = forward {
225 let (x, y) = fwd.forward(lon_out, lat_out)?;
226 Ok(Coord { x, y })
227 } else {
228 Ok(Coord {
229 x: lon_out.to_degrees(),
230 y: lat_out.to_degrees(),
231 })
232 }
233 }
234 }
235 }
236
237 pub fn convert_batch<T: Transformable + Clone>(&self, coords: &[T]) -> Result<Vec<T>> {
239 coords.iter().map(|c| self.convert(c.clone())).collect()
240 }
241
242 #[cfg(feature = "rayon")]
244 pub fn convert_batch_parallel<T: Transformable + Send + Sync + Clone>(
245 &self,
246 coords: &[T],
247 ) -> Result<Vec<T>> {
248 use rayon::prelude::*;
249 coords.par_iter().map(|c| self.convert(c.clone())).collect()
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 #[test]
258 fn identity_same_crs() {
259 let t = Transform::new("EPSG:4326", "EPSG:4326").unwrap();
260 let (x, y) = t.convert((-74.006, 40.7128)).unwrap();
261 assert_eq!(x, -74.006);
262 assert_eq!(y, 40.7128);
263 }
264
265 #[test]
266 fn wgs84_to_web_mercator() {
267 let t = Transform::new("EPSG:4326", "EPSG:3857").unwrap();
268 let (x, y) = t.convert((-74.006, 40.7128)).unwrap();
269 assert!((x - (-8238310.0)).abs() < 100.0, "x = {x}");
271 assert!((y - 4970072.0).abs() < 100.0, "y = {y}");
272 }
273
274 #[test]
275 fn web_mercator_to_wgs84() {
276 let t = Transform::new("EPSG:3857", "EPSG:4326").unwrap();
277 let (lon, lat) = t.convert((-8238310.0, 4970072.0)).unwrap();
278 assert!((lon - (-74.006)).abs() < 0.001, "lon = {lon}");
279 assert!((lat - 40.7128).abs() < 0.001, "lat = {lat}");
280 }
281
282 #[test]
283 fn roundtrip_4326_3857() {
284 let fwd = Transform::new("EPSG:4326", "EPSG:3857").unwrap();
285 let inv = Transform::new("EPSG:3857", "EPSG:4326").unwrap();
286
287 let original = (-74.0445, 40.6892);
288 let projected = fwd.convert(original).unwrap();
289 let back = inv.convert(projected).unwrap();
290
291 assert!(
292 (back.0 - original.0).abs() < 1e-8,
293 "lon: {} vs {}",
294 back.0,
295 original.0
296 );
297 assert!(
298 (back.1 - original.1).abs() < 1e-8,
299 "lat: {} vs {}",
300 back.1,
301 original.1
302 );
303 }
304
305 #[test]
306 fn wgs84_to_utm_18n() {
307 let t = Transform::new("EPSG:4326", "EPSG:32618").unwrap();
308 let (x, y) = t.convert((-74.006, 40.7128)).unwrap();
309 assert!((x - 583960.0).abs() < 1.0, "easting = {x}");
310 assert!(y > 4_500_000.0 && y < 4_510_000.0, "northing = {y}");
311 }
312
313 #[test]
314 fn utm_to_web_mercator() {
315 let t = Transform::new("EPSG:32618", "EPSG:3857").unwrap();
317 let (x, _y) = t.convert((583960.0, 4507523.0)).unwrap();
319 assert!((x - (-8238310.0)).abs() < 200.0, "x = {x}");
321 }
322
323 #[test]
324 fn wgs84_to_polar_stereo_3413() {
325 let t = Transform::new("EPSG:4326", "EPSG:3413").unwrap();
326 let (x, y) = t.convert((-45.0, 90.0)).unwrap();
327 assert!(x.abs() < 1.0, "x = {x}");
329 assert!(y.abs() < 1.0, "y = {y}");
330 }
331
332 #[test]
333 fn roundtrip_4326_3413() {
334 let fwd = Transform::new("EPSG:4326", "EPSG:3413").unwrap();
335 let inv = Transform::new("EPSG:3413", "EPSG:4326").unwrap();
336
337 let original = (-45.0, 75.0);
338 let projected = fwd.convert(original).unwrap();
339 let back = inv.convert(projected).unwrap();
340
341 assert!(
342 (back.0 - original.0).abs() < 1e-6,
343 "lon: {} vs {}",
344 back.0,
345 original.0
346 );
347 assert!(
348 (back.1 - original.1).abs() < 1e-6,
349 "lat: {} vs {}",
350 back.1,
351 original.1
352 );
353 }
354
355 #[test]
356 fn geographic_to_geographic_same_datum_is_identity() {
357 let t = Transform::new("EPSG:4269", "EPSG:4326").unwrap();
359 let (lon, lat) = t.convert((-74.006, 40.7128)).unwrap();
360 assert_eq!(lon, -74.006);
361 assert_eq!(lat, 40.7128);
362 }
363
364 #[test]
365 fn unknown_crs_error() {
366 let result = Transform::new("EPSG:99999", "EPSG:4326");
367 assert!(result.is_err());
368 }
369
370 #[test]
371 fn cross_datum_nad27_to_wgs84() {
372 let t = Transform::new("EPSG:4267", "EPSG:4326").unwrap();
373 let (lon, lat) = t.convert((-90.0, 45.0)).unwrap();
374 assert!((lon - (-90.0)).abs() < 0.01, "lon = {lon}");
376 assert!((lat - 45.0).abs() < 0.01, "lat = {lat}");
377 }
378
379 #[test]
380 fn cross_datum_roundtrip_nad27() {
381 let fwd = Transform::new("EPSG:4267", "EPSG:4326").unwrap();
382 let inv = Transform::new("EPSG:4326", "EPSG:4267").unwrap();
383 let original = (-90.0, 45.0);
384 let shifted = fwd.convert(original).unwrap();
385 let back = inv.convert(shifted).unwrap();
386 assert!(
387 (back.0 - original.0).abs() < 1e-6,
388 "lon: {} vs {}",
389 back.0,
390 original.0
391 );
392 assert!(
393 (back.1 - original.1).abs() < 1e-6,
394 "lat: {} vs {}",
395 back.1,
396 original.1
397 );
398 }
399
400 #[test]
401 fn cross_datum_osgb36_to_wgs84() {
402 let t = Transform::new("EPSG:4277", "EPSG:4326").unwrap();
403 let (lon, lat) = t.convert((-0.1278, 51.5074)).unwrap(); assert!((lon - (-0.1278)).abs() < 0.01, "lon = {lon}");
406 assert!((lat - 51.5074).abs() < 0.01, "lat = {lat}");
407 }
408
409 #[test]
410 fn batch_transform() {
411 let t = Transform::new("EPSG:4326", "EPSG:3857").unwrap();
412 let coords: Vec<(f64, f64)> = (0..10)
413 .map(|i| (-74.0 + i as f64 * 0.1, 40.0 + i as f64 * 0.1))
414 .collect();
415
416 let results = t.convert_batch(&coords).unwrap();
417 assert_eq!(results.len(), 10);
418 for (x, _y) in &results {
419 assert!(*x < 0.0); }
421 }
422
423 #[test]
424 fn coord_type() {
425 let t = Transform::new("EPSG:4326", "EPSG:3857").unwrap();
426 let c = Coord::new(-74.006, 40.7128);
427 let result = t.convert(c).unwrap();
428 assert!((result.x - (-8238310.0)).abs() < 100.0);
429 }
430
431 #[cfg(feature = "geo-types")]
432 #[test]
433 fn geo_types_coord() {
434 let t = Transform::new("EPSG:4326", "EPSG:3857").unwrap();
435 let c = geo_types::Coord {
436 x: -74.006,
437 y: 40.7128,
438 };
439 let result: geo_types::Coord<f64> = t.convert(c).unwrap();
440 assert!((result.x - (-8238310.0)).abs() < 100.0);
441 }
442
443 #[cfg(feature = "rayon")]
444 #[test]
445 fn parallel_batch_transform() {
446 let t = Transform::new("EPSG:4326", "EPSG:3857").unwrap();
447 let coords: Vec<(f64, f64)> = (0..100)
448 .map(|i| (-74.0 + i as f64 * 0.01, 40.0 + i as f64 * 0.01))
449 .collect();
450
451 let results = t.convert_batch_parallel(&coords).unwrap();
452 assert_eq!(results.len(), 100);
453 }
454}