1use crate::{
7 Coordinate, Error, Latitude, Longitude,
8 fmt::{FormatOptions, Formatter},
9};
10use serde::{Deserialize, Serialize};
11use core::hash::Hash;
12use std::{
13 fmt::{Display, Write},
14 str::FromStr,
15};
16use uom::{
17 fmt::DisplayStyle,
18 si::{f64::Length, length},
19};
20
21#[cfg(feature = "geojson")]
22use crate::Angle;
23#[cfg(feature = "geojson")]
24use crate::coord::{GEOJSON_COORDINATES_FIELD, GEOJSON_POINT_TYPE, GEOJSON_TYPE_FIELD};
25
26#[macro_export]
45macro_rules! elv {
46 ($value:expr; cm) => {
47 $crate::erlevation::Elevation::centimeters($value)
48 };
49 ($value:expr; m) => {
50 $crate::elevation::Elevation::meters($value)
51 };
52 ($value:expr; km) => {
53 $crate::elevation::Elevation::kilometers($value)
54 };
55 ($value:expr) => {
56 $crate::elevation::Elevation::meters($value)
57 };
58}
59#[allow(clippy::derive_ord_xor_partial_ord)]
67#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
68#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
69pub struct Elevation(Length);
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
89#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
90pub struct CoordinateWithElevation {
91 point: Coordinate,
92 elevation: Elevation,
93}
94
95const ELEVATION_ZERO: f64 = 0.0;
100
101impl Display for Elevation {
102 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
103 if f.alternate() {
104 match self.value() {
105 0.001..0.01 => self
106 .0
107 .into_format_args(length::millimeter, DisplayStyle::Description)
108 .fmt(f),
109 0.01..1.0 => self
110 .0
111 .into_format_args(length::centimeter, DisplayStyle::Description)
112 .fmt(f),
113 1_000.0.. => self
114 .0
115 .into_format_args(length::kilometer, DisplayStyle::Description)
116 .fmt(f),
117 _ => self
118 .0
119 .into_format_args(length::meter, DisplayStyle::Description)
120 .fmt(f),
121 }
122 } else {
123 self.0
124 .into_format_args(length::meter, DisplayStyle::Abbreviation)
125 .fmt(f)
126 }
127 }
128}
129
130impl FromStr for Elevation {
131 type Err = Error;
132
133 fn from_str(s: &str) -> Result<Self, Self::Err> {
134 let length_float =
135 f64::from_str(s).map_err(|_| Error::InvalidNumericFormat(s.to_string()))?;
136 Self::try_from(length_float)
137 }
138}
139
140impl TryFrom<Length> for Elevation {
141 type Error = Error;
142
143 fn try_from(value: Length) -> Result<Self, Self::Error> {
144 Self::try_from(value.value)
145 }
146}
147
148impl TryFrom<f64> for Elevation {
149 type Error = Error;
150
151 fn try_from(value: f64) -> Result<Self, Self::Error> {
152 if value.is_finite() && !value.is_nan() {
153 Ok(Self::meters(value))
154 } else {
155 Err(Error::InvalidNumericValue(value))
156 }
157 }
158}
159
160impl Eq for Elevation {}
161
162impl Hash for Elevation {
163 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
164 self.0.value.to_bits().hash(state);
165 }
166}
167
168impl Ord for Elevation {
169 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
170 self.0.value.total_cmp(&other.0.value)
171 }
172}
173
174impl From<Elevation> for Length {
175 fn from(value: Elevation) -> Self {
176 value.0
177 }
178}
179
180impl From<Elevation> for f64 {
181 fn from(value: Elevation) -> Self {
182 value.0.value
183 }
184}
185
186impl AsRef<Length> for Elevation {
187 fn as_ref(&self) -> &Length {
188 &self.0
189 }
190}
191
192impl Elevation {
193 pub fn zero() -> Self {
198 Self(Length::new::<length::meter>(ELEVATION_ZERO))
199 }
200
201 pub fn centimeters(value: f64) -> Self {
205 assert!(
206 value.is_finite() && !value.is_nan(),
207 "Invalid floating point value, `{value}`"
208 );
209 Self(Length::new::<length::centimeter>(value))
210 }
211
212 pub fn meters(value: f64) -> Self {
216 assert!(
217 value.is_finite() && !value.is_nan(),
218 "Invalid floating point value, `{value}`"
219 );
220 Self(Length::new::<length::meter>(value))
221 }
222
223 pub fn kilometers(value: f64) -> Self {
227 assert!(
228 value.is_finite() && !value.is_nan(),
229 "Invalid floating point value, `{value}`"
230 );
231 Self(Length::new::<length::kilometer>(value))
232 }
233
234 pub fn value(&self) -> f64 {
238 self.0.value
239 }
240
241 pub fn is_zero(&self) -> bool {
245 self.0.value == ELEVATION_ZERO
246 }
247}
248
249impl Display for CoordinateWithElevation {
254 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255 let format = if f.alternate() {
256 FormatOptions::dms()
257 } else {
258 FormatOptions::decimal()
259 };
260 self.format(f, &format)
261 }
262}
263
264impl Formatter for CoordinateWithElevation {
265 fn format<W: Write>(&self, f: &mut W, options: &FormatOptions) -> std::fmt::Result {
266 self.point.format(f, options)?;
267 write!(f, ", {}", self.elevation)
268 }
269}
270
271impl FromStr for CoordinateWithElevation {
272 type Err = Error;
273
274 fn from_str(s: &str) -> Result<Self, Self::Err> {
275 if let Some((coordinate, elevation)) = s.rsplit_once(',') {
276 Ok(Self::new(
277 Coordinate::from_str(coordinate.trim())?,
278 Elevation::from_str(elevation.trim())?,
279 ))
280 } else {
281 Err(Error::InvalidCoordinate)
282 }
283 }
284}
285
286impl CoordinateWithElevation {
287 #[must_use]
291 pub const fn new(point: Coordinate, elevation: Elevation) -> Self {
292 Self { point, elevation }
293 }
294
295 #[must_use]
298 pub const fn new_from(lat: Latitude, long: Longitude, elevation: Elevation) -> Self {
299 Self::new(Coordinate::new(lat, long), elevation)
300 }
301
302 #[must_use]
304 pub const fn with_point(mut self, point: Coordinate) -> Self {
305 self.point = point;
306 self
307 }
308
309 #[must_use]
311 pub const fn with_new_point(mut self, lat: Latitude, long: Longitude) -> Self {
312 self.point = Coordinate::new(lat, long);
313 self
314 }
315
316 #[must_use]
318 pub const fn with_elevation(mut self, elevation: Elevation) -> Self {
319 self.elevation = elevation;
320 self
321 }
322
323 #[must_use]
325 pub const fn point(&self) -> Coordinate {
326 self.point
327 }
328
329 #[must_use]
331 pub const fn elevation(&self) -> Elevation {
332 self.elevation
333 }
334
335 #[must_use]
337 pub fn is_on_equator(&self) -> bool {
338 self.point.latitude().is_on_equator()
339 }
340
341 #[must_use]
343 pub fn is_northern(&self) -> bool {
344 self.point.latitude().is_northern()
345 }
346
347 #[must_use]
349 pub fn is_southern(&self) -> bool {
350 self.point.latitude().is_southern()
351 }
352
353 #[must_use]
355 pub fn is_on_international_reference_meridian(&self) -> bool {
356 self.point
357 .longitude()
358 .is_on_international_reference_meridian()
359 }
360
361 #[must_use]
363 pub fn is_western(&self) -> bool {
364 self.point.longitude().is_western()
365 }
366
367 #[must_use]
369 pub fn is_eastern(&self) -> bool {
370 self.point.longitude().is_eastern()
371 }
372
373 #[must_use]
375 pub fn is_zero_elevation(&self) -> bool {
376 self.elevation.is_zero()
377 }
378}
379
380#[cfg(feature = "geojson")]
381impl From<CoordinateWithElevation> for serde_json::Value {
382 fn from(coord: CoordinateWithElevation) -> Self {
384 serde_json::json!({
385 GEOJSON_TYPE_FIELD: GEOJSON_POINT_TYPE,
386 GEOJSON_COORDINATES_FIELD: [
387 coord.point().latitude().as_float().0,
388 coord.point().longitude().as_float().0,
389 coord.elevation().value()
390 ]
391 })
392 }
393}
394
395#[cfg(feature = "geojson")]
396impl TryFrom<serde_json::Value> for CoordinateWithElevation {
397 type Error = crate::Error;
398
399 fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
400 if value[GEOJSON_TYPE_FIELD] != GEOJSON_POINT_TYPE {
401 return Err(crate::Error::InvalidCoordinate);
402 }
403 let coords = value[GEOJSON_COORDINATES_FIELD]
404 .as_array()
405 .ok_or(crate::Error::InvalidCoordinate)?;
406 if coords.len() != 3 {
407 return Err(crate::Error::InvalidCoordinate);
408 }
409 let lat_val: f64 = coords[0]
410 .as_f64()
411 .ok_or(crate::Error::InvalidNumericFormat(coords[0].to_string()))?;
412 let lon_val: f64 = coords[1]
413 .as_f64()
414 .ok_or(crate::Error::InvalidNumericFormat(coords[1].to_string()))?;
415 let alt_val: f64 = coords[2]
416 .as_f64()
417 .ok_or(crate::Error::InvalidNumericFormat(coords[2].to_string()))?;
418 let lat = Latitude::try_from(lat_val)?;
419 let lon = Longitude::try_from(lon_val)?;
420 let alt = Elevation::try_from(alt_val)?;
421 Ok(CoordinateWithElevation::new_from(lat, lon, alt))
422 }
423}