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::schema;
8use crate::xml::{RootRecordingReader, ensure_root_is};
9use anyhow::{Context, Result};
10use std::fs::File;
11use std::io::{BufRead, BufReader};
12use std::path::Path;
13use xsd_parser_types::quick_xml::{DeserializeSync, IoReader};
14
15/// Reads and deserializes the `.net.xml` file at `path` into a [`Network`].
16///
17/// ```no_run
18/// let network = sumo_types::read_network(std::path::Path::new("city.net.xml"))?;
19/// println!("{} edges", network.edges.len());
20/// # Ok::<(), anyhow::Error>(())
21/// ```
22///
23/// # Errors
24///
25/// Returns an error if `path` can't be opened, if the document isn't
26/// well-formed XML rooted at `<net>`, or if any of its attributes can't be
27/// interpreted (a malformed position, a boundary without 4 components, a
28/// non-finite coordinate, ...).
29pub fn read_network(path: &Path) -> Result<Network> {
30    let input_file = File::open(path)
31        .with_context(|| format!("Could not open input file: {}", path.display()))?;
32
33    read_network_from(BufReader::new(input_file))
34        .with_context(|| format!("invalid SUMO network in {}", path.display()))
35}
36
37/// Same as [`read_network`], for callers that already have the `.net.xml`
38/// bytes in hand (an in-memory buffer, a decompressed stream, ...) rather
39/// than a file on disk.
40///
41/// # Errors
42///
43/// Same as [`read_network`], minus the failure to open a file.
44pub fn read_network_from(source: impl BufRead) -> Result<Network> {
45    let mut reader = RootRecordingReader::new(IoReader::new(source));
46
47    let net = schema::NetType::deserialize(&mut reader)
48        .map_err(|error| anyhow::anyhow!("failed to parse SUMO network: {error}"))?;
49
50    // After deserializing, not before: the root name is only known once the
51    // reader has produced its first element event.
52    ensure_root_is(&reader, "net", "SUMO network")?;
53
54    Network::try_from(net)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60    use crate::domain::{EdgeId, JunctionKind, LaneId, Projection};
61
62    /// Minimal `.net.xml`: one edge with one lane, joining two junctions,
63    /// one of them traffic-light controlled with a two-phase program.
64    ///
65    /// Element order matters — `net_file.xsd` declares the children of
66    /// `<net>` as an `xsd:sequence`, so this mirrors the order netconvert
67    /// itself emits (location, type, edge, tlLogic, junction, connection).
68    const SAMPLE_NET: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
69<net version="1.20" junctionCornerDetail="5" limitTurnSpeed="5.50">
70    <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="!"/>
71    <edge id="e0" from="j0" to="j1" priority="1">
72        <lane id="e0_0" index="0" speed="13.89" length="100.00" shape="0.00,0.00 100.00,0.00"/>
73    </edge>
74    <tlLogic id="j1" type="static" programID="0" offset="0">
75        <phase duration="42" state="G"/>
76        <phase duration="3"  state="r"/>
77    </tlLogic>
78    <junction id="j0" type="dead_end" x="0.00" y="0.00" incLanes="" intLanes="" shape="0.00,0.00"/>
79    <junction id="j1" type="traffic_light" x="100.00" y="0.00" incLanes="e0_0" intLanes="" shape="100.00,0.00"/>
80    <connection from="e0" to="e0" fromLane="0" toLane="0" dir="s" state="M"/>
81</net>
82"#;
83
84    #[test]
85    fn reads_a_minimal_network_end_to_end() {
86        let network = read_network_from(SAMPLE_NET.as_bytes()).unwrap();
87
88        assert_eq!(network.edges.len(), 1);
89        assert_eq!(network.edges[0].id, EdgeId("e0".into()));
90        assert_eq!(network.edges[0].lanes[0].id, LaneId("e0_0".into()));
91
92        assert_eq!(network.junctions.len(), 2);
93        assert_eq!(network.junctions[1].kind, JunctionKind::TrafficLight);
94        assert_eq!(
95            network.junctions[1].incoming_lanes,
96            vec![LaneId("e0_0".into())]
97        );
98
99        assert_eq!(network.traffic_light_programs.len(), 1);
100        assert_eq!(network.traffic_light_programs[0].phases, vec!["G", "r"]);
101
102        assert_eq!(network.connections.len(), 1);
103        assert_eq!(network.location.projection, Projection::None);
104    }
105
106    #[test]
107    fn rejects_input_that_is_not_a_sumo_network() {
108        assert!(read_network_from(b"<not-a-net/>".as_slice()).is_err());
109    }
110
111    #[test]
112    fn rejects_a_net_shaped_document_under_the_wrong_root() {
113        // xsd-parser generates deserializers for XSD *types*, not elements,
114        // so without the root-name check in `read_network_from` this parses
115        // happily into an empty `Network`.
116        let disguised = SAMPLE_NET
117            .replace("<net ", "<additional ")
118            .replace("</net>", "</additional>");
119
120        let error = read_network_from(disguised.as_bytes()).unwrap_err();
121        assert!(
122            error.to_string().contains("<additional>"),
123            "error should name the offending root element, got: {error}"
124        );
125    }
126
127    #[test]
128    fn rejects_non_finite_coordinates() {
129        let poisoned = SAMPLE_NET.replace(r#"netOffset="0.00,0.00""#, r#"netOffset="NaN,0.00""#);
130        assert!(read_network_from(poisoned.as_bytes()).is_err());
131
132        let poisoned = SAMPLE_NET.replace(r#"x="100.00""#, r#"x="inf""#);
133        assert!(read_network_from(poisoned.as_bytes()).is_err());
134    }
135}