Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
# MSRV
- os: ubuntu-latest
binary_target: x86_64-unknown-linux-gnu
toolchain: 1.85.0
toolchain: 1.87.0
steps:
- uses: actions/checkout@v5
with:
Expand Down
6 changes: 2 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
[workspace]
members = [
"swiftnav",
# "swiftnav-sys"
]
resolver = "3"
members = ["swiftnav"]
8 changes: 8 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
max_width = 100
wrap_comments = true
comment_width = 100
format_strings = true
edition = "2024"
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
style_edition = "2024"
20 changes: 10 additions & 10 deletions swiftnav/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@
name = "swiftnav"
version = "0.10.0"
authors = ["Swift Navigation <dev@swiftnav.com>"]
edition = "2018"
edition = "2024"
description = "GNSS positioning and related utilities"
readme = "README.md"
repository = "https://github.com/swift-nav/swiftnav-rs"
license = "LGPL-3.0"
rust-version = "1.85.0"
rust-version = "1.87.0"

[dependencies]
rustversion = "1.0"
chrono = { version = "0.4", optional = true }
strum = { version = "0.27", features = ["derive"] }
nalgebra = "0.33"
thiserror = "2.0"
bon = "3.8.1"
rustversion = "1.0.22"
chrono = { version = "0.4.42" }
strum = { version = "0.27.2", features = ["derive"] }
nalgebra = "0.34.1"
thiserror = "2.0.17"

[dev-dependencies]
float_eq = "1.0.1"
proptest = "1.5"
proptest = "1.9.0"

# This tells docs.rs to include the katex header for math formatting
# To do this locally
[package.metadata.docs.rs]
rustdoc-args = [ "--html-in-header", "katex-header.html" ]

rustdoc-args = ["--html-in-header", "katex-header.html"]
3 changes: 2 additions & 1 deletion swiftnav/src/coords/ecef.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use nalgebra::Vector3;
use std::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign};

use nalgebra::Vector3;

use crate::{
coords::{AzimuthElevation, Ellipsoid, LLHDegrees, LLHRadians, NED, WGS84},
math,
Expand Down
43 changes: 43 additions & 0 deletions swiftnav/src/coords/hemisphere.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use core::fmt;

pub enum LatitudinalHemisphere {
North,
South,
}

impl fmt::Display for LatitudinalHemisphere {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LatitudinalHemisphere::North => write!(f, "N"),
LatitudinalHemisphere::South => write!(f, "S"),
}
}
}

pub enum LongitudinalHemisphere {
East,
West,
}

impl fmt::Display for LongitudinalHemisphere {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LongitudinalHemisphere::East => write!(f, "E"),
LongitudinalHemisphere::West => write!(f, "W"),
}
}
}

pub enum Hemisphere {
Latitudinal(LatitudinalHemisphere),
Longitudinal(LongitudinalHemisphere),
}

impl fmt::Display for Hemisphere {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Hemisphere::Latitudinal(hemisphere) => write!(f, "{hemisphere}"),
Hemisphere::Longitudinal(hemisphere) => write!(f, "{hemisphere}"),
}
}
}
62 changes: 59 additions & 3 deletions swiftnav/src/coords/llh.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use super::{Ellipsoid, ECEF, WGS84};
use nalgebra::Vector3;

use super::{ECEF, Ellipsoid, WGS84};
use crate::coords::{LatitudinalHemisphere, LongitudinalHemisphere};

/// WGS84 geodetic coordinates (Latitude, Longitude, Height), with angles in degrees.
///
/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above the ellipsoid in meters
/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above
/// the ellipsoid in meters
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)]
pub struct LLHDegrees(Vector3<f64>);

Expand Down Expand Up @@ -44,12 +47,64 @@ impl LLHDegrees {
self.0.x
}

/// Get the latitude in degrees and decimal minutes
#[must_use]
pub fn latitude_degree_decimal_minutes(&self) -> (u16, f64) {
let lat = self.latitude();

#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "We are using trunc() and abs() already to remove the fractional part"
)]
let degrees = lat.trunc().abs() as u16;
let minutes = lat.fract().abs() * 60.0;

(degrees, minutes)
}

/// Get the latitudinal hemisphere
#[must_use]
pub fn latitudinal_hemisphere(&self) -> LatitudinalHemisphere {
if self.latitude() >= 0.0 {
LatitudinalHemisphere::North
} else {
LatitudinalHemisphere::South
}
}

/// Get the longitude component
#[must_use]
pub fn longitude(&self) -> f64 {
self.0.y
}

/// Get the latitude in degrees and decimal minutes
#[must_use]
pub fn longitude_degree_decimal_minutes(&self) -> (u16, f64) {
let lon = self.longitude();

#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "We are using trunc() and abs() already to remove the fractional part"
)]
let degrees = lon.trunc().abs() as u16;
let minutes = lon.fract().abs() * 60.0;

(degrees, minutes)
}

/// Get the longitudinal hemisphere
#[must_use]
pub fn longitudinal_hemisphere(&self) -> LongitudinalHemisphere {
if self.longitude() >= 0.0 {
LongitudinalHemisphere::East
} else {
LongitudinalHemisphere::West
}
}

/// Get the height component
#[must_use]
pub fn height(&self) -> f64 {
Expand Down Expand Up @@ -135,7 +190,8 @@ impl AsMut<Vector3<f64>> for LLHDegrees {

/// WGS84 geodetic coordinates (Latitude, Longitude, Height), with angles in radians.
///
/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above the ellipsoid in meters
/// Internally stored as an array of 3 [f64](std::f64) values: latitude, longitude, and height above
/// the ellipsoid in meters
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)]
pub struct LLHRadians(Vector3<f64>);

Expand Down
31 changes: 17 additions & 14 deletions swiftnav/src/coords/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,10 @@
//! the more common algortihms based on the Newton-Raphson method.
//!
//! ## References
//! * "A comparison of methods used in rectangular to Geodetic Coordinates
//! Transformations", Burtch R. R. (2006), American Congress for Surveying
//! and Mapping Annual Conference. Orlando, Florida.
//! * "Transformation from Cartesian to Geodetic Coordinates Accelerated by
//! Halley’s Method", T. Fukushima (2006), Journal of Geodesy.
//! * "A comparison of methods used in rectangular to Geodetic Coordinates Transformations", Burtch
//! R. R. (2006), American Congress for Surveying and Mapping Annual Conference. Orlando, Florida.
//! * "Transformation from Cartesian to Geodetic Coordinates Accelerated by Halley’s Method", T.
//! Fukushima (2006), Journal of Geodesy.
//!
//! # Examples
//!
Expand Down Expand Up @@ -76,22 +75,27 @@

mod ecef;
mod ellipsoid;
mod hemisphere;
mod llh;
mod ned;

pub use ecef::*;
pub use ellipsoid::*;
pub use hemisphere::*;
pub use llh::*;
use nalgebra::Vector2;
pub use ned::*;

use crate::{reference_frame::ReferenceFrame, time::GpsTime};
use nalgebra::Vector2;

/// WGS84 local horizontal coordinates consisting of an Azimuth and Elevation, with angles stored as radians
/// WGS84 local horizontal coordinates consisting of an Azimuth and Elevation, with angles stored as
/// radians
///
/// Azimuth can range from $0$ to $2\pi$. North has an azimuth of $0$, east has an azimuth of $\frac{\pi}{2}$
/// Azimuth can range from $0$ to $2\pi$. North has an azimuth of $0$, east has an azimuth of
/// $\frac{\pi}{2}$
///
/// Elevation can range from $-\frac{\pi}{2}$ to $\frac{\pi}{2}$. Up has an elevation of $\frac{\pi}{2}$, down an elevation of $-\frac{\pi}{2}$
/// Elevation can range from $-\frac{\pi}{2}$ to $\frac{\pi}{2}$. Up has an elevation of
/// $\frac{\pi}{2}$, down an elevation of $-\frac{\pi}{2}$
#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Default)]
pub struct AzimuthElevation(Vector2<f64>);

Expand Down Expand Up @@ -290,11 +294,10 @@ impl Coordinate {
#[cfg(test)]
mod tests {
use float_eq::assert_float_eq;

use crate::time::UtcTime;
use proptest::prelude::*;

use super::*;
use proptest::prelude::*;
use crate::time::UtcTime;

/* Maximum allowable error in quantities with units of length (in meters). */
const MAX_DIST_ERROR_M: f64 = 1e-6;
Expand Down Expand Up @@ -535,11 +538,11 @@ mod tests {

let lat_err = llh_input.latitude() - llh_output.latitude();
assert!(lat_err.abs() < MAX_ANGLE_ERROR_RAD,
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {:?}, ECEF: {:?}, Final: {:?}, Lat error (rad): {}", llh_input, ecef, llh_output, lat_err);
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {llh_input:?}, ECEF: {ecef:?}, Final: {llh_output:?}, Lat error (rad): {lat_err}");

let lon_err = llh_input.longitude() - llh_output.longitude();
assert!(lon_err.abs() < MAX_ANGLE_ERROR_RAD,
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {:?}, ECEF: {:?}, Final: {:?}, Lon error (rad): {}", llh_input, ecef, llh_output, lon_err);
"Converting random WGS84 LLH to ECEF and back again does not return the original values. Initial: {llh_input:?}, ECEF: {ecef:?}, Final: {llh_output:?}, Lon error (rad): {lon_err}");

let hgt_err = llh_input.height() - llh_output.height();
assert!(hgt_err.abs() < MAX_DIST_ERROR_M,
Expand Down
12 changes: 5 additions & 7 deletions swiftnav/src/edc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -326,9 +326,10 @@ pub fn compute_crc24q(buf: &[u8], initial_value: u32) -> u32 {

#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;

use super::*;

const TEST_DATA: &[u8] = "123456789".as_bytes();

/// Helper function to append a CRC-24Q value as 3 bytes (big-endian) to a buffer
Expand All @@ -353,24 +354,21 @@ mod tests {
let crc = compute_crc24q(&TEST_DATA[0..0], 0);
assert!(
crc == 0,
"CRC of empty buffer with starting value 0 should be 0, not {}",
crc
"CRC of empty buffer with starting value 0 should be 0, not {crc}",
);

let crc = compute_crc24q(&TEST_DATA[0..0], 22);
assert!(
crc == 22,
"CRC of empty buffer with starting value 22 should be 22, not {}",
crc
"CRC of empty buffer with starting value 22 should be 22, not {crc}",
);

/* Test value taken from python crcmod package tests, see:
* http://crcmod.sourceforge.net/crcmod.predefined.html */
let crc = compute_crc24q(TEST_DATA, 0x00B7_04CE);
assert!(
crc == 0x0021_CF02,
"CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {}",
crc
"CRC of \"123456789\" with init value 0xB704CE should be 0x21CF02, not {crc}",
);
}

Expand Down
1 change: 1 addition & 0 deletions swiftnav/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
pub mod coords;
pub mod edc;
mod math;
pub mod nmea;
pub mod reference_frame;
pub mod signal;
pub mod time;
15 changes: 7 additions & 8 deletions swiftnav/src/math.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,7 @@

/// We define a `const` max function since [`std::cmp::max`] isn't `const`
pub(crate) const fn compile_time_max_u16(a: u16, b: u16) -> u16 {
if b < a {
a
} else {
b
}
if b < a { a } else { b }
}

/// Computes the square root of a given number at compile time using the Newton-Raphson method.
Expand All @@ -35,7 +31,8 @@ pub(crate) const fn compile_time_max_u16(a: u16, b: u16) -> u16 {
/// # Notes
///
/// - This function is marked as `const`, allowing it to be evaluated at compile time.
/// - The algorithm iteratively refines the approximation of the square root until the result stabilizes.
/// - The algorithm iteratively refines the approximation of the square root until the result
/// stabilizes.
#[expect(clippy::many_single_char_names, reason = "It's math, whatyagonnado?")]
pub(crate) const fn compile_time_sqrt(s: f64) -> f64 {
assert!(
Expand All @@ -57,7 +54,8 @@ pub(crate) const fn compile_time_sqrt(s: f64) -> f64 {
x
}

/// Calculate the rotation matrix for rotating between an [`crate::coords::ECEF`] and [`crate::coords::NED`] frames
/// Calculate the rotation matrix for rotating between an [`crate::coords::ECEF`] and
/// [`crate::coords::NED`] frames
#[must_use]
pub(crate) fn ecef2ned_matrix(llh: crate::coords::LLHRadians) -> nalgebra::Matrix3<f64> {
let sin_lat = llh.latitude().sin();
Expand All @@ -80,10 +78,11 @@ pub(crate) fn ecef2ned_matrix(llh: crate::coords::LLHRadians) -> nalgebra::Matri

#[cfg(test)]
mod tests {
use super::*;
use float_eq::assert_float_eq;
use proptest::prelude::*;

use super::*;

proptest! {
#![proptest_config(ProptestConfig::with_cases(1000))]

Expand Down
Loading
Loading