Files
enok/src/inndata/ifc.rs
T

926 lines
32 KiB
Rust
Raw Normal View History

//! 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::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>)>,
}
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
pub fn les(sti: &Path) -> Result<Importrapport> {
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
let tekst = String::from_utf8_lossy(&raa);
fra_tekst(
&tekst,
sti.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("Bygning"),
)
}
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
let fil = step::les(tekst);
let mut merknader = Vec::new();
let 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);
// ── oppvarmet areal fra rommene ─────────────────────────────────
let mut bra = 0.0;
let mut volum = 0.0;
let mut rom: Vec<(String, f64)> = 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 {
rom.push((navn, areal));
bra += areal;
volum += mengder
.get(&r)
.and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied())
.unwrap_or(0.0);
}
}
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 summen av alle rom, {bra:.1} m². IFC sier ikke hvilke rom som er oppvarmet, så trekk fra kjeller, kott, loft og garasje. Rom [m²]: {}.",
liste.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)),
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),
});
}
// 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;
}
// ── klimaskjerm ─────────────────────────────────────────────────
let mut flater: Vec<Flate> = Vec::new();
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
let mut uten_uverdi = 0usize;
let mut uten_geometri = 0usize;
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
// Laveste gulvnivå brukes til å skille dekke mot grunn fra etasjeskillere.
let laveste = deler
.iter()
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
.filter_map(|d| d.plass.map(|p| p.2))
.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;
}
let Some((typ, mot)) = klassifiser(d, laveste, helning) else {
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
continue;
};
if d.u_verdi.is_none() {
uten_uverdi += 1;
}
flater.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: None,
masseplassering: Masseplassering::Fordelt,
mot,
b_faktor: None,
absorptans: None,
omkrets: None,
dybde: None,
over_terreng: None,
sone: None,
kommentar: Some("importert fra IFC".into()),
});
}
slaa_sammen(&mut flater);
// ── vinduer og dører ────────────────────────────────────────────
let mut vinduer: Vec<Vindu> = Vec::new();
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)
};
// 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;
}
let asimut = vert
.and_then(|v| asimut_for_vegg.get(v))
.copied()
.unwrap_or(0.0);
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| {
uten_uverdi += 1;
if dor {
2.0
} else {
2.4
}
});
vinduer.push(Vindu {
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
areal,
antall: 1.0,
orientering: asimut,
helning: 90.0,
u_verdi: u,
g_verdi: egenskap(&egenskaper, r, "SolarHeatGainTransmittance")
.or_else(|| egenskap(&egenskaper, r, "SolarTransmittance")),
karmandel: egenskap(&egenskaper, r, "GlazingAreaFraction").map(|g| 1.0 - g),
solskjerming: None,
horisont: None,
dor,
kommentar: Some("importert fra IFC".into()),
});
}
// 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."
));
}
// ── 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 uten_uverdi > 0 {
merknader.push(format!(
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok bruke typiske verdier for byggeåret."
));
}
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 navn = fil
.av_type("IFCBUILDING")
.next()
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
.filter(|s| !s.is_empty() && s != "Default Building")
.unwrap_or_else(|| standardnavn.to_string());
let bygning = Bygning {
bygning: BygningInfo {
navn,
kategori: Kategori::Smaahus,
byggeaar: 2020,
oppvarmet_bra: bra,
oppvarmet_volum: (volum > 0.0).then_some(volum),
antall_boenheter: 1,
bygningens_oppvarmede_bra: None,
adresse: adresse(&fil),
gnr_bnr: None,
beskrivelse: Some("Importert fra IFC".into()),
energiraadgiver: None,
oppdragsgiver: None,
},
klima: Klima::default(),
flate: flater,
sone: Vec::new(),
grunn: Grunn::default(),
vindu: vinduer,
kuldebroer: Kuldebroer::default(),
infiltrasjon: Infiltrasjon::default(),
ventilasjon: Ventilasjon::default(),
tappevann: Tappevann::default(),
oppvarming: Oppvarming::default(),
distribusjon: Distribusjon::default(),
solcelle: None,
kjoling: Kjoling::default(),
lufting: Lufting::default(),
drift: Drift::default(),
scenario: Vec::new(),
};
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, slik at summen peker riktig vei.
let sum = g.areal + f.areal;
g.orientering = (g.orientering * g.areal + f.orientering * 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;
}
/// En bygningsdel slik den er lest av IFC-filen.
struct Del {
nr: u64,
typ: String,
navn: String,
ytre: Option<bool>,
u_verdi: Option<f64>,
plass: Option<(f64, f64, f64, f64, f64)>,
areal: f64,
normal: Option<[f64; 3]>,
}
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();
if vannrett > 0.2 {
// 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.
fn klassifiser(d: &Del, laveste: f64, helning: f64) -> 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
},
));
}
let nederst = d
.plass
.map(|p| (p.2 - laveste).abs() < 0.5)
.unwrap_or(false);
if nederst || navn.contains("grunn") || navn.contains("kjellergulv") {
return Some((FlateType::Gulv, Mot::Grunn));
}
// Etasjeskillere og balkongdekker er ikke klimaskjerm med mindre de er merket ytre.
if d.ytre == Some(true) {
return Some((FlateType::Gulv, Mot::Ute));
}
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}"),
}
}
fn adresse(fil: &Fil) -> Option<String> {
let (_, e) = fil.av_type("IFCPOSTALADDRESS").next()?;
let linjer: Vec<String> = e
.arg(4)?
.som_liste()
.iter()
.filter_map(|a| a.som_tekst().map(|s| s.to_string()))
.collect();
let by = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("");
let mut ut = linjer.join(", ");
if !by.is_empty() {
if !ut.is_empty() {
ut.push_str(", ");
}
ut.push_str(by);
}
(!ut.is_empty()).then_some(ut)
}
/// 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 {
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
));
};
vegg(100, "Sorvegg", -5.0);
vegg(120, "Nordvegg", 5.0);
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
);
}
/// 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);
}
#[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")));
}
}