Evaluate remaining red/green time for upcoming intersection
This commit is contained in:
parent
933106b7ea
commit
7463adb038
12 changed files with 2979 additions and 103 deletions
446
src/geo_alg.rs
Normal file
446
src/geo_alg.rs
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
//! 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<Self, String> {
|
||||
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<Self, String> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue