1use crate::{
4 Error, Latitude, Longitude,
5 fmt::{FormatKind, FormatOptions, Formatter},
6 parse::{self, Parsed},
7};
8use core::{
9 fmt::{Debug, Display, Write},
10 str::FromStr,
11};
12
13#[cfg(feature = "geojson")]
14use crate::Angle;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
35pub struct Coordinate {
36 lat: Latitude, long: Longitude, }
39
40pub const GEO_URL_SCHEME: &str = "geo";
48
49#[cfg(feature = "geojson")]
50pub const GEOJSON_TYPE_FIELD: &str = "type";
51#[cfg(feature = "geojson")]
52pub const GEOJSON_COORDINATES_FIELD: &str = "coordinates";
53#[cfg(feature = "geojson")]
54pub const GEOJSON_POINT_TYPE: &str = "Point";
55
56#[macro_export]
61macro_rules! coord {
62 ($lat:expr ; $lon:expr) => {
63 Coordinate::new($lat, $lon)
64 };
65}
66
67impl From<(Latitude, Longitude)> for Coordinate {
72 fn from(value: (Latitude, Longitude)) -> Self {
73 Self::new(value.0, value.1)
74 }
75}
76
77impl From<Coordinate> for (Latitude, Longitude) {
78 fn from(value: Coordinate) -> Self {
79 (value.lat, value.long)
80 }
81}
82
83impl From<Latitude> for Coordinate {
84 fn from(value: Latitude) -> Self {
85 Self::new(value, Longitude::default())
86 }
87}
88
89impl From<Longitude> for Coordinate {
90 fn from(value: Longitude) -> Self {
91 Self::new(Latitude::default(), value)
92 }
93}
94
95impl FromStr for Coordinate {
96 type Err = Error;
97
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
99 match parse::parse_str(s)? {
100 Parsed::Coordinate(coord) => Ok(coord),
101 _ => Err(Error::InvalidAngle(0.0, 0.0)),
102 }
103 }
104}
105
106impl Display for Coordinate {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112 let format = if f.alternate() {
113 FormatOptions::dms()
114 } else {
115 FormatOptions::decimal()
116 };
117 self.format(f, &format)
118 }
119}
120
121impl Formatter for Coordinate {
122 fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
123 let kind = fmt.kind();
124 self.lat.format(f, fmt)?;
125 write!(f, ",{}", if kind == FormatKind::DmsBare { "" } else { " " })?;
126 self.long.format(f, fmt)
127 }
128}
129
130impl Coordinate {
131 pub const fn new(lat: Latitude, long: Longitude) -> Self {
145 Self { lat, long }
146 }
147
148 #[must_use]
150 pub const fn with_latitude(mut self, lat: Latitude) -> Self {
151 self.lat = lat;
152 self
153 }
154
155 #[must_use]
157 pub const fn with_longitude(mut self, long: Longitude) -> Self {
158 self.long = long;
159 self
160 }
161
162 #[must_use]
164 pub const fn latitude(&self) -> Latitude {
165 self.lat
166 }
167
168 #[must_use]
170 pub const fn φ(&self) -> Latitude {
171 self.lat
172 }
173
174 #[must_use]
176 pub const fn longitude(&self) -> Longitude {
177 self.long
178 }
179
180 #[must_use]
182 pub const fn λ(&self) -> Longitude {
183 self.long
184 }
185
186 #[must_use]
188 pub fn is_on_equator(&self) -> bool {
189 self.lat.is_on_equator()
190 }
191
192 #[must_use]
194 pub fn is_northern(&self) -> bool {
195 self.lat.is_northern()
196 }
197
198 #[must_use]
200 pub fn is_southern(&self) -> bool {
201 self.lat.is_southern()
202 }
203
204 #[must_use]
206 pub fn is_on_international_reference_meridian(&self) -> bool {
207 self.long.is_on_international_reference_meridian()
208 }
209
210 #[must_use]
212 pub fn is_western(&self) -> bool {
213 self.long.is_western()
214 }
215
216 #[must_use]
218 pub fn is_eastern(&self) -> bool {
219 self.long.is_eastern()
220 }
221
222 #[must_use]
238 pub fn to_url_string(&self) -> String {
239 format!(
240 "{}:{},{}",
241 GEO_URL_SCHEME,
242 self.lat.to_formatted_string(&FormatOptions::decimal()),
243 self.long.to_formatted_string(&FormatOptions::decimal())
244 )
245 }
246
247 #[must_use]
266 pub fn to_microformat_string(&self) -> String {
267 format!(
268 "<span class=\"latitude\">{}</span>; <span class=\"longitude\">{}</span>",
269 self.lat.to_formatted_string(&FormatOptions::decimal()),
270 self.long.to_formatted_string(&FormatOptions::decimal())
271 )
272 }
273}
274
275#[cfg(feature = "urn")]
276impl From<Coordinate> for url::Url {
277 fn from(coord: Coordinate) -> Self {
278 Self::parse(&coord.to_url_string()).unwrap()
279 }
280}
281
282#[cfg(feature = "urn")]
283impl TryFrom<url::Url> for Coordinate {
284 type Error = crate::Error;
285
286 fn try_from(url: url::Url) -> Result<Self, Self::Error> {
287 if url.scheme() != GEO_URL_SCHEME {
288 return Err(crate::Error::InvalidUrnScheme);
289 }
290 let path = url.path();
291 let parts: Vec<&str> = path.split(',').collect();
292 if parts.len() != 2 {
293 return Err(crate::Error::InvalidCoordinate);
294 }
295 let lat_val: f64 = parts[0]
296 .parse()
297 .map_err(|_| crate::Error::InvalidCoordinate)?;
298 let lon_val: f64 = parts[1]
299 .parse()
300 .map_err(|_| crate::Error::InvalidCoordinate)?;
301 let lat = Latitude::try_from(lat_val).map_err(|_| crate::Error::InvalidCoordinate)?;
302 let lon = Longitude::try_from(lon_val).map_err(|_| crate::Error::InvalidCoordinate)?;
303 Ok(Coordinate::new(lat, lon))
304 }
305}
306
307#[cfg(feature = "geojson")]
308impl From<Coordinate> for serde_json::Value {
309 fn from(coord: Coordinate) -> Self {
311 serde_json::json!({
312 GEOJSON_TYPE_FIELD: GEOJSON_POINT_TYPE,
313 GEOJSON_COORDINATES_FIELD: [
314 coord.lat.as_float().0,
315 coord.long.as_float().0
316 ]
317 })
318 }
319}
320
321#[cfg(feature = "geojson")]
322impl TryFrom<serde_json::Value> for Coordinate {
323 type Error = crate::Error;
324
325 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
326 if value[GEOJSON_TYPE_FIELD] != GEOJSON_POINT_TYPE {
327 return Err(crate::Error::InvalidCoordinate);
328 }
329 let coords = value[GEOJSON_COORDINATES_FIELD]
330 .as_array()
331 .ok_or(crate::Error::InvalidCoordinate)?;
332 if coords.len() != 2 {
333 return Err(crate::Error::InvalidCoordinate);
334 }
335 let lat_val: f64 = coords[0]
336 .as_f64()
337 .ok_or(crate::Error::InvalidNumericFormat(coords[0].to_string()))?;
338 let lon_val: f64 = coords[1]
339 .as_f64()
340 .ok_or(crate::Error::InvalidNumericFormat(coords[1].to_string()))?;
341 let lat = Latitude::try_from(lat_val)?;
342 let lon = Longitude::try_from(lon_val)?;
343 Ok(Coordinate::new(lat, lon))
344 }
345}