Skip to main content

sumo_types/net/
domain.rs

1//! Layer 2: this crate's own domain types.
2//!
3//! These types are independent of SUMO/XSD (unlike the private `schema`
4//! module, layer
5//! 1, which is an almost literal mirror of the `.xsd` files) and are
6//! produced from it by [`crate::schema_mapper`]. Consumers of this crate
7//! should work on these types, not on the ones generated by xsd-parser.
8
9use derive_more::{AsRef, Display, From};
10use uom::si::f64::{Length, Time, Velocity};
11use uom::si::length::meter;
12
13/// SUMO's default lane width (`SUMO_const_laneWidth`), used by netconvert
14/// whenever a lane's `width` attribute is omitted from `.net.xml`.
15///
16/// This is the value SUMO itself has used across recent versions; it isn't
17/// derived from anything in this repo, so double-check it against the SUMO
18/// version producing your input if precision matters.
19#[must_use]
20pub fn default_lane_width() -> Length {
21    Length::new::<meter>(3.2)
22}
23
24/// SUMO's default lane end offset: `0`, meaning the lane extends the full
25/// length of its edge. Used whenever a lane's `endOffset` attribute is
26/// omitted from `.net.xml`.
27#[must_use]
28pub fn default_lane_end_offset() -> Length {
29    Length::new::<meter>(0.0)
30}
31
32/// Identifier of an [`Edge`]. A distinct type from a bare `String` so it
33/// can't be passed where a [`LaneId`] is expected — a mistake that would
34/// silently compile (and silently misbehave, e.g. as a wrong `HashMap`
35/// lookup) if every id were just a `String`.
36#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
37pub struct EdgeId(#[as_ref(forward)] pub String);
38
39/// Identifier of a [`Lane`]. See [`EdgeId`] for why this isn't a bare `String`.
40#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
41pub struct LaneId(#[as_ref(forward)] pub String);
42
43/// Identifier of a [`Junction`]. See [`EdgeId`] for why this isn't a bare `String`.
44#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
45pub struct JunctionId(#[as_ref(forward)] pub String);
46
47/// Identifier of a traffic light program (`tlLogic`). See [`EdgeId`] for why
48/// this isn't a bare `String`.
49#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From, AsRef)]
50pub struct TrafficLightId(#[as_ref(forward)] pub String);
51
52/// Index of a [`Lane`] within its parent [`Edge`] (`0` is the rightmost lane
53/// in SUMO's convention). A distinct type from a bare `usize` so it can't be
54/// confused with unrelated counts (e.g. a traffic light link index).
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From)]
56pub struct LaneIndex(pub usize);
57
58/// Relative right-of-way ranking of an [`Edge`] (higher wins), derived by
59/// netconvert from the road classification. Can be negative (e.g. `-1` for
60/// internal/connector edges). A distinct type from a bare `i32` so it can't
61/// be confused with unrelated integers (e.g. a traffic light link index).
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From)]
63pub struct Priority(pub i32);
64
65/// Index of a [`Connection`] within its traffic light's signal group
66/// (`tlLogic/@state` / `phase/@state` character position). A distinct type
67/// from a bare `i32` so it can't be confused with unrelated integers (e.g.
68/// an edge's [`Priority`]).
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display, From)]
70pub struct LinkIndex(pub i32);
71
72/// A point in the network's coordinate system, re-exported at this format's
73/// own path. Defined once for the whole crate — see the type's own docs for
74/// why this one is shared while the id newtypes above are not.
75pub use crate::sumo::Point;
76
77/// Polyline that defines the geometry of an edge, lane, or junction.
78#[derive(Debug, Clone, Default, PartialEq)]
79pub struct Shape(pub Vec<Point>);
80
81/// Bounding box (bottom-left corner, top-right corner). Each corner is a
82/// [`Point`] rather than a bare `(f64, f64)`: SUMO's `locationType` boundary
83/// is inherently 2D (no `z`), but reusing the type already shared by every
84/// other coordinate in this crate beats introducing a second, boundary-only
85/// pair type for the same two numbers.
86///
87/// [`Default`] is the degenerate box at the origin.
88#[derive(Debug, Clone, Copy, Default, PartialEq)]
89pub struct Boundary {
90    pub min: Point,
91    pub max: Point,
92}
93
94/// How a network's coordinates relate to real-world geographic coordinates,
95/// as declared by SUMO's `location/@projParameter`.
96#[derive(Debug, Clone, Default, PartialEq)]
97pub enum Projection {
98    /// SUMO's `"!"` sentinel: the network isn't georeferenced, its
99    /// coordinates are already local/Cartesian. Also the [`Default`]:
100    /// assuming plain Cartesian coordinates is the assumption that can't
101    /// silently misplace a network on the globe.
102    #[default]
103    None,
104    /// A PROJ definition string, e.g. `"+proj=utm +zone=32 +ellps=WGS84"`.
105    Proj4(String),
106}
107
108/// Geographic metadata of the network, as exported by netconvert. Unlike
109/// the XSD (which marks these attributes optional), a real `netconvert`
110/// output always populates all of them, so a missing one here is treated as
111/// a malformed input rather than modeled with `Option`.
112///
113/// [`Default`] is a non-georeferenced network at the origin with an empty
114/// bounding box — what an empty `.net.xml` would describe. It exists so
115/// [`Network`] can keep its own [`Default`] for test fixtures
116/// (`Network { edges, ..Default::default() }`) now that `location` is no
117/// longer an `Option`; it is not something [`crate::read_network`] ever
118/// produces.
119#[derive(Debug, Clone, Default, PartialEq)]
120pub struct Location {
121    pub net_offset: Point,
122    pub converted_boundary: Boundary,
123    pub original_boundary: Boundary,
124    pub projection: Projection,
125}
126
127/// The role an edge plays within the network.
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum EdgeFunction {
130    Normal,
131    Internal,
132    Connector,
133    Crossing,
134    Walkingarea,
135}
136
137/// How the edge's width is distributed relative to its axis.
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub enum SpreadType {
140    Center,
141    RoadCenter,
142    Right,
143}
144
145/// SUMO's own named vehicle/traffic-participant classes. Defined in the
146/// shared `crate::sumo` module, not here — `routes::domain::VehicleType`
147/// needs it too (`vClass` is a plain vehicle-class token there as well),
148/// and `routes` doesn't depend on `net` (see that module's own docs) — and
149/// re-exported at this path so `domain::VClass` stays the public spelling
150/// this crate has always used.
151pub use crate::sumo::{UnknownVClass, VClass};
152
153/// A lane within an edge.
154#[derive(Debug, Clone, PartialEq)]
155pub struct Lane {
156    pub id: LaneId,
157    pub index: LaneIndex,
158    pub speed: Velocity,
159    pub length: Length,
160    pub width: Length,
161    pub end_offset: Length,
162    pub shape: Shape,
163    pub allow: Vec<String>,
164    pub disallow: Vec<String>,
165}
166
167impl Lane {
168    /// Shorthand for `class.permitted_on(&self.allow, &self.disallow)` —
169    /// see [`VClass::permitted_on`] for the actual rule.
170    #[must_use]
171    pub fn permits(&self, class: VClass) -> bool {
172        class.permitted_on(&self.allow, &self.disallow)
173    }
174
175    /// Whether this lane is restricted to pedestrians only
176    /// (`allow="pedestrian"` in `.net.xml`, e.g. crossings and
177    /// walkingareas) — straight from SUMO's own vClass permissions, not
178    /// from the edge's `function` label. Distinct from
179    /// `self.permits(VClass::Pedestrian)`: that asks "may a pedestrian use
180    /// this lane", true for e.g. `allow="pedestrian bicycle"` too, while
181    /// this asks "is a pedestrian the *only* thing that may".
182    #[must_use]
183    pub fn is_pedestrian_only(&self) -> bool {
184        !self.allow.is_empty() && self.allow.iter().all(|vclass| vclass == "pedestrian")
185    }
186}
187
188/// A directed edge (arc) of the network.
189#[derive(Debug, Clone, PartialEq)]
190pub struct Edge {
191    pub id: EdgeId,
192    pub function: EdgeFunction,
193    pub from: Option<JunctionId>,
194    pub to: Option<JunctionId>,
195    pub name: Option<String>,
196    pub priority: Option<Priority>,
197    pub length: Option<Length>,
198    pub shape: Option<Shape>,
199    pub spread_type: Option<SpreadType>,
200    pub lanes: Vec<Lane>,
201}
202
203/// The kind of right-of-way control a junction has (traffic light, priority,
204/// yield, ...).
205#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub enum JunctionKind {
207    TrafficLight,
208    TrafficLightUnregulated,
209    TrafficLightRightOnRed,
210    RailSignal,
211    RailCrossing,
212    Priority,
213    PriorityStop,
214    LeftBeforeRight,
215    RightBeforeLeft,
216    AllWayStop,
217    Zipper,
218    District,
219    Unregulated,
220    Internal,
221    DeadEnd,
222}
223
224/// A network node where one or more edges meet.
225#[derive(Debug, Clone, PartialEq)]
226pub struct Junction {
227    pub id: JunctionId,
228    pub position: Point,
229    pub kind: JunctionKind,
230    pub incoming_lanes: Vec<LaneId>,
231    pub internal_lanes: Vec<LaneId>,
232    pub shape: Option<Shape>,
233    pub name: Option<String>,
234}
235
236/// Turning direction of a connection, following SUMO's convention
237/// (`SUMOXMLDefinitions::LinkDirection`).
238#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
239pub enum ConnectionDirection {
240    Straight,
241    Turn,
242    TurnLeftHand,
243    Left,
244    Right,
245    PartialLeft,
246    PartialRight,
247}
248
249/// Right-of-way state of a connection, following SUMO's convention
250/// (`SUMOXMLDefinitions::LinkState`) for the `state` attribute in `.net.xml`.
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
252pub enum LinkState {
253    /// Has the right of way ("major").
254    Major,
255    /// Does not have the right of way ("minor"), must yield.
256    Minor,
257    /// Controlled by a traffic light that is off, with no visible signal.
258    TlsOffNoSignal,
259    /// Controlled by a traffic light that is off, in blinking mode.
260    TlsOffBlinking,
261    /// Shared right of way (everyone yields equally).
262    Equal,
263    /// Must come to a full stop.
264    Stop,
265    /// "All-way stop" style intersection.
266    AllWayStop,
267    /// "Zipper" merge.
268    Zipper,
269    /// No real connection (dead end).
270    DeadEnd,
271}
272
273/// A connection between two lanes (possibly from different edges).
274#[derive(Debug, Clone, PartialEq)]
275pub struct Connection {
276    pub from_edge: EdgeId,
277    pub to_edge: EdgeId,
278    pub from_lane: LaneIndex,
279    pub to_lane: LaneIndex,
280    pub direction: ConnectionDirection,
281    pub state: LinkState,
282    pub via: Option<LaneId>,
283    pub traffic_light: Option<TrafficLightId>,
284    pub link_index: Option<LinkIndex>,
285    pub pass: bool,
286    pub keep_clear: bool,
287}
288
289/// A roundabout in the network.
290#[derive(Debug, Clone, PartialEq)]
291pub struct Roundabout {
292    pub nodes: Vec<JunctionId>,
293    pub edges: Vec<EdgeId>,
294}
295
296/// A traffic light program's control strategy (`tlLogic/@type`).
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum TrafficLightKind {
299    Actuated,
300    DelayBased,
301    Static,
302    Nema,
303}
304
305/// When a traffic light program's cycle starts, relative to the simulation
306/// clock (`tlLogic/@offset`).
307#[derive(Debug, Clone, Copy, PartialEq)]
308pub enum TrafficLightOffset {
309    /// A fixed offset into the program's cycle.
310    Fixed(Time),
311    /// SUMO's `"begin"` sentinel: the cycle starts at the simulation's own
312    /// begin time rather than at a fixed offset.
313    Begin,
314}
315
316/// One phase of a [`TrafficLightProgram`] (`phaseType`): how long it lasts
317/// and the `state` string every [`Connection`] it controls reads from at
318/// that phase — see the program's own docs for how the two connect.
319///
320/// Only `duration` and `state` are modelled. `phaseType` also has a group of
321/// actuated/NEMA-controller attributes this crate doesn't read yet —
322/// `minDur`, `maxDur`, `earliestEnd`, `latestEnd`, `earlyTarget`,
323/// `finalTarget`, `yellow`, `red`, `vehext`, `next`, `name` — meaningful
324/// only for a [`TrafficLightProgram`] whose `kind` is
325/// [`TrafficLightKind::Actuated`] or [`TrafficLightKind::Nema`], and absent
326/// or ignored for the plain [`TrafficLightKind::Static`] case this crate was
327/// first built to round-trip.
328#[derive(Debug, Clone, PartialEq)]
329pub struct Phase {
330    pub duration: Time,
331    pub state: String,
332}
333
334/// A traffic light program (`tlLogic`): an ordered list of [`Phase`]s. The
335/// character at index `i` of every phase's `state` governs the
336/// [`Connection`] whose `link_index == i` — two links that always share the
337/// same character, in every phase, are controlled by the same physical
338/// signal head and should be treated as one group, even if SUMO assigns
339/// them different `link_index`es.
340///
341/// A `tlLogic` id can appear more than once in `.net.xml` (multiple
342/// programs sharing one junction, e.g. a normal one and a night-time one) —
343/// [`Self::program_id`] is what tells them apart; which one is "the" active
344/// program for a junction is left to the caller. This type only models a
345/// single program as parsed.
346#[derive(Debug, Clone, PartialEq)]
347pub struct TrafficLightProgram {
348    pub id: TrafficLightId,
349    /// `programID`: SUMO requires this, so unlike [`Self::kind`] and
350    /// [`Self::offset`] it can't be `None` — a `.net.xml` omitting it fails
351    /// to parse rather than yielding a program with no `program_id`.
352    pub program_id: String,
353    pub kind: Option<TrafficLightKind>,
354    pub offset: Option<TrafficLightOffset>,
355    pub phases: Vec<Phase>,
356}
357
358/// Complete model of a SUMO network (`.net.xml`), already converted to the
359/// project's own types and decoupled from the original XSD schema.
360///
361/// [`Default`] is an empty network with a default [`Location`], which is
362/// useful for building test fixtures but is never what
363/// [`crate::read_network`] returns: `net_file.xsd` requires `<location>`
364/// (`minOccurs="1"`), so a parsed `Network` always carries the real one.
365#[derive(Debug, Clone, Default, PartialEq)]
366pub struct Network {
367    /// Always present: `net_file.xsd` declares `<location>` as
368    /// `minOccurs="1"`, so a `.net.xml` without one fails to parse rather
369    /// than yielding a `Network` with no location.
370    pub location: Location,
371    pub edges: Vec<Edge>,
372    pub junctions: Vec<Junction>,
373    pub connections: Vec<Connection>,
374    pub roundabouts: Vec<Roundabout>,
375    pub traffic_light_programs: Vec<TrafficLightProgram>,
376}
377
378#[cfg(test)]
379mod vclass_tests {
380    use super::*;
381
382    fn lane_with(allow: &[&str], disallow: &[&str]) -> Lane {
383        Lane {
384            id: LaneId("e0_0".into()),
385            index: LaneIndex(0),
386            speed: Velocity::new::<uom::si::velocity::meter_per_second>(10.0),
387            length: Length::new::<meter>(20.0),
388            width: default_lane_width(),
389            end_offset: default_lane_end_offset(),
390            shape: Shape::default(),
391            allow: allow.iter().map(|s| (*s).to_string()).collect(),
392            disallow: disallow.iter().map(|s| (*s).to_string()).collect(),
393        }
394    }
395
396    #[test]
397    fn a_lane_with_neither_attribute_permits_every_class() {
398        let lane = lane_with(&[], &[]);
399        assert!(lane.permits(VClass::Passenger));
400        assert!(lane.permits(VClass::Bicycle));
401        assert!(lane.permits(VClass::Pedestrian));
402    }
403
404    #[test]
405    fn disallow_excludes_only_the_classes_it_names() {
406        let lane = lane_with(&[], &["pedestrian", "tram"]);
407        assert!(!lane.permits(VClass::Pedestrian));
408        assert!(!lane.permits(VClass::Tram));
409        assert!(lane.permits(VClass::Passenger));
410        assert!(lane.permits(VClass::Bicycle));
411    }
412
413    #[test]
414    fn a_non_empty_allow_is_exhaustive_disallow_is_not_consulted() {
415        // Real `.net.xml` output never sets both, but `permitted_on`'s own
416        // contract is "allow wins outright" regardless.
417        let lane = lane_with(&["pedestrian"], &["pedestrian"]);
418        assert!(lane.permits(VClass::Pedestrian));
419        assert!(!lane.permits(VClass::Passenger));
420    }
421
422    #[test]
423    fn disallow_all_permits_nothing() {
424        let lane = lane_with(&[], &["all"]);
425        assert!(!lane.permits(VClass::Passenger));
426        assert!(!lane.permits(VClass::Pedestrian));
427        assert!(!lane.permits(VClass::Bicycle));
428    }
429
430    #[test]
431    fn allow_all_permits_everything_the_same_as_no_allow_at_all() {
432        let lane = lane_with(&["all"], &[]);
433        assert!(lane.permits(VClass::Passenger));
434        assert!(lane.permits(VClass::Pedestrian));
435    }
436
437    #[test]
438    fn an_unknown_token_in_disallow_does_not_affect_any_known_class() {
439        // Real, forward-compatible shape: a `.net.xml` from a newer SUMO
440        // naming a vClass this crate doesn't have a variant for yet.
441        let lane = lane_with(&[], &["some_future_vclass"]);
442        assert!(lane.permits(VClass::Passenger));
443        assert!(lane.permits(VClass::Bicycle));
444    }
445
446    #[test]
447    fn an_unknown_token_in_a_non_empty_allow_grants_no_known_class() {
448        let lane = lane_with(&["some_future_vclass"], &[]);
449        assert!(!lane.permits(VClass::Passenger));
450    }
451
452    #[test]
453    fn a_real_barcelona_style_vehicle_lane_permits_car_moped_and_bicycle() {
454        // Mirrors `disallow="pedestrian tram rail_urban rail rail_electric
455        // rail_fast ship container cable_car subway aircraft wheelchair
456        // scooter drone"`, the actual attribute this crate's own Barcelona
457        // fixture uses on an ordinary vehicle lane.
458        let lane = lane_with(
459            &[],
460            &[
461                "pedestrian",
462                "tram",
463                "rail_urban",
464                "rail",
465                "rail_electric",
466                "rail_fast",
467                "ship",
468                "container",
469                "cable_car",
470                "subway",
471                "aircraft",
472                "wheelchair",
473                "scooter",
474                "drone",
475            ],
476        );
477        assert!(lane.permits(VClass::Passenger));
478        assert!(lane.permits(VClass::Motorcycle));
479        assert!(lane.permits(VClass::Moped));
480        assert!(lane.permits(VClass::Bicycle));
481        assert!(!lane.permits(VClass::Pedestrian));
482    }
483
484    #[test]
485    fn a_pedestrian_only_lane_permits_only_pedestrians() {
486        let lane = lane_with(&["pedestrian"], &[]);
487        assert!(lane.permits(VClass::Pedestrian));
488        assert!(!lane.permits(VClass::Passenger));
489        assert!(!lane.permits(VClass::Bicycle));
490    }
491
492    #[test]
493    fn from_str_rejects_an_unknown_token() {
494        let err = "spaceship".parse::<VClass>().unwrap_err();
495        assert_eq!(err, UnknownVClass("spaceship".to_string()));
496    }
497}