Skip to main content

sumo_types/net/
reader.rs

1//! Reading a `.net.xml` file into a [`Network`]: deserializes the input into
2//! layer 1 (the private `schema` module) and converts it to layer 2
3//! ([`crate::domain`]) in one step, so consumers never have to name a
4//! generated schema type or depend on `xsd-parser-types` themselves.
5
6use super::domain::Network;
7use crate::Result;
8use crate::schema;
9use crate::xml::{read_document, read_document_at};
10use std::io::BufRead;
11use std::path::Path;
12
13/// Reads and deserializes the `.net.xml` file at `path` into [`Network`].
14///
15/// ```no_run
16/// let network = sumo_types::read_network(std::path::Path::new("city.net.xml"))?;
17/// println!("{} edges", network.edges.len());
18/// # Ok::<(), sumo_types::Error>(())
19/// ```
20///
21/// # Errors
22///
23/// Returns an error if `path` can't be opened, if the document isn't
24/// well-formed XML rooted at `<net>`, or if any of its attributes can't be
25/// interpreted.
26pub fn read_network(path: &Path) -> Result<Network> {
27    read_document_at::<schema::NetType, _>(path, "net", "SUMO network")
28}
29
30/// Same as [`read_network`], for callers that already have the `.net.xml` bytes
31/// in hand (an in-memory buffer, a decompressed stream, ...) rather than a
32/// file on disk.
33///
34/// # Errors
35///
36/// Same as [`read_network`], minus the failure to open a file.
37pub fn read_network_from(source: impl BufRead) -> Result<Network> {
38    read_document::<schema::NetType, _, _>(source, "net", "SUMO network")
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use crate::domain::{
45        EdgeId, JunctionKind, LaneId, Projection, TrafficLightKind, TrafficLightOffset,
46    };
47    use uom::si::f64::Time;
48    use uom::si::time::second;
49
50    /// Minimal `.net.xml`: one edge with one lane, joining two junctions,
51    /// one of them traffic-light controlled with a two-phase program.
52    ///
53    /// Element order matters — `net_file.xsd` declares the children of
54    /// `<net>` as an `xsd:sequence`, so this mirrors the order netconvert
55    /// itself emits (location, type, edge, tlLogic, junction, connection).
56    const SAMPLE_NET: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
57<net version="1.20" junctionCornerDetail="5" limitTurnSpeed="5.50">
58    <location netOffset="0.00,0.00" convBoundary="0.00,0.00,100.00,0.00" origBoundary="-1.00,-1.00,1.00,1.00" projParameter="!"/>
59    <edge id="e0" from="j0" to="j1" priority="1">
60        <lane id="e0_0" index="0" speed="13.89" length="100.00" shape="0.00,0.00 100.00,0.00"/>
61    </edge>
62    <tlLogic id="j1" type="static" programID="0" offset="0">
63        <phase duration="42" state="G"/>
64        <phase duration="3"  state="r"/>
65    </tlLogic>
66    <junction id="j0" type="dead_end" x="0.00" y="0.00" incLanes="" intLanes="" shape="0.00,0.00"/>
67    <junction id="j1" type="traffic_light" x="100.00" y="0.00" incLanes="e0_0" intLanes="" shape="100.00,0.00"/>
68    <connection from="e0" to="e0" fromLane="0" toLane="0" dir="s" state="M"/>
69</net>
70"#;
71
72    #[test]
73    fn reads_a_minimal_network_end_to_end() {
74        let network = read_network_from(SAMPLE_NET.as_bytes()).unwrap();
75
76        assert_eq!(network.edges.len(), 1);
77        assert_eq!(network.edges[0].id, EdgeId("e0".into()));
78        assert_eq!(network.edges[0].lanes[0].id, LaneId("e0_0".into()));
79
80        assert_eq!(network.junctions.len(), 2);
81        assert_eq!(network.junctions[1].kind, JunctionKind::TrafficLight);
82        assert_eq!(
83            network.junctions[1].incoming_lanes,
84            vec![LaneId("e0_0".into())]
85        );
86
87        assert_eq!(network.traffic_light_programs.len(), 1);
88        let program = &network.traffic_light_programs[0];
89        assert_eq!(program.program_id, "0");
90        assert_eq!(program.kind, Some(TrafficLightKind::Static));
91        assert_eq!(
92            program.offset,
93            Some(TrafficLightOffset::Fixed(Time::new::<second>(0.0)))
94        );
95        let states: Vec<&str> = program.phases.iter().map(|p| p.state.as_str()).collect();
96        assert_eq!(states, vec!["G", "r"]);
97        assert_eq!(program.phases[0].duration.get::<second>(), 42.0);
98        assert_eq!(program.phases[1].duration.get::<second>(), 3.0);
99
100        assert_eq!(network.connections.len(), 1);
101        assert_eq!(network.location.projection, Projection::None);
102    }
103
104    #[test]
105    fn rejects_input_that_is_not_a_sumo_network() {
106        assert!(read_network_from(b"<not-a-net/>".as_slice()).is_err());
107    }
108
109    #[test]
110    fn rejects_a_net_shaped_document_under_the_wrong_root() {
111        // xsd-parser generates deserializers for XSD *types*, not elements,
112        // so without the root-name check in `read_network_from` this parses
113        // happily into an empty `Network`.
114        let disguised = SAMPLE_NET
115            .replace("<net ", "<additional ")
116            .replace("</net>", "</additional>");
117
118        let error = read_network_from(disguised.as_bytes()).unwrap_err();
119
120        // Matched on structurally, not by string: this is what the typed
121        // `Error` buys a consumer over an erased one (see `src/error.rs`).
122        assert!(
123            matches!(
124                &error,
125                crate::Error::WrongRoot { expected: "net", found, .. } if found == "additional"
126            ),
127            "expected a WrongRoot naming <additional>, got: {error:?}"
128        );
129        assert!(
130            error.to_string().contains("<additional>"),
131            "error should name the offending root element, got: {error}"
132        );
133    }
134
135    #[test]
136    fn rejects_non_finite_coordinates() {
137        let poisoned = SAMPLE_NET.replace(r#"netOffset="0.00,0.00""#, r#"netOffset="NaN,0.00""#);
138        assert!(matches!(
139            read_network_from(poisoned.as_bytes()).unwrap_err(),
140            crate::Error::NonFiniteNumber { .. }
141        ));
142
143        let poisoned = SAMPLE_NET.replace(r#"x="100.00""#, r#"x="inf""#);
144        assert!(matches!(
145            read_network_from(poisoned.as_bytes()).unwrap_err(),
146            crate::Error::NonFiniteNumber { .. }
147        ));
148    }
149
150    #[test]
151    fn names_the_file_it_could_not_open() {
152        let error = read_network(Path::new("no/such/city.net.xml")).unwrap_err();
153
154        assert!(
155            matches!(&error, crate::Error::Open { path, .. } if path.ends_with("city.net.xml")),
156            "expected an Open error naming the path, got: {error:?}"
157        );
158    }
159}