survex_rs/
read.rs

1//! Helper functions for reading Survex files
2//!
3//! At present, this module only contains a single function:
4//! [`load_from_path`][`crate::read::load_from_path`]. Refer to the documentation for that function,
5//! or the [examples in the documentation index][`crate`] for more information.
6
7use crate::data::SurveyData;
8use crate::station::Point;
9use crate::survex;
10use log::trace;
11use std::error::Error;
12use std::ffi::{c_char, CStr};
13use std::path::PathBuf;
14use std::ptr;
15use uuid::Uuid;
16
17/// Create a [`SurveyData`] instance from a Survex file.
18///
19/// The path to the Survex file will be passed to the binding to the Survex C library, which will
20/// open and read the file. The data within the file will be iterated over to build a list of
21/// [Stations][`crate::station::Station`] and a graph of connections between them. The resulting
22/// [`SurveyData`] instance will be returned.
23pub fn load_from_path(path: PathBuf) -> Result<SurveyData, Box<dyn Error>> {
24    // Convert the path to the format required by img.c
25    let filename = path
26        .to_str()
27        .expect("Could not convert path to string")
28        .as_ptr() as *const c_char;
29
30    // Create an SurveyData instance to store and update data as it is read.
31    let mut data = SurveyData::new();
32
33    // The way Survex 3D file reading works is that it will first spit out a bunch of coordinates
34    // and centrelines (determined by MOVE and LINE) commands, and it will then later give names
35    // to those coordinates by means of a LABEL command. As such, we will store the connections
36    // between two coordinates in a vector and then later - once we have read the full .3d file and
37    // have labels for all sets of coordinates - add the connections to the graph.
38    let mut connections = Vec::new();
39
40    // These variables are used to store the data which is returned by each call to img_read_item.
41    // After a call to img_read_item, pimg will be updated with information from the current item,
42    // and p will be updated with the latest set of coordinates.
43
44    // (x, y, z) and label are used to store the previous label and set of coordinates after a
45    // call to img_read_item, as the next call may require them (such as in the case of a LINE
46    // command to create a leg between two points).
47    let pimg;
48    let (mut x, mut y, mut z) = (-1.0, -1.0, -1.0);
49    let mut label = "";
50    let mut p = survex::img_point {
51        x: 0.0,
52        y: 0.0,
53        z: 0.0,
54    };
55
56    // Open the Survex file and check that it was successful.
57    trace!(
58        "Opening Survex file '{:?}' in load_from_path function via Survex img library.",
59        path
60    );
61    unsafe {
62        pimg = survex::img_open_survey(filename, ptr::null_mut());
63    }
64    if pimg.is_null() {
65        trace!("Survex library returned a null pointer. Read failed.");
66        return Err("Could not open Survex file".into());
67    }
68
69    // Read the data from the Survex file - loop through calls to img_read_item until it returns
70    // a value below zero which indicates that the end of the data has been reached (-1) or that
71    // there is an error (-2).
72    trace!("Reading Survex file in load_from_path function.");
73    loop {
74        let result = unsafe { survex::img_read_item(pimg, &mut p) };
75
76        #[allow(clippy::if_same_then_else)]
77        if result == -2 {
78            // Bad data in Survex file
79            panic!("Bad data in Survex file.");
80        } else if result == -1 {
81            trace!("STOP: End of Survex file reached.");
82            // STOP command
83            break;
84        } else if result == 0 {
85            // MOVE command
86            (x, y, z) = (p.x, p.y, p.z);
87            trace!("MOVE: {}, {}, {}.", x, y, z);
88        } else if result == 1 {
89            // LINE command
90            // At this point (x, y, z) will have been set by a previous MOVE command. We can use
91            // the previous coordinates to create a connection between the previous station and
92            // the current station. After the 3d file has been read, we can use the connections
93            // vector to add the connections to the graph.
94            let from_coords = Point::new(x, y, z);
95            let to_coords = Point::new(p.x, p.y, p.z);
96            connections.push((from_coords, to_coords));
97            trace!("LINE: {} -> {}.", from_coords, to_coords);
98            (x, y, z) = (p.x, p.y, p.z);
99        } else if result == 2 {
100            // CROSS command
101            trace!("CROSS command received. Ignoring.");
102        } else if result == 3 {
103            // LABEL command
104            let flags;
105            unsafe {
106                label = CStr::from_ptr((*pimg).label).to_str().unwrap();
107                flags = (*pimg).flags & 0x7f;
108            }
109            let coords = Point::new(p.x, p.y, p.z);
110            let (station, _) = data.add_or_update(coords, label);
111            trace!("LABEL: {} -> {}.", coords, label);
112
113            // Set the flags for the station
114            if flags & 0x01 != 0 {
115                station.borrow_mut().surface = true;
116                trace!("LABEL: surface flag set for station '{}'.", label);
117            }
118            if flags & 0x02 != 0 {
119                station.borrow_mut().underground = true;
120                trace!("LABEL: underground flag set for station '{}'.", label);
121            }
122            if flags & 0x04 != 0 {
123                station.borrow_mut().entrance = true;
124                trace!("LABEL: entrance flag set for station '{}'.", label);
125            }
126            if flags & 0x08 != 0 {
127                station.borrow_mut().exported = true;
128                trace!("LABEL: exported flag set for station '{}'.", label);
129            }
130            if flags & 0x10 != 0 {
131                station.borrow_mut().fixed = true;
132                trace!("LABEL: fixed flag set for station '{}'.", label);
133            }
134            if flags & 0x20 != 0 {
135                // Anonymous stations are given a UUID as their label
136                station.borrow_mut().anonymous = true;
137                trace!("LABEL: anonymous flag set for station '{}'.", label);
138                station.borrow_mut().label = Uuid::new_v4().to_string();
139                trace!(
140                    "LABEL: UUID '{}' set for anonymous station.",
141                    station.borrow().label,
142                );
143            }
144            if flags & 0x40 != 0 {
145                station.borrow_mut().wall = true;
146                trace!("LABEL: wall flag set for station '{}'.", label);
147            }
148        } else if result == 4 {
149            // XSECT command
150            let (l, r, u, d, flags);
151            unsafe {
152                l = (*pimg).l;
153                r = (*pimg).r;
154                u = (*pimg).u;
155                d = (*pimg).d;
156                flags = (*pimg).flags & 0x7f;
157
158                // If 0x20 flag is set, do *not* update the label buffer, and instead use the
159                // previous label.
160                if flags & 0x20 == 0 {
161                    label = CStr::from_ptr((*pimg).label).to_str().unwrap();
162                    trace!("XSECT: label set to '{}'.", label);
163                } else {
164                    trace!("XSECT: label not from {}.", label);
165                }
166            }
167            trace!("XSECT: l={}, r={}, u={}, d={} for {}.", l, r, u, d, label);
168            data.get_by_label(label)
169                .unwrap_or_else(|| panic!("Could not find station with label {:?}", label))
170                .borrow_mut()
171                .lrud
172                .update(l, r, u, d);
173        } else if result == 5 {
174            // XSECT_END command
175            trace!("XSECT_END command received. Ignoring.");
176        } else if result == 6 {
177            // ERROR_INFO command
178            trace!("ERROR_INFO command received. Ignoring.");
179        } else {
180            panic!("Unknown item type in Survex file");
181        }
182    }
183
184    trace!(
185        "Survex file reading complete. Processed {} stations and {} connections.",
186        data.stations.len(),
187        connections.len()
188    );
189
190    // Survex file reading is complete. We now need to iterate over the connections vector and
191    // add the connections to the graph by looking up the node index for each station and adding
192    // an edge between them with the distance between the two stations as the weight.
193    for (p1, p2) in connections.iter() {
194        let from_station_node_index = data
195            .get_by_coords(p1)
196            .unwrap_or_else(|| panic!("Could not find station with coordinates {:?}", p1))
197            .borrow()
198            .index;
199        let to_station_node_index = data
200            .get_by_coords(p2)
201            .unwrap_or_else(|| panic!("Could not find station with coordinates {:?}", p2))
202            .borrow()
203            .index;
204        data.graph.add_edge(
205            from_station_node_index,
206            to_station_node_index,
207            p1.distance(p2),
208        );
209    }
210
211    trace!(
212        "Graph now has {} nodes and {} edges.",
213        data.graph.node_count(),
214        data.graph.edge_count()
215    );
216
217    Ok(data)
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn load_file() {
226        let path = PathBuf::from("tests/data/0733.3d");
227        assert!(load_from_path(path).is_ok());
228    }
229
230    #[test]
231    fn load_invalid_file() {
232        let path = PathBuf::from("tests/data/this-file-does-not-exist.3d");
233        assert!(load_from_path(path).is_err());
234    }
235
236    /// Check that the correct number of stations are generated from the 3d file. The verification
237    /// values were created by checking how many NODE lines were generated when running the same 3d
238    /// file through Survex `dump3d`.
239    #[test]
240    fn check_correct_number_nodes_generated() {
241        let path = PathBuf::from("tests/data/0733.3d");
242        let manager = load_from_path(path).unwrap();
243        assert_eq!(manager.stations.len(), 6104);
244
245        let path = PathBuf::from("tests/data/nottsii.3d");
246        let manager = load_from_path(path).unwrap();
247        assert_eq!(manager.stations.len(), 1904);
248    }
249
250    #[test]
251    /// As above, the verification values were calculated by checking how many LEG lines were
252    /// generated when running the 3d file through Survex `dump3d` with the `-l` option.
253    fn check_correct_number_legs_generated() {
254        let path = PathBuf::from("tests/data/0733.3d");
255        let manager = load_from_path(path).unwrap();
256        assert_eq!(manager.graph.edge_count(), 5929);
257
258        let path = PathBuf::from("tests/data/nottsii.3d");
259        let manager = load_from_path(path).unwrap();
260        assert_eq!(manager.graph.edge_count(), 1782);
261    }
262
263    #[test]
264    fn test_absent_lrud_measurements_are_represented_correctly() {
265        let path = PathBuf::from("tests/data/nottsii.3d");
266        let manager = load_from_path(path).unwrap();
267        let station = manager
268            .get_by_label("nottsii.inlet5.inlet5-resurvey-4.22")
269            .unwrap();
270        let station = station.borrow();
271        assert_eq!(station.lrud.left, None);
272        assert_eq!(station.lrud.right, None);
273        assert_eq!(station.lrud.up, None);
274        assert_eq!(station.lrud.down, Some(9.0));
275    }
276
277    #[test]
278    fn test_lrud_measurements_are_represented_correctly() {
279        let path = PathBuf::from("tests/data/nottsii.3d");
280        let manager = load_from_path(path).unwrap();
281        let station = manager
282            .get_by_label("nottsii.inlet5.inlet5-resurvey-4.26")
283            .unwrap();
284        let station = station.borrow();
285        assert_eq!(station.lrud.left, Some(1.0));
286        assert_eq!(station.lrud.right, Some(0.0));
287        assert_eq!(station.lrud.up, Some(0.3));
288        assert_eq!(station.lrud.down, Some(0.6));
289    }
290
291    #[test]
292    fn test_flags_are_set_correctly() {
293        let path = PathBuf::from("tests/data/nottsii.3d");
294        let manager = load_from_path(path).unwrap();
295        let station = manager.get_by_label("nottsii.entrance").unwrap();
296        let station = station.borrow();
297        assert_eq!(station.surface, false);
298        assert_eq!(station.underground, false);
299        assert_eq!(station.entrance, true);
300        assert_eq!(station.exported, true);
301        assert_eq!(station.fixed, true);
302        assert_eq!(station.anonymous, false);
303        assert_eq!(station.wall, false);
304
305        let station = manager
306            .get_by_label("nottsii.inlet5.inlet5-resurvey-2.3.17")
307            .unwrap();
308        let station = station.borrow();
309        assert_eq!(station.surface, false);
310        assert_eq!(station.underground, true);
311        assert_eq!(station.entrance, false);
312        assert_eq!(station.exported, true);
313        assert_eq!(station.fixed, false);
314        assert_eq!(station.anonymous, false);
315        assert_eq!(station.wall, false);
316
317        let station = manager
318            .get_by_label("nottsii.mainstreamway.mainstreamway3.27")
319            .unwrap();
320        let station = station.borrow();
321        assert_eq!(station.surface, false);
322        assert_eq!(station.underground, true);
323        assert_eq!(station.entrance, false);
324        assert_eq!(station.exported, false);
325        assert_eq!(station.fixed, false);
326        assert_eq!(station.anonymous, false);
327        assert_eq!(station.wall, false);
328
329        let station = manager
330            .get_by_label("nottsii.countlazloall.thecupcake.009")
331            .unwrap();
332        let station = station.borrow();
333        assert_eq!(station.surface, true);
334        assert_eq!(station.underground, false);
335        assert_eq!(station.entrance, false);
336        assert_eq!(station.exported, false);
337        assert_eq!(station.fixed, false);
338        assert_eq!(station.anonymous, false);
339        assert_eq!(station.wall, false);
340    }
341}