ecad_processor/readers/
station_reader.rs1use 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 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 if line.trim().is_empty() {
33 continue;
34 }
35
36 if self.skip_headers {
38 if !line
40 .trim_start()
41 .chars()
42 .next()
43 .unwrap_or(' ')
44 .is_ascii_digit()
45 {
46 continue;
47 }
48 }
49
50 if let Some(station) = self.parse_station_line(&line)? {
52 stations.push(station);
53 }
54 }
55
56 Ok(stations)
57 }
58
59 fn parse_station_line(&self, line: &str) -> Result<Option<StationMetadata>> {
61 let parts: Vec<&str> = line.split(',').map(|s| s.trim()).collect();
63
64 if parts.len() < 6 {
65 return Ok(None); }
67
68 let staid = parts[0].parse::<u32>().map_err(|_| {
70 ProcessingError::InvalidFormat(format!("Invalid station ID: '{}'", parts[0]))
71 })?;
72
73 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 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 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 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}