spatio_types/point.rs
1use geo::{Distance, Haversine, 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 ///
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 = Haversine.distance(self.point, 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 Haversine.distance(self.point, 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
178/// A geographic point with an associated timestamp.
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
180pub struct TemporalPoint {
181 pub point: Point<f64>,
182 pub timestamp: SystemTime,
183}
184
185impl TemporalPoint {
186 pub fn new(point: Point<f64>, timestamp: SystemTime) -> Self {
187 Self { point, timestamp }
188 }
189
190 pub fn point(&self) -> &Point<f64> {
191 &self.point
192 }
193
194 pub fn timestamp(&self) -> &SystemTime {
195 &self.timestamp
196 }
197}
198
199/// A geographic point with an associated altitude and timestamp.
200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
201pub struct TemporalPoint3D {
202 pub point: Point<f64>,
203 pub altitude: f64,
204 pub timestamp: SystemTime,
205}
206
207impl TemporalPoint3D {
208 pub fn new(point: Point<f64>, altitude: f64, timestamp: SystemTime) -> Self {
209 Self {
210 point,
211 altitude,
212 timestamp,
213 }
214 }
215
216 pub fn point(&self) -> &Point<f64> {
217 &self.point
218 }
219
220 pub fn altitude(&self) -> f64 {
221 self.altitude
222 }
223
224 pub fn timestamp(&self) -> &SystemTime {
225 &self.timestamp
226 }
227
228 /// Convert to a 3D point.
229 pub fn to_point_3d(&self) -> Point3d {
230 Point3d::from_point_and_altitude(self.point, self.altitude)
231 }
232
233 /// Calculate 3D haversine distance to another temporal 3D point.
234 pub fn distance_to(&self, other: &TemporalPoint3D) -> f64 {
235 self.to_point_3d().haversine_3d(&other.to_point_3d())
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 #[test]
244 fn test_point3d_creation() {
245 let p = Point3d::new(-74.0, 40.7, 100.0);
246 assert_eq!(p.x(), -74.0);
247 assert_eq!(p.y(), 40.7);
248 assert_eq!(p.z(), 100.0);
249 assert_eq!(p.altitude(), 100.0);
250 }
251
252 #[test]
253 fn test_point3d_distance_3d() {
254 let p1 = Point3d::new(0.0, 0.0, 0.0);
255 let p2 = Point3d::new(3.0, 4.0, 12.0);
256 let distance = p1.distance_3d(&p2);
257 assert_eq!(distance, 13.0);
258 }
259
260 #[test]
261 fn test_point3d_altitude_difference() {
262 let p1 = Point3d::new(-74.0, 40.7, 50.0);
263 let p2 = Point3d::new(-74.0, 40.7, 150.0);
264 assert_eq!(p1.altitude_difference(&p2), 100.0);
265 }
266
267 #[test]
268 fn test_point3d_to_2d() {
269 let p = Point3d::new(-74.0, 40.7, 100.0);
270 let p2d = p.to_2d();
271 assert_eq!(p2d.x(), -74.0);
272 assert_eq!(p2d.y(), 40.7);
273 }
274
275 #[test]
276 fn test_haversine_3d() {
277 // Two points at same location but different altitudes
278 let p1 = Point3d::new(-74.0, 40.7, 0.0);
279 let p2 = Point3d::new(-74.0, 40.7, 100.0);
280 let distance = p1.haversine_3d(&p2);
281 // Should be approximately 100 meters (just the altitude difference)
282 assert!((distance - 100.0).abs() < 0.1);
283 }
284
285 #[test]
286 fn test_haversine_distances() {
287 let p1 = Point3d::new(-74.0060, 40.7128, 0.0);
288 let p2 = Point3d::new(-74.0070, 40.7138, 100.0);
289 let (h_dist, alt_diff, dist_3d) = p1.haversine_distances(&p2);
290
291 // Verify altitude difference is correct
292 assert_eq!(alt_diff, 100.0);
293
294 // Verify 3D distance is correct
295 assert!((dist_3d - (h_dist * h_dist + alt_diff * alt_diff).sqrt()).abs() < 0.1);
296
297 // Verify it matches individual calls
298 assert!((h_dist - p1.haversine_2d(&p2)).abs() < 0.1);
299 assert!((dist_3d - p1.haversine_3d(&p2)).abs() < 0.1);
300 }
301
302 #[test]
303 fn test_temporal_point3d_to_point3d() {
304 let temporal = TemporalPoint3D::new(Point::new(-74.0, 40.7), 100.0, SystemTime::now());
305 let p3d = temporal.to_point_3d();
306 assert_eq!(p3d.x(), -74.0);
307 assert_eq!(p3d.y(), 40.7);
308 assert_eq!(p3d.altitude(), 100.0);
309 }
310}