#[derive(Debug, Clone, PartialEq)]
pub struct Coordinate {
pub latitude: f64,
pub longitude: f64,
pub format: CoordinateFormat,
pub datum: Datum,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoordinateFormat {
DecimalDegrees,
Dms,
Ddm,
Iso6709,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Datum {
Wgs84,
Jgd2011,
Cgcs2000,
Pz90,
Ktrf,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoordinateOrder {
LatLng,
LngLat,
Ambiguous,
}
pub fn parse_coordinate(s: &str) -> Option<Coordinate> {
let s = s.trim();
if s.is_empty() {
return None;
}
if s.starts_with('+') || s.starts_with('-') {
if let Some(coord) = try_parse_iso6709(s) {
return Some(coord);
}
}
if s.contains('°') || s.contains('\u{00B0}') || s.contains('\u{00BA}') {
if let Some(coord) = try_parse_dms(s) {
return Some(coord);
}
}
if let Some(coord) = try_parse_decimal(s) {
return Some(coord);
}
None
}
pub fn detect_coordinate_order(pairs: &[(f64, f64)]) -> CoordinateOrder {
let mut first_exceeds_90 = false;
let mut second_exceeds_90 = false;
for (a, b) in pairs {
if a.abs() > 90.0 {
first_exceeds_90 = true;
}
if b.abs() > 90.0 {
second_exceeds_90 = true;
}
}
if first_exceeds_90 && !second_exceeds_90 {
CoordinateOrder::LngLat } else if second_exceeds_90 && !first_exceeds_90 {
CoordinateOrder::LatLng } else {
CoordinateOrder::Ambiguous
}
}
fn try_parse_decimal(s: &str) -> Option<Coordinate> {
let parts: Vec<&str> = if s.contains(',') {
s.split(',').map(|p| p.trim()).collect()
} else {
s.split_whitespace().collect()
};
if parts.len() != 2 {
return None;
}
let a: f64 = parts[0].parse().ok()?;
let b: f64 = parts[1].parse().ok()?;
if a.abs() > 180.0 || b.abs() > 180.0 {
return None;
}
if a.abs() > 90.0 && b.abs() > 90.0 {
return None;
}
if a.abs() > 90.0 {
return None; }
let (lat, lng) = (a, b);
if lat.abs() > 90.0 || lng.abs() > 180.0 {
return None;
}
Some(Coordinate {
latitude: lat,
longitude: lng,
format: CoordinateFormat::DecimalDegrees,
datum: Datum::Unknown,
})
}
fn try_parse_dms(s: &str) -> Option<Coordinate> {
let s = s.replace('\u{00BA}', "°");
let upper = s.to_uppercase();
let (lat_str, lng_str) = if let Some(pos) = upper.find('N').or_else(|| upper.find('S')) {
let split = pos + 1;
(&s[..split], s[split..].trim())
} else {
let parts: Vec<&str> = s
.splitn(2, |c: char| {
c.is_whitespace() && !s[..s.find(c).unwrap_or(0)].ends_with('°')
})
.collect();
if parts.len() == 2 {
(parts[0], parts[1])
} else {
return None;
}
};
let lat = parse_single_dms(lat_str)?;
let lng = parse_single_dms(lng_str)?;
Some(Coordinate {
latitude: lat,
longitude: lng,
format: CoordinateFormat::Dms,
datum: Datum::Unknown,
})
}
fn parse_single_dms(s: &str) -> Option<f64> {
let s = s.trim();
let upper = s.to_uppercase();
let negative = upper.contains('S') || upper.contains('W');
let _cleaned: String = s
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect();
let numbers: Vec<f64> = s
.split(|c: char| !c.is_ascii_digit() && c != '.')
.filter(|p| !p.is_empty())
.filter_map(|p| p.parse::<f64>().ok())
.collect();
let decimal = match numbers.len() {
1 => numbers[0],
2 => numbers[0] + numbers[1] / 60.0,
3 => numbers[0] + numbers[1] / 60.0 + numbers[2] / 3600.0,
_ => return None,
};
Some(if negative { -decimal } else { decimal })
}
fn try_parse_iso6709(s: &str) -> Option<Coordinate> {
let s = s.trim_end_matches('/');
let bytes = s.as_bytes();
let mut split_pos = None;
for (i, &byte) in bytes.iter().enumerate().skip(1) {
if byte == b'+' || byte == b'-' {
split_pos = Some(i);
break;
}
}
let split = split_pos?;
let lat_str = &s[..split];
let lng_str = &s[split..];
let lat: f64 = lat_str.parse().ok()?;
let lng: f64 = lng_str.parse().ok()?;
if lat.abs() > 90.0 || lng.abs() > 180.0 {
return None;
}
Some(Coordinate {
latitude: lat,
longitude: lng,
format: CoordinateFormat::Iso6709,
datum: Datum::Unknown,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decimal_degrees() {
let c = parse_coordinate("40.7128, -74.0060").unwrap();
assert!((c.latitude - 40.7128).abs() < 0.001);
assert!((c.longitude - -74.006).abs() < 0.001);
assert_eq!(c.format, CoordinateFormat::DecimalDegrees);
}
#[test]
fn decimal_degrees_space_separated() {
let c = parse_coordinate("40.7128 -74.0060").unwrap();
assert!((c.latitude - 40.7128).abs() < 0.001);
}
#[test]
fn dms_with_nsew() {
let c = parse_coordinate("40°42'46\"N 74°0'22\"W").unwrap();
assert!((c.latitude - 40.7128).abs() < 0.01);
assert!((c.longitude - -74.006).abs() < 0.01);
assert_eq!(c.format, CoordinateFormat::Dms);
}
#[test]
fn iso6709() {
let c = parse_coordinate("+40.7128-074.0060/").unwrap();
assert!((c.latitude - 40.7128).abs() < 0.001);
assert!((c.longitude - -74.006).abs() < 0.001);
assert_eq!(c.format, CoordinateFormat::Iso6709);
}
#[test]
fn detect_order_lng_first() {
let pairs = vec![(-74.0060, 40.7128), (-118.2437, 34.0522)]; assert_eq!(detect_coordinate_order(&pairs), CoordinateOrder::LngLat);
}
#[test]
fn detect_order_lat_first() {
let pairs = vec![(40.7128, -74.0060), (34.0522, -118.2437)]; assert_eq!(detect_coordinate_order(&pairs), CoordinateOrder::LatLng);
}
#[test]
fn detect_order_ambiguous() {
let pairs = vec![(40.0, 50.0), (30.0, 45.0)]; assert_eq!(detect_coordinate_order(&pairs), CoordinateOrder::Ambiguous);
}
}