sumo-types 0.3.0

Reads and writes SUMO (Eclipse SUMO) .net.xml, .rou.xml and .add.xml files as well-typed Rust structs
Documentation
//! Layer 2 for `routes`: this crate's own domain types for `.rou.xml`.
//!
//! Independent of SUMO/XSD, same rationale as `net`'s own `crate::domain`
//! (see the crate docs) — and independent of `net`'s own domain types too,
//! by design: `routes` doesn't require `net` to be enabled, so it can't
//! borrow `crate::domain::EdgeId` and defines its own [`EdgeRef`] instead.
//!
//! Those cross-format names are code spans, not intra-doc links: `net` may
//! not be enabled, and a link to a feature-gated item is a broken link in
//! every build that leaves that feature off.
//!
//! Only the building blocks of a traffic demand file are modeled:
//! [`Route`], [`VehicleType`], [`Vehicle`], and — read-only, and only its
//! [`Walk`] stages — [`Person`]. `routesType` also allows `flow`, `trip`,
//! `personFlow`, `container`, `containerFlow`, `interval`, `include`,
//! `vTypeDistribution`, and `routeDistribution` elements, none of which are
//! mapped yet — [`Routes`] silently drops them (see
//! [`crate::routes::schema_mapper`]).

use derive_more::{AsRef, Display, From};
use uom::si::f64::{Length, Time, Velocity};

pub use crate::sumo::VClass;

/// Identifier of a [`Route`]. A distinct type from a bare `String`, same
/// rationale as `net`'s `crate::domain::EdgeId`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
pub struct RouteId(#[as_ref(forward)] pub String);

/// Identifier of a [`VehicleType`] (SUMO's confusingly-named `vType`
/// element — not to be confused with [`Vehicle`], an actual vehicle
/// instance; see that type's docs).
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
pub struct VehicleTypeId(#[as_ref(forward)] pub String);

/// Identifier of a [`Vehicle`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
pub struct VehicleId(#[as_ref(forward)] pub String);

/// Identifier of an edge in the `.net.xml` a route file's [`Route::edges`]
/// refer to. Deliberately not `crate::domain::EdgeId` — see the module
/// docs for why. If both `net` and `routes` are enabled and describe the
/// same network, the two id newtypes carry the same string ids but aren't
/// the same Rust type, so they can't be mixed up by accident.
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
pub struct EdgeRef(#[as_ref(forward)] pub String);

/// SUMO's `colorType`: either explicit RGBA components or one of SUMO's
/// named colors. Kept as two variants rather than resolving named colors to
/// RGBA: `invisible` and `random` aren't fixed colors to begin with, and
/// this crate doesn't want to guess RGB values for the rest independently
/// of SUMO's own source.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Color {
    /// Components parsed from `"r,g,b"` or `"r,g,b,a"` (`a` defaults to
    /// `1.0` when omitted), **on whichever scale the document used**.
    ///
    /// `colorType` accepts two numeric spellings — `0.0..=1.0` fractions
    /// and `0..=255` integers — as two `xsd:pattern`s over the same
    /// `xsd:string`, so nothing in the value itself says which was meant:
    /// `"1,0,0"` is a valid red on both scales, and `"1.0,0,0"` on the
    /// second. Reading them as one variant keeps that ambiguity where SUMO
    /// put it instead of resolving it with a guess that would be wrong for
    /// exactly the values where it matters. A consumer that needs
    /// fractions can divide by 255 when any component exceeds `1.0`.
    Rgba {
        r: f64,
        g: f64,
        b: f64,
        a: f64,
    },
    Named(NamedColor),
}

/// The symbolic color names `colorType` accepts as an alternative to RGBA.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NamedColor {
    Red,
    Green,
    Blue,
    Yellow,
    Cyan,
    Magenta,
    Orange,
    White,
    Black,
    Grey,
    Invisible,
    Random,
}

/// A named path through the network: an ordered list of edges a [`Vehicle`]
/// can be assigned to by referencing this route's [`RouteId`].
#[derive(Debug, Clone, PartialEq)]
pub struct Route {
    pub id: RouteId,
    /// `None` when the route is defined by `<stop>` children alone instead
    /// (not modeled here — see the module docs).
    pub edges: Option<Vec<EdgeRef>>,
    pub color: Option<Color>,
}

/// A vehicle class definition (SUMO's `vType` element, `vTypeType` in the
/// XSD), shared by every [`Vehicle`] that references it via
/// [`Vehicle::vehicle_type`].
///
/// Only the handful of attributes most consumers care about are modeled.
/// `vTypeType` also has car-following-model parameters (`carFollowing-*`
/// child elements), lane-changing parameters (`lcStrategic`, ...),
/// emission/mass/shape details, and more — none of which are mapped here.
#[derive(Debug, Clone, PartialEq)]
pub struct VehicleType {
    pub id: VehicleTypeId,
    pub vehicle_class: Option<VClass>,
    pub length: Option<Length>,
    pub max_speed: Option<Velocity>,
    pub color: Option<Color>,
}

/// When a [`Vehicle`] enters the simulation: a fixed point in time, or one
/// of SUMO's symbolic triggers (`departType`'s non-numeric union member).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Depart {
    Time(Time),
    Triggered,
    ContainerTriggered,
    Split,
    Begin,
}

/// A single vehicle running a route (SUMO's `vehicle` element,
/// `vehicleType` in the XSD — not to be confused with [`VehicleType`],
/// *this* crate's type for SUMO's `vType`/`vTypeType`, a vehicle *class*
/// definition. The clash is SUMO's own naming, not introduced here).
///
/// Only the attributes covering the common case (a vehicle referencing a
/// pre-defined `vType` and `route` by id) are modeled. `vehicleType` also
/// allows the route to be given inline (a nested `<route>` or
/// `<routeDistribution>` element instead of the `route` attribute), and has
/// arrival/departure lane, position, and speed attributes, none of which
/// are mapped here.
#[derive(Debug, Clone, PartialEq)]
pub struct Vehicle {
    pub id: VehicleId,
    pub vehicle_type: Option<VehicleTypeId>,
    /// The route this vehicle runs, by id. `None` if the route was instead
    /// given inline on the `<vehicle>` element (not modeled — see above).
    pub route: Option<RouteId>,
    pub depart: Depart,
    pub color: Option<Color>,
}

/// Identifier of a [`Person`].
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
pub struct PersonId(#[as_ref(forward)] pub String);

/// One `<walk>` stage of a [`Person`]'s plan (SUMO's `walkType`, nested in
/// `personType`): the edges it covers, however the file expressed them.
/// `walkType`'s `edges`/`from`/`to` are three independently optional
/// attributes at the XSD level, but a real document only ever uses one of
/// two shapes — [`Self::Edges`], an explicit list, or [`Self::Endpoints`]
/// (the common case: every `.rou.xml` in this project's own fixtures writes
/// a walk this way), leaving SUMO's own router to fill in the path between
/// them — so this crate models the real choice directly instead of leaving
/// 3 independent `Option`s a caller has to reconcile itself. Never resolved
/// to the full path here: that needs the road network this crate doesn't
/// necessarily have loaded (`routes` doesn't require `net`) plus a router of
/// its own — a consumer that needs the actual path already needs both.
#[derive(Debug, Clone, PartialEq)]
pub enum Walk {
    /// `edges`: an explicit, already-ordered list from the first edge to
    /// the last. Takes priority when a document sets `edges` alongside
    /// `from`/`to` — nothing in the XSD forbids that, and the edge list is
    /// the more complete of the two either way.
    Edges(Vec<EdgeRef>),
    /// `from`/`to`.
    Endpoints { from: EdgeRef, to: EdgeRef },
    /// Neither `edges` nor a complete `from`/`to` pair was given. Legal per
    /// the XSD (all 3 attributes are independently optional) but not a
    /// shape any real `.rou.xml` uses — kept rather than treating it as a
    /// parse error, so a document with one degenerate `<walk>` still reads
    /// instead of failing outright over a stage nothing here needs to
    /// resolve anyway.
    Unknown,
}

impl Walk {
    /// The edges this walk names — its full, ordered path for
    /// [`Self::Edges`], just its two endpoints for [`Self::Endpoints`] —
    /// enough to know which edges the walk is at least adjacent to without
    /// resolving the path SUMO's own router would fill in between (see this
    /// type's own docs for why that's not done here).
    #[must_use]
    pub fn edges(&self) -> Vec<&EdgeRef> {
        match self {
            Self::Edges(edges) => edges.iter().collect(),
            Self::Endpoints { from, to } => vec![from, to],
            Self::Unknown => Vec::new(),
        }
    }
}

/// A pedestrian (SUMO's `person` element, `personType` in the XSD).
///
/// Only [`Walk`] stages are modeled, and only for reading — `personType`
/// can also contain `personTrip`, `ride`, and `stop` children, and every
/// attribute on the element itself (`depart`, `arrival`, `color`, ...) is
/// dropped too, same as `Routes` itself drops whole element kinds it
/// doesn't map (see the module docs): nothing in this crate needs more
/// than which edges a pedestrian walks yet. [`crate::routes::schema_writer`]
/// has no `Person` -> `schema` direction at all for the same reason —
/// nothing in this crate writes a `<person>` back out.
#[derive(Debug, Clone, PartialEq)]
pub struct Person {
    pub id: PersonId,
    pub walks: Vec<Walk>,
}

/// A `.rou.xml` file's content, restricted to what this crate models — see
/// the module docs for what's dropped.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Routes {
    pub vehicle_types: Vec<VehicleType>,
    pub routes: Vec<Route>,
    pub vehicles: Vec<Vehicle>,
    pub persons: Vec<Person>,
}