#![forbid(unsafe_code)]
use bytes::{BufMut, Bytes, BytesMut};
use oximedia_core::{OxiError, OxiResult};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GpsCoordinate {
pub latitude: f64,
pub longitude: f64,
pub altitude: f64,
}
impl GpsCoordinate {
pub fn new(latitude: f64, longitude: f64, altitude: f64) -> OxiResult<Self> {
if !(-90.0..=90.0).contains(&latitude) {
return Err(OxiError::InvalidData(
"Latitude must be between -90 and 90".into(),
));
}
if !(-180.0..=180.0).contains(&longitude) {
return Err(OxiError::InvalidData(
"Longitude must be between -180 and 180".into(),
));
}
Ok(Self {
latitude,
longitude,
altitude,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct GpsDataPoint {
pub coordinate: GpsCoordinate,
pub speed: f64,
pub heading: f64,
pub horizontal_accuracy: f64,
pub vertical_accuracy: f64,
pub satellites: u8,
}
impl GpsDataPoint {
#[must_use]
pub const fn new(coordinate: GpsCoordinate) -> Self {
Self {
coordinate,
speed: 0.0,
heading: 0.0,
horizontal_accuracy: 0.0,
vertical_accuracy: 0.0,
satellites: 0,
}
}
#[must_use]
pub const fn with_speed(mut self, speed: f64) -> Self {
self.speed = speed;
self
}
#[must_use]
pub const fn with_heading(mut self, heading: f64) -> Self {
self.heading = heading;
self
}
#[must_use]
pub const fn with_accuracy(mut self, horizontal: f64, vertical: f64) -> Self {
self.horizontal_accuracy = horizontal;
self.vertical_accuracy = vertical;
self
}
#[must_use]
pub const fn with_satellites(mut self, satellites: u8) -> Self {
self.satellites = satellites;
self
}
#[must_use]
pub fn to_bytes(&self) -> Bytes {
let mut buf = BytesMut::with_capacity(64);
buf.put_f64(self.coordinate.latitude);
buf.put_f64(self.coordinate.longitude);
buf.put_f64(self.coordinate.altitude);
buf.put_f64(self.speed);
buf.put_f64(self.heading);
buf.put_f64(self.horizontal_accuracy);
buf.put_f64(self.vertical_accuracy);
buf.put_u8(self.satellites);
buf.freeze()
}
pub fn from_bytes(data: &[u8]) -> OxiResult<Self> {
if data.len() < 57 {
return Err(OxiError::InvalidData("GPS data too short".into()));
}
let conv = |s: &[u8]| -> OxiResult<[u8; 8]> {
s.try_into()
.map_err(|_| OxiError::InvalidData("GPS slice conversion failed".into()))
};
let latitude = f64::from_be_bytes(conv(&data[0..8])?);
let longitude = f64::from_be_bytes(conv(&data[8..16])?);
let altitude = f64::from_be_bytes(conv(&data[16..24])?);
let speed = f64::from_be_bytes(conv(&data[24..32])?);
let heading = f64::from_be_bytes(conv(&data[32..40])?);
let horizontal_accuracy = f64::from_be_bytes(conv(&data[40..48])?);
let vertical_accuracy = f64::from_be_bytes(conv(&data[48..56])?);
let satellites = data[56];
let coordinate = GpsCoordinate::new(latitude, longitude, altitude)?;
Ok(Self {
coordinate,
speed,
heading,
horizontal_accuracy,
vertical_accuracy,
satellites,
})
}
}
#[derive(Debug, Clone)]
pub struct GpsTrack {
points: Vec<(i64, GpsDataPoint)>, }
impl GpsTrack {
#[must_use]
pub fn new() -> Self {
Self { points: Vec::new() }
}
pub fn add_point(&mut self, timestamp: i64, point: GpsDataPoint) {
self.points.push((timestamp, point));
}
#[must_use]
pub fn points(&self) -> &[(i64, GpsDataPoint)] {
&self.points
}
#[must_use]
pub fn get_point_at(&self, timestamp: i64) -> Option<&GpsDataPoint> {
self.points
.iter()
.rev()
.find(|(ts, _)| *ts <= timestamp)
.map(|(_, point)| point)
}
#[must_use]
pub fn len(&self) -> usize {
self.points.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
#[must_use]
pub fn total_distance(&self) -> f64 {
let mut distance = 0.0;
for window in self.points.windows(2) {
let (_, p1) = &window[0];
let (_, p2) = &window[1];
distance += haversine_distance(
p1.coordinate.latitude,
p1.coordinate.longitude,
p2.coordinate.latitude,
p2.coordinate.longitude,
);
}
distance
}
}
impl Default for GpsTrack {
fn default() -> Self {
Self::new()
}
}
#[must_use]
#[allow(clippy::suboptimal_flops)]
fn haversine_distance(lat1: f64, lon1: f64, lat2: f64, lon2: f64) -> f64 {
const EARTH_RADIUS_M: f64 = 6_371_000.0;
let lat1_rad = lat1.to_radians();
let lat2_rad = lat2.to_radians();
let delta_lat = (lat2 - lat1).to_radians();
let delta_lon = (lon2 - lon1).to_radians();
let a = (delta_lat / 2.0).sin().powi(2)
+ lat1_rad.cos() * lat2_rad.cos() * (delta_lon / 2.0).sin().powi(2);
let c = 2.0 * a.sqrt().atan2((1.0 - a).sqrt());
EARTH_RADIUS_M * c
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gps_coordinate() {
let coord = GpsCoordinate::new(37.7749, -122.4194, 10.0).expect("operation should succeed");
assert_eq!(coord.latitude, 37.7749);
assert_eq!(coord.longitude, -122.4194);
assert_eq!(coord.altitude, 10.0);
assert!(GpsCoordinate::new(100.0, 0.0, 0.0).is_err());
assert!(GpsCoordinate::new(0.0, 200.0, 0.0).is_err());
}
#[test]
fn test_gps_data_point() {
let coord = GpsCoordinate::new(37.7749, -122.4194, 10.0).expect("operation should succeed");
let point = GpsDataPoint::new(coord)
.with_speed(5.0)
.with_heading(90.0)
.with_accuracy(10.0, 5.0)
.with_satellites(8);
assert_eq!(point.speed, 5.0);
assert_eq!(point.heading, 90.0);
assert_eq!(point.satellites, 8);
}
#[test]
fn test_gps_serialization() {
let coord = GpsCoordinate::new(37.7749, -122.4194, 10.0).expect("operation should succeed");
let point = GpsDataPoint::new(coord).with_speed(5.0);
let bytes = point.to_bytes();
let decoded = GpsDataPoint::from_bytes(&bytes).expect("operation should succeed");
assert!((decoded.coordinate.latitude - 37.7749).abs() < 0.0001);
assert!((decoded.speed - 5.0).abs() < 0.0001);
}
#[test]
fn test_gps_track() {
let mut track = GpsTrack::new();
let coord1 =
GpsCoordinate::new(37.7749, -122.4194, 10.0).expect("operation should succeed");
let point1 = GpsDataPoint::new(coord1);
track.add_point(0, point1);
let coord2 =
GpsCoordinate::new(37.7750, -122.4195, 11.0).expect("operation should succeed");
let point2 = GpsDataPoint::new(coord2);
track.add_point(1000, point2);
assert_eq!(track.len(), 2);
assert!(!track.is_empty());
let found = track.get_point_at(500);
assert!(found.is_some());
let distance = track.total_distance();
assert!(distance > 0.0);
}
#[test]
fn test_haversine_distance() {
let distance = haversine_distance(37.7749, -122.4194, 34.0522, -118.2437);
assert!(distance > 500_000.0 && distance < 600_000.0);
}
}