geo_uri/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![warn(
4    clippy::all,
5    missing_copy_implementations,
6    missing_debug_implementations,
7    rust_2018_idioms,
8    rustdoc::broken_intra_doc_links,
9    trivial_casts,
10    trivial_numeric_casts,
11    renamed_and_removed_lints,
12    unsafe_code,
13    unstable_features,
14    unused_import_braces,
15    unused_qualifications
16)]
17#![deny(missing_docs)]
18
19use std::fmt;
20use std::num::ParseFloatError;
21use std::str::FromStr;
22
23#[cfg(feature = "serde")]
24use serde::{
25    de::{Deserialize, Visitor},
26    ser::Serialize,
27};
28use thiserror::Error;
29#[cfg(feature = "url")]
30use url::Url;
31
32/// The scheme name of a geo URI.
33const URI_SCHEME_NAME: &str = "geo";
34
35/// Possible geo URI errors.
36#[derive(Debug, Error, Eq, PartialEq)]
37pub enum Error {
38    /// The geo URI contains an unparsable/invalid coordinate.
39    #[error("Invalid coordinate in geo URI: {0}")]
40    InvalidCoord(ParseFloatError),
41
42    /// The geo URI contains an unsupported/invalid coordinate reference system.
43    #[error("Invalid coordinate reference system")]
44    InvalidCoordRefSystem,
45
46    /// The geo URI contains an unparsable/invalid uncertainty distance.
47    #[error("Invalid distance in geo URI: {0}")]
48    InvalidUncertainty(ParseFloatError),
49
50    /// The geo URI contains no coordinates.
51    #[error("Missing coordinates in geo URI")]
52    MissingCoords,
53
54    /// The geo URI lacks the latitude coordinate.
55    #[error("Missing latitude coordinate in geo URI")]
56    MissingLatitude,
57
58    /// The geo URI lacks the longitude coordinate.
59    #[error("Missing longitude coordinate in geo URI")]
60    MissingLongitude,
61
62    /// The geo URI is missing a proper scheme, i.e. the prefix `geo:`.
63    #[error("Missing geo URI scheme")]
64    MissingScheme,
65
66    /// The latitude coordinate is out of range of `-90.0..=90.0` degrees.
67    ///
68    /// This can only fail for the WGS-84 coordinate reference system.
69    #[error("Latitude coordinate is out of range")]
70    OutOfRangeLatitude,
71
72    /// The longitude coordinate is out of range of `-180.0..=180.0` degrees.
73    ///
74    /// This can only fail for the WGS-84 coordinate reference system.
75    #[error("Longitude coordinate is out of range")]
76    OutOfRangeLongitude,
77
78    /// The uncertainty distance is not positive.
79    #[error("Uncertainty distance not positive")]
80    OutOfRangeUncertainty,
81}
82
83/// The reference system of the provided coordinates.
84///
85/// Currently only the `WGS-84` coordinate reference system is supported.
86/// It defines the latitude and longitude of the [`GeoUri`] to be in decimal degrees and the
87/// altitude in meters.
88///
89/// For more details see the
90/// [component description](ttps://www.rfc-editor.org/rfc/rfc5870#section-3.4.2) in
91/// [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870).
92#[non_exhaustive]
93#[derive(Copy, Clone, Debug, PartialEq, Eq)]
94pub enum CoordRefSystem {
95    /// The WGS-84 coordinate reference system.
96    Wgs84,
97}
98
99impl CoordRefSystem {
100    /// Validates geolocation coordinates against the selected coordinate reference system.
101    ///
102    /// # Examples
103    ///
104    /// ```rust
105    /// # use geo_uri::{CoordRefSystem, Error};
106    /// let crs = CoordRefSystem::Wgs84;
107    /// assert_eq!(crs.validate(52.107, 5.134), Ok(()));
108    /// assert_eq!(
109    ///     crs.validate(100.0, 5.134), // Latitude not in range `-90.0..=90.0`!
110    ///     Err(Error::OutOfRangeLatitude)
111    /// );
112    /// assert_eq!(
113    ///     crs.validate(51.107, -200.0), // Longitude not in range `-180.0..=180.0`!
114    ///     Err(Error::OutOfRangeLongitude)
115    /// );
116    /// ```
117    ///
118    /// # Errors
119    ///
120    /// An error is returned if the latitude/longitude is out of range with respect to the
121    /// coordinate reference system.
122    pub fn validate(&self, latitude: f64, longitude: f64) -> Result<(), Error> {
123        // This holds only for WGS-84, but it is the only one supported right now!
124        if !(-90.0..=90.0).contains(&latitude) {
125            return Err(Error::OutOfRangeLatitude);
126        }
127
128        // This holds only for WGS-84, but it is the only one supported right now!
129        if !(-180.0..=180.0).contains(&longitude) {
130            return Err(Error::OutOfRangeLongitude);
131        }
132
133        Ok(())
134    }
135}
136
137impl Default for CoordRefSystem {
138    fn default() -> Self {
139        Self::Wgs84
140    }
141}
142
143/// A uniform resource identifier for geographic locations (geo URI).
144///
145/// # Examples
146///
147/// ## Parsing
148///
149/// You can get a [`GeoUri`] by converting it from a geo URI string ([`&str`]):
150///
151/// ```rust
152/// use geo_uri::GeoUri;
153/// # use geo_uri::Error;
154///
155/// # fn main() -> Result<(), Error> {
156/// let geo_uri = GeoUri::try_from("geo:52.107,5.134,3.6;u=1000")?;
157/// assert_eq!(geo_uri.latitude(), 52.107);
158/// assert_eq!(geo_uri.longitude(), 5.134);
159/// assert_eq!(geo_uri.altitude(), Some(3.6));
160/// assert_eq!(geo_uri.uncertainty(), Some(1000.0));
161/// # Ok(())
162/// # }
163/// ```
164///
165/// or by calling the [`parse`](str::parse) method on a string (using the [`TryFrom`] trait):
166/// ```
167/// use geo_uri::GeoUri;
168/// # use geo_uri::Error;
169///
170/// # fn main() -> Result<(), Error> {
171/// let geo_uri: GeoUri = "geo:52.107,5.134;u=2000.0".parse()?;
172/// assert_eq!(geo_uri.latitude(), 52.107);
173/// assert_eq!(geo_uri.longitude(), 5.134);
174/// assert_eq!(geo_uri.altitude(), None);
175/// assert_eq!(geo_uri.uncertainty(), Some(2000.0));
176/// # Ok(())
177/// # }
178/// ```
179///
180/// It is also possible to call the parse function directly:
181///
182/// ```rust
183/// use geo_uri::GeoUri;
184/// # use geo_uri::Error;
185///
186/// # fn main() -> Result<(), Error> {
187/// let geo_uri = GeoUri::parse("geo:52.107,5.134,3.6")?;
188/// assert_eq!(geo_uri.latitude(), 52.107);
189/// assert_eq!(geo_uri.longitude(), 5.134);
190/// assert_eq!(geo_uri.altitude(), Some(3.6));
191/// assert_eq!(geo_uri.uncertainty(), None);
192/// # Ok(())
193/// # }
194/// ```
195///
196/// ## Generating
197///
198/// To get an geo URI string from some coordinates, use the [`GeoUriBuilder`]:
199///
200/// ```rust
201/// use geo_uri::GeoUri;
202/// # use geo_uri::GeoUriBuilderError;
203///
204/// # fn main() -> Result<(), GeoUriBuilderError> {
205/// let geo_uri = GeoUri::builder()
206///     .latitude(52.107)
207///     .longitude(5.134)
208///     .uncertainty(1_000.0)
209///     .build()?;
210/// assert_eq!(
211///     geo_uri.to_string(),
212///     String::from("geo:52.107,5.134;u=1000")
213/// );
214/// assert_eq!(
215///     format!("{geo_uri}"),
216///     String::from("geo:52.107,5.134;u=1000")
217/// );
218/// # Ok(())
219/// # }
220/// ```
221///
222/// It is also possible to construct a [`GeoUri`] struct from coordinate tuples
223/// using the [`TryFrom`] trait:
224///
225/// ```rust
226/// use geo_uri::GeoUri;
227///
228/// let geo_uri = GeoUri::try_from((52.107, 5.134)).expect("valid coordinates");
229/// let geo_uri = GeoUri::try_from((52.107, 5.134, 3.6)).expect("valid coordinates");
230/// ```
231///
232/// # See also
233///
234/// For the proposed IEEE standard, see [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870).
235#[derive(Copy, Clone, Debug, Default)]
236pub struct GeoUri {
237    /// The coordinate reference system used by the coordinates of this URI.
238    crs: CoordRefSystem,
239
240    /// The latitude coordinate of a location.
241    ///
242    /// For the WGS-84 coordinate reference system, this should be in the range of
243    /// `-90.0` up until including `90.0` degrees.
244    latitude: f64,
245
246    /// The longitude coordinate of a location.
247    ///
248    /// For the WGS-84 coordinate reference system, this should be in the range of
249    /// `-180.0` up until including `180.0` degrees.
250    longitude: f64,
251
252    /// The altitude coordinate of a location, if provided.
253    altitude: Option<f64>,
254
255    /// The uncertainty around the location as a radius (distance) in meters.
256    ///
257    /// This distance needs to be positive.
258    uncertainty: Option<f64>,
259}
260
261impl GeoUri {
262    /// Return a builder for `GeoUri`.
263    pub fn builder() -> GeoUriBuilder {
264        GeoUriBuilder::default()
265    }
266
267    /// Try parsing a geo URI string into a `GeoUri`.
268    ///
269    /// For the geo URI scheme syntax, see the proposed IEEE standard
270    /// [RFC 5870](https://www.rfc-editor.org/rfc/rfc5870#section-3.3).
271    ///
272    /// # Errors
273    ///
274    /// Will return an error if the parsing fails in any way.
275    pub fn parse(uri: &str) -> Result<Self, Error> {
276        let uri = uri.to_ascii_lowercase();
277        let uri_path = uri.strip_prefix("geo:").ok_or(Error::MissingScheme)?;
278        let mut parts = uri_path.split(';');
279
280        // Parse the coordinate part.
281        let coords_part = parts.next().expect("Split always yields at least one part");
282        // Don't iterate over anything if the coordinate part is empty!
283        let mut coords = if coords_part.is_empty() {
284            return Err(Error::MissingCoords);
285        } else {
286            coords_part.splitn(3, ',')
287        };
288        let latitude = coords
289            .next()
290            .ok_or(Error::MissingLatitude) // This cannot really happen
291            .and_then(|lat_s| lat_s.parse().map_err(Error::InvalidCoord))?;
292
293        let longitude = coords
294            .next()
295            .ok_or(Error::MissingLongitude)
296            .and_then(|lon_s| lon_s.parse().map_err(Error::InvalidCoord))?;
297
298        let altitude = coords
299            .next()
300            .map(|alt_s| alt_s.parse().map_err(Error::InvalidCoord))
301            .transpose()?;
302
303        // Parse the remaining (parameters) parts.
304        //
305        // TODO: Handle percent encoding of the parameters.
306        //
307        // If the "crs" parameter is passed, its value must be "wgs84" or it is unsupported.
308        // It can be followed by a "u" parameter or that can be the first one.
309        // All other parameters are ignored.
310        let mut param_parts = parts.flat_map(|part| part.split_once('='));
311        let (crs, uncertainty) = match param_parts.next() {
312            Some(("crs", value)) => {
313                if value != "wgs84" {
314                    return Err(Error::InvalidCoordRefSystem);
315                }
316
317                match param_parts.next() {
318                    Some(("u", value)) => (
319                        CoordRefSystem::Wgs84,
320                        Some(value.parse().map_err(Error::InvalidUncertainty)?),
321                    ),
322                    Some(_) | None => (CoordRefSystem::Wgs84, None),
323                }
324            }
325            Some(("u", value)) => (
326                CoordRefSystem::default(),
327                Some(value.parse().map_err(Error::InvalidUncertainty)?),
328            ),
329            Some(_) | None => (CoordRefSystem::default(), None),
330        };
331
332        // Validate the geo URI before returning it.
333        let geo_uri = GeoUri {
334            crs,
335            latitude,
336            longitude,
337            altitude,
338            uncertainty,
339        };
340        geo_uri.validate()?;
341
342        Ok(geo_uri)
343    }
344
345    /// Returns the latitude coordinate.
346    pub fn latitude(&self) -> f64 {
347        self.latitude
348    }
349
350    /// Changes the latitude coordinate.
351    ///
352    /// # Errors
353    ///
354    /// If the latitude is out of range for the coordinate reference system, an error will be
355    /// returned.
356    pub fn set_latitude(&mut self, latitude: f64) -> Result<(), Error> {
357        self.crs.validate(latitude, self.longitude)?;
358        self.latitude = latitude;
359
360        Ok(())
361    }
362
363    /// Returns the longitude coordinate.
364    pub fn longitude(&self) -> f64 {
365        self.longitude
366    }
367
368    /// Changes the longitude coordinate.
369    ///
370    /// # Errors
371    ///
372    /// If the longitude is out of range for the coordinate reference system, an error will be
373    /// returned.
374    pub fn set_longitude(&mut self, longitude: f64) -> Result<(), Error> {
375        self.crs.validate(self.latitude, longitude)?;
376        self.longitude = longitude;
377
378        Ok(())
379    }
380
381    /// Returns the altitude coordinate (if any).
382    pub fn altitude(&self) -> Option<f64> {
383        self.altitude
384    }
385
386    /// Changes the altitude coordinate.
387    pub fn set_altitude(&mut self, altitude: Option<f64>) {
388        self.altitude = altitude;
389    }
390
391    /// Returns the uncertainty around the location.
392    pub fn uncertainty(&self) -> Option<f64> {
393        self.uncertainty
394    }
395
396    /// Changes the uncertainty around the location.
397    ///
398    /// # Errors
399    ///
400    /// If the uncertainty distance is not zero or positive, an error will be returned.
401    pub fn set_uncertainty(&mut self, uncertainty: Option<f64>) -> Result<(), Error> {
402        if let Some(unc) = uncertainty {
403            if unc < 0.0 {
404                return Err(Error::OutOfRangeUncertainty);
405            }
406        }
407        self.uncertainty = uncertainty;
408
409        Ok(())
410    }
411
412    /// Validates the coordinates.
413    ///
414    /// This is only meant for internal use to prevent returning [`GeoUri`] objects that are
415    /// actually invalid.
416    ///
417    /// # Errors
418    ///
419    /// Returns an error if the current latitude/longitude is invalid with respect to the current
420    /// coordinate reference system, or if the uncertainty, if set, is not zero or positive.
421    fn validate(&self) -> Result<(), Error> {
422        // Validate the latitude/longitude against the coordinate reference system.
423        self.crs.validate(self.latitude, self.longitude)?;
424
425        // Ensure that the uncertainty is not negatify, if set.
426        if let Some(unc) = self.uncertainty {
427            if unc < 0.0 {
428                return Err(Error::OutOfRangeUncertainty);
429            }
430        }
431
432        Ok(())
433    }
434}
435
436/// Builder for [`GeoUri`].
437///
438/// Can be used to correctly construct a geo URI without using conversion or parsing.
439/// Use [`GeoUri::builder`] to construct this builder.
440///
441/// # Examples
442///
443/// ```
444/// # use geo_uri::GeoUri;
445/// let mut builder = GeoUri::builder();
446///
447/// // Required fields.
448/// builder.latitude(52.107);
449/// builder.longitude(5.134);
450/// let geo_uri = builder.build().expect("valid geo URI");
451/// assert_eq!(
452///   geo_uri.to_string(),
453///   String::from("geo:52.107,5.134")
454/// );
455///
456/// // Optional fields.
457/// builder.altitude(3.6);
458/// builder.uncertainty(1000.0);
459/// let geo_uri = builder.build().expect("valid geo URI");
460/// assert_eq!(
461///   geo_uri.to_string(),
462///   String::from("geo:52.107,5.134,3.6;u=1000")
463/// );
464/// ```
465#[derive(Clone, Copy, Debug, Default)]
466pub struct GeoUriBuilder {
467    /// The coordinate reference system used by the coordinates of this URI.
468    crs: Option<CoordRefSystem>,
469
470    /// The latitude coordinate of a location.
471    ///
472    /// For the WGS-84 coordinate reference system, this should be in the range of
473    /// `-90.0` up until including `90.0` degrees.
474    latitude: Option<f64>,
475
476    /// The longitude coordinate of a location.
477    ///
478    /// For the WGS-84 coordinate reference system, this should be in the range of
479    /// `-180.0` up until including `180.0` degrees.
480    longitude: Option<f64>,
481
482    /// The altitude coordinate of a location, if provided.
483    altitude: Option<f64>,
484
485    /// The uncertainty around the location as a radius (distance) in meters.
486    ///
487    /// This distance needs to be positive.
488    uncertainty: Option<f64>,
489}
490
491impl GeoUriBuilder {
492    /// The coordinate reference system used by the coordinates of this URI.
493    pub fn crs(&mut self, value: CoordRefSystem) -> &mut Self {
494        self.crs = Some(value);
495
496        self
497    }
498
499    /// The latitude coordinate of a location.
500    ///
501    /// For the WGS-84 coordinate reference system, this should be in the range of
502    /// `-90.0` up until including `90.0` degrees.
503    pub fn latitude(&mut self, value: f64) -> &mut Self {
504        self.latitude = Some(value);
505
506        self
507    }
508
509    /// The longitude coordinate of a location.
510    ///
511    /// For the WGS-84 coordinate reference system, this should be in the range of
512    /// `-180.0` up until including `180.0` degrees.
513    pub fn longitude(&mut self, value: f64) -> &mut Self {
514        self.longitude = Some(value);
515
516        self
517    }
518
519    /// The altitude coordinate of a location, if provided.
520    pub fn altitude(&mut self, value: f64) -> &mut Self {
521        self.altitude = Some(value);
522
523        self
524    }
525
526    /// The uncertainty around the location as a radius (distance) in meters.
527    ///
528    /// This distance needs to be positive, otherwise a later call to [`GeoUriBuilder::build`] will
529    /// fail.
530    #[allow(unused_mut)]
531    pub fn uncertainty(&mut self, value: f64) -> &mut Self {
532        self.uncertainty = Some(value);
533
534        self
535    }
536
537    /// Builds a new [`GeoUri`].
538    ///
539    /// # Errors
540    ///
541    /// Returns an error if the uncertainty is set but is not positive or if a required field has
542    /// not been initialized.
543    pub fn build(&self) -> Result<GeoUri, GeoUriBuilderError> {
544        self.validate()?;
545
546        Ok(GeoUri {
547            crs: self.crs.unwrap_or_default(),
548            latitude: self
549                .latitude
550                .ok_or(GeoUriBuilderError::UninitializedField("latitude"))?,
551            longitude: self
552                .longitude
553                .ok_or(GeoUriBuilderError::UninitializedField("longitude"))?,
554            altitude: self.altitude,
555            uncertainty: self.uncertainty,
556        })
557    }
558
559    /// Validates the coordinates.
560    ///
561    /// Performs the validation that [`GeoUri::validate`] would perform, but also checks if the
562    /// uncertainty value is not negative if provided to the builder.
563    ///
564    /// # Errors
565    ///
566    /// Returns an error if the currently configured coordinate values are invalid.
567    fn validate(&self) -> Result<(), GeoUriBuilderError> {
568        self.crs.unwrap_or_default().validate(
569            self.latitude.unwrap_or_default(),
570            self.longitude.unwrap_or_default(),
571        )?;
572
573        if let Some(unc) = self.uncertainty {
574            if unc < 0.0 {
575                Err(Error::OutOfRangeUncertainty)?;
576            }
577        }
578
579        Ok(())
580    }
581}
582
583/// Possible errors when using the geo URI builder [`GeoUriBuilder`].
584#[non_exhaustive]
585#[derive(Debug, Error)]
586pub enum GeoUriBuilderError {
587    /// Uninitialized field
588    #[error("uninitialized field `{0}`")]
589    UninitializedField(&'static str),
590
591    /// Custom validation error
592    #[error("validation error: {0}")]
593    ValidationError(#[from] Error),
594}
595
596#[cfg(feature = "serde")]
597struct GeoUriVisitor;
598
599#[cfg(feature = "serde")]
600impl<'de> Visitor<'de> for GeoUriVisitor {
601    type Value = GeoUri;
602
603    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
604        write!(formatter, "a string starting with {URI_SCHEME_NAME}:")
605    }
606
607    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
608    where
609        E: serde::de::Error,
610    {
611        GeoUri::parse(v).map_err(E::custom)
612    }
613}
614
615#[cfg(feature = "serde")]
616#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
617impl<'de> Deserialize<'de> for GeoUri {
618    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
619    where
620        D: serde::Deserializer<'de>,
621    {
622        deserializer.deserialize_str(GeoUriVisitor)
623    }
624}
625
626impl fmt::Display for GeoUri {
627    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628        let Self {
629            latitude,
630            longitude,
631            ..
632        } = self;
633        write!(f, "{URI_SCHEME_NAME}:{latitude},{longitude}")?;
634
635        if let Some(altitude) = self.altitude {
636            write!(f, ",{altitude}")?;
637        }
638
639        // Don't write the CRS since there is only one supported at the moment.
640        if let Some(uncertainty) = self.uncertainty {
641            write!(f, ";u={uncertainty}")?;
642        }
643
644        Ok(())
645    }
646}
647
648#[cfg(feature = "url")]
649#[cfg_attr(docsrs, doc(cfg(feature = "url")))]
650impl From<&GeoUri> for Url {
651    fn from(geo_uri: &GeoUri) -> Self {
652        Url::parse(&geo_uri.to_string()).expect("valid URL")
653    }
654}
655
656#[cfg(feature = "url")]
657#[cfg_attr(docsrs, doc(cfg(feature = "url")))]
658impl From<GeoUri> for Url {
659    fn from(geo_uri: GeoUri) -> Self {
660        Url::from(&geo_uri)
661    }
662}
663
664impl FromStr for GeoUri {
665    type Err = Error;
666
667    fn from_str(s: &str) -> Result<Self, Self::Err> {
668        Self::parse(s)
669    }
670}
671
672#[cfg(feature = "serde")]
673#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
674impl Serialize for GeoUri {
675    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
676    where
677        S: serde::Serializer,
678    {
679        serializer.serialize_str(&self.to_string())
680    }
681}
682
683impl TryFrom<&str> for GeoUri {
684    type Error = Error;
685
686    fn try_from(value: &str) -> Result<Self, Self::Error> {
687        Self::parse(value)
688    }
689}
690
691impl TryFrom<(f64, f64)> for GeoUri {
692    type Error = Error;
693
694    fn try_from((latitude, longitude): (f64, f64)) -> Result<Self, Self::Error> {
695        let geo_uri = GeoUri {
696            latitude,
697            longitude,
698            ..Default::default()
699        };
700        geo_uri.validate()?;
701
702        Ok(geo_uri)
703    }
704}
705
706impl TryFrom<(f64, f64, f64)> for GeoUri {
707    type Error = Error;
708
709    fn try_from((latitude, longitude, altitude): (f64, f64, f64)) -> Result<Self, Self::Error> {
710        let geo_uri = GeoUri {
711            latitude,
712            longitude,
713            altitude: Some(altitude),
714            ..Default::default()
715        };
716        geo_uri.validate()?;
717
718        Ok(geo_uri)
719    }
720}
721
722#[cfg(feature = "url")]
723#[cfg_attr(docsrs, doc(cfg(feature = "url")))]
724impl TryFrom<&Url> for GeoUri {
725    type Error = Error;
726
727    fn try_from(url: &Url) -> Result<Self, Self::Error> {
728        GeoUri::parse(url.as_str())
729    }
730}
731
732#[cfg(feature = "url")]
733#[cfg_attr(docsrs, doc(cfg(feature = "url")))]
734impl TryFrom<Url> for GeoUri {
735    type Error = Error;
736
737    fn try_from(url: Url) -> Result<Self, Self::Error> {
738        GeoUri::try_from(&url)
739    }
740}
741
742impl PartialEq for GeoUri {
743    fn eq(&self, other: &Self) -> bool {
744        // In the WGS-84 CRS the the longitude is ignored for the poles.
745        let ignore_longitude = self.crs == CoordRefSystem::Wgs84 && self.latitude.abs() == 90.0;
746
747        self.crs == other.crs
748            && self.latitude == other.latitude
749            && (ignore_longitude || self.longitude == other.longitude)
750            && self.altitude == other.altitude
751            && self.uncertainty == other.uncertainty
752    }
753}
754
755#[cfg(test)]
756mod tests {
757    #[cfg(feature = "serde")]
758    use serde_test::{assert_de_tokens_error, assert_tokens, Token};
759
760    use super::*;
761
762    #[test]
763    fn coord_ref_system_default() {
764        assert_eq!(CoordRefSystem::default(), CoordRefSystem::Wgs84);
765    }
766
767    #[test]
768    fn coord_ref_system_validate() {
769        let crs = CoordRefSystem::Wgs84;
770        assert_eq!(crs.validate(52.107, 5.134), Ok(()));
771        assert_eq!(crs.validate(100.0, 5.134), Err(Error::OutOfRangeLatitude));
772        assert_eq!(
773            crs.validate(51.107, -200.0),
774            Err(Error::OutOfRangeLongitude)
775        );
776    }
777
778    #[test]
779    fn geo_uri_builder() -> Result<(), GeoUriBuilderError> {
780        let mut builder = GeoUri::builder();
781        assert!(matches!(
782            builder.build(),
783            Err(GeoUriBuilderError::UninitializedField("latitude"))
784        ));
785
786        builder.latitude(52.107);
787        assert!(matches!(
788            builder.build(),
789            Err(GeoUriBuilderError::UninitializedField("longitude"))
790        ));
791
792        builder.longitude(5.134);
793        let geo_uri = builder.build()?;
794        assert_eq!(geo_uri.latitude, 52.107);
795        assert_eq!(geo_uri.longitude, 5.134);
796        assert_eq!(geo_uri.altitude, None);
797        assert_eq!(geo_uri.uncertainty, None);
798
799        builder.latitude(100.0);
800        assert!(matches!(
801            builder.build(),
802            Err(GeoUriBuilderError::ValidationError(_))
803        ));
804
805        builder.latitude(52.107).longitude(-200.0);
806        assert!(matches!(
807            builder.build(),
808            Err(GeoUriBuilderError::ValidationError(_))
809        ));
810
811        builder.longitude(5.134).uncertainty(-200.0);
812        assert!(matches!(
813            builder.build(),
814            Err(GeoUriBuilderError::ValidationError(_))
815        ));
816
817        Ok(())
818    }
819
820    #[test]
821    fn geo_uri_parse() -> Result<(), Error> {
822        let geo_uri = GeoUri::parse("geo:52.107,5.134")?;
823        assert_eq!(geo_uri.latitude, 52.107);
824        assert_eq!(geo_uri.longitude, 5.134);
825        assert_eq!(geo_uri.altitude, None);
826        assert_eq!(geo_uri.uncertainty, None);
827
828        let geo_uri = GeoUri::parse("52.107,5.134");
829        assert!(matches!(geo_uri, Err(Error::MissingScheme)));
830
831        let geo_uri = GeoUri::parse("geo:100.0,5.134");
832        assert!(matches!(geo_uri, Err(Error::OutOfRangeLatitude)));
833
834        let geo_uri = GeoUri::parse("geo:62.107,-200.0");
835        assert!(matches!(geo_uri, Err(Error::OutOfRangeLongitude)));
836
837        let geo_uri = GeoUri::parse("geo:geo:52.107,5.134");
838        assert!(matches!(geo_uri, Err(Error::InvalidCoord(_))));
839
840        let geo_uri = GeoUri::parse("geo:");
841        assert!(matches!(geo_uri, Err(Error::MissingCoords)));
842
843        let geo_uri = GeoUri::parse("geo:;u=5000");
844        assert!(matches!(geo_uri, Err(Error::MissingCoords)));
845
846        let geo_uri = GeoUri::parse("geo:52.107;u=1000");
847        assert!(matches!(geo_uri, Err(Error::MissingLongitude)));
848
849        let geo_uri = GeoUri::parse("geo:52.107,;u=1000");
850        assert!(matches!(geo_uri, Err(Error::InvalidCoord(_))));
851
852        let geo_uri = GeoUri::parse("geo:52.107,,6.50;u=1000");
853        assert!(matches!(geo_uri, Err(Error::InvalidCoord(_))));
854
855        let geo_uri = GeoUri::parse("geo:52.107,5.134,;u=1000");
856        assert!(matches!(geo_uri, Err(Error::InvalidCoord(_))));
857
858        let geo_uri = GeoUri::parse("geo:52.107,5.134,3.6")?;
859        assert_eq!(geo_uri.latitude, 52.107);
860        assert_eq!(geo_uri.longitude, 5.134);
861        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
862        assert_eq!(geo_uri.uncertainty, None);
863
864        let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;u=");
865        assert!(matches!(geo_uri, Err(Error::InvalidUncertainty(_))));
866
867        let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;u=foo");
868        assert!(matches!(geo_uri, Err(Error::InvalidUncertainty(_))));
869
870        let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;crs=wgs84;u=foo");
871        assert!(matches!(geo_uri, Err(Error::InvalidUncertainty(_))));
872
873        let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;u=-10.0");
874        assert!(matches!(geo_uri, Err(Error::OutOfRangeUncertainty)));
875
876        let geo_uri = GeoUri::parse("geo:52.107,5.134,3.6;u=25000")?;
877        assert_eq!(geo_uri.latitude, 52.107);
878        assert_eq!(geo_uri.longitude, 5.134);
879        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
880        assert_eq!(geo_uri.uncertainty, Some(25_000.0));
881
882        let geo_uri = GeoUri::parse("geo:52.107,5.134,3.6;crs=wgs84;u=25000")?;
883        assert_eq!(geo_uri.latitude, 52.107);
884        assert_eq!(geo_uri.longitude, 5.134);
885        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
886        assert_eq!(geo_uri.uncertainty, Some(25_000.0));
887
888        let geo_uri = GeoUri::parse("geo:52.107,5.134,3.6;CRS=wgs84;U=25000")?;
889        assert_eq!(geo_uri.uncertainty, Some(25_000.0));
890
891        let geo_uri = GeoUri::parse("geo:52.107,5.134,3.6;crs=wgs84;u=25000;foo=bar")?;
892        assert_eq!(geo_uri.latitude, 52.107);
893        assert_eq!(geo_uri.longitude, 5.134);
894        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
895        assert_eq!(geo_uri.uncertainty, Some(25_000.0));
896
897        let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;crs=foo");
898        assert!(matches!(geo_uri, Err(Error::InvalidCoordRefSystem)));
899
900        let geo_uri = GeoUri::parse("geo:52.107,5.34,3.6;crs=wgs84")?;
901        assert!(matches!(geo_uri.crs, CoordRefSystem::Wgs84));
902
903        // Examples from RFC 5870 (sections 1, 6.1, 6.2 and 9.4)!
904        let geo_uri = GeoUri::parse("geo:13.4125,103.8667")?;
905        assert_eq!(geo_uri.latitude, 13.4125);
906        assert_eq!(geo_uri.longitude, 103.8667);
907        assert_eq!(geo_uri.altitude, None);
908        assert_eq!(geo_uri.uncertainty, None);
909
910        let geo_uri = GeoUri::parse("geo:48.2010,16.3695,183")?;
911        assert_eq!(geo_uri.latitude, 48.2010);
912        assert_eq!(geo_uri.longitude, 16.3695);
913        assert_eq!(geo_uri.altitude.unwrap(), 183.0);
914        assert_eq!(geo_uri.uncertainty, None);
915
916        let geo_uri = GeoUri::parse("geo:48.198634,16.371648;crs=wgs84;u=40")?;
917        assert_eq!(geo_uri.crs, CoordRefSystem::Wgs84);
918        assert_eq!(geo_uri.latitude, 48.198634);
919        assert_eq!(geo_uri.longitude, 16.371648);
920        assert_eq!(geo_uri.altitude, None);
921        assert_eq!(geo_uri.uncertainty, Some(40.0));
922
923        let geo_uri = GeoUri::parse("geo:94,0");
924        assert_eq!(geo_uri, Err(Error::OutOfRangeLatitude));
925
926        Ok(())
927    }
928
929    #[test]
930    fn geo_uri_validate() {
931        let mut geo_uri = GeoUri {
932            crs: CoordRefSystem::Wgs84,
933            latitude: 52.107,
934            longitude: 5.134,
935            altitude: None,
936            uncertainty: None,
937        };
938        assert_eq!(geo_uri.validate(), Ok(()));
939
940        geo_uri.latitude = 100.0;
941        assert_eq!(geo_uri.validate(), Err(Error::OutOfRangeLatitude));
942
943        geo_uri.latitude = 52.107;
944        geo_uri.longitude = -200.0;
945        assert_eq!(geo_uri.validate(), Err(Error::OutOfRangeLongitude));
946
947        geo_uri.longitude = 5.134;
948        geo_uri.uncertainty = Some(-2000.0);
949        assert_eq!(geo_uri.validate(), Err(Error::OutOfRangeUncertainty));
950    }
951
952    #[test]
953    fn geo_uri_get_set() {
954        let mut geo_uri = GeoUri {
955            crs: CoordRefSystem::Wgs84,
956            latitude: 52.107,
957            longitude: 5.134,
958            altitude: None,
959            uncertainty: None,
960        };
961        assert_eq!(geo_uri.latitude(), 52.107);
962        assert_eq!(geo_uri.longitude(), 5.134);
963        assert_eq!(geo_uri.altitude(), None);
964        assert_eq!(geo_uri.uncertainty(), None);
965
966        assert_eq!(geo_uri.set_latitude(53.107), Ok(()));
967        assert_eq!(geo_uri.set_latitude(100.0), Err(Error::OutOfRangeLatitude));
968        assert_eq!(geo_uri.latitude(), 53.107);
969
970        assert_eq!(geo_uri.set_longitude(6.134), Ok(()));
971        assert_eq!(
972            geo_uri.set_longitude(-200.0),
973            Err(Error::OutOfRangeLongitude)
974        );
975        assert_eq!(geo_uri.longitude(), 6.134);
976
977        geo_uri.set_altitude(Some(3.6));
978        assert_eq!(geo_uri.altitude(), Some(3.6));
979
980        assert_eq!(geo_uri.set_uncertainty(Some(25_000.0)), Ok(()));
981        assert_eq!(
982            geo_uri.set_uncertainty(Some(-100.0)),
983            Err(Error::OutOfRangeUncertainty)
984        );
985        assert_eq!(geo_uri.uncertainty(), Some(25_000.0));
986    }
987
988    #[test]
989    fn geo_uri_display() {
990        let mut geo_uri = GeoUri {
991            crs: CoordRefSystem::Wgs84,
992            latitude: 52.107,
993            longitude: 5.134,
994            altitude: None,
995            uncertainty: None,
996        };
997        assert_eq!(&geo_uri.to_string(), "geo:52.107,5.134");
998
999        geo_uri.altitude = Some(3.6);
1000        assert_eq!(&geo_uri.to_string(), "geo:52.107,5.134,3.6");
1001
1002        geo_uri.uncertainty = Some(25_000.0);
1003        assert_eq!(&geo_uri.to_string(), "geo:52.107,5.134,3.6;u=25000");
1004    }
1005
1006    #[cfg(feature = "url")]
1007    #[test]
1008    fn geo_uri_from() {
1009        let geo_uri = GeoUri {
1010            crs: CoordRefSystem::Wgs84,
1011            latitude: 52.107,
1012            longitude: 5.134,
1013            altitude: Some(3.6),
1014            uncertainty: Some(1000.0),
1015        };
1016        let url = Url::from(&geo_uri);
1017        assert_eq!(url.scheme(), "geo");
1018        assert_eq!(url.path(), "52.107,5.134,3.6;u=1000");
1019
1020        let url = Url::from(geo_uri);
1021        assert_eq!(url.scheme(), "geo");
1022        assert_eq!(url.path(), "52.107,5.134,3.6;u=1000");
1023    }
1024
1025    #[test]
1026    fn geo_uri_from_str() -> Result<(), Error> {
1027        let geo_uri = GeoUri::from_str("geo:52.107,5.134")?;
1028        assert_eq!(geo_uri.latitude, 52.107);
1029        assert_eq!(geo_uri.longitude, 5.134);
1030        assert_eq!(geo_uri.altitude, None);
1031        assert_eq!(geo_uri.uncertainty, None);
1032
1033        Ok(())
1034    }
1035
1036    #[cfg(feature = "serde")]
1037    #[test]
1038    fn geo_uri_serde() {
1039        let geo_uri = GeoUri {
1040            crs: CoordRefSystem::Wgs84,
1041            latitude: 52.107,
1042            longitude: 5.134,
1043            altitude: Some(3.6),
1044            uncertainty: Some(1000.0),
1045        };
1046        assert_tokens(&geo_uri, &[Token::String("geo:52.107,5.134,3.6;u=1000")]);
1047
1048        assert_de_tokens_error::<GeoUri>(
1049            &[Token::I32(0)],
1050            "invalid type: integer `0`, expected a string starting with geo:",
1051        );
1052        assert_de_tokens_error::<GeoUri>(
1053            &[Token::String("geo:100.0,5.134,3.6")],
1054            &format!("{}", Error::OutOfRangeLatitude),
1055        );
1056    }
1057
1058    #[test]
1059    fn geo_uri_try_from() -> Result<(), Error> {
1060        // &str
1061        let geo_uri = GeoUri::try_from("geo:52.107,5.134")?;
1062        assert_eq!(geo_uri.latitude, 52.107);
1063        assert_eq!(geo_uri.longitude, 5.134);
1064        assert_eq!(geo_uri.altitude, None);
1065        assert_eq!(geo_uri.uncertainty, None);
1066
1067        // (f64, f64)
1068        let geo_uri = GeoUri::try_from((51.107, 5.134))?;
1069        assert_eq!(geo_uri.latitude, 51.107);
1070        assert_eq!(geo_uri.longitude, 5.134);
1071        assert_eq!(geo_uri.altitude, None);
1072        assert_eq!(geo_uri.uncertainty, None);
1073
1074        assert_eq!(
1075            GeoUri::try_from((100.0, 5.134)),
1076            Err(Error::OutOfRangeLatitude)
1077        );
1078        assert_eq!(
1079            GeoUri::try_from((51.107, -200.0)),
1080            Err(Error::OutOfRangeLongitude)
1081        );
1082
1083        // (f64, f64, f64)
1084        let geo_uri = GeoUri::try_from((51.107, 5.134, 3.6))?;
1085        assert_eq!(geo_uri.latitude, 51.107);
1086        assert_eq!(geo_uri.longitude, 5.134);
1087        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
1088        assert_eq!(geo_uri.uncertainty, None);
1089
1090        assert_eq!(
1091            GeoUri::try_from((100.0, 5.134, 3.6)),
1092            Err(Error::OutOfRangeLatitude)
1093        );
1094        assert_eq!(
1095            GeoUri::try_from((51.107, -200.0, 3.6)),
1096            Err(Error::OutOfRangeLongitude)
1097        );
1098
1099        Ok(())
1100    }
1101
1102    #[cfg(feature = "url")]
1103    #[test]
1104    fn geo_uri_try_from_url() -> Result<(), Error> {
1105        // Url
1106        let url = Url::parse("geo:51.107,5.134,3.6;crs=wgs84;u=1000;foo=bar").expect("valid URL");
1107        let geo_uri = GeoUri::try_from(&url)?;
1108        assert_eq!(geo_uri.latitude, 51.107);
1109        assert_eq!(geo_uri.longitude, 5.134);
1110        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
1111        assert_eq!(geo_uri.uncertainty, Some(1000.0));
1112
1113        let geo_uri = GeoUri::try_from(url)?;
1114        assert_eq!(geo_uri.latitude, 51.107);
1115        assert_eq!(geo_uri.longitude, 5.134);
1116        assert_eq!(geo_uri.altitude.unwrap(), 3.6);
1117        assert_eq!(geo_uri.uncertainty, Some(1000.0));
1118
1119        Ok(())
1120    }
1121
1122    #[test]
1123    fn geo_uri_partial_eq() -> Result<(), GeoUriBuilderError> {
1124        let geo_uri = GeoUri::builder()
1125            .latitude(52.107)
1126            .longitude(5.134)
1127            .build()?;
1128        let geo_uri2 = GeoUri::builder()
1129            .latitude(52.107)
1130            .longitude(5.134)
1131            .build()?;
1132        assert_eq!(geo_uri, geo_uri2);
1133        assert_eq!(geo_uri, geo_uri.clone());
1134
1135        let geo_uri = GeoUri::builder().latitude(90.0).longitude(5.134).build()?;
1136        let geo_uri2 = GeoUri::builder().latitude(90.0).longitude(5.134).build()?;
1137        assert_eq!(geo_uri, geo_uri2);
1138
1139        let geo_uri = GeoUri::builder().latitude(-90.0).longitude(5.134).build()?;
1140        let geo_uri2 = GeoUri::builder().latitude(-90.0).longitude(5.134).build()?;
1141        assert_eq!(geo_uri, geo_uri2);
1142
1143        // Examples from RFC 5870 (section 6.4)!
1144        let geo_uri = GeoUri::parse("geo:90,-22.43;crs=WGS84").expect("parsable geo URI");
1145        let geo_uri2 = GeoUri::parse("geo:90,46").expect("parsable geo URI");
1146        assert_eq!(geo_uri, geo_uri2);
1147
1148        let geo_uri = GeoUri::parse("geo:22.300,-118.44").expect("parsable geo URI");
1149        let geo_uri2 = GeoUri::parse("geo:22.3,-118.4400").expect("parsable geo URI");
1150        assert_eq!(geo_uri, geo_uri2);
1151
1152        let geo_uri = GeoUri::parse("geo:66,30;u=6.500;FOo=this%2dthat").expect("parsable geo URI");
1153        let geo_uri2 = GeoUri::parse("geo:66.0,30;u=6.5;foo=this-that").expect("parsable geo URI");
1154        assert_eq!(geo_uri, geo_uri2);
1155
1156        let _geo_uri = GeoUri::parse("geo:70,20;foo=1.00;bar=white").expect("parsable geo URI");
1157        let _geo_uri2 = GeoUri::parse("geo:70,20;foo=1;bar=white").expect("parsable geo URI");
1158        // This is undefined!
1159        // assert_eq!(geo_uri, geo_uri2);
1160
1161        let geo_uri = GeoUri::parse("geo:47,11;foo=blue;bar=white").expect("parsable geo URI");
1162        let geo_uri2 = GeoUri::parse("geo:47,11;bar=white;foo=blue").expect("parsable geo URI");
1163        assert_eq!(geo_uri, geo_uri2);
1164
1165        let _geo_uri = GeoUri::parse("geo:22,0;bar=Blue").expect("parsable geo URI");
1166        let _geo_uri2 = GeoUri::parse("geo:22,0;BAR=blue").expect("parsable geo URI");
1167        // This is undefined!
1168        // assert_eq!(geo_uri, geo_uri2);
1169
1170        Ok(())
1171    }
1172}