Files
enok/src/inndata/gbxml.rs
T

701 lines
26 KiB
Rust
Raw Normal View History

//! 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<f64>,
lag: Vec<String>,
}
#[derive(Default, Debug, Clone)]
struct Vindustype {
navn: String,
u_verdi: Option<f64>,
shgc: Option<f64>,
}
#[derive(Default, Debug, Clone)]
struct Aapning {
navn: String,
typ: String,
vindustype: String,
bredde: f64,
hoyde: f64,
areal: Option<f64>,
polygon: Vec<[f64; 3]>,
}
#[derive(Default, Debug, Clone)]
struct Overflate {
navn: String,
typ: String,
konstruksjon: String,
asimut: Option<f64>,
helning: Option<f64>,
bredde: f64,
hoyde: f64,
areal: Option<f64>,
polygon: Vec<[f64; 3]>,
naborom: Vec<String>,
aapninger: Vec<Aapning>,
}
#[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<String>,
}
pub fn les(sti: &Path) -> Result<Importrapport> {
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<Importrapport> {
let mut leser = Reader::from_str(tekst);
leser.config_mut().trim_text(true);
let mut materialer: HashMap<String, Material> = HashMap::new();
let mut lag: HashMap<String, Vec<String>> = HashMap::new();
let mut konstruksjoner: HashMap<String, Konstruksjon> = HashMap::new();
let mut vindustyper: HashMap<String, Vindustype> = HashMap::new();
let mut overflater: Vec<Overflate> = Vec::new();
let mut rom: Vec<Rom> = Vec::new();
// Gjeldende kontekst under parsingen.
let mut sti_elementer: Vec<String> = Vec::new();
let mut gjeldende_material: Option<(String, Material)> = None;
let mut gjeldende_lag: Option<(String, Vec<String>)> = None;
let mut gjeldende_konstruksjon: Option<(String, Konstruksjon)> = None;
let mut gjeldende_vindustype: Option<(String, Vindustype)> = None;
let mut gjeldende_overflate: Option<Overflate> = None;
let mut gjeldende_aapning: Option<Aapning> = None;
let mut gjeldende_rom: Option<Rom> = None;
let mut gjeldende_punkt: Vec<f64> = Vec::new();
let mut i_planar = false;
let mut bygningsnavn: Option<String> = None;
let mut bygningstype: Option<String> = 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<String> {
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<String> {
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::<f64>().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<T>(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<f64>, 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<String>,
bygningstype: Option<String>,
rom: Vec<Rom>,
overflater: Vec<Overflate>,
konstruksjoner: HashMap<String, Konstruksjon>,
lag: HashMap<String, Vec<String>>,
materialer: HashMap<String, Material>,
vindustyper: HashMap<String, Vindustype>,
) -> Result<Importrapport> {
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<f64> {
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<f64> {
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<Flate> = Vec::new();
let mut vinduer: Vec<Vindu> = 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).unwrap_or_else(|| {
merknader.push(format!(
"Åpningen «{}» mangler U-verdi i gbXML; satt til 2,4 W/(m²K).",
a.navn
));
2.4
});
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,
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,
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(),
};
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<Masseklasse> {
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#"<?xml version="1.0" encoding="UTF-8"?>
<gbXML xmlns="http://www.gbxml.org/schema" lengthUnit="Meters" areaUnit="SquareMeters" temperatureUnit="C">
<Campus id="c1">
<Building id="b1" buildingType="SingleFamily">
<Name>Prøvehus</Name>
<Space id="s1"><Area>100.0</Area><Volume>250.0</Volume></Space>
</Building>
<Surface id="v-sor" surfaceType="ExteriorWall" constructionIdRef="k-vegg">
<Name>Yttervegg sør</Name>
<AdjacentSpaceId spaceIdRef="s1"/>
<RectangularGeometry><Azimuth>180</Azimuth><Tilt>90</Tilt><Width>10</Width><Height>2.5</Height></RectangularGeometry>
<Opening id="vindu-1" openingType="FixedWindow" windowTypeIdRef="w1">
<RectangularGeometry><Width>2.0</Width><Height>1.5</Height></RectangularGeometry>
</Opening>
</Surface>
<Surface id="tak" surfaceType="Roof" constructionIdRef="k-tak">
<AdjacentSpaceId spaceIdRef="s1"/>
<PlanarGeometry><PolyLoop>
<CartesianPoint><Coordinate>0</Coordinate><Coordinate>0</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
<CartesianPoint><Coordinate>10</Coordinate><Coordinate>0</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
<CartesianPoint><Coordinate>10</Coordinate><Coordinate>10</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
<CartesianPoint><Coordinate>0</Coordinate><Coordinate>10</Coordinate><Coordinate>3</Coordinate></CartesianPoint>
</PolyLoop></PlanarGeometry>
</Surface>
<Surface id="innervegg" surfaceType="InteriorWall" constructionIdRef="k-vegg">
<RectangularGeometry><Width>5</Width><Height>2.5</Height></RectangularGeometry>
</Surface>
</Campus>
<Construction id="k-vegg"><Name>Bindingsverk 200</Name><U-value unit="WPerSquareMeterK">0.18</U-value></Construction>
<Construction id="k-tak">
<Name>Takkonstruksjon</Name>
<LayerId layerIdRef="l1"/>
</Construction>
<Layer id="l1"><MaterialId materialIdRef="m1"/></Layer>
<Material id="m1"><Thickness>0.3</Thickness><Conductivity>0.037</Conductivity><Density>30</Density><SpecificHeat>1030</SpecificHeat></Material>
<WindowType id="w1"><Name>2-lags</Name><U-value unit="WPerSquareMeterK">1.2</U-value><SolarHeatGainCoeff>0.5</SolarHeatGainCoeff></WindowType>
</gbXML>"#;
#[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, 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);
}
}