use crate::geo::Point;
use serde::{Deserialize, Serialize};
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Point3d {
pub point: Point,
pub z: f64,
}
impl Point3d {
#[inline]
#[must_use]
pub fn new(x: f64, y: f64, z: f64) -> Self {
Self {
point: Point::new(x, y),
z,
}
}
#[inline]
#[must_use]
pub fn from_point_and_altitude(point: Point, z: f64) -> Self {
Self { point, z }
}
#[inline]
pub fn x(&self) -> f64 {
self.point.x()
}
#[inline]
pub fn y(&self) -> f64 {
self.point.y()
}
#[inline]
pub fn z(&self) -> f64 {
self.z
}
#[inline]
pub fn altitude(&self) -> f64 {
self.z
}
#[inline]
pub fn point_2d(&self) -> &Point {
&self.point
}
#[inline]
#[must_use]
pub fn to_2d(&self) -> Point {
self.point
}
#[inline]
#[must_use]
pub fn distance_3d(&self, other: &Point3d) -> f64 {
let dx = self.x() - other.x();
let dy = self.y() - other.y();
let dz = self.z - other.z;
(dx * dx + dy * dy + dz * dz).sqrt()
}
pub fn haversine_distances(&self, other: &Point3d) -> (f64, f64, f64) {
let horizontal_distance = self.point.haversine_distance(&other.point);
let altitude_diff = (self.z - other.z).abs();
let distance_3d = (horizontal_distance.powi(2) + altitude_diff.powi(2)).sqrt();
(horizontal_distance, altitude_diff, distance_3d)
}
#[inline]
pub fn haversine_3d(&self, other: &Point3d) -> f64 {
let (_, _, dist_3d) = self.haversine_distances(other);
dist_3d
}
#[inline]
pub fn haversine_2d(&self, other: &Point3d) -> f64 {
self.point.haversine_distance(&other.point)
}
#[inline]
pub fn altitude_difference(&self, other: &Point3d) -> f64 {
(self.z - other.z).abs()
}
#[cfg(feature = "geojson")]
pub fn to_geojson(&self) -> Result<String, crate::geo::GeoJsonError> {
use geojson::{Geometry, Value};
let geom = Geometry::new(Value::Point(vec![self.x(), self.y(), self.z()]));
serde_json::to_string(&geom).map_err(|e| {
crate::geo::GeoJsonError::Serialization(format!("Failed to serialize 3D point: {}", e))
})
}
#[cfg(feature = "geojson")]
pub fn from_geojson(geojson: &str) -> Result<Self, crate::geo::GeoJsonError> {
use geojson::{Geometry, Value};
let geom: Geometry = serde_json::from_str(geojson).map_err(|e| {
crate::geo::GeoJsonError::Deserialization(format!("Failed to parse GeoJSON: {}", e))
})?;
match geom.value {
Value::Point(coords) => {
if coords.len() < 2 {
return Err(crate::geo::GeoJsonError::InvalidCoordinates(
"Point must have at least 2 coordinates".to_string(),
));
}
let z = coords.get(2).copied().unwrap_or(0.0);
if !(coords[0].is_finite() && coords[1].is_finite() && z.is_finite()) {
return Err(crate::geo::GeoJsonError::InvalidCoordinates(
"Point coordinates must be finite".to_string(),
));
}
Ok(Point3d::new(coords[0], coords[1], z))
}
_ => Err(crate::geo::GeoJsonError::InvalidGeometry(
"GeoJSON geometry is not a Point".to_string(),
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemporalPoint {
pub point: Point,
pub timestamp: SystemTime,
}
impl TemporalPoint {
pub fn new(point: Point, timestamp: SystemTime) -> Self {
Self { point, timestamp }
}
pub fn point(&self) -> &Point {
&self.point
}
pub fn timestamp(&self) -> &SystemTime {
&self.timestamp
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TemporalPoint3D {
pub point: Point,
pub altitude: f64,
pub timestamp: SystemTime,
}
impl TemporalPoint3D {
pub fn new(point: Point, altitude: f64, timestamp: SystemTime) -> Self {
Self {
point,
altitude,
timestamp,
}
}
pub fn point(&self) -> &Point {
&self.point
}
pub fn altitude(&self) -> f64 {
self.altitude
}
pub fn timestamp(&self) -> &SystemTime {
&self.timestamp
}
pub fn to_point_3d(&self) -> Point3d {
Point3d::from_point_and_altitude(self.point, self.altitude)
}
pub fn distance_to(&self, other: &TemporalPoint3D) -> f64 {
self.to_point_3d().haversine_3d(&other.to_point_3d())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_point3d_creation() {
let p = Point3d::new(-74.0, 40.7, 100.0);
assert_eq!(p.x(), -74.0);
assert_eq!(p.y(), 40.7);
assert_eq!(p.z(), 100.0);
assert_eq!(p.altitude(), 100.0);
}
#[test]
fn test_point3d_distance_3d() {
let p1 = Point3d::new(0.0, 0.0, 0.0);
let p2 = Point3d::new(3.0, 4.0, 12.0);
let distance = p1.distance_3d(&p2);
assert_eq!(distance, 13.0);
}
#[test]
fn test_point3d_altitude_difference() {
let p1 = Point3d::new(-74.0, 40.7, 50.0);
let p2 = Point3d::new(-74.0, 40.7, 150.0);
assert_eq!(p1.altitude_difference(&p2), 100.0);
}
#[test]
fn test_point3d_to_2d() {
let p = Point3d::new(-74.0, 40.7, 100.0);
let p2d = p.to_2d();
assert_eq!(p2d.x(), -74.0);
assert_eq!(p2d.y(), 40.7);
}
#[test]
fn test_haversine_3d() {
let p1 = Point3d::new(-74.0, 40.7, 0.0);
let p2 = Point3d::new(-74.0, 40.7, 100.0);
let distance = p1.haversine_3d(&p2);
assert!((distance - 100.0).abs() < 0.1);
}
#[test]
fn test_haversine_distances() {
let p1 = Point3d::new(-74.0060, 40.7128, 0.0);
let p2 = Point3d::new(-74.0070, 40.7138, 100.0);
let (h_dist, alt_diff, dist_3d) = p1.haversine_distances(&p2);
assert_eq!(alt_diff, 100.0);
assert!((dist_3d - (h_dist * h_dist + alt_diff * alt_diff).sqrt()).abs() < 0.1);
assert!((h_dist - p1.haversine_2d(&p2)).abs() < 0.1);
assert!((dist_3d - p1.haversine_3d(&p2)).abs() < 0.1);
}
#[test]
fn test_temporal_point3d_to_point3d() {
let temporal = TemporalPoint3D::new(Point::new(-74.0, 40.7), 100.0, SystemTime::now());
let p3d = temporal.to_point_3d();
assert_eq!(p3d.x(), -74.0);
assert_eq!(p3d.y(), 40.7);
assert_eq!(p3d.altitude(), 100.0);
}
#[cfg(feature = "geojson")]
#[test]
fn test_point3d_geojson_roundtrip() {
let original = Point3d::new(-74.0060, 40.7128, 100.0);
let json = original.to_geojson().unwrap();
let parsed = Point3d::from_geojson(&json).unwrap();
assert!((original.x() - parsed.x()).abs() < 1e-10);
assert!((original.y() - parsed.y()).abs() < 1e-10);
assert!((original.z() - parsed.z()).abs() < 1e-10);
}
#[cfg(feature = "geojson")]
#[test]
fn test_point3d_from_geojson_defaults_z() {
let json = r#"{"type":"Point","coordinates":[-74.006,40.7128]}"#;
let point = Point3d::from_geojson(json).unwrap();
assert_eq!(point.z(), 0.0);
}
}