//! Geo Algorithms //! //! Since the `geo` crate is not `no_std`, we need to implement the most important algorithms on our own use alloc::string::{String, ToString}; use core::ops::{Mul, Sub}; use log::warn; #[cfg(not(feature = "std"))] use num_traits::float::Float; const EARTH_RADIUS: f64 = 6_371_008.8; // mean radius of GRS80 ellipsoid const EARTH_CIRCUMFERENCE: f64 = 40_030_228.884; // calculated from `EARTH_RADIUS * 2 * PI` /// Haversine distance between two geo coordinates (in meters) #[allow(unused)] pub fn haversine_dist(a: &geo_types::Point, b: &geo_types::Point) -> f64 { let theta1 = a.y().to_radians(); let theta2 = b.y().to_radians(); let delta_theta = (b.y() - a.y()).to_radians(); let delta_lambda = (b.x() - a.x()).to_radians(); let a = (delta_theta / 2.).sin().powi(2) + theta1.cos() * theta2.cos() * (delta_lambda / 2.).sin().powi(2); let c = 2. * a.sqrt().asin(); EARTH_RADIUS * c } /// Haversine bearing from point `a` to point `b` #[allow(unused)] pub fn haversine_bearing(a: &geo_types::Point, b: &geo_types::Point) -> f32 { let (lng_a, lat_a) = (a.x().to_radians(), a.y().to_radians()); let (lng_b, lat_b) = (b.x().to_radians(), b.y().to_radians()); let delta_lng = lng_b - lng_a; #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] let s = (lat_b.cos() * delta_lng.sin()) as f32; #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] let c = (lat_a.cos() * lat_b.sin() - lat_a.sin() * lat_b.cos() * delta_lng.cos()) as f32; let degrees = f32::atan2(s, c).to_degrees(); (degrees + 360.0) % 360.0 } /// Cartesian distance between to X/Y coordinates (in unit of the coordinates) #[allow(unused)] pub fn cartesian_dist(a: geo_types::Point, b: geo_types::Point) -> f64 { let vector = b.sub(a); cartesian_norm(vector) } /// Cartesian bearing from X/Y point `a` to X/Y point `b` #[allow(unused)] pub fn cartesian_bearing(a: geo_types::Point, b: geo_types::Point) -> f64 { let vector = b.sub(a); cartesian_theta(vector) } /// Cartesian vector norm (length) #[allow(unused)] pub fn cartesian_norm(vec: geo_types::Point) -> f64 { f64::sqrt(vec.x() * vec.x() + vec.y() * vec.y()) } /// Cartesian vector direction (polar coordinate angle) #[allow(unused)] pub fn cartesian_theta(vec: geo_types::Point) -> f64 { let theta_rad = f64::atan2(vec.y(), vec.x()); theta_rad * 180. / core::f64::consts::PI } /// Normalizes an angle (in degrees) to (-180, 180] interval #[allow(unused)] pub fn deg_180(angle_deg: f32) -> f32 { let full_circle_deg = 360.0; let angle = angle_deg % full_circle_deg; if angle > full_circle_deg / 2. { return angle - full_circle_deg; } if angle <= -full_circle_deg / 2. { return angle + full_circle_deg; } angle } /// Normalizes an angle (in degrees) to [0, 360) interval #[allow(unused)] pub fn deg_360(angle_deg: f32) -> f32 { let full_circle_deg = 360.0; let angle = angle_deg % full_circle_deg; if (angle < 0.) { return angle + full_circle_deg; } angle } /// Converts a geo-position (lon/lat) to cartesian coordinates relative to a reference positions /// /// Both `ref_pos` and `position` are expected to be lon/lat in degrees. /// The output will be in a local X/Y coordinate system with X pointing north and Y pointing east. #[allow(unused)] pub fn lonlat_to_dcart( ref_pos: &geo_types::Point, position: &geo_types::Point, ) -> geo_types::Point { let pos_diff = position.sub(*ref_pos); let dlat_deg = pos_diff.y(); let dlon_deg = pos_diff.x(); // latitude (north/south) degree per meter is independent from longitude let dx = dlat_deg / 360. * EARTH_CIRCUMFERENCE; // longitude (east/west) degree per meter has a different value depending on the latitude #[allow(clippy::cast_possible_truncation)] let ref_lat_rad = ref_pos.to_radians().y(); let dy = dlon_deg / 360. * (EARTH_CIRCUMFERENCE * ref_lat_rad.cos()); geo_types::Point::new(dx, dy) } /// Converts a X/Y coordinate (NED relative to a reference position) to a geo-position /// /// The `ref_pos` is expected to be lon/lat in degrees and the output will be the same. /// The `position` is expected to be in a local X/Y coordinate system with X pointing north and Y pointing east. #[allow(unused)] pub fn dcart_to_lonlat( ref_pos: &geo_types::Point, position: &geo_types::Point, ) -> geo_types::Point { // note that we're using NED while c-its-parser uses ITS-G5 style ENU X/Y coordinates // so we need to swap X and Y coordinates #[allow(clippy::cast_possible_truncation)] let dx = position.y() as f32; #[allow(clippy::cast_possible_truncation)] let dy = position.x() as f32; c_its_parser::geo_utils::point_from_dxy(dx, dy, ref_pos) } /// Describes how a position is related to a line segment #[derive(Debug, Default)] #[allow(unused)] pub struct LinePosition { /// Own position relative to the line segment pub semantic: LinePositionType, /// Distance to center line (always positive) pub parallel_dist: f64, /// Distance to first node (negative if `BeforeStart`) pub perpendicular_dist: f64, } #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord)] #[allow(unused)] pub enum LinePositionType { #[default] Undefined, BeforeStart, InBetween, AfterEnd, } #[allow(unused)] impl LinePosition { pub fn new(parallel_dist: f64, perpendicular_dist: f64, after_end: bool) -> Self { let semantic = if after_end { LinePositionType::AfterEnd } else if parallel_dist < 0. { LinePositionType::BeforeStart } else { LinePositionType::InBetween }; Self { semantic, parallel_dist, perpendicular_dist, } } /// Determines `point` position relative to a line segment (in X/Y coordinates) /// /// The `ls` shall consist of 2 points! /// /// # Algorithm /// The line segment can be converted to a vector which describes an infinite line. /// So we can calculate the (perpendicular) projection of the point on that line: /// ```text /// [Case A] (point) | [Case B] (point) /// | | | /// | | | /// (node1)---(node2)-----(q) | (node1)------(q)------(node2) /// ``` /// /// This will already give us the perpendicular and parallel distance. /// Since we came from a finite line segment and want to know if the point is bejond the segment, /// we also need to compare the parallel distance with the length of the vector. /// So both case are the same mathematically. /// /// # Errors /// Fails if points are the same pub fn for_ls(point: geo_types::Point, line: &geo_types::Line) -> Result { let node1 = line.start_point(); let node2 = line.end_point(); let line_vec = node2.sub(node1); let line_vec_length = cartesian_norm(line_vec); // input validation if line_vec_length < f64::EPSILON { return Err("Points are the same".to_string()); } // calculate projection of `p` onto the line // ```math // # n needs to be a unit vector // n = (node2 - node1) / |node2 - node1| // # vector from node1 to the point // p = point - node1 // # projection is the unit-vector multipled by the dot-product of p and n // q = (p {dot} n) * n // # perpendicular length is calculated from the vector q->p // QP = q - p // ``` let p = point.sub(node1); let n = line_vec.mul(1. / line_vec_length); let q = n.mul(p.dot(n)); let qp_length = cartesian_norm(q.sub(p)); // calculate parallel distance of `q` to `node1`. // This can be achieved by the dot-product since `n` is a unit vector and // the dot-product of two paralle vectors is the same as the multiplication of the vector norms. let q_dist = n.dot(q); // generate output let after_end = q_dist > line_vec_length; let line_pos = Self::new(q_dist, qp_length, after_end); Ok(line_pos) } /// Determines `point` position relative to nodes of an ingress `Lane` /// /// Keep in mind, that the lane's node list starts at the stop line and is extending away from the intersection. /// But we are driving the other way around! /// /// # Errors /// Returns a human-readable string if position can't be determined pub fn for_ingress_lane( point: geo_types::Point, nodes: &geo_types::LineString, ) -> Result { let mut result = Self::default(); let mut lines = nodes.lines(); // check, that the stop line is not behind us let line = lines .next() .ok_or("Lane has less than two nodes".to_string())?; let pos = Self::for_ls(point, &line)?; if pos.semantic == LinePositionType::BeforeStart { // we have crossed the stop line -> abort search for position inside the lane return Ok(pos); } if pos.semantic == LinePositionType::InBetween { // we're at this segment -> search finished return Ok(pos); } // otherwise: continue with next segment (and store this segment's length) let mut lane_length = cartesian_norm(line.delta().into()); let mut last_pos = LinePosition::default(); // continue with remaining lines for line in lines { let line_len = cartesian_norm(line.delta().into()); // ignored line, if points are too close <10cm if line_len < 0.1 { warn!("Lane nodes are just {line_len:.1} m apart -> ignoring"); continue; } let pos = Self::for_ls(point, &line)?; match pos.semantic { LinePositionType::BeforeStart => { // we're at the outer arc of the lane right at the border between line segments // -> set parallel distance to zero and finish search return Ok(LinePosition::new( lane_length, pos.perpendicular_dist, false, )); } LinePositionType::InBetween => { // we're at this segment // -> add segment's distance to accumulated lane length and finish search return Ok(LinePosition::new( lane_length + pos.parallel_dist, pos.perpendicular_dist, false, )); } _ => { // continue searching, add segment's distance to lane length accumulator lane_length += line_len; last_pos = pos; } } } // if we came this far, we're after the lane's end (actually the start in driving direction) // -> add parallel distance to accumulated lane length Ok(LinePosition::new( lane_length + last_pos.parallel_dist, last_pos.perpendicular_dist, true, )) } } #[cfg(test)] mod tests { use crate::geo_alg::{LinePosition, cartesian_bearing}; #[test] fn test_cart_bearing() { let start = geo_types::Point::new(0., 0.); assert_float_eq::assert_float_absolute_eq!( 0., cartesian_bearing(start, geo_types::Point::new(10., 0.)) ); assert_float_eq::assert_float_absolute_eq!( 90., cartesian_bearing(start, geo_types::Point::new(0., 10.)) ); assert_float_eq::assert_float_absolute_eq!( 45., cartesian_bearing(start, geo_types::Point::new(10., 10.)) ); assert_float_eq::assert_float_absolute_eq!( 135., cartesian_bearing(start, geo_types::Point::new(-10., 10.)) ); assert_float_eq::assert_float_absolute_eq!( -135., cartesian_bearing(start, geo_types::Point::new(-10., -10.)) ); } #[test] fn linepos_for_ls_validation() { // identical nodes shall result in an error let point = geo_types::Point::new(0., 0.); let node1 = geo_types::Point::new(0., 0.); let node2 = geo_types::Point::new(10., -5.); let ls = geo_types::Line::new(node1, node1); assert!(LinePosition::for_ls(point, &ls).is_err()); let ls = geo_types::Line::new(node2, node2); assert!(LinePosition::for_ls(point, &ls).is_err()); } #[test] fn linepos_for_ls_test() { use crate::geo_alg::LinePositionType; // test on X axis { let node1 = geo_types::Point::new(0., 0.); let node2 = geo_types::Point::new(5., 0.); let ls = geo_types::Line::new(node1, node2); // test some positions let test_tuples = vec![ (node1.x() - 1000., LinePositionType::BeforeStart), (node1.x() - 1., LinePositionType::BeforeStart), (node1.x(), LinePositionType::InBetween), (node1.x() + 1., LinePositionType::InBetween), (node2.x() - 1., LinePositionType::InBetween), (node2.x(), LinePositionType::InBetween), (node2.x() + 1., LinePositionType::AfterEnd), (node2.x() + 1000., LinePositionType::AfterEnd), ]; let y_values = vec![(node1.y() - 1.), (node1.y()), (node1.y() + 1.)]; // y position should only affect the perpendicular distance for y in &y_values { // x position affects the position type and parallel distance for (x, semantic) in &test_tuples { let point = geo_types::Point::new(*x, *y); let result = LinePosition::for_ls(point, &ls).unwrap(); assert_eq!(semantic, &result.semantic); assert_eq!((y - node1.y()).abs(), result.perpendicular_dist); assert_eq!(x - node1.x(), result.parallel_dist); } } } // test on Y axis { let node1 = geo_types::Point::new(3., 5.); let node2 = geo_types::Point::new(3., 100.); let ls = geo_types::Line::new(node1, node2); // test some positions let test_tuples = vec![ (node1.y() - 1000., LinePositionType::BeforeStart), (node1.y() - 1., LinePositionType::BeforeStart), (node1.y(), LinePositionType::InBetween), (node1.y() + 1., LinePositionType::InBetween), (node2.y() - 1., LinePositionType::InBetween), (node2.y(), LinePositionType::InBetween), (node2.y() + 1., LinePositionType::AfterEnd), (node2.y() + 1000., LinePositionType::AfterEnd), ]; let x_values = vec![(node1.x() - 1.), (node1.x()), (node1.x() + 1.)]; // x position should only affect the perpendicular distance for x in &x_values { // y position affects the position type and parallel distance for (y, semantic) in &test_tuples { let point = geo_types::Point::new(*x, *y); let result = LinePosition::for_ls(point, &ls).unwrap(); assert_eq!(semantic, &result.semantic); assert_eq!((x - node1.x()).abs(), result.perpendicular_dist); assert_eq!(y - node1.y(), result.parallel_dist); } } } } }