Skip to main content

sumo_types/additional/
reader.rs

1//! Reading a `.add.xml` file into [`Additional`]: deserializes the input
2//! into layer 1 (the private `schema` module) and converts it to layer 2
3//! ([`crate::additional::domain`]) in one step, mirroring the other
4//! formats' readers.
5
6use super::domain::Additional;
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 `.add.xml` file at `path` into [`Additional`].
14///
15/// # Errors
16///
17/// Returns an error if `path` can't be opened, if the document isn't
18/// well-formed XML rooted at `<additional>`, or if any of its attributes can't be
19/// interpreted.
20pub fn read_additional(path: &Path) -> Result<Additional> {
21    read_document_at::<schema::AdditionalType, _>(path, "additional", "SUMO additional file")
22}
23
24/// Same as [`read_additional`], for callers that already have the `.add.xml` bytes
25/// in hand (an in-memory buffer, a decompressed stream, ...) rather than a
26/// file on disk.
27///
28/// # Errors
29///
30/// Same as [`read_additional`], minus the failure to open a file.
31pub fn read_additional_from(source: impl BufRead) -> Result<Additional> {
32    read_document::<schema::AdditionalType, _, _>(source, "additional", "SUMO additional file")
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38    use crate::additional::domain::{DetectorId, LaneCoverage, LanePosition, LaneRef, Point};
39    use uom::si::f64::Length;
40    use uom::si::length::meter;
41    use uom::si::time::second;
42
43    /// One of each detector kind, written the way SUMO's own tools emit
44    /// them (`netedit` writes `e1Detector`/`e2Detector`/`e3Detector`).
45    const SAMPLE_ADDITIONAL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
46<additional>
47    <e1Detector id="loop0" lane="e0_0" pos="12.50" period="60.00" file="out/loop0.xml"/>
48    <e2Detector id="area0" lane="e0_0" pos="0.00" endPos="50.00" file="out/area0.xml"/>
49    <e3Detector id="zone0" file="out/zone0.xml" pos="42.00,7.00">
50        <detEntry lane="e0_0" pos="0.00"/>
51        <detEntry lane="e1_0" pos="0.00"/>
52        <detExit lane="e0_0" pos="100.00"/>
53    </e3Detector>
54</additional>
55"#;
56
57    #[test]
58    fn reads_one_of_each_detector_kind_end_to_end() {
59        let additional = read_additional_from(SAMPLE_ADDITIONAL.as_bytes()).unwrap();
60
61        assert_eq!(additional.induction_loops.len(), 1);
62        let loop0 = &additional.induction_loops[0];
63        assert_eq!(loop0.id, DetectorId("loop0".into()));
64        assert_eq!(loop0.lane, LaneRef("e0_0".into()));
65        assert_eq!(
66            loop0.position,
67            LanePosition::FromStart(Length::new::<meter>(12.5))
68        );
69        assert_eq!(loop0.period.unwrap().get::<second>(), 60.0);
70
71        assert_eq!(additional.lane_area_detectors.len(), 1);
72        let area0 = &additional.lane_area_detectors[0];
73        assert_eq!(
74            area0.coverage,
75            Some(LaneCoverage::SingleLane(LaneRef("e0_0".into())))
76        );
77        assert_eq!(
78            area0.end_position,
79            Some(LanePosition::FromStart(Length::new::<meter>(50.0)))
80        );
81
82        assert_eq!(additional.entry_exit_detectors.len(), 1);
83        let zone0 = &additional.entry_exit_detectors[0];
84        assert_eq!(zone0.entries.len(), 2);
85        assert_eq!(zone0.exits.len(), 1);
86        assert_eq!(
87            zone0.exits[0].position,
88            LanePosition::FromStart(Length::new::<meter>(100.0))
89        );
90        assert_eq!(
91            zone0.icon_position,
92            Some(Point {
93                x: 42.0,
94                y: 7.0,
95                z: 0.0
96            })
97        );
98    }
99
100    #[test]
101    fn treats_sumos_alternative_element_names_as_the_same_detector() {
102        // `inductionLoop`/`laneAreaDetector`/`entryExitDetector` are the
103        // same XSD types as `e1Detector`/`e2Detector`/`e3Detector` under a
104        // second element name, and must not land in different buckets.
105        let xml = r#"<additional>
106            <inductionLoop id="loop0" lane="e0_0" pos="1.00" file="a.xml"/>
107            <laneAreaDetector id="area0" lane="e0_0" file="b.xml"/>
108            <entryExitDetector id="zone0" file="c.xml"><detEntry lane="e0_0" pos="0.00"/></entryExitDetector>
109        </additional>"#;
110
111        let additional = read_additional_from(xml.as_bytes()).unwrap();
112
113        assert_eq!(additional.induction_loops.len(), 1);
114        assert_eq!(additional.lane_area_detectors.len(), 1);
115        assert_eq!(additional.entry_exit_detectors.len(), 1);
116    }
117
118    #[test]
119    fn ignores_element_kinds_this_crate_does_not_model() {
120        let xml = r#"<additional>
121            <busStop id="stop0" lane="e0_0" startPos="0.00" endPos="20.00"/>
122            <e1Detector id="loop0" lane="e0_0" pos="1.00" file="a.xml"/>
123        </additional>"#;
124
125        let additional = read_additional_from(xml.as_bytes()).unwrap();
126
127        assert_eq!(additional.induction_loops.len(), 1);
128        assert!(additional.lane_area_detectors.is_empty());
129    }
130
131    #[test]
132    fn rejects_unrelated_root_elements() {
133        // `additionalType`'s content is entirely optional, so nothing else
134        // would make an unrelated document fail — it would parse as an
135        // empty `Additional`.
136        let error = read_additional_from(b"<not-additional/>".as_slice()).unwrap_err();
137        assert!(
138            error.to_string().contains("<not-additional>"),
139            "error should name the offending root element, got: {error}"
140        );
141    }
142
143    #[test]
144    fn accepts_an_empty_but_correctly_rooted_file() {
145        let additional = read_additional_from(b"<additional/>".as_slice()).unwrap();
146        assert_eq!(additional, Additional::default());
147    }
148
149    #[test]
150    fn rejects_a_non_finite_position() {
151        let xml =
152            r#"<additional><e1Detector id="l" lane="e0_0" pos="NaN" file="a.xml"/></additional>"#;
153        assert!(read_additional_from(xml.as_bytes()).is_err());
154    }
155}