spatio_types/
point.rs

1use geo::Point;
2use serde::{Deserialize, Serialize};
3use std::time::SystemTime;
4
5/// A 3D geographic point with x, y (longitude/latitude) and z (altitude/elevation).
6///
7/// This type represents a point in 3D space, typically used for altitude-aware
8/// geospatial applications like drone tracking, aviation, or multi-floor buildings.
9///
10/// # Examples
11///
12/// ```
13/// use spatio_types::point::Point3d;
14/// use geo::Point;
15///
16/// // Create a 3D point for a drone at 100 meters altitude
17/// let drone_position = Point3d::new(-74.0060, 40.7128, 100.0);
18/// assert_eq!(drone_position.altitude(), 100.0);
19///
20/// // Calculate 3D distance to another point
21/// let other = Point3d::new(-74.0070, 40.7138, 150.0);
22/// let distance = drone_position.distance_3d(&other);
23/// ```
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Point3d {
26    /// The 2D geographic point (longitude/latitude or x/y)
27    pub point: Point<f64>,
28    /// The altitude/elevation/z-coordinate (in meters typically)
29    pub z: f64,
30}
31
32impl Point3d {
33    /// Create a new 3D point from x, y, and z coordinates.
34    ///
35    /// # Arguments
36    ///
37    /// * `x` - Longitude or x-coordinate
38    /// * `y` - Latitude or y-coordinate
39    /// * `z` - Altitude/elevation in meters
40    ///
41    /// # Examples
42    ///
43    /// ```
44    /// use spatio_types::point::Point3d;
45    ///
46    /// let point = Point3d::new(-74.0060, 40.7128, 100.0);
47    /// ```
48    pub fn new(x: f64, y: f64, z: f64) -> Self {
49        Self {
50            point: Point::new(x, y),
51            z,
52        }
53    }
54
55    /// Create a 3D point from a 2D point and altitude.
56    pub fn from_point_and_altitude(point: Point<f64>, z: f64) -> Self {
57        Self { point, z }
58    }
59
60    /// Get the x coordinate (longitude).
61    pub fn x(&self) -> f64 {
62        self.point.x()
63    }
64
65    /// Get the y coordinate (latitude).
66    pub fn y(&self) -> f64 {
67        self.point.y()
68    }
69
70    /// Get the z coordinate (altitude/elevation).
71    pub fn z(&self) -> f64 {
72        self.z
73    }
74
75    /// Get the altitude (alias for z()).
76    pub fn altitude(&self) -> f64 {
77        self.z
78    }
79
80    /// Get a reference to the underlying 2D point.
81    pub fn point_2d(&self) -> &Point<f64> {
82        &self.point
83    }
84
85    /// Project this 3D point to 2D by discarding the z coordinate.
86    pub fn to_2d(&self) -> Point<f64> {
87        self.point
88    }
89
90    /// Calculate the 3D Euclidean distance to another 3D point.
91    ///
92    /// This calculates the straight-line distance in 3D space using the Pythagorean theorem.
93    /// Note: For geographic coordinates, this treats lat/lon as Cartesian coordinates,
94    /// which is only accurate for small distances. For large distances, use `haversine_3d`.
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use spatio_types::point::Point3d;
100    ///
101    /// let p1 = Point3d::new(0.0, 0.0, 0.0);
102    /// let p2 = Point3d::new(3.0, 4.0, 12.0);
103    /// let distance = p1.distance_3d(&p2);
104    /// assert_eq!(distance, 13.0); // 3-4-5 triangle extended to 3D
105    /// ```
106    pub fn distance_3d(&self, other: &Point3d) -> f64 {
107        let dx = self.x() - other.x();
108        let dy = self.y() - other.y();
109        let dz = self.z - other.z;
110        (dx * dx + dy * dy + dz * dz).sqrt()
111    }
112
113    /// Calculate all distance components at once (horizontal, altitude, 3D).
114    ///
115    /// This is more efficient than calling haversine_2d and haversine_3d separately
116    /// as it calculates the haversine formula only once.
117    ///
118    /// # Returns
119    ///
120    /// Tuple of (horizontal_distance, altitude_difference, distance_3d) in meters.
121    ///
122    /// # Examples
123    ///
124    /// ```
125    /// use spatio_types::point::Point3d;
126    ///
127    /// let p1 = Point3d::new(-74.0060, 40.7128, 0.0);
128    /// let p2 = Point3d::new(-74.0070, 40.7138, 100.0);
129    /// let (h_dist, alt_diff, dist_3d) = p1.haversine_distances(&p2);
130    /// ```
131    pub fn haversine_distances(&self, other: &Point3d) -> (f64, f64, f64) {
132        const EARTH_RADIUS_METERS: f64 = 6_371_000.0;
133
134        let lat1 = self.y().to_radians();
135        let lat2 = other.y().to_radians();
136        let delta_lat = (other.y() - self.y()).to_radians();
137        let delta_lon = (other.x() - self.x()).to_radians();
138
139        let a = (delta_lat / 2.0).sin().powi(2)
140            + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
141        let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
142
143        let horizontal_distance = EARTH_RADIUS_METERS * c;
144        let altitude_diff = (self.z - other.z).abs();
145        let distance_3d =
146            (horizontal_distance * horizontal_distance + altitude_diff * altitude_diff).sqrt();
147
148        (horizontal_distance, altitude_diff, distance_3d)
149    }
150
151    /// Calculate the haversine distance combined with altitude difference.
152    ///
153    /// This uses the haversine formula for the horizontal distance (considering Earth's curvature)
154    /// and combines it with the altitude difference using the Pythagorean theorem.
155    ///
156    /// # Returns
157    ///
158    /// Distance in meters.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use spatio_types::point::Point3d;
164    ///
165    /// let p1 = Point3d::new(-74.0060, 40.7128, 0.0);    // NYC sea level
166    /// let p2 = Point3d::new(-74.0070, 40.7138, 100.0);  // Nearby, 100m up
167    /// let distance = p1.haversine_3d(&p2);
168    /// ```
169    #[inline]
170    pub fn haversine_3d(&self, other: &Point3d) -> f64 {
171        let (_, _, dist_3d) = self.haversine_distances(other);
172        dist_3d
173    }
174
175    /// Calculate the haversine distance on the 2D plane (ignoring altitude).
176    ///
177    /// # Returns
178    ///
179    /// Distance in meters.
180    #[inline]
181    pub fn haversine_2d(&self, other: &Point3d) -> f64 {
182        const EARTH_RADIUS_METERS: f64 = 6_371_000.0;
183
184        let lat1 = self.y().to_radians();
185        let lat2 = other.y().to_radians();
186        let delta_lat = (other.y() - self.y()).to_radians();
187        let delta_lon = (other.x() - self.x()).to_radians();
188
189        let a = (delta_lat / 2.0).sin().powi(2)
190            + lat1.cos() * lat2.cos() * (delta_lon / 2.0).sin().powi(2);
191        let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
192
193        EARTH_RADIUS_METERS * c
194    }
195
196    /// Get the altitude difference to another point.
197    #[inline]
198    pub fn altitude_difference(&self, other: &Point3d) -> f64 {
199        (self.z - other.z).abs()
200    }
201}
202
203/// A geographic point with an associated timestamp.
204#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
205pub struct TemporalPoint {
206    pub point: Point<f64>,
207    pub timestamp: SystemTime,
208}
209
210impl TemporalPoint {
211    pub fn new(point: Point<f64>, timestamp: SystemTime) -> Self {
212        Self { point, timestamp }
213    }
214
215    pub fn point(&self) -> &Point<f64> {
216        &self.point
217    }
218
219    pub fn timestamp(&self) -> &SystemTime {
220        &self.timestamp
221    }
222}
223
224/// A geographic point with an associated altitude and timestamp.
225#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
226pub struct TemporalPoint3D {
227    pub point: Point<f64>,
228    pub altitude: f64,
229    pub timestamp: SystemTime,
230}
231
232impl TemporalPoint3D {
233    pub fn new(point: Point<f64>, altitude: f64, timestamp: SystemTime) -> Self {
234        Self {
235            point,
236            altitude,
237            timestamp,
238        }
239    }
240
241    pub fn point(&self) -> &Point<f64> {
242        &self.point
243    }
244
245    pub fn altitude(&self) -> f64 {
246        self.altitude
247    }
248
249    pub fn timestamp(&self) -> &SystemTime {
250        &self.timestamp
251    }
252
253    /// Convert to a 3D point.
254    pub fn to_point_3d(&self) -> Point3d {
255        Point3d::from_point_and_altitude(self.point, self.altitude)
256    }
257
258    /// Calculate 3D haversine distance to another temporal 3D point.
259    pub fn distance_to(&self, other: &TemporalPoint3D) -> f64 {
260        self.to_point_3d().haversine_3d(&other.to_point_3d())
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn test_point3d_creation() {
270        let p = Point3d::new(-74.0, 40.7, 100.0);
271        assert_eq!(p.x(), -74.0);
272        assert_eq!(p.y(), 40.7);
273        assert_eq!(p.z(), 100.0);
274        assert_eq!(p.altitude(), 100.0);
275    }
276
277    #[test]
278    fn test_point3d_distance_3d() {
279        let p1 = Point3d::new(0.0, 0.0, 0.0);
280        let p2 = Point3d::new(3.0, 4.0, 12.0);
281        let distance = p1.distance_3d(&p2);
282        assert_eq!(distance, 13.0);
283    }
284
285    #[test]
286    fn test_point3d_altitude_difference() {
287        let p1 = Point3d::new(-74.0, 40.7, 50.0);
288        let p2 = Point3d::new(-74.0, 40.7, 150.0);
289        assert_eq!(p1.altitude_difference(&p2), 100.0);
290    }
291
292    #[test]
293    fn test_point3d_to_2d() {
294        let p = Point3d::new(-74.0, 40.7, 100.0);
295        let p2d = p.to_2d();
296        assert_eq!(p2d.x(), -74.0);
297        assert_eq!(p2d.y(), 40.7);
298    }
299
300    #[test]
301    fn test_haversine_3d() {
302        // Two points at same location but different altitudes
303        let p1 = Point3d::new(-74.0, 40.7, 0.0);
304        let p2 = Point3d::new(-74.0, 40.7, 100.0);
305        let distance = p1.haversine_3d(&p2);
306        // Should be approximately 100 meters (just the altitude difference)
307        assert!((distance - 100.0).abs() < 0.1);
308    }
309
310    #[test]
311    fn test_haversine_distances() {
312        let p1 = Point3d::new(-74.0060, 40.7128, 0.0);
313        let p2 = Point3d::new(-74.0070, 40.7138, 100.0);
314        let (h_dist, alt_diff, dist_3d) = p1.haversine_distances(&p2);
315
316        // Verify altitude difference is correct
317        assert_eq!(alt_diff, 100.0);
318
319        // Verify 3D distance is correct
320        assert!((dist_3d - (h_dist * h_dist + alt_diff * alt_diff).sqrt()).abs() < 0.1);
321
322        // Verify it matches individual calls
323        assert!((h_dist - p1.haversine_2d(&p2)).abs() < 0.1);
324        assert!((dist_3d - p1.haversine_3d(&p2)).abs() < 0.1);
325    }
326
327    #[test]
328    fn test_temporal_point3d_to_point3d() {
329        let temporal = TemporalPoint3D::new(Point::new(-74.0, 40.7), 100.0, SystemTime::now());
330        let p3d = temporal.to_point_3d();
331        assert_eq!(p3d.x(), -74.0);
332        assert_eq!(p3d.y(), 40.7);
333        assert_eq!(p3d.altitude(), 100.0);
334    }
335}