use super::domain::{
Color, Depart, NamedColor, Route, RouteId, Routes, Vehicle, VehicleId, VehicleType,
VehicleTypeId,
};
use crate::schema;
use crate::sumo::{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;
impl From<schema::ColorType6Type> for NamedColor {
fn from(value: schema::ColorType6Type) -> Self {
use schema::ColorType6Type as S;
match value {
S::red => NamedColor::Red,
S::green => NamedColor::Green,
S::blue => NamedColor::Blue,
S::yellow => NamedColor::Yellow,
S::cyan => NamedColor::Cyan,
S::magenta => NamedColor::Magenta,
S::orange => NamedColor::Orange,
S::white => NamedColor::White,
S::black => NamedColor::Black,
S::grey | S::gray => NamedColor::Grey,
S::invisible => NamedColor::Invisible,
S::random => NamedColor::Random,
}
}
}
impl TryFrom<&str> for Color {
type Error = Error;
fn try_from(raw: &str) -> Result<Self> {
let parts = raw
.split(',')
.map(|part| parse_finite(part, "SUMO color component"))
.collect::<Result<Vec<_>>>()?;
match *parts.as_slice() {
[r, g, b] => Ok(Color::Rgba { r, g, b, a: 1.0 }),
[r, g, b, a] => Ok(Color::Rgba { r, g, b, a }),
_ => Err(Error::InvalidColor {
value: raw.to_owned(),
}),
}
}
}
fn named_color(raw: &str) -> Option<NamedColor> {
Some(match raw {
"red" => NamedColor::Red,
"green" => NamedColor::Green,
"blue" => NamedColor::Blue,
"yellow" => NamedColor::Yellow,
"cyan" => NamedColor::Cyan,
"magenta" => NamedColor::Magenta,
"orange" => NamedColor::Orange,
"white" => NamedColor::White,
"black" => NamedColor::Black,
"grey" | "gray" => NamedColor::Grey,
"invisible" => NamedColor::Invisible,
"random" => NamedColor::Random,
_ => return None,
})
}
impl TryFrom<schema::ColorType> for Color {
type Error = Error;
fn try_from(value: schema::ColorType) -> Result<Self> {
match value {
schema::ColorType::ColorType4(raw) => match named_color(&raw) {
Some(named) => Ok(Color::Named(named)),
None => Color::try_from(raw.as_str()),
},
schema::ColorType::ColorType6(named) => Ok(Color::Named(NamedColor::from(named))),
}
}
}
fn parse_clock_time(raw: &str) -> Result<Time> {
let parts = raw
.split(':')
.map(|part| parse_finite(part, "SUMO clock time component"))
.collect::<Result<Vec<_>>>()?;
let seconds = match *parts.as_slice() {
[h, m, s] => h * 3600.0 + m * 60.0 + s,
[h, m, s, ms] => h * 3600.0 + m * 60.0 + s + ms / 1000.0,
_ => {
return Err(Error::InvalidClockTime {
value: raw.to_owned(),
});
}
};
Ok(Time::new::<second>(seconds))
}
impl TryFrom<schema::SumoTimeType> for Time {
type Error = Error;
fn try_from(value: schema::SumoTimeType) -> Result<Self> {
match value {
schema::SumoTimeType::nonNegativeFloatType(seconds) => {
Ok(Time::new::<second>(f64::from(seconds)))
}
schema::SumoTimeType::SumoTimeType9(raw) => parse_clock_time(&raw),
}
}
}
fn depart_trigger(raw: &str) -> Option<Depart> {
Some(match raw {
"triggered" => Depart::Triggered,
"containerTriggered" => Depart::ContainerTriggered,
"split" => Depart::Split,
"begin" => Depart::Begin,
_ => return None,
})
}
impl TryFrom<schema::DepartType> for Depart {
type Error = Error;
fn try_from(value: schema::DepartType) -> Result<Self> {
if let schema::DepartType::sumoTimeType(schema::SumoTimeType::SumoTimeType9(raw)) = &value {
if let Some(depart) = depart_trigger(raw) {
return Ok(depart);
}
}
match value {
schema::DepartType::sumoTimeType(time) => Ok(Depart::Time(Time::try_from(time)?)),
schema::DepartType::DepartType13(trigger) => {
use schema::DepartType13Type as S;
Ok(match trigger {
S::triggered => Depart::Triggered,
S::containerTriggered => Depart::ContainerTriggered,
S::split => Depart::Split,
S::begin => Depart::Begin,
})
}
}
}
}
impl TryFrom<schema::RouteType> for Route {
type Error = Error;
fn try_from(value: schema::RouteType) -> Result<Self> {
Ok(Route {
id: RouteId(value.id),
edges: value.edges.as_deref().map(split_ids),
color: value.color.map(Color::try_from).transpose()?,
})
}
}
impl TryFrom<schema::VTypeType> for VehicleType {
type Error = Error;
fn try_from(value: schema::VTypeType) -> Result<Self> {
Ok(VehicleType {
id: VehicleTypeId(value.id),
vehicle_class: value.v_class,
length: value.length.map(|l| Length::new::<meter>(f64::from(l))),
max_speed: value
.max_speed
.map(|s| Velocity::new::<meter_per_second>(f64::from(s))),
color: value.color.map(Color::try_from).transpose()?,
})
}
}
impl TryFrom<schema::VehicleType> for Vehicle {
type Error = Error;
fn try_from(value: schema::VehicleType) -> Result<Self> {
Ok(Vehicle {
id: VehicleId(value.id),
vehicle_type: value.type_.map(VehicleTypeId),
route: value.route.map(RouteId),
depart: Depart::try_from(value.depart)?,
color: value.color.map(Color::try_from).transpose()?,
})
}
}
impl TryFrom<schema::RoutesType> for Routes {
type Error = Error;
fn try_from(value: schema::RoutesType) -> Result<Self> {
let mut routes = Routes::default();
for item in value.content {
match item {
schema::RoutesTypeContent::vType(Some(v_type)) => {
routes.vehicle_types.push(VehicleType::try_from(v_type)?);
}
schema::RoutesTypeContent::route(Some(route)) => {
routes.routes.push(Route::try_from(route)?);
}
schema::RoutesTypeContent::vehicle(Some(vehicle)) => {
routes.vehicles.push(Vehicle::try_from(vehicle)?);
}
_ => {}
}
}
Ok(routes)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::routes::domain::EdgeRef;
#[test]
fn reads_depart_triggers_even_though_the_union_always_offers_them_as_clock_time_text_first() {
let raw = schema::DepartType::sumoTimeType(schema::SumoTimeType::SumoTimeType9(
"begin".to_string(),
));
assert!(matches!(Depart::try_from(raw).unwrap(), Depart::Begin));
}
#[test]
fn reads_a_named_color_even_though_the_union_always_offers_it_as_color_type_4_first() {
for raw in ["red", "grey", "gray", "invisible", "random"] {
let color = Color::try_from(schema::ColorType::ColorType4(raw.to_string())).unwrap();
assert!(
matches!(color, Color::Named(_)),
"{raw:?} should read as a named color, got {color:?}"
);
}
}
#[test]
fn parses_numeric_rgb_color_defaulting_alpha() {
let color = Color::try_from("0.8,0.1,0.1").unwrap();
assert_eq!(
color,
Color::Rgba {
r: 0.8,
g: 0.1,
b: 0.1,
a: 1.0
}
);
}
#[test]
fn parses_numeric_rgba_color() {
let color = Color::try_from("0.8,0.1,0.1,0.5").unwrap();
assert_eq!(
color,
Color::Rgba {
r: 0.8,
g: 0.1,
b: 0.1,
a: 0.5
}
);
}
#[test]
fn rejects_malformed_color() {
assert!(Color::try_from("not-a-color").is_err());
assert!(Color::try_from("1,2").is_err());
assert!(Color::try_from("NaN,0,0").is_err());
}
#[test]
fn maps_both_spellings_of_grey_to_the_same_named_color() {
assert_eq!(
NamedColor::from(schema::ColorType6Type::grey),
NamedColor::Grey
);
assert_eq!(
NamedColor::from(schema::ColorType6Type::gray),
NamedColor::Grey
);
}
#[test]
fn parses_clock_time_without_milliseconds() {
use uom::si::time::second;
let time = parse_clock_time("1:02:03").unwrap();
assert_eq!(time.get::<second>(), 3723.0);
}
#[test]
fn parses_clock_time_with_milliseconds() {
use uom::si::time::second;
let time = parse_clock_time("0:00:01:500").unwrap();
assert_eq!(time.get::<second>(), 1.5);
}
#[test]
fn rejects_malformed_clock_time() {
assert!(parse_clock_time("not-a-time").is_err());
assert!(parse_clock_time("1:02").is_err());
assert!(parse_clock_time("NaN:02:03").is_err());
}
#[test]
fn splits_whitespace_separated_edge_list() {
assert_eq!(
split_ids::<EdgeRef>("e0 e1 e2"),
vec![
EdgeRef("e0".into()),
EdgeRef("e1".into()),
EdgeRef("e2".into())
]
);
}
}