0
0
Fork 0

Evaluate remaining red/green time for upcoming intersection

This commit is contained in:
Jannik Beyerstedt 2026-05-30 23:34:06 +02:00
commit 7463adb038
12 changed files with 2979 additions and 103 deletions

123
Cargo.lock generated
View file

@ -17,6 +17,56 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c583acf993cf4245c4acb0a2cc2ab1f9cc097de73411bb6d3647ff6af2b1013d" 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]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
@ -29,6 +79,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "assert_float_eq"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd9777fd8234cccd34019d9e26d70c8247f9baf24c8fdc0b56c548cf943af2ed"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.0"
@ -164,6 +220,12 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "colorchoice"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]] [[package]]
name = "const-default" name = "const-default"
version = "1.0.0" version = "1.0.0"
@ -633,6 +695,29 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -1011,6 +1096,7 @@ dependencies = [
name = "esp32-c_its-companion" name = "esp32-c_its-companion"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"assert_float_eq",
"c-its-parser", "c-its-parser",
"chrono", "chrono",
"critical-section", "critical-section",
@ -1020,6 +1106,7 @@ dependencies = [
"embassy_gps", "embassy_gps",
"embedded-io 0.7.1", "embedded-io 0.7.1",
"embedded-io-async 0.7.0", "embedded-io-async 0.7.0",
"env_logger",
"esp-alloc", "esp-alloc",
"esp-backtrace", "esp-backtrace",
"esp-bootloader-esp-idf", "esp-bootloader-esp-idf",
@ -1027,7 +1114,9 @@ dependencies = [
"esp-println", "esp-println",
"esp-radio", "esp-radio",
"esp-rtos", "esp-rtos",
"geo-types",
"log", "log",
"num-traits",
] ]
[[package]] [[package]]
@ -1333,6 +1422,12 @@ dependencies = [
"syn 2.0.117", "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]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@ -1594,6 +1689,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "paste" name = "paste"
version = "1.0.15" version = "1.0.15"
@ -1757,6 +1858,18 @@ dependencies = [
"uuid", "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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.14" version = "0.4.14"
@ -1770,9 +1883,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.10" version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]] [[package]]
name = "riscv" name = "riscv"
@ -2202,6 +2315,12 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.23.1" version = "1.23.1"

View file

@ -40,6 +40,8 @@ cam = ["c-its-parser/cam"]
denm = ["c-its-parser/denm"] denm = ["c-its-parser/denm"]
# evaluate SPAT information # evaluate SPAT information
spat = ["c-its-parser/spatem", "c-its-parser/mapem"] spat = ["c-its-parser/spatem", "c-its-parser/mapem"]
# enable some debug printing (without enabling debug logging of the ESP RTOS)
spat_debug = []
[dependencies] [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"] } chrono = { version = "0.4.44", default-features = false, features = ["alloc"] }
crossbeam-queue = { version = "0.3.12", 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 # For fine tuning these settings, please refer to https://doc.rust-lang.org/cargo/reference/profiles.html

View file

@ -2,6 +2,29 @@
Displays different C-ITS receive-only use cases for cheap. 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 ## Usage
### Toolchain Installation ### Toolchain Installation
@ -24,10 +47,23 @@ rustup component add --toolchain nightly rustfmt
``` ```
### Build and Flash ### 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: Connect your ESP32-C5 and run:
``` ```
# default configuration
cargo run --release cargo run --release
# for e.g. also enable DENM feature
cargo run --release -F denm
``` ```
### Run Unit Tests on Host ### Run Unit Tests on Host

View file

@ -1,8 +1,551 @@
//! Application Logic //! 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")] #[cfg(feature = "cam")]
pub mod cam; pub mod cam;
#[cfg(feature = "denm")] #[cfg(feature = "denm")]
pub mod denm; pub mod denm;
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
pub mod tlc; 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<f32>,
speed_mps: Option<f32>,
#[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<core::cell::RefCell<v2x::map::Intersection>>,
lane_id: u8,
approach_id: Option<u8>,
signal_groups: Vec<u8>,
}
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::<Vec<_>>();
// 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<TlData> {
// 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::<Vec<_>>();
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::<Vec<_>>()
} 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::<Vec<_>>()
};
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<u8>,
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));
}
}

View file

@ -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 alloc::rc::Rc;
use c_its_parser::standards::spatem_2_2_1::spatem_pdu_descriptions; 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; use chrono::Datelike;
#[cfg(target_arch = "riscv32")] #[cfg(all(target_arch = "riscv32", feature = "spat_debug"))]
use esp_println::println; use esp_println::println;
pub fn handle_mapem(etsi: &mapem_pdu_descriptions::MAPEM) { use super::v2x::map;
// TODO: proof of concept only -- replace with actual evaluation use crate::cache::Cache;
if let Some(map) = &etsi.map.intersections {
for int in &map.0 { pub struct Data {
println!("got MAPEM for intersection {}", int.id.id.0); intersections: Cache<u16, map::Intersection>,
}
}
} }
pub fn handle_spatem(etsi: &spatem_pdu_descriptions::SPATEM, now: &chrono::NaiveDateTime) { impl Data {
// TODO: proof of concept only -- replace with actual evaluation pub fn new(cache_lifetime: chrono::Duration) -> Self {
for int in &etsi.spat.intersections.0 { Self {
println!("got SPATEM for intersection {}", int.id.id.0); intersections: Cache::<u16, map::Intersection>::new(cache_lifetime),
}
}
if let (Some(moy), Some(second)) = (&int.moy, &int.time_stamp) { pub fn known_intersections(&self) -> Vec<u16> {
let year = now.year(); self.intersections.keys()
}
let ref_time = c_its_parser::time_utils::time_from_moy_and_dsecond(moy, second, year); pub fn intersections_matching<P>(
println!("- ref time: {ref_time:?}"); &self,
predicate: P,
) -> Vec<Rc<core::cell::RefCell<map::Intersection>>>
where
P: Fn(&map::Intersection) -> bool,
{
self.intersections.get_filtered(predicate)
}
for state in &int.states.0 { pub fn handle_mapem(&mut self, time_now: chrono::NaiveDateTime, etsi: &MAPEM) {
let sig_grp = state.signal_group.0; 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 self.intersections.contains(&int_id) {
if let Some(timing) = &event.timing { // update with new map layers
let min_end_time = timing.min_end_time.to_datetime_from_moy(moy, year);
println!( // we can ignore the result, since we're sure the item exists
"- sig grp. {sig_grp:03}: until {min_end_time:?} at {:?}", let _ = self.intersections.update(time_now, int_id, |i| {
event.event_state if !i.is_complete() {
); i.add_layer(layer_id, geom);
} else {
println!( #[cfg(feature = "spat_debug")]
"- sig grp. {sig_grp:03}: {:?} until unknown", if i.is_complete() {
event.event_state Self::pretty_print_intersection(&i);
); }
} }
});
} else { } 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
}
}
}
} }

1019
src/applogic/v2x/map.rs Normal file

File diff suppressed because it is too large Load diff

4
src/applogic/v2x/mod.rs Normal file
View file

@ -0,0 +1,4 @@
//! Types for V2X data handling
#[cfg(feature = "spat")]
pub mod map;

221
src/cache.rs Normal file
View file

@ -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<K>
where
K: core::cmp::PartialEq,
{
fn key(&self) -> K;
}
struct CacheItem<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq,
{
timestamp: chrono::NaiveDateTime,
data: Rc<core::cell::RefCell<T>>,
phantom: core::marker::PhantomData<K>, // rust wants this
}
impl<K, T> core::fmt::Display for CacheItem<K, T>
where
T: Cachable<K>,
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<K, T> CacheItem<K, T>
where
T: Cachable<K>,
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<K>
where
K: core::fmt::Display,
{
KeyNotFound(K),
InsertKeyAlreadyPresent(K),
}
impl<K> core::fmt::Display for CacheError<K>
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<K, T>
where
T: Cachable<K>,
K: core::cmp::PartialEq,
{
lifetime: chrono::Duration,
items: Vec<CacheItem<K, T>>,
}
impl<K, T> core::fmt::Display for Cache<K, T>
where
T: Cachable<K>,
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<K, T> Cache<K, T>
where
T: Cachable<K> + 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<K> {
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<K>> {
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<Rc<core::cell::RefCell<T>>> {
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<P>(&self, predicate: P) -> Vec<Rc<core::cell::RefCell<T>>>
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<F>(
&mut self,
time_now: chrono::NaiveDateTime,
key: K,
func: F,
) -> Result<(), CacheError<K>>
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
});
}
}

446
src/geo_alg.rs Normal file
View file

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

View file

@ -23,65 +23,13 @@ use log::{debug, error, info, warn};
extern crate alloc; extern crate alloc;
mod applogic; mod applogic;
#[cfg(feature = "spat")]
mod cache;
mod geo_alg;
mod radio; mod radio;
const WIFI_CHANNEL: radio::Channel = 180; 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<f32>,
speed_mps: Option<f32>,
}
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. // This creates a default app-descriptor required by the esp-idf bootloader.
// For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description> // For more information see: <https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/app_image_format.html#application-description>
esp_bootloader_esp_idf::esp_app_desc!(); esp_bootloader_esp_idf::esp_app_desc!();
@ -157,7 +105,7 @@ async fn main(spawner: Spawner) -> ! {
); );
info!("GNSS task started, waiting for GNSS fix..."); info!("GNSS task started, waiting for GNSS fix...");
let mut state = State::default(); let mut state = applogic::State::new();
loop { loop {
embassy_time::Timer::after(embassy_time::Duration::from_millis(5)).await; 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) { if let Some(fix) = gnss_update_ref.replace(None) {
state.update_with_gpsfix(&fix); state.update_with_gpsfix(&fix);
if let Some(time) = fix.get_timestamp() { if fix.get_timestamp().is_some() {
state.time = time; info!("{}", state.print_fix());
info!("{}", state.print_fix()); // TODO: degrate to debug later?
// run GLOSA algorithm
#[cfg(feature = "spat")]
state.run_glosa();
} }
} }
}); });
@ -193,15 +144,15 @@ async fn main(spawner: Spawner) -> ! {
geonetworking: _, geonetworking: _,
transport: _, transport: _,
etsi, etsi,
} => applogic::tlc::handle_mapem(&etsi), } => state.handle_mapem(&etsi),
#[cfg(feature = "spat")] #[cfg(feature = "spat")]
ItsMessage::Spatem { ItsMessage::Spatem {
geonetworking: _, geonetworking: _,
transport: _, transport: _,
etsi, etsi,
} => { } => {
if state.initialized { if state.initialized() {
applogic::tlc::handle_spatem(&etsi, &state.time); state.handle_spatem(&etsi);
} }
} }
@ -235,6 +186,9 @@ async fn main(spawner: Spawner) -> ! {
} }
} }
}); });
// maintenance
state.prune();
} }
} }

View file

@ -1,10 +1,23 @@
#![cfg(not(target_arch = "riscv32"))] #![cfg(not(target_arch = "riscv32"))]
#![allow(unused)]
extern crate alloc; extern crate alloc;
mod applogic; mod applogic;
mod cache; mod cache;
mod geo_alg;
mod testdata;
fn main() { fn main() {
// env_logger::init();
// dummy only // 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();
}

457
src/testdata.rs Normal file
View file

@ -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,
];