use super::domain::{
Additional, DetectorGate, DetectorId, E1Detector, E2Detector, E3Detector, LaneCoverage,
LanePosition, LaneRef, Point,
};
use crate::schema;
use crate::sumo::{parse_bool_opt, parse_finite, split_ids};
use crate::{Error, Result};
use uom::si::f64::{Length, Time, Velocity};
use uom::si::length::meter;
use uom::si::time::second;
use uom::si::velocity::meter_per_second;
fn lane_position(raw: &str, context: &'static str) -> Result<LanePosition> {
let value = parse_finite(raw, context)?;
Ok(if value < 0.0 {
LanePosition::FromEnd(Length::new::<meter>(-value))
} else {
LanePosition::FromStart(Length::new::<meter>(value))
})
}
fn icon_position(raw: &str) -> Result<Point> {
Point::parse(raw, "detector icon position")
}
fn seconds(value: Option<f32>) -> Option<Time> {
value.map(|s| Time::new::<second>(f64::from(s)))
}
fn meters(value: Option<f32>) -> Option<Length> {
value.map(|m| Length::new::<meter>(f64::from(m)))
}
fn sampling_period(period: Option<f32>, freq: Option<f32>) -> Option<Time> {
seconds(period.or(freq))
}
impl TryFrom<schema::DetEntryExitType> for DetectorGate {
type Error = Error;
fn try_from(value: schema::DetEntryExitType) -> Result<Self> {
Ok(DetectorGate {
lane: LaneRef(value.lane),
position: lane_position(&value.pos, "detector gate position")?,
friendly_position: parse_bool_opt(value.friendly_pos)?,
})
}
}
impl TryFrom<schema::E1DetectorType> for E1Detector {
type Error = Error;
fn try_from(value: schema::E1DetectorType) -> Result<Self> {
Ok(E1Detector {
id: DetectorId(value.id),
lane: LaneRef(value.lane),
position: lane_position(&value.pos, "e1Detector position")?,
file: value.file,
period: sampling_period(value.period, value.freq),
length: meters(value.length),
name: value.name,
friendly_position: parse_bool_opt(value.friendly_pos)?,
})
}
}
fn lane_coverage(
lane: Option<String>,
lanes: Option<&str>,
length: Option<&str>,
) -> Result<Option<LaneCoverage>> {
match (lane, lanes) {
(Some(_), Some(_)) => Err(Error::ConflictingAttributes {
element: "e2Detector",
first: "lane",
second: "lanes",
}),
(_, Some(_)) if length.is_some() => Err(Error::ConflictingAttributes {
element: "e2Detector",
first: "lanes",
second: "length",
}),
(Some(lane), None) => Ok(Some(LaneCoverage::SingleLane(LaneRef(lane)))),
(None, Some(lanes)) => Ok(Some(LaneCoverage::LaneChain(split_ids(lanes)))),
(None, None) => Ok(None),
}
}
impl TryFrom<schema::E2DetectorType> for E2Detector {
type Error = Error;
fn try_from(value: schema::E2DetectorType) -> Result<Self> {
Ok(E2Detector {
id: DetectorId(value.id),
coverage: lane_coverage(value.lane, value.lanes.as_deref(), value.length.as_deref())?,
position: value
.pos
.as_deref()
.map(|pos| lane_position(pos, "e2Detector position"))
.transpose()?,
end_position: value
.end_pos
.as_deref()
.map(|pos| lane_position(pos, "e2Detector end position"))
.transpose()?,
length: value
.length
.as_deref()
.map(|raw| parse_finite(raw, "e2Detector length"))
.transpose()?
.map(Length::new::<meter>),
file: value.file,
period: sampling_period(value.period, value.freq),
name: value.name,
friendly_position: parse_bool_opt(value.friendly_pos)?,
speed_threshold: value
.speed_threshold
.as_deref()
.map(|raw| parse_finite(raw, "e2Detector speedThreshold"))
.transpose()?
.map(Velocity::new::<meter_per_second>),
time_threshold: value
.time_threshold
.as_deref()
.map(|raw| parse_finite(raw, "e2Detector timeThreshold"))
.transpose()?
.map(Time::new::<second>),
jam_threshold: value
.jam_threshold
.as_deref()
.map(|raw| parse_finite(raw, "e2Detector jamThreshold"))
.transpose()?
.map(Length::new::<meter>),
})
}
}
impl TryFrom<schema::E3DetectorType> for E3Detector {
type Error = Error;
fn try_from(value: schema::E3DetectorType) -> Result<Self> {
let mut entries = Vec::new();
let mut exits = Vec::new();
for item in value.content.det_gate_group {
match item.content {
schema::E3DetectorDetGateGroupTypeContent::detEntry(gates) => {
for gate in gates {
entries.push(DetectorGate::try_from(gate)?);
}
}
schema::E3DetectorDetGateGroupTypeContent::detExit(gates) => {
for gate in gates {
exits.push(DetectorGate::try_from(gate)?);
}
}
}
}
Ok(E3Detector {
id: DetectorId(value.id),
entries,
exits,
file: value.file,
period: sampling_period(value.period, value.freq),
name: value.name,
icon_position: value.pos.as_deref().map(icon_position).transpose()?,
speed_threshold: value
.speed_threshold
.map(|s| Velocity::new::<meter_per_second>(f64::from(s))),
time_threshold: value
.time_threshold
.as_deref()
.map(|raw| parse_finite(raw, "e3Detector timeThreshold"))
.transpose()?
.map(Time::new::<second>),
open_entry: parse_bool_opt(value.open_entry)?,
})
}
}
impl TryFrom<schema::AdditionalType> for Additional {
type Error = Error;
fn try_from(value: schema::AdditionalType) -> Result<Self> {
let mut additional = Additional::default();
for item in value.content {
match item {
schema::AdditionalTypeContent::e1Detector(Some(detector))
| schema::AdditionalTypeContent::inductionLoop(Some(detector)) => {
additional
.induction_loops
.push(E1Detector::try_from(detector)?);
}
schema::AdditionalTypeContent::e2Detector(Some(detector))
| schema::AdditionalTypeContent::laneAreaDetector(Some(detector)) => {
additional
.lane_area_detectors
.push(E2Detector::try_from(detector)?);
}
schema::AdditionalTypeContent::e3Detector(Some(detector))
| schema::AdditionalTypeContent::entryExitDetector(Some(detector)) => {
additional
.entry_exit_detectors
.push(E3Detector::try_from(detector)?);
}
_ => {}
}
}
Ok(additional)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn reads_the_sign_of_a_position_as_which_end_it_is_measured_from() {
assert_eq!(
lane_position("12.5", "test").unwrap(),
LanePosition::FromStart(Length::new::<meter>(12.5))
);
assert_eq!(
lane_position("-12.5", "test").unwrap(),
LanePosition::FromEnd(Length::new::<meter>(12.5))
);
}
#[test]
fn rejects_the_lane_combinations_sumo_rejects() {
assert!(lane_coverage(Some("e0_0".into()), Some("e0_0 e1_0"), None).is_err());
assert!(lane_coverage(None, Some("e0_0 e1_0"), Some("50.0")).is_err());
assert_eq!(
lane_coverage(Some("e0_0".into()), None, Some("50.0")).unwrap(),
Some(LaneCoverage::SingleLane(LaneRef("e0_0".into())))
);
assert_eq!(lane_coverage(None, None, Some("50.0")).unwrap(), None);
assert_eq!(lane_coverage(None, None, None).unwrap(), None);
}
#[test]
fn parses_the_icon_position_with_and_without_z() {
assert_eq!(
icon_position("42,7").unwrap(),
Point {
x: 42.0,
y: 7.0,
z: 0.0
}
);
assert_eq!(
icon_position("42,7,1.5").unwrap(),
Point {
x: 42.0,
y: 7.0,
z: 1.5
}
);
assert!(icon_position("42").is_err());
assert!(icon_position("42,7,1,9").is_err());
}
#[test]
fn falls_back_from_period_to_the_deprecated_freq() {
use uom::si::time::second;
assert_eq!(
sampling_period(Some(60.0), None).unwrap().get::<second>(),
60.0
);
assert_eq!(
sampling_period(None, Some(30.0)).unwrap().get::<second>(),
30.0
);
assert_eq!(
sampling_period(Some(60.0), Some(30.0))
.unwrap()
.get::<second>(),
60.0
);
assert!(sampling_period(None, None).is_none());
}
#[test]
fn splits_a_lane_chain() {
assert_eq!(
split_ids::<LaneRef>("e0_0 e1_0 e2_0"),
vec![
LaneRef("e0_0".into()),
LaneRef("e1_0".into()),
LaneRef("e2_0".into())
]
);
}
#[test]
fn rejects_bool_without_binary_meaning() {
assert!(parse_bool_opt(Some(schema::BoolType::x)).is_err());
assert_eq!(parse_bool_opt(None).unwrap(), None);
assert_eq!(
parse_bool_opt(Some(schema::BoolType::True)).unwrap(),
Some(true)
);
}
}