Skip to main content

sumo_types/additional/
domain.rs

1//! Layer 2 for `additional`: this crate's own domain types for `.add.xml`.
2//!
3//! Independent of SUMO/XSD, same rationale as `net`'s own `crate::domain`
4//! (see the crate docs) — and independent of the other formats' domain
5//! types, by design: `additional` doesn't require `net` or `routes` to be
6//! enabled, so it can't borrow their types and defines its own [`LaneRef`]
7//! instead.
8//!
9//! Those cross-format names are code spans, not intra-doc links: `net` may
10//! not be enabled, and a link to a feature-gated item is a broken link in
11//! every build that leaves that feature off.
12//!
13//! Only the detector definitions are modelled: [`E1Detector`] (induction
14//! loop), [`E2Detector`] (lane area) and [`E3Detector`] (multi-entry /
15//! multi-exit). `additionalType` allows about forty more element kinds —
16//! `busStop`, `chargingStation`, `parkingArea`, `rerouter`, `calibrator`,
17//! `variableSpeedSign`, `WAUT`, `poly`, `poi`, the whole of `routesType`
18//! again, ... — none of which are mapped; [`Additional`] silently drops
19//! them (see [`crate::additional::schema_mapper`]).
20
21use derive_more::{AsRef, Display, From};
22use uom::si::f64::{Length, Time, Velocity};
23
24/// Identifier of a detector. A distinct type from a bare `String`, same
25/// rationale as `net`'s `crate::domain::EdgeId`.
26#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
27pub struct DetectorId(#[as_ref(forward)] pub String);
28
29/// A point in the network's coordinate system, re-exported at this format's
30/// own path. This *is* `net`'s `crate::domain::Point`, not a lookalike: the
31/// two used to be separate byte-identical structs only because `additional`
32/// cannot name a type from `net`, which may not be enabled, and both are now
33/// the one definition in a module that is always compiled. Unlike
34/// [`LaneRef`] below, nothing was gained by keeping them apart — see the
35/// type's own docs.
36pub use crate::sumo::Point;
37
38/// Identifier of a lane a detector sits on, in the `.net.xml` this
39/// `.add.xml` accompanies. Deliberately not `net`'s `crate::domain::LaneId`
40/// — see the module docs for why. The two carry the same string ids but
41/// aren't the same Rust type, so they can't be mixed up by accident.
42#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
43pub struct LaneRef(#[as_ref(forward)] pub String);
44
45/// A position along a lane.
46///
47/// SUMO encodes the two cases in the sign of one number: a negative `pos`
48/// means "measured back from the lane's end". That is spelled out as two
49/// variants rather than left as a signed [`Length`] because the difference
50/// is not cosmetic — reading a `-12.5` as a distance from the start puts a
51/// detector at the wrong end of the road, and nothing about the number
52/// itself says so.
53///
54/// Neither variant is resolved to the other here: doing that needs the
55/// lane's length, which lives in the `.net.xml` this format deliberately
56/// doesn't require to be loaded alongside.
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum LanePosition {
59    /// Distance from the lane's start, SUMO's non-negative `pos`.
60    FromStart(Length),
61    /// Distance back from the lane's end, i.e. SUMO's negative `pos` with
62    /// the sign already taken off: `pos="-12.5"` is `FromEnd(12.5 m)`.
63    FromEnd(Length),
64}
65
66/// One entry or exit point of an [`E3Detector`], mirroring SUMO's
67/// `detEntryExitType`.
68#[derive(Debug, Clone, PartialEq)]
69pub struct DetectorGate {
70    pub lane: LaneRef,
71    pub position: LanePosition,
72    /// SUMO's `friendlyPos`: whether a position outside the lane is clamped
73    /// into range instead of being an error. `None` when the attribute is
74    /// absent, rather than resolved to SUMO's default, so a round-trip can
75    /// tell "unset" from "explicitly set".
76    pub friendly_position: Option<bool>,
77}
78
79/// An induction loop (SUMO's `e1Detector`, also spelled `inductionLoop`): a
80/// point detector at a fixed position on a single lane.
81#[derive(Debug, Clone, PartialEq)]
82pub struct E1Detector {
83    pub id: DetectorId,
84    pub lane: LaneRef,
85    pub position: LanePosition,
86    /// Where the detector writes its output, as given — a path relative to
87    /// the `.add.xml` itself, per how SUMO resolves it.
88    pub file: String,
89    pub period: Option<Time>,
90    pub length: Option<Length>,
91    pub name: Option<String>,
92    pub friendly_position: Option<bool>,
93}
94
95/// What an [`E2Detector`] is stretched over: SUMO's mutually exclusive
96/// `lane` and `lanes` attributes, as one value that can only hold one of
97/// them. Setting both is an error SUMO rejects, and this makes it
98/// unrepresentable rather than something each consumer has to check.
99#[derive(Debug, Clone, PartialEq)]
100pub enum LaneCoverage {
101    /// A stretch of one lane (`lane`).
102    SingleLane(LaneRef),
103    /// A chain of consecutive lanes (`lanes`).
104    LaneChain(Vec<LaneRef>),
105}
106
107/// A lane area detector (SUMO's `e2Detector`, also spelled
108/// `laneAreaDetector`): covers a stretch of one lane, or a chain of lanes.
109#[derive(Debug, Clone, PartialEq)]
110pub struct E2Detector {
111    pub id: DetectorId,
112    /// Which lanes the detector covers. `None` when the document set
113    /// neither `lane` nor `lanes`, which `additional_file.xsd` permits
114    /// (both are `use="optional"`) even though SUMO itself rejects it.
115    pub coverage: Option<LaneCoverage>,
116    pub position: Option<LanePosition>,
117    pub end_position: Option<LanePosition>,
118    pub length: Option<Length>,
119    pub file: String,
120    pub period: Option<Time>,
121    pub name: Option<String>,
122    pub friendly_position: Option<bool>,
123    /// `speedThreshold`: below this speed a vehicle counts as halting.
124    pub speed_threshold: Option<Velocity>,
125    /// `timeThreshold`: how long a vehicle must stay under
126    /// [`Self::speed_threshold`] before it counts as halting.
127    pub time_threshold: Option<Time>,
128    /// `jamThreshold`: how close the next halting vehicle has to be for the
129    /// two to count as one jam. A distance, not a speed — it is a separate
130    /// attribute from [`Self::speed_threshold`], which an earlier version of
131    /// this file conflated.
132    pub jam_threshold: Option<Length>,
133}
134
135/// A multi-entry / multi-exit detector (SUMO's `e3Detector`, also spelled
136/// `entryExitDetector`): an area of the network delimited by
137/// [`DetectorGate`]s, measuring whatever is between them.
138#[derive(Debug, Clone, PartialEq)]
139pub struct E3Detector {
140    pub id: DetectorId,
141    pub entries: Vec<DetectorGate>,
142    pub exits: Vec<DetectorGate>,
143    pub file: String,
144    pub period: Option<Time>,
145    pub name: Option<String>,
146    /// SUMO's `pos`: where editors like netedit draw the detector's icon.
147    /// Purely cosmetic — SUMO uses the gates, not this, for detection.
148    ///
149    /// `additional_file.xsd` types this attribute as a bare `xsd:string`
150    /// rather than one of SUMO's position types, but every writer emits
151    /// `"x,y"`, so it is parsed like any other position and a value that
152    /// isn't one is an error — same treatment the rest of the crate gives
153    /// a malformed coordinate.
154    pub icon_position: Option<Point>,
155    /// `speedThreshold`: below this speed a vehicle counts as halting.
156    pub speed_threshold: Option<Velocity>,
157    /// `timeThreshold`: how long a vehicle must stay under
158    /// [`Self::speed_threshold`] before it counts as halting.
159    pub time_threshold: Option<Time>,
160    /// `openEntry`: whether vehicles already inside the area at the start
161    /// are counted, rather than only those seen crossing an entry gate.
162    pub open_entry: Option<bool>,
163    /// `detectPersons`: which person-only transport modes also count as
164    /// detected, on top of vehicles. Empty (the attribute omitted) means
165    /// vehicles only, SUMO's own default.
166    pub detect_persons: Vec<PersonMode>,
167}
168
169/// One of the person-only transport modes SUMO's `detectPersons` attribute
170/// lists, one per space-separated word (see [`E3Detector::detect_persons`]).
171/// Not generated from the XSD like layer 1: `detectPersons` is plain
172/// `xsd:string` there, SUMO doesn't constrain it to an enumeration at the
173/// schema level. Hand-written instead, from SUMO's own `detectPersons`
174/// reference (<https://sumo.dlr.de/docs/Simulation/Output/Persons.html#detectors_for_persons>).
175#[derive(Debug, Clone, Copy, PartialEq, Eq)]
176pub enum PersonMode {
177    Walk,
178    Bicycle,
179}
180
181/// A `detectPersons` token that isn't one of [`PersonMode`]'s own known
182/// variants — carries the token back so a caller can report it, same
183/// rationale as `net::domain::UnknownVClass`.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct UnknownPersonMode(pub String);
186
187impl std::str::FromStr for PersonMode {
188    type Err = UnknownPersonMode;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        Ok(match s {
192            "walk" => Self::Walk,
193            "bicycle" => Self::Bicycle,
194            other => return Err(UnknownPersonMode(other.to_string())),
195        })
196    }
197}
198
199impl PersonMode {
200    /// The exact spelling [`std::str::FromStr::from_str`] reads back into
201    /// this variant.
202    #[must_use]
203    pub fn as_str(self) -> &'static str {
204        match self {
205            Self::Walk => "walk",
206            Self::Bicycle => "bicycle",
207        }
208    }
209}
210
211impl std::fmt::Display for PersonMode {
212    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213        f.write_str(self.as_str())
214    }
215}
216
217/// So `crate::sumo::join_ids_opt` — already shared by every whitespace-
218/// separated id-list attribute this crate writes — can format
219/// [`E3Detector::detect_persons`] too.
220impl AsRef<str> for PersonMode {
221    fn as_ref(&self) -> &str {
222        self.as_str()
223    }
224}
225
226/// A `.add.xml` file's content, restricted to what this crate models — see
227/// the module docs for what's dropped.
228#[derive(Debug, Clone, Default, PartialEq)]
229pub struct Additional {
230    pub induction_loops: Vec<E1Detector>,
231    pub lane_area_detectors: Vec<E2Detector>,
232    pub entry_exit_detectors: Vec<E3Detector>,
233}