2026-09-14 18:03:00 +02:00
|
|
|
//! 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,
|
2026-09-14 19:16:18 +02:00
|
|
|
over_terreng: None,
|
|
|
|
|
sone: None,
|
2026-09-14 18:03:00 +02:00
|
|
|
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,
|
2026-09-14 19:16:18 +02:00
|
|
|
sone: Vec::new(),
|
|
|
|
|
grunn: Grunn::default(),
|
2026-09-14 18:03:00 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|