use crate::{
Error, Latitude, Longitude,
fmt::{FormatKind, FormatOptions, Formatter},
lat::EQUATOR,
long::INTERNATIONAL_REFERENCE_MERIDIAN,
parse::{self, Parsed},
};
use core::{
fmt::{Debug, Display, Write},
str::FromStr,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg(feature = "geojson")]
use crate::Angle;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct Coordinate {
lat: Latitude, long: Longitude, }
pub const GEO_URL_SCHEME: &str = "geo";
#[cfg(feature = "geojson")]
pub const GEOJSON_TYPE_FIELD: &str = "type";
#[cfg(feature = "geojson")]
pub const GEOJSON_COORDINATES_FIELD: &str = "coordinates";
#[cfg(feature = "geojson")]
pub const GEOJSON_POINT_TYPE: &str = "Point";
#[cfg(not(feature = "3d"))]
#[macro_export]
macro_rules! coord {
($lat:expr ; $lon:expr) => {
$crate::coord::Coordinate::new($lat, $lon)
};
}
#[cfg(feature = "3d")]
#[macro_export]
macro_rules! coord {
($lat:expr ; $lon:expr) => {
$crate::coord::Coordinate::new($lat, $lon)
};
($lat:expr ; $lon:expr ; $alt:expr) => {
$crate::alt::Coordinate3d::new_from($lat, $lon, $alt)
};
}
impl Default for Coordinate {
fn default() -> Self {
Self {
lat: EQUATOR,
long: INTERNATIONAL_REFERENCE_MERIDIAN,
}
}
}
impl From<(Latitude, Longitude)> for Coordinate {
fn from(value: (Latitude, Longitude)) -> Self {
Self::new(value.0, value.1)
}
}
impl From<Coordinate> for (Latitude, Longitude) {
fn from(value: Coordinate) -> Self {
(value.lat, value.long)
}
}
impl From<Latitude> for Coordinate {
fn from(value: Latitude) -> Self {
Self::new(value, Longitude::default())
}
}
impl From<Longitude> for Coordinate {
fn from(value: Longitude) -> Self {
Self::new(Latitude::default(), value)
}
}
impl FromStr for Coordinate {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match parse::parse_str(s)? {
Parsed::Coordinate(coord) => Ok(coord),
_ => Err(Error::InvalidAngle(0.0, 0.0)),
}
}
}
impl Display for Coordinate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let format = if f.alternate() {
FormatOptions::dms()
} else {
FormatOptions::decimal()
};
self.format(f, &format)
}
}
impl Formatter for Coordinate {
fn format<W: Write>(&self, f: &mut W, fmt: &FormatOptions) -> std::fmt::Result {
let kind = fmt.kind();
self.lat.format(f, fmt)?;
write!(f, ",{}", if kind == FormatKind::DmsBare { "" } else { " " })?;
self.long.format(f, fmt)
}
}
impl Coordinate {
pub const fn new(lat: Latitude, long: Longitude) -> Self {
Self { lat, long }
}
#[must_use]
pub const fn with_latitude(mut self, lat: Latitude) -> Self {
self.lat = lat;
self
}
#[must_use]
pub const fn with_longitude(mut self, long: Longitude) -> Self {
self.long = long;
self
}
#[must_use]
pub const fn latitude(&self) -> Latitude {
self.lat
}
#[must_use]
pub const fn φ(&self) -> Latitude {
self.lat
}
#[must_use]
pub const fn longitude(&self) -> Longitude {
self.long
}
#[must_use]
pub const fn λ(&self) -> Longitude {
self.long
}
#[must_use]
pub fn is_on_equator(&self) -> bool {
self.lat.is_on_equator()
}
#[must_use]
pub fn is_northern(&self) -> bool {
self.lat.is_northern()
}
#[must_use]
pub fn is_southern(&self) -> bool {
self.lat.is_southern()
}
#[must_use]
pub fn is_on_international_reference_meridian(&self) -> bool {
self.long.is_on_international_reference_meridian()
}
#[must_use]
pub fn is_western(&self) -> bool {
self.long.is_western()
}
#[must_use]
pub fn is_eastern(&self) -> bool {
self.long.is_eastern()
}
#[must_use]
pub fn to_url_string(&self) -> String {
format!(
"{}:{},{}",
GEO_URL_SCHEME,
self.lat.to_formatted_string(&FormatOptions::decimal()),
self.long.to_formatted_string(&FormatOptions::decimal())
)
}
#[must_use]
pub fn to_microformat_string(&self) -> String {
format!(
"<span class=\"latitude\">{}</span>; <span class=\"longitude\">{}</span>",
self.lat.to_formatted_string(&FormatOptions::decimal()),
self.long.to_formatted_string(&FormatOptions::decimal())
)
}
}
#[cfg(feature = "urn")]
impl From<Coordinate> for url::Url {
fn from(coord: Coordinate) -> Self {
Self::parse(&coord.to_url_string()).unwrap()
}
}
#[cfg(feature = "urn")]
impl TryFrom<url::Url> for Coordinate {
type Error = crate::Error;
fn try_from(url: url::Url) -> Result<Self, Self::Error> {
if url.scheme() != GEO_URL_SCHEME {
return Err(crate::Error::InvalidUrnScheme);
}
let path = url.path();
let parts: Vec<&str> = path.split(',').collect();
if parts.len() != 2 {
return Err(crate::Error::InvalidCoordinate);
}
let lat_val: f64 = parts[0]
.parse()
.map_err(|_| crate::Error::InvalidCoordinate)?;
let lon_val: f64 = parts[1]
.parse()
.map_err(|_| crate::Error::InvalidCoordinate)?;
let lat = Latitude::try_from(lat_val).map_err(|_| crate::Error::InvalidCoordinate)?;
let lon = Longitude::try_from(lon_val).map_err(|_| crate::Error::InvalidCoordinate)?;
Ok(Coordinate::new(lat, lon))
}
}
#[cfg(feature = "geojson")]
impl From<Coordinate> for serde_json::Value {
fn from(coord: Coordinate) -> Self {
serde_json::json!({
GEOJSON_TYPE_FIELD: GEOJSON_POINT_TYPE,
GEOJSON_COORDINATES_FIELD: [
coord.lat.as_float().0,
coord.long.as_float().0
]
})
}
}
#[cfg(feature = "geojson")]
impl TryFrom<serde_json::Value> for Coordinate {
type Error = crate::Error;
fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
if value[GEOJSON_TYPE_FIELD] != GEOJSON_POINT_TYPE {
return Err(crate::Error::InvalidCoordinate);
}
let coords = value[GEOJSON_COORDINATES_FIELD]
.as_array()
.ok_or(crate::Error::InvalidCoordinate)?;
if coords.len() != 2 {
return Err(crate::Error::InvalidCoordinate);
}
let lat_val: f64 = coords[0]
.as_f64()
.ok_or(crate::Error::InvalidNumericFormat(coords[0].to_string()))?;
let lon_val: f64 = coords[1]
.as_f64()
.ok_or(crate::Error::InvalidNumericFormat(coords[1].to_string()))?;
let lat = Latitude::try_from(lat_val)?;
let lon = Longitude::try_from(lon_val)?;
Ok(Coordinate::new(lat, lon))
}
}