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}