ecad_processor/utils/
coordinates.rs

1use crate::error::{ProcessingError, Result};
2
3/// Convert DMS (Degrees:Minutes:Seconds) format to decimal degrees
4///
5/// # Examples
6/// ```
7/// use ecad_processor::utils::dms_to_decimal;
8///
9/// let decimal = dms_to_decimal("50:30:15").unwrap();
10/// assert!((decimal - 50.504167).abs() < 0.000001);
11/// ```
12pub fn dms_to_decimal(dms: &str) -> Result<f64> {
13    let parts: Vec<&str> = dms.split(':').collect();
14
15    if parts.len() != 3 {
16        return Err(ProcessingError::InvalidCoordinate(format!(
17            "Invalid DMS format: '{}'. Expected format: 'DD:MM:SS'",
18            dms
19        )));
20    }
21
22    // Check if the coordinate is negative (can be indicated by a minus sign anywhere)
23    let is_negative = dms.starts_with('-');
24
25    let degrees = parts[0].parse::<f64>().map_err(|_| {
26        ProcessingError::InvalidCoordinate(format!("Invalid degrees value: '{}'", parts[0]))
27    })?;
28
29    let minutes = parts[1].parse::<f64>().map_err(|_| {
30        ProcessingError::InvalidCoordinate(format!("Invalid minutes value: '{}'", parts[1]))
31    })?;
32
33    let seconds = parts[2].parse::<f64>().map_err(|_| {
34        ProcessingError::InvalidCoordinate(format!("Invalid seconds value: '{}'", parts[2]))
35    })?;
36
37    // Validate ranges
38    if !(0.0..60.0).contains(&minutes) {
39        return Err(ProcessingError::InvalidCoordinate(format!(
40            "Minutes must be between 0 and 60, got: {}",
41            minutes
42        )));
43    }
44
45    if !(0.0..60.0).contains(&seconds) {
46        return Err(ProcessingError::InvalidCoordinate(format!(
47            "Seconds must be between 0 and 60, got: {}",
48            seconds
49        )));
50    }
51
52    // Calculate decimal value
53    let decimal_value = degrees.abs() + minutes / 60.0 + seconds / 3600.0;
54
55    // Apply sign
56    if is_negative {
57        Ok(-decimal_value)
58    } else {
59        Ok(decimal_value)
60    }
61}
62
63/// Convert decimal degrees to DMS format
64pub fn decimal_to_dms(decimal: f64) -> String {
65    let sign = if decimal < 0.0 { "-" } else { "" };
66    let abs_decimal = decimal.abs();
67
68    let degrees = abs_decimal.floor() as i32;
69    let minutes_decimal = (abs_decimal - degrees as f64) * 60.0;
70    let minutes = minutes_decimal.floor() as i32;
71    let seconds = (minutes_decimal - minutes as f64) * 60.0;
72
73    format!("{}{}:{:02}:{:05.2}", sign, degrees, minutes, seconds)
74}
75
76/// Parse coordinate that might be in DMS or decimal format
77pub fn parse_coordinate(coord_str: &str) -> Result<f64> {
78    let trimmed = coord_str.trim();
79
80    // Check if it's already in decimal format
81    if !trimmed.contains(':') {
82        trimmed.parse::<f64>().map_err(|_| {
83            ProcessingError::InvalidCoordinate(format!("Invalid coordinate value: '{}'", coord_str))
84        })
85    } else {
86        dms_to_decimal(trimmed)
87    }
88}
89
90/// Validate UK coordinate bounds
91pub fn validate_uk_coordinates(latitude: f64, longitude: f64) -> Result<()> {
92    if !(49.5..=61.0).contains(&latitude) {
93        return Err(ProcessingError::InvalidCoordinate(format!(
94            "Latitude {} is outside UK bounds [49.5, 61.0]",
95            latitude
96        )));
97    }
98
99    if !(-8.0..=2.0).contains(&longitude) {
100        return Err(ProcessingError::InvalidCoordinate(format!(
101            "Longitude {} is outside UK bounds [-8.0, 2.0]",
102            longitude
103        )));
104    }
105
106    Ok(())
107}
108
109/// Calculate the distance between two points using the Haversine formula
110pub fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
111    const EARTH_RADIUS_KM: f64 = 6371.0;
112
113    let lat1_rad = lat1.to_radians();
114    let lat2_rad = lat2.to_radians();
115    let delta_lat = (lat2 - lat1).to_radians();
116    let delta_lon = (lon2 - lon1).to_radians();
117
118    let a = (delta_lat / 2.0).sin().powi(2)
119        + lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
120    let c = 2.0 * a.sqrt().asin();
121
122    EARTH_RADIUS_KM * c
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    fn test_dms_to_decimal() {
131        assert!((dms_to_decimal("50:30:15").unwrap() - 50.504167).abs() < 0.000001);
132        assert!((dms_to_decimal("51:28:38").unwrap() - 51.477222).abs() < 0.000001);
133
134        // -0:07:39 = -(7/60 + 39/3600) = -(0.116667 + 0.010833) = -0.1275
135        let result = dms_to_decimal("-0:07:39").unwrap();
136        let expected = -0.1275;
137        println!(
138            "Result: {}, Expected: {}, Diff: {}",
139            result,
140            expected,
141            (result - expected).abs()
142        );
143        assert!((result - expected).abs() < 0.0001); // Slightly larger tolerance
144    }
145
146    #[test]
147    fn test_invalid_dms_format() {
148        assert!(dms_to_decimal("50:30").is_err());
149        assert!(dms_to_decimal("50:70:15").is_err()); // Invalid minutes
150        assert!(dms_to_decimal("50:30:70").is_err()); // Invalid seconds
151    }
152
153    #[test]
154    fn test_decimal_to_dms() {
155        assert_eq!(decimal_to_dms(50.504167), "50:30:15.00");
156        assert_eq!(decimal_to_dms(-0.1275), "-0:07:39.00");
157    }
158
159    #[test]
160    fn test_parse_coordinate() {
161        assert!((parse_coordinate("51.5074").unwrap() - 51.5074).abs() < 0.000001);
162        assert!((parse_coordinate("50:30:15").unwrap() - 50.504167).abs() < 0.000001);
163        assert!((parse_coordinate(" -0.1278 ").unwrap() - -0.1278).abs() < 0.000001);
164    }
165
166    #[test]
167    fn test_uk_coordinate_validation() {
168        assert!(validate_uk_coordinates(51.5074, -0.1278).is_ok()); // London
169        assert!(validate_uk_coordinates(55.9533, -3.1883).is_ok()); // Edinburgh
170        assert!(validate_uk_coordinates(48.0, 0.0).is_err()); // Too far south
171        assert!(validate_uk_coordinates(62.0, 0.0).is_err()); // Too far north
172    }
173
174    #[test]
175    fn test_haversine_distance() {
176        // London to Edinburgh
177        let distance = haversine_distance(51.5074, -0.1278, 55.9533, -3.1883);
178        assert!((distance - 534.0).abs() < 10.0); // ~534km with 10km tolerance
179    }
180}