use crate::{
surface::Surface, Angle, Cartesian3DVector, GeocentricPosition, GeodeticPosition, LatLong,
Length, Mat33, Vec3,
};
#[derive(PartialEq, Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] enum Orientation {
#[default]
Ned,
Enu,
}
#[derive(PartialEq, Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct LocalPosition {
x: Length,
y: Length,
z: Length,
o: Orientation,
}
impl LocalPosition {
pub const fn new(x: Length, y: Length, z: Length) -> Self {
Self::new_with_o(x, y, z, Orientation::Ned)
}
pub fn from_metres(x: f64, y: f64, z: f64) -> Self {
Self::new_with_o(
Length::from_metres(x),
Length::from_metres(y),
Length::from_metres(z),
Orientation::Ned,
)
}
const fn new_with_o(x: Length, y: Length, z: Length, o: Orientation) -> Self {
Self { x, y, z, o }
}
fn from_metres_with_o(v: Vec3, o: Orientation) -> Self {
Self::new_with_o(
Length::from_metres(v.x()),
Length::from_metres(v.y()),
Length::from_metres(v.z()),
o,
)
}
fn with_orientation(&self, o: Orientation) -> Self {
if self.o == o {
*self
} else {
LocalPosition {
x: self.y,
y: self.x,
z: -self.z,
o,
}
}
}
pub fn aer_to_ned(azimuth: Angle, elevation: Angle, slant_range: Length) -> Self {
let (north, east, z) = Self::aer_to_enz(azimuth, elevation, slant_range);
LocalPosition::new_with_o(north, east, -z, Orientation::Ned)
}
pub fn aer_to_enu(azimuth: Angle, elevation: Angle, slant_range: Length) -> Self {
let (north, east, z) = Self::aer_to_enz(azimuth, elevation, slant_range);
LocalPosition::new_with_o(east, north, z, Orientation::Enu)
}
fn aer_to_enz(
azimuth: Angle,
elevation: Angle,
slant_range: Length,
) -> (Length, Length, Length) {
let cose = elevation.as_radians().cos();
let east = azimuth.as_radians().sin() * cose * slant_range;
let north = azimuth.as_radians().cos() * cose * slant_range;
let z = elevation.as_radians().sin() * slant_range;
(north, east, z)
}
pub fn azimuth(&self) -> Angle {
let (e, n) = match self.o {
Orientation::Ned => (self.y(), self.x()),
Orientation::Enu => (self.x(), self.y()),
};
Angle::from_radians(e.as_metres().atan2(n.as_metres())).normalised()
}
pub fn elevation(&self) -> Angle {
let ev = Angle::from_radians((self.z() / self.slant_range()).asin());
match self.o {
Orientation::Ned => -ev,
Orientation::Enu => ev,
}
}
pub fn slant_range(&self) -> Length {
Length::from_metres(self.as_metres().norm())
}
}
impl Cartesian3DVector for LocalPosition {
#[inline]
fn x(&self) -> Length {
self.x
}
#[inline]
fn y(&self) -> Length {
self.y
}
#[inline]
fn z(&self) -> Length {
self.z
}
fn round<F>(&self, round: F) -> Self
where
F: Fn(Length) -> Length,
{
Self::new_with_o(round(self.x()), round(self.y()), round(self.z()), self.o)
}
}
#[derive(PartialEq, Clone, Copy, Debug, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct LocalFrame<S> {
origin: Vec3,
dir_rm: Mat33,
inv_rm: Mat33,
surface: S,
o: Orientation,
}
impl<S> LocalFrame<S>
where
S: Surface,
{
pub fn enu(origin: GeodeticPosition, surface: S) -> Self {
let vo = origin.horizontal_position().as_vec3();
let ru = vo;
let re = Vec3::UNIT_Z.orthogonal_to(vo);
let rn = ru.cross_prod(re);
let inv_rm = Mat33::new(re, rn, ru);
Self {
origin: surface.geodetic_to_geocentric_position(origin).as_metres(),
dir_rm: inv_rm.transpose(),
inv_rm,
surface,
o: Orientation::Enu,
}
}
pub fn ned(origin: GeodeticPosition, surface: S) -> Self {
let vo = origin.horizontal_position().as_vec3();
let rd = -1.0 * vo;
let re = Vec3::UNIT_Z.orthogonal_to(vo);
let rn = re.cross_prod(rd);
let inv_rm = Mat33::new(rn, re, rd);
Self {
origin: surface.geodetic_to_geocentric_position(origin).as_metres(),
dir_rm: inv_rm.transpose(),
inv_rm,
surface,
o: Orientation::Ned,
}
}
pub fn body(
yaw: Angle,
pitch: Angle,
roll: Angle,
origin: GeodeticPosition,
surface: S,
) -> Self {
let r_nb = zyx2r(yaw, pitch, roll);
let r_en = Self::ned(origin, surface).dir_rm;
let dir_rm = r_en * r_nb;
Self {
origin: surface.geodetic_to_geocentric_position(origin).as_metres(),
dir_rm,
inv_rm: dir_rm.transpose(),
surface,
o: Orientation::Ned,
}
}
pub fn local_level(wander_azimuth: Angle, origin: GeodeticPosition, surface: S) -> Self {
let ll = LatLong::from_nvector(origin.horizontal_position());
let r = xyz2r(ll.longitude(), -ll.latitude(), wander_azimuth);
let r_ee = Mat33::new(Vec3::NEG_UNIT_Z, Vec3::UNIT_Y, Vec3::UNIT_X);
let dir_rm = r_ee * r;
Self {
origin: surface.geodetic_to_geocentric_position(origin).as_metres(),
dir_rm,
inv_rm: dir_rm.transpose(),
surface,
o: Orientation::Ned,
}
}
pub fn geodetic_to_local_position(&self, p: GeodeticPosition) -> LocalPosition {
let p_geocentric = self.surface.geodetic_to_geocentric_position(p).as_metres();
let de = p_geocentric - self.origin;
let d = de * self.inv_rm;
LocalPosition::from_metres_with_o(d, self.o)
}
pub fn local_to_geodetic_position(&self, p: LocalPosition) -> GeodeticPosition {
let op = p.with_orientation(self.o);
let c = op.as_metres() * self.dir_rm;
let v = self.origin + c;
let p_geocentric = GeocentricPosition::from_vec3_metres(v);
self.surface.geocentric_to_geodetic_position(p_geocentric)
}
}
pub fn r2xyz(m: Mat33) -> (Angle, Angle, Angle) {
let r0 = m.row0();
let r1 = m.row1();
let r2 = m.row2();
let v00 = r0.x();
let v01 = r0.y();
let v12 = r1.z();
let v22 = r2.z();
let z = -v01.atan2(v00);
let x = -v12.atan2(v22);
let sy = r0.z();
let cy = ((v00 * v00 + v01 * v01 + v12 * v12 + v22 * v22) / 2.0).sqrt();
let y = sy.atan2(cy);
(
Angle::from_radians(x),
Angle::from_radians(y),
Angle::from_radians(z),
)
}
pub fn r2zyx(m: Mat33) -> (Angle, Angle, Angle) {
let (x, y, z) = r2xyz(m.transpose());
(-z, -y, -x)
}
pub fn zyx2r(z: Angle, y: Angle, x: Angle) -> Mat33 {
let cx = x.as_radians().cos();
let sx = x.as_radians().sin();
let cy = y.as_radians().cos();
let sy = y.as_radians().sin();
let cz = z.as_radians().cos();
let sz = z.as_radians().sin();
let r0 = Vec3::new(cz * cy, -sz * cx + cz * sy * sx, sz * sx + cz * sy * cx);
let r1 = Vec3::new(sz * cy, cz * cx + sz * sy * sx, -cz * sx + sz * sy * cx);
let r2 = Vec3::new(-sy, cy * sx, cy * cx);
Mat33::new(r0, r1, r2)
}
pub fn xyz2r(x: Angle, y: Angle, z: Angle) -> Mat33 {
let cx = x.as_radians().cos();
let sx = x.as_radians().sin();
let cy = y.as_radians().cos();
let sy = y.as_radians().sin();
let cz = z.as_radians().cos();
let sz = z.as_radians().sin();
let r0 = Vec3::new(cy * cz, -cy * sz, sy);
let r1 = Vec3::new(sy * sx * cz + cx * sz, -sy * sx * sz + cx * cz, -cy * sx);
let r2 = Vec3::new(-sy * cx * cz + sx * sz, sy * cx * sz + sx * cz, cy * cx);
Mat33::new(r0, r1, r2)
}
#[cfg(test)]
mod tests {
use crate::{
ellipsoidal::Ellipsoid, positions::assert_geod_eq_d7_mm, r2xyz, r2zyx, Angle,
Cartesian3DVector, GeodeticPosition, LatLong, Length, LocalFrame, LocalPosition, Mat33,
NVector, Vec3,
};
#[test]
fn local_position_from_metres() {
assert_eq!(
LocalPosition::new(
Length::from_metres(1.0),
Length::from_metres(2.0),
Length::from_metres(3.0)
),
LocalPosition::from_metres(1.0, 2.0, 3.0)
);
}
#[test]
fn geodetic_to_local_pos_w_in_moving_frame_east() {
let ship_position_0 =
GeodeticPosition::new(LatLong::from_degrees(1.0, 2.0).to_nvector(), Length::ZERO);
let ship_position_1 =
GeodeticPosition::new(LatLong::from_degrees(1.0, 2.005).to_nvector(), Length::ZERO);
let sensor_position = GeodeticPosition::new(
LatLong::from_degrees(1.000090437, 2.0025).to_nvector(),
Length::ZERO,
);
let f0: LocalFrame<Ellipsoid> =
LocalFrame::local_level(Angle::from_degrees(90.0), ship_position_0, Ellipsoid::WGS84);
let local_0 = f0.geodetic_to_local_position(sensor_position).round_mm();
assert_eq!(Length::from_metres(278.257), local_0.x());
assert_eq!(Length::from_metres(-10.0), local_0.y());
assert_eq!(Length::ZERO, local_0.z().round_m());
assert_eq!(358.0, local_0.azimuth().as_degrees().round());
let f1: LocalFrame<Ellipsoid> =
LocalFrame::local_level(Angle::from_degrees(90.0), ship_position_1, Ellipsoid::WGS84);
let local_1 = f1.geodetic_to_local_position(sensor_position).round_mm();
assert_eq!(Length::from_metres(-278.257), local_1.x());
assert_eq!(Length::from_metres(-10.0), local_1.y());
assert_eq!(Length::ZERO, local_1.z().round_m());
assert_eq!(182.0, local_1.azimuth().as_degrees().round());
}
#[test]
fn geodetic_to_local_pos_n_in_moving_frame_east() {
let ship_position_0 =
GeodeticPosition::new(LatLong::from_degrees(1.0, 2.0).to_nvector(), Length::ZERO);
let ship_position_1 =
GeodeticPosition::new(LatLong::from_degrees(1.0, 2.005).to_nvector(), Length::ZERO);
let sensor_position = GeodeticPosition::new(
LatLong::from_degrees(1.0, 2.0025).to_nvector(),
Length::ZERO,
);
let f0: LocalFrame<Ellipsoid> = LocalFrame::ned(ship_position_0, Ellipsoid::WGS84);
let local_0 = f0.geodetic_to_local_position(sensor_position).round_mm();
assert_eq!(Length::ZERO, local_0.x());
assert_eq!(Length::from_metres(278.257), local_0.y());
assert_eq!(Length::ZERO, local_0.z().round_m());
assert_eq!(90.0, local_0.azimuth().as_degrees());
let f1: LocalFrame<Ellipsoid> = LocalFrame::ned(ship_position_1, Ellipsoid::WGS84);
let local_1 = f1.geodetic_to_local_position(sensor_position).round_mm();
assert_eq!(Length::ZERO, local_1.x());
assert_eq!(Length::from_metres(-278.257), local_1.y());
assert_eq!(Length::ZERO, local_1.z().round_m());
assert_eq!(270.0, local_1.azimuth().as_degrees());
}
#[test]
fn geodetic_to_local_pos_ned() {
let origin = GeodeticPosition::new(
NVector::from_lat_long_degrees(44.532, -72.782),
Length::from_metres(1699.0),
);
let point = GeodeticPosition::new(
NVector::from_lat_long_degrees(44.544, -72.814),
Length::from_metres(1340.0),
);
let ned = LocalFrame::ned(origin, Ellipsoid::WGS84);
let local = ned.geodetic_to_local_position(point);
assert_eq!(Length::from_metres(1334.252), local.x().round_mm());
assert_eq!(Length::from_metres(-2543.564), local.y().round_mm());
assert_eq!(Length::from_metres(359.646), local.z().round_mm());
assert_eq!(Angle::from_degrees(297.6796990), local.azimuth().round_d7());
assert_eq!(
Angle::from_degrees(-7.1370359),
local.elevation().round_d7()
);
assert_eq!(
Length::from_metres(2894.701),
local.slant_range().round_mm()
);
}
#[test]
fn geodetic_to_local_pos_enu() {
let origin = GeodeticPosition::new(
NVector::from_lat_long_degrees(46.017, 7.750),
Length::from_metres(1673.0),
);
let point = GeodeticPosition::new(
NVector::from_lat_long_degrees(45.976, 7.658),
Length::from_metres(4531.0),
);
let enu: LocalFrame<Ellipsoid> = LocalFrame::enu(origin, Ellipsoid::WGS84);
let local = enu.geodetic_to_local_position(point);
assert_eq!(Length::from_metres(-7134.757), local.x().round_mm());
assert_eq!(Length::from_metres(-4556.322), local.y().round_mm());
assert_eq!(Length::from_metres(2852.39), local.z().round_mm());
assert_eq!(Angle::from_degrees(237.4373247), local.azimuth().round_d7());
assert_eq!(
Angle::from_degrees(18.6208639),
local.elevation().round_d7()
);
}
#[test]
fn aer_to_ned() {
let az: Angle = Angle::from_degrees(155.427);
let el = Angle::from_degrees(-23.161);
let sr = Length::from_metres(10.885);
let local = LocalPosition::aer_to_ned(az, el, sr);
assert_eq!(Length::from_metres(-9.101), local.x().round_mm());
assert_eq!(Length::from_metres(4.162), local.y().round_mm());
assert_eq!(Length::from_metres(4.281), local.z().round_mm());
assert_eq!(az, local.azimuth().round_d7());
assert_eq!(el, local.elevation().round_d7());
assert_eq!(sr, local.slant_range().round_mm());
}
#[test]
fn aer_to_enu() {
let az: Angle = Angle::from_degrees(34.1160);
let el = Angle::from_degrees(4.1931);
let sr = Length::from_metres(15.1070);
let local = LocalPosition::aer_to_enu(az, el, sr);
assert_eq!(Length::from_metres(8.45), local.x().round_mm());
assert_eq!(Length::from_metres(12.474), local.y().round_mm());
assert_eq!(Length::from_metres(1.105), local.z().round_mm());
assert_eq!(az, local.azimuth().round_d7());
assert_eq!(el, local.elevation().round_d7());
assert_eq!(sr, local.slant_range().round_mm());
}
#[test]
fn local_to_geodetic_pos_enu() {
let origin = GeodeticPosition::new(
NVector::from_lat_long_degrees(46.017, 7.750),
Length::from_metres(1673.0),
);
let point = GeodeticPosition::new(
NVector::from_lat_long_degrees(45.976, 7.658),
Length::from_metres(4531.0),
);
let enu: LocalFrame<Ellipsoid> = LocalFrame::enu(origin, Ellipsoid::WGS84);
let local_enu: LocalPosition = enu.geodetic_to_local_position(point);
let local_ned = LocalPosition::new(local_enu.y(), local_enu.x(), -local_enu.z());
assert_geod_eq_d7_mm(point, enu.local_to_geodetic_position(local_ned));
}
#[test]
fn transitiviy_enu() {
let point_a = GeodeticPosition::new(
NVector::from_lat_long_degrees(1.0, 2.0),
Length::from_metres(-3.0),
);
let point_b = GeodeticPosition::new(
NVector::from_lat_long_degrees(4.0, 5.0),
Length::from_metres(-6.0),
);
let enu = LocalFrame::enu(point_a, Ellipsoid::WGS84);
assert_geod_eq_d7_mm(
point_b,
enu.local_to_geodetic_position(enu.geodetic_to_local_position(point_b)),
)
}
#[test]
fn transitiviy_ned() {
let point_a = GeodeticPosition::new(
NVector::from_lat_long_degrees(1.0, 2.0),
Length::from_metres(-3.0),
);
let point_b = GeodeticPosition::new(
NVector::from_lat_long_degrees(4.0, 5.0),
Length::from_metres(-6.0),
);
let ned = LocalFrame::ned(point_a, Ellipsoid::WGS84);
assert_geod_eq_d7_mm(
point_b,
ned.local_to_geodetic_position(ned.geodetic_to_local_position(point_b)),
)
}
#[test]
fn transitiviy_body() {
let point_a = GeodeticPosition::new(
NVector::from_lat_long_degrees(1.0, 2.0),
Length::from_metres(-3.0),
);
let point_b = GeodeticPosition::new(
NVector::from_lat_long_degrees(4.0, 5.0),
Length::from_metres(-6.0),
);
let body = LocalFrame::body(
Angle::from_degrees(45.0),
Angle::from_degrees(10.0),
Angle::from_degrees(5.0),
point_a,
Ellipsoid::WGS84,
);
assert_geod_eq_d7_mm(
point_b,
body.local_to_geodetic_position(body.geodetic_to_local_position(point_b)),
)
}
#[test]
fn transitiviy_local_level() {
let point_a = GeodeticPosition::new(
NVector::from_lat_long_degrees(1.0, 2.0),
Length::from_metres(-3.0),
);
let point_b = GeodeticPosition::new(
NVector::from_lat_long_degrees(4.0, 5.0),
Length::from_metres(-6.0),
);
let local_level =
LocalFrame::local_level(Angle::from_degrees(45.0), point_a, Ellipsoid::WGS84);
assert_geod_eq_d7_mm(
point_b,
local_level.local_to_geodetic_position(local_level.geodetic_to_local_position(point_b)),
)
}
#[test]
fn test_r2xyz() {
let m = Mat33::new(
Vec3::new(
0.7044160264027587,
-6.162841671621935e-2,
0.7071067811865475,
),
Vec3::new(0.559725765762092, 0.6608381550289296, -0.5),
Vec3::new(0.43646893232965345, 0.7479938977765876, 0.5),
);
let (x, y, z) = r2xyz(m);
assert_eq!(Angle::from_degrees(45.0), x.round_d7());
assert_eq!(Angle::from_degrees(45.0), y.round_d7());
assert_eq!(Angle::from_degrees(5.0), z.round_d7());
}
#[test]
fn test_r2zyx() {
let m = Mat33::new(
Vec3::new(
0.9254165783983234,
1.802831123629725e-2,
0.37852230636979245,
),
Vec3::new(
0.16317591116653482,
0.8825641192593856,
-0.44096961052988237,
),
Vec3::new(-0.3420201433256687, 0.46984631039295416, 0.8137976813493738),
);
let (z, y, x) = r2zyx(m);
assert_eq!(Angle::from_degrees(10.0), z.round_d7());
assert_eq!(Angle::from_degrees(20.0), y.round_d7());
assert_eq!(Angle::from_degrees(30.0), x.round_d7());
}
}