Energiberegning etter NS 3031:2025 med timesmetode og energirapport
enok leser en bygningsbeskrivelse i TOML, henter typisk klimaår for kommunen, simulerer 8760 timer etter NS-EN ISO 52016-1 og skriver energirapport, CSV og JSON. Motor - Femnodemodell for opake bygningsdeler, tonodemodell for vinduer, luftnode og strålingsnode per sone, implisitt løsning per time med egen tett LU-løser. - Varme- og kjølebehov ved eksakt interpolasjon mellom to løsninger, siden systemet er lineært. Vinduslufting for bygninger uten kjøleanlegg. - Solberegning etter ISO 52010-1, kontrollert mot pvlib med avvik under 1 prosent. Beregningsgang - Beregningspunktene A til E: netto, brutto, tilført, levert per energibærer og vektet levert energi, med solceller, klimakorrigering og effektbehov. - Energikarakter etter Enovas skala fra 1.1.2026, med arealkorreksjon. Inndata - TOML med scenarier for tiltak, og import fra IFC, gbXML og regneark. - Normerte verdier, karakterskala, vektingsfaktorer og klimadata i data/, som kan overstyres med --data. Kontroll - Stasjonært varmetap mot U·A·ΔT innenfor 2 prosent, energibalansen innenfor 2 prosent, varmetapsbudsjettet for TEK17-referansebygget identisk med publisert tabell. Kjent avvik i romoppvarming mot Simien 8 er dokumentert i README. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -0,0 +1,700 @@
|
||||
//! 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,751 @@
|
||||
//! Import fra IFC (IFC2x3 og IFC4), som er formatet Vectorworks eksporterer med
|
||||
//! «Export IFC Project».
|
||||
//!
|
||||
//! Importen henter klimaskjermen: yttervegger, tak, gulv, vinduer og dører, med areal,
|
||||
//! U-verdi og himmelretning. Areal og U-verdi hentes fra mengder (`IfcElementQuantity`) og
|
||||
//! egenskapssett (`Pset_WallCommon.ThermalTransmittance` m.fl.) når modellen har dem, ellers
|
||||
//! av geometriske attributter. Himmelretningen utledes av elementets plassering: veggens
|
||||
//! lokale x-akse gir planet, og ytre normal velges som den som peker bort fra bygningens
|
||||
//! tyngdepunkt. Prosjektets sanne nord (`IfcGeometricRepresentationContext.TrueNorth`)
|
||||
//! legges til.
|
||||
|
||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
pub struct Importrapport {
|
||||
pub bygning: Bygning,
|
||||
pub merknader: Vec<String>,
|
||||
}
|
||||
|
||||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let tekst = String::from_utf8_lossy(&raa);
|
||||
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 fil = step::les(tekst);
|
||||
let mut merknader = Vec::new();
|
||||
|
||||
let lengdeskala = lengdeenhet(&fil);
|
||||
let sant_nord = sant_nord(&fil);
|
||||
let egenskaper = samle_egenskaper(&fil);
|
||||
let mengder = samle_mengder(&fil);
|
||||
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
||||
|
||||
// Areal og volum fra rom.
|
||||
let mut bra = 0.0;
|
||||
let mut volum = 0.0;
|
||||
for (r, _) in fil.av_type("IFCSPACE") {
|
||||
if let Some(m) = mengder.get(&r) {
|
||||
bra += m
|
||||
.get("NetFloorArea")
|
||||
.or_else(|| m.get("GrossFloorArea"))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
volum += m
|
||||
.get("NetVolume")
|
||||
.or_else(|| m.get("GrossVolume"))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Plasseringer for alle bygningsdeler.
|
||||
let mut plassering: HashMap<u64, (f64, f64, f64, f64, f64)> = HashMap::new();
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER).chain(fil.av_typer(&PLATETYPER)) {
|
||||
if let Some(p) = e.arg(5).and_then(|a| a.som_ref()) {
|
||||
if let Some(v) = global_plassering(&fil, p, lengdeskala) {
|
||||
plassering.insert(r, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Tyngdepunkt av ytterveggene, for å bestemme hvilken vei normalen peker.
|
||||
let ytre: Vec<u64> = fil
|
||||
.av_typer(&VEGGTYPER)
|
||||
.filter(|(r, _)| er_ytre(&egenskaper, *r).unwrap_or(true))
|
||||
.map(|(r, _)| r)
|
||||
.collect();
|
||||
let (mut cx, mut cy, mut n) = (0.0, 0.0, 0.0);
|
||||
for r in &ytre {
|
||||
if let Some((x, y, _, _, _)) = plassering.get(r) {
|
||||
cx += x;
|
||||
cy += y;
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
if n > 0.0 {
|
||||
cx /= n;
|
||||
cy /= n;
|
||||
}
|
||||
|
||||
let mut flater: Vec<Flate> = Vec::new();
|
||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||||
let mut uten_uverdi = 0usize;
|
||||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||||
|
||||
// ── vegger ──────────────────────────────────────────────────────
|
||||
for (r, e) in fil.av_typer(&VEGGTYPER) {
|
||||
let ytre_vegg = er_ytre(&egenskaper, r).unwrap_or(true);
|
||||
let asimut = plassering
|
||||
.get(&r)
|
||||
.map(|(x, y, _, dx, dy)| asimut_av(*x, *y, *dx, *dy, cx, cy, sant_nord))
|
||||
.unwrap_or(0.0);
|
||||
asimut_for_vegg.insert(r, asimut);
|
||||
if !ytre_vegg {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let brutto = m
|
||||
.and_then(|m| {
|
||||
m.get("NetSideArea")
|
||||
.or_else(|| m.get("GrossSideArea"))
|
||||
.copied()
|
||||
})
|
||||
.unwrap_or(0.0);
|
||||
if brutto <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, "Vegg"),
|
||||
typ: FlateType::Vegg,
|
||||
areal: brutto,
|
||||
orientering: asimut,
|
||||
helning: Some(90.0),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot: Mot::Ute,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── plater: tak, gulv, dekke mot grunn ──────────────────────────
|
||||
for (r, e) in fil.av_typer(&PLATETYPER) {
|
||||
if !er_ytre(&egenskaper, r).unwrap_or(true) {
|
||||
continue;
|
||||
}
|
||||
let m = mengder.get(&r);
|
||||
let a = m
|
||||
.and_then(|m| m.get("NetArea").or_else(|| m.get("GrossArea")).copied())
|
||||
.unwrap_or(0.0);
|
||||
if a <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let type_tekst = e
|
||||
.args
|
||||
.iter()
|
||||
.filter_map(|x| x.som_tekst())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
.to_uppercase();
|
||||
// Plater uten ROOF-merking regnes som dekke mot grunn; kontroller `mot` etter importen
|
||||
// dersom modellen har dekker mot det fri eller mot uoppvarmet kjeller.
|
||||
let (typ, mot, helning) = if e.typ == "IFCROOF" || type_tekst.contains("ROOF") {
|
||||
(FlateType::Tak, Mot::Ute, 0.0)
|
||||
} else {
|
||||
(FlateType::Gulv, Mot::Grunn, 180.0)
|
||||
};
|
||||
let u = u_verdi(&egenskaper, r);
|
||||
if u.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
navn: navn_av(e, r, if typ == FlateType::Tak { "Tak" } else { "Gulv" }),
|
||||
typ,
|
||||
areal: a,
|
||||
orientering: 0.0,
|
||||
helning: Some(helning),
|
||||
u_verdi: u,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// ── vinduer og dører ────────────────────────────────────────────
|
||||
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||
let dor = e.typ == "IFCDOOR";
|
||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
||||
let areal = mengder
|
||||
.get(&r)
|
||||
.and_then(|m| m.get("Area").or_else(|| m.get("GrossArea")).copied())
|
||||
.filter(|a| *a > 0.01)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
if areal <= 0.01 {
|
||||
continue;
|
||||
}
|
||||
let vert = if dor {
|
||||
dor_vert.get(&r)
|
||||
} else {
|
||||
vindu_vert.get(&r)
|
||||
};
|
||||
let asimut = vert
|
||||
.and_then(|v| asimut_for_vegg.get(v))
|
||||
.copied()
|
||||
.unwrap_or(0.0);
|
||||
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| {
|
||||
uten_uverdi += 1;
|
||||
if dor {
|
||||
2.0
|
||||
} else {
|
||||
2.4
|
||||
}
|
||||
});
|
||||
vinduer.push(Vindu {
|
||||
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
|
||||
areal,
|
||||
antall: 1.0,
|
||||
orientering: asimut,
|
||||
helning: 90.0,
|
||||
u_verdi: u,
|
||||
g_verdi: egenskap(&egenskaper, r, "SolarHeatGainTransmittance")
|
||||
.or_else(|| egenskap(&egenskaper, r, "SolarTransmittance")),
|
||||
karmandel: egenskap(&egenskaper, r, "GlazingAreaFraction").map(|g| 1.0 - g),
|
||||
solskjerming: None,
|
||||
horisont: None,
|
||||
dor,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
});
|
||||
}
|
||||
|
||||
// Trekk vindus- og dørareal fra veggene de sitter i.
|
||||
for (v, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let areal = vinduer
|
||||
.iter()
|
||||
.find(|x| x.kommentar.is_some() && x.navn.ends_with(&format!("#{v}")))
|
||||
.map(|x| x.areal);
|
||||
let _ = (areal, vegg);
|
||||
}
|
||||
let apningsareal_per_vegg = {
|
||||
let mut m: HashMap<u64, f64> = HashMap::new();
|
||||
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||||
let e = fil.hent(*a);
|
||||
let hoyde = e
|
||||
.and_then(|e| e.arg(8))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let bredde = e
|
||||
.and_then(|e| e.arg(9))
|
||||
.and_then(|x| x.som_tall())
|
||||
.unwrap_or(0.0)
|
||||
* lengdeskala;
|
||||
let areal = mengder
|
||||
.get(a)
|
||||
.and_then(|q| q.get("Area").or_else(|| q.get("GrossArea")).copied())
|
||||
.filter(|v| *v > 0.01)
|
||||
.unwrap_or(hoyde * bredde);
|
||||
*m.entry(*vegg).or_default() += areal;
|
||||
}
|
||||
m
|
||||
};
|
||||
for (r, _) in fil.av_typer(&VEGGTYPER) {
|
||||
if let Some(ap) = apningsareal_per_vegg.get(&r) {
|
||||
let navn = fil.hent(r).map(|e| navn_av(e, r, "Vegg"));
|
||||
if let Some(f) = navn.and_then(|n| flater.iter_mut().find(|f| f.navn == n)) {
|
||||
f.areal = (f.areal - ap).max(0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
flater.retain(|f| f.areal > 0.01);
|
||||
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum::<f64>();
|
||||
if bra > 0.0 {
|
||||
merknader.push(format!(
|
||||
"IFC-modellen har ingen romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvarealet. Kontroller verdien."
|
||||
));
|
||||
} else {
|
||||
merknader.push(
|
||||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
if uten_uverdi > 0 {
|
||||
merknader.push(format!(
|
||||
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok \
|
||||
bruke typiske verdier for byggeåret."
|
||||
));
|
||||
}
|
||||
if n == 0.0 {
|
||||
merknader.push(
|
||||
"Fant ingen plasseringer for ytterveggene; alle himmelretninger er satt til nord."
|
||||
.into(),
|
||||
);
|
||||
} else {
|
||||
merknader.push(
|
||||
"Himmelretningene er utledet av elementenes plassering og bygningens tyngdepunkt. Kontroller dem mot tegningen."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
merknader.push(
|
||||
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], \
|
||||
[infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
let navn = fil
|
||||
.av_type("IFCBUILDING")
|
||||
.next()
|
||||
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| standardnavn.to_string());
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
kategori: Kategori::Smaahus,
|
||||
byggeaar: 2020,
|
||||
oppvarmet_bra: bra,
|
||||
oppvarmet_volum: (volum > 0.0).then_some(volum),
|
||||
antall_boenheter: 1,
|
||||
bygningens_oppvarmede_bra: None,
|
||||
adresse: adresse(&fil),
|
||||
gnr_bnr: None,
|
||||
beskrivelse: Some("Importert fra IFC".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 navn_av(e: &Entitet, r: u64, standard: &str) -> String {
|
||||
match e.navn() {
|
||||
Some(n) if !n.trim().is_empty() => format!("{n} #{r}"),
|
||||
_ => format!("{standard} #{r}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn adresse(fil: &Fil) -> Option<String> {
|
||||
let (_, e) = fil.av_type("IFCPOSTALADDRESS").next()?;
|
||||
let linjer: Vec<String> = e
|
||||
.arg(4)?
|
||||
.som_liste()
|
||||
.iter()
|
||||
.filter_map(|a| a.som_tekst().map(|s| s.to_string()))
|
||||
.collect();
|
||||
let by = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("");
|
||||
let mut ut = linjer.join(", ");
|
||||
if !by.is_empty() {
|
||||
if !ut.is_empty() {
|
||||
ut.push_str(", ");
|
||||
}
|
||||
ut.push_str(by);
|
||||
}
|
||||
(!ut.is_empty()).then_some(ut)
|
||||
}
|
||||
|
||||
/// Faktor som gjør lengder om til meter.
|
||||
fn lengdeenhet(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCSIUNIT") {
|
||||
let er_lengde = e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT");
|
||||
if !er_lengde {
|
||||
continue;
|
||||
}
|
||||
let prefiks = e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("");
|
||||
return match prefiks {
|
||||
"MILLI" => 0.001,
|
||||
"CENTI" => 0.01,
|
||||
"DECI" => 0.1,
|
||||
"KILO" => 1000.0,
|
||||
_ => 1.0,
|
||||
};
|
||||
}
|
||||
1.0
|
||||
}
|
||||
|
||||
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
||||
fn sant_nord(fil: &Fil) -> f64 {
|
||||
for (_, e) in fil.av_type("IFCGEOMETRICREPRESENTATIONCONTEXT") {
|
||||
if let Some(r) = e.arg(5).and_then(|a| a.som_ref()) {
|
||||
if let Some(d) = fil.hent(r) {
|
||||
let v = d.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
if v.len() >= 2 {
|
||||
let (x, y) = (
|
||||
v[0].som_tall().unwrap_or(0.0),
|
||||
v[1].som_tall().unwrap_or(1.0),
|
||||
);
|
||||
// TrueNorth peker mot nord i prosjektets koordinater.
|
||||
return x.atan2(y).to_degrees();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
0.0
|
||||
}
|
||||
|
||||
/// Sammensatt global plassering: (x, y, z, dx, dy) der (dx, dy) er lokal x-akse i planet.
|
||||
fn global_plassering(fil: &Fil, r: u64, skala: f64) -> Option<(f64, f64, f64, f64, f64)> {
|
||||
let mut x = 0.0;
|
||||
let mut y = 0.0;
|
||||
let mut z = 0.0;
|
||||
let mut dx = 1.0;
|
||||
let mut dy = 0.0;
|
||||
let mut kjede = Vec::new();
|
||||
let mut gjeldende = Some(r);
|
||||
let mut vakt = 0;
|
||||
while let Some(p) = gjeldende {
|
||||
vakt += 1;
|
||||
if vakt > 64 {
|
||||
break;
|
||||
}
|
||||
let e = fil.hent(p)?;
|
||||
if e.typ != "IFCLOCALPLACEMENT" {
|
||||
break;
|
||||
}
|
||||
kjede.push(p);
|
||||
gjeldende = e.arg(0).and_then(|a| a.som_ref());
|
||||
}
|
||||
for p in kjede.into_iter().rev() {
|
||||
let e = fil.hent(p)?;
|
||||
let ax = e
|
||||
.arg(1)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|a| fil.hent(a))?;
|
||||
let (lx, ly, lz) = punkt(fil, ax.arg(0).and_then(|a| a.som_ref()), skala);
|
||||
let (rx, ry) = retning(fil, ax.arg(2).and_then(|a| a.som_ref())).unwrap_or((1.0, 0.0));
|
||||
// Roter det lokale punktet med foreldrenes akse og legg det til.
|
||||
let nx = x + lx * dx - ly * dy;
|
||||
let ny = y + lx * dy + ly * dx;
|
||||
x = nx;
|
||||
y = ny;
|
||||
z += lz;
|
||||
let ndx = dx * rx - dy * ry;
|
||||
let ndy = dy * rx + dx * ry;
|
||||
let lengde = (ndx * ndx + ndy * ndy).sqrt().max(1e-12);
|
||||
dx = ndx / lengde;
|
||||
dy = ndy / lengde;
|
||||
}
|
||||
Some((x, y, z, dx, dy))
|
||||
}
|
||||
|
||||
fn punkt(fil: &Fil, r: Option<u64>, skala: f64) -> (f64, f64, f64) {
|
||||
let Some(e) = r.and_then(|r| fil.hent(r)) else {
|
||||
return (0.0, 0.0, 0.0);
|
||||
};
|
||||
let v = e.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let g = |i: usize| v.get(i).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||
(g(0), g(1), g(2))
|
||||
}
|
||||
|
||||
fn retning(fil: &Fil, r: Option<u64>) -> Option<(f64, f64)> {
|
||||
let e = r.and_then(|r| fil.hent(r))?;
|
||||
let v = e.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
if v.len() < 2 {
|
||||
return None;
|
||||
}
|
||||
Some((
|
||||
v[0].som_tall().unwrap_or(1.0),
|
||||
v[1].som_tall().unwrap_or(0.0),
|
||||
))
|
||||
}
|
||||
|
||||
/// Himmelretning for en vegg: normalen står vinkelrett på veggens akse, og velges slik at
|
||||
/// den peker bort fra bygningens tyngdepunkt.
|
||||
fn asimut_av(x: f64, y: f64, dx: f64, dy: f64, cx: f64, cy: f64, sant_nord: f64) -> f64 {
|
||||
let kandidater = [(dy, -dx), (-dy, dx)];
|
||||
let ut = (x - cx, y - cy);
|
||||
let valgt = if kandidater[0].0 * ut.0 + kandidater[0].1 * ut.1
|
||||
>= kandidater[1].0 * ut.0 + kandidater[1].1 * ut.1
|
||||
{
|
||||
kandidater[0]
|
||||
} else {
|
||||
kandidater[1]
|
||||
};
|
||||
// Asimut fra nord med klokka; prosjektets y-akse peker mot nord etter TrueNorth-dreining.
|
||||
(valgt.0.atan2(valgt.1).to_degrees() + sant_nord).rem_euclid(360.0)
|
||||
}
|
||||
|
||||
type Egenskaper = HashMap<u64, HashMap<String, Arg>>;
|
||||
|
||||
/// Samler alle egenskaper fra `IfcRelDefinesByProperties` per objekt.
|
||||
fn samle_egenskaper(fil: &Fil) -> Egenskaper {
|
||||
let mut ut: Egenskaper = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELDEFINESBYPROPERTIES") {
|
||||
let objekter = rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let Some(def) = rel
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|r| fil.hent(r))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if def.typ != "IFCPROPERTYSET" {
|
||||
continue;
|
||||
}
|
||||
let mut verdier = HashMap::new();
|
||||
for p in def.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(pe) = p.som_ref().and_then(|r| fil.hent(r)) else {
|
||||
continue;
|
||||
};
|
||||
if pe.typ != "IFCPROPERTYSINGLEVALUE" {
|
||||
continue;
|
||||
}
|
||||
let (Some(navn), Some(v)) = (pe.arg(0).and_then(|a| a.som_tekst()), pe.arg(2)) else {
|
||||
continue;
|
||||
};
|
||||
verdier.insert(navn.to_string(), v.clone());
|
||||
}
|
||||
for o in objekter {
|
||||
if let Some(r) = o.som_ref() {
|
||||
ut.entry(r).or_default().extend(verdier.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
// Typeobjektenes egenskaper arves av forekomstene.
|
||||
let mut arv: Vec<(u64, HashMap<String, Arg>)> = Vec::new();
|
||||
for (_, rel) in fil.av_type("IFCRELDEFINESBYTYPE") {
|
||||
let Some(typ) = rel.arg(5).and_then(|a| a.som_ref()) else {
|
||||
continue;
|
||||
};
|
||||
let Some(verdier) = ut.get(&typ).cloned() else {
|
||||
continue;
|
||||
};
|
||||
for o in rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
if let Some(r) = o.som_ref() {
|
||||
arv.push((r, verdier.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (r, verdier) in arv {
|
||||
let e = ut.entry(r).or_default();
|
||||
for (k, v) in verdier {
|
||||
e.entry(k).or_insert(v);
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
/// Samler alle mengder fra `IfcElementQuantity` per objekt.
|
||||
fn samle_mengder(fil: &Fil) -> HashMap<u64, HashMap<String, f64>> {
|
||||
let mut ut: HashMap<u64, HashMap<String, f64>> = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELDEFINESBYPROPERTIES") {
|
||||
let objekter = rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]);
|
||||
let Some(def) = rel
|
||||
.arg(5)
|
||||
.and_then(|a| a.som_ref())
|
||||
.and_then(|r| fil.hent(r))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if def.typ != "IFCELEMENTQUANTITY" {
|
||||
continue;
|
||||
}
|
||||
let mut verdier = HashMap::new();
|
||||
for q in def.arg(5).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||||
let Some(qe) = q.som_ref().and_then(|r| fil.hent(r)) else {
|
||||
continue;
|
||||
};
|
||||
let Some(navn) = qe.arg(0).and_then(|a| a.som_tekst()) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(v) = qe.arg(3).and_then(|a| a.som_tall()) {
|
||||
verdier.insert(navn.to_string(), v);
|
||||
}
|
||||
}
|
||||
for o in objekter {
|
||||
if let Some(r) = o.som_ref() {
|
||||
ut.entry(r).or_default().extend(verdier.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
/// Kobler vinduer og dører til veggen de sitter i, via åpningene.
|
||||
fn samle_apninger(fil: &Fil) -> (HashMap<u64, u64>, HashMap<u64, u64>) {
|
||||
// åpning → vertselement
|
||||
let mut vert_for_apning: HashMap<u64, u64> = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELVOIDSELEMENT") {
|
||||
if let (Some(vert), Some(apning)) = (
|
||||
rel.arg(4).and_then(|a| a.som_ref()),
|
||||
rel.arg(5).and_then(|a| a.som_ref()),
|
||||
) {
|
||||
vert_for_apning.insert(apning, vert);
|
||||
}
|
||||
}
|
||||
let mut vindu = HashMap::new();
|
||||
let mut dor = HashMap::new();
|
||||
for (_, rel) in fil.av_type("IFCRELFILLSELEMENT") {
|
||||
let (Some(apning), Some(element)) = (
|
||||
rel.arg(4).and_then(|a| a.som_ref()),
|
||||
rel.arg(5).and_then(|a| a.som_ref()),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
let Some(vert) = vert_for_apning.get(&apning) else {
|
||||
continue;
|
||||
};
|
||||
match fil.hent(element).map(|e| e.typ.as_str()) {
|
||||
Some("IFCDOOR") => {
|
||||
dor.insert(element, *vert);
|
||||
}
|
||||
Some("IFCWINDOW") => {
|
||||
vindu.insert(element, *vert);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(vindu, dor)
|
||||
}
|
||||
|
||||
fn egenskap(e: &Egenskaper, r: u64, navn: &str) -> Option<f64> {
|
||||
e.get(&r)?.get(navn)?.som_tall()
|
||||
}
|
||||
|
||||
fn u_verdi(e: &Egenskaper, r: u64) -> Option<f64> {
|
||||
[
|
||||
"ThermalTransmittance",
|
||||
"UValue",
|
||||
"U-Value",
|
||||
"Varmegjennomgangskoeffisient",
|
||||
]
|
||||
.iter()
|
||||
.find_map(|n| egenskap(e, r, n))
|
||||
.filter(|u| *u > 0.0 && *u < 20.0)
|
||||
}
|
||||
|
||||
fn er_ytre(e: &Egenskaper, r: u64) -> Option<bool> {
|
||||
let v = e.get(&r)?.get("IsExternal")?;
|
||||
match v {
|
||||
Arg::Enum(s) => Some(s == "T" || s.eq_ignore_ascii_case("true")),
|
||||
Arg::Tall(n) => Some(*n != 0.0),
|
||||
Arg::Tekst(s) => Some(s == "T" || s.eq_ignore_ascii_case("true")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const PROVE: &str = r#"ISO-10303-21;
|
||||
HEADER;ENDSEC;
|
||||
DATA;
|
||||
#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
||||
#2= IFCDIRECTION((0.,1.));
|
||||
#3= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#2);
|
||||
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);
|
||||
#20= IFCLOCALPLACEMENT($,#21);
|
||||
#21= IFCAXIS2PLACEMENT3D(#22,$,$);
|
||||
#22= IFCCARTESIANPOINT((0.,0.,0.));
|
||||
#100= IFCWALLSTANDARDCASE('w1',$,'Sorvegg',$,$,#110,$,$);
|
||||
#110= IFCLOCALPLACEMENT(#20,#111);
|
||||
#111= IFCAXIS2PLACEMENT3D(#112,$,#113);
|
||||
#112= IFCCARTESIANPOINT((0.,-5.,0.));
|
||||
#113= IFCDIRECTION((1.,0.,0.));
|
||||
#120= IFCWALLSTANDARDCASE('w2',$,'Nordvegg',$,$,#130,$,$);
|
||||
#130= IFCLOCALPLACEMENT(#20,#131);
|
||||
#131= IFCAXIS2PLACEMENT3D(#132,$,#133);
|
||||
#132= IFCCARTESIANPOINT((0.,5.,0.));
|
||||
#133= IFCDIRECTION((1.,0.,0.));
|
||||
#200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);
|
||||
#201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
|
||||
#202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));
|
||||
#203= IFCRELDEFINESBYPROPERTIES('r',$,$,$,(#100,#120),#202);
|
||||
#210= IFCQUANTITYAREA('NetSideArea',$,$,25.0,$);
|
||||
#211= IFCELEMENTQUANTITY('q',$,'BaseQuantities',$,$,(#210));
|
||||
#212= IFCRELDEFINESBYPROPERTIES('r2',$,$,$,(#100,#120),#211);
|
||||
#300= IFCSPACE('s',$,'Stue',$,$,#20,$,$,.ELEMENT.,.INTERNAL.,$);
|
||||
#310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);
|
||||
#311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);
|
||||
#312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));
|
||||
#313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);
|
||||
ENDSEC;
|
||||
END-ISO-10303-21;"#;
|
||||
|
||||
#[test]
|
||||
fn leser_vegger_areal_uverdi_og_himmelretning() {
|
||||
let r = fra_tekst(PROVE, "prove").unwrap();
|
||||
let b = &r.bygning;
|
||||
assert_eq!(b.bygning.navn, "Testhus");
|
||||
assert_eq!(b.bygning.oppvarmet_bra, 100.0);
|
||||
assert_eq!(b.bygning.oppvarmet_volum, Some(250.0));
|
||||
assert_eq!(b.flate.len(), 2);
|
||||
for f in &b.flate {
|
||||
assert_eq!(f.areal, 25.0);
|
||||
assert_eq!(f.u_verdi, Some(0.18));
|
||||
}
|
||||
// Veggen ved y = −5 vender sør (180°), den ved y = +5 vender nord (0°).
|
||||
let sor = b
|
||||
.flate
|
||||
.iter()
|
||||
.find(|f| f.navn.starts_with("Sorvegg"))
|
||||
.unwrap();
|
||||
let nord = b
|
||||
.flate
|
||||
.iter()
|
||||
.find(|f| f.navn.starts_with("Nordvegg"))
|
||||
.unwrap();
|
||||
assert!(
|
||||
(sor.orientering - 180.0).abs() < 1.0,
|
||||
"sør = {}",
|
||||
sor.orientering
|
||||
);
|
||||
assert!(
|
||||
nord.orientering < 1.0 || nord.orientering > 359.0,
|
||||
"nord = {}",
|
||||
nord.orientering
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn millimeterenhet_skaleres() {
|
||||
let t = PROVE.replace(
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);",
|
||||
"#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);",
|
||||
);
|
||||
let r = fra_tekst(&t, "prove").unwrap();
|
||||
// Arealene kommer fra mengder og er alltid i m², så de endres ikke.
|
||||
assert_eq!(r.bygning.flate[0].areal, 25.0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Innlesing av bygningsbeskrivelser: `bygning.toml` og gbXML fra BIM-verktøy.
|
||||
|
||||
pub mod gbxml;
|
||||
pub mod ifc;
|
||||
pub mod regneark;
|
||||
pub mod step;
|
||||
pub mod toml_inn;
|
||||
|
||||
pub use toml_inn::{les_bygning, slaa_sammen_scenario};
|
||||
@@ -0,0 +1,244 @@
|
||||
//! Import fra regneark (CSV), for eksempel en Vectorworks-arbeidstegning eksportert med
|
||||
//! «Export Worksheet». Formatet er bevisst enkelt, slik at det kan settes opp som en
|
||||
//! rapport i BIM-verktøyet og eksporteres direkte.
|
||||
//!
|
||||
//! Hver rad begynner med en type:
|
||||
//!
|
||||
//! ```text
|
||||
//! type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall
|
||||
//! bygning,Nordhusstykket 5,140,,,,smaahus,1948,
|
||||
//! vegg,Yttervegg sør,32,1.0,180,90,ute,,
|
||||
//! vindu,Stue sør,4.2,2.8,180,90,,0.75,2
|
||||
//! ```
|
||||
//!
|
||||
//! For `bygning`-raden brukes kolonnene: navn, oppvarmet BRA, (tom), (tom), (tom),
|
||||
//! kategori, byggeår.
|
||||
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
use anyhow::{bail, Context};
|
||||
use std::path::Path;
|
||||
|
||||
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"),
|
||||
)
|
||||
}
|
||||
|
||||
fn skilletegn(tekst: &str) -> u8 {
|
||||
let forste = tekst.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
|
||||
if forste.matches(';').count() > forste.matches(',').count() {
|
||||
b';'
|
||||
} else {
|
||||
b','
|
||||
}
|
||||
}
|
||||
|
||||
fn tall(s: Option<&str>) -> Option<f64> {
|
||||
let s = s?.trim().replace(',', ".");
|
||||
if s.is_empty() {
|
||||
return None;
|
||||
}
|
||||
s.parse().ok()
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let mut leser = csv::ReaderBuilder::new()
|
||||
.delimiter(skilletegn(tekst))
|
||||
.flexible(true)
|
||||
.has_headers(true)
|
||||
.from_reader(tekst.as_bytes());
|
||||
|
||||
let mut merknader = Vec::new();
|
||||
let mut flater = Vec::new();
|
||||
let mut vinduer = Vec::new();
|
||||
let mut navn = standardnavn.to_string();
|
||||
let mut kategori = Kategori::Smaahus;
|
||||
let mut byggeaar = 2020;
|
||||
let mut bra = 0.0;
|
||||
|
||||
for (nr, rad) in leser.records().enumerate() {
|
||||
let rad = rad.with_context(|| format!("rad {}", nr + 2))?;
|
||||
let felt = |i: usize| rad.get(i).map(|s| s.trim()).filter(|s| !s.is_empty());
|
||||
let Some(typ) = felt(0).map(|s| s.to_lowercase()) else {
|
||||
continue;
|
||||
};
|
||||
let radnavn = felt(1).unwrap_or("uten navn").to_string();
|
||||
match typ.as_str() {
|
||||
"bygning" => {
|
||||
navn = radnavn;
|
||||
bra = tall(felt(2)).unwrap_or(0.0);
|
||||
if let Some(k) = felt(6) {
|
||||
kategori = match k.to_lowercase().as_str() {
|
||||
"smaahus" | "småhus" | "enebolig" => Kategori::Smaahus,
|
||||
"boligblokk" | "leilighet" => Kategori::Boligblokk,
|
||||
"kontorbygg" | "kontor" => Kategori::Kontorbygg,
|
||||
"skolebygg" | "skole" => Kategori::Skolebygg,
|
||||
"barnehage" => Kategori::Barnehage,
|
||||
annen => {
|
||||
merknader
|
||||
.push(format!("Ukjent bygningskategori «{annen}»; bruker småhus."));
|
||||
Kategori::Smaahus
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(a) = tall(felt(7)) {
|
||||
byggeaar = a as i32;
|
||||
}
|
||||
}
|
||||
"vegg" | "tak" | "gulv" => {
|
||||
let ft = match typ.as_str() {
|
||||
"vegg" => FlateType::Vegg,
|
||||
"tak" => FlateType::Tak,
|
||||
_ => FlateType::Gulv,
|
||||
};
|
||||
let mot = match felt(6).map(|s| s.to_lowercase()).as_deref() {
|
||||
Some("grunn") | Some("grunnen") => Mot::Grunn,
|
||||
Some("uoppvarmet") | Some("kjeller") | Some("loft") => Mot::Uoppvarmet,
|
||||
_ => Mot::Ute,
|
||||
};
|
||||
let Some(areal) = tall(felt(2)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler areal og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
flater.push(Flate {
|
||||
navn: radnavn,
|
||||
typ: ft,
|
||||
areal,
|
||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||
helning: tall(felt(5)),
|
||||
u_verdi: tall(felt(3)),
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
"vindu" | "dør" | "dor" => {
|
||||
let Some(areal) = tall(felt(2)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler areal og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
let Some(u) = tall(felt(3)) else {
|
||||
merknader.push(format!(
|
||||
"Raden «{radnavn}» mangler U-verdi og er hoppet over."
|
||||
));
|
||||
continue;
|
||||
};
|
||||
vinduer.push(Vindu {
|
||||
navn: radnavn,
|
||||
areal,
|
||||
antall: tall(felt(8)).unwrap_or(1.0),
|
||||
orientering: tall(felt(4)).unwrap_or(0.0),
|
||||
helning: tall(felt(5)).unwrap_or(90.0),
|
||||
u_verdi: u,
|
||||
g_verdi: tall(felt(7)),
|
||||
karmandel: None,
|
||||
solskjerming: None,
|
||||
horisont: None,
|
||||
dor: typ != "vindu",
|
||||
kommentar: None,
|
||||
});
|
||||
}
|
||||
annen => merknader.push(format!("Ukjent radtype «{annen}» på rad {}.", nr + 2)),
|
||||
}
|
||||
}
|
||||
|
||||
if flater.is_empty() {
|
||||
bail!("regnearket inneholder ingen flater (rader som begynner med vegg, tak eller gulv)");
|
||||
}
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
.iter()
|
||||
.filter(|f| f.typ == FlateType::Gulv)
|
||||
.map(|f| f.areal)
|
||||
.sum();
|
||||
merknader.push(format!(
|
||||
"Regnearket mangler en bygning-rad med oppvarmet BRA; satt til {bra:.0} m² av gulvarealet."
|
||||
));
|
||||
}
|
||||
merknader.push(
|
||||
"Fyll inn [ventilasjon], [infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
||||
.into(),
|
||||
);
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
kategori,
|
||||
byggeaar,
|
||||
oppvarmet_bra: bra,
|
||||
oppvarmet_volum: None,
|
||||
antall_boenheter: 1,
|
||||
bygningens_oppvarmede_bra: None,
|
||||
adresse: None,
|
||||
gnr_bnr: None,
|
||||
beskrivelse: Some("Importert fra regneark".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 })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn leser_enkelt_regneark() {
|
||||
let csv = "type,navn,areal,u_verdi,orientering,helning,mot,g_verdi,antall\n\
|
||||
bygning,Testhus,140,,,,smaahus,1948,\n\
|
||||
vegg,Yttervegg sør,32,1.0,180,90,ute,,\n\
|
||||
gulv,Kjellergulv,70,0.8,,180,grunn,,\n\
|
||||
vindu,Stue sør,2.1,2.8,180,90,,0.75,2\n";
|
||||
let r = fra_tekst(csv, "x").unwrap();
|
||||
assert_eq!(r.bygning.bygning.navn, "Testhus");
|
||||
assert_eq!(r.bygning.bygning.oppvarmet_bra, 140.0);
|
||||
assert_eq!(r.bygning.bygning.byggeaar, 1948);
|
||||
assert_eq!(r.bygning.flate.len(), 2);
|
||||
assert_eq!(r.bygning.flate[1].mot, Mot::Grunn);
|
||||
assert_eq!(r.bygning.vindu.len(), 1);
|
||||
assert_eq!(r.bygning.vindu[0].antall, 2.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn godtar_semikolon_og_desimalkomma() {
|
||||
let csv = "type;navn;areal;u_verdi\nvegg;Vegg;32,5;1,0\n";
|
||||
let r = fra_tekst(csv, "x").unwrap();
|
||||
assert_eq!(r.bygning.flate[0].areal, 32.5);
|
||||
assert_eq!(r.bygning.flate[0].u_verdi, Some(1.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
//! Minimal leser for STEP-filer etter ISO 10303-21, som er formatet IFC bruker.
|
||||
//!
|
||||
//! Filen består av linjer på formen `#12=IFCWALL('guid',#3,'Navn',...);`. Denne modulen
|
||||
//! deler filen i entiteter med type og argumenter, uten å tolke IFC-skjemaet.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Arg {
|
||||
Ref(u64),
|
||||
Tekst(String),
|
||||
Tall(f64),
|
||||
Enum(String),
|
||||
Liste(Vec<Arg>),
|
||||
Tom,
|
||||
}
|
||||
|
||||
impl Arg {
|
||||
pub fn som_ref(&self) -> Option<u64> {
|
||||
match self {
|
||||
Arg::Ref(r) => Some(*r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn som_tall(&self) -> Option<f64> {
|
||||
match self {
|
||||
Arg::Tall(v) => Some(*v),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn som_tekst(&self) -> Option<&str> {
|
||||
match self {
|
||||
Arg::Tekst(s) | Arg::Enum(s) => Some(s),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn som_liste(&self) -> &[Arg] {
|
||||
match self {
|
||||
Arg::Liste(l) => l,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entitet {
|
||||
pub typ: String,
|
||||
pub args: Vec<Arg>,
|
||||
}
|
||||
|
||||
impl Entitet {
|
||||
pub fn arg(&self, i: usize) -> Option<&Arg> {
|
||||
self.args.get(i)
|
||||
}
|
||||
pub fn navn(&self) -> Option<&str> {
|
||||
self.arg(2).and_then(|a| a.som_tekst())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Fil {
|
||||
pub entiteter: HashMap<u64, Entitet>,
|
||||
}
|
||||
|
||||
impl Fil {
|
||||
pub fn hent(&self, r: u64) -> Option<&Entitet> {
|
||||
self.entiteter.get(&r)
|
||||
}
|
||||
/// Alle entiteter av en gitt type (uten hensyn til store/små bokstaver).
|
||||
pub fn av_type<'a>(&'a self, typ: &'a str) -> impl Iterator<Item = (u64, &'a Entitet)> + 'a {
|
||||
self.entiteter
|
||||
.iter()
|
||||
.filter_map(move |(k, e)| (e.typ == typ).then_some((*k, e)))
|
||||
}
|
||||
pub fn av_typer<'a>(
|
||||
&'a self,
|
||||
typer: &'a [&'a str],
|
||||
) -> impl Iterator<Item = (u64, &'a Entitet)> + 'a {
|
||||
self.entiteter
|
||||
.iter()
|
||||
.filter_map(move |(k, e)| typer.contains(&e.typ.as_str()).then_some((*k, e)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Deler en STEP-fil i entiteter.
|
||||
pub fn les(tekst: &str) -> Fil {
|
||||
let mut entiteter = HashMap::new();
|
||||
let mut setning = String::new();
|
||||
let mut i_streng = false;
|
||||
let mut i_data = false;
|
||||
|
||||
for ch in tekst.chars() {
|
||||
if !i_data {
|
||||
setning.push(ch);
|
||||
if setning.len() > 4096 {
|
||||
setning.drain(..2048);
|
||||
}
|
||||
if setning.to_uppercase().ends_with("DATA;") {
|
||||
i_data = true;
|
||||
setning.clear();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match ch {
|
||||
'\'' => {
|
||||
i_streng = !i_streng;
|
||||
setning.push(ch);
|
||||
}
|
||||
';' if !i_streng => {
|
||||
if let Some((nr, e)) = tolk_setning(&setning) {
|
||||
entiteter.insert(nr, e);
|
||||
}
|
||||
setning.clear();
|
||||
}
|
||||
'\n' | '\r' if !i_streng => setning.push(' '),
|
||||
_ => setning.push(ch),
|
||||
}
|
||||
}
|
||||
Fil { entiteter }
|
||||
}
|
||||
|
||||
fn tolk_setning(s: &str) -> Option<(u64, Entitet)> {
|
||||
let s = s.trim();
|
||||
let s = s.strip_prefix('#')?;
|
||||
let (nr, rest) = s.split_once('=')?;
|
||||
let nr: u64 = nr.trim().parse().ok()?;
|
||||
let rest = rest.trim();
|
||||
let paren = rest.find('(')?;
|
||||
let typ = rest[..paren].trim().to_uppercase();
|
||||
if typ.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let innhold = &rest[paren + 1..rest.rfind(')')?];
|
||||
Some((
|
||||
nr,
|
||||
Entitet {
|
||||
typ,
|
||||
args: del_argumenter(innhold),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// Deler argumentlisten på toppnivå-kommaer.
|
||||
fn del_argumenter(s: &str) -> Vec<Arg> {
|
||||
let mut ut = Vec::new();
|
||||
let mut dybde = 0usize;
|
||||
let mut i_streng = false;
|
||||
let mut start = 0usize;
|
||||
let b: Vec<char> = s.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
let c = b[i];
|
||||
match c {
|
||||
'\'' => {
|
||||
if i_streng && i + 1 < b.len() && b[i + 1] == '\'' {
|
||||
i += 1;
|
||||
} else {
|
||||
i_streng = !i_streng;
|
||||
}
|
||||
}
|
||||
'(' if !i_streng => dybde += 1,
|
||||
')' if !i_streng => dybde = dybde.saturating_sub(1),
|
||||
',' if !i_streng && dybde == 0 => {
|
||||
ut.push(tolk_arg(&b[start..i].iter().collect::<String>()));
|
||||
start = i + 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if start <= b.len() {
|
||||
let siste: String = b[start..].iter().collect();
|
||||
if !(siste.trim().is_empty() && ut.is_empty()) {
|
||||
ut.push(tolk_arg(&siste));
|
||||
}
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
fn tolk_arg(s: &str) -> Arg {
|
||||
let t = s.trim();
|
||||
if t.is_empty() || t == "$" || t == "*" {
|
||||
return Arg::Tom;
|
||||
}
|
||||
if let Some(r) = t.strip_prefix('#') {
|
||||
if let Ok(n) = r.trim().parse::<u64>() {
|
||||
return Arg::Ref(n);
|
||||
}
|
||||
}
|
||||
if t.starts_with('\'') && t.ends_with('\'') && t.len() >= 2 {
|
||||
return Arg::Tekst(avkod_tekst(&t[1..t.len() - 1]));
|
||||
}
|
||||
if t.starts_with('.') && t.ends_with('.') && t.len() >= 2 {
|
||||
return Arg::Enum(t[1..t.len() - 1].to_string());
|
||||
}
|
||||
if t.starts_with('(') && t.ends_with(')') {
|
||||
return Arg::Liste(del_argumenter(&t[1..t.len() - 1]));
|
||||
}
|
||||
if let Ok(v) = t.parse::<f64>() {
|
||||
return Arg::Tall(v);
|
||||
}
|
||||
// Typede verdier som IFCREAL(3.0) eller IFCLABEL('x').
|
||||
if let Some(p) = t.find('(') {
|
||||
if t.ends_with(')') {
|
||||
let indre = &t[p + 1..t.len() - 1];
|
||||
return tolk_arg(indre);
|
||||
}
|
||||
}
|
||||
Arg::Tekst(t.to_string())
|
||||
}
|
||||
|
||||
/// Dekoder ISO 10303-21-strenger: `''` er apostrof, `\X2\…\X0\` er UTF-16, `\X\hh` er latin-1.
|
||||
fn avkod_tekst(s: &str) -> String {
|
||||
let mut ut = String::with_capacity(s.len());
|
||||
let b: Vec<char> = s.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < b.len() {
|
||||
if b[i] == '\'' && i + 1 < b.len() && b[i + 1] == '\'' {
|
||||
ut.push('\'');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if b[i] == '\\' && i + 3 < b.len() && b[i + 1] == 'X' && b[i + 2] == '2' && b[i + 3] == '\\'
|
||||
{
|
||||
let mut j = i + 4;
|
||||
let mut enheter: Vec<u16> = Vec::new();
|
||||
while j + 3 < b.len() {
|
||||
if b[j] == '\\' {
|
||||
break;
|
||||
}
|
||||
let hex: String = b[j..(j + 4).min(b.len())].iter().collect();
|
||||
match u16::from_str_radix(&hex, 16) {
|
||||
Ok(v) => enheter.push(v),
|
||||
Err(_) => break,
|
||||
}
|
||||
j += 4;
|
||||
}
|
||||
ut.push_str(&String::from_utf16_lossy(&enheter));
|
||||
// Hopp over avslutningen \X0\.
|
||||
while j < b.len() && b[j] != '0' {
|
||||
j += 1;
|
||||
}
|
||||
i = (j + 2).min(b.len());
|
||||
continue;
|
||||
}
|
||||
if b[i] == '\\' && i + 3 < b.len() && b[i + 1] == 'X' && b[i + 2] == '\\' {
|
||||
let hex: String = b[i + 3..(i + 5).min(b.len())].iter().collect();
|
||||
if let Ok(v) = u8::from_str_radix(&hex, 16) {
|
||||
ut.push(v as char);
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ut.push(b[i]);
|
||||
i += 1;
|
||||
}
|
||||
ut
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn leser_entiteter_og_argumenter() {
|
||||
let f = les("ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n\
|
||||
#1= IFCWALL('0xABC',#2,'Yttervegg s\\X2\\00F8\\X0\\r',$,$,#10,#20,$);\n\
|
||||
#2= IFCOWNERHISTORY(#3,#4,$,.ADDED.,$,$,$,0);\n\
|
||||
#10= IFCLOCALPLACEMENT($,#11);\n\
|
||||
ENDSEC;");
|
||||
assert_eq!(f.entiteter.len(), 3);
|
||||
let v = f.hent(1).unwrap();
|
||||
assert_eq!(v.typ, "IFCWALL");
|
||||
assert_eq!(v.navn(), Some("Yttervegg sør"));
|
||||
assert_eq!(v.arg(5).unwrap().som_ref(), Some(10));
|
||||
assert_eq!(
|
||||
f.hent(2).unwrap().arg(3).unwrap().som_tekst(),
|
||||
Some("ADDED")
|
||||
);
|
||||
assert_eq!(f.hent(10).unwrap().arg(0), Some(&Arg::Tom));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn haandterer_lister_og_semikolon_i_streng() {
|
||||
let f = les("DATA;\n#5= IFCPOLYLOOP((#1,#2,#3));\n#6= IFCLABEL('a;b');\nENDSEC;");
|
||||
assert_eq!(f.hent(5).unwrap().arg(0).unwrap().som_liste().len(), 3);
|
||||
assert_eq!(f.hent(6).unwrap().arg(0).unwrap().som_tekst(), Some("a;b"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leser_tall_og_typede_verdier() {
|
||||
let f = les("DATA;\n#7= IFCPROPERTYSINGLEVALUE('U',$,IFCREAL(0.18),$);\nENDSEC;");
|
||||
assert_eq!(f.hent(7).unwrap().arg(2).unwrap().som_tall(), Some(0.18));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Leser `bygning.toml` og fletter inn scenarioendringer.
|
||||
|
||||
use crate::modell::{Bygning, Scenario};
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use std::path::Path;
|
||||
use toml::Value;
|
||||
|
||||
pub fn les_bygning(sti: &Path) -> Result<Bygning> {
|
||||
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
||||
}
|
||||
|
||||
/// Lager en ny bygning der scenarioets endringer er flettet inn.
|
||||
///
|
||||
/// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches
|
||||
/// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges
|
||||
/// til, og `slett = true` fjerner et element. Andre verdier erstattes.
|
||||
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
||||
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
|
||||
flett(&mut grunn, &s.endringer);
|
||||
grunn
|
||||
.try_into()
|
||||
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
||||
}
|
||||
|
||||
fn navnenokkel(v: &Value) -> Option<String> {
|
||||
let t = v.as_table()?;
|
||||
t.get("navn")
|
||||
.and_then(|x| x.as_str())
|
||||
.or_else(|| t.get("type").and_then(|x| x.as_str()))
|
||||
.map(|s| s.to_string())
|
||||
}
|
||||
|
||||
fn skal_slettes(v: &Value) -> bool {
|
||||
v.as_table()
|
||||
.and_then(|t| t.get("slett"))
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn flett(grunn: &mut Value, over: &Value) {
|
||||
match (grunn, over) {
|
||||
(Value::Table(g), Value::Table(o)) => {
|
||||
for (k, v) in o {
|
||||
if k == "slett" {
|
||||
continue;
|
||||
}
|
||||
match g.get_mut(k) {
|
||||
Some(eksisterende) => flett(eksisterende, v),
|
||||
None => {
|
||||
g.insert(k.clone(), v.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Value::Array(g), Value::Array(o)) => {
|
||||
for element in o {
|
||||
match navnenokkel(element) {
|
||||
Some(navn) => {
|
||||
let treff = g
|
||||
.iter()
|
||||
.position(|e| navnenokkel(e).as_deref() == Some(&navn));
|
||||
match treff {
|
||||
Some(i) if skal_slettes(element) => {
|
||||
g.remove(i);
|
||||
}
|
||||
Some(i) => flett(&mut g[i], element),
|
||||
None if skal_slettes(element) => {}
|
||||
None => g.push(element.clone()),
|
||||
}
|
||||
}
|
||||
None => g.push(element.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
(g, o) => *g = o.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tester {
|
||||
use super::*;
|
||||
|
||||
const GRUNN: &str = r#"
|
||||
[bygning]
|
||||
navn = "Testhus"
|
||||
kategori = "smaahus"
|
||||
byggeaar = 1948
|
||||
oppvarmet_bra = 100.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg"
|
||||
type = "vegg"
|
||||
areal = 80.0
|
||||
u_verdi = 1.0
|
||||
|
||||
[[flate]]
|
||||
navn = "Tak"
|
||||
type = "tak"
|
||||
areal = 100.0
|
||||
u_verdi = 0.8
|
||||
"#;
|
||||
|
||||
#[test]
|
||||
fn scenario_endrer_u_verdi_for_navngitt_flate() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let s = Scenario {
|
||||
navn: "Etterisolert".into(),
|
||||
beskrivelse: None,
|
||||
endringer: toml::from_str(r#"flate = [{ navn = "Yttervegg", u_verdi = 0.22 }]"#)
|
||||
.unwrap(),
|
||||
};
|
||||
let ny = slaa_sammen_scenario(&b, &s).unwrap();
|
||||
assert_eq!(ny.flate.len(), 2);
|
||||
assert_eq!(ny.flate[0].u_verdi, Some(0.22));
|
||||
assert_eq!(ny.flate[1].u_verdi, Some(0.8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_legger_til_og_sletter() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let s = Scenario {
|
||||
navn: "Ombygd".into(),
|
||||
beskrivelse: None,
|
||||
endringer: toml::from_str(
|
||||
r#"flate = [{ navn = "Tak", slett = true }, { navn = "Nytt tak", type = "tak", areal = 100.0, u_verdi = 0.13 }]"#,
|
||||
)
|
||||
.unwrap(),
|
||||
};
|
||||
let ny = slaa_sammen_scenario(&b, &s).unwrap();
|
||||
let navn: Vec<&str> = ny.flate.iter().map(|f| f.navn.as_str()).collect();
|
||||
assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_endrer_skalarfelt() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let s = Scenario {
|
||||
navn: "Varmepumpe".into(),
|
||||
beskrivelse: None,
|
||||
endringer: toml::from_str(
|
||||
r#"
|
||||
[infiltrasjon]
|
||||
n50 = 3.0
|
||||
[oppvarming]
|
||||
kilde = [{ type = "luft_luft_vp", andel_rom = 0.6 }]
|
||||
"#,
|
||||
)
|
||||
.unwrap(),
|
||||
};
|
||||
let ny = slaa_sammen_scenario(&b, &s).unwrap();
|
||||
assert_eq!(ny.infiltrasjon.n50, Some(3.0));
|
||||
assert_eq!(ny.oppvarming.kilde.len(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user