Høyden ble målt fra gulvet i etasjen over, så dekkets egen tykkelse ble regnet som mur over bakken. Nå måles den fra overkant av selve muren, som geometrien gir den. For Nordhusstykket 5 går høyden fra 1,11–1,50 m til 0,76–1,24 m, i snitt 0,98 m. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
2199 lines
82 KiB
Rust
2199 lines
82 KiB
Rust
//! 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::ifcgeom;
|
||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||
use crate::inndata::toml_inn::Importvalg;
|
||
use crate::klima::geo;
|
||
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>,
|
||
/// Bygningsdeler som ikke er tatt med i klimaskjermen: navn, areal og U-verdi.
|
||
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
||
}
|
||
|
||
/// Konstruksjonsnavn og avrundet plassering, som skiller én bygningsdel fra en annen.
|
||
type Dublettnokkel = (String, i64, i64, i64, i64);
|
||
|
||
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||
|
||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||
les_med(sti, &Importvalg::default())
|
||
}
|
||
|
||
pub fn les_med(sti: &Path, valg: &Importvalg) -> Result<Importrapport> {
|
||
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||
let tekst = String::from_utf8_lossy(&raa);
|
||
fra_tekst_med(
|
||
&tekst,
|
||
sti.file_stem()
|
||
.and_then(|s| s.to_str())
|
||
.unwrap_or("Bygning"),
|
||
valg,
|
||
)
|
||
}
|
||
|
||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||
fra_tekst_med(tekst, standardnavn, &Importvalg::default())
|
||
}
|
||
|
||
pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Result<Importrapport> {
|
||
let fil = step::les(tekst);
|
||
let mut merknader = Vec::new();
|
||
|
||
let skala = ifcgeom::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);
|
||
let (etasjer, etasje_for) = les_etasjer(&fil, skala);
|
||
|
||
// En eksport uten bygningsdeler gir ingen klimaskjerm å regne på. Det skjer typisk når
|
||
// designlagene ikke er knyttet til etasjer i eksportdialogen, og da er det bedre å stoppe
|
||
// enn å skrive en tom bygningsbeskrivelse.
|
||
let antall_deler: usize = [
|
||
"IFCWALL",
|
||
"IFCWALLSTANDARDCASE",
|
||
"IFCCURTAINWALL",
|
||
"IFCSLAB",
|
||
"IFCROOF",
|
||
"IFCWINDOW",
|
||
"IFCDOOR",
|
||
"IFCSPACE",
|
||
]
|
||
.iter()
|
||
.map(|t| fil.av_type(t).count())
|
||
.sum();
|
||
if antall_deler == 0 {
|
||
anyhow::bail!(
|
||
"IFC-filen har ingen bygningsdeler: ingen vegger, dekker, tak, vinduer, dører eller rom. \
|
||
Den inneholder bare prosjektstrukturen{}. I Vectorworks skjer dette gjerne når designlagene \
|
||
ikke er med under «Layer Mapping» i «Export IFC Project».",
|
||
if etasjer.is_empty() {
|
||
String::new()
|
||
} else {
|
||
format!(" med {} etasjer", etasjer.len())
|
||
}
|
||
);
|
||
}
|
||
|
||
// ── oppvarmet areal fra rommene ─────────────────────────────────
|
||
// Rom i en etasje under terreng hører til kjelleren og ikke til oppvarmet BRA.
|
||
let mut bra = 0.0;
|
||
let mut volum = 0.0;
|
||
let mut kjellerareal = 0.0;
|
||
let mut kjellervolum = 0.0;
|
||
let mut rom: Vec<(String, f64)> = Vec::new();
|
||
let mut kjellerrom: Vec<(String, f64)> = Vec::new();
|
||
let mut mistenkelige: Vec<String> = Vec::new();
|
||
let mut uoppvarmede: Vec<String> = Vec::new();
|
||
for (r, e) in fil.av_type("IFCSPACE") {
|
||
let navn = romnavn(e);
|
||
let areal = mengder.get(&r).map(arealmengde).unwrap_or(0.0);
|
||
// Rom som beskriver bebygget areal eller bruttoareal er ikke gulvareal.
|
||
let maalerom = matches!(navn.to_uppercase().as_str(), n if n.starts_with("BYA") || n.starts_with("BTA"));
|
||
if areal <= 0.0 || maalerom {
|
||
continue;
|
||
}
|
||
let romvolum = mengder
|
||
.get(&r)
|
||
.and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied())
|
||
.unwrap_or(0.0);
|
||
let under = etasje_for
|
||
.get(&r)
|
||
.and_then(|i| etasjer.get(*i))
|
||
.map(|e| e.under_terreng)
|
||
.unwrap_or(false);
|
||
if under {
|
||
kjellerrom.push((navn, areal));
|
||
kjellerareal += areal;
|
||
kjellervolum += romvolum;
|
||
} else {
|
||
let lavt = navn.to_lowercase();
|
||
if valg
|
||
.uoppvarmede_rom
|
||
.iter()
|
||
.any(|u| lavt.contains(&u.to_lowercase()))
|
||
{
|
||
uoppvarmede.push(format!("{navn} {areal:.1} m²"));
|
||
continue;
|
||
}
|
||
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
|
||
.iter()
|
||
.any(|o| lavt.contains(o))
|
||
{
|
||
mistenkelige.push(format!("{navn} {areal:.1} m²"));
|
||
}
|
||
rom.push((navn, areal));
|
||
bra += areal;
|
||
volum += romvolum;
|
||
}
|
||
}
|
||
rom.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
|
||
if !rom.is_empty() {
|
||
let liste: Vec<String> = rom.iter().map(|(n, a)| format!("{n} {a:.1}")).collect();
|
||
merknader.push(format!(
|
||
"Oppvarmet BRA er satt til {bra:.1} m², summen av rommene i etasjene over terreng. Rom [m²]: {}.",
|
||
liste.join(", ")
|
||
));
|
||
}
|
||
if !mistenkelige.is_empty() {
|
||
merknader.push(format!(
|
||
"Disse rommene er regnet som oppvarmet, men navnet tyder på at de ikke er det: {}. Trekk dem fra oppvarmet_bra om de står uoppvarmet.",
|
||
mistenkelige.join(", ")
|
||
));
|
||
}
|
||
if !uoppvarmede.is_empty() {
|
||
merknader.push(format!(
|
||
"Disse rommene er uoppvarmet etter tilleggsfilen og holdt utenfor oppvarmet BRA: {}.",
|
||
uoppvarmede.join(", ")
|
||
));
|
||
}
|
||
|
||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||
let mut deler: Vec<Del> = Vec::new();
|
||
for (r, e) in fil.av_typer(&typer) {
|
||
let plass = e
|
||
.arg(5)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|p| global_plassering(&fil, p, skala));
|
||
let netter = ifcgeom::netter_for_produkt(&fil, e.arg(6).and_then(|a| a.som_ref()));
|
||
let flate = ifcgeom::samlet_hovedflate(&netter, skala);
|
||
deler.push(Del {
|
||
nr: r,
|
||
typ: e.typ.clone(),
|
||
navn: navn_av(e, r, standardnavn_for(&e.typ)),
|
||
etasje: etasje_for.get(&r).copied(),
|
||
ytre: er_ytre(&egenskaper, r),
|
||
u_verdi: u_verdi(&egenskaper, r),
|
||
plass,
|
||
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
||
normal: flate.map(|f| f.normal),
|
||
z: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.hoyde).unwrap_or(0.0),
|
||
topp: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.topp).unwrap_or(0.0),
|
||
});
|
||
}
|
||
|
||
// Noen eksporter legger den samme bygningsdelen i flere etasjer. To deler med samme
|
||
// konstruksjon, samme areal og samme plassering er den samme veggen eller det samme
|
||
// dekket. Kopiene blir stående, slik at vinduene i dem finner veggen sin, men de telles
|
||
// ikke med i klimaskjermen. Av kopiene beholdes den som ligger i den etasjen høyden
|
||
// tilsier, slik at en kjellervegg havner i kjelleren.
|
||
let konstruksjon = |navn: &str| navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||
let mut grupper: Vec<(Dublettnokkel, Vec<u64>)> = Vec::new();
|
||
for d in &deler {
|
||
let Some((x, y, _, _, _)) = d.plass else {
|
||
continue;
|
||
};
|
||
let cm = |v: f64| (v * 50.0).round() as i64;
|
||
let nokkel = (
|
||
konstruksjon(&d.navn),
|
||
cm(x),
|
||
cm(y),
|
||
cm(d.z),
|
||
(d.areal * 100.0).round() as i64,
|
||
);
|
||
match grupper.iter_mut().find(|(k, _)| *k == nokkel) {
|
||
Some((_, medlemmer)) => medlemmer.push(d.nr),
|
||
None => grupper.push((nokkel, vec![d.nr])),
|
||
}
|
||
}
|
||
let mut dublett_av: HashMap<u64, u64> = HashMap::new();
|
||
for (_, medlemmer) in grupper.iter().filter(|(_, m)| m.len() > 1) {
|
||
let avstand_til_etasjen = |nr: u64| {
|
||
let d = deler.iter().find(|d| d.nr == nr)?;
|
||
let hoyde = d.etasje.and_then(|i| etasjer.get(i)).map(|e| e.hoyde)?;
|
||
Some((d.z - hoyde).abs())
|
||
};
|
||
let beholdt = *medlemmer
|
||
.iter()
|
||
.min_by(|a, b| {
|
||
let (fa, fb) = (avstand_til_etasjen(**a), avstand_til_etasjen(**b));
|
||
match (fa, fb) {
|
||
(Some(x), Some(y)) => x.total_cmp(&y).then(a.cmp(b)),
|
||
(Some(_), None) => std::cmp::Ordering::Less,
|
||
(None, Some(_)) => std::cmp::Ordering::Greater,
|
||
(None, None) => a.cmp(b),
|
||
}
|
||
})
|
||
.unwrap_or(&medlemmer[0]);
|
||
for nr in medlemmer.iter().filter(|nr| **nr != beholdt) {
|
||
dublett_av.insert(*nr, beholdt);
|
||
}
|
||
}
|
||
let dubletter = dublett_av.len();
|
||
|
||
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
||
let ytre_deler: Vec<&Del> = deler
|
||
.iter()
|
||
.filter(|d| VEGGTYPER.contains(&d.typ.as_str()) && d.ytre.unwrap_or(false))
|
||
.collect();
|
||
let referanse: Vec<&Del> = if ytre_deler.is_empty() {
|
||
deler
|
||
.iter()
|
||
.filter(|d| VEGGTYPER.contains(&d.typ.as_str()))
|
||
.collect()
|
||
} else {
|
||
ytre_deler
|
||
};
|
||
let (mut cx, mut cy, mut n) = (0.0, 0.0, 0.0);
|
||
for d in &referanse {
|
||
if let Some((x, y, _, _, _)) = d.plass {
|
||
cx += x;
|
||
cy += y;
|
||
n += 1.0;
|
||
}
|
||
}
|
||
if n > 0.0 {
|
||
cx /= n;
|
||
cy /= n;
|
||
}
|
||
|
||
// ── kjeller som egen sone ───────────────────────────────────────
|
||
// Finnes det en etasje under terreng med rom, er den nesten alltid uoppvarmet.
|
||
// Bygningsdelene der hører til kjellerens klimaskjerm, og etasjeskilleren over blir
|
||
// grensen mellom den oppvarmede delen og sonen.
|
||
let kjelleretasje = (!kjellerrom.is_empty())
|
||
.then(|| etasjer.iter().position(|e| e.under_terreng))
|
||
.flatten();
|
||
let kjellernavn = kjelleretasje
|
||
.and_then(|i| etasjer.get(i))
|
||
.map(|e| e.navn.clone())
|
||
.unwrap_or_default();
|
||
// Etasjehøyden gir hvor dypt kjelleren ligger.
|
||
let kjellerdybde = kjelleretasje
|
||
.and_then(|i| {
|
||
let denne = etasjer.get(i)?.hoyde;
|
||
let over = etasjer.get(i + 1).map(|e| e.hoyde).unwrap_or(denne + 2.4);
|
||
Some(over - denne)
|
||
})
|
||
.unwrap_or(0.0);
|
||
// Den laveste etasjen over terreng, der etasjeskilleren mot kjelleren ligger.
|
||
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
|
||
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
|
||
let terrengmodell = terrengpunkter(&fil, skala);
|
||
let terreng = terrenghoyder(&fil, skala);
|
||
// Ligger terrengmodellen over gulvet i første etasje, er den ikke lagt i bakkenivå, og da
|
||
// sier den ingenting om hvor mye av grunnmuren som er nedgravd.
|
||
let terreng_over_gulvet = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||
(Some((referanse, terrengnivaa)), Some(forste)) => {
|
||
terrengnivaa > referanse + forste.hoyde + 0.5
|
||
}
|
||
_ => false,
|
||
};
|
||
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||
(Some((referanse, terrengnivaa)), Some(forste)) if !terreng_over_gulvet => {
|
||
over_terreng_for_kjeller(referanse, terrengnivaa, forste.hoyde, kjellerdybde)
|
||
}
|
||
_ => 0.0,
|
||
};
|
||
|
||
// ── klimaskjerm ─────────────────────────────────────────────────
|
||
let mut kjellerflater: Vec<Flate> = Vec::new();
|
||
let mut flater: Vec<Flate> = Vec::new();
|
||
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
||
let mut uten_uverdi: Vec<String> = Vec::new();
|
||
let mut uten_geometri = 0usize;
|
||
let mut grunnmurshoyder: Vec<f64> = Vec::new();
|
||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||
|
||
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
||
let etasjekontekst = Etasjekontekst {
|
||
kjeller: kjelleretasje,
|
||
forste_over,
|
||
forste_over_hoyde: forste_over.and_then(|i| etasjer.get(i)).map(|e| e.hoyde),
|
||
};
|
||
let laveste = deler
|
||
.iter()
|
||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||
.map(|d| d.z)
|
||
.fold(f64::INFINITY, f64::min);
|
||
|
||
for d in &deler {
|
||
let er_vegg = VEGGTYPER.contains(&d.typ.as_str());
|
||
let (asimut, helning) = retning_av(d, cx, cy, sant_nord);
|
||
if er_vegg {
|
||
asimut_for_vegg.insert(d.nr, asimut);
|
||
}
|
||
if d.areal <= 0.05 {
|
||
uten_geometri += 1;
|
||
continue;
|
||
}
|
||
if dublett_av.contains_key(&d.nr) {
|
||
continue;
|
||
}
|
||
let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
|
||
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
|
||
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
||
continue;
|
||
};
|
||
// En etasjeskiller like over kjelleren er grensen mot den uoppvarmede sonen. Høyden
|
||
// avgjør, ikke etasjetilknytningen, som kan være feil i eksporten.
|
||
let ved_kjellertaket = etasjekontekst
|
||
.forste_over_hoyde
|
||
.map(|h| (d.z - h).abs() < 0.6)
|
||
.unwrap_or(false);
|
||
let (mot, sone) = if kjelleretasje.is_some()
|
||
&& !i_kjeller
|
||
&& ved_kjellertaket
|
||
&& typ == FlateType::Gulv
|
||
&& mot != Mot::Grunn
|
||
{
|
||
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
||
} else {
|
||
(mot, None)
|
||
};
|
||
if d.u_verdi.is_none() {
|
||
uten_uverdi.push(d.navn.clone());
|
||
}
|
||
// Terrengmodellen sier hvor høyt bakken ligger akkurat der denne veggen står, så en
|
||
// skrånende tomt gir forskjellig høyde over bakken på hver side av huset.
|
||
// Høyden måles fra overkant mur, ikke fra gulvet over: dekket selv står ikke i bakken.
|
||
let over_bakken = d
|
||
.plass
|
||
.and_then(|(x, y, _, _, _)| terreng_ved(&terrengmodell, x, y))
|
||
.map(|z| (d.topp - z).clamp(0.0, kjellerdybde))
|
||
.unwrap_or(kjeller_over_terreng);
|
||
if i_kjeller && mot == Mot::Grunn && typ == FlateType::Vegg {
|
||
grunnmurshoyder.push(over_bakken);
|
||
}
|
||
let mal = if i_kjeller {
|
||
&mut kjellerflater
|
||
} else {
|
||
&mut flater
|
||
};
|
||
mal.push(Flate {
|
||
navn: d.navn.clone(),
|
||
typ,
|
||
areal: d.areal,
|
||
orientering: if typ == FlateType::Vegg { asimut } else { 0.0 },
|
||
helning: Some(helning),
|
||
u_verdi: d.u_verdi,
|
||
lag: None,
|
||
masseklasse: masseklasse_av_materialer(&fil, d.nr),
|
||
masseplassering: Masseplassering::Fordelt,
|
||
mot,
|
||
b_faktor: None,
|
||
absorptans: None,
|
||
omkrets: None,
|
||
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - over_bakken),
|
||
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(over_bakken),
|
||
sone,
|
||
kommentar: d
|
||
.etasje
|
||
.and_then(|i| etasjer.get(i))
|
||
.map(|e| format!("{}, importert fra IFC", e.navn))
|
||
.or_else(|| Some("importert fra IFC".into())),
|
||
});
|
||
}
|
||
|
||
slaa_sammen(&mut flater);
|
||
slaa_sammen(&mut kjellerflater);
|
||
|
||
// ── vinduer og dører ────────────────────────────────────────────
|
||
let mut vinduer: Vec<Vindu> = Vec::new();
|
||
let mut sette_vinduer: std::collections::HashSet<(String, (i64, i64, i64), i64)> =
|
||
Default::default();
|
||
let mut vindusdubletter = 0usize;
|
||
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) * skala;
|
||
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||
let areal = mengder
|
||
.get(&r)
|
||
.map(arealmengde)
|
||
.filter(|a| *a > 0.05)
|
||
.unwrap_or(hoyde * bredde);
|
||
if areal <= 0.05 {
|
||
continue;
|
||
}
|
||
let vert = if dor {
|
||
dor_vert.get(&r)
|
||
} else {
|
||
vindu_vert.get(&r)
|
||
}
|
||
.map(|v| dublett_av.get(v).copied().unwrap_or(*v));
|
||
let vert = vert.as_ref();
|
||
// Vinduer i innervegger hører ikke til klimaskjermen.
|
||
let i_ytre_vegg = vert
|
||
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
||
.map(|d| d.ytre.unwrap_or(true))
|
||
.unwrap_or(true);
|
||
if !i_ytre_vegg {
|
||
utelatt.push((
|
||
format!("{} (i innervegg)", navn_av(e, r, "Vindu")),
|
||
areal,
|
||
u_verdi(&egenskaper, r),
|
||
));
|
||
continue;
|
||
}
|
||
// Det samme vinduet i to kopier av den samme veggen er ett vindu.
|
||
let sted = e
|
||
.arg(5)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|p| global_plassering(&fil, p, skala))
|
||
.map(|(x, y, z, _, _)| {
|
||
let cm = |v: f64| (v * 50.0).round() as i64;
|
||
(cm(x), cm(y), cm(z))
|
||
})
|
||
.unwrap_or_default();
|
||
let nokkel = (
|
||
konstruksjon(&navn_av(e, r, if dor { "Dør" } else { "Vindu" })),
|
||
sted,
|
||
(areal * 100.0).round() as i64,
|
||
);
|
||
if !sette_vinduer.insert(nokkel) {
|
||
vindusdubletter += 1;
|
||
continue;
|
||
}
|
||
let asimut = vert
|
||
.and_then(|v| asimut_for_vegg.get(v))
|
||
.copied()
|
||
.unwrap_or(0.0);
|
||
// Uten U-verdi i modellen lar vi feltet stå tomt, slik at beregningen bruker en
|
||
// typisk verdi for byggeåret i stedet for en fast standardverdi.
|
||
let u = u_verdi(&egenskaper, r);
|
||
if u.is_none() {
|
||
uten_uverdi.push(navn_av(e, r, if dor { "Dør" } else { "Vindu" }));
|
||
}
|
||
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()),
|
||
});
|
||
}
|
||
|
||
// Vindus- og dørarealet trekkes fra veggene de sitter i, dersom geometrien ikke
|
||
// allerede har hull der åpningene er.
|
||
let mut apning_per_vegg: HashMap<u64, f64> = HashMap::new();
|
||
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
||
let e = fil.hent(*a);
|
||
let h = e
|
||
.and_then(|e| e.arg(8))
|
||
.and_then(|x| x.som_tall())
|
||
.unwrap_or(0.0)
|
||
* skala;
|
||
let b = e
|
||
.and_then(|e| e.arg(9))
|
||
.and_then(|x| x.som_tall())
|
||
.unwrap_or(0.0)
|
||
* skala;
|
||
*apning_per_vegg.entry(*vegg).or_default() += h * b;
|
||
}
|
||
let mut trukket_fra = 0usize;
|
||
for d in &deler {
|
||
let Some(ap) = apning_per_vegg.get(&d.nr) else {
|
||
continue;
|
||
};
|
||
let Some(f) = flater.iter_mut().find(|f| f.navn == d.navn) else {
|
||
continue;
|
||
};
|
||
// Ligger veggarealet nær bruttoarealet, har geometrien ikke hull, og åpningene trekkes fra.
|
||
if f.areal > *ap * 1.5 && d.normal.is_some() {
|
||
let netto = (f.areal - ap).max(0.0);
|
||
if netto > 0.05 {
|
||
f.areal = netto;
|
||
trukket_fra += 1;
|
||
}
|
||
}
|
||
}
|
||
if trukket_fra > 0 {
|
||
merknader.push(format!(
|
||
"Vindus- og dørareal er trukket fra {trukket_fra} vegger. Kontroller veggarealene mot tegningen."
|
||
));
|
||
}
|
||
|
||
// Tilleggsfiler og scenarioer viser til flatene med navn, så navnene må være entydige.
|
||
unike_navn(&mut flater);
|
||
unike_navn(&mut kjellerflater);
|
||
|
||
// ── merknader ───────────────────────────────────────────────────
|
||
if bra <= 0.0 {
|
||
bra = flater
|
||
.iter()
|
||
.filter(|f| f.typ == FlateType::Gulv)
|
||
.map(|f| f.areal)
|
||
.sum::<f64>();
|
||
merknader.push(if bra > 0.0 {
|
||
format!("IFC-modellen mangler romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvflatene. Kontroller verdien.")
|
||
} else {
|
||
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra.".into()
|
||
});
|
||
}
|
||
if uten_geometri > 0 {
|
||
merknader.push(format!(
|
||
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
||
));
|
||
}
|
||
if !utelatt.is_empty() {
|
||
// Gruppert per konstruksjon, slik at listen er til å lese.
|
||
let mut grupper: Vec<(String, usize, f64)> = Vec::new();
|
||
for (navn, areal, _) in &utelatt {
|
||
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||
match grupper.iter_mut().find(|(n, _, _)| *n == kort) {
|
||
Some((_, antall, sum)) => {
|
||
*antall += 1;
|
||
*sum += areal;
|
||
}
|
||
None => grupper.push((kort, 1, *areal)),
|
||
}
|
||
}
|
||
grupper.sort_by(|a, b| b.2.total_cmp(&a.2));
|
||
let liste: Vec<String> = grupper
|
||
.iter()
|
||
.take(10)
|
||
.map(|(navn, antall, sum)| {
|
||
if *antall > 1 {
|
||
format!("{navn} ×{antall} ({sum:.0} m²)")
|
||
} else {
|
||
format!("{navn} ({sum:.0} m²)")
|
||
}
|
||
})
|
||
.collect();
|
||
merknader.push(format!(
|
||
"{} bygningsdeler er ikke regnet med i klimaskjermen, som innervegger og dekker inne i bygningen: {}{}.",
|
||
utelatt.len(),
|
||
liste.join(", "),
|
||
if grupper.len() > liste.len() { " …" } else { "" }
|
||
));
|
||
}
|
||
if dubletter + vindusdubletter > 0 {
|
||
merknader.push(format!(
|
||
"{} bygningsdeler ligger dobbelt i modellen, samme konstruksjon på samme sted, og er telt én gang. Det skjer når eksporten legger de samme delene i flere etasjer.",
|
||
dubletter + vindusdubletter
|
||
));
|
||
}
|
||
if !uten_uverdi.is_empty() {
|
||
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
|
||
// konstruksjon nevnes én gang, med antallet.
|
||
let mut antall: Vec<(String, usize)> = Vec::new();
|
||
for navn in &uten_uverdi {
|
||
let kort = navn.split(" #").next().unwrap_or(navn).trim().to_string();
|
||
match antall.iter_mut().find(|(n, _)| *n == kort) {
|
||
Some((_, a)) => *a += 1,
|
||
None => antall.push((kort, 1)),
|
||
}
|
||
}
|
||
let liste: Vec<String> = antall
|
||
.iter()
|
||
.map(|(n, a)| {
|
||
if *a > 1 {
|
||
format!("{n} ×{a}")
|
||
} else {
|
||
n.clone()
|
||
}
|
||
})
|
||
.collect();
|
||
merknader.push(format!(
|
||
"{} bygningsdeler mangler U-verdi i IFC-filen: {}. Legg inn U-verdier i modellen, eller la enok bruke typiske verdier for byggeåret.",
|
||
uten_uverdi.len(),
|
||
liste.join(", ")
|
||
));
|
||
}
|
||
merknader.push(
|
||
"Arealene er regnet av geometrien: trekantene er gruppert etter retning, og den største gruppen er delens hovedflate. Himmelretningene følger flatenormalen. Kontroller begge 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 (data, funnet) = les_byggdata(&fil, &egenskaper);
|
||
if !funnet.is_empty() {
|
||
merknader.push(format!(
|
||
"Modellen har egenskaper for drift og forsyning, og disse er tatt i bruk: {}.",
|
||
funnet.join(", ")
|
||
));
|
||
} else {
|
||
merknader.push(
|
||
"Modellen har ingen egenskaper for byggeår, tetthet, ventilasjon eller varmekilde. \
|
||
Festes et postformat med slike felter på bygningen i BIM-verktøyet og knyttes til et \
|
||
IFC-egenskapssett, kommer de med hit. Se «Egenskaper enok leser» i enoks dokumentasjon."
|
||
.into(),
|
||
);
|
||
}
|
||
|
||
let ubrukelig = |s: &str| {
|
||
let s = s.trim();
|
||
s.is_empty() || s == "Default Building" || s == "Vectorworks Project"
|
||
};
|
||
// Bygningens navn, ellers prosjektets langnavn eller navn, ellers filnavnet.
|
||
let navn = fil
|
||
.av_type("IFCBUILDING")
|
||
.next()
|
||
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
||
.filter(|s| !ubrukelig(s))
|
||
.or_else(|| {
|
||
fil.av_type("IFCPROJECT").next().and_then(|(_, p)| {
|
||
[p.arg(5), p.arg(2)]
|
||
.into_iter()
|
||
.flatten()
|
||
.filter_map(|a| a.som_tekst())
|
||
.find(|s| !ubrukelig(s))
|
||
.map(|s| s.trim().to_string())
|
||
})
|
||
})
|
||
.unwrap_or_else(|| standardnavn.to_string());
|
||
|
||
// Vernede bygninger er unntatt plikten til å ha energiattest.
|
||
let vernet = fil
|
||
.av_type("IFCBUILDING")
|
||
.next()
|
||
.and_then(|(r, _)| egenskaper.get(&r))
|
||
.and_then(|e| e.get("IsLandmarked"))
|
||
.map(|a| matches!(a.som_tekst(), Some("T") | Some("TRUE") | Some("true")))
|
||
.unwrap_or(false);
|
||
if vernet {
|
||
merknader.push(
|
||
"Bygningen er merket som vernet i modellen. Vernede bygninger etter kulturminneloven og \
|
||
plan- og bygningsloven er unntatt plikten til å ha energiattest, men beregningen kan \
|
||
fortsatt brukes til å vurdere tiltak."
|
||
.into(),
|
||
);
|
||
}
|
||
|
||
// Kjellersonen settes sammen av flatene i etasjen under terreng.
|
||
let mut soner: Vec<Sone> = Vec::new();
|
||
if let Some(i) = kjelleretasje {
|
||
if !kjellerflater.is_empty() {
|
||
let volum = if kjellervolum > 0.0 {
|
||
kjellervolum
|
||
} else {
|
||
kjellerareal * kjellerdybde.max(2.0)
|
||
};
|
||
let rom: Vec<String> = kjellerrom
|
||
.iter()
|
||
.map(|(n, a)| format!("{n} {a:.1}"))
|
||
.collect();
|
||
soner.push(Sone {
|
||
navn: kjellernavn.clone(),
|
||
volum: (volum > 0.0).then_some(volum),
|
||
luftveksling: None,
|
||
flater: kjellerflater,
|
||
andel_distribusjonstap: None,
|
||
andel_tappevannstap: None,
|
||
fast_effekt: 0.0,
|
||
kommentar: Some(format!(
|
||
"Etasje «{}» ligger {:.2} m under referansenivået. Rom [m²]: {}",
|
||
etasjer[i].navn,
|
||
-etasjer[i].hoyde,
|
||
rom.join(", ")
|
||
)),
|
||
});
|
||
let grunnmur = if !grunnmurshoyder.is_empty() && !terrengmodell.is_empty() {
|
||
let laveste = grunnmurshoyder
|
||
.iter()
|
||
.copied()
|
||
.fold(f64::INFINITY, f64::min);
|
||
let hoyeste = grunnmurshoyder
|
||
.iter()
|
||
.copied()
|
||
.fold(f64::NEG_INFINITY, f64::max);
|
||
let snitt = grunnmurshoyder.iter().sum::<f64>() / grunnmurshoyder.len() as f64;
|
||
format!(
|
||
"Terrengmodellen i filen gir hver kjellervegg sin egen høyde over bakken: \
|
||
{laveste:.2}–{hoyeste:.2} m, i snitt {snitt:.2} m. Resten av veggen er regnet nedgravd."
|
||
)
|
||
} else if terreng_over_gulvet {
|
||
format!(
|
||
"Terrengmodellen ligger over gulvet i første etasje, så den er ikke brukt. \
|
||
Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Legg \
|
||
terrengmodellen i bakkenivå, eller sett `over_terreng` i meter på veggene."
|
||
)
|
||
} else if terreng.is_some() {
|
||
format!(
|
||
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
||
{:.2} m under.",
|
||
kjellerdybde - kjeller_over_terreng
|
||
)
|
||
} else {
|
||
format!(
|
||
"Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Står en del av \
|
||
grunnmuren over bakken, sett `over_terreng` i meter på veggene, eller sett terrenghøyden \
|
||
på bygningen i modellen. Utslaget er lite, om lag én prosent per meter."
|
||
)
|
||
};
|
||
merknader.push(format!(
|
||
"Etasjen «{}» ligger under terreng og er satt opp som en uoppvarmet sone med {:.0} m² gulv. \
|
||
{grunnmur}",
|
||
etasjer[i].navn, kjellerareal
|
||
));
|
||
merknader.push("Er kjelleren oppvarmet likevel, slett [[sone]] og legg flatene inn i klimaskjermen. \
|
||
Går det uisolerte varmerør der, sett `andel_distribusjonstap = 1.0` på sonen.".to_string());
|
||
}
|
||
}
|
||
|
||
let klimaposisjon = match posisjon(&fil) {
|
||
Some(p) if p.er_standardplassering() => {
|
||
merknader.push(
|
||
"Modellen står på Vectorworks' standardplassering i Washington, så posisjonen er ikke \
|
||
brukt. Georefereres tegningen eller settes prosjektstedet, velger beregningen \
|
||
klimasted ut fra den."
|
||
.into(),
|
||
);
|
||
None
|
||
}
|
||
Some(p) if !p.i_norge() => {
|
||
merknader.push(format!(
|
||
"Modellen oppgir posisjon {:.4}° nord, {:.4}° øst, som ligger utenfor Norge. \
|
||
Posisjonen er ikke brukt til å velge klimasted.",
|
||
p.bredde, p.lengde
|
||
));
|
||
None
|
||
}
|
||
Some(p) => {
|
||
merknader.push(format!(
|
||
"Modellen oppgir posisjon {:.4}° nord, {:.4}° øst. Uten kommune velger beregningen \
|
||
klimastedet nærmest denne.",
|
||
p.bredde, p.lengde
|
||
));
|
||
Some([p.bredde, p.lengde])
|
||
}
|
||
None => {
|
||
merknader.push(
|
||
"Modellen oppgir ingen posisjon. Georefereres tegningen eller settes prosjektstedet, \
|
||
velger beregningen klimasted ut fra den."
|
||
.into(),
|
||
);
|
||
None
|
||
}
|
||
};
|
||
|
||
vinduer.sort_by(|a, b| a.navn.cmp(&b.navn));
|
||
|
||
let bygning = Bygning {
|
||
bygning: BygningInfo {
|
||
navn,
|
||
kategori: data
|
||
.kategori
|
||
.as_deref()
|
||
.and_then(kategori_av_tekst)
|
||
.unwrap_or(Kategori::Smaahus),
|
||
byggeaar: data
|
||
.byggeaar
|
||
.unwrap_or(crate::modell::kontroll::IMPORTERT_BYGGEAAR),
|
||
oppvarmet_bra: data.oppvarmet_bra.unwrap_or(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: forfatter(&fil),
|
||
oppdragsgiver: None,
|
||
},
|
||
klima: Klima {
|
||
kommune: data.kommune.clone(),
|
||
posisjon: klimaposisjon,
|
||
..Klima::default()
|
||
},
|
||
flate: flater,
|
||
sone: soner,
|
||
grunn: Grunn::default(),
|
||
vindu: vinduer,
|
||
kuldebroer: Kuldebroer::default(),
|
||
infiltrasjon: Infiltrasjon {
|
||
n50: data.n50,
|
||
..Infiltrasjon::default()
|
||
},
|
||
ventilasjon: Ventilasjon {
|
||
typ: data
|
||
.ventilasjon
|
||
.as_deref()
|
||
.and_then(ventilasjon_av_tekst)
|
||
.unwrap_or_default(),
|
||
gjenvinning: data
|
||
.gjenvinning
|
||
.map(|v| if v > 1.0 { v / 100.0 } else { v }),
|
||
..Ventilasjon::default()
|
||
},
|
||
tappevann: Tappevann::default(),
|
||
oppvarming: Oppvarming {
|
||
kilde: data
|
||
.varmekilde
|
||
.as_deref()
|
||
.and_then(varmekilde_av_tekst)
|
||
.map(|t| {
|
||
vec![Varmekilde {
|
||
typ: t,
|
||
andel_rom: 1.0,
|
||
andel_ventilasjon: None,
|
||
andel_tappevann: 1.0,
|
||
spf: None,
|
||
turtemperatur: None,
|
||
lukket: true,
|
||
kommentar: Some("fra egenskap i IFC-modellen".into()),
|
||
}]
|
||
})
|
||
.unwrap_or_default(),
|
||
..Oppvarming::default()
|
||
},
|
||
distribusjon: Distribusjon::default(),
|
||
solcelle: None,
|
||
kjoling: Kjoling::default(),
|
||
lufting: Lufting::default(),
|
||
drift: Drift::default(),
|
||
scenario: Vec::new(),
|
||
rapport: Rapport::default(),
|
||
};
|
||
Ok(Importrapport {
|
||
bygning,
|
||
merknader,
|
||
utelatt,
|
||
})
|
||
}
|
||
|
||
/// Slår sammen flater med samme konstruksjon, type og himmelretning til én oppføring.
|
||
///
|
||
/// En BIM-modell deler gjerne den samme veggen i mange elementer, ett per rom eller etasje.
|
||
/// For en energiberegning er det konstruksjonen og himmelretningen som teller, så oppføringene
|
||
/// summeres. Himmelretningen rundes til nærmeste 45 grader før sammenligningen.
|
||
fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||
let nokkel = |f: &Flate| {
|
||
let navn = f.navn.split(" #").next().unwrap_or(&f.navn).to_string();
|
||
let sektor = ((f.orientering / 45.0).round() as i64).rem_euclid(8);
|
||
let u = (f.u_verdi.unwrap_or(-1.0) * 100.0).round() as i64;
|
||
(navn, f.typ, f.mot, sektor, u)
|
||
};
|
||
let mut samlet: Vec<Flate> = Vec::new();
|
||
for f in flater.drain(..) {
|
||
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
|
||
Some(g) => {
|
||
// Arealvektet himmelretning og helning, slik at summen peker riktig vei og ikke
|
||
// avhenger av rekkefølgen elementene kommer i.
|
||
let sum = g.areal + f.areal;
|
||
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
||
if let (Some(a), Some(b)) = (g.helning, f.helning) {
|
||
g.helning = Some((a * g.areal + b * f.areal) / sum);
|
||
}
|
||
g.areal = sum;
|
||
}
|
||
None => samlet.push(f),
|
||
}
|
||
}
|
||
for f in &mut samlet {
|
||
f.navn = f
|
||
.navn
|
||
.split(" #")
|
||
.next()
|
||
.unwrap_or(&f.navn)
|
||
.trim()
|
||
.to_string();
|
||
f.orientering = (f.orientering / 45.0).round() * 45.0;
|
||
}
|
||
*flater = samlet;
|
||
}
|
||
|
||
/// Gjør flatenavnene entydige. Vegger får himmelretningen etter konstruksjonsnavnet, og andre
|
||
/// flater som fortsatt deler navn, nummereres.
|
||
fn unike_navn(flater: &mut [Flate]) {
|
||
// Fast rekkefølge, slik at samme modell alltid gir samme fil og samme nummerering.
|
||
flater.sort_by(|a, b| {
|
||
(a.typ as u8, &a.navn)
|
||
.cmp(&(b.typ as u8, &b.navn))
|
||
.then(a.orientering.total_cmp(&b.orientering))
|
||
.then(b.areal.total_cmp(&a.areal))
|
||
});
|
||
for f in flater.iter_mut().filter(|f| f.typ == FlateType::Vegg) {
|
||
f.navn = format!("{}, {}", f.navn, himmelretning(f.orientering));
|
||
}
|
||
let mut antall: HashMap<String, usize> = HashMap::new();
|
||
for f in flater.iter_mut() {
|
||
let n = antall.entry(f.navn.clone()).or_insert(0);
|
||
*n += 1;
|
||
if *n > 1 {
|
||
f.navn = format!("{} ({n})", f.navn);
|
||
}
|
||
}
|
||
}
|
||
|
||
fn himmelretning(grader: f64) -> &'static str {
|
||
const RETNINGER: [&str; 8] = [
|
||
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
|
||
];
|
||
RETNINGER[((grader / 45.0).round() as i64).rem_euclid(8) as usize]
|
||
}
|
||
|
||
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
|
||
#[derive(Clone, Debug)]
|
||
pub struct Etasje {
|
||
pub navn: String,
|
||
/// Høyde over byggets referansenivå, m. Negativ for kjeller.
|
||
pub hoyde: f64,
|
||
/// Ligger etasjen under terreng, slik navn eller høyde tyder på?
|
||
pub under_terreng: bool,
|
||
}
|
||
|
||
/// Leser etasjene og hvilken etasje hver bygningsdel og hvert rom hører til.
|
||
///
|
||
/// Etasjeinndelingen er den sikreste opplysningen en BIM-modell har om hva som er hva.
|
||
/// En etasje som heter «Kjeller» og ligger under referansenivået er nesten alltid
|
||
/// uoppvarmet, og bygningsdelene der hører til kjellerens klimaskjerm og ikke husets.
|
||
fn les_etasjer(fil: &Fil, skala: f64) -> (Vec<Etasje>, HashMap<u64, usize>) {
|
||
let mut etasjer: Vec<(u64, Etasje)> = fil
|
||
.av_type("IFCBUILDINGSTOREY")
|
||
.map(|(r, e)| {
|
||
let navn = e
|
||
.navn()
|
||
.filter(|s| !s.is_empty())
|
||
.unwrap_or("etasje")
|
||
.to_string();
|
||
let hoyde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||
let lavt = navn.to_lowercase();
|
||
let under_terreng = hoyde < -0.5
|
||
|| ["kjeller", "basement", "souterreng", "underetasje"]
|
||
.iter()
|
||
.any(|o| lavt.contains(o));
|
||
(
|
||
r,
|
||
Etasje {
|
||
navn,
|
||
hoyde,
|
||
under_terreng,
|
||
},
|
||
)
|
||
})
|
||
.collect();
|
||
etasjer.sort_by(|a, b| {
|
||
a.1.hoyde
|
||
.partial_cmp(&b.1.hoyde)
|
||
.unwrap_or(std::cmp::Ordering::Equal)
|
||
});
|
||
|
||
let indeks: HashMap<u64, usize> = etasjer
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(i, (r, _))| (*r, i))
|
||
.collect();
|
||
let mut for_element: HashMap<u64, usize> = HashMap::new();
|
||
|
||
// Bygningsdeler knyttes til etasjen med IfcRelContainedInSpatialStructure.
|
||
for (_, rel) in fil.av_type("IFCRELCONTAINEDINSPATIALSTRUCTURE") {
|
||
let Some(etasje) = rel
|
||
.arg(5)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|r| indeks.get(&r))
|
||
else {
|
||
continue;
|
||
};
|
||
for e in rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||
if let Some(r) = e.som_ref() {
|
||
for_element.insert(r, *etasje);
|
||
}
|
||
}
|
||
}
|
||
// Rom knyttes til etasjen med IfcRelAggregates.
|
||
for (_, rel) in fil.av_type("IFCRELAGGREGATES") {
|
||
let Some(etasje) = rel
|
||
.arg(4)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|r| indeks.get(&r))
|
||
else {
|
||
continue;
|
||
};
|
||
for e in rel.arg(5).map(|a| a.som_liste()).unwrap_or(&[]) {
|
||
if let Some(r) = e.som_ref() {
|
||
for_element.insert(r, *etasje);
|
||
}
|
||
}
|
||
}
|
||
(etasjer.into_iter().map(|(_, e)| e).collect(), for_element)
|
||
}
|
||
|
||
/// Masseklasse ut fra materialnavnene i konstruksjonen.
|
||
///
|
||
/// IFC-filer fra Vectorworks oppgir materialene som andeler uten tykkelse, så U-verdien
|
||
/// kan ikke regnes av dem. Navnene sier likevel nok om massen: betong og tegl er tungt,
|
||
/// bindingsverk og isolasjon er lett.
|
||
fn masseklasse_av_materialer(fil: &Fil, element: u64) -> Option<Masseklasse> {
|
||
let mut navn: Vec<String> = Vec::new();
|
||
for (_, rel) in fil.av_type("IFCRELASSOCIATESMATERIAL") {
|
||
let treff = rel
|
||
.arg(4)
|
||
.map(|a| a.som_liste())
|
||
.unwrap_or(&[])
|
||
.iter()
|
||
.any(|o| o.som_ref() == Some(element));
|
||
if !treff {
|
||
continue;
|
||
}
|
||
let Some(m) = rel
|
||
.arg(5)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|r| fil.hent(r))
|
||
else {
|
||
continue;
|
||
};
|
||
samle_materialnavn(fil, m, &mut navn, 0);
|
||
}
|
||
if navn.is_empty() {
|
||
return None;
|
||
}
|
||
let tekst = navn.join(" ").to_lowercase();
|
||
let tungt = [
|
||
"concrete", "betong", "brick", "tegl", "masonry", "mur", "stone", "stein", "screed",
|
||
];
|
||
let lett = [
|
||
"insulat",
|
||
"isolasjon",
|
||
"timber",
|
||
"wood",
|
||
"tre",
|
||
"panel",
|
||
"gypsum",
|
||
"gips",
|
||
"air",
|
||
];
|
||
let t = tungt.iter().filter(|o| tekst.contains(**o)).count();
|
||
let l = lett.iter().filter(|o| tekst.contains(**o)).count();
|
||
Some(match (t, l) {
|
||
(0, 0) => return None,
|
||
(t, l) if t > l => Masseklasse::Tung,
|
||
(t, l) if t == l && t > 0 => Masseklasse::Middels,
|
||
_ => Masseklasse::Lett,
|
||
})
|
||
}
|
||
|
||
fn samle_materialnavn(fil: &Fil, e: &Entitet, ut: &mut Vec<String>, dybde: u32) {
|
||
if dybde > 4 {
|
||
return;
|
||
}
|
||
if e.typ == "IFCMATERIAL" {
|
||
if let Some(n) = e.arg(0).and_then(|a| a.som_tekst()) {
|
||
ut.push(n.to_string());
|
||
}
|
||
return;
|
||
}
|
||
for a in &e.args {
|
||
match a {
|
||
Arg::Ref(r) => {
|
||
if let Some(n) = fil.hent(*r) {
|
||
samle_materialnavn(fil, n, ut, dybde + 1);
|
||
}
|
||
}
|
||
Arg::Liste(l) => {
|
||
for x in l {
|
||
if let Some(n) = x.som_ref().and_then(|r| fil.hent(r)) {
|
||
samle_materialnavn(fil, n, ut, dybde + 1);
|
||
}
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Opplysninger om drift og forsyning som IFC ikke har noen fast plass til.
|
||
///
|
||
/// IFC beskriver geometri og konstruksjon, men ikke byggeår, tetthet, ventilasjon eller
|
||
/// hvordan huset varmes opp. Det er nettopp de opplysningene som skiller en god
|
||
/// energiberegning fra en middelmådig. De kan likevel følge med modellen: i Vectorworks
|
||
/// lager man et postformat, fester det på bygningen og knytter det til et IFC-egenskapssett.
|
||
/// Feltnavnene under leses fra et hvilket som helst egenskapssett på `IfcBuilding`,
|
||
/// `IfcSite` eller `IfcProject`, på norsk eller engelsk.
|
||
#[derive(Default, Debug)]
|
||
pub struct Byggdata {
|
||
pub byggeaar: Option<i32>,
|
||
pub kategori: Option<String>,
|
||
pub kommune: Option<String>,
|
||
pub n50: Option<f64>,
|
||
pub ventilasjon: Option<String>,
|
||
pub gjenvinning: Option<f64>,
|
||
pub varmekilde: Option<String>,
|
||
pub oppvarmet_bra: Option<f64>,
|
||
}
|
||
|
||
/// Leser `Byggdata` fra egenskapssettene på bygningen, tomta eller prosjektet.
|
||
fn les_byggdata(fil: &Fil, egenskaper: &Egenskaper) -> (Byggdata, Vec<String>) {
|
||
let mut d = Byggdata::default();
|
||
let mut funnet = Vec::new();
|
||
let baerere: Vec<u64> = ["IFCBUILDING", "IFCSITE", "IFCPROJECT"]
|
||
.iter()
|
||
.flat_map(|t| fil.av_type(t).map(|(r, _)| r).collect::<Vec<_>>())
|
||
.collect();
|
||
|
||
let mut hent = |navn: &[&str]| -> Option<Arg> {
|
||
for b in &baerere {
|
||
let Some(sett) = egenskaper.get(b) else {
|
||
continue;
|
||
};
|
||
for n in navn {
|
||
if let Some((nokkel, v)) = sett.iter().find(|(k, _)| {
|
||
k.eq_ignore_ascii_case(n) || k.replace(' ', "").eq_ignore_ascii_case(n)
|
||
}) {
|
||
funnet.push(nokkel.clone());
|
||
return Some(v.clone());
|
||
}
|
||
}
|
||
}
|
||
None
|
||
};
|
||
|
||
let tall = |a: Option<Arg>| -> Option<f64> {
|
||
let a = a?;
|
||
a.som_tall().or_else(|| {
|
||
a.som_tekst()
|
||
.and_then(|s| s.replace(',', ".").trim().parse().ok())
|
||
})
|
||
};
|
||
let tekst = |a: Option<Arg>| -> Option<String> {
|
||
a.and_then(|x| x.som_tekst().map(|s| s.trim().to_string()))
|
||
.filter(|s| !s.is_empty())
|
||
};
|
||
|
||
d.byggeaar = tall(hent(&[
|
||
"Byggeaar",
|
||
"Byggeår",
|
||
"YearOfConstruction",
|
||
"ConstructionYear",
|
||
]))
|
||
.map(|v| v as i32);
|
||
d.kategori = tekst(hent(&["Bygningskategori", "Kategori", "BuildingCategory"]));
|
||
d.kommune = tekst(hent(&["Kommune", "Municipality", "Klimasted"]));
|
||
d.n50 = tall(hent(&[
|
||
"Lekkasjetall",
|
||
"n50",
|
||
"AirPermeability",
|
||
"AirTightness",
|
||
]));
|
||
d.ventilasjon = tekst(hent(&[
|
||
"Ventilasjonstype",
|
||
"Ventilasjon",
|
||
"VentilationType",
|
||
]));
|
||
d.gjenvinning = tall(hent(&[
|
||
"Varmegjenvinning",
|
||
"Gjenvinning",
|
||
"HeatRecoveryEfficiency",
|
||
"TemperaturVirkningsgrad",
|
||
]));
|
||
d.varmekilde = tekst(hent(&[
|
||
"Varmekilde",
|
||
"Oppvarming",
|
||
"HeatingSource",
|
||
"HeatSource",
|
||
]));
|
||
d.oppvarmet_bra = tall(hent(&["OppvarmetBRA", "Oppvarmet BRA", "HeatedFloorArea"]));
|
||
(d, funnet)
|
||
}
|
||
|
||
/// Byggets posisjon.
|
||
///
|
||
/// Georeferering gjennom `IfcMapConversion` og `IfcProjectedCRS` brukes først, siden den er
|
||
/// mest presis. Ellers brukes breddegrad og lengdegrad på `IfcSite`. Vectorworks fyller begge
|
||
/// med en standardplassering i Washington når ingenting er satt, og den velges bare når ingen
|
||
/// annen posisjon finnes, slik at den kan kjennes igjen og oversees.
|
||
fn posisjon(fil: &Fil) -> Option<geo::Posisjon> {
|
||
let fra_kart = fil.av_type("IFCMAPCONVERSION").next().and_then(|(_, m)| {
|
||
let crs = m
|
||
.arg(1)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|r| fil.hent(r))?;
|
||
let navn = crs.arg(0).and_then(|a| a.som_tekst())?;
|
||
let x = m.arg(2).and_then(|a| a.som_tall())?;
|
||
let y = m.arg(3).and_then(|a| a.som_tall())?;
|
||
geo::fra_koordinatsystem(navn, x, y)
|
||
});
|
||
let fra_tomt = fil.av_type("IFCSITE").next().and_then(|(_, e)| {
|
||
let grad = |a: Option<&Arg>| -> Option<f64> {
|
||
let l = a?.som_liste();
|
||
if l.is_empty() {
|
||
return None;
|
||
}
|
||
let g = |i: usize| l.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||
let verdi = g(0).abs() + g(1).abs() / 60.0 + g(2).abs() / 3600.0 + g(3).abs() / 3.6e9;
|
||
Some(if g(0) < 0.0 { -verdi } else { verdi })
|
||
};
|
||
Some(geo::Posisjon {
|
||
bredde: grad(e.arg(9))?,
|
||
lengde: grad(e.arg(10))?,
|
||
})
|
||
});
|
||
[fra_kart, fra_tomt]
|
||
.into_iter()
|
||
.flatten()
|
||
.find(|p| !p.er_standardplassering())
|
||
.or(fra_kart)
|
||
.or(fra_tomt)
|
||
}
|
||
|
||
/// Referansehøyden og terrenghøyden på bygningen, begge over havet, m.
|
||
fn terrenghoyder(fil: &Fil, skala: f64) -> Option<(f64, f64)> {
|
||
let (_, e) = fil.av_type("IFCBUILDING").next()?;
|
||
let referanse = e.arg(9).and_then(|a| a.som_tall())? * skala;
|
||
let terreng = e.arg(10).and_then(|a| a.som_tall())? * skala;
|
||
Some((referanse, terreng))
|
||
}
|
||
|
||
/// Punktene i terrengmodellen, i byggets koordinater, m.
|
||
///
|
||
/// Vectorworks legger terrenget som et `IfcGeographicElement` med typen TERRAIN, eller på
|
||
/// tomta. Punktene brukes til å finne bakkenivået ved hver enkelt kjellervegg.
|
||
fn terrengpunkter(fil: &Fil, skala: f64) -> Vec<[f64; 3]> {
|
||
let mut ut = Vec::new();
|
||
for (_, e) in fil
|
||
.av_type("IFCGEOGRAPHICELEMENT")
|
||
.chain(fil.av_type("IFCSITE"))
|
||
{
|
||
let (px, py, pz) = e
|
||
.arg(5)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|p| global_plassering(fil, p, skala))
|
||
.map(|(x, y, z, _, _)| (x, y, z))
|
||
.unwrap_or((0.0, 0.0, 0.0));
|
||
for nett in ifcgeom::netter_for_produkt(fil, e.arg(6).and_then(|a| a.som_ref())) {
|
||
for p in &nett.punkter {
|
||
ut.push([p[0] * skala + px, p[1] * skala + py, p[2] * skala + pz]);
|
||
}
|
||
}
|
||
}
|
||
ut
|
||
}
|
||
|
||
/// Bakkenivået ved et punkt, som snittet av de nærmeste punktene i terrengmodellen, m.
|
||
fn terreng_ved(punkter: &[[f64; 3]], x: f64, y: f64) -> Option<f64> {
|
||
if punkter.is_empty() {
|
||
return None;
|
||
}
|
||
let mut avstand: Vec<(f64, f64)> = punkter
|
||
.iter()
|
||
.map(|p| ((p[0] - x).powi(2) + (p[1] - y).powi(2), p[2]))
|
||
.collect();
|
||
avstand.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
let antall = avstand.len().min(8);
|
||
Some(avstand[..antall].iter().map(|(_, z)| z).sum::<f64>() / antall as f64)
|
||
}
|
||
|
||
/// Hvor mye av grunnmuren som står over bakken, m.
|
||
///
|
||
/// Toppen av grunnmuren ligger ved gulvet i første etasje over kjelleren. Den står over
|
||
/// terrenget med differansen mellom etasjens høyde over havet og terrengets, begrenset til
|
||
/// kjellerens høyde.
|
||
fn over_terreng_for_kjeller(
|
||
referanse: f64,
|
||
terreng: f64,
|
||
forste_etasje: f64,
|
||
kjellerdybde: f64,
|
||
) -> f64 {
|
||
((referanse + forste_etasje) - terreng).clamp(0.0, kjellerdybde.max(0.0))
|
||
}
|
||
|
||
/// En bygningsdel slik den er lest av IFC-filen.
|
||
struct Del {
|
||
nr: u64,
|
||
typ: String,
|
||
navn: String,
|
||
etasje: Option<usize>,
|
||
ytre: Option<bool>,
|
||
u_verdi: Option<f64>,
|
||
plass: Option<(f64, f64, f64, f64, f64)>,
|
||
areal: f64,
|
||
normal: Option<[f64; 3]>,
|
||
/// Delens høyde over byggets nullpunkt, av geometrien, m.
|
||
z: f64,
|
||
/// Overkanten av delen over byggets nullpunkt, m. For en kjellervegg er det overkant mur.
|
||
topp: f64,
|
||
}
|
||
|
||
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
||
fn kategori_av_tekst(s: &str) -> Option<Kategori> {
|
||
let t = s.to_lowercase();
|
||
Some(match t.as_str() {
|
||
x if x.contains("småhus") || x.contains("smaahus") || x.contains("enebolig") => {
|
||
Kategori::Smaahus
|
||
}
|
||
x if x.contains("boligblokk") || x.contains("leilighet") => Kategori::Boligblokk,
|
||
x if x.contains("kontor") => Kategori::Kontorbygg,
|
||
x if x.contains("skole") => Kategori::Skolebygg,
|
||
x if x.contains("barnehage") => Kategori::Barnehage,
|
||
x if x.contains("hotell") => Kategori::Hotell,
|
||
x if x.contains("sykehus") => Kategori::Sykehus,
|
||
x if x.contains("sykehjem") => Kategori::Sykehjem,
|
||
x if x.contains("idrett") => Kategori::Idrettsbygg,
|
||
x if x.contains("forretning") || x.contains("butikk") => Kategori::Forretningsbygg,
|
||
x if x.contains("kultur") => Kategori::Kulturbygg,
|
||
x if x.contains("industri") || x.contains("verksted") => Kategori::Industribygg,
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
fn ventilasjon_av_tekst(s: &str) -> Option<VentilasjonType> {
|
||
let t = s.to_lowercase();
|
||
Some(match t.as_str() {
|
||
x if x.contains("balansert") || x.contains("balanced") || x.contains("mekanisk til") => {
|
||
VentilasjonType::Balansert
|
||
}
|
||
x if x.contains("avtrekk") || x.contains("exhaust") => VentilasjonType::Avtrekk,
|
||
x if x.contains("naturlig") || x.contains("natural") => VentilasjonType::Naturlig,
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
fn varmekilde_av_tekst(s: &str) -> Option<VarmekildeType> {
|
||
let t = s.to_lowercase();
|
||
Some(match t.as_str() {
|
||
x if x.contains("luft-luft") || x.contains("luft/luft") || x.contains("air-air") => {
|
||
VarmekildeType::LuftLuftVp
|
||
}
|
||
x if x.contains("luft-vann") || x.contains("luft/vann") => VarmekildeType::LuftVannVp,
|
||
x if x.contains("berg")
|
||
|| x.contains("væske")
|
||
|| x.contains("vaeske")
|
||
|| x.contains("ground") =>
|
||
{
|
||
VarmekildeType::VaeskeVannVp
|
||
}
|
||
x if x.contains("avtrekksvarmepumpe") => VarmekildeType::AvtrekkVp,
|
||
x if x.contains("vedovn") || x.contains("ved") || x.contains("wood") => {
|
||
VarmekildeType::Vedovn
|
||
}
|
||
x if x.contains("pellet") => VarmekildeType::Pellets,
|
||
x if x.contains("fjernvarme") || x.contains("district") => VarmekildeType::Fjernvarme,
|
||
x if x.contains("olje") => VarmekildeType::Olje,
|
||
x if x.contains("gass") || x.contains("gas") => VarmekildeType::Gass,
|
||
x if x.contains("elkjel") || x.contains("elektrokjel") || x.contains("kjel") => {
|
||
VarmekildeType::ElKjel
|
||
}
|
||
x if x.contains("elektr") || x.contains("panelovn") || x.contains("electric") => {
|
||
VarmekildeType::DirekteEl
|
||
}
|
||
_ => return None,
|
||
})
|
||
}
|
||
|
||
fn standardnavn_for(typ: &str) -> &'static str {
|
||
if typ == "IFCROOF" {
|
||
"Tak"
|
||
} else if PLATETYPER.contains(&typ) {
|
||
"Dekke"
|
||
} else {
|
||
"Vegg"
|
||
}
|
||
}
|
||
|
||
fn romnavn(e: &Entitet) -> String {
|
||
e.arg(7)
|
||
.and_then(|a| a.som_tekst())
|
||
.filter(|s| !s.is_empty())
|
||
.or_else(|| e.arg(2).and_then(|a| a.som_tekst()))
|
||
.unwrap_or("rom")
|
||
.to_string()
|
||
}
|
||
|
||
/// Arealet fra en mengdesamling. Godtar både standardnavnene og verktøyspesifikke navn.
|
||
fn arealmengde(m: &HashMap<String, f64>) -> f64 {
|
||
for navn in [
|
||
"NetFloorArea",
|
||
"GrossFloorArea",
|
||
"NetArea",
|
||
"GrossArea",
|
||
"Area",
|
||
] {
|
||
if let Some(v) = m.get(navn) {
|
||
return *v;
|
||
}
|
||
}
|
||
// Ellers: et hvilket som helst areal, slik at verktøyspesifikke navn som
|
||
// «GSA BIM Area» også virker.
|
||
m.iter()
|
||
.filter(|(k, _)| k.to_lowercase().contains("area"))
|
||
.map(|(_, v)| *v)
|
||
.fold(0.0, f64::max)
|
||
}
|
||
|
||
/// Himmelretning og helning ut fra hovedflatens normal.
|
||
fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||
let Some(n) = d.normal else {
|
||
return (
|
||
0.0,
|
||
if PLATETYPER.contains(&d.typ.as_str()) {
|
||
0.0
|
||
} else {
|
||
90.0
|
||
},
|
||
);
|
||
};
|
||
// Normalen er gitt i lokale koordinater; roter den med delens plassering.
|
||
let (dx, dy) = d.plass.map(|p| (p.3, p.4)).unwrap_or((1.0, 0.0));
|
||
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]];
|
||
|
||
let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
|
||
// Et skrått tak har også en vannrett komponent, men skal alltid ha normalen oppover;
|
||
// ellers kan undersiden bli hovedflaten og gi helning 180° minus takvinkelen.
|
||
let skraa_plate = PLATETYPER.contains(&d.typ.as_str()) && g[2].abs() > 0.1;
|
||
if vannrett > 0.2 && !skraa_plate {
|
||
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
|
||
if let Some((x, y, _, _, _)) = d.plass {
|
||
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
|
||
g = [-g[0], -g[1], -g[2]];
|
||
}
|
||
}
|
||
} else if g[2] < 0.0 {
|
||
// Vannrett flate: normalen oppover gir helning 0, nedover 180.
|
||
g = [-g[0], -g[1], -g[2]];
|
||
}
|
||
let helning = g[2].clamp(-1.0, 1.0).acos().to_degrees();
|
||
let asimut = (g[0].atan2(g[1]).to_degrees() + sant_nord).rem_euclid(360.0);
|
||
(asimut, helning)
|
||
}
|
||
|
||
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
|
||
/// Etasjeinndelingen som klassifiseringen trenger.
|
||
struct Etasjekontekst {
|
||
/// Etasjen under terreng, dersom modellen har en.
|
||
kjeller: Option<usize>,
|
||
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
||
forste_over: Option<usize>,
|
||
/// Høyden på den etasjen, m. Geometrien veier tyngre enn etasjetilknytningen, som kan
|
||
/// være feil i eksporten.
|
||
forste_over_hoyde: Option<f64>,
|
||
}
|
||
|
||
fn klassifiser(
|
||
d: &Del,
|
||
laveste: f64,
|
||
helning: f64,
|
||
e: &Etasjekontekst,
|
||
) -> Option<(FlateType, Mot)> {
|
||
let navn = d.navn.to_lowercase();
|
||
let grunnord = [
|
||
"kjellermur",
|
||
"grunnmur",
|
||
"ringmur",
|
||
"såle",
|
||
"sale",
|
||
"fundament",
|
||
];
|
||
let takord = ["tak", "roof", "loft"];
|
||
|
||
if PLATETYPER.contains(&d.typ.as_str()) {
|
||
if d.typ == "IFCROOF" || takord.iter().any(|o| navn.contains(o)) {
|
||
return Some((
|
||
FlateType::Tak,
|
||
if navn.contains("loft") {
|
||
Mot::Uoppvarmet
|
||
} else {
|
||
Mot::Ute
|
||
},
|
||
));
|
||
}
|
||
// Et utkraget dekke eller balkonggulv er merket som ytre og vender mot det fri.
|
||
if d.ytre == Some(true) {
|
||
return Some((FlateType::Gulv, Mot::Ute));
|
||
}
|
||
// Høyden av geometrien avgjør, siden etasjetilknytningen i en eksport kan være feil.
|
||
// Et dekke som ligger på bakken sier det i navnet. Ellers er et dekke like under
|
||
// gulvet i første etasje himlingen over kjelleren, og det nederste dekket ligger mot
|
||
// grunnen. «Kjellergulv» er ikke nok til å si at dekket ligger på bakken: dekket over
|
||
// kjelleren bærer ofte det samme navnet.
|
||
let paa_grunnen = ["on grade", "på grunn", "paa grunn", "såle", "sale"];
|
||
if paa_grunnen.iter().any(|o| navn.contains(o)) {
|
||
return Some((FlateType::Gulv, Mot::Grunn));
|
||
}
|
||
if e.kjeller.is_some() {
|
||
let ved_forste_over = e
|
||
.forste_over_hoyde
|
||
.map(|h| (d.z - h).abs() < 0.6)
|
||
.unwrap_or(false);
|
||
if ved_forste_over || (d.etasje.is_some() && d.etasje == e.forste_over) {
|
||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
||
}
|
||
}
|
||
if (d.z - laveste).abs() < 0.5 {
|
||
return Some((FlateType::Gulv, Mot::Grunn));
|
||
}
|
||
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
||
return None;
|
||
}
|
||
|
||
// Vegger.
|
||
if d.ytre == Some(true) {
|
||
return Some((FlateType::Vegg, Mot::Ute));
|
||
}
|
||
if grunnord.iter().any(|o| navn.contains(o)) {
|
||
return Some((FlateType::Vegg, Mot::Grunn));
|
||
}
|
||
if d.ytre.is_none() && helning > 60.0 {
|
||
// Uten `IsExternal` må vi anta at veggen er ytre.
|
||
return Some((FlateType::Vegg, Mot::Ute));
|
||
}
|
||
None
|
||
}
|
||
|
||
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}"),
|
||
}
|
||
}
|
||
|
||
/// Byggets adresse fra `IfcBuilding.BuildingAddress`, eller fra tomta.
|
||
///
|
||
/// Adresser knyttet til personer og organisasjoner er forfatterens, ikke byggets, og brukes
|
||
/// ikke. Plassholdere uten gate eller poststed, som Vectorworks setter inn, hoppes over.
|
||
fn adresse(fil: &Fil) -> Option<String> {
|
||
let bygning = fil
|
||
.av_type("IFCBUILDING")
|
||
.filter_map(|(_, e)| e.arg(11).and_then(|a| a.som_ref()));
|
||
let tomt = fil
|
||
.av_type("IFCSITE")
|
||
.filter_map(|(_, e)| e.arg(13).and_then(|a| a.som_ref()));
|
||
bygning
|
||
.chain(tomt)
|
||
.filter_map(|r| fil.hent(r))
|
||
.filter(|e| e.typ == "IFCPOSTALADDRESS")
|
||
.find_map(formater_adresse)
|
||
}
|
||
|
||
/// `IfcPostalAddress` har feltene Purpose, Description, UserDefinedPurpose, InternalLocation,
|
||
/// AddressLines, PostalBox, Town, Region, PostalCode og Country.
|
||
fn formater_adresse(e: &Entitet) -> Option<String> {
|
||
let linjer: Vec<String> = e
|
||
.arg(4)
|
||
.map(|a| a.som_liste())
|
||
.unwrap_or(&[])
|
||
.iter()
|
||
.filter_map(|a| a.som_tekst())
|
||
.map(|s| s.trim().to_string())
|
||
.filter(|s| !s.is_empty())
|
||
.collect();
|
||
let postnummer = e.arg(8).and_then(|a| a.som_tekst()).unwrap_or("").trim();
|
||
let sted = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("").trim();
|
||
if linjer.is_empty() && sted.is_empty() {
|
||
return None;
|
||
}
|
||
let poststed: Vec<&str> = [postnummer, sted]
|
||
.into_iter()
|
||
.filter(|s| !s.is_empty())
|
||
.collect();
|
||
Some(
|
||
linjer
|
||
.into_iter()
|
||
.chain((!poststed.is_empty()).then(|| poststed.join(" ")))
|
||
.collect::<Vec<_>>()
|
||
.join(", "),
|
||
)
|
||
}
|
||
|
||
/// Forfatteren av modellen, som person og organisasjon.
|
||
///
|
||
/// I en modell laget for energirådgivning er det energirådgiveren. Vectorworks' egen
|
||
/// organisasjon, som står når ingenting er fylt inn, tas ikke med.
|
||
fn forfatter(fil: &Fil) -> Option<String> {
|
||
let (_, po) = fil.av_type("IFCPERSONANDORGANIZATION").next()?;
|
||
let person = po
|
||
.arg(0)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|r| fil.hent(r));
|
||
let organisasjon = po
|
||
.arg(1)
|
||
.and_then(|a| a.som_ref())
|
||
.and_then(|r| fil.hent(r));
|
||
// IfcPerson: Identification, FamilyName, GivenName, …
|
||
let personnavn = person
|
||
.map(|p| {
|
||
[p.arg(2), p.arg(1)]
|
||
.into_iter()
|
||
.flatten()
|
||
.filter_map(|a| a.som_tekst())
|
||
.map(str::trim)
|
||
.filter(|s| !s.is_empty())
|
||
.collect::<Vec<_>>()
|
||
.join(" ")
|
||
})
|
||
.filter(|s| !s.is_empty());
|
||
// IfcOrganization: Identification, Name, …
|
||
let organisasjonsnavn = organisasjon
|
||
.and_then(|o| o.arg(1).and_then(|a| a.som_tekst()))
|
||
.map(str::trim)
|
||
.filter(|s| !s.is_empty() && !s.to_lowercase().contains("vectorworks"))
|
||
.map(str::to_string);
|
||
match (personnavn, organisasjonsnavn) {
|
||
(Some(p), Some(o)) => Some(format!("{p}, {o}")),
|
||
(Some(p), None) => Some(p),
|
||
(None, Some(o)) => Some(o),
|
||
(None, None) => None,
|
||
}
|
||
}
|
||
|
||
/// 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),
|
||
))
|
||
}
|
||
|
||
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::*;
|
||
|
||
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
||
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
||
fn prove() -> String {
|
||
prove_med(&[(100, "Sorvegg", -5.0), (120, "Nordvegg", 5.0)])
|
||
}
|
||
|
||
/// Som `prove`, men med de veggene testen ber om: (entitetsnummer, navn, y i meter).
|
||
fn prove_med(vegger: &[(u64, &str, f64)]) -> String {
|
||
let mut s = String::from(
|
||
"ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n #1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n #2= IFCUNITASSIGNMENT((#1));\n #3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);\n #4= IFCDIRECTION((0.,1.));\n #40= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#4);\n #5= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);\n #10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);\n #20= IFCLOCALPLACEMENT($,#21);\n #21= IFCAXIS2PLACEMENT3D(#22,$,$);\n #22= IFCCARTESIANPOINT((0.,0.,0.));\n #200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);\n #201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);\n #202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));\n #300= IFCSPACE('s',$,'Stue',$,$,#20,$,'Stue',.ELEMENT.,.INTERNAL.,$);\n #310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);\n #311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);\n #312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));\n #313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);\n",
|
||
);
|
||
// En vegg som en tynn kasse: 5 m lang, 2,5 m høy, 0,2 m tykk, i millimeter.
|
||
let mut vegg = |nr: u64, navn: &str, y: f64| {
|
||
let (p, l, h, tj) = (nr, 5000.0, 2500.0, 200.0);
|
||
s.push_str(&format!(
|
||
"#{}= IFCWALL('w{p}',$,'{navn}',$,$,#{},#{},$,$);\n",
|
||
p,
|
||
p + 1,
|
||
p + 4
|
||
));
|
||
s.push_str(&format!("#{}= IFCLOCALPLACEMENT(#20,#{});\n", p + 1, p + 2));
|
||
s.push_str(&format!(
|
||
"#{}= IFCAXIS2PLACEMENT3D(#{},$,#{});\n",
|
||
p + 2,
|
||
p + 3,
|
||
p + 9
|
||
));
|
||
s.push_str(&format!(
|
||
"#{}= IFCCARTESIANPOINT((0.,{:.1},0.));\n",
|
||
p + 3,
|
||
y * 1000.0
|
||
));
|
||
s.push_str(&format!(
|
||
"#{}= IFCPRODUCTDEFINITIONSHAPE($,$,(#{}));\n",
|
||
p + 4,
|
||
p + 5
|
||
));
|
||
s.push_str(&format!(
|
||
"#{}= IFCSHAPEREPRESENTATION(#40,'Body','Tessellation',(#{}));\n",
|
||
p + 5,
|
||
p + 6
|
||
));
|
||
s.push_str(&format!(
|
||
"#{}= IFCTRIANGULATEDFACESET(#{},$,.T.,((1,2,3),(1,3,4),(5,7,6),(5,8,7),(1,5,6),(1,6,2),(2,6,7),(2,7,3),(3,7,8),(3,8,4),(4,8,5),(4,5,1)),$);\n",
|
||
p + 6,
|
||
p + 7
|
||
));
|
||
s.push_str(&format!(
|
||
"#{}= IFCCARTESIANPOINTLIST3D(((0.,0.,0.),({l:.1},0.,0.),({l:.1},{tj:.1},0.),(0.,{tj:.1},0.),(0.,0.,{h:.1}),({l:.1},0.,{h:.1}),({l:.1},{tj:.1},{h:.1}),(0.,{tj:.1},{h:.1})));\n",
|
||
p + 7
|
||
));
|
||
s.push_str(&format!("#{}= IFCDIRECTION((1.,0.,0.));\n", p + 9));
|
||
s.push_str(&format!(
|
||
"#{}= IFCRELDEFINESBYPROPERTIES('rel{p}',$,$,$,(#{p}),#202);\n",
|
||
p + 8
|
||
));
|
||
};
|
||
for (nr, navn, y) in vegger {
|
||
vegg(*nr, navn, *y);
|
||
}
|
||
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
||
s
|
||
}
|
||
|
||
#[test]
|
||
fn leser_areal_uverdi_og_himmelretning_av_geometrien() {
|
||
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, "flater: {:?}", b.flate);
|
||
for f in &b.flate {
|
||
// Hovedflaten er 5 × 2,5 m, ikke summen av alle sider.
|
||
assert!(
|
||
(f.areal - 12.5).abs() < 0.01,
|
||
"{} fikk areal {}",
|
||
f.navn,
|
||
f.areal
|
||
);
|
||
assert_eq!(f.u_verdi, Some(0.18));
|
||
assert_eq!(f.mot, Mot::Ute);
|
||
}
|
||
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 samme_vegg_i_flere_etasjer_telles_en_gang() {
|
||
// Den samme veggen på nøyaktig samme sted to ganger, slik en eksport gjør når den
|
||
// legger de samme delene i flere etasjer.
|
||
let tekst = prove_med(&[
|
||
(100, "Sorvegg", -5.0),
|
||
(120, "Nordvegg", 5.0),
|
||
(140, "Sorvegg", -5.0),
|
||
]);
|
||
let r = fra_tekst(&tekst, "prove").unwrap();
|
||
assert_eq!(r.bygning.flate.len(), 2, "flater: {:?}", r.bygning.flate);
|
||
assert!(
|
||
r.merknader.iter().any(|m| m.contains("dobbelt i modellen")),
|
||
"{:?}",
|
||
r.merknader
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn vegger_faar_himmelretning_i_navnet() {
|
||
let r = fra_tekst(&prove(), "prove").unwrap();
|
||
let mut navn: Vec<&str> = r.bygning.flate.iter().map(|f| f.navn.as_str()).collect();
|
||
navn.sort();
|
||
assert_eq!(navn, vec!["Nordvegg, nord", "Sorvegg, sør"]);
|
||
}
|
||
|
||
#[test]
|
||
fn uoppvarmede_rom_fra_tilleggsfilen_holdes_utenfor_bra() {
|
||
let valg = Importvalg {
|
||
uoppvarmede_rom: vec!["STUE".into()],
|
||
};
|
||
let r = fra_tekst_med(&prove(), "prove", &valg).unwrap();
|
||
assert_ne!(r.bygning.bygning.oppvarmet_bra, 100.0);
|
||
assert!(r
|
||
.merknader
|
||
.iter()
|
||
.any(|m| m.contains("uoppvarmet etter tilleggsfilen")));
|
||
}
|
||
|
||
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
||
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
||
#[test]
|
||
fn lengdeenheten_hentes_fra_prosjektet() {
|
||
let fil = step::les(&prove());
|
||
assert_eq!(ifcgeom::lengdeenhet(&fil), 0.001);
|
||
}
|
||
|
||
/// Et postformat festet på bygningen og eksportert som IFC-egenskapssett skal leses,
|
||
/// slik at byggeår, tetthet, ventilasjon og varmekilde kommer med fra modellen.
|
||
#[test]
|
||
fn egenskapssett_paa_bygningen_leses() {
|
||
let t = prove().replace(
|
||
"#300= IFCSPACE(",
|
||
"#400= IFCPROPERTYSINGLEVALUE('Byggeaar',$,IFCINTEGER(1948),$);\n\
|
||
#401= IFCPROPERTYSINGLEVALUE('Lekkasjetall',$,IFCREAL(8.0),$);\n\
|
||
#402= IFCPROPERTYSINGLEVALUE('Ventilasjonstype',$,IFCLABEL('Balansert'),$);\n\
|
||
#403= IFCPROPERTYSINGLEVALUE('Varmegjenvinning',$,IFCREAL(80.0),$);\n\
|
||
#404= IFCPROPERTYSINGLEVALUE('Varmekilde',$,IFCLABEL('Bergvarmepumpe'),$);\n\
|
||
#405= IFCPROPERTYSINGLEVALUE('Kommune',$,IFCLABEL('Indre \u{d8}stfold'),$);\n\
|
||
#406= IFCPROPERTYSET('ps2',$,'Enok',$,(#400,#401,#402,#403,#404,#405));\n\
|
||
#407= IFCRELDEFINESBYPROPERTIES('r4',$,$,$,(#10),#406);\n\
|
||
#300= IFCSPACE(",
|
||
);
|
||
let r = fra_tekst(&t, "prove").unwrap();
|
||
let b = &r.bygning;
|
||
assert_eq!(b.bygning.byggeaar, 1948);
|
||
assert_eq!(b.infiltrasjon.n50, Some(8.0));
|
||
assert_eq!(b.ventilasjon.typ, VentilasjonType::Balansert);
|
||
assert_eq!(b.ventilasjon.gjenvinning, Some(0.8));
|
||
assert_eq!(b.klima.kommune.as_deref(), Some("Indre Østfold"));
|
||
assert_eq!(b.oppvarming.kilde.len(), 1);
|
||
assert_eq!(b.oppvarming.kilde[0].typ, VarmekildeType::VaeskeVannVp);
|
||
assert!(r.merknader.iter().any(|m| m.contains("tatt i bruk")));
|
||
}
|
||
|
||
#[test]
|
||
fn eksport_uten_bygningsdeler_gir_tydelig_feil() {
|
||
let tom = "ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n\
|
||
#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n\
|
||
#2= IFCUNITASSIGNMENT((#1));\n\
|
||
#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,$,#2);\n\
|
||
#10= IFCBUILDING('b',$,'Hus',$,$,$,$,$,.ELEMENT.,$,$,$);\n\
|
||
#11= IFCBUILDINGSTOREY('k',$,'Kjeller',$,$,$,$,$,.ELEMENT.,-2350.);\n\
|
||
ENDSEC;\nEND-ISO-10303-21;";
|
||
let feil = fra_tekst(tom, "tom")
|
||
.err()
|
||
.expect("en tom eksport skal gi feil");
|
||
let tekst = feil.to_string();
|
||
assert!(tekst.contains("ingen bygningsdeler"), "{tekst}");
|
||
assert!(tekst.contains("1 etasjer"), "{tekst}");
|
||
}
|
||
|
||
/// Adressen på firmaet som laget modellen skal ikke bli byggets adresse.
|
||
#[test]
|
||
fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren() {
|
||
let t = prove().replace(
|
||
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
|
||
"#501= IFCPOSTALADDRESS($,$,$,$,('Firmaveien 1'),$,'Firmaby',$,'9999','Norge');\n\
|
||
#502= IFCORGANIZATION('999999999','Eksempel AS',$,$,(#501));\n\
|
||
#503= IFCPERSON($,'Nordmann','Kari',$,$,$,$,$);\n\
|
||
#504= IFCPERSONANDORGANIZATION(#503,#502,$);\n\
|
||
#500= IFCPOSTALADDRESS($,$,$,$,('Eksempelveien 12'),$,'Eksempelby',$,'1234','Norge');\n\
|
||
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
|
||
);
|
||
let r = fra_tekst(&t, "prove").unwrap();
|
||
assert_eq!(
|
||
r.bygning.bygning.adresse.as_deref(),
|
||
Some("Eksempelveien 12, 1234 Eksempelby")
|
||
);
|
||
assert_eq!(
|
||
r.bygning.bygning.energiraadgiver.as_deref(),
|
||
Some("Kari Nordmann, Eksempel AS")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn plassholderadresse_og_standardnavn_brukes_ikke() {
|
||
let t = prove()
|
||
.replace(
|
||
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
|
||
"#500= IFCPOSTALADDRESS($,$,$,'Vectorworks Architect',$,$,$,$,$,$);\n\
|
||
#10= IFCBUILDING('b',$,'Default Building',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
|
||
)
|
||
.replace(
|
||
"#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);",
|
||
"#3= IFCPROJECT('p',$,'Eksempelhuset','Energianalyse',$,'Eksempelhuset','Eksisterende',(#40),#2);",
|
||
);
|
||
let r = fra_tekst(&t, "prove").unwrap();
|
||
assert_eq!(r.bygning.bygning.adresse, None);
|
||
assert_eq!(r.bygning.bygning.navn, "Eksempelhuset");
|
||
}
|
||
|
||
#[test]
|
||
fn georeferering_i_utm_gir_posisjon_for_klimavalg() {
|
||
let t = prove().replace(
|
||
"#4= IFCDIRECTION((0.,1.));",
|
||
"#4= IFCDIRECTION((0.,1.));\n\
|
||
#600= IFCPROJECTEDCRS('EPSG:25832','ETRS89 / UTM zone 32N','ETRS89',$,$,$,$);\n\
|
||
#601= IFCMAPCONVERSION(#40,#600,613351.291,6611921.980,0.,1.,0.,$);",
|
||
);
|
||
let r = fra_tekst(&t, "prove").unwrap();
|
||
let [b, l] = r
|
||
.bygning
|
||
.klima
|
||
.posisjon
|
||
.expect("posisjon fra georefereringen");
|
||
assert!(
|
||
(b - 59.63).abs() < 1e-4 && (l - 11.01).abs() < 1e-4,
|
||
"{b}, {l}"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn vectorworks_standardplassering_brukes_ikke() {
|
||
let t = prove().replace(
|
||
"#4= IFCDIRECTION((0.,1.));",
|
||
"#4= IFCDIRECTION((0.,1.));\n\
|
||
#600= IFCPROJECTEDCRS('EPSG:3857',$,'WGS 84',$,$,$,$);\n\
|
||
#601= IFCMAPCONVERSION(#40,#600,-8575524.35228859,4705852.25746973,0.,1.,0.,$);",
|
||
);
|
||
let r = fra_tekst(&t, "prove").unwrap();
|
||
assert_eq!(r.bygning.klima.posisjon, None);
|
||
assert!(r.merknader.iter().any(|m| m.contains("standardplassering")));
|
||
}
|
||
|
||
#[test]
|
||
fn terrenget_leses_av_de_naermeste_punktene() {
|
||
// Et terreng som faller mot øst: bakken er høyere ved vestveggen enn ved østveggen.
|
||
let punkter: Vec<[f64; 3]> = (0..10)
|
||
.flat_map(|i| (0..10).map(move |j| [i as f64, j as f64, -1.0 - 0.2 * i as f64]))
|
||
.collect();
|
||
let vest = terreng_ved(&punkter, 0.5, 5.0).unwrap();
|
||
let ost = terreng_ved(&punkter, 8.5, 5.0).unwrap();
|
||
assert!(vest > ost, "vest {vest}, øst {ost}");
|
||
assert!((vest + 1.1).abs() < 0.2, "vest {vest}");
|
||
assert!((ost + 2.7).abs() < 0.2, "øst {ost}");
|
||
assert_eq!(terreng_ved(&[], 0.0, 0.0), None);
|
||
}
|
||
|
||
#[test]
|
||
fn grunnmur_over_terreng_av_hoydene() {
|
||
// ±0 ligger 131,2 moh., terrenget 130,6 moh. og første etasje på ±0: 0,6 m over bakken.
|
||
assert!((over_terreng_for_kjeller(131.2, 130.6, 0.0, 2.35) - 0.6).abs() < 1e-9);
|
||
// Aldri mer enn kjellerhøyden, og aldri under null.
|
||
assert_eq!(over_terreng_for_kjeller(131.2, 125.0, 0.0, 2.35), 2.35);
|
||
assert_eq!(over_terreng_for_kjeller(130.0, 131.0, 0.0, 2.35), 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn vegg_uten_geometri_utelates() {
|
||
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
|
||
let r = fra_tekst(&uten, "prove").unwrap();
|
||
assert!(r.bygning.flate.is_empty());
|
||
assert!(r
|
||
.merknader
|
||
.iter()
|
||
.any(|m| m.contains("geometrien ikke ga noe areal")));
|
||
}
|
||
}
|