scirs2_spatial/
geospatial.rs

1//! Geospatial functionality for working with geographic coordinates
2//!
3//! This module provides basic geospatial operations including:
4//! - Coordinate system transformations
5//! - Great circle distance calculations (Haversine formula)
6//! - Bearing and azimuth calculations
7//! - Geodesic operations on the sphere
8//!
9//! # Coordinate Systems
10//!
11//! The module supports common geographic coordinate systems:
12//! - **WGS84**: World Geodetic System 1984 (GPS standard)
13//! - **Geographic**: Latitude/Longitude coordinates
14//! - **UTM**: Universal Transverse Mercator projections
15//! - **Web Mercator**: Spherical Mercator (EPSG:3857)
16//!
17//! # Examples
18//!
19//! ```
20//! use scirs2_spatial::geospatial::{haversine_distance, initial_bearing, destination_point};
21//!
22//! // Calculate distance between two cities
23//! let london = (51.5074, -0.1278);  // Latitude, Longitude
24//! let paris = (48.8566, 2.3522);
25//!
26//! let distance = haversine_distance(london, paris);
27//! println!("Distance from London to Paris: {:.1} km", distance / 1000.0);
28//!
29//! // Calculate bearing
30//! let bearing = initial_bearing(london, paris);
31//! println!("Initial bearing: {:.1}°", bearing.to_degrees());
32//!
33//! // Find destination point
34//! let destination = destination_point(london, 100000.0, bearing); // 100 km
35//! println!("100km from London: ({:.4}, {:.4})", destination.0, destination.1);
36//! ```
37
38use crate::error::{SpatialError, SpatialResult};
39use std::f64::consts::PI;
40
41/// Earth radius in meters (WGS84 mean radius)
42pub const EARTH_RADIUS_M: f64 = 6_371_008.8;
43
44/// Earth radius in kilometers
45pub const EARTH_RADIUS_KM: f64 = EARTH_RADIUS_M / 1000.0;
46
47/// Earth's equatorial radius in meters (WGS84)
48pub const EARTH_EQUATORIAL_RADIUS_M: f64 = 6_378_137.0;
49
50/// Earth's polar radius in meters (WGS84)
51pub const EARTH_POLAR_RADIUS_M: f64 = 6_356_752.314245;
52
53/// Earth's flattening (WGS84)
54pub const EARTH_FLATTENING: f64 = 1.0 / 298.257223563;
55
56/// Earth's eccentricity squared (WGS84)
57pub const EARTH_ECCENTRICITY_SQ: f64 = 2.0 * EARTH_FLATTENING - EARTH_FLATTENING * EARTH_FLATTENING;
58
59/// Convert degrees to radians
60///
61/// # Arguments
62///
63/// * `degrees` - Angle in degrees
64///
65/// # Returns
66///
67/// * Angle in radians
68///
69/// # Examples
70///
71/// ```
72/// use scirs2_spatial::geospatial::deg_to_rad;
73///
74/// let radians = deg_to_rad(180.0);
75/// assert!((radians - std::f64::consts::PI).abs() < 1e-10);
76/// ```
77#[allow(dead_code)]
78pub fn deg_to_rad(degrees: f64) -> f64 {
79    degrees * PI / 180.0
80}
81
82/// Convert radians to degrees
83///
84/// # Arguments
85///
86/// * `radians` - Angle in radians
87///
88/// # Returns
89///
90/// * Angle in degrees
91///
92/// # Examples
93///
94/// ```
95/// use scirs2_spatial::geospatial::rad_to_deg;
96///
97/// let degrees = rad_to_deg(std::f64::consts::PI);
98/// assert!((degrees - 180.0).abs() < 1e-10);
99/// ```
100#[allow(dead_code)]
101pub fn rad_to_deg(radians: f64) -> f64 {
102    radians * 180.0 / PI
103}
104
105/// Normalize angle to [0, 2π) range
106///
107/// # Arguments
108///
109/// * `angle` - Angle in radians
110///
111/// # Returns
112///
113/// * Normalized angle in the range [0, 2π)
114///
115/// # Examples
116///
117/// ```
118/// use scirs2_spatial::geospatial::normalize_angle;
119///
120/// let normalized = normalize_angle(3.0 * std::f64::consts::PI);
121/// assert!((normalized - std::f64::consts::PI).abs() < 1e-10);
122/// ```
123#[allow(dead_code)]
124pub fn normalize_angle(angle: f64) -> f64 {
125    let normalized = angle % (2.0 * PI);
126    if normalized < 0.0 {
127        normalized + 2.0 * PI
128    } else {
129        normalized
130    }
131}
132
133/// Normalize bearing to [0°, 360°) range
134///
135/// # Arguments
136///
137/// * `bearing_deg` - Bearing in degrees
138///
139/// # Returns
140///
141/// * Normalized bearing in the range [0°, 360°)
142///
143/// # Examples
144///
145/// ```
146/// use scirs2_spatial::geospatial::normalize_bearing;
147///
148/// let normalized = normalize_bearing(450.0);
149/// assert!((normalized - 90.0).abs() < 1e-10);
150/// ```
151#[allow(dead_code)]
152pub fn normalize_bearing(_bearingdeg: f64) -> f64 {
153    let normalized = _bearingdeg % 360.0;
154    if normalized < 0.0 {
155        normalized + 360.0
156    } else {
157        normalized
158    }
159}
160
161/// Calculate the great circle distance between two points using the Haversine formula
162///
163/// This is the most common method for calculating distances on a sphere.
164/// The Haversine formula is numerically stable for small distances.
165///
166/// # Arguments
167///
168/// * `point1` - First point as (latitude, longitude) in degrees
169/// * `point2` - Second point as (latitude, longitude) in degrees
170///
171/// # Returns
172///
173/// * Distance in meters
174///
175/// # Examples
176///
177/// ```
178/// use scirs2_spatial::geospatial::haversine_distance;
179///
180/// let new_york = (40.7128, -74.0060);
181/// let london = (51.5074, -0.1278);
182///
183/// let distance = haversine_distance(new_york, london);
184/// println!("Distance: {:.1} km", distance / 1000.0);
185/// ```
186#[allow(dead_code)]
187pub fn haversine_distance(point1: (f64, f64), point2: (f64, f64)) -> f64 {
188    let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
189    let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
190
191    let dlat = lat2 - lat1;
192    let dlon = lon2 - lon1;
193
194    let a = (dlat / 2.0).sin().powi(2) + lat1.cos() * lat2.cos() * (dlon / 2.0).sin().powi(2);
195    let c = 2.0 * a.sqrt().asin();
196
197    EARTH_RADIUS_M * c
198}
199
200/// Calculate the initial bearing (forward azimuth) from point1 to point2
201///
202/// # Arguments
203///
204/// * `point1` - Starting point as (latitude, longitude) in degrees
205/// * `point2` - End point as (latitude, longitude) in degrees
206///
207/// # Returns
208///
209/// * Initial bearing in radians (0 = North, π/2 = East, π = South, 3π/2 = West)
210///
211/// # Examples
212///
213/// ```
214/// use scirs2_spatial::geospatial::initial_bearing;
215///
216/// let start = (40.7128, -74.0060);  // New York
217/// let end = (51.5074, -0.1278);     // London
218///
219/// let bearing = initial_bearing(start, end);
220/// println!("Bearing: {:.1}°", bearing.to_degrees());
221/// ```
222#[allow(dead_code)]
223pub fn initial_bearing(point1: (f64, f64), point2: (f64, f64)) -> f64 {
224    let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
225    let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
226
227    let dlon = lon2 - lon1;
228
229    let y = dlon.sin() * lat2.cos();
230    let x = lat1.cos() * lat2.sin() - lat1.sin() * lat2.cos() * dlon.cos();
231
232    normalize_angle(y.atan2(x))
233}
234
235/// Calculate the final bearing (back azimuth) when arriving at point2 from point1
236///
237/// # Arguments
238///
239/// * `point1` - Starting point as (latitude, longitude) in degrees
240/// * `point2` - End point as (latitude, longitude) in degrees
241///
242/// # Returns
243///
244/// * Final bearing in radians
245#[allow(dead_code)]
246pub fn final_bearing(point1: (f64, f64), point2: (f64, f64)) -> f64 {
247    let reverse_bearing = initial_bearing(point2, point1);
248    normalize_angle(reverse_bearing + PI)
249}
250
251/// Calculate the destination point given a starting point, distance, and bearing
252///
253/// # Arguments
254///
255/// * `start` - Starting point as (latitude, longitude) in degrees
256/// * `distance` - Distance to travel in meters
257/// * `bearing` - Bearing to travel in radians (0 = North)
258///
259/// # Returns
260///
261/// * Destination point as (latitude, longitude) in degrees
262///
263/// # Examples
264///
265/// ```
266/// use scirs2_spatial::geospatial::destination_point;
267///
268/// let start = (40.7128, -74.0060);  // New York
269/// let distance = 100_000.0;         // 100 km
270/// let bearing = std::f64::consts::PI / 4.0;  // 45° (Northeast)
271///
272/// let destination = destination_point(start, distance, bearing);
273/// println!("Destination: ({:.4}, {:.4})", destination.0, destination.1);
274/// ```
275#[allow(dead_code)]
276pub fn destination_point(start: (f64, f64), distance: f64, bearing: f64) -> (f64, f64) {
277    let (lat1, lon1) = (deg_to_rad(start.0), deg_to_rad(start.1));
278
279    let angular_distance = distance / EARTH_RADIUS_M;
280
281    let lat2 = (lat1.sin() * angular_distance.cos()
282        + lat1.cos() * angular_distance.sin() * bearing.cos())
283    .asin();
284
285    let lon2 = lon1
286        + (bearing.sin() * angular_distance.sin() * lat1.cos())
287            .atan2(angular_distance.cos() - lat1.sin() * lat2.sin());
288
289    (rad_to_deg(lat2), rad_to_deg(lon2))
290}
291
292/// Calculate the midpoint between two geographic points
293///
294/// # Arguments
295///
296/// * `point1` - First point as (latitude, longitude) in degrees
297/// * `point2` - Second point as (latitude, longitude) in degrees
298///
299/// # Returns
300///
301/// * Midpoint as (latitude, longitude) in degrees
302#[allow(dead_code)]
303pub fn midpoint(point1: (f64, f64), point2: (f64, f64)) -> (f64, f64) {
304    let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
305    let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
306
307    let dlon = lon2 - lon1;
308
309    let bx = lat2.cos() * dlon.cos();
310    let by = lat2.cos() * dlon.sin();
311
312    let lat_mid = (lat1.sin() + lat2.sin()).atan2(((lat1.cos() + bx).powi(2) + by.powi(2)).sqrt());
313
314    let lon_mid = lon1 + by.atan2(lat1.cos() + bx);
315
316    (rad_to_deg(lat_mid), rad_to_deg(lon_mid))
317}
318
319/// Calculate the cross-track distance (distance from a point to a great circle path)
320///
321/// # Arguments
322///
323/// * `point` - Point to measure distance from, as (latitude, longitude) in degrees
324/// * `path_start` - Start of the great circle path, as (latitude, longitude) in degrees
325/// * `path_end` - End of the great circle path, as (latitude, longitude) in degrees
326///
327/// # Returns
328///
329/// * Cross-track distance in meters (positive if point is to the right of the path)
330#[allow(dead_code)]
331pub fn cross_track_distance(
332    point: (f64, f64),
333    path_start: (f64, f64),
334    path_end: (f64, f64),
335) -> f64 {
336    let distance_to_start = haversine_distance(path_start, point) / EARTH_RADIUS_M;
337    let bearing_to_point = initial_bearing(path_start, point);
338    let bearing_to_end = initial_bearing(path_start, path_end);
339
340    let cross_track_angular =
341        (distance_to_start.sin() * (bearing_to_point - bearing_to_end).sin()).asin();
342
343    EARTH_RADIUS_M * cross_track_angular
344}
345
346/// Calculate the along-track distance (distance along a great circle path to the closest point)
347///
348/// # Arguments
349///
350/// * `point` - Point to project onto the path, as (latitude, longitude) in degrees
351/// * `path_start` - Start of the great circle path, as (latitude, longitude) in degrees
352/// * `path_end` - End of the great circle path, as (latitude, longitude) in degrees
353///
354/// # Returns
355///
356/// * Along-track distance in meters from path_start to the closest point on the path
357#[allow(dead_code)]
358pub fn along_track_distance(
359    point: (f64, f64),
360    path_start: (f64, f64),
361    path_end: (f64, f64),
362) -> f64 {
363    let distance_to_start = haversine_distance(path_start, point) / EARTH_RADIUS_M;
364    let cross_track_angular = cross_track_distance(point, path_start, path_end) / EARTH_RADIUS_M;
365
366    let along_track_angular = (distance_to_start.powi(2) - cross_track_angular.powi(2))
367        .sqrt()
368        .acos();
369
370    EARTH_RADIUS_M * along_track_angular
371}
372
373/// Calculate the area of a polygon on the sphere using spherical excess
374///
375/// # Arguments
376///
377/// * `polygon` - Vector of points as (latitude, longitude) in degrees
378///
379/// # Returns
380///
381/// * Area in square meters
382///
383/// # Note
384///
385/// This uses the spherical excess method. For very large polygons, more sophisticated
386/// methods may be needed to handle numerical precision issues.
387#[allow(dead_code)]
388pub fn spherical_polygon_area(polygon: &[(f64, f64)]) -> SpatialResult<f64> {
389    if polygon.len() < 3 {
390        return Err(SpatialError::ValueError(
391            "Polygon must have at least 3 vertices".to_string(),
392        ));
393    }
394
395    let n = polygon.len();
396    let mut sum = 0.0;
397
398    for i in 0..n {
399        let j = (i + 1) % n;
400        let (lat1, lon1) = (deg_to_rad(polygon[i].0), deg_to_rad(polygon[i].1));
401        let (lat2, lon2) = (deg_to_rad(polygon[j].0), deg_to_rad(polygon[j].1));
402
403        sum += (lon2 - lon1) * (2.0 + lat1.sin() + lat2.sin());
404    }
405
406    let area = (sum.abs() / 2.0) * EARTH_RADIUS_M * EARTH_RADIUS_M;
407    Ok(area)
408}
409
410/// Check if a point is inside a spherical polygon using the winding number method
411///
412/// # Arguments
413///
414/// * `point` - Point to test as (latitude, longitude) in degrees
415/// * `polygon` - Polygon vertices as (latitude, longitude) in degrees
416///
417/// # Returns
418///
419/// * true if point is inside the polygon, false otherwise
420#[allow(dead_code)]
421pub fn point_in_spherical_polygon(point: (f64, f64), polygon: &[(f64, f64)]) -> bool {
422    if polygon.len() < 3 {
423        return false;
424    }
425
426    // For small polygons (< 10 degrees), use planar approximation for better numerical stability
427    let max_extent = polygon
428        .iter()
429        .flat_map(|(lat, lon)| [lat.abs(), lon.abs()])
430        .fold(0.0, f64::max);
431
432    if max_extent < 10.0 {
433        // Use planar point-in-polygon algorithm (ray casting)
434        let (x, y) = point;
435        let mut inside = false;
436        let n = polygon.len();
437
438        for i in 0..n {
439            let j = (i + 1) % n;
440            let (xi, yi) = polygon[i];
441            let (xj, yj) = polygon[j];
442
443            if ((yi > y) != (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
444                inside = !inside;
445            }
446        }
447        return inside;
448    }
449
450    // For larger polygons, use proper spherical calculation
451    let (test_lat, test_lon) = (deg_to_rad(point.0), deg_to_rad(point.1));
452    let mut angle_sum = 0.0;
453
454    for i in 0..polygon.len() {
455        let j = (i + 1) % polygon.len();
456        let (lat1, lon1) = (deg_to_rad(polygon[i].0), deg_to_rad(polygon[i].1));
457        let (lat2, lon2) = (deg_to_rad(polygon[j].0), deg_to_rad(polygon[j].1));
458
459        // Convert to 3D Cartesian coordinates on unit sphere
460        let x1 = lat1.cos() * lon1.cos();
461        let y1 = lat1.cos() * lon1.sin();
462        let z1 = lat1.sin();
463
464        let x2 = lat2.cos() * lon2.cos();
465        let y2 = lat2.cos() * lon2.sin();
466        let z2 = lat2.sin();
467
468        let xt = test_lat.cos() * test_lon.cos();
469        let yt = test_lat.cos() * test_lon.sin();
470        let zt = test_lat.sin();
471
472        // Vectors from test point to polygon vertices
473        let v1x = x1 - xt;
474        let v1y = y1 - yt;
475        let v1z = z1 - zt;
476
477        let v2x = x2 - xt;
478        let v2y = y2 - yt;
479        let v2z = z2 - zt;
480
481        // Normalize vectors
482        let v1_len = (v1x * v1x + v1y * v1y + v1z * v1z).sqrt();
483        let v2_len = (v2x * v2x + v2y * v2y + v2z * v2z).sqrt();
484
485        if v1_len < 1e-10 || v2_len < 1e-10 {
486            continue; // Point is on a vertex
487        }
488
489        let v1x_norm = v1x / v1_len;
490        let v1y_norm = v1y / v1_len;
491        let v1z_norm = v1z / v1_len;
492
493        let v2x_norm = v2x / v2_len;
494        let v2y_norm = v2y / v2_len;
495        let v2z_norm = v2z / v2_len;
496
497        // Calculate angle between vectors
498        let dot = v1x_norm * v2x_norm + v1y_norm * v2y_norm + v1z_norm * v2z_norm;
499        let dot = dot.clamp(-1.0, 1.0); // Handle numerical errors
500
501        // Cross product for sign
502        let cross_x = v1y_norm * v2z_norm - v1z_norm * v2y_norm;
503        let cross_y = v1z_norm * v2x_norm - v1x_norm * v2z_norm;
504        let cross_z = v1x_norm * v2y_norm - v1y_norm * v2x_norm;
505
506        // Project cross product onto normal at test point to get sign
507        let normal_dot = cross_x * xt + cross_y * yt + cross_z * zt;
508        let angle = dot.acos();
509
510        if normal_dot < 0.0 {
511            angle_sum -= angle;
512        } else {
513            angle_sum += angle;
514        }
515    }
516
517    (angle_sum.abs() / (2.0 * PI)) > 0.5
518}
519
520/// Convert geographic coordinates to UTM coordinates
521///
522/// # Arguments
523///
524/// * `lat` - Latitude in degrees
525/// * `lon` - Longitude in degrees
526///
527/// # Returns
528///
529/// * (easting, northing, zone_number, zone_letter)
530///
531/// # Note
532///
533/// This is a simplified UTM conversion. For high-precision applications,
534/// use specialized geospatial libraries like PROJ.
535#[allow(dead_code)]
536pub fn geographic_to_utm(lat: f64, lon: f64) -> SpatialResult<(f64, f64, i32, char)> {
537    if !(-80.0..=84.0).contains(&lat) {
538        return Err(SpatialError::ValueError(
539            "Latitude must be between -80° and 84° for UTM".to_string(),
540        ));
541    }
542
543    let zone_number = ((lon + 180.0) / 6.0).floor() as i32 + 1;
544    let zone_letter = utm_zone_letter(lat)?;
545
546    let lat_rad = deg_to_rad(lat);
547    let lon_rad = deg_to_rad(lon);
548    let central_meridian = deg_to_rad(((zone_number - 1) * 6 - 177) as f64);
549
550    let k0 = 0.9996; // UTM scale factor
551    let a = EARTH_EQUATORIAL_RADIUS_M;
552    let e_sq = EARTH_ECCENTRICITY_SQ;
553
554    let n = a / (1.0 - e_sq * lat_rad.sin().powi(2)).sqrt();
555    let t = lat_rad.tan().powi(2);
556    let c = EARTH_ECCENTRICITY_SQ * lat_rad.cos().powi(2) / (1.0 - EARTH_ECCENTRICITY_SQ);
557    let a_coeff = lat_rad.cos() * (lon_rad - central_meridian);
558
559    let m = a
560        * ((1.0 - e_sq / 4.0 - 3.0 * e_sq.powi(2) / 64.0 - 5.0 * e_sq.powi(3) / 256.0) * lat_rad
561            - (3.0 * e_sq / 8.0 + 3.0 * e_sq.powi(2) / 32.0 + 45.0 * e_sq.powi(3) / 1024.0)
562                * (2.0 * lat_rad).sin()
563            + (15.0 * e_sq.powi(2) / 256.0 + 45.0 * e_sq.powi(3) / 1024.0) * (4.0 * lat_rad).sin()
564            - (35.0 * e_sq.powi(3) / 3072.0) * (6.0 * lat_rad).sin());
565
566    let easting = k0
567        * n
568        * (a_coeff
569            + (1.0 - t + c) * a_coeff.powi(3) / 6.0
570            + (5.0 - 18.0 * t + t.powi(2) + 72.0 * c - 58.0 * EARTH_ECCENTRICITY_SQ)
571                * a_coeff.powi(5)
572                / 120.0)
573        + 500000.0;
574
575    let northing = k0
576        * (m + n
577            * lat_rad.tan()
578            * (a_coeff.powi(2) / 2.0
579                + (5.0 - t + 9.0 * c + 4.0 * c.powi(2)) * a_coeff.powi(4) / 24.0
580                + (61.0 - 58.0 * t + t.powi(2) + 600.0 * c - 330.0 * EARTH_ECCENTRICITY_SQ)
581                    * a_coeff.powi(6)
582                    / 720.0));
583
584    let final_northing = if lat < 0.0 {
585        northing + 10000000.0
586    } else {
587        northing
588    };
589
590    Ok((easting, final_northing, zone_number, zone_letter))
591}
592
593/// Get UTM zone letter from latitude
594#[allow(dead_code)]
595fn utm_zone_letter(lat: f64) -> SpatialResult<char> {
596    let letters = [
597        'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
598        'W', 'X',
599    ];
600
601    if !(-80.0..=84.0).contains(&lat) {
602        return Err(SpatialError::ValueError(
603            "Latitude out of UTM range".to_string(),
604        ));
605    }
606
607    let index = ((lat + 80.0) / 8.0).floor() as usize;
608    if index < letters.len() {
609        Ok(letters[index])
610    } else {
611        Ok('X') // Special case for 72°-84°N
612    }
613}
614
615/// Convert geographic coordinates to Web Mercator (EPSG:3857)
616///
617/// # Arguments
618///
619/// * `lat` - Latitude in degrees
620/// * `lon` - Longitude in degrees
621///
622/// # Returns
623///
624/// * (x, y) in Web Mercator coordinates (meters)
625#[allow(dead_code)]
626pub fn geographic_to_web_mercator(lat: f64, lon: f64) -> SpatialResult<(f64, f64)> {
627    if lat.abs() >= 85.051_128_779_806_59 {
628        return Err(SpatialError::ValueError(
629            "Latitude must be between -85.051° and 85.051° for Web Mercator".to_string(),
630        ));
631    }
632
633    let x = deg_to_rad(lon) * EARTH_EQUATORIAL_RADIUS_M;
634    let y = ((deg_to_rad(lat) / 2.0 + PI / 4.0).tan()).ln() * EARTH_EQUATORIAL_RADIUS_M;
635
636    Ok((x, y))
637}
638
639/// Convert Web Mercator coordinates to geographic coordinates
640///
641/// # Arguments
642///
643/// * `x` - X coordinate in Web Mercator (meters)
644/// * `y` - Y coordinate in Web Mercator (meters)
645///
646/// # Returns
647///
648/// * (latitude, longitude) in degrees
649#[allow(dead_code)]
650pub fn web_mercator_to_geographic(x: f64, y: f64) -> (f64, f64) {
651    let lon = rad_to_deg(x / EARTH_EQUATORIAL_RADIUS_M);
652    let lat = rad_to_deg(2.0 * ((y / EARTH_EQUATORIAL_RADIUS_M).exp().atan() - PI / 4.0));
653
654    (lat, lon)
655}
656
657/// Calculate the vincenty distance between two points (more accurate than Haversine)
658///
659/// This uses Vincenty's inverse formula for ellipsoidal calculations.
660/// More accurate than Haversine for long distances.
661///
662/// # Arguments
663///
664/// * `point1` - First point as (latitude, longitude) in degrees
665/// * `point2` - Second point as (latitude, longitude) in degrees
666///
667/// # Returns
668///
669/// * Distance in meters
670#[allow(dead_code)]
671pub fn vincenty_distance(point1: (f64, f64), point2: (f64, f64)) -> SpatialResult<f64> {
672    let (lat1, lon1) = (deg_to_rad(point1.0), deg_to_rad(point1.1));
673    let (lat2, lon2) = (deg_to_rad(point2.0), deg_to_rad(point2.1));
674
675    let a = EARTH_EQUATORIAL_RADIUS_M;
676    let b = EARTH_POLAR_RADIUS_M;
677    let f = EARTH_FLATTENING;
678
679    let l = lon2 - lon1;
680    let u1 = ((1.0 - f) * lat1.tan()).atan();
681    let u2 = ((1.0 - f) * lat2.tan()).atan();
682
683    let sin_u1 = u1.sin();
684    let cos_u1 = u1.cos();
685    let sin_u2 = u2.sin();
686    let cos_u2 = u2.cos();
687
688    let mut lambda = l;
689    let mut lambda_prev;
690    let mut iteration_limit = 100;
691
692    let (cos_sq_alpha, sin_sigma, cos_sigma, sigma, cos_2sigma_m) = loop {
693        iteration_limit -= 1;
694        if iteration_limit == 0 {
695            return Err(SpatialError::ComputationError(
696                "Vincenty formula failed to converge".to_string(),
697            ));
698        }
699
700        let sin_lambda = lambda.sin();
701        let cos_lambda = lambda.cos();
702
703        let sin_sigma = ((cos_u2 * sin_lambda).powi(2)
704            + (cos_u1 * sin_u2 - sin_u1 * cos_u2 * cos_lambda).powi(2))
705        .sqrt();
706
707        if sin_sigma == 0.0 {
708            return Ok(0.0); // Co-incident points
709        }
710
711        let cos_sigma = sin_u1 * sin_u2 + cos_u1 * cos_u2 * cos_lambda;
712        let sigma = sin_sigma.atan2(cos_sigma);
713
714        let sin_alpha = cos_u1 * cos_u2 * sin_lambda / sin_sigma;
715        let cos_sq_alpha = 1.0 - sin_alpha.powi(2);
716
717        let cos_2sigma_m = if cos_sq_alpha == 0.0 {
718            0.0 // Equatorial line
719        } else {
720            cos_sigma - 2.0 * sin_u1 * sin_u2 / cos_sq_alpha
721        };
722
723        let c = f / 16.0 * cos_sq_alpha * (4.0 + f * (4.0 - 3.0 * cos_sq_alpha));
724
725        lambda_prev = lambda;
726        lambda = l
727            + (1.0 - c)
728                * f
729                * sin_alpha
730                * (sigma
731                    + c * sin_sigma
732                        * (cos_2sigma_m + c * cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))));
733
734        if (lambda - lambda_prev).abs() < 1e-12 {
735            break (cos_sq_alpha, sin_sigma, cos_sigma, sigma, cos_2sigma_m);
736        }
737    };
738
739    let u_sq = cos_sq_alpha * (a.powi(2) - b.powi(2)) / b.powi(2);
740    let a_coeff = 1.0 + u_sq / 16384.0 * (4096.0 + u_sq * (-768.0 + u_sq * (320.0 - 175.0 * u_sq)));
741    let b_coeff = u_sq / 1024.0 * (256.0 + u_sq * (-128.0 + u_sq * (74.0 - 47.0 * u_sq)));
742
743    let delta_sigma = b_coeff
744        * sin_sigma
745        * (cos_2sigma_m
746            + b_coeff / 4.0
747                * (cos_sigma * (-1.0 + 2.0 * cos_2sigma_m.powi(2))
748                    - b_coeff / 6.0
749                        * cos_2sigma_m
750                        * (-3.0 + 4.0 * sin_sigma.powi(2))
751                        * (-3.0 + 4.0 * cos_2sigma_m.powi(2))));
752
753    let distance = b * a_coeff * (sigma - delta_sigma);
754
755    Ok(distance)
756}
757
758#[cfg(test)]
759mod tests {
760    use super::*;
761    use approx::assert_relative_eq;
762
763    #[test]
764    fn test_degree_radian_conversion() {
765        assert_relative_eq!(deg_to_rad(0.0), 0.0, epsilon = 1e-10);
766        assert_relative_eq!(deg_to_rad(90.0), PI / 2.0, epsilon = 1e-10);
767        assert_relative_eq!(deg_to_rad(180.0), PI, epsilon = 1e-10);
768        assert_relative_eq!(deg_to_rad(360.0), 2.0 * PI, epsilon = 1e-10);
769
770        assert_relative_eq!(rad_to_deg(0.0), 0.0, epsilon = 1e-10);
771        assert_relative_eq!(rad_to_deg(PI / 2.0), 90.0, epsilon = 1e-10);
772        assert_relative_eq!(rad_to_deg(PI), 180.0, epsilon = 1e-10);
773        assert_relative_eq!(rad_to_deg(2.0 * PI), 360.0, epsilon = 1e-10);
774    }
775
776    #[test]
777    fn test_haversine_distance() {
778        // Distance between London and Paris (approximately 344 km)
779        let london = (51.5074, -0.1278);
780        let paris = (48.8566, 2.3522);
781        let distance = haversine_distance(london, paris);
782        assert!((distance - 344_000.0).abs() < 5_000.0); // Within 5km tolerance
783
784        // Distance from a point to itself should be 0
785        assert_relative_eq!(haversine_distance(london, london), 0.0, epsilon = 1e-6);
786
787        // Antipodal points (opposite sides of Earth)
788        let north_pole = (90.0, 0.0);
789        let south_pole = (-90.0, 0.0);
790        let antipodal_distance = haversine_distance(north_pole, south_pole);
791        let expected_distance = PI * EARTH_RADIUS_M;
792        assert_relative_eq!(antipodal_distance, expected_distance, epsilon = 1000.0);
793    }
794
795    #[test]
796    fn test_initial_bearing() {
797        // Bearing from London to Paris should be roughly southeast
798        let london = (51.5074, -0.1278);
799        let paris = (48.8566, 2.3522);
800        let bearing = initial_bearing(london, paris);
801        let bearing_deg = rad_to_deg(bearing);
802
803        // Should be roughly in southeast direction (around 120-150 degrees)
804        assert!(bearing_deg > 100.0 && bearing_deg < 180.0);
805
806        // Bearing due north
807        let start = (0.0, 0.0);
808        let north = (1.0, 0.0);
809        let north_bearing = initial_bearing(start, north);
810        assert_relative_eq!(north_bearing, 0.0, epsilon = 1e-6);
811
812        // Bearing due east
813        let east = (0.0, 1.0);
814        let east_bearing = initial_bearing(start, east);
815        assert_relative_eq!(east_bearing, PI / 2.0, epsilon = 1e-6);
816    }
817
818    #[test]
819    fn test_destination_point() {
820        let start = (51.5074, -0.1278); // London
821        let distance = 100_000.0; // 100 km
822        let bearing = 0.0; // Due north
823
824        let destination = destination_point(start, distance, bearing);
825
826        // Should be roughly north of London
827        assert!(destination.0 > start.0); // Latitude should increase
828        assert!((destination.1 - start.1).abs() < 0.1); // Longitude should change little
829
830        // Verify round trip
831        let calculated_distance = haversine_distance(start, destination);
832        assert_relative_eq!(calculated_distance, distance, epsilon = 1000.0); // Within 1km
833    }
834
835    #[test]
836    fn test_midpoint() {
837        let london = (51.5074, -0.1278);
838        let paris = (48.8566, 2.3522);
839        let mid = midpoint(london, paris);
840
841        // Midpoint should be between the two cities
842        assert!(mid.0 < london.0 && mid.0 > paris.0); // Latitude between
843        assert!(mid.1 > london.1 && mid.1 < paris.1); // Longitude between
844
845        // Distance from midpoint to each city should be roughly equal
846        let dist_to_london = haversine_distance(mid, london);
847        let dist_to_paris = haversine_distance(mid, paris);
848        assert_relative_eq!(dist_to_london, dist_to_paris, epsilon = 1000.0);
849    }
850
851    #[test]
852    fn test_normalize_angle() {
853        assert_relative_eq!(normalize_angle(0.0), 0.0, epsilon = 1e-10);
854        assert_relative_eq!(normalize_angle(PI), PI, epsilon = 1e-10);
855        assert_relative_eq!(normalize_angle(2.0 * PI), 0.0, epsilon = 1e-10);
856        assert_relative_eq!(normalize_angle(-PI), PI, epsilon = 1e-10);
857        assert_relative_eq!(normalize_angle(3.0 * PI), PI, epsilon = 1e-10);
858    }
859
860    #[test]
861    fn test_normalize_bearing() {
862        assert_relative_eq!(normalize_bearing(0.0), 0.0, epsilon = 1e-10);
863        assert_relative_eq!(normalize_bearing(180.0), 180.0, epsilon = 1e-10);
864        assert_relative_eq!(normalize_bearing(360.0), 0.0, epsilon = 1e-10);
865        assert_relative_eq!(normalize_bearing(-90.0), 270.0, epsilon = 1e-10);
866        assert_relative_eq!(normalize_bearing(450.0), 90.0, epsilon = 1e-10);
867    }
868
869    #[test]
870    fn test_spherical_polygon_area() {
871        // Simple triangle
872        let triangle = vec![
873            (0.0, 0.0), // Equator, Greenwich
874            (0.0, 1.0), // Equator, 1° East
875            (1.0, 0.0), // 1° North, Greenwich
876        ];
877
878        let area = spherical_polygon_area(&triangle).unwrap();
879        assert!(area > 0.0);
880
881        // Area should be reasonable for a 1°×1° triangle
882        // Expected area is roughly (π/180)² * R² / 2
883        let expected = (PI / 180.0).powi(2) * EARTH_RADIUS_M.powi(2) / 2.0;
884        assert_relative_eq!(area, expected, epsilon = expected * 0.1);
885    }
886
887    #[test]
888    fn test_geographic_to_web_mercator() {
889        // Test equator and prime meridian
890        let (x, y) = geographic_to_web_mercator(0.0, 0.0).unwrap();
891        assert_relative_eq!(x, 0.0, epsilon = 1e-6);
892        assert_relative_eq!(y, 0.0, epsilon = 1e-6);
893
894        // Test round trip
895        let original = (45.0, -90.0);
896        let (x, y) = geographic_to_web_mercator(original.0, original.1).unwrap();
897        let back = web_mercator_to_geographic(x, y);
898        assert_relative_eq!(back.0, original.0, epsilon = 1e-6);
899        assert_relative_eq!(back.1, original.1, epsilon = 1e-6);
900
901        // Test error case
902        let result = geographic_to_web_mercator(86.0, 0.0);
903        assert!(result.is_err());
904    }
905
906    #[test]
907    fn test_geographic_to_utm() {
908        // Test a known location (London)
909        let london = (51.5074, -0.1278);
910        let (easting, northing, zone, letter) = geographic_to_utm(london.0, london.1).unwrap();
911
912        // London should be in UTM zone 30 or 31
913        assert!(zone == 30 || zone == 31);
914        assert!(letter == 'U' || letter == 'V');
915
916        // Coordinates should be reasonable
917        assert!(easting > 400_000.0 && easting < 700_000.0);
918        assert!(northing > 5_700_000.0 && northing < 5_800_000.0);
919
920        // Test error cases
921        assert!(geographic_to_utm(85.0, 0.0).is_err()); // Latitude too high
922        assert!(geographic_to_utm(-85.0, 0.0).is_err()); // Latitude too low
923    }
924
925    #[test]
926    fn test_cross_track_distance() {
927        let start = (51.0, 0.0);
928        let end = (52.0, 1.0);
929        let point = (51.5, 0.0); // Point on the same meridian as start
930
931        let cross_track = cross_track_distance(point, start, end);
932
933        // Should be relatively small since point is close to the great circle
934        assert!(cross_track.abs() < 50_000.0); // Within 50km
935    }
936
937    #[test]
938    fn test_vincenty_distance() {
939        // Test against Haversine for short distance
940        let london = (51.5074, -0.1278);
941        let paris = (48.8566, 2.3522);
942
943        let haversine_dist = haversine_distance(london, paris);
944        let vincenty_dist = vincenty_distance(london, paris).unwrap();
945
946        // Should be very close for moderate distances
947        let diff_percent = ((vincenty_dist - haversine_dist) / haversine_dist * 100.0).abs();
948        assert!(diff_percent < 1.0); // Within 1%
949
950        // Test identical points
951        let same_point_dist = vincenty_distance(london, london).unwrap();
952        assert_relative_eq!(same_point_dist, 0.0, epsilon = 1e-6);
953    }
954
955    #[test]
956    fn test_point_in_spherical_polygon() {
957        // Simple square around equator
958        let square = vec![(-1.0, -1.0), (1.0, -1.0), (1.0, 1.0), (-1.0, 1.0)];
959
960        // Point inside
961        assert!(point_in_spherical_polygon((0.0, 0.0), &square));
962
963        // Point outside
964        assert!(!point_in_spherical_polygon((2.0, 2.0), &square));
965
966        // Point on edge (may be unstable, so just ensure it doesn't crash)
967        let _ = point_in_spherical_polygon((1.0, 0.0), &square);
968    }
969}