use super::domain::{
Boundary, Connection, ConnectionDirection, Edge, EdgeFunction, EdgeId, Junction, JunctionId,
JunctionKind, Lane, LaneId, LaneIndex, LinkIndex, LinkState, Location, Network, Phase, Point,
Priority, Projection, Roundabout, Shape, SpreadType, TrafficLightId, TrafficLightKind,
TrafficLightOffset, TrafficLightProgram, default_lane_end_offset, default_lane_width,
};
use crate::schema;
use crate::sumo::{parse_bool_opt, parse_finite, split_ids, split_ids_opt};
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;
impl TryFrom<&str> for Shape {
type Error = Error;
fn try_from(raw: &str) -> Result<Self> {
raw.split_whitespace()
.map(Point::try_from)
.collect::<Result<Vec<_>>>()
.map(Shape)
}
}
impl TryFrom<&str> for Boundary {
type Error = Error;
fn try_from(raw: &str) -> Result<Self> {
let coords = raw
.split(',')
.map(|part| parse_finite(part, "SUMO boundary component"))
.collect::<Result<Vec<_>>>()?;
let [min_x, min_y, max_x, max_y]: [f64; 4] =
coords.try_into().map_err(|_| Error::InvalidBoundary {
value: raw.to_owned(),
})?;
Ok(Boundary {
min: Point { x: min_x, y: min_y, z: 0.0 },
max: Point { x: max_x, y: max_y, z: 0.0 },
})
}
}
impl From<String> for Projection {
fn from(value: String) -> Self {
match value.as_str() {
"!" => Projection::None,
_ => Projection::Proj4(value),
}
}
}
impl TryFrom<schema::LocationType> for Location {
type Error = Error;
fn try_from(value: schema::LocationType) -> Result<Self> {
Ok(Location {
net_offset: Point::try_from(value.net_offset.as_deref().ok_or(
Error::MissingAttribute {
element: "location",
attribute: "netOffset",
},
)?)?,
converted_boundary: Boundary::try_from(value.conv_boundary.as_deref().ok_or(
Error::MissingAttribute {
element: "location",
attribute: "convBoundary",
},
)?)?,
original_boundary: Boundary::try_from(value.orig_boundary.as_deref().ok_or(
Error::MissingAttribute {
element: "location",
attribute: "origBoundary",
},
)?)?,
projection: Projection::from(value.proj_parameter),
})
}
}
impl TryFrom<schema::LaneType> for Lane {
type Error = Error;
fn try_from(value: schema::LaneType) -> Result<Self> {
Ok(Lane {
id: LaneId(value.id),
index: LaneIndex(value.index),
speed: Velocity::new::<meter_per_second>(f64::from(value.speed)),
length: Length::new::<meter>(f64::from(value.length)),
width: value
.width
.map_or_else(default_lane_width, |w| Length::new::<meter>(f64::from(w))),
end_offset: value.end_offset.map_or_else(default_lane_end_offset, |o| {
Length::new::<meter>(f64::from(o))
}),
shape: Shape::try_from(value.shape.as_str())?,
allow: split_ids_opt(value.allow.as_deref()),
disallow: split_ids_opt(value.disallow.as_deref()),
})
}
}
impl From<schema::EdgeTypeFunctionType> for EdgeFunction {
fn from(value: schema::EdgeTypeFunctionType) -> Self {
use schema::EdgeTypeFunctionType as S;
match value {
S::normal => EdgeFunction::Normal,
S::internal => EdgeFunction::Internal,
S::connector => EdgeFunction::Connector,
S::crossing => EdgeFunction::Crossing,
S::walkingarea => EdgeFunction::Walkingarea,
}
}
}
impl From<schema::SpreadTypeType> for SpreadType {
fn from(value: schema::SpreadTypeType) -> Self {
use schema::SpreadTypeType as S;
match value {
S::center => SpreadType::Center,
S::roadCenter => SpreadType::RoadCenter,
S::right => SpreadType::Right,
}
}
}
impl TryFrom<schema::EdgeType> for Edge {
type Error = Error;
fn try_from(value: schema::EdgeType) -> Result<Self> {
let lanes = value
.content
.into_iter()
.filter_map(|content| match content {
schema::EdgeTypeContent::lane(lanes) => Some(lanes),
_ => None,
})
.flatten()
.map(Lane::try_from)
.collect::<Result<Vec<_>>>()?;
Ok(Edge {
id: EdgeId(value.id),
function: value
.function
.map_or(EdgeFunction::Normal, EdgeFunction::from),
from: value.from.map(JunctionId),
to: value.to.map(JunctionId),
name: value.name,
priority: value.priority.map(Priority),
length: value.length.map(|l| Length::new::<meter>(f64::from(l))),
shape: value.shape.as_deref().map(Shape::try_from).transpose()?,
spread_type: value.spread_type.map(SpreadType::from),
lanes,
})
}
}
impl From<schema::JunctionTypeType> for JunctionKind {
fn from(value: schema::JunctionTypeType) -> Self {
use schema::JunctionTypeType as S;
match value {
S::traffic_light => JunctionKind::TrafficLight,
S::traffic_light_unregulated => JunctionKind::TrafficLightUnregulated,
S::traffic_light_right_on_red => JunctionKind::TrafficLightRightOnRed,
S::rail_signal => JunctionKind::RailSignal,
S::rail_crossing => JunctionKind::RailCrossing,
S::priority => JunctionKind::Priority,
S::priority_stop => JunctionKind::PriorityStop,
S::left_before_right => JunctionKind::LeftBeforeRight,
S::right_before_left => JunctionKind::RightBeforeLeft,
S::allway_stop => JunctionKind::AllWayStop,
S::zipper => JunctionKind::Zipper,
S::district => JunctionKind::District,
S::unregulated => JunctionKind::Unregulated,
S::internal => JunctionKind::Internal,
S::dead_end => JunctionKind::DeadEnd,
}
}
}
impl TryFrom<schema::JunctionType> for Junction {
type Error = Error;
fn try_from(value: schema::JunctionType) -> Result<Self> {
Ok(Junction {
id: JunctionId(value.id),
position: Point {
x: parse_finite(&value.x, "junction x coordinate")?,
y: parse_finite(&value.y, "junction y coordinate")?,
z: value
.z
.as_deref()
.map(|z| parse_finite(z, "junction z coordinate"))
.transpose()?
.unwrap_or(0.0),
},
kind: JunctionKind::from(value.type_),
incoming_lanes: split_ids(&value.inc_lanes),
internal_lanes: split_ids(&value.int_lanes),
shape: value.shape.as_deref().map(Shape::try_from).transpose()?,
name: value.name,
})
}
}
impl From<schema::ConnectionTypeDirType> for ConnectionDirection {
fn from(value: schema::ConnectionTypeDirType) -> Self {
use schema::ConnectionTypeDirType as S;
match value {
S::s => ConnectionDirection::Straight,
S::t => ConnectionDirection::Turn,
S::T => ConnectionDirection::TurnLeftHand,
S::l => ConnectionDirection::Left,
S::r => ConnectionDirection::Right,
S::L => ConnectionDirection::PartialLeft,
S::R => ConnectionDirection::PartialRight,
}
}
}
impl From<schema::ConnectionTypeStateType> for LinkState {
fn from(value: schema::ConnectionTypeStateType) -> Self {
use schema::ConnectionTypeStateType as S;
match value {
S::M => LinkState::Major,
S::m => LinkState::Minor,
S::O => LinkState::TlsOffNoSignal,
S::o => LinkState::TlsOffBlinking,
S::Eq => LinkState::Equal,
S::s => LinkState::Stop,
S::w => LinkState::AllWayStop,
S::Z => LinkState::Zipper,
S::Dash => LinkState::DeadEnd,
}
}
}
impl TryFrom<schema::ConnectionType> for Connection {
type Error = Error;
fn try_from(value: schema::ConnectionType) -> Result<Self> {
Ok(Connection {
from_edge: EdgeId(value.from),
to_edge: EdgeId(value.to),
from_lane: LaneIndex(value.from_lane),
to_lane: LaneIndex(value.to_lane),
direction: ConnectionDirection::from(value.dir),
state: LinkState::from(value.state),
via: value.via.map(LaneId),
traffic_light: value.tl.map(TrafficLightId),
link_index: value.link_index.map(LinkIndex),
pass: parse_bool_opt(value.pass)?.unwrap_or(false),
keep_clear: parse_bool_opt(value.keep_clear)?.unwrap_or(true),
})
}
}
impl TryFrom<schema::RoundaboutType> for Roundabout {
type Error = Error;
fn try_from(value: schema::RoundaboutType) -> Result<Self> {
Ok(Roundabout {
nodes: split_ids(&value.nodes),
edges: split_ids(&value.edges),
})
}
}
impl From<schema::TlTypeType> for TrafficLightKind {
fn from(value: schema::TlTypeType) -> Self {
use schema::TlTypeType as S;
match value {
S::actuated => TrafficLightKind::Actuated,
S::delay_based => TrafficLightKind::DelayBased,
S::static_ => TrafficLightKind::Static,
S::NEMA => TrafficLightKind::Nema,
}
}
}
impl TryFrom<schema::OffsetType> for TrafficLightOffset {
type Error = Error;
fn try_from(value: schema::OffsetType) -> Result<Self> {
match value {
schema::OffsetType::sumoFloatType(raw) => parse_finite(&raw, "tlLogic offset")
.map(|seconds| TrafficLightOffset::Fixed(Time::new::<second>(seconds))),
schema::OffsetType::OffsetType11(schema::OffsetType11Type::begin) => {
Ok(TrafficLightOffset::Begin)
}
}
}
}
impl From<schema::PhaseType> for Phase {
fn from(value: schema::PhaseType) -> Self {
Phase {
duration: Time::new::<second>(f64::from(value.duration)),
state: value.state,
}
}
}
impl TryFrom<schema::TlLogicType> for TrafficLightProgram {
type Error = Error;
fn try_from(value: schema::TlLogicType) -> Result<Self> {
let phases = value
.content
.into_iter()
.filter_map(|content| match content {
schema::TlLogicTypeContent::phase(phase) => Some(Phase::from(phase)),
_ => None,
})
.collect();
Ok(TrafficLightProgram {
id: TrafficLightId(value.id),
program_id: value.program_id,
kind: value.type_.map(TrafficLightKind::from),
offset: value.offset.map(TrafficLightOffset::try_from).transpose()?,
phases,
})
}
}
impl TryFrom<schema::NetType> for Network {
type Error = Error;
fn try_from(value: schema::NetType) -> Result<Self> {
let content = value.content;
Ok(Network {
location: Location::try_from(content.location)?,
edges: content
.edge
.into_iter()
.map(Edge::try_from)
.collect::<Result<Vec<_>>>()?,
junctions: content
.junction
.into_iter()
.map(Junction::try_from)
.collect::<Result<Vec<_>>>()?,
connections: content
.connection
.into_iter()
.map(Connection::try_from)
.collect::<Result<Vec<_>>>()?,
roundabouts: content
.roundabout
.into_iter()
.map(Roundabout::try_from)
.collect::<Result<Vec<_>>>()?,
traffic_light_programs: content
.tl_logic
.into_iter()
.map(TrafficLightProgram::try_from)
.collect::<Result<Vec<_>>>()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_junction_with_quirky_sumo_spellings() {
let junction = schema::JunctionType {
id: "j0".into(),
x: "10.5".into(),
y: "-3".into(),
z: None,
type_: schema::JunctionTypeType::traffic_light,
inc_lanes: "e0_0 e0_1".into(),
int_lanes: String::new(),
shape: Some("0,0 1,1 2,2".into()),
name: None,
radius: None,
custom_shape: None,
right_of_way: None,
fringe: None,
roundabout: None,
content: schema::JunctionTypeContent {
request: vec![],
param: vec![],
},
};
let domain = Junction::try_from(junction).unwrap();
assert_eq!(
domain.position,
Point {
x: 10.5,
y: -3.0,
z: 0.0
}
);
assert_eq!(domain.kind, JunctionKind::TrafficLight);
assert_eq!(
domain.incoming_lanes,
vec![LaneId("e0_0".into()), LaneId("e0_1".into())]
);
assert_eq!(domain.shape.unwrap().0.len(), 3);
}
#[test]
fn parses_connection_dir_and_state_single_char_codes() {
let connection = schema::ConnectionType {
from: "e0".into(),
to: "e1".into(),
from_lane: 0,
to_lane: 0,
pass: None,
keep_clear: None,
cont_pos: None,
visibility: None,
allow: None,
disallow: None,
speed: None,
length: None,
shape: None,
uncontrolled: None,
via: Some("e0_via".into()),
tl: None,
link_index: Some(3),
link_index_2: None,
change_right: None,
change_left: None,
indirect: None,
type_: None,
dir: schema::ConnectionTypeDirType::s,
state: schema::ConnectionTypeStateType::M,
content: vec![],
};
let domain = Connection::try_from(connection).unwrap();
assert_eq!(domain.direction, ConnectionDirection::Straight);
assert_eq!(domain.state, LinkState::Major);
assert!(
domain.keep_clear,
"keepClear defaults to true in SUMO when absent"
);
assert!(!domain.pass, "pass defaults to false when absent");
}
fn sample_location(proj_parameter: &str) -> schema::LocationType {
schema::LocationType {
net_offset: Some("0.00,0.00".into()),
conv_boundary: Some("0.00,0.00,100.00,100.00".into()),
orig_boundary: Some("-1.00,-1.00,1.00,1.00".into()),
proj_parameter: proj_parameter.into(),
}
}
#[test]
fn recognizes_the_unprojected_sentinel() {
let domain = Location::try_from(sample_location("!")).unwrap();
assert_eq!(domain.projection, Projection::None);
}
#[test]
fn keeps_a_real_proj4_string_as_is() {
let domain =
Location::try_from(sample_location("+proj=utm +zone=32 +ellps=WGS84")).unwrap();
assert_eq!(
domain.projection,
Projection::Proj4("+proj=utm +zone=32 +ellps=WGS84".into())
);
}
#[test]
fn rejects_location_missing_required_attributes() {
let mut location = sample_location("!");
location.net_offset = None;
assert!(Location::try_from(location).is_err());
}
#[test]
fn converts_lane_speed_and_length_with_real_units() {
use uom::si::length::centimeter;
use uom::si::velocity::kilometer_per_hour;
let lane = schema::LaneType {
id: "e0_0".into(),
index: 0,
allow: None,
disallow: None,
prefer: None,
speed: 13.89, friction: None,
length: 100.0,
end_offset: None,
width: Some(3.5),
acceleration: None,
shape: "0,0 100,0".into(),
custom_shape: None,
type_: None,
change_right: None,
change_left: None,
outline_shape: None,
content: vec![],
};
let domain = Lane::try_from(lane).unwrap();
assert!((domain.speed.get::<kilometer_per_hour>() - 50.0).abs() < 0.01);
assert_eq!(domain.length.get::<meter>(), 100.0);
assert_eq!(domain.width.get::<centimeter>(), 350.0); assert_eq!(
domain.end_offset,
default_lane_end_offset(),
"omitted endOffset should fall back to SUMO's default (0)"
);
}
#[test]
fn falls_back_to_sumos_default_lane_width_when_omitted() {
let lane = schema::LaneType {
id: "e0_0".into(),
index: 0,
allow: None,
disallow: None,
prefer: None,
speed: 13.89,
friction: None,
length: 100.0,
end_offset: None,
width: None,
acceleration: None,
shape: "0,0 100,0".into(),
custom_shape: None,
type_: None,
change_right: None,
change_left: None,
outline_shape: None,
content: vec![],
};
let domain = Lane::try_from(lane).unwrap();
assert_eq!(domain.width, default_lane_width());
}
}