ecad_processor/utils/
coordinates.rs1use crate::error::{ProcessingError, Result};
2
3pub 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 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 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 let decimal_value = degrees.abs() + minutes / 60.0 + seconds / 3600.0;
54
55 if is_negative {
57 Ok(-decimal_value)
58 } else {
59 Ok(decimal_value)
60 }
61}
62
63pub 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
76pub fn parse_coordinate(coord_str: &str) -> Result<f64> {
78 let trimmed = coord_str.trim();
79
80 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
90pub 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
109pub 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 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); }
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()); assert!(dms_to_decimal("50:30:70").is_err()); }
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()); assert!(validate_uk_coordinates(55.9533, -3.1883).is_ok()); assert!(validate_uk_coordinates(48.0, 0.0).is_err()); assert!(validate_uk_coordinates(62.0, 0.0).is_err()); }
173
174 #[test]
175 fn test_haversine_distance() {
176 let distance = haversine_distance(51.5074, -0.1278, 55.9533, -3.1883);
178 assert!((distance - 534.0).abs() < 10.0); }
180}