Skip to main content

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