1use glam::DVec3;
43use std::fmt;
44
45pub(crate) const MAX_MERCATOR_LAT: f64 = 85.051_129;
52
53#[derive(Debug, Clone, Copy, PartialEq)]
81#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
82pub struct GeoCoord {
83 pub lat: f64,
85 pub lon: f64,
87 pub alt: f64,
89}
90
91impl GeoCoord {
92 #[inline]
102 pub fn new(lat: f64, lon: f64, alt: f64) -> Self {
103 const EPS: f64 = 1e-10;
104 debug_assert!(
105 (-90.0 - EPS..=90.0 + EPS).contains(&lat),
106 "latitude {lat} out of range [-90, 90]"
107 );
108 debug_assert!(
109 (-180.0 - EPS..=180.0 + EPS).contains(&lon),
110 "longitude {lon} out of range [-180, 180]"
111 );
112 Self { lat, lon, alt }
113 }
114
115 #[inline]
117 pub fn from_lat_lon(lat: f64, lon: f64) -> Self {
118 Self::new(lat, lon, 0.0)
119 }
120
121 #[inline]
126 pub fn new_checked(lat: f64, lon: f64, alt: f64) -> Option<Self> {
127 if !(-90.0..=90.0).contains(&lat) || !(-180.0..=180.0).contains(&lon) {
128 return None;
129 }
130 Some(Self { lat, lon, alt })
131 }
132
133 #[inline]
138 pub fn is_web_mercator_valid(&self) -> bool {
139 self.lat.abs() <= MAX_MERCATOR_LAT && self.lon.abs() <= 180.0
140 }
141
142 #[inline]
149 pub fn clamped_mercator(&self) -> Self {
150 let lat = self.lat.clamp(-MAX_MERCATOR_LAT, MAX_MERCATOR_LAT);
151 let mut lon = self.lon % 360.0;
152 if lon > 180.0 {
153 lon -= 360.0;
154 }
155 if lon < -180.0 {
156 lon += 360.0;
157 }
158 Self {
159 lat,
160 lon,
161 alt: self.alt,
162 }
163 }
164}
165
166impl Default for GeoCoord {
167 fn default() -> Self {
169 Self {
170 lat: 0.0,
171 lon: 0.0,
172 alt: 0.0,
173 }
174 }
175}
176
177impl fmt::Display for GeoCoord {
178 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 let ns = if self.lat >= 0.0 { 'N' } else { 'S' };
181 let ew = if self.lon >= 0.0 { 'E' } else { 'W' };
182 write!(
183 f,
184 "{:.6} {} {:.6} {} {:.1}m",
185 self.lat.abs(),
186 ns,
187 self.lon.abs(),
188 ew,
189 self.alt
190 )
191 }
192}
193
194impl From<(f64, f64)> for GeoCoord {
197 #[inline]
199 fn from((lat, lon): (f64, f64)) -> Self {
200 Self::from_lat_lon(lat, lon)
201 }
202}
203
204impl From<(f64, f64, f64)> for GeoCoord {
205 #[inline]
207 fn from((lat, lon, alt): (f64, f64, f64)) -> Self {
208 Self::new(lat, lon, alt)
209 }
210}
211
212impl From<[f64; 2]> for GeoCoord {
213 #[inline]
215 fn from(arr: [f64; 2]) -> Self {
216 Self::from_lat_lon(arr[0], arr[1])
217 }
218}
219
220impl From<[f64; 3]> for GeoCoord {
221 #[inline]
223 fn from(arr: [f64; 3]) -> Self {
224 Self::new(arr[0], arr[1], arr[2])
225 }
226}
227
228impl From<GeoCoord> for (f64, f64, f64) {
229 #[inline]
231 fn from(c: GeoCoord) -> Self {
232 (c.lat, c.lon, c.alt)
233 }
234}
235
236impl From<GeoCoord> for [f64; 3] {
237 #[inline]
239 fn from(c: GeoCoord) -> Self {
240 [c.lat, c.lon, c.alt]
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq)]
274pub struct WorldCoord {
275 pub position: DVec3,
277}
278
279impl WorldCoord {
280 #[inline]
282 pub fn new(x: f64, y: f64, z: f64) -> Self {
283 Self {
284 position: DVec3::new(x, y, z),
285 }
286 }
287}
288
289impl Default for WorldCoord {
290 fn default() -> Self {
292 Self {
293 position: DVec3::ZERO,
294 }
295 }
296}
297
298impl fmt::Display for WorldCoord {
299 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(
302 f,
303 "({:.2}, {:.2}, {:.2})m",
304 self.position.x, self.position.y, self.position.z
305 )
306 }
307}
308
309impl From<DVec3> for WorldCoord {
312 #[inline]
314 fn from(v: DVec3) -> Self {
315 Self { position: v }
316 }
317}
318
319impl From<WorldCoord> for DVec3 {
320 #[inline]
322 fn from(c: WorldCoord) -> Self {
323 c.position
324 }
325}
326
327impl From<[f64; 3]> for WorldCoord {
328 #[inline]
330 fn from(arr: [f64; 3]) -> Self {
331 Self::new(arr[0], arr[1], arr[2])
332 }
333}
334
335impl From<WorldCoord> for [f64; 3] {
336 #[inline]
338 fn from(c: WorldCoord) -> Self {
339 [c.position.x, c.position.y, c.position.z]
340 }
341}
342
343#[cfg(feature = "serde")]
346impl serde::Serialize for WorldCoord {
347 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
348 use serde::ser::SerializeStruct;
349 let mut s = serializer.serialize_struct("WorldCoord", 3)?;
350 s.serialize_field("x", &self.position.x)?;
351 s.serialize_field("y", &self.position.y)?;
352 s.serialize_field("z", &self.position.z)?;
353 s.end()
354 }
355}
356
357#[cfg(feature = "serde")]
358impl<'de> serde::Deserialize<'de> for WorldCoord {
359 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
360 #[derive(serde::Deserialize)]
361 struct Helper {
362 x: f64,
363 y: f64,
364 z: f64,
365 }
366 let h = Helper::deserialize(deserializer)?;
367 Ok(Self::new(h.x, h.y, h.z))
368 }
369}
370
371#[cfg(test)]
376mod tests {
377 use super::*;
378
379 #[test]
382 fn default_geo_coord() {
383 let c = GeoCoord::default();
384 assert_eq!(c.lat, 0.0);
385 assert_eq!(c.lon, 0.0);
386 assert_eq!(c.alt, 0.0);
387 }
388
389 #[test]
390 fn geo_coord_checked_valid() {
391 assert!(GeoCoord::new_checked(45.0, 90.0, 0.0).is_some());
392 }
393
394 #[test]
395 fn geo_coord_checked_invalid_lat() {
396 assert!(GeoCoord::new_checked(91.0, 0.0, 0.0).is_none());
397 }
398
399 #[test]
400 fn geo_coord_checked_invalid_lon() {
401 assert!(GeoCoord::new_checked(0.0, 181.0, 0.0).is_none());
402 }
403
404 #[test]
405 fn geo_coord_checked_boundary_values() {
406 assert!(GeoCoord::new_checked(90.0, 180.0, 0.0).is_some());
408 assert!(GeoCoord::new_checked(-90.0, -180.0, 0.0).is_some());
409 }
410
411 #[test]
414 fn geo_coord_display_north_east() {
415 let c = GeoCoord::new(51.1, 17.0, 100.0);
416 let s = format!("{c}");
417 assert!(s.contains('N'));
418 assert!(s.contains('E'));
419 assert!(s.contains("100.0m"));
420 }
421
422 #[test]
423 fn geo_coord_display_south_west() {
424 let c = GeoCoord::new(-33.9, -70.6, 0.0);
425 let s = format!("{c}");
426 assert!(s.contains('S'));
427 assert!(s.contains('W'));
428 }
429
430 #[test]
433 fn from_tuple_2() {
434 let c: GeoCoord = (51.1, 17.0).into();
435 assert_eq!(c.lat, 51.1);
436 assert_eq!(c.lon, 17.0);
437 assert_eq!(c.alt, 0.0);
438 }
439
440 #[test]
441 fn from_tuple_3() {
442 let c: GeoCoord = (51.1, 17.0, 500.0).into();
443 assert_eq!(c.lat, 51.1);
444 assert_eq!(c.lon, 17.0);
445 assert_eq!(c.alt, 500.0);
446 }
447
448 #[test]
449 fn from_array_2() {
450 let c: GeoCoord = [51.1, 17.0].into();
451 assert_eq!(c.lat, 51.1);
452 assert_eq!(c.lon, 17.0);
453 assert_eq!(c.alt, 0.0);
454 }
455
456 #[test]
457 fn from_array_3() {
458 let c: GeoCoord = [51.1, 17.0, 100.0].into();
459 assert_eq!(c.lat, 51.1);
460 assert_eq!(c.alt, 100.0);
461 }
462
463 #[test]
464 fn into_tuple() {
465 let c = GeoCoord::new(51.1, 17.0, 100.0);
466 let (lat, lon, alt): (f64, f64, f64) = c.into();
467 assert_eq!(lat, 51.1);
468 assert_eq!(lon, 17.0);
469 assert_eq!(alt, 100.0);
470 }
471
472 #[test]
473 fn into_array() {
474 let c = GeoCoord::new(51.1, 17.0, 100.0);
475 let arr: [f64; 3] = c.into();
476 assert_eq!(arr, [51.1, 17.0, 100.0]);
477 }
478
479 #[test]
482 fn is_web_mercator_valid() {
483 assert!(GeoCoord::from_lat_lon(51.0, 17.0).is_web_mercator_valid());
484 assert!(!GeoCoord::from_lat_lon(86.0, 17.0).is_web_mercator_valid());
485 }
486
487 #[test]
488 fn clamped_mercator_positive_overflow() {
489 let c = GeoCoord {
490 lat: 89.0,
491 lon: 200.0,
492 alt: 42.0,
493 };
494 let m = c.clamped_mercator();
495 assert!(m.lat <= MAX_MERCATOR_LAT);
496 assert!(m.lon >= -180.0 && m.lon <= 180.0);
497 assert!((m.lon - (-160.0)).abs() < 1e-10);
499 assert_eq!(m.alt, 42.0);
501 }
502
503 #[test]
504 fn clamped_mercator_negative_overflow() {
505 let c = GeoCoord {
506 lat: -89.0,
507 lon: -200.0,
508 alt: 0.0,
509 };
510 let m = c.clamped_mercator();
511 assert!(m.lat >= -MAX_MERCATOR_LAT);
512 assert!((m.lon - 160.0).abs() < 1e-10);
514 }
515
516 #[test]
517 fn clamped_mercator_already_valid() {
518 let c = GeoCoord::from_lat_lon(51.0, 17.0);
519 let m = c.clamped_mercator();
520 assert!((m.lat - 51.0).abs() < 1e-10);
521 assert!((m.lon - 17.0).abs() < 1e-10);
522 }
523
524 #[test]
527 fn default_world_coord() {
528 let c = WorldCoord::default();
529 assert_eq!(c.position, DVec3::ZERO);
530 }
531
532 #[test]
535 fn world_coord_display() {
536 let c = WorldCoord::new(1.0, 2.0, 3.0);
537 let s = format!("{c}");
538 assert!(s.contains("1.00"));
539 assert!(s.contains("2.00"));
540 assert!(s.contains("3.00"));
541 assert!(s.ends_with(")m"));
542 }
543
544 #[test]
547 fn world_coord_from_dvec3() {
548 let v = DVec3::new(1.0, 2.0, 3.0);
549 let c: WorldCoord = v.into();
550 assert_eq!(c.position, v);
551 let back: DVec3 = c.into();
552 assert_eq!(back, v);
553 }
554
555 #[test]
556 fn world_coord_from_array() {
557 let c: WorldCoord = [10.0, 20.0, 30.0].into();
558 assert_eq!(c.position.x, 10.0);
559 assert_eq!(c.position.y, 20.0);
560 assert_eq!(c.position.z, 30.0);
561 }
562
563 #[test]
564 fn world_coord_into_array() {
565 let c = WorldCoord::new(10.0, 20.0, 30.0);
566 let arr: [f64; 3] = c.into();
567 assert_eq!(arr, [10.0, 20.0, 30.0]);
568 }
569}