//! Import av gbXML fra BIM-verktøy (Vectorworks, Archicad, Revit m.fl.). //! //! gbXML er formatet Vectorworks Architect eksporterer for energianalyse, og inneholder rom, //! flater med himmelretning og helning, åpninger, konstruksjoner og vindustyper. Importen //! oversetter dette til en `bygning.toml` som kan fylles ut med det gbXML ikke beskriver //! (varmekilder, ventilasjon, lekkasjetall, kommune). use crate::modell::*; use crate::Result; use anyhow::{anyhow, Context}; use quick_xml::events::Event; use quick_xml::Reader; use std::collections::HashMap; use std::path::Path; #[derive(Default, Debug, Clone)] struct Material { tykkelse: f64, ledningsevne: f64, r_verdi: f64, densitet: f64, varmekapasitet: f64, } #[derive(Default, Debug, Clone)] struct Konstruksjon { navn: String, u_verdi: Option, lag: Vec, } #[derive(Default, Debug, Clone)] struct Vindustype { navn: String, u_verdi: Option, shgc: Option, } #[derive(Default, Debug, Clone)] struct Aapning { navn: String, typ: String, vindustype: String, bredde: f64, hoyde: f64, areal: Option, polygon: Vec<[f64; 3]>, } #[derive(Default, Debug, Clone)] struct Overflate { navn: String, typ: String, konstruksjon: String, asimut: Option, helning: Option, bredde: f64, hoyde: f64, areal: Option, polygon: Vec<[f64; 3]>, naborom: Vec, aapninger: Vec, } #[derive(Default, Debug)] struct Rom { areal: f64, volum: f64, } /// Rapport fra importen: hva som ble lest og hva som må fylles ut manuelt. pub struct Importrapport { pub bygning: Bygning, pub merknader: Vec, } pub fn les(sti: &Path) -> Result { let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?; fra_tekst( &tekst, sti.file_stem() .and_then(|s| s.to_str()) .unwrap_or("Bygning"), ) } pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { let mut leser = Reader::from_str(tekst); leser.config_mut().trim_text(true); let mut materialer: HashMap = HashMap::new(); let mut lag: HashMap> = HashMap::new(); let mut konstruksjoner: HashMap = HashMap::new(); let mut vindustyper: HashMap = HashMap::new(); let mut overflater: Vec = Vec::new(); let mut rom: Vec = Vec::new(); // Gjeldende kontekst under parsingen. let mut sti_elementer: Vec = Vec::new(); let mut gjeldende_material: Option<(String, Material)> = None; let mut gjeldende_lag: Option<(String, Vec)> = None; let mut gjeldende_konstruksjon: Option<(String, Konstruksjon)> = None; let mut gjeldende_vindustype: Option<(String, Vindustype)> = None; let mut gjeldende_overflate: Option = None; let mut gjeldende_aapning: Option = None; let mut gjeldende_rom: Option = None; let mut gjeldende_punkt: Vec = Vec::new(); let mut i_planar = false; let mut bygningsnavn: Option = None; let mut bygningstype: Option = None; let mut tekstbuffer = String::new(); let mut buf = Vec::new(); loop { match leser.read_event_into(&mut buf) { Err(e) => return Err(anyhow!("gbXML er ikke gyldig XML: {e}")), Ok(Event::Eof) => break, Ok(Event::Start(e)) => { let navn = lokalt_navn(e.name().as_ref()); let attr = |n: &str| -> Option { e.attributes().flatten().find_map(|a| { if lokalt_navn(a.key.as_ref()) == n { Some(String::from_utf8_lossy(&a.value).to_string()) } else { None } }) }; match navn.as_str() { "Material" => { gjeldende_material = Some((attr("id").unwrap_or_default(), Material::default())) } "Layer" => gjeldende_lag = Some((attr("id").unwrap_or_default(), Vec::new())), "Construction" => { gjeldende_konstruksjon = Some((attr("id").unwrap_or_default(), Konstruksjon::default())) } "WindowType" => { gjeldende_vindustype = Some((attr("id").unwrap_or_default(), Vindustype::default())) } "Building" => { bygningstype = attr("buildingType"); } "Space" => gjeldende_rom = Some(Rom::default()), "Surface" => { gjeldende_overflate = Some(Overflate { navn: attr("id").unwrap_or_default(), typ: attr("surfaceType").unwrap_or_default(), konstruksjon: attr("constructionIdRef").unwrap_or_default(), ..Default::default() }) } "Opening" => { gjeldende_aapning = Some(Aapning { navn: attr("id").unwrap_or_default(), typ: attr("openingType").unwrap_or_default(), vindustype: attr("windowTypeIdRef").unwrap_or_default(), ..Default::default() }) } "PlanarGeometry" => i_planar = true, "CartesianPoint" => gjeldende_punkt.clear(), _ => {} } sti_elementer.push(navn); tekstbuffer.clear(); } Ok(Event::Empty(e)) => { let navn = lokalt_navn(e.name().as_ref()); let attr = |n: &str| -> Option { e.attributes().flatten().find_map(|a| { if lokalt_navn(a.key.as_ref()) == n { Some(String::from_utf8_lossy(&a.value).to_string()) } else { None } }) }; match navn.as_str() { "MaterialId" => { if let (Some((_, l)), Some(r)) = (gjeldende_lag.as_mut(), attr("materialIdRef")) { l.push(r); } } "LayerId" => { if let (Some((_, k)), Some(r)) = (gjeldende_konstruksjon.as_mut(), attr("layerIdRef")) { k.lag.push(r); } } "AdjacentSpaceId" => { if let (Some(o), Some(r)) = (gjeldende_overflate.as_mut(), attr("spaceIdRef")) { o.naborom.push(r); } } _ => {} } } Ok(Event::Text(t)) => { tekstbuffer = t.unescape().unwrap_or_default().to_string(); } Ok(Event::End(e)) => { let navn = lokalt_navn(e.name().as_ref()); let tall = tekstbuffer.trim().parse::().ok(); let i_aapning = gjeldende_aapning.is_some(); match navn.as_str() { "Thickness" => sett(&mut gjeldende_material, |m| { m.tykkelse = tall.unwrap_or(0.0) }), "Conductivity" => sett(&mut gjeldende_material, |m| { m.ledningsevne = tall.unwrap_or(0.0) }), "Density" => sett(&mut gjeldende_material, |m| { m.densitet = tall.unwrap_or(0.0) }), "SpecificHeat" => sett(&mut gjeldende_material, |m| { m.varmekapasitet = tall.unwrap_or(0.0) }), "R-value" => sett(&mut gjeldende_material, |m| m.r_verdi = tall.unwrap_or(0.0)), "U-value" => { if let Some((_, v)) = gjeldende_vindustype.as_mut() { v.u_verdi = tall; } else if let Some((_, k)) = gjeldende_konstruksjon.as_mut() { k.u_verdi = tall; } } "SolarHeatGainCoeff" => { if let Some((_, v)) = gjeldende_vindustype.as_mut() { v.shgc = tall; } } "Name" => { if let Some((_, v)) = gjeldende_vindustype.as_mut() { v.navn = tekstbuffer.clone(); } else if let Some((_, k)) = gjeldende_konstruksjon.as_mut() { k.navn = tekstbuffer.clone(); } else if let Some(o) = gjeldende_overflate.as_mut() { if !tekstbuffer.is_empty() { o.navn = tekstbuffer.clone(); } } else if sti_elementer.iter().rev().nth(1).map(|s| s.as_str()) == Some("Building") { bygningsnavn = Some(tekstbuffer.clone()); } } "Azimuth" => { if let Some(o) = gjeldende_overflate.as_mut() { o.asimut = tall; } } "Tilt" => { if let Some(o) = gjeldende_overflate.as_mut() { o.helning = tall; } } "Width" => { if let Some(a) = gjeldende_aapning.as_mut() { a.bredde = tall.unwrap_or(0.0); } else if let Some(o) = gjeldende_overflate.as_mut() { o.bredde = tall.unwrap_or(0.0); } } "Height" => { if let Some(a) = gjeldende_aapning.as_mut() { a.hoyde = tall.unwrap_or(0.0); } else if let Some(o) = gjeldende_overflate.as_mut() { o.hoyde = tall.unwrap_or(0.0); } } "Area" => { if let Some(a) = gjeldende_aapning.as_mut() { a.areal = tall; } else if let Some(o) = gjeldende_overflate.as_mut() { o.areal = tall; } else if let Some(r) = gjeldende_rom.as_mut() { r.areal = tall.unwrap_or(0.0); } } "Volume" => { if let Some(r) = gjeldende_rom.as_mut() { r.volum = tall.unwrap_or(0.0); } } "Coordinate" => { if let Some(v) = tall { gjeldende_punkt.push(v); } } "CartesianPoint" => { if i_planar && gjeldende_punkt.len() >= 3 { let p = [gjeldende_punkt[0], gjeldende_punkt[1], gjeldende_punkt[2]]; if i_aapning { if let Some(a) = gjeldende_aapning.as_mut() { a.polygon.push(p); } } else if let Some(o) = gjeldende_overflate.as_mut() { o.polygon.push(p); } } gjeldende_punkt.clear(); } "PlanarGeometry" => i_planar = false, "Material" => { if let Some((id, m)) = gjeldende_material.take() { materialer.insert(id, m); } } "Layer" => { if let Some((id, l)) = gjeldende_lag.take() { lag.insert(id, l); } } "Construction" => { if let Some((id, k)) = gjeldende_konstruksjon.take() { konstruksjoner.insert(id, k); } } "WindowType" => { if let Some((id, v)) = gjeldende_vindustype.take() { vindustyper.insert(id, v); } } "Opening" => { if let (Some(a), Some(o)) = (gjeldende_aapning.take(), gjeldende_overflate.as_mut()) { o.aapninger.push(a); } } "Surface" => { if let Some(o) = gjeldende_overflate.take() { overflater.push(o); } } "Space" => { if let Some(r) = gjeldende_rom.take() { rom.push(r); } } _ => {} } sti_elementer.pop(); tekstbuffer.clear(); } _ => {} } buf.clear(); } bygg( standardnavn, bygningsnavn, bygningstype, rom, overflater, konstruksjoner, lag, materialer, vindustyper, ) } fn sett(mål: &mut Option<(String, T)>, f: impl FnOnce(&mut T)) { if let Some((_, v)) = mål.as_mut() { f(v); } } fn lokalt_navn(b: &[u8]) -> String { let s = String::from_utf8_lossy(b); s.rsplit(':').next().unwrap_or(&s).to_string() } /// Arealet av et plant polygon i rommet. fn polygonareal(p: &[[f64; 3]]) -> f64 { if p.len() < 3 { return 0.0; } let mut n = [0.0f64; 3]; for i in 0..p.len() { let a = p[i]; let b = p[(i + 1) % p.len()]; n[0] += a[1] * b[2] - a[2] * b[1]; n[1] += a[2] * b[0] - a[0] * b[2]; n[2] += a[0] * b[1] - a[1] * b[0]; } 0.5 * (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt() } fn areal_av(areal: Option, bredde: f64, hoyde: f64, polygon: &[[f64; 3]]) -> f64 { if let Some(a) = areal.filter(|a| *a > 0.0) { return a; } if bredde > 0.0 && hoyde > 0.0 { return bredde * hoyde; } polygonareal(polygon) } #[allow(clippy::too_many_arguments)] fn bygg( standardnavn: &str, bygningsnavn: Option, bygningstype: Option, rom: Vec, overflater: Vec, konstruksjoner: HashMap, lag: HashMap>, materialer: HashMap, vindustyper: HashMap, ) -> Result { let mut merknader = Vec::new(); let bra: f64 = rom.iter().map(|r| r.areal).sum(); let volum: f64 = rom.iter().map(|r| r.volum).sum(); if bra <= 0.0 { merknader.push( "gbXML-filen oppgir ikke romareal. Oppvarmet BRA er satt til 0 og må fylles inn." .into(), ); } let u_av_konstruksjon = |id: &str| -> Option { let k = konstruksjoner.get(id)?; if let Some(u) = k.u_verdi.filter(|u| *u > 0.0) { return Some(u); } // Summer lagvis varmemotstand og legg til overgangsmotstander. let mut r = 0.17; let mut fant = false; for lid in &k.lag { for mid in lag.get(lid).into_iter().flatten() { if let Some(m) = materialer.get(mid) { if m.r_verdi > 0.0 { r += m.r_verdi; fant = true; } else if m.ledningsevne > 0.0 && m.tykkelse > 0.0 { r += m.tykkelse / m.ledningsevne; fant = true; } } } } fant.then(|| 1.0 / r) }; let kappa_av_konstruksjon = |id: &str| -> Option { let k = konstruksjoner.get(id)?; let mut kappa = 0.0; for lid in &k.lag { for mid in lag.get(lid).into_iter().flatten() { if let Some(m) = materialer.get(mid) { if m.densitet > 0.0 && m.varmekapasitet > 0.0 && m.tykkelse > 0.0 { kappa += m.tykkelse * m.densitet * m.varmekapasitet; } } } } (kappa > 0.0).then_some(kappa) }; let mut flater: Vec = Vec::new(); let mut vinduer: Vec = Vec::new(); let mut hoppet_over = 0; for o in &overflater { let Some((typ, mot)) = klassifiser(&o.typ) else { hoppet_over += 1; continue; }; let brutto = areal_av(o.areal, o.bredde, o.hoyde, &o.polygon); let mut aapningsareal = 0.0; for a in &o.aapninger { let areal = areal_av(a.areal, a.bredde, a.hoyde, &a.polygon); aapningsareal += areal; let vt = vindustyper.get(&a.vindustype); let u = vt.and_then(|v| v.u_verdi); if u.is_none() { merknader.push(format!( "Åpningen «{}» mangler U-verdi i gbXML; verdien utledes av byggeåret.", a.navn )); } let dor = a.typ.to_lowercase().contains("door"); vinduer.push(Vindu { navn: vt .map(|v| v.navn.clone()) .filter(|n| !n.is_empty()) .unwrap_or_else(|| a.navn.clone()), areal, antall: 1.0, orientering: o.asimut.unwrap_or(0.0), helning: o.helning.unwrap_or(90.0), u_verdi: u, g_verdi: vt.and_then(|v| v.shgc), karmandel: None, solskjerming: None, horisont: None, dor, kommentar: Some(format!("fra gbXML, flate {}", o.navn)), }); } let netto = (brutto - aapningsareal).max(0.0); if netto <= 0.01 { continue; } let u = u_av_konstruksjon(&o.konstruksjon); if u.is_none() { merknader.push(format!( "Flaten «{}» mangler U-verdi og lagoppbygging i gbXML; verdien utledes av byggeår.", o.navn )); } let helning = o.helning.unwrap_or(match typ { FlateType::Vegg => 90.0, FlateType::Tak => 0.0, FlateType::Gulv => 180.0, }); flater.push(Flate { navn: o.navn.clone(), typ, areal: netto, orientering: o.asimut.unwrap_or(0.0), helning: Some(helning), u_verdi: u, lag: None, masseklasse: kappa_av_konstruksjon(&o.konstruksjon).and_then(masseklasse_av_kappa), masseplassering: Masseplassering::Fordelt, mot, b_faktor: None, absorptans: None, omkrets: None, dybde: None, over_terreng: None, sone: None, kommentar: konstruksjoner .get(&o.konstruksjon) .map(|k| k.navn.clone()) .filter(|n| !n.is_empty()), }); } if hoppet_over > 0 { merknader.push(format!( "{hoppet_over} innvendige flater og skyggeflater er utelatt; bare klimaskjermen regnes med." )); } merknader.push( "gbXML beskriver ikke varmekilder, ventilasjon, lekkasjetall eller kommune. \ Fyll inn [ventilasjon], [infiltrasjon], [oppvarming] og [klima] før beregning." .into(), ); let kategori = match bygningstype.as_deref() { Some(t) if t.contains("MultiFamily") || t.contains("Apartment") => Kategori::Boligblokk, Some(t) if t.contains("Office") => Kategori::Kontorbygg, Some(t) if t.contains("School") => Kategori::Skolebygg, _ => Kategori::Smaahus, }; let bygning = Bygning { bygning: BygningInfo { navn: bygningsnavn.unwrap_or_else(|| standardnavn.to_string()), kategori, byggeaar: 2020, oppvarmet_bra: bra, oppvarmet_volum: (volum > 0.0).then_some(volum), antall_boenheter: 1, bygningens_oppvarmede_bra: None, adresse: None, gnr_bnr: None, beskrivelse: Some("Importert fra gbXML".into()), energiraadgiver: None, oppdragsgiver: None, }, klima: Klima::default(), flate: flater, sone: Vec::new(), grunn: Grunn::default(), vindu: vinduer, kuldebroer: Kuldebroer::default(), infiltrasjon: Infiltrasjon::default(), ventilasjon: Ventilasjon::default(), tappevann: Tappevann::default(), oppvarming: Oppvarming::default(), distribusjon: Distribusjon::default(), solcelle: None, kjoling: Kjoling::default(), lufting: Lufting::default(), drift: Drift::default(), scenario: Vec::new(), rapport: Rapport::default(), }; Ok(Importrapport { bygning, merknader }) } fn klassifiser(typ: &str) -> Option<(FlateType, Mot)> { match typ { "ExteriorWall" => Some((FlateType::Vegg, Mot::Ute)), "UndergroundWall" => Some((FlateType::Vegg, Mot::Grunn)), "Roof" => Some((FlateType::Tak, Mot::Ute)), "Ceiling" | "UndergroundCeiling" => Some((FlateType::Tak, Mot::Uoppvarmet)), "SlabOnGrade" | "UndergroundSlab" => Some((FlateType::Gulv, Mot::Grunn)), "RaisedFloor" => Some((FlateType::Gulv, Mot::Ute)), "ExposedFloor" => Some((FlateType::Gulv, Mot::Ute)), _ => None, } } fn masseklasse_av_kappa(kappa: f64) -> Option { Some(match kappa { k if k < 62_500.0 => Masseklasse::SvaertLett, k if k < 92_500.0 => Masseklasse::Lett, k if k < 142_500.0 => Masseklasse::Middels, k if k < 212_500.0 => Masseklasse::Tung, _ => Masseklasse::SvaertTung, }) } #[cfg(test)] mod tester { use super::*; const PROVE: &str = r#" Prøvehus 100.0250.0 Yttervegg sør 18090102.5 2.01.5 003 1003 10103 0103 52.5 Bindingsverk 2000.18 Takkonstruksjon 0.30.037301030 2-lags1.20.5 "#; #[test] fn leser_flater_vinduer_og_arealer() { let r = fra_tekst(PROVE, "prove").unwrap(); let b = r.bygning; assert_eq!(b.bygning.navn, "Prøvehus"); assert_eq!(b.bygning.oppvarmet_bra, 100.0); assert_eq!(b.bygning.oppvarmet_volum, Some(250.0)); // Ytterveggen: 25 m² minus 3 m² vindu. let vegg = b.flate.iter().find(|f| f.navn == "Yttervegg sør").unwrap(); assert!((vegg.areal - 22.0).abs() < 1e-9); assert_eq!(vegg.u_verdi, Some(0.18)); assert_eq!(vegg.orientering, 180.0); // Taket: polygonareal 100 m², U-verdi utledet av lag. let tak = b.flate.iter().find(|f| f.navn == "tak").unwrap(); assert!((tak.areal - 100.0).abs() < 1e-6); let u = tak.u_verdi.unwrap(); assert!((u - 1.0 / (0.17 + 0.3 / 0.037)).abs() < 1e-9, "u = {u}"); // Innvendig vegg skal ikke bli med. assert!(b.flate.iter().all(|f| f.navn != "innervegg")); // Vinduet. assert_eq!(b.vindu.len(), 1); assert_eq!(b.vindu[0].areal, 3.0); assert_eq!(b.vindu[0].u_verdi, Some(1.2)); assert_eq!(b.vindu[0].g_verdi, Some(0.5)); } #[test] fn polygonareal_av_kvadrat() { let p = [ [0.0, 0.0, 0.0], [2.0, 0.0, 0.0], [2.0, 3.0, 0.0], [0.0, 3.0, 0.0], ]; assert!((polygonareal(&p) - 6.0).abs() < 1e-12); } }