//! RID station mapping: report column number (1..16) -> station metadata. //! Same precedence as the Python scraper: the runtime file //! (STATION_CONFIG_PATH, edited through the station CRUD API) if it exists, //! else the defaults bundled in src/data/stations.json. use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::BTreeMap; use std::path::Path; const BUNDLED: &str = include_str!("../../../src/data/stations.json"); #[derive(Clone, Debug, Deserialize)] pub struct StationInfo { pub code: String, pub thai_name: String, pub english_name: String, pub latitude: Option, pub longitude: Option, pub geohash: Option, } #[derive(Clone, Debug)] pub struct Stations(BTreeMap); impl Stations { pub fn load(runtime_path: &Path) -> Result { if runtime_path.exists() { let text = std::fs::read_to_string(runtime_path) .with_context(|| format!("reading {}", runtime_path.display()))?; let stations = Self::parse(&text) .with_context(|| format!("parsing {}", runtime_path.display()))?; tracing::info!( "loaded {} stations from {}", stations.0.len(), runtime_path.display() ); return Ok(stations); } let stations = Self::bundled(); tracing::info!("loaded {} bundled default stations", stations.0.len()); Ok(stations) } pub fn bundled() -> Self { Self::parse(BUNDLED).expect("bundled stations.json is valid") } fn parse(text: &str) -> Result { let raw: BTreeMap = serde_json::from_str(text)?; let map = raw .into_iter() .map(|(k, v)| Ok((k.trim().parse::()?, v))) .collect::>>()?; Ok(Stations(map)) } /// Metadata for report column `n`, with the Python fallback for unknown /// columns (`P.{19+n}` / `Station n`). pub fn get(&self, n: u32) -> StationInfo { self.0.get(&n).cloned().unwrap_or_else(|| StationInfo { code: format!("P.{}", 19 + n), thai_name: format!("Station {n}"), english_name: format!("Station {n}"), latitude: None, longitude: None, geohash: None, }) } } #[cfg(test)] mod tests { use super::*; #[test] fn bundled_mapping_has_the_16_rid_gauges() { let s = Stations::bundled(); assert_eq!(s.0.len(), 16); assert_eq!(s.get(1).code, "P.20"); assert_eq!(s.get(8).code, "P.1"); assert_eq!(s.get(99).code, "P.118"); // Python fallback } }