use super::domain::{
Color, Depart, EdgeRef, NamedColor, Person, PersonId, Route, RouteId, Routes, VClass, Vehicle,
VehicleId, VehicleType, VehicleTypeId, Walk,
};
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;
macro_rules! named_colors {
($($variant:ident: $spelling:ident $(| $alias:ident)* => $str:literal $(| $str_alias:literal)*),+ $(,)?) => {
impl From<schema::ColorType6Type> for NamedColor {
fn from(value: schema::ColorType6Type) -> Self {
use schema::ColorType6Type as S;
match value {
$(S::$spelling $(| S::$alias)* => NamedColor::$variant,)+
}
}
}
fn named_color(raw: &str) -> Option<NamedColor> {
Some(match raw {
$($str $(| $str_alias)* => NamedColor::$variant,)+
_ => return None,
})
}
#[cfg(feature = "write")]
impl From<&NamedColor> for schema::ColorType6Type {
fn from(value: &NamedColor) -> Self {
match value {
$(NamedColor::$variant => schema::ColorType6Type::$spelling,)+
}
}
}
};
}
named_colors! {
Red: red => "red",
Green: green => "green",
Blue: blue => "blue",
Yellow: yellow => "yellow",
Cyan: cyan => "cyan",
Magenta: magenta => "magenta",
Orange: orange => "orange",
White: white => "white",
Black: black => "black",
Grey: grey | gray => "grey" | "gray",
Invisible: invisible => "invisible",
Random: random => "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(),
}),
}
}
}
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
.map(|raw| raw.parse::<VClass>().map_err(|_| Error::InvalidVehicleClass { value: raw }))
.transpose()?,
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 From<schema::PersonBaseTypeWalk> for Walk {
fn from(value: schema::PersonBaseTypeWalk) -> Self {
match (value.edges.as_deref().map(split_ids), value.from, value.to) {
(Some(edges), ..) => Walk::Edges(edges),
(None, Some(from), Some(to)) => Walk::Endpoints { from: EdgeRef(from), to: EdgeRef(to) },
(None, _, _) => Walk::Unknown,
}
}
}
impl From<schema::PersonType> for Person {
fn from(value: schema::PersonType) -> Self {
let walks = value
.content
.into_iter()
.filter_map(|item| match item {
schema::PersonTypeContent::walk(walk) => Some(Walk::from(walk)),
_ => None,
})
.collect();
Person {
id: PersonId(value.id),
walks,
}
}
}
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)?);
}
schema::RoutesTypeContent::person(Some(person)) => {
routes.persons.push(Person::from(person));
}
_ => {}
}
}
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())
]
);
}
}