use super::domain::{
Color, Depart, EdgeRef, NamedColor, Route, RouteId, Routes, Vehicle, VehicleId, VehicleType,
VehicleTypeId,
};
use crate::schema;
use anyhow::{Context, Result, bail};
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 split_edge_refs(raw: &str) -> Vec<EdgeRef> {
raw.split_whitespace()
.map(|s| EdgeRef(s.to_owned()))
.collect()
}
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 = anyhow::Error;
fn try_from(raw: &str) -> Result<Self> {
let parts = raw
.split(',')
.map(|part| {
part.parse::<f64>()
.with_context(|| format!("invalid SUMO color: {raw:?}"))
})
.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 }),
_ => bail!("SUMO color must have 3 or 4 components: {raw:?}"),
}
}
}
impl TryFrom<schema::ColorType> for Color {
type Error = anyhow::Error;
fn try_from(value: schema::ColorType) -> Result<Self> {
match value {
schema::ColorType::ColorType4(raw) => 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| {
part.parse::<f64>()
.with_context(|| format!("invalid SUMO clock time: {raw:?}"))
})
.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,
_ => bail!("SUMO clock time must have 3 or 4 components: {raw:?}"),
};
Ok(Time::new::<second>(seconds))
}
impl TryFrom<schema::SumoTimeType> for Time {
type Error = anyhow::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),
}
}
}
impl TryFrom<schema::DepartType> for Depart {
type Error = anyhow::Error;
fn try_from(value: schema::DepartType) -> Result<Self> {
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 = anyhow::Error;
fn try_from(value: schema::RouteType) -> Result<Self> {
Ok(Route {
id: RouteId(value.id),
edges: value.edges.as_deref().map(split_edge_refs),
color: value.color.map(Color::try_from).transpose()?,
})
}
}
impl TryFrom<schema::VTypeType> for VehicleType {
type Error = anyhow::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 = anyhow::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 = anyhow::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::*;
#[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());
}
#[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());
}
#[test]
fn splits_whitespace_separated_edge_list() {
assert_eq!(
split_edge_refs("e0 e1 e2"),
vec![
EdgeRef("e0".into()),
EdgeRef("e1".into()),
EdgeRef("e2".into())
]
);
}
}