1use crate::geo::Point;
2use serde::{Deserialize, Serialize};
3use std::time::SystemTime;
4
5#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25pub struct Point3d {
26 pub point: Point,
28 pub z: f64,
30}
31
32impl Point3d {
33 pub fn new(x: f64, y: f64, z: f64) -> Self {
49 Self {
50 point: Point::new(x, y),
51 z,
52 }
53 }
54
55 pub fn from_point_and_altitude(point: Point, z: f64) -> Self {
57 Self { point, z }
58 }
59
60 pub fn x(&self) -> f64 {
62 self.point.x()
63 }
64
65 pub fn y(&self) -> f64 {
67 self.point.y()
68 }
69
70 pub fn z(&self) -> f64 {
72 self.z
73 }
74
75 pub fn altitude(&self) -> f64 {
77 self.z
78 }
79
80 pub fn point_2d(&self) -> &Point {
82 &self.point
83 }
84
85 pub fn to_2d(&self) -> Point {
87 self.point
88 }
89
90 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 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 #[inline]
156 pub fn haversine_3d(&self, other: &Point3d) -> f64 {
157 let (_, _, dist_3d) = self.haversine_distances(other);
158 dist_3d
159 }
160
161 #[inline]
167 pub fn haversine_2d(&self, other: &Point3d) -> f64 {
168 self.point.haversine_distance(&other.point)
169 }
170
171 #[inline]
173 pub fn altitude_difference(&self, other: &Point3d) -> f64 {
174 (self.z - other.z).abs()
175 }
176
177 #[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 #[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#[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#[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 pub fn to_point_3d(&self) -> Point3d {
293 Point3d::from_point_and_altitude(self.point, self.altitude)
294 }
295
296 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 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 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 assert_eq!(alt_diff, 100.0);
356
357 assert!((dist_3d - (h_dist * h_dist + alt_diff * alt_diff).sqrt()).abs() < 0.1);
359
360 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}