tackler_api/
location.rs

1/*
2 * Tackler-NG 2022-2025
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! Transaction Geo location
7//!
8use crate::tackler;
9use rust_decimal::Decimal;
10use serde::Serialize;
11use std::fmt::{Display, Formatter};
12
13/// Geo Point
14///
15#[derive(Serialize, Debug, Clone)]
16pub struct GeoPoint {
17    /// Latitude in decimal format
18    pub lat: Decimal,
19    /// Longitude in decimal format
20    pub lon: Decimal,
21    /// optional depth/altitude, in meters
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub alt: Option<Decimal>,
24}
25
26impl Display for GeoPoint {
27    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
28        let alt = match &self.alt {
29            Some(a) => format!(",{a}"),
30            None => String::new(),
31        };
32        write!(f, "geo:{},{}{}", self.lat, self.lon, alt)
33    }
34}
35/// Maximum latitude 90 deg (of North)
36pub const MAX_LAT: Decimal = Decimal::from_parts(90, 0, 0, false, 0);
37/// Minimum latitude -90 deg (of South)
38pub const MIN_LAT: Decimal = Decimal::from_parts(90, 0, 0, true, 0);
39/// Maximum longitude 180 (of East)
40pub const MAX_LON: Decimal = Decimal::from_parts(180, 0, 0, false, 0);
41/// Minimum longitude -180 (of West)
42pub const MIN_LON: Decimal = Decimal::from_parts(180, 0, 0, true, 0);
43/// Minimum altitude (Jules Verne: Voyage au centre de la Terre)
44pub const MIN_ALTITUDE: Decimal = Decimal::from_parts(6_378_137, 0, 0, true, 0);
45
46#[allow(clippy::manual_range_contains)]
47impl GeoPoint {
48    /// Make Geo point from given coordinates.
49    ///
50    /// * `lat` in decimals, must be inclusive -90 -- 90
51    /// * `lon` in decimals, must be inclusive -180 -- 180
52    /// * `alt` in meters, must be more than -6378137 meters
53    ///
54    /// # Errors
55    ///
56    /// Returns `Err` in case of invalid coordinates
57    pub fn from(
58        lat: Decimal,
59        lon: Decimal,
60        alt: Option<Decimal>,
61    ) -> Result<GeoPoint, tackler::Error> {
62        if lat < MIN_LAT || MAX_LAT < lat {
63            let msg = format!("Value out of specification for Latitude: {lat}");
64            return Err(msg.into());
65        }
66        if lon < MIN_LON || MAX_LON < lon {
67            let msg = format!("Value out of specification for Longitude: {lon}");
68            return Err(msg.into());
69        }
70        if let Some(z) = alt {
71            if z < Decimal::from(-6_378_137) {
72                // Jules Verne: Voyage au centre de la Terre
73                let msg = format!("Value Out of specification for Altitude: {z}");
74                return Err(msg.into());
75            }
76        }
77        Ok(GeoPoint { lat, lon, alt })
78    }
79}
80#[cfg(test)]
81mod tests {
82    use crate::location::GeoPoint;
83    use rust_decimal_macros::dec;
84
85    // todo: GeoPoint::from + checks
86
87    #[test]
88    fn geo_display() {
89        let tests: Vec<(GeoPoint, String)> = vec![
90            (
91                GeoPoint::from(dec!(60), dec!(24), None).unwrap(/*:test:*/),
92                "geo:60,24".to_string(),
93            ),
94            (
95                GeoPoint::from(dec!(60), dec!(24), Some(dec!(5))).unwrap(/*:test:*/),
96                "geo:60,24,5".to_string(),
97            ),
98            (
99                GeoPoint::from(dec!(60.167), dec!(24.955), Some(dec!(5.0))).unwrap(/*:test:*/),
100                "geo:60.167,24.955,5.0".to_string(),
101            ),
102            (
103                GeoPoint::from(dec!(60.167000), dec!(24.955000), Some(dec!(5.000))).unwrap(/*:test:*/),
104                "geo:60.167000,24.955000,5.000".to_string(),
105            ),
106            (
107                GeoPoint::from(dec!(-60), dec!(-24), Some(dec!(-5))).unwrap(/*:test:*/),
108                "geo:-60,-24,-5".to_string(),
109            ),
110            (
111                GeoPoint::from(dec!(-60.167), dec!(-24.955), Some(dec!(-5.0))).unwrap(/*:test:*/),
112                "geo:-60.167,-24.955,-5.0".to_string(),
113            ),
114            (
115                GeoPoint::from(dec!(-60.167000), dec!(-24.955000), Some(dec!(-5.000))).unwrap(/*:test:*/),
116                "geo:-60.167000,-24.955000,-5.000".to_string(),
117            ),
118        ];
119
120        let mut count = 0;
121        let should_be_count = tests.len();
122        for t in tests {
123            let geo_str = format!("{}", t.0);
124            assert_eq!(geo_str, t.1);
125            count += 1;
126        }
127        assert_eq!(count, should_be_count);
128    }
129}