1#[cfg(feature = "elevation")]
4use crate::{Elevation, elevation::CoordinateWithElevation};
5use crate::{
6 Error, Latitude, Longitude,
7 fmt::{FormatKind, FormatOptions, Formatter},
8 latitude::EQUATOR,
9 longitude::INTERNATIONAL_REFERENCE_MERIDIAN,
10 parse::{self, Parsed},
11};
12use core::{
13 fmt::{Debug, Display, Write},
14 hash::Hash,
15 str::FromStr,
16};
17
18#[cfg(feature = "serde")]
19use serde::{Deserialize, Serialize};
20
21#[cfg(feature = "geojson")]
22use crate::Angle;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
43#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
44pub struct Coordinate {
45 lat: Latitude, long: Longitude, }
48
49pub const GEO_URL_SCHEME: &str = "geo";
57
58#[cfg(feature = "geojson")]
59pub const GEOJSON_TYPE_FIELD: &str = "type";
60#[cfg(feature = "geojson")]
61pub const GEOJSON_COORDINATES_FIELD: &str = "coordinates";
62#[cfg(feature = "geojson")]
63pub const GEOJSON_POINT_TYPE: &str = "Point";
64
65#[cfg(not(feature = "elevation"))]
70#[macro_export]
71macro_rules! coord {
72 ($lat:expr ; $lon:expr) => {
73 $crate::coord::Coordinate::new($lat, $lon)
74 };
75}
76
77#[cfg(feature = "elevation")]
78#[macro_export]
79macro_rules! coord {
80 ($lat:expr ; $lon:expr) => {
81 $crate::coord::Coordinate::new($lat, $lon)
82 };
83 ($lat:expr ; $lon:expr ; $alt:expr) => {
84 $crate::elevation::Coordinate::new_from($lat, $lon, $alt)
85 };
86}
87
88impl Default for Coordinate {
93 fn default() -> Self {
94 Self {
95 lat: EQUATOR,
96 long: INTERNATIONAL_REFERENCE_MERIDIAN,
97 }
98 }
99}
100
101impl From<(Latitude, Longitude)> for Coordinate {
102 fn from(value: (Latitude, Longitude)) -> Self {
103 Self::new(value.0, value.1)
104 }
105}
106
107impl From<Coordinate> for (Latitude, Longitude) {
108 fn from(value: Coordinate) -> Self {
109 (value.lat, value.long)
110 }
111}
112
113impl From<Latitude> for Coordinate {
114 fn from(value: Latitude) -> Self {
115 Self::new(value, Longitude::default())
116 }
117}
118
119impl From<Longitude> for Coordinate {
120 fn from(value: Longitude) -> Self {
121 Self::new(Latitude::default(), value)
122 }
123}
124
125impl FromStr for Coordinate {
126 type Err = Error;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 match parse::parse_str(s)? {
130 Parsed::Coordinate(coord) => Ok(coord),
131 _ => Err(Error::InvalidAngle(0.0, 0.0)),
132 }
133 }
134}
135
136impl Display for Coordinate {
137 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142 let format = if f.alternate() {
143 FormatOptions::dms()
144 } else {
145 FormatOptions::decimal()
146 };
147 self.format(f, &format)
148 }
149}
150
151impl Formatter for Coordinate {
152 fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
153 let kind = fmt.kind();
154 self.lat.format(f, fmt)?;
155 write!(f, ",{}", if kind == FormatKind::DmsBare { "" } else { " " })?;
156 self.long.format(f, fmt)
157 }
158}
159
160impl Coordinate {
161 pub const fn new(lat: Latitude, long: Longitude) -> Self {
175 Self { lat, long }
176 }
177
178 #[must_use]
180 pub const fn with_latitude(mut self, lat: Latitude) -> Self {
181 self.lat = lat;
182 self
183 }
184
185 #[must_use]
187 pub const fn with_longitude(mut self, long: Longitude) -> Self {
188 self.long = long;
189 self
190 }
191
192 #[cfg(feature = "elevation")]
193 #[must_use]
194 pub const fn with_elevation(&self, elevation: Elevation) -> CoordinateWithElevation {
195 CoordinateWithElevation::new(*self, elevation)
196 }
197
198 #[must_use]
200 pub const fn latitude(&self) -> Latitude {
201 self.lat
202 }
203
204 #[must_use]
206 pub const fn φ(&self) -> Latitude {
207 self.lat
208 }
209
210 #[must_use]
212 pub const fn longitude(&self) -> Longitude {
213 self.long
214 }
215
216 #[must_use]
218 pub const fn λ(&self) -> Longitude {
219 self.long
220 }
221
222 #[must_use]
224 pub fn is_on_equator(&self) -> bool {
225 self.lat.is_on_equator()
226 }
227
228 #[must_use]
230 pub fn is_northern(&self) -> bool {
231 self.lat.is_northern()
232 }
233
234 #[must_use]
236 pub fn is_southern(&self) -> bool {
237 self.lat.is_southern()
238 }
239
240 #[must_use]
242 pub fn is_on_international_reference_meridian(&self) -> bool {
243 self.long.is_on_international_reference_meridian()
244 }
245
246 #[must_use]
248 pub fn is_western(&self) -> bool {
249 self.long.is_western()
250 }
251
252 #[must_use]
254 pub fn is_eastern(&self) -> bool {
255 self.long.is_eastern()
256 }
257
258 #[must_use]
274 pub fn to_url_string(&self) -> String {
275 format!(
276 "{}:{},{}",
277 GEO_URL_SCHEME,
278 self.lat.to_formatted_string(&FormatOptions::decimal()),
279 self.long.to_formatted_string(&FormatOptions::decimal())
280 )
281 }
282
283 #[must_use]
302 pub fn to_microformat_string(&self) -> String {
303 format!(
304 "<span class=\"latitude\">{}</span>; <span class=\"longitude\">{}</span>",
305 self.lat.to_formatted_string(&FormatOptions::decimal()),
306 self.long.to_formatted_string(&FormatOptions::decimal())
307 )
308 }
309}
310
311#[cfg(feature = "urn")]
312impl From<Coordinate> for url::Url {
313 fn from(coord: Coordinate) -> Self {
314 Self::parse(&coord.to_url_string()).unwrap()
315 }
316}
317
318#[cfg(feature = "urn")]
319impl TryFrom<url::Url> for Coordinate {
320 type Error = crate::Error;
321
322 fn try_from(url: url::Url) -> Result<Self, Self::Error> {
323 if url.scheme() != GEO_URL_SCHEME {
324 return Err(crate::Error::InvalidUrnScheme);
325 }
326 let path = url.path();
327 let parts: Vec<&str> = path.split(',').collect();
328 if parts.len() != 2 {
329 return Err(crate::Error::InvalidCoordinate);
330 }
331 let lat_val: f64 = parts[0]
332 .parse()
333 .map_err(|_| crate::Error::InvalidCoordinate)?;
334 let lon_val: f64 = parts[1]
335 .parse()
336 .map_err(|_| crate::Error::InvalidCoordinate)?;
337 let lat = Latitude::try_from(lat_val).map_err(|_| crate::Error::InvalidCoordinate)?;
338 let lon = Longitude::try_from(lon_val).map_err(|_| crate::Error::InvalidCoordinate)?;
339 Ok(Coordinate::new(lat, lon))
340 }
341}
342
343#[cfg(feature = "geojson")]
344impl From<Coordinate> for serde_json::Value {
345 fn from(coord: Coordinate) -> Self {
347 serde_json::json!({
348 GEOJSON_TYPE_FIELD: GEOJSON_POINT_TYPE,
349 GEOJSON_COORDINATES_FIELD: [
350 coord.lat.as_float().0,
351 coord.long.as_float().0
352 ]
353 })
354 }
355}
356
357#[cfg(feature = "geojson")]
358impl TryFrom<serde_json::Value> for Coordinate {
359 type Error = crate::Error;
360
361 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
362 if value[GEOJSON_TYPE_FIELD] != GEOJSON_POINT_TYPE {
363 return Err(crate::Error::InvalidCoordinate);
364 }
365 let coords = value[GEOJSON_COORDINATES_FIELD]
366 .as_array()
367 .ok_or(crate::Error::InvalidCoordinate)?;
368 if coords.len() != 2 {
369 return Err(crate::Error::InvalidCoordinate);
370 }
371 let lat_val: f64 = coords[0]
372 .as_f64()
373 .ok_or(crate::Error::InvalidNumericFormat(coords[0].to_string()))?;
374 let lon_val: f64 = coords[1]
375 .as_f64()
376 .ok_or(crate::Error::InvalidNumericFormat(coords[1].to_string()))?;
377 let lat = Latitude::try_from(lat_val)?;
378 let lon = Longitude::try_from(lon_val)?;
379 Ok(Coordinate::new(lat, lon))
380 }
381}