ecad_processor/readers/
station_reader.rs

1use crate::error::{ProcessingError, Result};
2use crate::models::StationMetadata;
3use crate::utils::coordinates::parse_coordinate;
4use std::collections::HashMap;
5use std::fs::File;
6use std::io::{BufRead, BufReader};
7use std::path::Path;
8
9pub struct StationReader {
10    skip_headers: bool,
11}
12
13impl StationReader {
14    pub fn new() -> Self {
15        Self { skip_headers: true }
16    }
17
18    pub fn with_skip_headers(skip_headers: bool) -> Self {
19        Self { skip_headers }
20    }
21
22    /// Read station metadata from the stations.txt file
23    pub fn read_stations(&self, path: &Path) -> Result<Vec<StationMetadata>> {
24        let file = File::open(path)?;
25        let reader = BufReader::new(file);
26        let mut stations = Vec::new();
27        for (_line_count, line_result) in reader.lines().enumerate() {
28            let line = line_result?;
29            let _line_count = _line_count + 1;
30
31            // Skip empty lines
32            if line.trim().is_empty() {
33                continue;
34            }
35
36            // Skip header lines and detect data start automatically
37            if self.skip_headers {
38                // Skip lines that don't start with a number (station ID)
39                if !line
40                    .trim_start()
41                    .chars()
42                    .next()
43                    .unwrap_or(' ')
44                    .is_ascii_digit()
45                {
46                    continue;
47                }
48            }
49
50            // Parse station data
51            if let Some(station) = self.parse_station_line(&line)? {
52                stations.push(station);
53            }
54        }
55
56        Ok(stations)
57    }
58
59    /// Parse a single line from the stations file
60    fn parse_station_line(&self, line: &str) -> Result<Option<StationMetadata>> {
61        // Expected format: STAID, STANAME                                 , CN, LAT    , LON     , HGHT
62        let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
63
64        if parts.len() < 6 {
65            return Ok(None); // Skip malformed lines
66        }
67
68        // Parse station ID
69        let staid = parts[0].parse::<u32>().map_err(|_| {
70            ProcessingError::InvalidFormat(format!("Invalid station ID: '{}'", parts[0]))
71        })?;
72
73        // Parse other fields
74        let name = parts[1].to_string();
75        let country = parts[2].to_string();
76        let latitude = parse_coordinate(parts[3])?;
77        let longitude = parse_coordinate(parts[4])?;
78
79        // Parse elevation (can be negative or missing)
80        let elevation = if parts[5].is_empty() || parts[5] == "-999" {
81            None
82        } else {
83            Some(parts[5].parse::<i32>().map_err(|_| {
84                ProcessingError::InvalidFormat(format!("Invalid elevation: '{}'", parts[5]))
85            })?)
86        };
87
88        Ok(Some(StationMetadata::new(
89            staid, name, country, latitude, longitude, elevation,
90        )))
91    }
92
93    /// Read station metadata from a map of station IDs
94    pub fn read_stations_map(&self, path: &Path) -> Result<HashMap<u32, StationMetadata>> {
95        let stations = self.read_stations(path)?;
96        let mut map = HashMap::with_capacity(stations.len());
97
98        for station in stations {
99            map.insert(station.staid, station);
100        }
101
102        Ok(map)
103    }
104}
105
106impl Default for StationReader {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115    use std::io::Write;
116    use tempfile::NamedTempFile;
117
118    #[test]
119    fn test_parse_station_line() {
120        let reader = StationReader::new();
121
122        let line = "12345, London Weather Station        , GB, 51:30:26, -0:07:39,   35";
123        let station = reader.parse_station_line(line).unwrap().unwrap();
124
125        assert_eq!(station.staid, 12345);
126        assert_eq!(station.name, "London Weather Station");
127        assert_eq!(station.country, "GB");
128        assert!((station.latitude - 51.507222).abs() < 0.00001);
129        assert!((station.longitude - -0.1275).abs() < 0.00001);
130        assert_eq!(station.elevation, Some(35));
131    }
132
133    #[test]
134    fn test_read_stations_file() -> Result<()> {
135        let mut temp_file = NamedTempFile::new()?;
136        writeln!(
137            temp_file,
138            "STAID, STANAME                                 , CN, LAT    , LON     , HGHT"
139        )?;
140        writeln!(
141            temp_file,
142            "------,----------------------------------------,---,--------,--------,-----"
143        )?;
144        writeln!(temp_file, "")?;
145        writeln!(
146            temp_file,
147            "    1, VAEXJOE                                 , SE, 56:52:00, 14:48:00,  166"
148        )?;
149        writeln!(
150            temp_file,
151            "    2, BRAGANCA                                , PT, 41:48:00, -6:44:00,  691"
152        )?;
153
154        let reader_test = StationReader::new();
155        let stations = reader_test.read_stations(temp_file.path())?;
156
157        assert_eq!(stations.len(), 2);
158        assert_eq!(stations[0].staid, 1);
159        assert_eq!(stations[0].name, "VAEXJOE");
160        assert_eq!(stations[1].staid, 2);
161        assert_eq!(stations[1].name, "BRAGANCA");
162
163        Ok(())
164    }
165
166    #[test]
167    fn test_read_real_stations_file() -> Result<()> {
168        use std::path::Path;
169
170        let stations_path = Path::new("data/uk_temp_min/stations.txt");
171        if !stations_path.exists() {
172            // Skip test if data file doesn't exist
173            return Ok(());
174        }
175
176        let reader = StationReader::new();
177        let stations = reader.read_stations(stations_path)?;
178
179        println!("Found {} stations", stations.len());
180        if !stations.is_empty() {
181            println!(
182                "First station: ID={}, Name={}",
183                stations[0].staid, stations[0].name
184            );
185        }
186
187        assert!(!stations.is_empty(), "Should find at least one station");
188
189        Ok(())
190    }
191}