bufkit_data/archive/query/
station_summary.rs1use crate::{
2 errors::BufkitDataErr,
3 models::Model,
4 site::{StateProv, StationNumber},
5};
6use chrono::FixedOffset;
7use std::{collections::HashMap, str::FromStr};
8use rusqlite::Statement;
9
10#[derive(Debug)]
12pub struct StationSummary {
13 pub station_num: StationNumber,
15 pub ids: Vec<String>,
17 pub models: Vec<Model>,
19 pub name: Option<String>,
21 pub notes: Option<String>,
23 pub state: Option<StateProv>,
25 pub time_zone: Option<FixedOffset>,
27 pub coords: Vec<(f64, f64)>,
29 pub number_of_files: u32,
31}
32
33struct StationEntry {
34 station_num: StationNumber,
35 id: Option<String>,
36 model: Option<Model>,
37 name: Option<String>,
38 notes: Option<String>,
39 state: Option<StateProv>,
40 time_zone: Option<FixedOffset>,
41 lat: f64,
42 lon: f64,
43 number_of_files: u32,
44}
45
46impl StationSummary {
47 pub fn ids_as_string(&self) -> String {
49 self.ids.join(", ")
50 }
51
52 pub fn models_as_string(&self) -> String {
54 self.models
55 .iter()
56 .map(|m| m.as_static_str().to_owned())
57 .collect::<Vec<_>>()
58 .join(", ")
59 }
60
61 pub fn coords_as_string(&self) -> String {
63 self.coords
64 .iter()
65 .map(|(lat, lon)| format!("({},{})", lat, lon))
66 .collect::<Vec<_>>()
67 .join(", ")
68 }
69}
70
71impl From<StationEntry> for StationSummary {
72 fn from(entry: StationEntry) -> Self {
73 let StationEntry {
74 station_num,
75 id,
76 model,
77 name,
78 notes,
79 state,
80 time_zone,
81 lat,
82 lon,
83 number_of_files,
84 } = entry;
85
86 let mut models = vec![];
87 if let Some(model) = model {
88 models.push(model);
89 }
90
91 let mut ids = vec![];
92 if let Some(id) = id {
93 ids.push(id);
94 }
95
96 let coords = vec![(lat, lon),];
97
98 StationSummary {
99 station_num,
100 ids,
101 models,
102 name,
103 notes,
104 state,
105 time_zone,
106 coords,
107 number_of_files,
108 }
109 }
110}
111
112impl crate::Archive {
113 pub fn station_summaries(&self) -> Result<Vec<StationSummary>, BufkitDataErr> {
115 let mut stmt = self.db_conn.prepare(include_str!("station_summary.sql"))?;
116
117 Self::process_summary_statement(&mut stmt)
118 }
119
120 pub fn station_summaries_near(&self, lat: f64, lon: f64) -> Result<Vec<StationSummary>, BufkitDataErr> {
122
123 let max_lat = lat + 0.5;
124 let min_lat = lat - 0.5;
125 let max_lon = lon + 0.5;
126 let min_lon = lon - 0.5;
127
128 let query_str = format!(r#"
129 SELECT
130 sites.station_num,
131 files.id,
132 files.model,
133 sites.name,
134 sites.state,
135 sites.notes,
136 sites.tz_offset_sec,
137 files.lat,
138 files.lon,
139 COUNT(files.station_num)
140 FROM sites LEFT JOIN files ON files.station_num = sites.station_num
141 WHERE files.lat > {} AND files.lat < {} AND files.lon > {} AND files.lon < {}
142 GROUP BY sites.station_num, id, model, lat, lon
143 "#, min_lat, max_lat, min_lon, max_lon);
144
145 let mut stmt = self.db_conn.prepare(&query_str)?;
146
147 Self::process_summary_statement(&mut stmt)
148 }
149
150 fn process_summary_statement(stmt: &mut Statement) -> Result<Vec<StationSummary>, BufkitDataErr> {
151
152 let mut vals: HashMap<StationNumber, StationSummary> = HashMap::new();
153
154 stmt.query_and_then([], Self::parse_row_to_entry)?
155 .for_each(|stn_entry| {
156 if let Ok(stn_entry) = stn_entry {
157 if let Some(summary) = vals.get_mut(&stn_entry.station_num) {
158 if let Some(id) = stn_entry.id {
159 summary.ids.push(id);
160 }
161
162 if let Some(model) = stn_entry.model {
163 summary.models.push(model);
164 }
165
166 summary.number_of_files += stn_entry.number_of_files;
167 } else {
168 vals.insert(stn_entry.station_num, StationSummary::from(stn_entry));
169 }
170 }
171 });
172
173 let mut vals: Vec<StationSummary> = vals.into_iter().map(|(_, v)| v).collect();
174
175 vals.iter_mut().for_each(|summary| {
176 summary.ids.sort_unstable();
177 summary.ids.dedup();
178 summary.models.sort_unstable();
179 summary.models.dedup();
180 });
181
182 Ok(vals)
183 }
184
185 fn parse_row_to_entry(row: &rusqlite::Row) -> Result<StationEntry, rusqlite::Error> {
186 let station_num: StationNumber = row.get::<_, u32>(0).map(StationNumber::from)?;
187 let id: Option<String> = row.get(1)?;
188
189 let model: Option<Model> = row.get::<_, Option<String>>(2).and_then(|string_opt| {
190 string_opt
191 .map(|string| Model::from_str(&string).map_err(|_| rusqlite::Error::InvalidQuery))
192 .transpose()
193 })?;
194
195 let name: Option<String> = row.get(3)?;
196
197 let state: Option<StateProv> = row
198 .get::<_, String>(4)
199 .ok()
200 .and_then(|a_string| StateProv::from_str(&a_string).ok());
201
202 let notes: Option<String> = row.get(5)?;
203
204 let time_zone: Option<chrono::FixedOffset> =
205 row.get::<_, i32>(6).ok().and_then(|offset: i32| {
206 if offset < 0 {
207 chrono::FixedOffset::west_opt(offset.abs())
208 } else {
209 chrono::FixedOffset::east_opt(offset)
210 }
211 });
212
213 let lat: f64 = row.get(7)?;
214 let lon: f64 = row.get(8)?;
215 let number_of_files: u32 = row.get(9)?;
216
217 Ok(StationEntry {
218 station_num,
219 id,
220 model,
221 name,
222 state,
223 notes,
224 time_zone,
225 lat,
226 lon,
227 number_of_files,
228 })
229 }
230}
231
232#[cfg(test)]
233mod unit {
234 use crate::archive::unit::*; use crate::{Model, StationNumber};
236
237 #[test]
238 fn test_summaries() {
239 let TestArchive {
240 tmp: _tmp,
241 mut arch,
242 } = create_test_archive().expect("Failed to create test archive.");
243
244 fill_test_archive(&mut arch);
245
246 let sums = arch.station_summaries().unwrap();
247
248 for sum in sums {
249 println!("{:?}", sum);
250
251 assert_eq!(sum.ids.len(), 1);
252 assert_eq!(sum.ids[0], "KMSO");
253
254 assert_eq!(sum.models.len(), 2);
255 assert!(sum.models.contains(&Model::GFS));
256 assert!(sum.models.contains(&Model::NAM));
257
258 assert_eq!(sum.station_num, StationNumber::new(727730));
259 assert_eq!(sum.number_of_files, 6);
260 assert!(sum.name.is_none());
261 assert!(sum.notes.is_none());
262 assert!(sum.time_zone.is_none());
263 assert!(sum.state.is_none());
264 }
265 }
266}