diff --git a/Cargo.lock b/Cargo.lock index aa7736c..e0cb9fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ dependencies = [ [[package]] name = "enok" -version = "0.4.3" +version = "0.4.4" dependencies = [ "anyhow", "approx", diff --git a/Cargo.toml b/Cargo.toml index 4fae9ae..de3bef4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enok" -version = "0.4.3" +version = "0.4.4" edition = "2021" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" license = "MIT" diff --git a/src/inndata/ifc.rs b/src/inndata/ifc.rs index 36bd471..5763e5c 100644 --- a/src/inndata/ifc.rs +++ b/src/inndata/ifc.rs @@ -286,6 +286,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu // Den laveste etasjen over terreng, der etasjeskilleren mot kjelleren ligger. let forste_over = etasjer.iter().position(|e| !e.under_terreng); // Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken. + let terrengmodell = terrengpunkter(&fil, skala); let terreng = terrenghoyder(&fil, skala); // Ligger terrengmodellen over gulvet i første etasje, er den ikke lagt i bakkenivå, og da // sier den ingenting om hvor mye av grunnmuren som er nedgravd. @@ -308,6 +309,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu let mut utelatt: Vec<(String, f64, Option)> = Vec::new(); let mut uten_uverdi: Vec = Vec::new(); let mut uten_geometri = 0usize; + let mut grunnmurshoyder: Vec = Vec::new(); let mut asimut_for_vegg: HashMap = HashMap::new(); // Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere. @@ -359,6 +361,16 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu if d.u_verdi.is_none() { uten_uverdi.push(d.navn.clone()); } + // Terrengmodellen sier hvor høyt bakken ligger akkurat der denne veggen står, så en + // skrånende tomt gir forskjellig høyde over bakken på hver side av huset. + let over_bakken = d + .plass + .and_then(|(x, y, _, _, _)| terreng_ved(&terrengmodell, x, y)) + .map(|z| (etasjekontekst.forste_over_hoyde.unwrap_or(0.0) - z).clamp(0.0, kjellerdybde)) + .unwrap_or(kjeller_over_terreng); + if i_kjeller && mot == Mot::Grunn { + grunnmurshoyder.push(over_bakken); + } let mal = if i_kjeller { &mut kjellerflater } else { @@ -378,8 +390,8 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu b_faktor: None, absorptans: None, omkrets: None, - dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - kjeller_over_terreng), - over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(kjeller_over_terreng), + dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - over_bakken), + over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(over_bakken), sone, kommentar: d .etasje @@ -693,7 +705,21 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu rom.join(", ") )), }); - let grunnmur = if terreng_over_gulvet { + let grunnmur = if !grunnmurshoyder.is_empty() && !terrengmodell.is_empty() { + let laveste = grunnmurshoyder + .iter() + .copied() + .fold(f64::INFINITY, f64::min); + let hoyeste = grunnmurshoyder + .iter() + .copied() + .fold(f64::NEG_INFINITY, f64::max); + let snitt = grunnmurshoyder.iter().sum::() / grunnmurshoyder.len() as f64; + format!( + "Terrengmodellen i filen gir hver kjellervegg sin egen høyde over bakken: \ + {laveste:.2}–{hoyeste:.2} m, i snitt {snitt:.2} m. Resten av veggen er regnet nedgravd." + ) + } else if terreng_over_gulvet { format!( "Terrengmodellen ligger over gulvet i første etasje, så den er ikke brukt. \ Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Legg \ @@ -1224,6 +1250,45 @@ fn terrenghoyder(fil: &Fil, skala: f64) -> Option<(f64, f64)> { Some((referanse, terreng)) } +/// Punktene i terrengmodellen, i byggets koordinater, m. +/// +/// Vectorworks legger terrenget som et `IfcGeographicElement` med typen TERRAIN, eller på +/// tomta. Punktene brukes til å finne bakkenivået ved hver enkelt kjellervegg. +fn terrengpunkter(fil: &Fil, skala: f64) -> Vec<[f64; 3]> { + let mut ut = Vec::new(); + for (_, e) in fil + .av_type("IFCGEOGRAPHICELEMENT") + .chain(fil.av_type("IFCSITE")) + { + let (px, py, pz) = e + .arg(5) + .and_then(|a| a.som_ref()) + .and_then(|p| global_plassering(fil, p, skala)) + .map(|(x, y, z, _, _)| (x, y, z)) + .unwrap_or((0.0, 0.0, 0.0)); + for nett in ifcgeom::netter_for_produkt(fil, e.arg(6).and_then(|a| a.som_ref())) { + for p in &nett.punkter { + ut.push([p[0] * skala + px, p[1] * skala + py, p[2] * skala + pz]); + } + } + } + ut +} + +/// Bakkenivået ved et punkt, som snittet av de nærmeste punktene i terrengmodellen, m. +fn terreng_ved(punkter: &[[f64; 3]], x: f64, y: f64) -> Option { + if punkter.is_empty() { + return None; + } + let mut avstand: Vec<(f64, f64)> = punkter + .iter() + .map(|p| ((p[0] - x).powi(2) + (p[1] - y).powi(2), p[2])) + .collect(); + avstand.sort_by(|a, b| a.0.total_cmp(&b.0)); + let antall = avstand.len().min(8); + Some(avstand[..antall].iter().map(|(_, z)| z).sum::() / antall as f64) +} + /// Hvor mye av grunnmuren som står over bakken, m. /// /// Toppen av grunnmuren ligger ved gulvet i første etasje over kjelleren. Den står over @@ -2095,6 +2160,20 @@ mod tester { assert!(r.merknader.iter().any(|m| m.contains("standardplassering"))); } + #[test] + fn terrenget_leses_av_de_naermeste_punktene() { + // Et terreng som faller mot øst: bakken er høyere ved vestveggen enn ved østveggen. + let punkter: Vec<[f64; 3]> = (0..10) + .flat_map(|i| (0..10).map(move |j| [i as f64, j as f64, -1.0 - 0.2 * i as f64])) + .collect(); + let vest = terreng_ved(&punkter, 0.5, 5.0).unwrap(); + let ost = terreng_ved(&punkter, 8.5, 5.0).unwrap(); + assert!(vest > ost, "vest {vest}, øst {ost}"); + assert!((vest + 1.1).abs() < 0.2, "vest {vest}"); + assert!((ost + 2.7).abs() < 0.2, "øst {ost}"); + assert_eq!(terreng_ved(&[], 0.0, 0.0), None); + } + #[test] fn grunnmur_over_terreng_av_hoydene() { // ±0 ligger 131,2 moh., terrenget 130,6 moh. og første etasje på ±0: 0,6 m over bakken.