From 7463adb038cc2ce13142a03622c1efe18b484581 Mon Sep 17 00:00:00 2001 From: Jannik Beyerstedt Date: Sat, 30 May 2026 23:34:06 +0200 Subject: [PATCH] Evaluate remaining red/green time for upcoming intersection --- Cargo.lock | 123 ++++- Cargo.toml | 8 + Readme.md | 36 ++ src/applogic/mod.rs | 543 +++++++++++++++++++++ src/applogic/tlc.rs | 126 +++-- src/applogic/v2x/map.rs | 1019 +++++++++++++++++++++++++++++++++++++++ src/applogic/v2x/mod.rs | 4 + src/cache.rs | 221 +++++++++ src/geo_alg.rs | 446 +++++++++++++++++ src/main.rs | 78 +-- src/test.rs | 13 + src/testdata.rs | 457 ++++++++++++++++++ 12 files changed, 2975 insertions(+), 99 deletions(-) create mode 100644 src/applogic/v2x/map.rs create mode 100644 src/applogic/v2x/mod.rs create mode 100644 src/cache.rs create mode 100644 src/geo_alg.rs create mode 100644 src/testdata.rs diff --git a/Cargo.lock b/Cargo.lock index f183605..23c5a3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,56 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d" +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + [[package]] name = "anyhow" version = "1.0.102" @@ -29,6 +79,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert_float_eq" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd9777fd8234cccd34019d9e26d70c8247f9baf24c8fdc0b56c548cf943af2ed" + [[package]] name = "autocfg" version = "1.5.0" @@ -164,6 +220,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + [[package]] name = "const-default" version = "1.0.0" @@ -633,6 +695,29 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1011,6 +1096,7 @@ dependencies = [ name = "esp32-c_its-companion" version = "0.1.0" dependencies = [ + "assert_float_eq", "c-its-parser", "chrono", "critical-section", @@ -1020,6 +1106,7 @@ dependencies = [ "embassy_gps", "embedded-io 0.7.1", "embedded-io-async 0.7.0", + "env_logger", "esp-alloc", "esp-backtrace", "esp-bootloader-esp-idf", @@ -1027,7 +1114,9 @@ dependencies = [ "esp-println", "esp-radio", "esp-rtos", + "geo-types", "log", + "num-traits", ] [[package]] @@ -1333,6 +1422,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "itertools" version = "0.13.0" @@ -1594,6 +1689,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "paste" version = "1.0.15" @@ -1757,6 +1858,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1770,9 +1883,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.10" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" [[package]] name = "riscv" @@ -2202,6 +2315,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.23.1" diff --git a/Cargo.toml b/Cargo.toml index 5968d98..0c91516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ cam = ["c-its-parser/cam"] denm = ["c-its-parser/denm"] # evaluate SPAT information spat = ["c-its-parser/spatem", "c-its-parser/mapem"] +# enable some debug printing (without enabling debug logging of the ESP RTOS) +spat_debug = [] [dependencies] @@ -86,6 +88,12 @@ c-its-parser = { version = "2.2.2", default-features = false, features = [ ] } chrono = { version = "0.4.44", default-features = false, features = ["alloc"] } crossbeam-queue = { version = "0.3.12", default-features = false, features = ["alloc"] } +geo-types = { version = "0.7.19", default-features = false } +num-traits = { version = "0.2.19", default-features = false, features = ["libm"] } + +[dev-dependencies] +assert_float_eq = "1.2.0" +env_logger = "0.11.10" # For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html diff --git a/Readme.md b/Readme.md index c2d3983..2393479 100644 --- a/Readme.md +++ b/Readme.md @@ -2,6 +2,29 @@ Displays different C-ITS receive-only use cases for cheap. +## Hardware Requirements +The code is currently tailored to: +- Seeed XIAO ESP32-C5 (8MB PSRAM) +- Seeed XIAO L67K (connected on GPIO12 (RX), GPIO11 (TX), GPIO1 (wakeup) and GPIO25 (reset)) +TODO: only enable in `spat` feature? +- TODO: display (when using `spat` feature) + +## Features + +### SPAT +Takes the current position and heading from the GNSS receiver and displays the signal phases of the upcoming intersection. +The upcoming intersection is selected when the own position is "inside" some ingress approach of the intersection. +The display will show the current phase and remaining time for all signal groups of the bike-able lanes in the approach. + +### DENM +TODO: Not implemented yet! + +Shall "publish" the most relevant information of a DENMs via BLE + +### CAM +Just prints a line for each received CAM. + + ## Usage ### Toolchain Installation @@ -24,10 +47,23 @@ rustup component add --toolchain nightly rustfmt ``` ### Build and Flash +In default configuration only signal phases (SPAT) data is displayed for the upcoming intersection. +Additional features are available though feature flags: + +- `esp`: Run on ESP (required as feature flag to enable unit tests on host) +- `std`: Run on system with `std` library +- `spat`: Enable signal phase (and intersection map) data +- `denm`: Enable DENMs +- `cam`: Enable CAMs + Connect your ESP32-C5 and run: ``` +# default configuration cargo run --release + +# for e.g. also enable DENM feature +cargo run --release -F denm ``` ### Run Unit Tests on Host diff --git a/src/applogic/mod.rs b/src/applogic/mod.rs index 1e61aa8..194ab6c 100644 --- a/src/applogic/mod.rs +++ b/src/applogic/mod.rs @@ -1,8 +1,551 @@ //! Application Logic +#[cfg(feature = "spat")] +use alloc::rc::Rc; +#[cfg(feature = "spat")] +use alloc::vec::Vec; + +#[cfg(all(target_arch = "riscv32", feature = "spat"))] +use esp_println::println; +#[cfg(feature = "spat")] +use log::{debug, error}; +use log::{info, warn}; + +#[cfg(feature = "spat")] +use crate::geo_alg; + +pub mod v2x; + #[cfg(feature = "cam")] pub mod cam; #[cfg(feature = "denm")] pub mod denm; #[cfg(feature = "spat")] pub mod tlc; + +#[cfg(feature = "spat")] +const PRUNE_INTERVAL: chrono::Duration = chrono::Duration::seconds(5); + +#[cfg(feature = "spat")] +const GLOSA_SEARCH_DIST_M: f32 = 400.; +#[cfg(feature = "spat")] +const GLOSA_SEARCH_ANGLE_PARAMS: [(f32, f32); 2] = [(400., 45.), (50., 180.)]; // far and near (distance, angle) tuple +#[cfg(feature = "spat")] +const GLOSA_LANE_HEADING_DIFF_DEG: f32 = 35.; +#[cfg(feature = "spat")] +const GLOSA_AT_LANE_OFFSET_M: f64 = 10.; +#[cfg(feature = "spat")] +const GLOSA_LANE_LEFT_OFFSET_M: f64 = 20.; +#[cfg(feature = "spat")] +const GLOSA_STOP_LINE_PASSED_M: f64 = 10.; + +pub struct State { + initialized: bool, + time: chrono::NaiveDateTime, + + position: geo_types::Point, + heading_deg: Option, + speed_mps: Option, + + #[cfg(feature = "spat")] + last_prune: chrono::NaiveDateTime, + #[cfg(feature = "spat")] + tlc_cache: tlc::Data, + + // GLOSA + #[cfg(feature = "spat")] + #[allow(clippy::struct_field_names)] + glosa_state: GlosaState, +} + +#[cfg(feature = "spat")] +#[derive(Debug, Default)] +enum GlosaState { + #[default] + Idle, + Locked(TlData), +} + +/// Where we are and pre-processed information about the current intersection +#[cfg(feature = "spat")] +#[derive(Debug)] +struct TlData { + intersection_id: u16, // mostly for debug printing + intersection: Rc>, + + lane_id: u8, + approach_id: Option, + + signal_groups: Vec, +} + +impl State { + pub fn new() -> Self { + Self { + initialized: false, + time: chrono::NaiveDateTime::default(), + position: geo_types::Point::default(), + heading_deg: None, + speed_mps: None, + #[cfg(feature = "spat")] + last_prune: chrono::NaiveDateTime::default(), + #[cfg(feature = "spat")] + tlc_cache: tlc::Data::new(chrono::Duration::seconds(20)), + #[cfg(feature = "spat")] + glosa_state: GlosaState::default(), + } + } + + #[cfg(feature = "esp")] + pub fn update_with_gpsfix(&mut self, fix: &embassy_gps::types::GpsFix) { + self.position = geo_types::Point::new(fix.longitude_deg, fix.latitude_deg); + + if let Some(heading) = fix.true_course_deg { + self.heading_deg = Some(heading); + } else { + warn!("State: Course missing from GpsFix"); + } + + if let Some(speed) = fix.speed_over_ground { + self.speed_mps = Some(speed); + } else { + warn!("State: Speed missing from GpsFix"); + } + + if let Some(time) = fix.get_timestamp() { + self.time = time; + + if !self.initialized { + info!("GNSS fix: {}", self.print_fix()); + } + self.initialized = true; + } + } + + pub fn print_fix(&self) -> alloc::string::String { + alloc::format!( + "{:?}, {:.4} N, {:.4} E, {:.0}°, {:.2} m/s", + self.time, + self.position.y(), + self.position.x(), + self.heading_deg.unwrap_or_default(), + self.speed_mps.unwrap_or_default(), + ) + } + + pub fn prune(&mut self) { + #[cfg(feature = "spat")] + if self.last_prune + PRUNE_INTERVAL < self.time { + self.tlc_cache.prune(self.time); + + info!("Known int.: {:?}", self.tlc_cache.known_intersections()); + + self.last_prune = self.time; + } + } + + #[cfg(feature = "spat")] + pub fn handle_mapem(&mut self, etsi: &tlc::MAPEM) { + self.tlc_cache.handle_mapem(self.time, etsi); + } + + #[cfg(feature = "spat")] + pub fn handle_spatem(&mut self, etsi: &tlc::SPATEM) { + // only update "our" intersection + if let GlosaState::Locked(data) = &self.glosa_state + && etsi + .spat + .intersections + .0 + .iter() + .find(|i| i.id.id.0 == data.intersection_id) + .is_some() + { + self.tlc_cache.handle_spatem(self.time, etsi); + } + } + + #[cfg(feature = "spat")] + pub fn run_glosa(&mut self) { + let Some(own_heading) = self.heading_deg else { + return; + }; + + let state_update = match &self.glosa_state { + // search for upcoming intersection + GlosaState::Idle => { + debug!( + "GLOSA: Searching with {} known intersections", + self.tlc_cache.known_intersections().len() + ); + + if let Some(search_result) = + Self::find_intersection(&self.position, own_heading, &self.tlc_cache) + { + info!( + "GLOSA: Approach has signal groups: {:?}", + search_result.signal_groups + ); + + Some(GlosaState::Locked(search_result)) + } else { + None + } + } + GlosaState::Locked(info) => { + debug!( + "GLOSA: Re-evaluating intersection {:4} (at lane {:3})", + info.intersection_id, info.lane_id + ); + + // determine if intersection is left + let intersection = info.intersection.borrow(); + if Self::is_intersection_left( + &intersection, + info.lane_id, + info.approach_id, + &self.position, + ) { + info!("GLOSA: Intersection was left -> back to idle"); + Some(GlosaState::Idle) + } else { + // get signal groups which are in our approach + let mut approach_sig_grps = intersection + .signal_groups() + .iter() + .filter(|i| { + info.signal_groups + .iter() + .find(|sg_id| **sg_id == i.id()) + .is_some() + }) + .collect::>(); + + // sort signal groups by maneuver + approach_sig_grps.sort_by_key(|v| v.maneuvers()); + + // display signal phases + for sig in approach_sig_grps { + if let Some(phase) = sig.phase_to_str() + && let Some(manv) = sig.maneuvers() + { + use core::ops::Sub; + + let min_end = sig.min_end_time(); + let dur = min_end.sub(self.time.and_utc()).as_seconds_f32(); + println!( + "GLOSA: {manv} {phase} for {dur:.1} s (until {})", + sig.min_end_time() + ); + } + } + + None // keep state + } + } + }; + + // can't update the state from within the match statement, so do it here + if let Some(new) = state_update { + self.glosa_state = new; + } + } + + #[cfg(feature = "spat")] + #[allow(clippy::too_many_lines)] + fn find_intersection( + own_pos: &geo_types::Point, + own_heading: f32, + tlc_cache: &tlc::Data, + ) -> Option { + // filter known intersections by distance (and bearing) + let upcoming_intersections = tlc_cache.intersections_matching(|i| { + let int_pos = i.center(); + + #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] + let distance = geo_alg::haversine_dist(own_pos, &int_pos) as f32; + let bearing = geo_alg::haversine_bearing(own_pos, &int_pos); + let bearing_diff = geo_alg::deg_180(own_heading - bearing); + + distance < GLOSA_SEARCH_DIST_M && bearing_diff.abs() < Self::get_search_angle(distance) + }); + + info!( + "GLOSA: upcoming int. count: {}", + upcoming_intersections.len() + ); + + // determine relevant approach + for int in upcoming_intersections { + let intersection = int.borrow(); + let lanes = intersection.ingress_lanes(); + + let own_pos_cart = geo_alg::lonlat_to_dcart(&intersection.ref_point(), own_pos); + + // find lanes with matching heading + let relevant_lanes = lanes + .iter() + .filter(|i| { + let lane_heading = i.get_heading(); + let heading_diff = geo_alg::deg_180(own_heading - lane_heading); + + debug!( + " - Lane {:4}/{:03} has heading {lane_heading:4.0} -> diff {heading_diff:4.0}°", + intersection.id(), + i.id() + ); + + heading_diff.abs() < GLOSA_LANE_HEADING_DIFF_DEG + }) + .collect::>(); + + info!( + "GLOSA: Int {:4} has {} relevant lanes", + intersection.id(), + relevant_lanes.len() + ); + for lane in &relevant_lanes { + info!( + " - lane {:03} from appr. {:?}, usages {:?}", + lane.id(), + lane.approach_id(), + lane.usages() + ); + } + + // determine in which lane/ approach we are + let current_lane = relevant_lanes.iter().find(|lane| { + #[cfg(feature = "spat_debug")] + info!("GLOSA: Searching for current lane"); + match geo_alg::LinePosition::for_ingress_lane(own_pos_cart, lane.nodes()) { + Ok(pos) => { + #[cfg(feature = "spat_debug")] + info!( + " - Lane {:4}/{:03} {:?} with {:.0} m offset", + intersection.id(), + lane.id(), + pos.semantic, + pos.perpendicular_dist, + ); + + if pos.perpendicular_dist < GLOSA_AT_LANE_OFFSET_M + && pos.semantic == geo_alg::LinePositionType::InBetween + { + info!( + "GLOSA: At lane {:4}/{:03} -> locking intersection", + intersection.id(), + lane.id() + ); + + true + } else { + false + } + } + Err(err) => { + warn!("Lane {:04}: Failed to determine position: {err}", lane.id()); + false + } + } + }); + + if let Some(lane) = current_lane { + // we found some lane which we are "inside" -> lane search can be aborted here + // but we should extract some information which is needed for future processing first + + // collect all signal groups from the current approach + let mut signal_groups = if let Some(own_approach) = lane.approach_id() { + let approach_lanes = relevant_lanes.iter().filter(|lane| { + if let Some(approach) = lane.approach_id() { + approach == own_approach + } else { + false + } + }); + + approach_lanes + .flat_map(|lane| { + lane.connections() + .iter() + .filter_map(v2x::map::Connection::signal_group) + }) + .collect::>() + } else { + error!( + "GLOSA: Lane has no approach ID, can't determine all possibly relevant signal groups" + ); + // just fall back to only the current lane's signal groups + lane.connections() + .iter() + .filter_map(v2x::map::Connection::signal_group) + .collect::>() + }; + + signal_groups.sort_unstable(); + signal_groups.dedup(); + + return Some(TlData { + intersection_id: intersection.id(), + intersection: int.clone(), + lane_id: lane.id(), + approach_id: lane.approach_id(), + signal_groups, + }); + } + } + + None + } + + #[cfg(feature = "spat")] + fn get_search_angle(distance: f32) -> f32 { + let (dist_near, angle_near) = GLOSA_SEARCH_ANGLE_PARAMS[0]; + let (dist_far, angle_far) = GLOSA_SEARCH_ANGLE_PARAMS[1]; + + if distance > dist_far { + angle_far + } else if distance < dist_near { + angle_near + } else { + // interpolate angle from near to far + let diff_dist = dist_far - dist_near; + let diff_angle = angle_far - angle_near; + + angle_near + (diff_angle / diff_dist) * (distance - dist_near) + } + } + + /// Determines if intersection was left + /// + /// TODO: return if left via stop line or otherwise + #[cfg(feature = "spat")] + fn is_intersection_left( + intersection: &v2x::map::Intersection, + lane_id: u8, + approach_id: Option, + own_pos: &geo_types::Point, + ) -> bool { + // get all lanes from the current approach + let lanes = if let Some(approach) = approach_id { + intersection.approach_lanes(approach) + } else if let Some(lane) = intersection.lane(lane_id) { + alloc::vec![lane] + } else { + alloc::vec![] + }; + + // find minimal distance to any lane where we are "inside" + let own_pos_cart = geo_alg::lonlat_to_dcart(&intersection.ref_point(), own_pos); + let min_perp_dist = lanes.iter().fold(f64::MAX, |min_dist, i| { + match geo_alg::LinePosition::for_ingress_lane(own_pos_cart, i.nodes()) { + Ok(pos) => { + // check if we left the lane via the stop line, but only count stop line as passed after X meters + if pos.semantic == geo_alg::LinePositionType::InBetween + || (pos.semantic == geo_alg::LinePositionType::BeforeStart + && pos.parallel_dist.abs() < GLOSA_STOP_LINE_PASSED_M) + { + // still at the lane -> update min_dist + pos.perpendicular_dist + } else { + // not at the lane -> take old min_dist + min_dist + } + } + Err(err) => { + warn!("Lane {:04}: Failed to determine position: {err}", i.id()); + min_dist + } + } + }); + + min_perp_dist > GLOSA_LANE_LEFT_OFFSET_M + } + + // -- getters -- + + #[allow(unused)] + pub fn initialized(&self) -> bool { + self.initialized + } + + #[allow(unused)] + pub fn time(&self) -> chrono::prelude::NaiveDateTime { + self.time + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::init_test_env_logger; + use crate::testdata::{MAPEM_HH_560, MAPEM_HH_1328}; + + #[cfg(feature = "spat")] + #[test] + fn glosa() { + init_test_env_logger(); + + // prime TLC cache + let mut state = State::new(); + let time_now = chrono::NaiveDateTime::default(); + + let etsi = if let c_its_parser::ItsMessage::Mapem { + geonetworking: _, + transport: _, + etsi, + } = + c_its_parser::de::decode(MAPEM_HH_1328, c_its_parser::Headers::None).unwrap() + { + etsi + } else { + panic!("Unexpected C-ITS message") + }; + state.tlc_cache.handle_mapem(time_now, &etsi); + + let etsi = if let c_its_parser::ItsMessage::Mapem { + geonetworking: _, + transport: _, + etsi, + } = + c_its_parser::de::decode(MAPEM_HH_560, c_its_parser::Headers::None).unwrap() + { + etsi + } else { + panic!("Unexpected C-ITS message") + }; + state.tlc_cache.handle_mapem(time_now, &etsi); + + // test GLOSA + let own_pos_infront = geo_types::Point::new(9.9752864, 53.5564028); + let own_pos_inside = geo_types::Point::new(9.9760764, 53.5563621); + let own_pos_passed = geo_types::Point::new(9.977217, 53.5560602); + let own_heading = 90.; + + state.heading_deg = Some(own_heading); + + state.position = own_pos_infront; + state.run_glosa(); + assert!(matches!(state.glosa_state, GlosaState::Idle)); + + state.position = own_pos_inside; + state.run_glosa(); + assert!(matches!(state.glosa_state, GlosaState::Locked(_))); + if let GlosaState::Locked(info) = &state.glosa_state { + assert_eq!(1328, info.intersection_id); + assert!(info.lane_id == 1 || info.lane_id == 2); + } + + state.position = own_pos_inside; + state.run_glosa(); + assert!(matches!(state.glosa_state, GlosaState::Locked(_))); + if let GlosaState::Locked(info) = &state.glosa_state { + assert_eq!(1328, info.intersection_id); + assert!(info.lane_id == 1 || info.lane_id == 2); + } + + state.position = own_pos_passed; + state.run_glosa(); + assert!(matches!(state.glosa_state, GlosaState::Idle)); + } +} diff --git a/src/applogic/tlc.rs b/src/applogic/tlc.rs index 4b631c9..96cbd8a 100644 --- a/src/applogic/tlc.rs +++ b/src/applogic/tlc.rs @@ -1,52 +1,108 @@ -//! C-ITS Traffic Light Data (MAPEM/ SPATEM) Evaluation +//! C-ITS Traffic Light Data (MAPEM/ SPATEM) Evaluation/ Cache -use c_its_parser::standards::mapem_2_2_1::mapem_pdu_descriptions; -use c_its_parser::standards::spatem_2_2_1::spatem_pdu_descriptions; +use alloc::rc::Rc; +use alloc::vec::Vec; + +pub use c_its_parser::standards::mapem_2_2_1::mapem_pdu_descriptions::MAPEM; +pub use c_its_parser::standards::spatem_2_2_1::spatem_pdu_descriptions::SPATEM; use chrono::Datelike; -#[cfg(target_arch = "riscv32")] +#[cfg(all(target_arch = "riscv32", feature = "spat_debug"))] use esp_println::println; -pub fn handle_mapem(etsi: &mapem_pdu_descriptions::MAPEM) { - // TODO: proof of concept only -- replace with actual evaluation - if let Some(map) = &etsi.map.intersections { - for int in &map.0 { - println!("got MAPEM for intersection {}", int.id.id.0); - } - } +use super::v2x::map; +use crate::cache::Cache; + +pub struct Data { + intersections: Cache, } -pub fn handle_spatem(etsi: &spatem_pdu_descriptions::SPATEM, now: &chrono::NaiveDateTime) { - // TODO: proof of concept only -- replace with actual evaluation - for int in &etsi.spat.intersections.0 { - println!("got SPATEM for intersection {}", int.id.id.0); +impl Data { + pub fn new(cache_lifetime: chrono::Duration) -> Self { + Self { + intersections: Cache::::new(cache_lifetime), + } + } - if let (Some(moy), Some(second)) = (&int.moy, &int.time_stamp) { - let year = now.year(); + pub fn known_intersections(&self) -> Vec { + self.intersections.keys() + } - let ref_time = c_its_parser::time_utils::time_from_moy_and_dsecond(moy, second, year); - println!("- ref time: {ref_time:?}"); + pub fn intersections_matching

( + &self, + predicate: P, + ) -> Vec>> + where + P: Fn(&map::Intersection) -> bool, + { + self.intersections.get_filtered(predicate) + } - for state in &int.states.0 { - let sig_grp = state.signal_group.0; + pub fn handle_mapem(&mut self, time_now: chrono::NaiveDateTime, etsi: &MAPEM) { + let layer_id = etsi.map.layer_id.as_ref().map(|v| v.0); + if let Some(map) = &etsi.map.intersections { + for geom in &map.0 { + let int_id = geom.id.id.0; - if let Some(event) = state.state_time_speed.0.first() { - if let Some(timing) = &event.timing { - let min_end_time = timing.min_end_time.to_datetime_from_moy(moy, year); + if self.intersections.contains(&int_id) { + // update with new map layers - println!( - "- sig grp. {sig_grp:03}: until {min_end_time:?} at {:?}", - event.event_state - ); - } else { - println!( - "- sig grp. {sig_grp:03}: {:?} until unknown", - event.event_state - ); - } + // we can ignore the result, since we're sure the item exists + let _ = self.intersections.update(time_now, int_id, |i| { + if !i.is_complete() { + i.add_layer(layer_id, geom); + + #[cfg(feature = "spat_debug")] + if i.is_complete() { + Self::pretty_print_intersection(&i); + } + } + }); } else { - println!("- sig grp. {sig_grp:03}: no events"); + // create new intersection + let intersection = map::Intersection::new(layer_id, geom); + + #[cfg(feature = "spat_debug")] + if intersection.is_complete() { + Self::pretty_print_intersection(&intersection); + } + + // we can ignore the result, since we're sure the item does not exist + let _ = self.intersections.insert(time_now, intersection); } } } } + + #[cfg(feature = "spat_debug")] + fn pretty_print_intersection(intersection: &map::Intersection) { + println!("TLC: {intersection}"); + println!("TLC: Signal Groups"); + for grp in intersection.signal_groups() { + println!(" {grp}"); + } + } + + pub fn prune(&mut self, time_now: chrono::NaiveDateTime) { + self.intersections.prune(time_now); + } + + pub fn handle_spatem(&mut self, time_now: chrono::NaiveDateTime, etsi: &SPATEM) { + for int in &etsi.spat.intersections.0 { + let int_id = int.id.id.0; + + if self.intersections.contains(&int_id) { + // update with SPAT data + + // we can ignore the result, since we're sure the item exists + let _ = self.intersections.update(time_now, int_id, |i| { + if i.is_complete() { + // add SPAT information to intersection + i.add_spat(int, time_now.year()); + } + }); + } else { + // drop message since we don't have (full) MAP yet + } + } + } } diff --git a/src/applogic/v2x/map.rs b/src/applogic/v2x/map.rs new file mode 100644 index 0000000..b49e818 --- /dev/null +++ b/src/applogic/v2x/map.rs @@ -0,0 +1,1019 @@ +//! Types for V2X data handling + +use alloc::string::{String, ToString}; +use alloc::vec::Vec; +use alloc::{format, vec}; +use core::ops::Div; + +use c_its_parser::standards::dsrc_2_2_1::etsi_its_dsrc; +use log::warn; + +use crate::cache::Cachable; +use crate::geo_alg; + +#[derive(Debug, Clone)] +#[allow(unused)] +pub struct Intersection { + id: u16, + /// reference point as lon/lat + ref_point: geo_types::Point, + /// guessed center point as lon/lat + center: geo_types::Point, + lanes: Vec, + signal_groups: Vec, + + // internal state + num_layers: u8, + layers_present: Vec, + is_complete: bool, + last_spat_millis: u16, +} + +impl Cachable for Intersection { + fn key(&self) -> u16 { + self.id + } +} + +impl core::fmt::Display for Intersection { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let lanes = self.lanes.iter().fold(String::new(), |mut acc, i| { + use core::fmt::Write; + let _ = writeln!(acc, " {i}"); + acc + }); + + write!( + f, + "Intersection {} {{ ref: {:?}, \n{lanes}}}", + self.id, self.ref_point + ) + } +} + +#[allow(unused)] +impl Intersection { + pub fn new(layer_id: Option, value: &etsi_its_dsrc::IntersectionGeometry) -> Self { + let id = value.id.id.0; + let ref_point = value.ref_point.clone().into(); + let lanes = value + .lane_set + .0 + .iter() + .filter_map(|v| match Lane::new(v, &ref_point) { + Ok(lane) => Some(lane), + Err(err) => { + warn!("{err}"); + None + } + }) + .collect::>(); + + let (num_layers, layers_present, is_complete) = if let Some(layer_id) = layer_id { + let (num_layers, layer_num) = Self::layer_num_and_count(layer_id); + let complete = num_layers == 1; // just to be save from stupid input data + + (num_layers, alloc::vec![layer_num], complete) + } else { + (1, alloc::vec![1], true) + }; + + let signal_groups = if is_complete { + Self::collect_signal_groups(&lanes) + } else { + Vec::default() + }; + + let center = if is_complete { + Self::calculate_center(&ref_point, &lanes) + } else { + geo_types::Point::default() + }; + + Self { + id, + ref_point, + center, + lanes, + signal_groups, + num_layers, + layers_present, + is_complete, + last_spat_millis: 0, + } + } + + pub fn add_layer(&mut self, layer_id: Option, value: &etsi_its_dsrc::IntersectionGeometry) { + if self.is_complete { + return; + } + let Some(layer_id) = layer_id else { + return; + }; + let (_, layer_num) = Self::layer_num_and_count(layer_id); + + // determine if we already have this layer + if self.layers_present.contains(&layer_num) { + return; + } + + // add lanes + for lane in &value.lane_set.0 { + let lane_id = lane.lane_id.0; + + if self.lanes.iter().find(|i| i.id == lane_id).is_none() { + match Lane::new(lane, &self.ref_point) { + Ok(lane) => self.lanes.push(lane), + Err(err) => { + warn!("{err}"); + } + } + } + } + + self.layers_present.push(layer_num); + + // we can just count the present layers since we made sure to not add already known ones beforehand + if self.layers_present.len() == self.num_layers as usize { + self.signal_groups = Self::collect_signal_groups(&self.lanes); + self.center = Self::calculate_center(&self.ref_point, &self.lanes); + self.is_complete = true; + } + } + + pub fn add_spat(&mut self, value: &etsi_its_dsrc::IntersectionState, year: i32) { + if let Some(millis) = &value.time_stamp { + if self.last_spat_millis == millis.0 { + // skip this SPATEM since we already processed one with the same timestamp + return; + } + self.last_spat_millis = millis.0; + } + + if let Some(moy) = &value.moy { + for state in &value.states.0 { + let sig_grp = state.signal_group.0; + + if let Some(event) = state.state_time_speed.0.first() + && let Some(timing) = &event.timing + { + // get write access to SignalGroup item + if let Some(idx) = self.signal_groups.iter().position(|i| i.id == sig_grp) + && let Some(sig_grp) = self.signal_groups.get_mut(idx) + { + sig_grp.phase = Some(event.event_state); + sig_grp.min_end_time = timing.min_end_time.to_datetime_from_moy(moy, year); + } + } else { + // warn!("SPAT: SG {sig_grp:03} has no timing in first event"); + } + } + } + } + + fn collect_signal_groups(lanes: &[Lane]) -> Vec { + let mut connections = lanes + .iter() + .flat_map(|i| i.connections.clone()) + .filter(|i| i.signal_group.is_some()) + .collect::>(); + + connections.sort_by_key(|i| i.signal_group.unwrap_or_default()); + + let chunks = connections.chunk_by(|a, b| a.signal_group == b.signal_group); + chunks + .map(|chunk| { + let (sig_grp_id, maneuvers, mut target_lanes) = chunk.iter().fold( + (None, Maneuvers::default(), vec![]), + |(sig_grp_id, maneuvers, mut lanes), i| { + let new_maneuvers = i + .maneuvers + .map(|v| core::ops::Add::add(maneuvers, v)) + .unwrap_or_default(); + lanes.push(i.target_lane_id); + + (i.signal_group, new_maneuvers, lanes) + }, + ); + + // unwrap is fine since we removed all connections without signal group beforehand + let id = sig_grp_id.unwrap(); + let maneuvers = if maneuvers.is_emtpy() { + None + } else { + Some(maneuvers) + }; + target_lanes.sort_unstable(); + // TODO: dedup as well!? + + SignalGroup { + id, + maneuvers, + target_lanes, + phase: None, + min_end_time: chrono::DateTime::default(), + } + }) + .collect() + } + + fn calculate_center(ref_pos: &geo_types::Point, lanes: &[Lane]) -> geo_types::Point { + let (sum, count) = lanes.iter().fold( + (geo_types::Point::default(), 0), + |(center_sum, count), lane| { + // only use vehicle lanes + if lane.usages.contains(&UsageType::Vehicle) + && let Some(stop_line) = lane.get_stop_line() + { + (center_sum + stop_line.into(), count + 1) + } else { + (center_sum, count) + } + }, + ); + + // convert from X/Y to geo coordinate + let center_xy = sum.div(f64::from(count)); + + geo_alg::dcart_to_lonlat(ref_pos, ¢er_xy) + } + + fn layer_num_and_count(layer_id: u8) -> (u8, u8) { + let layer_num = layer_id % 10; + let num_layers = (layer_id - layer_num) / 10; + + (num_layers, layer_num) + } + + pub fn is_complete(&self) -> bool { + self.is_complete + } + + pub fn id(&self) -> u16 { + self.id + } + pub fn ref_point(&self) -> geo_types::Point { + self.ref_point + } + pub fn center(&self) -> geo_types::Point { + self.center + } + pub fn lanes(&self) -> &[Lane] { + &self.lanes + } + pub fn signal_groups(&self) -> &[SignalGroup] { + &self.signal_groups + } + + pub fn lane(&self, lane_id: u8) -> Option<&Lane> { + self.lanes.iter().find(|i| i.id == lane_id) + } + + pub fn ingress_lanes(&self) -> Vec<&Lane> { + self.lanes.iter().filter(|i| i.is_ingress).collect() + } + + pub fn bikeable_ingress_lanes(&self) -> Vec<&Lane> { + self.lanes + .iter() + .filter(|i| i.is_ingress && i.usages.contains(&UsageType::Bike)) + .collect() + } + + pub fn approach_lanes(&self, approach_id: u8) -> Vec<&Lane> { + self.lanes + .iter() + .filter(|i| i.approach_id.is_some_and(|i| i == approach_id)) + .collect() + } +} + +#[derive(Debug, Clone)] +#[allow(unused)] +pub struct Lane { + id: u8, + approach_id: Option, + /// lane nodes as local X/Y coordinates, X to the North, Y to the East + nodes: geo_types::LineString, + is_ingress: bool, + is_egress: bool, + usages: Vec, + connections: Vec, + maneuvers: Vec, +} + +impl core::fmt::Display for Lane { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let type_str = match (self.is_ingress, self.is_egress) { + (true, true) => "IE", + (true, false) => "I", + (false, true) => "E", + (false, false) => "", + }; + let appr_str = if let Some(appr) = self.approach_id { + format!("{appr}") + } else { + String::new() + }; + let conns_str: String = self.connections.iter().fold(String::new(), |mut acc, i| { + use core::fmt::Write; + let _ = write!(acc, "{i}"); + acc + }); + + write!( + f, + "Lane {} ({type_str}-{appr_str}, {:?}) -> [{conns_str}]", + self.id, self.usages + ) + } +} + +#[derive(Debug, Copy, Clone)] +#[allow(unused)] +pub struct Connection { + id: Option, + target_lane_id: u8, + maneuvers: Option, + signal_group: Option, +} + +impl core::fmt::Display for Connection { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let id = self.id.unwrap_or_default(); + let sig_str = match self.signal_group { + Some(sig) => format!("sig. {sig}"), + None => String::new(), + }; + + write!(f, "{{ conn. {id}: to {}, {sig_str} }}", self.target_lane_id) + } +} + +#[allow(unused)] +impl Connection { + pub fn id(&self) -> Option { + self.id + } + pub fn maneuvers(&self) -> Option { + self.maneuvers + } + pub fn signal_group(&self) -> Option { + self.signal_group + } +} + +impl From<&etsi_its_dsrc::Connection> for Connection { + fn from(value: &etsi_its_dsrc::Connection) -> Self { + let id = value.connection_id.as_ref().map(|v| v.0); + let target_lane_id = value.connecting_lane.lane.0; + let maneuvers = value + .connecting_lane + .maneuver + .as_ref() + .map(Maneuvers::from_dsrc); + let signal_group = value.signal_group.as_ref().map(|v| v.0); + + Self { + id, + target_lane_id, + maneuvers, + signal_group, + } + } +} + +/// Different view on a lane connection +/// +/// Technically this data is redundant with the `Connection` data of a `Lane`, +/// but it makes processing SPAT information easier, if signal group maneuvers can be accessed directly and not my collecting them from all lane connections. +#[derive(Debug, Clone)] +#[allow(unused)] +pub struct SignalGroup { + id: u8, + maneuvers: Option, + target_lanes: Vec, // mainly for debugging + + // SPAT data + /// current signal phase + phase: Option, + min_end_time: chrono::DateTime, + // max_end_time: Option>, + // likely_time: Option>, +} + +impl core::fmt::Display for SignalGroup { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let manv_str = self.maneuvers.map(|v| format!(" {v}")).unwrap_or_default(); + let phase_str = self.phase_to_str().unwrap_or_default(); + write!( + f, + "SG {:2} {{{manv_str} to {:?} {phase_str}}}", + self.id, self.target_lanes + ) + } +} + +#[allow(unused)] +impl SignalGroup { + pub fn id(&self) -> u8 { + self.id + } + pub fn maneuvers(&self) -> Option { + self.maneuvers + } + pub fn phase(&self) -> Option { + self.phase + } + pub fn min_end_time(&self) -> chrono::prelude::DateTime { + self.min_end_time + } + + pub fn phase_to_str(&self) -> Option { + self.phase.map(|v| { + match v { + etsi_its_dsrc::MovementPhaseState::unavailable => "n.a.", + etsi_its_dsrc::MovementPhaseState::dark => "dark", + etsi_its_dsrc::MovementPhaseState::stop_Then_Proceed => "stop", + etsi_its_dsrc::MovementPhaseState::stop_And_Remain => "RED", + etsi_its_dsrc::MovementPhaseState::pre_Movement => "PRE", + etsi_its_dsrc::MovementPhaseState::permissive_Movement_Allowed + | etsi_its_dsrc::MovementPhaseState::protected_Movement_Allowed => "GRN", + etsi_its_dsrc::MovementPhaseState::permissive_clearance + | etsi_its_dsrc::MovementPhaseState::protected_clearance => "YEL", + + etsi_its_dsrc::MovementPhaseState::caution_Conflicting_Traffic => "blnk", + } + .to_string() + }) + } +} + +#[derive(Debug, Clone)] +#[allow(unused)] +pub struct SignalPhase { + state: etsi_its_dsrc::SignalStatus, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +#[allow(unused)] +pub enum UsageType { + /// vehicle lanes that are not limited to Bus or Taxi use, nor restricted from public use + Vehicle, + /// vehicle lanes that are restricted to bus use or vehicle lanes shared with bus vehicle traffic + Bus, + /// tracked-vehicle lanes or vehicle lanes shared with tracked vehicle traffic + Tram, + /// bike-lanes or vehicle lanes shared with cyclist vehicle traffic + Bike, + /// crosswalks or sidewalks + Pedestrian, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)] +pub struct Maneuvers { + left_allowed: bool, + staight_allowed: bool, + right_allowed: bool, +} + +impl core::fmt::Display for Maneuvers { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "Maneuvers {{ {}{}{} }}", + if self.left_allowed { "l" } else { "_" }, + if self.staight_allowed { "s" } else { "_" }, + if self.right_allowed { "r" } else { "_" }, + ) + } +} + +impl PartialOrd for Maneuvers { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for Maneuvers { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + if self.eq(other) { + core::cmp::Ordering::Equal + } else { + #[allow(clippy::unnested_or_patterns)] + match (self.left_allowed, self.staight_allowed, self.right_allowed) { + // left (or none) < everything + (true, false, false) | (false, false, false) => core::cmp::Ordering::Less, + // left and straight > (just left) + (true, true, false) => { + if other.eq(&Maneuvers::new(true, false, false)) { + core::cmp::Ordering::Greater + } else { + core::cmp::Ordering::Less + } + } + // omni > (anything without right) + (true, true, true) | (true, false, true) => { + if other.right_allowed { + core::cmp::Ordering::Less + } else { + core::cmp::Ordering::Greater + } + } + // (only straight) > omni + (false, true, false) => { + if other.eq(&Maneuvers::new(true, true, true)) { + core::cmp::Ordering::Greater + } else { + core::cmp::Ordering::Less + } + } + // (straight and right) < right + (false, true, true) => { + if other.eq(&Maneuvers::new(false, false, true)) { + core::cmp::Ordering::Less + } else { + core::cmp::Ordering::Greater + } + } + // right > alles + (false, false, true) => core::cmp::Ordering::Greater, + } + } + } +} + +impl core::ops::Add for Maneuvers { + type Output = Maneuvers; + + fn add(self, rhs: Self) -> Self::Output { + Self::Output { + staight_allowed: self.staight_allowed || rhs.staight_allowed, + left_allowed: self.left_allowed || rhs.left_allowed, + right_allowed: self.right_allowed || rhs.right_allowed, + } + } +} + +#[allow(unused)] +impl Maneuvers { + pub fn new(left_allowed: bool, staight_allowed: bool, right_allowed: bool) -> Self { + Self { + left_allowed, + staight_allowed, + right_allowed, + } + } + + pub fn from_dsrc(value: &etsi_its_dsrc::AllowedManeuvers) -> Self { + let left_allowed = + value.get_maneuver_left_allowed() || value.get_maneuver_left_turn_on_red_allowed(); + let right_allowed = + value.get_maneuver_right_allowed() || value.get_maneuver_right_turn_on_red_allowed(); + + Self { + left_allowed, + staight_allowed: value.get_maneuver_straight_allowed(), + right_allowed, + } + } + + pub fn is_emtpy(self) -> bool { + !self.left_allowed && !self.staight_allowed && !self.right_allowed + } +} + +#[allow(unused)] +impl Lane { + /// Creates a `Lane` from C-ITS data + pub fn new( + lane: &etsi_its_dsrc::GenericLane, + ref_pos: &geo_types::Point, + ) -> Result { + let etsi_its_dsrc::NodeListXY::nodes(node_set_xy) = &lane.node_list else { + return Err(format!( + "Lane::new(): ComputedLane not supported (used in lane {})", + lane.lane_id.0 + )); + }; + + let nodes_enu = Self::node_set_to_line_string_xy(node_set_xy, *ref_pos); + + // convert to NED instead of ENU + let nodes = nodes_enu + .points() + .map(|p| geo_types::Point::new(p.y(), p.x())) + .collect(); + + let usages = UsageType::get_usages(&lane.lane_attributes)?; + let connections = lane + .connects_to + .as_ref() + .map(|v| v.0.iter().map(core::convert::Into::into).collect()) + .unwrap_or_default(); + + let maneuvers = alloc::vec![]; + + let is_ingress = lane.lane_attributes.directional_use.get_ingress_path(); + let is_egress = lane.lane_attributes.directional_use.get_egress_path(); + + Ok(Self { + id: lane.lane_id.0, + approach_id: lane.ingress_approach.as_ref().map(|v| v.0), + nodes, + usages, + connections, + maneuvers, + is_ingress, + is_egress, + }) + } + + fn node_set_to_line_string_xy( + data: &etsi_its_dsrc::NodeSetXY, + ref_pos: geo_types::Point, + ) -> geo_types::LineString { + let mut prev_delta = geo_types::Point::default(); + let mut prev_ref_pos = ref_pos; + data.0 + .iter() + .map(|pt| { + let node = match &pt.delta { + etsi_its_dsrc::NodeOffsetPointXY::node_LatLon(node_llm_d64b) => { + let result = pt.delta.to_ddist(&prev_ref_pos); + + // reference position needs to be the last `node_LatLon` + prev_ref_pos = geo_types::Point::new( + node_llm_d64b.lon.as_deg(), + node_llm_d64b.lat.as_deg(), + ); + result + } + _ => prev_delta + pt.delta.to_ddist(&ref_pos), + }; + prev_delta = node; + + node + }) + .collect() + } + + pub fn id(&self) -> u8 { + self.id + } + pub fn approach_id(&self) -> Option { + self.approach_id + } + pub fn is_ingress(&self) -> bool { + self.is_ingress + } + pub fn nodes(&self) -> &geo_types::LineString { + &self.nodes + } + pub fn usages(&self) -> &[UsageType] { + &self.usages + } + pub fn connections(&self) -> &[Connection] { + &self.connections + } + pub fn maneuvers(&self) -> &[Maneuvers] { + &self.maneuvers + } + + pub fn get_stop_line(&self) -> Option { + self.nodes.0.first().copied() + } + + /// Determines average heading + pub fn get_heading(&self) -> f32 { + // unwrap is fine since a lane always contains at least 2 nodes + let stop_line = self.nodes.points().next().unwrap(); + // unwrap is fine since a lane always contains at least 2 nodes + let second_point = self.nodes.points().next_back().unwrap(); + + #[allow(clippy::cast_possible_truncation)] + let average_heading = geo_alg::cartesian_bearing(second_point, stop_line) as f32; + + average_heading + } + + /// Determines heading at stop line and average + pub fn get_headings(&self) -> (f32, f32) { + // unwrap is fine since a lane always contains at least 2 nodes + let stop_line = self.nodes.points().next().unwrap(); + // unwrap is fine since a lane always contains at least 2 nodes + let second_point = self.nodes.points().next().unwrap(); + // unwrap is fine since a lane always contains at least 2 nodes + let entry_point = self.nodes.points().next_back().unwrap(); + + #[allow(clippy::cast_possible_truncation)] + let stop_line_heading = geo_alg::cartesian_bearing(second_point, stop_line) as f32; + #[allow(clippy::cast_possible_truncation)] + let average_heading = geo_alg::cartesian_bearing(entry_point, stop_line) as f32; + + (stop_line_heading, average_heading) + } +} + +#[allow(dead_code)] +impl UsageType { + /// Determine possible usages of the lane + /// + /// Lanes may be used my multiple classes of traffic at once, so this needs to + /// return a vector of [`UsageType`]. + /// + /// # Errors + /// Returns display string of the [`LaneAttributes`], if no suitable lane class was found + pub fn get_usages(lane_attrs: &etsi_its_dsrc::LaneAttributes) -> Result, String> { + let mut result = alloc::vec![]; + if Self::is_vehicle_useable(lane_attrs) { + result.push(Self::Vehicle); + } + if Self::is_bus_useable(lane_attrs) { + result.push(Self::Bus); + } + if Self::is_tram_useable(lane_attrs) { + result.push(Self::Tram); + } + if Self::is_bike_useable(lane_attrs) { + result.push(Self::Bike); + } + if Self::is_pedestrian_useable(lane_attrs) { + result.push(Self::Pedestrian); + } + + if result.is_empty() { + Err(format!("Unsupported LaneAttributes: {lane_attrs}")) + } else { + Ok(result) + } + } + + /// See [`UsageType::Vehicle`] + fn is_vehicle_useable(lane_attrs: &etsi_its_dsrc::LaneAttributes) -> bool { + match &lane_attrs.lane_type { + etsi_its_dsrc::LaneTypeAttributes::vehicle(attrs) => { + !attrs.get_restricted_to_bus_use() + && !attrs.get_restricted_to_taxi_use() + && !attrs.get_restricted_from_public_use() + } + _ => false, + } + } + + /// See [`UsageType::Bus`] + fn is_bus_useable(lane_attrs: &etsi_its_dsrc::LaneAttributes) -> bool { + match &lane_attrs.lane_type { + etsi_its_dsrc::LaneTypeAttributes::vehicle(attrs) => { + attrs.get_restricted_to_bus_use() | lane_attrs.shared_with.get_bus_vehicle_traffic() + } + _ => false, + } + } + + /// See [`UsageType::Tram`] + fn is_tram_useable(lane_attrs: &etsi_its_dsrc::LaneAttributes) -> bool { + match &lane_attrs.lane_type { + etsi_its_dsrc::LaneTypeAttributes::trackedVehicle(_) => true, + etsi_its_dsrc::LaneTypeAttributes::vehicle(_) => { + lane_attrs.shared_with.get_tracked_vehicle_traffic() + } + _ => false, + } + } + + /// See [`UsageType::Bike`] + fn is_bike_useable(lane_attrs: &etsi_its_dsrc::LaneAttributes) -> bool { + match &lane_attrs.lane_type { + etsi_its_dsrc::LaneTypeAttributes::bikeLane(_) => true, + // etsi_its_dsrc::LaneTypeAttributes::sidewalk(attrs) => { + // attrs.get_bicycle_use_allowed() + // } + etsi_its_dsrc::LaneTypeAttributes::vehicle(_) => { + lane_attrs.shared_with.get_cyclist_vehicle_traffic() + } + _ => false, + } + } + + /// See [`UsageType::Pedestrian`] + fn is_pedestrian_useable(lane_attrs: &etsi_its_dsrc::LaneAttributes) -> bool { + match &lane_attrs.lane_type { + etsi_its_dsrc::LaneTypeAttributes::crosswalk(_) + | etsi_its_dsrc::LaneTypeAttributes::sidewalk(_) => true, + // etsi_its_dsrc::LaneTypeAttributes::vehicle(_) => { + // lane_attrs.shared_with.get_pedestrian_traffic() + // || lane_attrs.shared_with.get_pedestrians_traffic() + // } + _ => false, + } + } +} + +#[cfg(test)] +mod tests { + use c_its_parser::standards::dsrc_2_2_1::etsi_its_dsrc::IntersectionGeometry; + use c_its_parser::standards::mapem_2_2_1::mapem_pdu_descriptions; + + use super::*; + use crate::init_test_env_logger; + use crate::testdata::{ + MAPEM_HH_560, + MAPEM_HH_1328, + MAPEM_HH_2349_L31, + MAPEM_HH_2349_L32, + MAPEM_HH_2349_L33, + }; + + #[test] + fn map_simple() { + let etsi = decode_mapem(MAPEM_HH_1328); + + let layer_id = etsi.map.layer_id.map(|v| v.0); + if let Some(map) = &etsi.map.intersections { + for geom in &map.0 { + let intersection = Intersection::new(layer_id, geom); + + assert!(intersection.is_complete()); + assert_ne!(geo_types::Point::default(), intersection.center()); + + // validate that center position is reasonable + let ref_pos_dist = + geo_alg::haversine_dist(&intersection.center, &intersection.ref_point); + println!( + "Map ref {:?}, center {ref_pos_dist:.1} m away", + intersection.ref_point + ); + assert!(ref_pos_dist < 15.); // for MAP 1328 the reference point is ~12 meters from the intersection center + } + } else { + panic!("MAPEM is missing intersection data") + } + + // forge a layered mapem + let layer_id = Some(11); + if let Some(map) = &etsi.map.intersections { + for geom in &map.0 { + let intersection = Intersection::new(layer_id, geom); + + assert!(intersection.is_complete()); + assert_ne!(geo_types::Point::default(), intersection.center()); + } + } else { + panic!("MAPEM is missing intersection data") + } + + let layer_id = Some(22); + if let Some(map) = &etsi.map.intersections { + for geom in &map.0 { + let intersection = Intersection::new(layer_id, geom); + + assert!(!intersection.is_complete()); + assert_eq!(geo_types::Point::default(), intersection.center()); + } + } else { + panic!("MAPEM is missing intersection data") + } + } + + #[test] + fn map_layered() { + let etsi_layer31 = decode_mapem(MAPEM_HH_2349_L31); + let etsi_layer32 = decode_mapem(MAPEM_HH_2349_L32); + let etsi_layer33 = decode_mapem(MAPEM_HH_2349_L33); + + // add some layer + let (layer_id, geom) = extract_geom(&etsi_layer32); + let mut intersection = Intersection::new(layer_id, geom); + assert!(!intersection.is_complete()); + assert_eq!(geo_types::Point::default(), intersection.center()); + + // add a second layer + { + let num_lanes_before = intersection.lanes().len(); + + let (layer_id, geom) = extract_geom(&etsi_layer33); + intersection.add_layer(layer_id, geom); + assert!(!intersection.is_complete()); + assert_eq!(geo_types::Point::default(), intersection.center()); + + let num_lanes_after = intersection.lanes().len(); + assert!(num_lanes_before <= num_lanes_after); + } + + // add first layer again + { + let num_lanes_before = intersection.lanes().len(); + + let (layer_id, geom) = extract_geom(&etsi_layer32); + intersection.add_layer(layer_id, geom); + assert!(!intersection.is_complete()); + assert_eq!(geo_types::Point::default(), intersection.center()); + + let num_lanes_after = intersection.lanes().len(); + assert!(num_lanes_before == num_lanes_after); + } + + // add final layer + { + let num_lanes_before = intersection.lanes().len(); + + let (layer_id, geom) = extract_geom(&etsi_layer31); + intersection.add_layer(layer_id, geom); + assert!(intersection.is_complete()); + assert_ne!(geo_types::Point::default(), intersection.center()); + + let num_lanes_after = intersection.lanes().len(); + assert!(num_lanes_before <= num_lanes_after); + } + } + + #[test] + fn map_signal_groups() { + init_test_env_logger(); + + let etsi = decode_mapem(MAPEM_HH_1328); + let layer_id = etsi.map.layer_id.map(|v| v.0); + if let Some(map) = &etsi.map.intersections { + for geom in &map.0 { + let intersection = Intersection::new(layer_id, geom); + + assert!(intersection.is_complete()); + assert_eq!(2, intersection.signal_groups.len()); + } + } else { + panic!("MAPEM is missing intersection data") + } + + let etsi = decode_mapem(MAPEM_HH_560); + let layer_id = etsi.map.layer_id.map(|v| v.0); + if let Some(map) = &etsi.map.intersections { + for geom in &map.0 { + let intersection = Intersection::new(layer_id, geom); + + assert!(intersection.is_complete()); + assert_eq!(10, intersection.signal_groups.len()); + } + } else { + panic!("MAPEM is missing intersection data") + } + } + + fn decode_mapem(uper: &[u8]) -> Box { + if let c_its_parser::ItsMessage::Mapem { + geonetworking: _, + transport: _, + etsi, + } = c_its_parser::de::decode(uper, c_its_parser::Headers::None).unwrap() + { + etsi + } else { + panic!("Unexpected C-ITS message") + } + } + + fn extract_geom( + etsi: &Box, + ) -> (Option, &IntersectionGeometry) { + let layer_id = etsi.map.layer_id.as_ref().map(|v| v.0); + if let Some(map) = &etsi.map.intersections { + // just assume we only have one intersection per mapem + (layer_id, map.0.first().unwrap()) + } else { + panic!("MAPEM is missing intersection data") + } + } + + #[test] + fn maneuver_order() { + let left = Maneuvers::new(true, false, false); + let straight_and_left = Maneuvers::new(true, true, false); + let straight = Maneuvers::new(false, true, false); + let omni = Maneuvers::new(true, true, true); + let straight_and_right = Maneuvers::new(false, true, true); + let right = Maneuvers::new(false, false, true); + + let maneuvers = vec![ + right, + omni, + straight, + left, + straight_and_left, + straight_and_right, + ]; + + let mut sorted = maneuvers.clone(); + sorted.sort(); + + println!("Sorted maneuvers:"); + for manv in &sorted { + println!("- {manv}"); + } + + assert_eq!(left, sorted[0]); + assert_eq!(straight_and_left, sorted[1]); + assert_eq!(omni, sorted[2]); + assert_eq!(straight, sorted[3]); + assert_eq!(straight_and_right, sorted[4]); + assert_eq!(right, sorted[5]); + } +} diff --git a/src/applogic/v2x/mod.rs b/src/applogic/v2x/mod.rs new file mode 100644 index 0000000..8f027ce --- /dev/null +++ b/src/applogic/v2x/mod.rs @@ -0,0 +1,4 @@ +//! Types for V2X data handling + +#[cfg(feature = "spat")] +pub mod map; diff --git a/src/cache.rs b/src/cache.rs new file mode 100644 index 0000000..7cdb138 --- /dev/null +++ b/src/cache.rs @@ -0,0 +1,221 @@ +//! Data Cache +//! +//! Store items for some time + +use alloc::rc::Rc; +use alloc::vec::Vec; +use core::ops::Sub; + +pub trait Cachable +where + K: core::cmp::PartialEq, +{ + fn key(&self) -> K; +} + +struct CacheItem +where + T: Cachable, + K: core::cmp::PartialEq, +{ + timestamp: chrono::NaiveDateTime, + data: Rc>, + phantom: core::marker::PhantomData, // rust wants this +} + +impl core::fmt::Display for CacheItem +where + T: Cachable, + K: core::cmp::PartialEq + core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "CacheItem({}, {:?})", + self.data.borrow().key(), + self.timestamp + ) + } +} + +impl CacheItem +where + T: Cachable, + K: core::cmp::PartialEq, +{ + fn new(time_now: chrono::NaiveDateTime, data: T) -> Self { + Self { + timestamp: time_now, + data: Rc::new(core::cell::RefCell::new(data)), + phantom: core::marker::PhantomData, + } + } +} + +#[derive(Debug, Clone)] +pub enum CacheError +where + K: core::fmt::Display, +{ + KeyNotFound(K), + InsertKeyAlreadyPresent(K), +} + +impl core::fmt::Display for CacheError +where + K: core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CacheError::KeyNotFound(key) => write!(f, "Key '{key}' doesn't exist in cache"), + CacheError::InsertKeyAlreadyPresent(key) => { + write!(f, "Insert failed b/c key '{key}' already exists in cache") + } + } + } +} + +/// Item cache +/// +/// Stores items for a certain time. +/// Items need to implement `Cachable` so that an itentifier (key) can be retreived from them. +pub struct Cache +where + T: Cachable, + K: core::cmp::PartialEq, +{ + lifetime: chrono::Duration, + items: Vec>, +} + +impl core::fmt::Display for Cache +where + T: Cachable, + K: core::cmp::PartialEq + core::fmt::Display, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "Cache({}, {} items)", self.lifetime, self.items.len()) + } +} + +#[allow(unused)] +impl Cache +where + T: Cachable + Clone, + K: core::cmp::PartialEq + core::fmt::Display, +{ + /// Creates a new cache with a certain item lifetime + pub fn new(lifetime: chrono::Duration) -> Self { + Self { + lifetime, + items: alloc::vec![], + } + } + + /// Determines if a key is in the cache + pub fn contains(&mut self, key: &K) -> bool { + self.items + .iter() + .find(|i| i.data.borrow().key() == *key) + .is_some() + } + + /// Retrieves all known keys + pub fn keys(&self) -> Vec { + self.items.iter().map(|i| i.data.borrow().key()).collect() + } + + /// Adds a new item to the cache + /// + /// Call [`Self::update`] to update the contents of the cached item + /// or just to bump the timestamp. + /// + /// # Errors + /// Fails if the specified key already exist + pub fn insert( + &mut self, + time_now: chrono::NaiveDateTime, + data: T, + ) -> Result<(), CacheError> { + let key = data.key(); + if self.contains(&key) { + Err(CacheError::InsertKeyAlreadyPresent(key)) + } else { + self.items.push(CacheItem::new(time_now, data)); + Ok(()) + } + } + + /// Retrieves an item from the cache + /// + /// Returns `None` if no item exists for this key + pub fn get(&mut self, key: &K) -> Option>> { + self.items.iter().find_map(|i| { + if i.data.borrow().key() == *key { + Some(i.data.clone()) + } else { + None + } + }) + } + + /// Retrieves items from the cache which match the predicate + pub fn get_filtered

(&self, predicate: P) -> Vec>> + where + P: Fn(&T) -> bool, + { + self.items + .iter() + .filter_map(|i| { + let data = i.data.borrow(); + if predicate(&data) { + Some(i.data.clone()) + } else { + None + } + }) + .collect() + } + + /// Updates an item in the cache using the provided function + /// + /// This can also be used to just bump the timestamp of the cached item by providing an empty closure. + /// + /// # Errors + /// Fails if the specified key does not exist in the cache + pub fn update( + &mut self, + time_now: chrono::NaiveDateTime, + key: K, + func: F, + ) -> Result<(), CacheError> + where + F: Fn(&mut T), + K: core::fmt::Display, + { + // find item + let Some(pos) = self.items.iter().position(|i| i.data.borrow().key() == key) else { + return Err(CacheError::KeyNotFound(key)); + }; + + // unwrap is fine since we fetched the position before + let item = self.items.get_mut(pos).unwrap(); + + let mut data = item.data.borrow_mut(); + func(&mut data); + + item.timestamp = time_now; + + Ok(()) + } + + /// Cleans items which timed out from the cache + /// + /// Call this regularly to prune old items. + pub fn prune(&mut self, time_now: chrono::NaiveDateTime) { + self.items.retain(|i| { + let age = time_now.sub(i.timestamp); + age < self.lifetime + }); + } +} diff --git a/src/geo_alg.rs b/src/geo_alg.rs new file mode 100644 index 0000000..12efd8f --- /dev/null +++ b/src/geo_alg.rs @@ -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 { + 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); + } + } + } + } +} diff --git a/src/main.rs b/src/main.rs index 807561e..79a3024 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,65 +23,13 @@ use log::{debug, error, info, warn}; extern crate alloc; mod applogic; +#[cfg(feature = "spat")] +mod cache; +mod geo_alg; mod radio; const WIFI_CHANNEL: radio::Channel = 180; -#[derive(Debug, Default)] -struct State { - initialized: bool, - time: chrono::NaiveDateTime, - - lat_deg: f32, - lon_deg: f32, - heading_deg: Option, - speed_mps: Option, -} - -impl State { - fn update_with_gpsfix(&mut self, fix: &embassy_gps::types::GpsFix) { - #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] - let lat_deg = fix.latitude_deg as f32; - #[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation)] - let lon_deg = fix.longitude_deg as f32; - - self.lat_deg = lat_deg; - self.lon_deg = lon_deg; - - if let Some(heading) = fix.true_course_deg { - self.heading_deg = Some(heading); - } else { - warn!("State: Course missing from GpsFix"); - } - - if let Some(speed) = fix.speed_over_ground { - self.speed_mps = Some(speed); - } else { - warn!("State: Speed missing from GpsFix"); - } - - if let Some(time) = fix.get_timestamp() { - self.time = time; - - if !self.initialized { - info!("GNSS fix: {}", self.print_fix()); - } - self.initialized = true; - } - } - - fn print_fix(&self) -> alloc::string::String { - alloc::format!( - "{:?}, {:.4} N, {:.4} E, {:.0}°, {:.2} m/s", - self.time, - self.lat_deg, - self.lon_deg, - self.heading_deg.unwrap_or_default(), - self.speed_mps.unwrap_or_default(), - ) - } -} - // This creates a default app-descriptor required by the esp-idf bootloader. // For more information see: esp_bootloader_esp_idf::esp_app_desc!(); @@ -157,7 +105,7 @@ async fn main(spawner: Spawner) -> ! { ); info!("GNSS task started, waiting for GNSS fix..."); - let mut state = State::default(); + let mut state = applogic::State::new(); loop { embassy_time::Timer::after(embassy_time::Duration::from_millis(5)).await; @@ -167,9 +115,12 @@ async fn main(spawner: Spawner) -> ! { if let Some(fix) = gnss_update_ref.replace(None) { state.update_with_gpsfix(&fix); - if let Some(time) = fix.get_timestamp() { - state.time = time; - info!("{}", state.print_fix()); // TODO: degrate to debug later? + if fix.get_timestamp().is_some() { + info!("{}", state.print_fix()); + + // run GLOSA algorithm + #[cfg(feature = "spat")] + state.run_glosa(); } } }); @@ -193,15 +144,15 @@ async fn main(spawner: Spawner) -> ! { geonetworking: _, transport: _, etsi, - } => applogic::tlc::handle_mapem(&etsi), + } => state.handle_mapem(&etsi), #[cfg(feature = "spat")] ItsMessage::Spatem { geonetworking: _, transport: _, etsi, } => { - if state.initialized { - applogic::tlc::handle_spatem(&etsi, &state.time); + if state.initialized() { + state.handle_spatem(&etsi); } } @@ -235,6 +186,9 @@ async fn main(spawner: Spawner) -> ! { } } }); + + // maintenance + state.prune(); } } diff --git a/src/test.rs b/src/test.rs index 0fb9fac..3eb42dc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,10 +1,23 @@ #![cfg(not(target_arch = "riscv32"))] +#![allow(unused)] extern crate alloc; mod applogic; mod cache; +mod geo_alg; +mod testdata; fn main() { + // env_logger::init(); // dummy only } + +/// Initializes the env-logger to be used inside tests +fn init_test_env_logger() { + let _ = env_logger::builder() + .format_timestamp(None) + .filter_level(log::LevelFilter::Debug) + .is_test(true) + .try_init(); +} diff --git a/src/testdata.rs b/src/testdata.rs new file mode 100644 index 0000000..567410d --- /dev/null +++ b/src/testdata.rs @@ -0,0 +1,457 @@ +//! Central provider for V2X test messages +//! +//! All messages are without any headers (so they start with the ItsPduHeader) + +#![cfg(all(not(target_arch = "riscv32"), any(feature = "spat", feature = "cam")))] +#![allow(unused)] + +/// Intersection "Sievekingplatz-Südfahrbahn/Ziviljustizgebäude" +/// +/// Pedestrian crossing over a one-way street (going from west to east) +/// +/// Possible start point (heading 90° to 135°): +/// - in front: `geo_types::Point::new(9.9752864, 53.5564028)` +/// - at lane: `geo_types::Point::new(9.9760764, 53.5563621)` +/// - passed: `geo_types::Point::new(9.977217, 53.5560602)` +#[cfg(feature = "spat")] +pub const MAPEM_HH_1328: &[u8] = &[ + 0x2, 0x5, 0x0, 0x12, 0x19, 0x8c, 0x8, 0x0, 0x3, 0x9, 0xcd, 0x83, 0x42, 0xfc, 0x9a, 0x94, 0xef, + 0xb1, 0x67, 0x71, 0x8b, 0x36, 0x4e, 0x2e, 0x31, 0x60, 0xb9, 0xa4, 0x0, 0xc, 0x14, 0xc0, 0x20, + 0xab, 0x21, 0xda, 0x5c, 0xe2, 0x78, 0x72, 0x74, 0x5, 0x14, 0x18, 0x90, 0x4, 0x50, 0x80, 0x0, + 0x0, 0x85, 0xc, 0x5c, 0xa8, 0xa0, 0x0, 0x80, 0x23, 0xc1, 0x42, 0x7e, 0xa8, 0x58, 0x46, 0x1d, + 0xa0, 0x11, 0xef, 0x4d, 0xf0, 0x45, 0xa5, 0x56, 0x8, 0x2d, 0xa6, 0x2, 0x30, 0xc9, 0x26, 0x11, + 0xb7, 0xc5, 0x90, 0x84, 0xca, 0x16, 0xc1, 0x60, 0x70, 0x0, 0x2, 0x2, 0x20, 0x6, 0x44, 0x40, + 0x0, 0x0, 0x0, 0x8f, 0x5, 0xf8, 0x83, 0xed, 0x8c, 0x19, 0x20, 0x10, 0xa1, 0x0, 0x0, 0x1, 0xa, + 0x23, 0x2a, 0x63, 0x40, 0x1, 0x9, 0xb9, 0xa7, 0x40, 0x25, 0xec, 0x21, 0x17, 0xad, 0x3c, 0x8, + 0xef, 0x66, 0xfc, 0x47, 0xa1, 0x2e, 0x82, 0x39, 0x99, 0x2b, 0x11, 0xd2, 0xc7, 0x8, 0x8f, 0x32, + 0x18, 0x4, 0x70, 0x50, 0x8c, 0x2b, 0x5, 0x80, 0x0, 0x10, 0x25, 0x82, 0x40, 0x0, 0x8, 0x18, + 0x80, 0x29, 0x11, 0x0, 0x0, 0x0, 0x1, 0x17, 0x14, 0x80, 0x3f, 0x70, 0xf8, 0x88, 0x2, 0x11, + 0x10, 0x0, 0x0, 0x0, 0x11, 0x23, 0x38, 0x83, 0xe5, 0x8c, 0x90, 0x20, 0x33, 0x2, 0x4, 0x0, 0x0, + 0x1, 0x43, 0xd6, 0xe2, 0x8, 0x0, 0x20, 0x72, 0x5a, 0xd0, 0x50, 0x38, 0x10, 0x20, 0x20, 0x3b, + 0x2, 0x4, 0x0, 0x0, 0x1, 0x20, 0x23, 0x20, 0xa0, 0x0, 0x82, 0x37, 0x94, 0xc1, 0x40, 0xc0, 0x40, + 0xa0, +]; + +/// Intersection "Johannisbollwerk/Ditmar-Koel-Straße" +/// +/// Has a very complex geometry with multiple stop lines after each other. +/// Main traffic direction is east-west, has separate bike lanes. +/// +/// Possible start point (heading 90°): TODO +#[cfg(feature = "spat")] +pub const MAPEM_HH_2053: &[u8] = &[ + 0x2, 0x5, 0x0, 0x12, 0x43, 0x84, 0x8, 0x0, 0x3, 0x9, 0xcd, 0x83, 0x42, 0xfc, 0x9a, 0x94, 0xef, + 0xb2, 0x61, 0x71, 0x93, 0x6, 0xac, 0xee, 0x31, 0x66, 0xb9, 0x94, 0x0, 0xc, 0x20, 0x14, 0x10, + 0xab, 0x1e, 0x9c, 0x96, 0xe2, 0x77, 0x51, 0x1e, 0x5, 0x14, 0xc8, 0x90, 0x1c, 0xd0, 0x88, 0x0, + 0x0, 0x15, 0x8f, 0x5d, 0xb6, 0x88, 0x0, 0x20, 0xc, 0x8e, 0xb0, 0x1a, 0x4a, 0xb8, 0x2c, 0xa, + 0x0, 0x0, 0x40, 0x44, 0x0, 0x84, 0x88, 0x80, 0x0, 0x1, 0x9, 0xdd, 0xc0, 0x41, 0xdd, 0x54, 0xb0, + 0x59, 0xe9, 0x39, 0x4, 0x80, 0x64, 0x84, 0x0, 0x0, 0x0, 0xaa, 0x39, 0xc9, 0x5d, 0x0, 0x4, 0x5c, + 0x8c, 0x91, 0x84, 0xd8, 0x75, 0x3d, 0x21, 0x58, 0x34, 0x0, 0x3, 0x1, 0x2c, 0x8, 0x80, 0x0, + 0x81, 0x4, 0x1, 0x90, 0x88, 0x0, 0x0, 0x1, 0x18, 0xe6, 0x61, 0xa8, 0x13, 0xaf, 0x1d, 0xc0, + 0x98, 0xc4, 0x44, 0x80, 0x84, 0x84, 0x0, 0x0, 0x0, 0xaa, 0x11, 0x44, 0xe9, 0x0, 0x4, 0x5c, + 0x8c, 0x91, 0x84, 0xd8, 0xdd, 0x3c, 0x50, 0x58, 0x2c, 0x0, 0x3, 0x1, 0x88, 0x2, 0xa1, 0x10, + 0x0, 0x0, 0x2, 0x31, 0xa3, 0x3e, 0x34, 0x27, 0x7a, 0x38, 0x81, 0x38, 0x8a, 0xc9, 0x0, 0x45, + 0x8, 0x0, 0x0, 0x1, 0x48, 0x1a, 0xd6, 0x68, 0x0, 0x22, 0xcf, 0xe9, 0xc7, 0x1e, 0x92, 0xe7, + 0x46, 0x2b, 0x6, 0x80, 0x0, 0x30, 0x55, 0x82, 0xc0, 0x0, 0x18, 0x31, 0x21, 0x34, 0x21, 0x0, + 0x0, 0x0, 0x2b, 0x4c, 0x89, 0xc, 0x90, 0x0, 0x44, 0x3d, 0xb5, 0x5a, 0x12, 0x39, 0x48, 0xc1, + 0x65, 0x10, 0x0, 0x26, 0xe, 0x20, 0x50, 0xe4, 0x40, 0x0, 0x0, 0x8, 0xda, 0x2, 0x25, 0x30, 0xe3, + 0x85, 0x9c, 0x1e, 0x1e, 0x92, 0x89, 0x9, 0x61, 0x8, 0x0, 0x0, 0x1, 0x5a, 0x3e, 0x86, 0x18, + 0x80, 0x2, 0x21, 0xcb, 0xaa, 0x80, 0x94, 0x8a, 0x1c, 0xb, 0x29, 0x80, 0x1, 0x30, 0x81, 0x2, + 0x97, 0x22, 0x0, 0x0, 0x0, 0x46, 0xc5, 0xb0, 0x9a, 0x7, 0xc, 0x2d, 0xe0, 0xf3, 0x54, 0x88, + 0x48, 0x4f, 0x8, 0x4, 0x20, 0x0, 0x0, 0x15, 0xa2, 0x28, 0x44, 0x88, 0x80, 0x2b, 0x50, 0x87, + 0x5e, 0xa8, 0xc2, 0x1f, 0xaa, 0x0, 0x2c, 0xaa, 0x0, 0x4, 0xc2, 0x44, 0xa, 0x9c, 0x80, 0x84, + 0x0, 0x0, 0x2, 0xb5, 0xec, 0x1, 0x40, 0x11, 0x6a, 0xe, 0x75, 0x55, 0xc1, 0xdb, 0x69, 0xc4, + 0x90, 0x7d, 0x90, 0x8, 0x40, 0x0, 0x0, 0x6d, 0x5e, 0x40, 0x79, 0xb, 0x44, 0x1, 0x54, 0x46, + 0x7e, 0x8, 0x37, 0x88, 0x6f, 0x23, 0xd0, 0x44, 0x91, 0x34, 0x45, 0x5f, 0x6f, 0x8e, 0x65, 0x5, + 0x90, 0x40, 0x0, 0x48, 0x50, 0x81, 0x2, 0x90, 0x10, 0x80, 0x0, 0x0, 0x58, 0x59, 0x12, 0xe1, + 0x80, 0x8a, 0x88, 0x37, 0x26, 0x78, 0x1c, 0xf3, 0x20, 0x48, 0x2c, 0xc8, 0x40, 0x0, 0x0, 0x1b, + 0x57, 0x80, 0x1e, 0x86, 0x10, 0x0, 0x46, 0x70, 0x58, 0x42, 0x88, 0xa4, 0x23, 0xcc, 0x44, 0x4f, + 0x38, 0x44, 0x0, 0xae, 0xe2, 0x21, 0x58, 0xa4, 0x0, 0x4, 0x85, 0xac, 0x90, 0x80, 0x4, 0x83, + 0x84, 0x5, 0x14, 0x88, 0x0, 0x0, 0x1, 0x21, 0x96, 0x6b, 0xd8, 0x3, 0x6f, 0x67, 0xc1, 0xaa, + 0x35, 0x24, 0x82, 0xec, 0x84, 0x0, 0x0, 0x1, 0xb5, 0x77, 0xb1, 0xed, 0xd9, 0x0, 0x4, 0x67, + 0x6c, 0x83, 0xd0, 0x8b, 0x2a, 0x32, 0x44, 0x1c, 0x54, 0x6a, 0x40, 0x5c, 0xad, 0xb3, 0x15, 0x8a, + 0xc0, 0x0, 0x48, 0x62, 0xcb, 0xd0, 0x0, 0x44, 0x34, 0x40, 0x55, 0x48, 0x80, 0x0, 0x0, 0x12, + 0x1b, 0xf0, 0xc6, 0x68, 0x2d, 0xd7, 0x8, 0x19, 0xe3, 0x4c, 0x20, 0x5f, 0x44, 0x44, 0x0, 0x0, + 0x9, 0xf, 0x23, 0x6c, 0x98, 0x15, 0xac, 0xb6, 0xa, 0x62, 0x52, 0x10, 0x24, 0x92, 0x20, 0x0, + 0x0, 0x4, 0x84, 0x6b, 0x2a, 0xd6, 0x1c, 0x8a, 0xcf, 0x3, 0x90, 0xdc, 0xc9, 0x21, 0x1c, 0xa0, + 0x10, 0x80, 0x0, 0x0, 0x58, 0x4a, 0x82, 0xe0, 0xc8, 0x80, 0x2b, 0x50, 0x19, 0x7, 0x6c, 0x8, + 0x4, 0x82, 0xb, 0x2f, 0x20, 0x1, 0x60, 0xf2, 0x41, 0x25, 0x42, 0x0, 0x0, 0x1, 0x98, 0x22, 0xb3, + 0x96, 0xe8, 0x0, 0x20, 0x80, 0xac, 0x80, 0x73, 0x37, 0x90, 0x75, 0x54, 0xbe, 0xf, 0xdd, 0x81, + 0x60, 0xb2, 0xe0, 0x20, 0x6c, 0x59, 0x42, 0x2c, 0xb7, 0xd5, 0x0, 0x10, 0x96, 0x28, 0x20, 0x1, + 0x42, 0xb, 0x2f, 0x20, 0x0, 0xb1, 0x15, 0x8a, 0x88, 0x0, 0x50, 0xa9, 0x20, 0x9a, 0xa0, 0x10, + 0x80, 0x0, 0x0, 0xd8, 0x5, 0xcc, 0x2e, 0xe8, 0x80, 0x2a, 0x88, 0x75, 0xc4, 0x84, 0xf, 0x81, + 0x92, 0xc1, 0xf5, 0x11, 0x1c, 0x59, 0x84, 0xf8, 0x40, 0xb1, 0xa8, 0x0, 0xc, 0x12, 0x10, 0x1a, + 0x62, 0x2, 0x10, 0x0, 0x0, 0xb, 0x57, 0x61, 0xa0, 0x42, 0x1, 0x15, 0x11, 0x1e, 0x53, 0xdb, + 0x88, 0xc7, 0x21, 0xa0, 0x90, 0x41, 0x50, 0x80, 0x0, 0x0, 0x36, 0x1, 0xc7, 0x3, 0xea, 0x0, 0x8, + 0x75, 0x7c, 0x79, 0xf, 0xfb, 0x95, 0xe1, 0xef, 0x70, 0xd0, 0x59, 0xd6, 0xf5, 0xe0, 0xb1, 0x98, + 0x0, 0xc, 0x13, 0x10, 0x19, 0x62, 0x20, 0x0, 0x0, 0x4, 0xab, 0xb4, 0x4f, 0xff, 0x82, 0x3b, + 0x97, 0xb6, 0x11, 0x77, 0xc1, 0x99, 0x20, 0x8a, 0xa1, 0x0, 0x0, 0x0, 0x8c, 0x4, 0x11, 0xf4, + 0xc4, 0x0, 0x10, 0xec, 0x88, 0xfe, 0x2c, 0x2c, 0x89, 0xe0, 0xf0, 0x38, 0x5e, 0xac, 0x74, 0x7c, + 0x24, 0x0, 0x40, 0x4f, 0x6f, 0xc8, 0x2c, 0x62, 0x0, 0x3, 0x5, 0x4, 0x6, 0x18, 0x88, 0x0, 0x0, + 0x1, 0x2a, 0xed, 0xc3, 0xf5, 0xc0, 0x8e, 0xa1, 0xed, 0x84, 0x5b, 0xd0, 0x66, 0x8, 0x10, 0xc0, + 0x81, 0x0, 0x0, 0x0, 0x53, 0x6a, 0x8a, 0x3a, 0x0, 0x8, 0x1d, 0x86, 0x94, 0x14, 0x12, 0xe, 0x2c, + 0x8, 0x12, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x53, 0xeb, 0x9c, 0x8a, 0x0, 0x8, 0x22, 0x89, 0x6c, 0x14, + 0x10, 0xe, 0x2e, 0x8, 0x14, 0x80, 0x42, 0x0, 0x0, 0x0, 0x52, 0xdd, 0x8e, 0xf2, 0x0, 0x8, 0x20, + 0xd6, 0x74, 0x14, 0x16, 0x10, 0x30, 0x0, 0x16, 0x40, 0x42, 0x0, 0x0, 0x0, 0x49, 0x4e, 0x21, + 0x48, 0x0, 0x20, 0x7c, 0xe6, 0x30, 0x40, 0xc6, 0x4, 0x8, 0x0, 0x0, 0x2, 0x82, 0xce, 0x9a, 0x10, + 0x0, 0x40, 0xd4, 0x45, 0xe0, 0xa0, 0xd0, 0x41, 0x90, 0x40, 0xd6, 0x4, 0x8, 0x0, 0x0, 0x2, 0x46, + 0x74, 0x3c, 0x40, 0x1, 0x4, 0xb0, 0xe8, 0x82, 0x83, 0x1, 0x6, 0x81, 0x3, 0x90, 0x8, 0x40, 0x0, + 0x0, 0x9, 0xc, 0xcb, 0xe9, 0x0, 0x4, 0x12, 0xd3, 0xaa, 0xa, 0xf, 0x5, 0x1b, 0x0, 0xf, 0x20, + 0x21, 0x0, 0x0, 0x0, 0x2c, 0x7b, 0xf1, 0xcf, 0x40, 0x1, 0x3, 0x3b, 0x7, 0x2, 0xd, 0xa0, 0x20, + 0x40, 0x0, 0x0, 0x1a, 0xbd, 0xaf, 0x2, 0xfa, 0x80, 0x2, 0x7, 0xca, 0x63, 0x5, 0xe, 0x6, 0x8e, + 0x0, 0xe, 0x10, 0x20, 0x40, 0x0, 0x0, 0x1a, 0xbe, 0x96, 0xfc, 0xb4, 0x80, 0x2, 0x8, 0x39, 0x9d, + 0x4, 0x1d, 0x40, 0x40, 0x80, 0x0, 0x0, 0x35, 0x7e, 0x35, 0xf0, 0x9d, 0x0, 0x4, 0xf, 0x8c, 0xc6, + 0xa, 0x1e, 0xe, 0x1d, 0x0, 0x1e, 0x20, 0x40, 0x80, 0x0, 0x0, 0x30, 0x1, 0x86, 0x20, 0x50, 0x0, + 0x41, 0x7, 0xb3, 0xa0, 0x84, 0x2c, 0x8, 0x10, 0x0, 0x0, 0x6, 0x9, 0xca, 0xb9, 0xea, 0x0, 0x8, + 0x19, 0xd8, 0x30, 0x14, 0x44, 0x2e, 0x3c, 0x8, 0x44, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x60, 0xfe, + 0xcb, 0x6e, 0xa0, 0x0, 0x82, 0x63, 0x7d, 0x1, 0x44, 0x22, 0xe3, 0xe0, 0x85, 0x6c, 0x8, 0x10, + 0x0, 0x0, 0x5, 0xaf, 0x58, 0x78, 0xc8, 0x0, 0x20, 0x8b, 0x65, 0x90, 0x51, 0x60, 0xa1, 0x0, + 0x21, 0x63, 0x2, 0x4, 0x0, 0x0, 0x1, 0x6a, 0x5a, 0x6, 0xe2, 0x0, 0x8, 0x1d, 0x36, 0x9c, 0x14, + 0x56, 0x28, 0x42, 0x8, 0x5a, 0xc0, 0x42, 0x0, 0x0, 0x0, 0x5a, 0x4c, 0xc2, 0xc6, 0x80, 0x2, 0x7, + 0x45, 0xa8, 0x5, 0x17, 0xa, 0x91, 0x2, 0x17, 0x30, 0x10, 0x80, 0x0, 0x0, 0x16, 0xac, 0x12, + 0x2b, 0x20, 0x0, 0x82, 0x2f, 0x96, 0x1, 0x45, 0xa2, 0xa4, 0x60, 0x86, 0x8, 0x8, 0x10, 0x0, 0x0, + 0x6, 0x0, 0x3c, 0xc4, 0x5a, 0x0, 0x8, 0x20, 0xe6, 0x74, 0x14, 0x62, 0x1e, 0x48, 0x0, 0x62, + 0x40, 0x81, 0x0, 0x0, 0x0, 0x6a, 0xfc, 0xd3, 0xe1, 0x5a, 0x0, 0x8, 0x1f, 0x29, 0x8c, 0x10, + 0xc9, 0x1, 0x2, 0x0, 0x0, 0x0, 0xd5, 0xf4, 0xa7, 0xe6, 0xa4, 0x0, 0x10, 0x41, 0xac, 0xe8, 0x28, + 0xcc, 0x40, 0x94, 0x0, 0xcc, 0x81, 0x2, 0x0, 0x0, 0x0, 0xd5, 0xed, 0xe8, 0x18, 0xd4, 0x0, 0x10, + 0x3e, 0x73, 0x18, +]; + +/// Intersection "Feldstraße/Glacischaussee" +/// +/// Has a very complex geometry with multiple stop lines after each other. +/// Main traffic direction is east-west, has separate bike lanes. +/// +/// Possible start point (heading 90°): TODO +#[cfg(feature = "spat")] +pub const MAPEM_HH_560: &[u8] = &[ + 0x2, 0x5, 0x0, 0x1, 0x61, 0x2b, 0x8, 0x0, 0x3, 0x9, 0x4d, 0x83, 0x42, 0xfc, 0x9a, 0x94, 0xef, + 0xb0, 0x6b, 0x7d, 0xab, 0x66, 0x17, 0xd8, 0xb3, 0x5c, 0xda, 0x0, 0x6, 0x4, 0x60, 0x18, 0x55, + 0x91, 0x1, 0x73, 0x71, 0x3b, 0xe2, 0xcb, 0x2, 0x58, 0x56, 0x48, 0x1c, 0x28, 0x44, 0x0, 0x0, + 0x32, 0xbb, 0xbd, 0x9a, 0x91, 0x0, 0x61, 0x90, 0x6e, 0x15, 0x40, 0x3c, 0x5c, 0xac, 0x39, 0x63, + 0x28, 0x8e, 0xcc, 0x98, 0x51, 0xe, 0x55, 0xe1, 0x2, 0xd0, 0x3d, 0xd2, 0xe, 0x4, 0xe8, 0x22, + 0xb0, 0xa2, 0x0, 0x1, 0x7, 0x58, 0x49, 0x0, 0x0, 0x84, 0x8, 0x5, 0x21, 0x11, 0x0, 0x0, 0x2, + 0x21, 0x1f, 0x51, 0xf0, 0x9e, 0xd9, 0x70, 0x12, 0xab, 0x42, 0xc4, 0x2, 0x50, 0x8c, 0x80, 0x0, + 0x1, 0x10, 0x5e, 0xa0, 0x20, 0x4f, 0x4c, 0xba, 0x9, 0x52, 0xa2, 0xe4, 0x81, 0x8, 0x84, 0x40, + 0x0, 0x2, 0xad, 0x36, 0xa3, 0x6c, 0x40, 0x1, 0x2, 0x93, 0x4b, 0x4, 0x38, 0xd7, 0x80, 0x2, 0x16, + 0x50, 0x1, 0xff, 0x2c, 0x0, 0x2f, 0xd6, 0x13, 0xb, 0x6f, 0x1, 0x60, 0x84, 0x0, 0x82, 0x2, 0x20, + 0x8, 0x64, 0x44, 0x0, 0x0, 0x9, 0x1b, 0x9, 0x2c, 0xd0, 0x4, 0xf0, 0x82, 0x25, 0x1c, 0x3f, 0xd2, + 0x40, 0xd1, 0x42, 0x20, 0x0, 0x1, 0xd7, 0xa0, 0xe6, 0xac, 0xa2, 0x0, 0xc3, 0x21, 0xc1, 0xfa, + 0xd8, 0x58, 0xf5, 0xb9, 0x2, 0xc1, 0x7d, 0x4e, 0xe, 0x68, 0xf1, 0x3, 0xa9, 0x26, 0x13, 0x81, + 0x8c, 0xef, 0x60, 0xdb, 0x75, 0xa0, 0xea, 0xdd, 0xa4, 0xb, 0x4, 0x80, 0x0, 0x20, 0x22, 0x40, + 0xb1, 0x42, 0x20, 0x0, 0x1, 0x97, 0xce, 0x86, 0x7c, 0x22, 0x0, 0xc3, 0x20, 0xcd, 0x78, 0x81, + 0x63, 0xde, 0xe5, 0xb, 0x8, 0xf5, 0x74, 0x39, 0xef, 0xd1, 0x8b, 0x2f, 0x7e, 0x90, 0x55, 0x57, + 0x82, 0x41, 0xca, 0x7d, 0xdc, 0x16, 0x22, 0x80, 0x0, 0x61, 0xc2, 0x2, 0x24, 0x44, 0x40, 0x0, + 0x0, 0xb3, 0x4b, 0x65, 0x8e, 0x81, 0x21, 0x90, 0xe0, 0x57, 0xc6, 0xb, 0xe9, 0xef, 0x24, 0xc, + 0x14, 0x22, 0x0, 0x0, 0x19, 0x7b, 0x78, 0x69, 0x4a, 0x20, 0xc, 0x32, 0x1b, 0x4b, 0x87, 0x85, + 0x8f, 0x7b, 0x92, 0x2c, 0x1d, 0xd5, 0x50, 0xe7, 0x2f, 0x2a, 0x2c, 0xbb, 0xfa, 0x11, 0x64, 0x6f, + 0x83, 0x2, 0x81, 0xb1, 0xc1, 0x62, 0x48, 0x0, 0x6, 0x1e, 0x20, 0x24, 0x44, 0x44, 0x0, 0x0, 0xb, + 0x34, 0xa5, 0x54, 0x3c, 0x12, 0x19, 0x7, 0xee, 0xf2, 0x2, 0x99, 0x7b, 0xc9, 0x1, 0x51, 0x8, + 0x80, 0x0, 0x7, 0x51, 0x5d, 0x86, 0x5a, 0x20, 0xc, 0x32, 0x0, 0x62, 0x8c, 0x11, 0xe2, 0x4c, + 0xd0, 0x42, 0x65, 0xb3, 0x9, 0x3a, 0xaa, 0xc1, 0xa, 0xb8, 0x18, 0x26, 0x2, 0xb0, 0x88, 0xc6, + 0x67, 0xb8, 0x23, 0x96, 0x95, 0x82, 0xc3, 0xd0, 0x0, 0x10, 0x24, 0x40, 0x3c, 0x48, 0x88, 0x0, + 0x0, 0x11, 0xfc, 0x8d, 0xab, 0xa0, 0xcd, 0x78, 0x81, 0x5f, 0x56, 0x9b, 0x12, 0x3, 0x22, 0x11, + 0x0, 0x0, 0x14, 0xb7, 0x45, 0x19, 0x21, 0x0, 0x4, 0x44, 0x19, 0x3b, 0xa2, 0x3b, 0x49, 0xa0, + 0x8, 0x40, 0xb7, 0x1, 0x26, 0x15, 0x6c, 0x47, 0x6f, 0x49, 0x42, 0x3d, 0xba, 0x89, 0x11, 0x52, + 0x56, 0x78, 0xce, 0xf9, 0x3c, 0xc1, 0xb, 0x1d, 0x3a, 0x88, 0x77, 0xa8, 0x3c, 0x44, 0xc5, 0x39, + 0x80, 0xb1, 0x18, 0x0, 0x5, 0xa, 0x24, 0x7, 0x44, 0x22, 0x0, 0x0, 0x29, 0x6d, 0xa7, 0x28, 0xda, + 0x0, 0x8, 0x89, 0xf6, 0x6a, 0x44, 0x71, 0x53, 0x50, 0x10, 0xc3, 0x6d, 0x82, 0x46, 0x2c, 0xa8, + 0x8e, 0xa2, 0x94, 0x84, 0x7a, 0x94, 0xca, 0x22, 0xbf, 0xae, 0x81, 0x9e, 0x3e, 0x78, 0x82, 0x17, + 0x2a, 0x6f, 0x10, 0xf2, 0xd0, 0x60, 0x89, 0x96, 0x72, 0xc1, 0x62, 0x50, 0x0, 0xa, 0x16, 0x48, + 0x6, 0x68, 0x44, 0x0, 0x0, 0x33, 0x24, 0x13, 0x3d, 0xf1, 0x10, 0x6, 0x19, 0x1, 0x56, 0x4a, 0x4, + 0x84, 0xc, 0x80, 0x99, 0x12, 0x30, 0x5, 0x81, 0x2c, 0x9, 0x6, 0x1, 0xc2, 0x15, 0xd0, 0xa5, 0x9, + 0x18, 0x0, 0x22, 0xb1, 0x22, 0x0, 0x6, 0x3, 0x58, 0x89, 0x0, 0x3, 0x2, 0x12, 0x0, 0x9a, 0x11, + 0x0, 0x0, 0xe, 0xc8, 0x3b, 0x8f, 0xac, 0x44, 0x1, 0x86, 0x42, 0x88, 0xf, 0xb0, 0x51, 0xa1, + 0xff, 0x9, 0x20, 0xc, 0x41, 0x26, 0x60, 0xd8, 0x28, 0x4c, 0x97, 0x5, 0x39, 0x1b, 0x91, 0x2d, + 0xc1, 0x75, 0x5, 0x30, 0x9c, 0x50, 0x58, 0x4a, 0x0, 0x3, 0x82, 0x92, 0x1, 0x1a, 0x11, 0x0, 0x0, + 0xc, 0xc8, 0x9b, 0x4f, 0x8f, 0x44, 0x1, 0x86, 0x42, 0x5f, 0x8a, 0x48, 0x3, 0xb0, 0x1c, 0x24, + 0xa8, 0x37, 0x88, 0xd9, 0xc5, 0xe4, 0x69, 0x7b, 0x38, 0x49, 0xf, 0x9b, 0x5, 0xdc, 0xac, 0xcd, + 0x2a, 0x16, 0x20, 0xb0, 0xf8, 0x0, 0x8, 0x6, 0x24, 0x10, 0x24, 0x12, 0x0, 0x0, 0x15, 0x9a, + 0x65, 0xaf, 0x56, 0x88, 0x3, 0xc, 0x83, 0xf2, 0x7b, 0x5, 0xb9, 0xe, 0xc6, 0x2e, 0x1, 0x6b, + 0xe1, 0x6c, 0xf3, 0x28, 0xb, 0x63, 0x56, 0xec, 0x5c, 0x8a, 0x98, 0x20, 0xb0, 0x98, 0x0, 0x9, + 0xc, 0x24, 0x2d, 0x44, 0x2, 0x10, 0x0, 0x0, 0x3a, 0xd9, 0xca, 0x42, 0xc4, 0x40, 0x16, 0x20, + 0x0, 0x34, 0xec, 0x3, 0xe, 0x65, 0x1, 0x15, 0x3e, 0x84, 0x3a, 0xd6, 0xe1, 0x3, 0x15, 0xb9, + 0x8c, 0xa6, 0x16, 0x3d, 0x0, 0xab, 0x7d, 0x80, 0x1, 0xc3, 0x81, 0x65, 0xd0, 0x0, 0xa, 0x1a, + 0x20, 0x5c, 0x44, 0x4, 0x20, 0x0, 0x0, 0x11, 0xe4, 0x58, 0x4a, 0x20, 0xfc, 0x9e, 0xc0, 0x66, + 0xaf, 0xa0, 0x20, 0x9b, 0x2, 0x4, 0x0, 0x0, 0x1, 0x7e, 0x98, 0x22, 0x8a, 0x0, 0x8, 0x1d, 0x6, + 0xa0, 0x4, 0x28, 0x2, 0x8, 0x28, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x66, 0x20, 0x8c, 0x7b, 0x20, 0x0, + 0x82, 0x26, 0x97, 0x40, 0x42, 0x60, 0x40, 0x82, 0xac, 0x8, 0x10, 0x0, 0x0, 0x5, 0xb9, 0x28, + 0x6f, 0xe8, 0x0, 0x20, 0x71, 0x1b, 0x0, 0x10, 0xb0, 0x18, 0x20, 0xb3, 0x2, 0x4, 0x0, 0x0, 0x1, + 0x56, 0xc2, 0x4b, 0x68, 0x0, 0x20, 0x8f, 0x25, 0x0, 0x10, 0xa8, 0x20, 0x20, 0xbb, 0x2, 0x4, + 0x0, 0x0, 0x1, 0x7e, 0x63, 0x57, 0xea, 0x0, 0x8, 0x26, 0x37, 0xcc, 0x4, 0x30, 0xa, 0x8, 0x30, + 0xc0, 0x81, 0x0, 0x0, 0x0, 0x5e, 0x5, 0x57, 0xac, 0x80, 0x2, 0x6, 0x76, 0xd, 0x1, 0xb, 0x83, + 0x2, 0xc, 0xb0, 0x20, 0x40, 0x0, 0x0, 0x13, 0xeb, 0xc2, 0x9a, 0x0, 0x8, 0x22, 0x56, 0x8c, 0x4, + 0x34, 0xe, 0x8, 0x34, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x4e, 0xf3, 0x45, 0x28, 0x0, 0x20, 0x76, 0xe5, + 0xd0, 0x10, 0xc8, 0x40, 0x20, 0xdb, 0x2, 0x4, 0x0, 0x0, 0x1, 0x6f, 0xd8, 0xd, 0x32, 0x0, 0x8, + 0x19, 0xd8, 0x34, 0x4, 0x38, 0x12, 0x8, 0x38, 0xc0, 0x81, 0x0, 0x0, 0x0, 0x5d, 0x90, 0xc1, + 0x90, 0x80, 0x2, 0x9, 0x8d, 0xf3, 0x1, 0xd, 0x85, 0x2, 0xf, 0xa0, 0x10, 0x80, 0x0, 0x0, 0x15, + 0xa2, 0x24, 0x2e, 0x80, 0x2, 0x8, 0xee, 0x50, 0x1, 0x10, 0x5, 0x80, 0x10, 0x10, 0x10, 0x80, + 0x0, 0x0, 0x17, 0x0, 0x81, 0x9d, 0xa0, 0x0, 0x81, 0xc8, 0x6b, 0x41, 0x8, 0x50, 0x8, 0x40, 0x0, + 0x0, 0xb, 0x8a, 0xd8, 0xd8, 0x50, 0x0, 0x40, 0xcf, 0x42, 0x0, 0x22, 0x20, 0xc0, 0x2, 0x22, 0x2, + 0x10, 0x0, 0x0, 0x2, 0xef, 0x9c, 0x24, 0xe4, 0x0, 0x10, 0x4c, 0x4f, 0x80, 0x21, 0x1a, 0x1, 0x8, + 0x0, 0x0, 0x1, 0x5a, 0xd2, 0xde, 0x48, 0x0, 0x20, 0x80, 0x26, 0x40, 0x11, 0x20, 0x68, 0x1, + 0x21, 0x1, 0x8, 0x0, 0x0, 0x1, 0x5a, 0xce, 0x9a, 0x68, 0x0, 0x20, 0x80, 0x19, 0xc0, 0x42, 0x54, + 0x2, 0x10, 0x0, 0x0, 0x2, 0xa4, 0x6c, 0xcc, 0x10, 0x0, 0x41, 0x2a, 0x39, 0x40, 0x22, 0x60, + 0xe0, 0x2, 0x62, 0x2, 0x10, 0x0, 0x0, 0x2, 0x47, 0x69, 0x47, 0x40, 0x1, 0x3, 0x49, 0x13, 0x2, + 0x13, 0xa0, 0x10, 0x80, 0x0, 0x0, 0x17, 0xe0, 0x4, 0x82, 0x20, 0x0, 0x82, 0x63, 0x7d, 0x0, + 0x45, 0x1, 0xe0, 0x5, 0x4, 0x4, 0x20, 0x0, 0x0, 0x5, 0x6c, 0xd8, 0xff, 0xa0, 0x0, 0x81, 0x9d, + 0x83, 0x1, 0xa, 0x58, 0x10, 0x20, 0x0, 0x0, 0xb, 0x60, 0x30, 0x70, 0x10, 0x0, 0x41, 0x31, 0x42, + 0x0, 0x22, 0xa1, 0x0, 0x42, 0xa6, 0x4, 0x8, 0x0, 0x0, 0x2, 0xd3, 0xa2, 0x16, 0x24, 0x0, 0x10, + 0x33, 0xcf, 0x80, 0x8, 0xa4, 0x44, 0x10, 0xad, 0x0, 0x84, 0x0, 0x0, 0x0, 0xbd, 0xe7, 0x8f, + 0xdd, 0x0, 0x4, 0xe, 0x8b, 0x50, 0x2, 0x2c, 0x12, 0x0, 0x2c, 0x20, 0x21, 0x0, 0x0, 0x0, 0x2f, + 0xe0, 0x89, 0xbb, 0x40, 0x1, 0x4, 0x71, 0x29, 0x80, +]; + +/// Intersection "TODO" +/// +/// 3 layers +#[cfg(feature = "spat")] +pub const MAPEM_HH_2349_L31: &[u8] = &[ + 0x2, 0x5, 0x0, 0x12, 0x25, 0xf4, 0x18, 0x0, 0x3e, 0x6, 0xd, 0x91, 0x21, 0x93, 0x3b, 0x8c, 0x99, + 0xb4, 0x73, 0x71, 0x8b, 0x6, 0xe, 0x60, 0x0, 0x61, 0x25, 0xa1, 0x5, 0x58, 0xef, 0x69, 0x97, + 0x13, 0xed, 0x54, 0x20, 0x28, 0xa3, 0x68, 0x4, 0x49, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, + 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x99, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, + 0x72, 0xf, 0xe5, 0xc0, 0x7d, 0xa6, 0x34, 0x41, 0xf4, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, + 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcd, 0x1, 0x2, 0x4, 0x0, 0x0, + 0x55, 0xb8, 0x57, 0xf1, 0x50, 0x3e, 0x73, 0x18, 0x28, 0x80, 0x3c, 0x51, 0x10, 0x6d, 0x38, 0xdd, + 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, + 0x40, 0x40, 0x81, 0x0, 0x0, 0x10, 0x8c, 0x36, 0xc6, 0x1, 0x31, 0xc1, 0xc0, 0xa1, 0xc0, 0x91, + 0x24, 0x1, 0xe4, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, + 0x10, 0x79, 0x6d, 0x40, 0xcc, 0x81, 0x2, 0x4, 0x0, 0x0, 0x55, 0xaf, 0x28, 0x32, 0xd0, 0x3e, + 0x73, 0x1a, 0x0, 0xd2, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, + 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x40, 0x81, 0x2, 0x0, 0x0, 0x20, 0x21, 0xcd, 0x23, 0x81, + 0x9c, 0x7e, 0x51, 0x7, 0x53, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, + 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x34, 0x4, 0x8, 0x10, 0x0, 0x1, 0x56, 0xd5, 0x20, 0xe, 0xc1, + 0x6, 0xb3, 0xa0, 0xa1, 0xe0, 0xe1, 0x34, 0x41, 0x94, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, + 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xc9, 0x1, 0x2, 0x4, 0x0, 0x0, + 0x40, 0xe9, 0x9a, 0x78, 0x4, 0xc9, 0x3, 0x82, 0x86, 0x83, 0x4, 0x50, 0x6, 0x13, 0x8d, 0xd7, + 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x22, + 0x2, 0x8, 0x10, 0x0, 0x1, 0x8, 0xf2, 0x70, 0xe4, 0x13, 0x1c, 0x22, 0x88, 0x2e, 0x9c, 0x6e, + 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, 0x19, + 0x20, 0x10, 0x40, 0x80, 0x0, 0x8, 0x2f, 0xb3, 0x77, 0x0, 0x67, 0x5e, 0xf0, 0x50, 0xc0, 0x58, + 0x82, 0x0, 0xb2, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, + 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x40, 0x41, 0x2, 0x0, 0x0, 0x20, 0x73, 0x2d, 0xbe, 0x82, 0x64, + 0x81, 0x51, 0x5, 0x53, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, 0x3b, + 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x24, 0x2, 0x8, 0x10, 0x0, 0x1, 0x0, 0x69, 0x6d, 0x60, 0xc, 0xe3, + 0xf6, 0xa, 0x16, 0xa, 0xf, 0x40, 0x14, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, + 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x88, 0x10, 0x20, 0x40, 0x0, 0x4, 0xe, + 0xb5, 0xa7, 0x50, 0x4c, 0x90, 0x2b, 0x20, 0xa, 0xd1, 0xb0, 0xe8, 0xe5, 0xcf, 0xa7, 0x2c, 0xba, + 0x73, 0x65, 0xdc, 0x81, 0x8a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x11, 0x40, 0x20, 0x0, + 0x2, 0x88, 0x69, 0x7c, 0x9a, 0x43, 0xec, 0xa4, 0x90, 0x1, 0x0, 0xc0, 0x0, 0xd1, 0xe3, 0x90, + 0x68, 0xf2, 0x0, 0x34, 0x59, 0x18, 0x1a, 0x1c, 0x98, 0x8, 0x2, 0x0, 0x16, 0x86, 0xc9, 0xe0, + 0x31, 0x99, 0x20, 0x18, 0xbc, 0x9e, 0x9c, 0x5b, 0x29, 0x10, 0x1, 0x41, 0x61, 0x90, 0x0, 0x2, + 0x2, 0x80, 0x20, 0x9c, 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, + 0x82, 0xf, 0x2d, 0xa8, 0x19, 0x90, 0x20, 0x40, 0x80, 0x0, 0xa, 0xb6, 0xbd, 0x0, 0x6c, 0x8, + 0x35, 0x9d, 0x50, 0xc, 0x5a, 0x36, 0x1d, 0x1c, 0xb9, 0xf4, 0xe5, 0x97, 0x4e, 0x6c, 0xbb, 0x90, + 0x34, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x64, 0x4, 0x0, 0x0, 0x9, 0x55, 0x73, + 0x60, 0x6b, 0xc2, 0x13, 0x5c, 0x90, 0x86, 0xc9, 0xce, 0x59, 0x0, 0x96, 0x8d, 0x87, 0x47, 0x2e, + 0x7d, 0x39, 0x65, 0xd3, 0x9b, 0x2e, 0xe4, 0xc, 0x90, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, 0x18, + 0x8a, 0x10, 0x0, 0x0, 0xe, 0x43, 0x56, 0xe3, 0x87, 0x1f, 0xbb, 0xe5, 0x46, 0x1c, 0x3, 0x18, + 0x41, 0xff, 0x4d, 0xe0, 0x80, 0x20, 0x0, 0xff, 0xa9, 0xd8, 0xd, 0x6e, 0x53, 0x6, 0xb3, 0x2d, + 0xa7, 0x58, 0xcb, 0xa4, 0x0, 0x50, 0x58, 0x5c, 0x0, 0x0, 0x81, 0x20, 0x9, 0x27, 0x1b, 0xaf, + 0x2e, 0x88, 0x13, 0xc6, 0xeb, 0xcb, 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x44, + 0x8, 0x10, 0x20, 0x0, 0x2, 0x11, 0x7e, 0xd9, 0x90, 0x26, 0x38, 0x31, 0x0, 0x81, 0x38, 0xdd, + 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x33, + 0x20, 0x40, 0x81, 0x0, 0x0, 0x15, 0x6f, 0x79, 0xf1, 0x88, 0x10, 0x6b, 0x3a, 0x80, 0x38, 0x9c, + 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, + 0x19, 0x10, 0x20, 0x40, 0x80, 0x0, 0x8, 0x31, 0x1b, 0x57, 0x40, 0x67, 0x5f, 0x25, 0x0, 0xb5, + 0xa3, 0x61, 0xd1, 0xcb, 0x9f, 0x4e, 0x59, 0x74, 0xe6, 0xcb, 0xb9, 0x3, 0x34, 0x1a, 0x76, 0xa0, + 0x83, 0xcb, 0x6a, 0x6, 0x66, 0x44, 0x0, 0x0, 0x0, 0x95, 0x57, 0xe2, 0x0, 0xfc, 0x46, 0x24, + 0xec, 0x62, 0x2e, 0xc7, 0x57, 0x44, 0x21, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, + 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0xd0, 0x10, 0x20, 0x40, 0x0, 0x5, 0x5b, + 0xd9, 0x7c, 0x72, 0x4, 0x14, 0xce, 0x82, 0x88, 0x83, 0x45, 0x54, 0x2, 0x16, 0x8d, 0x87, 0x47, + 0x2e, 0x7d, 0x39, 0x65, 0xd3, 0x9b, 0x2e, 0xe4, 0xc, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, + 0x19, 0x11, 0x10, 0x0, 0x0, 0x2, 0x40, 0xab, 0x99, 0x11, 0xc, 0x3d, 0x3b, 0x22, 0x87, 0x13, + 0xc5, 0x44, 0xd, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, + 0x41, 0x7, 0x96, 0xd4, 0xc, 0x58, 0x10, 0x20, 0x40, 0x0, 0x4, 0x2e, 0x8a, 0x53, 0x20, 0x3e, + 0xb3, 0x18, 0x28, 0x38, 0x14, 0x29, 0x10, 0x39, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, + 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x31, 0x60, 0x40, 0x81, 0x0, 0x0, 0x10, + 0xc6, 0x17, 0xad, 0x41, 0x5, 0xb3, 0xa0, 0xa0, 0xd0, 0x50, 0xb4, 0x40, 0xf4, 0xe3, 0x75, 0xe5, + 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcd, 0x1, + 0x2, 0x4, 0x0, 0x0, 0x55, 0xaf, 0xc8, 0x33, 0x20, 0x3e, 0x73, 0x18, 0x28, 0x40, 0x34, 0x31, + 0x90, 0xd, 0x68, 0xd8, 0x74, 0x72, 0xe7, 0xd3, 0x96, 0x5d, 0x39, 0xb2, 0xee, 0x40, 0xcd, 0x6, + 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x88, 0xa1, 0x0, 0x0, 0x2, 0x44, 0x36, 0x2e, 0x23, 0x61, + 0x79, 0xf4, 0x9a, 0x83, 0xcf, 0x94, 0x1, 0xe7, 0x4a, 0xc0, 0xf3, 0x65, 0xc0, 0x79, 0x73, 0x10, + 0xf5, 0xf5, 0x26, 0x64, 0x46, 0x30, 0x58, 0x23, 0x6, 0x9, 0x32, 0x18, 0x4, 0x8b, 0x13, 0x82, + 0x3b, 0x8d, 0x1, 0x18, 0x48, 0x0, 0x96, 0x69, 0x30, 0x40, 0x10, 0x0, 0x23, 0x9, 0x0, 0x11, + 0xdc, 0x6a, 0x9, 0x12, 0x2a, 0x4, 0x97, 0xe, 0x2, 0x0, 0x80, 0x51, 0x18, 0x41, 0xa8, 0x0, 0xa0, + 0xb0, 0x84, 0x0, 0x2, 0x3, 0x44, 0x11, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, + 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0x18, + 0x69, 0xad, 0xe0, 0x33, 0xaf, 0xa0, 0x28, 0x48, 0x20, 0x35, 0x10, 0x4d, 0x38, 0xdd, 0x79, 0x74, + 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x40, 0x40, + 0x81, 0x0, 0x0, 0x10, 0x10, 0xa6, 0x95, 0x80, 0xce, 0x3f, 0x60, 0xa1, 0x40, 0x90, 0xe0, +]; +#[cfg(feature = "spat")] +pub const MAPEM_HH_2349_L32: &[u8] = &[ + 0x2, 0x5, 0x0, 0x12, 0x25, 0xf4, 0x18, 0x0, 0x40, 0x6, 0xd, 0x91, 0x21, 0x93, 0x3b, 0x8c, 0x99, + 0xb4, 0x73, 0x71, 0x8b, 0x6, 0xe, 0x60, 0x0, 0x61, 0x25, 0xa1, 0x5, 0x58, 0xef, 0x69, 0x97, + 0x13, 0xed, 0x54, 0x20, 0x28, 0xa3, 0x48, 0x84, 0x29, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, + 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x9a, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, + 0x7b, 0x2f, 0x8e, 0x40, 0x82, 0x99, 0xd0, 0x51, 0x10, 0x68, 0xaa, 0x0, 0xf2, 0x71, 0xba, 0xf2, + 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x40, + 0x81, 0x2, 0x0, 0x0, 0x2a, 0xd7, 0x94, 0x19, 0x68, 0x1f, 0x39, 0x8d, 0x0, 0x71, 0x38, 0xdd, + 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, + 0x20, 0x40, 0x81, 0x0, 0x0, 0x10, 0x62, 0x36, 0xae, 0x80, 0xce, 0xbe, 0x48, 0x3, 0x49, 0xc6, + 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, + 0x91, 0x2, 0x4, 0x8, 0x0, 0x0, 0x80, 0x87, 0x34, 0x8e, 0x6, 0x71, 0xf9, 0x44, 0x19, 0x4e, 0x37, + 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, + 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0xe, 0x99, 0xa7, 0x80, 0x4c, 0x90, 0x38, 0x28, 0x68, 0x30, + 0x45, 0x0, 0x61, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, + 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x20, 0x20, 0x81, 0x0, 0x0, 0x10, 0x8f, 0x27, 0xe, 0x41, 0x31, + 0xc2, 0x28, 0x82, 0xe9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, + 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x92, 0x1, 0x4, 0x8, 0x0, 0x0, 0x82, 0xfb, 0x37, 0x70, 0x6, 0x75, + 0xef, 0x5, 0xc, 0x5, 0x88, 0x20, 0xb, 0x27, 0x1b, 0xaf, 0x2e, 0x88, 0x13, 0xc6, 0xeb, 0xcb, + 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x44, 0x4, 0x10, 0x20, 0x0, 0x2, 0x7, + 0x32, 0xdb, 0xe8, 0x26, 0x48, 0x15, 0x10, 0x55, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, + 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x40, 0x20, 0x81, 0x0, 0x0, 0x10, + 0x6, 0x96, 0xd6, 0x0, 0xce, 0x3f, 0x60, 0xa1, 0x60, 0xa0, 0xf5, 0x0, 0xb5, 0xa3, 0x61, 0xd1, + 0xcb, 0x9f, 0x4e, 0x59, 0x74, 0xe6, 0xcb, 0xb9, 0x3, 0x34, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, + 0x6, 0x66, 0x44, 0x0, 0x0, 0x0, 0x95, 0x57, 0xe2, 0x0, 0xfc, 0x46, 0x24, 0xec, 0x62, 0x2e, + 0xc7, 0x57, 0x40, 0x12, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, + 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x88, 0x10, 0x20, 0x40, 0x0, 0x4, 0x22, 0xfd, 0xb3, 0x20, + 0x4c, 0x70, 0x62, 0x20, 0xea, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, + 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x80, 0x81, 0x2, 0x0, 0x0, 0x2a, 0xda, 0xa4, 0x1, + 0xd8, 0x20, 0xd6, 0x74, 0x14, 0x3c, 0x1c, 0x26, 0xa0, 0xa, 0xb4, 0x6c, 0x3a, 0x39, 0x73, 0xe9, + 0xcb, 0x2e, 0x9c, 0xd9, 0x77, 0x20, 0x6a, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xc4, 0x48, + 0x80, 0x0, 0x0, 0x11, 0x81, 0x6e, 0x2, 0x81, 0xcc, 0x71, 0x88, 0x5c, 0x25, 0x20, 0x4c, 0x80, + 0xeb, 0x46, 0xc3, 0xa3, 0x97, 0x3e, 0x9c, 0xb2, 0xe9, 0xcd, 0x97, 0x72, 0x6, 0x28, 0x34, 0xed, + 0x41, 0x7, 0x96, 0xd4, 0xc, 0x89, 0x8, 0x0, 0x0, 0xf, 0x21, 0x6, 0xc, 0xfb, 0xa, 0x3e, 0x44, + 0xf0, 0x10, 0xc, 0x0, 0x18, 0xb2, 0x5d, 0x83, 0x13, 0xcb, 0xa8, 0xa2, 0x58, 0xba, 0x11, 0x16, + 0x8e, 0xa0, 0x8c, 0x18, 0x20, 0x7, 0xfc, 0x10, 0x5a, 0x76, 0x7, 0xd1, 0x3b, 0xb, 0xa1, 0x1b, + 0x40, 0x80, 0x20, 0x0, 0x3f, 0xc, 0xe8, 0x1f, 0xe6, 0x78, 0x10, 0x23, 0x3a, 0x8, 0x25, 0x9e, + 0x44, 0x6a, 0x3b, 0x20, 0x2, 0x8a, 0xc1, 0x48, 0x0, 0x10, 0x1d, 0x60, 0x84, 0x0, 0x8, 0x10, + 0x80, 0x44, 0x9c, 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, + 0xf, 0x2d, 0xa8, 0x19, 0x90, 0x20, 0x40, 0x80, 0x0, 0xa, 0xb7, 0x20, 0xfe, 0x5c, 0x7, 0xda, + 0x63, 0x44, 0x11, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, + 0x41, 0x7, 0x96, 0xd4, 0xc, 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0x18, 0x69, 0xad, 0xe0, 0x33, + 0xaf, 0xa0, 0x28, 0x48, 0x20, 0x35, 0x10, 0x6d, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, + 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x40, 0x40, 0x81, 0x0, 0x0, 0x10, + 0x8c, 0x36, 0xc6, 0x1, 0x31, 0xc1, 0xc0, 0xa1, 0xc0, 0x91, 0x25, 0x0, 0x45, 0xa3, 0x61, 0xd1, + 0xcb, 0x9f, 0x4e, 0x59, 0x74, 0xe6, 0xcb, 0xb9, 0x3, 0x44, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, + 0x6, 0x22, 0x44, 0x0, 0x0, 0x0, 0x8c, 0x3, 0xf1, 0x64, 0xf, 0xed, 0x92, 0xe2, 0xc5, 0xf8, 0xba, + 0x64, 0x6, 0x5a, 0x36, 0x1d, 0x1c, 0xb9, 0xf4, 0xe5, 0x97, 0x4e, 0x6c, 0xbb, 0x90, 0x32, 0x41, + 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x48, 0x40, 0x0, 0x0, 0x29, 0x6, 0xee, 0x66, 0xf4, + 0x91, 0x7b, 0x93, 0xc8, 0x8, 0x6, 0x0, 0x4c, 0x7f, 0x2, 0x68, 0x0, 0xa1, 0x8e, 0x0, 0x44, 0x51, + 0xa4, 0x3, 0xe5, 0x88, 0x34, 0x38, 0x3, 0x5, 0x85, 0xa0, 0x0, 0x38, 0x4a, 0x20, 0x6a, 0x71, + 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, + 0x62, 0xc0, 0x81, 0x2, 0x0, 0x0, 0x21, 0x74, 0x52, 0x99, 0x1, 0xf5, 0x98, 0xc1, 0x41, 0xc0, + 0xa1, 0x48, 0x2, 0x89, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, + 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x91, 0x2, 0x4, 0x8, 0x0, 0x0, 0x81, 0xd6, 0xb4, 0xea, 0x9, 0x92, + 0x5, 0x44, 0x1f, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, + 0x41, 0x7, 0x96, 0xd4, 0xc, 0xd0, 0x10, 0x20, 0x40, 0x0, 0x5, 0x5b, 0x85, 0x7f, 0x15, 0x3, + 0xe7, 0x31, 0x82, 0x88, 0x3, 0xc5, 0x11, 0x3, 0x93, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, + 0xe5, 0xd1, 0x3a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x16, 0x4, 0x8, 0x10, 0x0, 0x1, 0xc, + 0x61, 0x7a, 0xd4, 0x10, 0x5b, 0x3a, 0xa, 0xd, 0x5, 0xb, 0x40, 0x20, 0x4e, 0x37, 0x5e, 0x5d, + 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0xc8, 0x10, + 0x20, 0x40, 0x0, 0x5, 0x5b, 0xde, 0x7c, 0x62, 0x4, 0x1a, 0xce, 0xa2, 0x7, 0xa7, 0x1b, 0xaf, + 0x2e, 0x88, 0x13, 0xc6, 0xeb, 0xcb, 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x68, + 0x8, 0x10, 0x20, 0x0, 0x2, 0xad, 0x7e, 0x41, 0x99, 0x1, 0xf3, 0x98, 0xc1, 0x42, 0x1, 0xa1, + 0x88, 0x2, 0x9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, + 0x20, 0xf2, 0xda, 0x81, 0x99, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, 0x6b, 0xd0, 0x6, 0xc0, 0x83, 0x59, + 0xd4, 0x41, 0x34, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, + 0x10, 0x79, 0x6d, 0x40, 0xc9, 0x1, 0x2, 0x4, 0x0, 0x0, 0x40, 0x42, 0x9a, 0x56, 0x3, 0x38, 0xfd, + 0x82, 0x85, 0x2, 0x43, 0x80, +]; +#[cfg(feature = "spat")] +pub const MAPEM_HH_2349_L33: &[u8] = &[ + 0x2, 0x5, 0x0, 0x12, 0x25, 0xf4, 0x18, 0x0, 0x42, 0x6, 0xd, 0x91, 0x21, 0x93, 0x3b, 0x8c, 0x99, + 0xb4, 0x73, 0x71, 0x8b, 0x6, 0xe, 0x60, 0x0, 0x61, 0x25, 0xa1, 0x5, 0x58, 0xef, 0x69, 0x97, + 0x13, 0xed, 0x54, 0x20, 0x28, 0xa3, 0x48, 0x84, 0x29, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, + 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x9a, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, + 0x7b, 0x2f, 0x8e, 0x40, 0x82, 0x99, 0xd0, 0x51, 0x10, 0x68, 0xaa, 0x0, 0xf2, 0x71, 0xba, 0xf2, + 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x66, 0x40, + 0x81, 0x2, 0x0, 0x0, 0x2a, 0xd7, 0x94, 0x19, 0x68, 0x1f, 0x39, 0x8d, 0x10, 0x75, 0x38, 0xdd, + 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x33, + 0x40, 0x40, 0x81, 0x0, 0x0, 0x15, 0x6d, 0x52, 0x0, 0xec, 0x10, 0x6b, 0x3a, 0xa, 0x1e, 0xe, + 0x13, 0x44, 0x1b, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, + 0x41, 0x7, 0x96, 0xd4, 0xc, 0x90, 0x10, 0x20, 0x40, 0x0, 0x4, 0x23, 0xd, 0xb1, 0x80, 0x4c, + 0x70, 0x70, 0x28, 0x70, 0x24, 0x49, 0x0, 0x69, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, 0x5e, + 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x20, 0x40, 0x81, 0x0, 0x0, 0x10, + 0x10, 0xe6, 0x91, 0xc0, 0xce, 0x3f, 0x28, 0x83, 0x29, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, + 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x92, 0x2, 0x4, 0x8, 0x0, 0x0, 0x81, + 0xd3, 0x34, 0xf0, 0x9, 0x92, 0x7, 0x5, 0xd, 0x6, 0x8, 0xa2, 0xb, 0xa7, 0x1b, 0xaf, 0x2e, 0x88, + 0x13, 0xc6, 0xeb, 0xcb, 0xa2, 0x74, 0x1a, 0x76, 0xa0, 0x83, 0xcb, 0x6a, 0x6, 0x48, 0x4, 0x10, + 0x20, 0x0, 0x2, 0xb, 0xec, 0xdd, 0xc0, 0x19, 0xd7, 0xbc, 0x14, 0x30, 0x16, 0x20, 0x80, 0x2c, + 0x9c, 0x6e, 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, + 0xa8, 0x19, 0x10, 0x10, 0x40, 0x80, 0x0, 0x8, 0x1c, 0xcb, 0x6f, 0xa0, 0x99, 0x20, 0x54, 0x41, + 0x54, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, + 0x6d, 0x40, 0xc9, 0x0, 0x82, 0x4, 0x0, 0x0, 0x40, 0x1a, 0x5b, 0x58, 0x3, 0x38, 0xfd, 0x82, + 0x85, 0x82, 0x83, 0xd0, 0x7, 0x13, 0x8d, 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, + 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, 0x22, 0x4, 0x8, 0x10, 0x0, 0x1, 0x6, 0x23, 0x6a, 0xe8, + 0xc, 0xeb, 0xe4, 0xc8, 0x12, 0xb4, 0x6c, 0x3a, 0x39, 0x73, 0xe9, 0xcb, 0x2e, 0x9c, 0xd9, 0x77, + 0x20, 0x64, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcc, 0xd0, 0x80, 0x0, 0x0, 0x12, 0xad, + 0x1e, 0x3f, 0x1c, 0x4, 0xed, 0x3b, 0xc2, 0xa7, 0xc3, 0x3c, 0xe1, 0x82, 0xc1, 0x20, 0x0, 0xc, + 0x11, 0x40, 0x21, 0x68, 0xd8, 0x74, 0x72, 0xe7, 0xd3, 0x96, 0x5d, 0x39, 0xb2, 0xee, 0x40, 0xcd, + 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x91, 0x11, 0x0, 0x0, 0x0, 0x24, 0xa, 0xb9, 0x91, + 0x10, 0xc3, 0xd3, 0xb2, 0x28, 0x71, 0x3c, 0x54, 0x41, 0x34, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, + 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xc9, 0x1, 0x2, 0x4, 0x0, + 0x0, 0x40, 0x42, 0x9a, 0x56, 0x3, 0x38, 0xfd, 0x82, 0x85, 0x2, 0x43, 0x90, 0x6, 0x13, 0x8d, + 0xd7, 0x97, 0x44, 0x9, 0xe3, 0x75, 0xe5, 0xd1, 0x3a, 0xd, 0x3b, 0x50, 0x41, 0xe5, 0xb5, 0x3, + 0x22, 0x2, 0x8, 0x10, 0x0, 0x1, 0x8, 0xf2, 0x70, 0xe4, 0x13, 0x1c, 0x22, 0xa0, 0x8, 0xb4, 0x6c, + 0x3a, 0x39, 0x73, 0xe9, 0xcb, 0x2e, 0x9c, 0xd9, 0x77, 0x20, 0x68, 0x83, 0x4e, 0xd4, 0x10, 0x79, + 0x6d, 0x40, 0xc4, 0x48, 0x80, 0x0, 0x0, 0x11, 0x80, 0x7e, 0x2c, 0x81, 0xfd, 0xb2, 0x5c, 0x58, + 0xbf, 0x17, 0x48, 0x2, 0x49, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, + 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x91, 0x2, 0x4, 0x8, 0x0, 0x0, 0x84, 0x5f, 0xb6, 0x64, 0x9, + 0x8e, 0xc, 0x44, 0xd, 0x4e, 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, + 0xed, 0x41, 0x7, 0x96, 0xd4, 0xc, 0x58, 0x10, 0x20, 0x40, 0x0, 0x4, 0x2e, 0x8a, 0x53, 0x20, + 0x3e, 0xb3, 0x18, 0x28, 0x38, 0x14, 0x29, 0x0, 0x51, 0x38, 0xdd, 0x79, 0x74, 0x40, 0x9e, 0x37, + 0x5e, 0x5d, 0x13, 0xa0, 0xd3, 0xb5, 0x4, 0x1e, 0x5b, 0x50, 0x32, 0x20, 0x40, 0x81, 0x0, 0x0, + 0x10, 0x3a, 0xd6, 0x9d, 0x41, 0x32, 0x40, 0xa8, 0x83, 0xe9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, + 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x9a, 0x2, 0x4, 0x8, 0x0, 0x0, + 0xab, 0x70, 0xaf, 0xe2, 0xa0, 0x7c, 0xe6, 0x30, 0x51, 0x0, 0x78, 0xa2, 0x20, 0x72, 0x71, 0xba, + 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x62, + 0xc0, 0x81, 0x2, 0x0, 0x0, 0x21, 0x8c, 0x2f, 0x5a, 0x82, 0xb, 0x67, 0x41, 0x41, 0xa0, 0xa1, + 0x68, 0x4, 0x9, 0xc6, 0xeb, 0xcb, 0xa2, 0x4, 0xf1, 0xba, 0xf2, 0xe8, 0x9d, 0x6, 0x9d, 0xa8, + 0x20, 0xf2, 0xda, 0x81, 0x99, 0x2, 0x4, 0x8, 0x0, 0x0, 0xab, 0x7b, 0xcf, 0x8c, 0x40, 0x83, + 0x59, 0xd4, 0x40, 0xf4, 0xe3, 0x75, 0xe5, 0xd1, 0x2, 0x78, 0xdd, 0x79, 0x74, 0x4e, 0x83, 0x4e, + 0xd4, 0x10, 0x79, 0x6d, 0x40, 0xcd, 0x1, 0x2, 0x4, 0x0, 0x0, 0x55, 0xaf, 0xc8, 0x33, 0x20, + 0x3e, 0x73, 0x18, 0x28, 0x40, 0x34, 0x31, 0x90, 0x29, 0x68, 0xd8, 0x74, 0x72, 0xe7, 0xd3, 0x96, + 0x5d, 0x39, 0xb2, 0xee, 0x40, 0xc5, 0x6, 0x9d, 0xa8, 0x20, 0xf2, 0xda, 0x81, 0x99, 0xa1, 0x0, + 0x0, 0x0, 0x45, 0x5a, 0x6e, 0x7c, 0xf8, 0x9, 0xda, 0x77, 0x80, 0x80, 0x60, 0x2, 0xa7, 0xbf, + 0xbc, 0xf1, 0xa, 0xc1, 0x60, 0x0, 0xc, 0x15, 0x61, 0x4, 0x0, 0xc, 0xc, 0x80, 0x20, 0x9c, 0x6e, + 0xbc, 0xba, 0x20, 0x4f, 0x1b, 0xaf, 0x2e, 0x89, 0xd0, 0x69, 0xda, 0x82, 0xf, 0x2d, 0xa8, 0x19, + 0x90, 0x20, 0x40, 0x80, 0x0, 0xa, 0xb6, 0xbd, 0x0, 0x6c, 0x8, 0x35, 0x9d, 0x40, 0x22, 0x4e, + 0x37, 0x5e, 0x5d, 0x10, 0x27, 0x8d, 0xd7, 0x97, 0x44, 0xe8, 0x34, 0xed, 0x41, 0x7, 0x96, 0xd4, + 0xc, 0xc8, 0x10, 0x20, 0x40, 0x0, 0x5, 0x5b, 0x90, 0x7f, 0x2e, 0x3, 0xed, 0x31, 0xa8, 0x2, + 0xad, 0x1b, 0xe, 0x8e, 0x5c, 0xfa, 0x72, 0xcb, 0xa7, 0x36, 0x5d, 0xc8, 0x1a, 0xa0, 0xd3, 0xb5, + 0x4, 0x1e, 0x5b, 0x50, 0x31, 0x12, 0x20, 0x0, 0x0, 0x4, 0x60, 0x5b, 0x80, 0xa0, 0x73, 0x1c, + 0x62, 0x17, 0x9, 0x48, 0x12, 0x20, 0x8a, 0x71, 0xba, 0xf2, 0xe8, 0x81, 0x3c, 0x6e, 0xbc, 0xba, + 0x27, 0x41, 0xa7, 0x6a, 0x8, 0x3c, 0xb6, 0xa0, 0x64, 0x80, 0x81, 0x2, 0x0, 0x0, 0x20, 0xc3, + 0x4d, 0x6f, 0x1, 0x9d, 0x7d, 0x1, 0x42, 0x41, 0x1, 0xa0, +]; + +// More data: +// - 34 (Neuer Pferdemarkt/Neuer Kamp): 2 layers, quite complex bike crossings + +/// Just some random SPATEM +#[cfg(feature = "spat")] +pub const SPATEM: &[u8] = &[ + 0x02, 0x04, 0x00, 0x00, 0x30, 0x16, 0x00, 0x38, 0x4a, 0x6c, 0x1a, 0x17, 0xe4, 0xd4, 0xa7, 0x7d, + 0x93, 0x5b, 0x8c, 0x9a, 0xb0, 0x63, 0x71, 0x92, 0xe6, 0x50, 0x00, 0x30, 0x9c, 0x50, 0x40, 0x00, + 0x0e, 0xa7, 0x1d, 0x4f, 0xf0, 0x10, 0x00, 0x22, 0x8c, 0xe2, 0xf9, 0x44, 0xff, 0x63, 0x5b, 0x63, + 0x48, 0x71, 0xad, 0xb1, 0xae, 0xa1, 0xae, 0xa7, 0x80, 0x10, 0xa1, 0xb8, 0xbe, 0x29, 0x40, 0xa0, + 0xd6, 0xd8, 0xd1, 0x5c, 0x6b, 0x6c, 0x6c, 0x0c, 0x6c, 0x0d, 0xe0, 0x06, 0x28, 0x0e, 0x2d, 0xfa, + 0x68, 0xf6, 0x35, 0xb6, 0x34, 0x57, 0x1a, 0xdb, 0x1b, 0x03, 0x1b, 0x03, 0x78, 0x02, 0x0a, 0x1b, + 0x8d, 0x8b, 0x8d, 0xc0, 0x0d, 0xa7, 0x0d, 0x11, 0xc6, 0xd3, 0x86, 0xd4, 0xc6, 0xd4, 0xde, 0x00, + 0xa2, 0x8a, 0xe3, 0x5f, 0x23, 0x6c, 0x43, 0x66, 0x03, 0x43, 0x71, 0xb3, 0x01, 0xbb, 0x71, 0xba, + 0xd1, 0x80, 0x30, 0xa0, 0x38, 0xaf, 0x79, 0x86, 0x50, 0xd6, 0xd8, 0xd1, 0x5c, 0x6b, 0x6c, 0x6c, + 0x0c, 0x6c, 0x0c, 0x60, 0x0e, 0x28, 0xce, 0x35, 0xfc, 0x36, 0xce, 0x36, 0x6a, 0x34, 0x87, 0x1b, + 0x35, 0x1b, 0x44, 0x1b, 0x44, 0x78, 0x04, 0x0a, 0x1b, 0x8d, 0x8b, 0x8d, 0xc0, 0x0d, 0xa7, 0x0d, + 0x15, 0xc6, 0xd3, 0x86, 0xe3, 0xc6, 0xe1, 0x46, 0x01, 0x22, 0x80, 0xe2, 0xdf, 0xa5, 0xcd, 0x03, + 0x5b, 0x63, 0x45, 0x71, 0xad, 0xb1, 0xb1, 0xc1, 0xb0, 0xd1, 0x80, +]; + +/// Some random CAM +#[cfg(feature = "cam")] +pub const CAM: &[u8] = &[ + 0x02, 0x02, 0xde, 0x14, 0x0c, 0xe5, 0xc7, 0xc0, 0x40, 0x5a, 0xb2, 0x3d, 0x82, 0xce, 0x27, 0x81, + 0xe9, 0xa2, 0x78, 0x27, 0x4b, 0xc6, 0x33, 0xfa, 0x54, 0x58, 0x7c, 0xa0, 0xa2, 0x7e, 0x83, 0x02, + 0x96, 0x8a, 0x97, 0x33, 0xff, 0x82, 0x00, 0x1a, 0x10, 0x3f, 0xe0, 0x14, 0x39, 0x80, 0x10, 0x6e, + 0x00, 0x75, 0x80, 0x11, 0x58, 0xce, 0x00, 0x02, 0xf0, 0x3a, 0xdc, 0x08, 0xc4, 0xc8, 0x00, 0x01, + 0x57, 0x81, 0xd6, 0x20, 0x46, 0x96, 0x33, 0x80, 0x0a, 0xbc, 0x0e, 0xdb, 0x02, 0x39, 0x31, 0x9c, + 0x00, 0x55, 0xe0, 0x75, 0x08, 0x11, 0x85, 0x90, 0x00, 0x02, 0xaf, 0x03, 0xa0, 0xc0, 0x91, 0x2c, + 0x80, 0x00, 0x16, 0x78, 0x1c, 0x9e, 0x05, 0x65, 0x64, 0x00, 0x00, 0xc3, 0xc0, 0xe0, 0x90, 0x2d, + 0xbb, 0x19, 0xc0, 0x06, 0xde, 0x05, 0x8d, 0x81, 0x02, 0x18, 0xce, 0x00, 0x35, 0xf0, 0x15, 0x5c, + 0x00, 0x06, 0xc6, 0x70, 0x00, 0xdf, 0x80, 0x8d, 0x5f, 0xde, 0x66, 0x27, 0x00, 0x07, 0x3c, 0x04, + 0x76, 0xfd, 0x67, 0x31, 0x9c, 0x00, 0x58, 0x60, 0x41, 0x37, 0xd3, 0x15, 0x89, 0xc0, 0x06, 0xdc, +];