//! Henter og hurtiglagrer TMY-filer for norske kommuner. //! //! Datasettet «TMY files for Norway based on CERRA data from 1991-2020» (Zenodo, //! doi 10.5281/zenodo.14779899, CC-BY-4.0) er laget i tilknytning til NS 3031:2025 og dekker //! alle norske kommuner med timesverdier. Zip-filen lastes ned én gang til //! `$XDG_CACHE_HOME/enok` og pakkes ut derfra. use crate::data::normaliser_stedsnavn; use crate::klima::Vaerdata; use crate::Result; use anyhow::{anyhow, bail, Context}; use std::io::Read; use std::path::{Path, PathBuf}; const ZIP_URL: &str = "https://zenodo.org/api/records/19566253/files/EPW-TMYNO_CERRA_1991-2020.zip/content"; const ZIP_NAVN: &str = "EPW-TMYNO_CERRA_1991-2020.zip"; pub fn hurtiglager() -> PathBuf { std::env::var_os("ENOK_CACHE") .map(PathBuf::from) .or_else(|| std::env::var_os("XDG_CACHE_HOME").map(|c| PathBuf::from(c).join("enok"))) .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache/enok"))) .unwrap_or_else(|| PathBuf::from(".enok-cache")) } fn zip_sti() -> PathBuf { hurtiglager().join(ZIP_NAVN) } /// Laster ned TMY-arkivet dersom det ikke finnes lokalt. pub fn sikre_arkiv() -> Result { let sti = zip_sti(); if sti.exists() && std::fs::metadata(&sti)?.len() > 1_000_000 { return Ok(sti); } std::fs::create_dir_all(sti.parent().unwrap())?; eprintln!( "Laster ned TMY-klimadata (74 MB) fra Zenodo til {} …", sti.display() ); let svar = ureq::get(ZIP_URL) .timeout(std::time::Duration::from_secs(900)) .call() .context("nedlasting av TMY-arkivet feilet")?; let mut data = Vec::new(); svar.into_reader().read_to_end(&mut data)?; if data.len() < 1_000_000 { bail!("nedlastet arkiv er uventet lite ({} byte)", data.len()); } std::fs::write(&sti, &data)?; Ok(sti) } /// Navnene på alle kommuner i arkivet. pub fn kommuner() -> Result> { let sti = sikre_arkiv()?; let fil = std::fs::File::open(&sti)?; let mut zip = zip::ZipArchive::new(fil)?; let mut ut = Vec::new(); for i in 0..zip.len() { let navn = zip.by_index(i)?.name().to_string(); if let Some(k) = kommune_av_filnavn(&navn) { ut.push(k); } } ut.sort(); Ok(ut) } fn kommune_av_filnavn(navn: &str) -> Option { let base = Path::new(navn).file_name()?.to_str()?; let base = base.strip_suffix(".epw")?; let base = base.strip_prefix("TMYNO_")?; let base = base.strip_suffix("_CERRA_1991-2020")?; Some(base.to_string()) } /// Klimastedet nærmest en posisjon, og avstanden dit i km. /// /// Hver TMY-fil oppgir i første linje koordinatene for punktet dataene gjelder. Bare starten /// av hver fil pakkes ut, så oppslaget går raskt selv om arkivet er stort. pub fn naermeste_kommune(p: &crate::klima::geo::Posisjon) -> Result<(String, f64)> { let sti = sikre_arkiv()?; let fil = std::fs::File::open(&sti)?; let mut zip = zip::ZipArchive::new(fil)?; let mut best: Option<(String, f64)> = None; for i in 0..zip.len() { let oppf = zip.by_index(i)?; let Some(kommune) = kommune_av_filnavn(oppf.name()) else { continue; }; let mut hode = Vec::with_capacity(512); oppf.take(512).read_to_end(&mut hode)?; let linje = crate::klima::epw::avkod(&hode); let felt: Vec<&str> = linje.lines().next().unwrap_or("").split(',').collect(); let tall = |i: usize| felt.get(i).and_then(|s| s.trim().parse::().ok()); let (Some(bredde), Some(lengde)) = (tall(6), tall(7)) else { continue; }; let d = p.avstand_km(&crate::klima::geo::Posisjon { bredde, lengde }); if best.as_ref().is_none_or(|(_, bd)| d < *bd) { best = Some((kommune, d)); } } best.ok_or_else(|| anyhow!("TMY-arkivet inneholder ingen klimafiler med koordinater")) } /// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både /// «Indre Østfold» og «Indre Ostfold» treffer samme fil. pub fn for_kommune(kommune: &str) -> Result { let sti = sikre_arkiv()?; let fil = std::fs::File::open(&sti)?; let mut zip = zip::ZipArchive::new(fil)?; let mål = normaliser_stedsnavn(kommune); let mut treff = None; for i in 0..zip.len() { let navn = zip.by_index(i)?.name().to_string(); if let Some(k) = kommune_av_filnavn(&navn) { if normaliser_stedsnavn(&k) == mål { treff = Some((i, k)); break; } } } let (indeks, navn) = treff.ok_or_else(|| { anyhow!( "fant ingen TMY-fil for kommunen «{}» (bruk `enok klima --liste` for gyldige navn)", kommune ) })?; let mut oppf = zip.by_index(indeks)?; let mut data = Vec::new(); oppf.read_to_end(&mut data)?; let tekst = String::from_utf8_lossy(&data); let mut v = Vaerdata::fra_tekst(&tekst).with_context(|| format!("TMY-fil for {navn}"))?; v.sted = navn; Ok(v) }