recoord/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(
3    missing_docs,
4    clippy::missing_docs_in_private_items
5)]
6
7//! Recoord is a create for handling work with coordinates
8//!
9//! All corrdinates are always converted to the latitude and longitude float format
10
11use std::{
12    fmt,
13    fmt::{Display, Formatter},
14};
15/// A wrapper around different coordinate formats
16pub mod formats;
17
18/// A wrapper around differend resolvers for Coordinates
19pub mod resolvers;
20
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24#[cfg(feature = "format_any")]
25use std::str::FromStr;
26
27#[cfg(any(feature = "format_dd", feature = "format_dms", feature = "resolve_osm"))]
28use std::num::ParseFloatError;
29
30use thiserror::Error;
31
32/// The base coordinate struct.
33/// It stores the location as latitude, longitude floats
34///
35#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
36#[derive(Debug, Clone, PartialEq)]
37pub struct Coordinate {
38    /// Longitude of the coordinate (-90 - 90)
39    pub lat: f64,
40    /// Latitude of the coordinate (-180 - 180)
41    pub lng: f64,
42}
43
44impl Coordinate {
45    /// Create a new coordinate with longitude and latitude
46    ///
47    /// ```
48    /// /// Normal Coordinate creation
49    /// # use recoord::Coordinate;
50    /// let manual = Coordinate { lat: 10., lng: 20. };
51    /// let coordinate = Coordinate::new(10., 20.);
52    /// assert_eq!(coordinate, manual)
53    /// ```
54    pub fn new(lat: f64, lng: f64) -> Self {
55        Self { lat, lng }
56    }
57}
58
59impl Display for Coordinate {
60    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
61        write!(f, "{},{}", self.lat, self.lng)
62    }
63}
64
65/// Error when handling coordinates
66#[derive(Debug, Error)]
67pub enum CoordinateError {
68    /// No parser available - enable them via features
69    #[error("No parser available - enable them via features")]
70    MissingParser,
71    /// Value can't be converted into a coordinate
72    #[error("Value can't be converted into a coordinate")]
73    InvalidValue,
74    /// String passed into from_str was malformed
75    #[cfg(any(
76        feature = "format_dd",
77        feature = "format_dms",
78        feature = "format_geohash"
79    ))]
80    #[error("String passed into from_str was malformed")]
81    Malformed,
82    /// String passed into from_str contained invalid floats
83    #[cfg(any(feature = "format_dd", feature = "format_dms", feature = "resolve_osm"))]
84    #[error("String passed into from_str contained invalid floats")]
85    ParseFloatError(#[from] ParseFloatError),
86    /// Location not resolvable
87    #[cfg(feature = "resolve_osm")]
88    #[error("Location not resolvable")]
89    Unresolveable,
90    /// There was a problem connecting to the API
91    #[cfg(feature = "resolve_osm")]
92    #[error("There was a problem connecting to the API")]
93    ReqwestError(#[from] reqwest::Error),
94}
95
96impl TryFrom<(f64, f64)> for Coordinate {
97    type Error = CoordinateError;
98    /// Try to convert a tuple of coordinates into a Coordinate struct
99    ///
100    /// ```
101    /// /// Parsing works
102    /// # use recoord::Coordinate;
103    /// let from = Coordinate::try_from((10., 20.));
104    /// assert_eq!(Coordinate { lat: 10.0, lng: 20.0}, from.unwrap());
105    /// ```
106    ///
107    /// ```
108    /// /// Detect invalid values
109    /// # use recoord::{Coordinate, CoordinateError};
110    /// let from = Coordinate::try_from((100., 20.));
111    /// assert!(from.is_err());
112    /// ```
113    fn try_from(tupl_coord: (f64, f64)) -> Result<Self, Self::Error> {
114        match tupl_coord {
115            (lat, lng) if (-90.0..=90.0).contains(&lat) && (-180.0..=180.0).contains(&lng) => {
116                Ok(Self { lat, lng })
117            }
118            _ => Err(CoordinateError::InvalidValue),
119        }
120    }
121}
122
123#[cfg(feature = "format_any")]
124impl FromStr for Coordinate {
125    type Err = CoordinateError;
126
127    fn from_str(str_coords: &str) -> Result<Self, Self::Err> {
128        let mut result: Result<Coordinate, CoordinateError> = Err(CoordinateError::MissingParser);
129
130        #[cfg(feature = "format_dd")]
131        {
132            result = result
133                .or_else(|_| formats::dd::DDCoordinate::from_str(str_coords).map(Coordinate::from));
134        }
135        #[cfg(feature = "format_dms")]
136        {
137            result = result.or_else(|_| {
138                formats::dms::DMSCoordinate::from_str(str_coords).map(Coordinate::from)
139            });
140        }
141        #[cfg(feature = "format_geohash")]
142        {
143            result = result
144                .or_else(|_| formats::geohash::Geohash::from_str(str_coords).map(Coordinate::from));
145        }
146
147        result
148    }
149}
150
151// /// Resolver for strings to Coordinates - this should be used for more expensive (and async) resolving
152// pub trait Resolver {
153//     /// Resolve a &str to a Coordinate
154//     fn resolve(s: &str) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<Coordinate, CoordinateError>> + Send + '_>>;
155// }
156
157// #[cfg(test)]
158// mod tests {
159//     #[cfg(feature = "format_dd")]
160//     #[test]
161//     fn format_dd_integer() {
162//         use crate::Coordinate;
163
164//         let expected = Coordinate { lat: 10., lng: 20. };
165//         let real = Coordinate::format_dd("10,20").unwrap();
166//         assert_eq!(expected, real);
167//     }
168//     #[cfg(feature = "format_dd")]
169//     #[test]
170//     fn format_dd_float() {
171//         use crate::Coordinate;
172
173//         let expected = Coordinate { lat: 10., lng: 20. };
174//         let real = Coordinate::format_dd("10.0,20.0").unwrap();
175//         assert_eq!(expected, real);
176//     }
177//     #[cfg(feature = "format_dd")]
178//     #[test]
179//     fn format_dd_invalid() {
180//         use crate::{Coordinate, CoordinateError};
181
182//         match Coordinate::format_dd("Asd,20.0") {
183//             Err(CoordinateError::Malformed) => {}
184//             Err(_) => panic!("Wrong Error"),
185//             Ok(_) => panic!("Should've failed"),
186//         }
187//     }
188// }