spatio_types/
point.rs

1use crate::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 spatio_types::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,
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, 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 {
82        &self.point
83    }
84
85    /// Project this 3D point to 2D by discarding the z coordinate.
86    pub fn to_2d(&self) -> Point {
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    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use spatio_types::point::Point3d;
98    ///
99    /// let p1 = Point3d::new(0.0, 0.0, 0.0);
100    /// let p2 = Point3d::new(3.0, 4.0, 12.0);
101    /// let distance = p1.distance_3d(&p2);
102    /// assert_eq!(distance, 13.0); // 3-4-5 triangle extended to 3D
103    /// ```
104    pub fn distance_3d(&self, other: &Point3d) -> f64 {
105        let dx = self.x() - other.x();
106        let dy = self.y() - other.y();
107        let dz = self.z - other.z;
108        (dx * dx + dy * dy + dz * dz).sqrt()
109    }
110
111    /// Calculate all distance components at once (horizontal, altitude, 3D).
112    ///
113    /// This is more efficient than calling haversine_2d and haversine_3d separately
114    /// as it calculates the haversine formula only once.
115    ///
116    /// # Returns
117    ///
118    /// Tuple of (horizontal_distance, altitude_difference, distance_3d) in meters.
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use spatio_types::point::Point3d;
124    ///
125    /// let p1 = Point3d::new(-74.0060, 40.7128, 0.0);
126    /// let p2 = Point3d::new(-74.0070, 40.7138, 100.0);
127    /// let (h_dist, alt_diff, dist_3d) = p1.haversine_distances(&p2);
128    /// ```
129    pub fn haversine_distances(&self, other: &Point3d) -> (f64, f64, f64) {
130        let horizontal_distance = self.point.haversine_distance(&other.point);
131        let altitude_diff = (self.z - other.z).abs();
132        let distance_3d = (horizontal_distance.powi(2) + altitude_diff.powi(2)).sqrt();
133
134        (horizontal_distance, altitude_diff, distance_3d)
135    }
136
137    /// Calculate the haversine distance combined with altitude difference.
138    ///
139    /// This uses the haversine formula for the horizontal distance (considering Earth's curvature)
140    /// and combines it with the altitude difference using the Pythagorean theorem.
141    ///
142    /// # Returns
143    ///
144    /// Distance in meters.
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use spatio_types::point::Point3d;
150    ///
151    /// let p1 = Point3d::new(-74.0060, 40.7128, 0.0);    // NYC sea level
152    /// let p2 = Point3d::new(-74.0070, 40.7138, 100.0);  // Nearby, 100m up
153    /// let distance = p1.haversine_3d(&p2);
154    /// ```
155    #[inline]
156    pub fn haversine_3d(&self, other: &Point3d) -> f64 {
157        let (_, _, dist_3d) = self.haversine_distances(other);
158        dist_3d
159    }
160
161    /// Calculate the haversine distance on the 2D plane (ignoring altitude).
162    ///
163    /// # Returns
164    ///
165    /// Distance in meters.
166    #[inline]
167    pub fn haversine_2d(&self, other: &Point3d) -> f64 {
168        self.point.haversine_distance(&other.point)
169    }
170
171    /// Get the altitude difference to another point.
172    #[inline]
173    pub fn altitude_difference(&self, other: &Point3d) -> f64 {
174        (self.z - other.z).abs()
175    }
176
177    /// Convert to GeoJSON string representation.
178    ///
179    /// # Examples
180    ///
181    /// ```
182    /// # #[cfg(feature = "geojson")]
183    /// # {
184    /// use spatio_types::point::Point3d;
185    ///
186    /// let point = Point3d::new(-74.0060, 40.7128, 100.0);
187    /// let json = point.to_geojson().unwrap();
188    /// assert!(json.contains("Point"));
189    /// # }
190    /// ```
191    #[cfg(feature = "geojson")]
192    pub fn to_geojson(&self) -> Result<String, crate::geo::GeoJsonError> {
193        use geojson::{Geometry, Value};
194
195        let geom = Geometry::new(Value::Point(vec![self.x(), self.y(), self.z()]));
196        serde_json::to_string(&geom).map_err(|e| {
197            crate::geo::GeoJsonError::Serialization(format!("Failed to serialize 3D point: {}", e))
198        })
199    }
200
201    /// Parse from GeoJSON string. Defaults altitude to 0.0 if not present.
202    ///
203    /// # Examples
204    ///
205    /// ```
206    /// # #[cfg(feature = "geojson")]
207    /// # {
208    /// use spatio_types::point::Point3d;
209    ///
210    /// let json = r#"{"type":"Point","coordinates":[-74.006,40.7128,100.0]}"#;
211    /// let point = Point3d::from_geojson(json).unwrap();
212    /// assert_eq!(point.x(), -74.006);
213    /// assert_eq!(point.z(), 100.0);
214    /// # }
215    /// ```
216    #[cfg(feature = "geojson")]
217    pub fn from_geojson(geojson: &str) -> Result<Self, crate::geo::GeoJsonError> {
218        use geojson::{Geometry, Value};
219
220        let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
221            crate::geo::GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
222        })?;
223
224        match geom.value {
225            Value::Point(coords) => {
226                if coords.len() < 2 {
227                    return Err(crate::geo::GeoJsonError::InvalidCoordinates(
228                        "Point must have at least 2 coordinates".to_string(),
229                    ));
230                }
231                let z = coords.get(2).copied().unwrap_or(0.0);
232                Ok(Point3d::new(coords[0], coords[1], z))
233            }
234            _ => Err(crate::geo::GeoJsonError::InvalidGeometry(
235                "GeoJSON geometry is not a Point".to_string(),
236            )),
237        }
238    }
239}
240
241/// A geographic point with an associated timestamp.
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243pub struct TemporalPoint {
244    pub point: Point,
245    pub timestamp: SystemTime,
246}
247
248impl TemporalPoint {
249    pub fn new(point: Point, timestamp: SystemTime) -> Self {
250        Self { point, timestamp }
251    }
252
253    pub fn point(&self) -> &Point {
254        &self.point
255    }
256
257    pub fn timestamp(&self) -> &SystemTime {
258        &self.timestamp
259    }
260}
261
262/// A geographic point with an associated altitude and timestamp.
263#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
264pub struct TemporalPoint3D {
265    pub point: Point,
266    pub altitude: f64,
267    pub timestamp: SystemTime,
268}
269
270impl TemporalPoint3D {
271    pub fn new(point: Point, altitude: f64, timestamp: SystemTime) -> Self {
272        Self {
273            point,
274            altitude,
275            timestamp,
276        }
277    }
278
279    pub fn point(&self) -> &Point {
280        &self.point
281    }
282
283    pub fn altitude(&self) -> f64 {
284        self.altitude
285    }
286
287    pub fn timestamp(&self) -> &SystemTime {
288        &self.timestamp
289    }
290
291    /// Convert to a 3D point.
292    pub fn to_point_3d(&self) -> Point3d {
293        Point3d::from_point_and_altitude(self.point, self.altitude)
294    }
295
296    /// Calculate 3D haversine distance to another temporal 3D point.
297    pub fn distance_to(&self, other: &TemporalPoint3D) -> f64 {
298        self.to_point_3d().haversine_3d(&other.to_point_3d())
299    }
300}
301
302#[cfg(test)]
303mod tests {
304    use super::*;
305
306    #[test]
307    fn test_point3d_creation() {
308        let p = Point3d::new(-74.0, 40.7, 100.0);
309        assert_eq!(p.x(), -74.0);
310        assert_eq!(p.y(), 40.7);
311        assert_eq!(p.z(), 100.0);
312        assert_eq!(p.altitude(), 100.0);
313    }
314
315    #[test]
316    fn test_point3d_distance_3d() {
317        let p1 = Point3d::new(0.0, 0.0, 0.0);
318        let p2 = Point3d::new(3.0, 4.0, 12.0);
319        let distance = p1.distance_3d(&p2);
320        assert_eq!(distance, 13.0);
321    }
322
323    #[test]
324    fn test_point3d_altitude_difference() {
325        let p1 = Point3d::new(-74.0, 40.7, 50.0);
326        let p2 = Point3d::new(-74.0, 40.7, 150.0);
327        assert_eq!(p1.altitude_difference(&p2), 100.0);
328    }
329
330    #[test]
331    fn test_point3d_to_2d() {
332        let p = Point3d::new(-74.0, 40.7, 100.0);
333        let p2d = p.to_2d();
334        assert_eq!(p2d.x(), -74.0);
335        assert_eq!(p2d.y(), 40.7);
336    }
337
338    #[test]
339    fn test_haversine_3d() {
340        // Two points at same location but different altitudes
341        let p1 = Point3d::new(-74.0, 40.7, 0.0);
342        let p2 = Point3d::new(-74.0, 40.7, 100.0);
343        let distance = p1.haversine_3d(&p2);
344        // Should be approximately 100 meters (just the altitude difference)
345        assert!((distance - 100.0).abs() < 0.1);
346    }
347
348    #[test]
349    fn test_haversine_distances() {
350        let p1 = Point3d::new(-74.0060, 40.7128, 0.0);
351        let p2 = Point3d::new(-74.0070, 40.7138, 100.0);
352        let (h_dist, alt_diff, dist_3d) = p1.haversine_distances(&p2);
353
354        // Verify altitude difference is correct
355        assert_eq!(alt_diff, 100.0);
356
357        // Verify 3D distance is correct
358        assert!((dist_3d - (h_dist * h_dist + alt_diff * alt_diff).sqrt()).abs() < 0.1);
359
360        // Verify it matches individual calls
361        assert!((h_dist - p1.haversine_2d(&p2)).abs() < 0.1);
362        assert!((dist_3d - p1.haversine_3d(&p2)).abs() < 0.1);
363    }
364
365    #[test]
366    fn test_temporal_point3d_to_point3d() {
367        let temporal = TemporalPoint3D::new(Point::new(-74.0, 40.7), 100.0, SystemTime::now());
368        let p3d = temporal.to_point_3d();
369        assert_eq!(p3d.x(), -74.0);
370        assert_eq!(p3d.y(), 40.7);
371        assert_eq!(p3d.altitude(), 100.0);
372    }
373
374    #[cfg(feature = "geojson")]
375    #[test]
376    fn test_point3d_geojson_roundtrip() {
377        let original = Point3d::new(-74.0060, 40.7128, 100.0);
378        let json = original.to_geojson().unwrap();
379        let parsed = Point3d::from_geojson(&json).unwrap();
380
381        assert!((original.x() - parsed.x()).abs() < 1e-10);
382        assert!((original.y() - parsed.y()).abs() < 1e-10);
383        assert!((original.z() - parsed.z()).abs() < 1e-10);
384    }
385
386    #[cfg(feature = "geojson")]
387    #[test]
388    fn test_point3d_from_geojson_defaults_z() {
389        let json = r#"{"type":"Point","coordinates":[-74.006,40.7128]}"#;
390        let point = Point3d::from_geojson(json).unwrap();
391        assert_eq!(point.z(), 0.0);
392    }
393}