1use 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
15pub 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
37pub 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 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 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 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}