bufkit_data/archive/query/
station_summary.rs

1use 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/// A summary of the information about a station.
11#[derive(Debug)]
12pub struct StationSummary {
13    /// Station number
14    pub station_num: StationNumber,
15    /// List of ids associated with this site
16    pub ids: Vec<String>,
17    /// All the models in the archive associated with this site
18    pub models: Vec<Model>,
19    /// Station name, common name
20    pub name: Option<String>,
21    /// Notes related to the site
22    pub notes: Option<String>,
23    /// The state-province associated with the site.
24    pub state: Option<StateProv>,
25    /// The time zone offset to local standard time.
26    pub time_zone: Option<FixedOffset>,
27    /// Coordinates
28    pub coords: Vec<(f64, f64)>,
29    /// The number of files in the archive related to this site.
30    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    /// Concantenate the ids into a comma separated list.
48    pub fn ids_as_string(&self) -> String {
49        self.ids.join(", ")
50    }
51
52    /// Concatenate the models into a comma separated list.
53    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    /// Concatenate the different coordinates as a string.
62    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    /// Get a summary of all the stations in the archive.
114    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    /// Get a summary of all the stations in the archive near a point..
121    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::*; // test helpers.
235    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}