752 lines
25 KiB
Rust
752 lines
25 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::step::{self, Arg, Entitet, Fil};
|
|||
|
|
use crate::modell::*;
|
|||
|
|
use crate::Result;
|
|||
|
|
use anyhow::Context;
|
|||
|
|
use std::collections::HashMap;
|
|||
|
|
use std::path::Path;
|
|||
|
|
|
|||
|
|
pub struct Importrapport {
|
|||
|
|
pub bygning: Bygning,
|
|||
|
|
pub merknader: Vec<String>,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
|||
|
|
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
|||
|
|
|
|||
|
|
pub fn les(sti: &Path) -> Result<Importrapport> {
|
|||
|
|
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
|||
|
|
let tekst = String::from_utf8_lossy(&raa);
|
|||
|
|
fra_tekst(
|
|||
|
|
&tekst,
|
|||
|
|
sti.file_stem()
|
|||
|
|
.and_then(|s| s.to_str())
|
|||
|
|
.unwrap_or("Bygning"),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||
|
|
let fil = step::les(tekst);
|
|||
|
|
let mut merknader = Vec::new();
|
|||
|
|
|
|||
|
|
let lengdeskala = lengdeenhet(&fil);
|
|||
|
|
let sant_nord = sant_nord(&fil);
|
|||
|
|
let egenskaper = samle_egenskaper(&fil);
|
|||
|
|
let mengder = samle_mengder(&fil);
|
|||
|
|
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
|||
|
|
|
|||
|
|
// Areal og volum fra rom.
|
|||
|
|
let mut bra = 0.0;
|
|||
|
|
let mut volum = 0.0;
|
|||
|
|
for (r, _) in fil.av_type("IFCSPACE") {
|
|||
|
|
if let Some(m) = mengder.get(&r) {
|
|||
|
|
bra += m
|
|||
|
|
.get("NetFloorArea")
|
|||
|
|
.or_else(|| m.get("GrossFloorArea"))
|
|||
|
|
.copied()
|
|||
|
|
.unwrap_or(0.0);
|
|||
|
|
volum += m
|
|||
|
|
.get("NetVolume")
|
|||
|
|
.or_else(|| m.get("GrossVolume"))
|
|||
|
|
.copied()
|
|||
|
|
.unwrap_or(0.0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Plasseringer for alle bygningsdeler.
|
|||
|
|
let mut plassering: HashMap<u64, (f64, f64, f64, f64, f64)> = HashMap::new();
|
|||
|
|
for (r, e) in fil.av_typer(&VEGGTYPER).chain(fil.av_typer(&PLATETYPER)) {
|
|||
|
|
if let Some(p) = e.arg(5).and_then(|a| a.som_ref()) {
|
|||
|
|
if let Some(v) = global_plassering(&fil, p, lengdeskala) {
|
|||
|
|
plassering.insert(r, v);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Tyngdepunkt av ytterveggene, for å bestemme hvilken vei normalen peker.
|
|||
|
|
let ytre: Vec<u64> = fil
|
|||
|
|
.av_typer(&VEGGTYPER)
|
|||
|
|
.filter(|(r, _)| er_ytre(&egenskaper, *r).unwrap_or(true))
|
|||
|
|
.map(|(r, _)| r)
|
|||
|
|
.collect();
|
|||
|
|
let (mut cx, mut cy, mut n) = (0.0, 0.0, 0.0);
|
|||
|
|
for r in &ytre {
|
|||
|
|
if let Some((x, y, _, _, _)) = plassering.get(r) {
|
|||
|
|
cx += x;
|
|||
|
|
cy += y;
|
|||
|
|
n += 1.0;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if n > 0.0 {
|
|||
|
|
cx /= n;
|
|||
|
|
cy /= n;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
let mut flater: Vec<Flate> = Vec::new();
|
|||
|
|
let mut vinduer: Vec<Vindu> = Vec::new();
|
|||
|
|
let mut uten_uverdi = 0usize;
|
|||
|
|
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
|||
|
|
|
|||
|
|
// ── vegger ──────────────────────────────────────────────────────
|
|||
|
|
for (r, e) in fil.av_typer(&VEGGTYPER) {
|
|||
|
|
let ytre_vegg = er_ytre(&egenskaper, r).unwrap_or(true);
|
|||
|
|
let asimut = plassering
|
|||
|
|
.get(&r)
|
|||
|
|
.map(|(x, y, _, dx, dy)| asimut_av(*x, *y, *dx, *dy, cx, cy, sant_nord))
|
|||
|
|
.unwrap_or(0.0);
|
|||
|
|
asimut_for_vegg.insert(r, asimut);
|
|||
|
|
if !ytre_vegg {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let m = mengder.get(&r);
|
|||
|
|
let brutto = m
|
|||
|
|
.and_then(|m| {
|
|||
|
|
m.get("NetSideArea")
|
|||
|
|
.or_else(|| m.get("GrossSideArea"))
|
|||
|
|
.copied()
|
|||
|
|
})
|
|||
|
|
.unwrap_or(0.0);
|
|||
|
|
if brutto <= 0.01 {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let u = u_verdi(&egenskaper, r);
|
|||
|
|
if u.is_none() {
|
|||
|
|
uten_uverdi += 1;
|
|||
|
|
}
|
|||
|
|
flater.push(Flate {
|
|||
|
|
navn: navn_av(e, r, "Vegg"),
|
|||
|
|
typ: FlateType::Vegg,
|
|||
|
|
areal: brutto,
|
|||
|
|
orientering: asimut,
|
|||
|
|
helning: Some(90.0),
|
|||
|
|
u_verdi: u,
|
|||
|
|
lag: None,
|
|||
|
|
masseklasse: None,
|
|||
|
|
masseplassering: Masseplassering::Fordelt,
|
|||
|
|
mot: Mot::Ute,
|
|||
|
|
b_faktor: None,
|
|||
|
|
absorptans: None,
|
|||
|
|
omkrets: None,
|
|||
|
|
dybde: None,
|
|||
|
|
kommentar: Some("importert fra IFC".into()),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── plater: tak, gulv, dekke mot grunn ──────────────────────────
|
|||
|
|
for (r, e) in fil.av_typer(&PLATETYPER) {
|
|||
|
|
if !er_ytre(&egenskaper, r).unwrap_or(true) {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let m = mengder.get(&r);
|
|||
|
|
let a = m
|
|||
|
|
.and_then(|m| m.get("NetArea").or_else(|| m.get("GrossArea")).copied())
|
|||
|
|
.unwrap_or(0.0);
|
|||
|
|
if a <= 0.01 {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let type_tekst = e
|
|||
|
|
.args
|
|||
|
|
.iter()
|
|||
|
|
.filter_map(|x| x.som_tekst())
|
|||
|
|
.collect::<Vec<_>>()
|
|||
|
|
.join(" ")
|
|||
|
|
.to_uppercase();
|
|||
|
|
// Plater uten ROOF-merking regnes som dekke mot grunn; kontroller `mot` etter importen
|
|||
|
|
// dersom modellen har dekker mot det fri eller mot uoppvarmet kjeller.
|
|||
|
|
let (typ, mot, helning) = if e.typ == "IFCROOF" || type_tekst.contains("ROOF") {
|
|||
|
|
(FlateType::Tak, Mot::Ute, 0.0)
|
|||
|
|
} else {
|
|||
|
|
(FlateType::Gulv, Mot::Grunn, 180.0)
|
|||
|
|
};
|
|||
|
|
let u = u_verdi(&egenskaper, r);
|
|||
|
|
if u.is_none() {
|
|||
|
|
uten_uverdi += 1;
|
|||
|
|
}
|
|||
|
|
flater.push(Flate {
|
|||
|
|
navn: navn_av(e, r, if typ == FlateType::Tak { "Tak" } else { "Gulv" }),
|
|||
|
|
typ,
|
|||
|
|
areal: a,
|
|||
|
|
orientering: 0.0,
|
|||
|
|
helning: Some(helning),
|
|||
|
|
u_verdi: u,
|
|||
|
|
lag: None,
|
|||
|
|
masseklasse: None,
|
|||
|
|
masseplassering: Masseplassering::Fordelt,
|
|||
|
|
mot,
|
|||
|
|
b_faktor: None,
|
|||
|
|
absorptans: None,
|
|||
|
|
omkrets: None,
|
|||
|
|
dybde: None,
|
|||
|
|
kommentar: Some("importert fra IFC".into()),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ── vinduer og dører ────────────────────────────────────────────
|
|||
|
|
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
|||
|
|
let dor = e.typ == "IFCDOOR";
|
|||
|
|
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
|||
|
|
let bredde = e.arg(9).and_then(|a| a.som_tall()).unwrap_or(0.0) * lengdeskala;
|
|||
|
|
let areal = mengder
|
|||
|
|
.get(&r)
|
|||
|
|
.and_then(|m| m.get("Area").or_else(|| m.get("GrossArea")).copied())
|
|||
|
|
.filter(|a| *a > 0.01)
|
|||
|
|
.unwrap_or(hoyde * bredde);
|
|||
|
|
if areal <= 0.01 {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let vert = if dor {
|
|||
|
|
dor_vert.get(&r)
|
|||
|
|
} else {
|
|||
|
|
vindu_vert.get(&r)
|
|||
|
|
};
|
|||
|
|
let asimut = vert
|
|||
|
|
.and_then(|v| asimut_for_vegg.get(v))
|
|||
|
|
.copied()
|
|||
|
|
.unwrap_or(0.0);
|
|||
|
|
let u = u_verdi(&egenskaper, r).unwrap_or_else(|| {
|
|||
|
|
uten_uverdi += 1;
|
|||
|
|
if dor {
|
|||
|
|
2.0
|
|||
|
|
} else {
|
|||
|
|
2.4
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
vinduer.push(Vindu {
|
|||
|
|
navn: navn_av(e, r, if dor { "Dør" } else { "Vindu" }),
|
|||
|
|
areal,
|
|||
|
|
antall: 1.0,
|
|||
|
|
orientering: asimut,
|
|||
|
|
helning: 90.0,
|
|||
|
|
u_verdi: u,
|
|||
|
|
g_verdi: egenskap(&egenskaper, r, "SolarHeatGainTransmittance")
|
|||
|
|
.or_else(|| egenskap(&egenskaper, r, "SolarTransmittance")),
|
|||
|
|
karmandel: egenskap(&egenskaper, r, "GlazingAreaFraction").map(|g| 1.0 - g),
|
|||
|
|
solskjerming: None,
|
|||
|
|
horisont: None,
|
|||
|
|
dor,
|
|||
|
|
kommentar: Some("importert fra IFC".into()),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Trekk vindus- og dørareal fra veggene de sitter i.
|
|||
|
|
for (v, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
|||
|
|
let areal = vinduer
|
|||
|
|
.iter()
|
|||
|
|
.find(|x| x.kommentar.is_some() && x.navn.ends_with(&format!("#{v}")))
|
|||
|
|
.map(|x| x.areal);
|
|||
|
|
let _ = (areal, vegg);
|
|||
|
|
}
|
|||
|
|
let apningsareal_per_vegg = {
|
|||
|
|
let mut m: HashMap<u64, f64> = HashMap::new();
|
|||
|
|
for (a, vegg) in vindu_vert.iter().chain(dor_vert.iter()) {
|
|||
|
|
let e = fil.hent(*a);
|
|||
|
|
let hoyde = e
|
|||
|
|
.and_then(|e| e.arg(8))
|
|||
|
|
.and_then(|x| x.som_tall())
|
|||
|
|
.unwrap_or(0.0)
|
|||
|
|
* lengdeskala;
|
|||
|
|
let bredde = e
|
|||
|
|
.and_then(|e| e.arg(9))
|
|||
|
|
.and_then(|x| x.som_tall())
|
|||
|
|
.unwrap_or(0.0)
|
|||
|
|
* lengdeskala;
|
|||
|
|
let areal = mengder
|
|||
|
|
.get(a)
|
|||
|
|
.and_then(|q| q.get("Area").or_else(|| q.get("GrossArea")).copied())
|
|||
|
|
.filter(|v| *v > 0.01)
|
|||
|
|
.unwrap_or(hoyde * bredde);
|
|||
|
|
*m.entry(*vegg).or_default() += areal;
|
|||
|
|
}
|
|||
|
|
m
|
|||
|
|
};
|
|||
|
|
for (r, _) in fil.av_typer(&VEGGTYPER) {
|
|||
|
|
if let Some(ap) = apningsareal_per_vegg.get(&r) {
|
|||
|
|
let navn = fil.hent(r).map(|e| navn_av(e, r, "Vegg"));
|
|||
|
|
if let Some(f) = navn.and_then(|n| flater.iter_mut().find(|f| f.navn == n)) {
|
|||
|
|
f.areal = (f.areal - ap).max(0.0);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
flater.retain(|f| f.areal > 0.01);
|
|||
|
|
|
|||
|
|
if bra <= 0.0 {
|
|||
|
|
bra = flater
|
|||
|
|
.iter()
|
|||
|
|
.filter(|f| f.typ == FlateType::Gulv)
|
|||
|
|
.map(|f| f.areal)
|
|||
|
|
.sum::<f64>();
|
|||
|
|
if bra > 0.0 {
|
|||
|
|
merknader.push(format!(
|
|||
|
|
"IFC-modellen har ingen romarealer; oppvarmet BRA er anslått til {bra:.0} m² av gulvarealet. Kontroller verdien."
|
|||
|
|
));
|
|||
|
|
} else {
|
|||
|
|
merknader.push(
|
|||
|
|
"IFC-modellen har verken romarealer eller gulvflater; fyll inn oppvarmet_bra."
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if uten_uverdi > 0 {
|
|||
|
|
merknader.push(format!(
|
|||
|
|
"{uten_uverdi} bygningsdeler mangler U-verdi i IFC-filen. Legg inn U-verdier, eller la enok \
|
|||
|
|
bruke typiske verdier for byggeåret."
|
|||
|
|
));
|
|||
|
|
}
|
|||
|
|
if n == 0.0 {
|
|||
|
|
merknader.push(
|
|||
|
|
"Fant ingen plasseringer for ytterveggene; alle himmelretninger er satt til nord."
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
} else {
|
|||
|
|
merknader.push(
|
|||
|
|
"Himmelretningene er utledet av elementenes plassering og bygningens tyngdepunkt. Kontroller dem mot tegningen."
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
merknader.push(
|
|||
|
|
"IFC beskriver ikke drift, lekkasjetall eller energiforsyning. Fyll inn [ventilasjon], \
|
|||
|
|
[infiltrasjon], [oppvarming], [tappevann] og [klima] før beregning."
|
|||
|
|
.into(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
let navn = fil
|
|||
|
|
.av_type("IFCBUILDING")
|
|||
|
|
.next()
|
|||
|
|
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
|||
|
|
.filter(|s| !s.is_empty())
|
|||
|
|
.unwrap_or_else(|| standardnavn.to_string());
|
|||
|
|
|
|||
|
|
let bygning = Bygning {
|
|||
|
|
bygning: BygningInfo {
|
|||
|
|
navn,
|
|||
|
|
kategori: Kategori::Smaahus,
|
|||
|
|
byggeaar: 2020,
|
|||
|
|
oppvarmet_bra: bra,
|
|||
|
|
oppvarmet_volum: (volum > 0.0).then_some(volum),
|
|||
|
|
antall_boenheter: 1,
|
|||
|
|
bygningens_oppvarmede_bra: None,
|
|||
|
|
adresse: adresse(&fil),
|
|||
|
|
gnr_bnr: None,
|
|||
|
|
beskrivelse: Some("Importert fra IFC".into()),
|
|||
|
|
energiraadgiver: None,
|
|||
|
|
oppdragsgiver: None,
|
|||
|
|
},
|
|||
|
|
klima: Klima::default(),
|
|||
|
|
flate: flater,
|
|||
|
|
vindu: vinduer,
|
|||
|
|
kuldebroer: Kuldebroer::default(),
|
|||
|
|
infiltrasjon: Infiltrasjon::default(),
|
|||
|
|
ventilasjon: Ventilasjon::default(),
|
|||
|
|
tappevann: Tappevann::default(),
|
|||
|
|
oppvarming: Oppvarming::default(),
|
|||
|
|
distribusjon: Distribusjon::default(),
|
|||
|
|
solcelle: None,
|
|||
|
|
kjoling: Kjoling::default(),
|
|||
|
|
lufting: Lufting::default(),
|
|||
|
|
drift: Drift::default(),
|
|||
|
|
scenario: Vec::new(),
|
|||
|
|
};
|
|||
|
|
Ok(Importrapport { bygning, merknader })
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn navn_av(e: &Entitet, r: u64, standard: &str) -> String {
|
|||
|
|
match e.navn() {
|
|||
|
|
Some(n) if !n.trim().is_empty() => format!("{n} #{r}"),
|
|||
|
|
_ => format!("{standard} #{r}"),
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn adresse(fil: &Fil) -> Option<String> {
|
|||
|
|
let (_, e) = fil.av_type("IFCPOSTALADDRESS").next()?;
|
|||
|
|
let linjer: Vec<String> = e
|
|||
|
|
.arg(4)?
|
|||
|
|
.som_liste()
|
|||
|
|
.iter()
|
|||
|
|
.filter_map(|a| a.som_tekst().map(|s| s.to_string()))
|
|||
|
|
.collect();
|
|||
|
|
let by = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("");
|
|||
|
|
let mut ut = linjer.join(", ");
|
|||
|
|
if !by.is_empty() {
|
|||
|
|
if !ut.is_empty() {
|
|||
|
|
ut.push_str(", ");
|
|||
|
|
}
|
|||
|
|
ut.push_str(by);
|
|||
|
|
}
|
|||
|
|
(!ut.is_empty()).then_some(ut)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Faktor som gjør lengder om til meter.
|
|||
|
|
fn lengdeenhet(fil: &Fil) -> f64 {
|
|||
|
|
for (_, e) in fil.av_type("IFCSIUNIT") {
|
|||
|
|
let er_lengde = e.arg(1).and_then(|a| a.som_tekst()) == Some("LENGTHUNIT");
|
|||
|
|
if !er_lengde {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let prefiks = e.arg(2).and_then(|a| a.som_tekst()).unwrap_or("");
|
|||
|
|
return match prefiks {
|
|||
|
|
"MILLI" => 0.001,
|
|||
|
|
"CENTI" => 0.01,
|
|||
|
|
"DECI" => 0.1,
|
|||
|
|
"KILO" => 1000.0,
|
|||
|
|
_ => 1.0,
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
1.0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
|||
|
|
fn sant_nord(fil: &Fil) -> f64 {
|
|||
|
|
for (_, e) in fil.av_type("IFCGEOMETRICREPRESENTATIONCONTEXT") {
|
|||
|
|
if let Some(r) = e.arg(5).and_then(|a| a.som_ref()) {
|
|||
|
|
if let Some(d) = fil.hent(r) {
|
|||
|
|
let v = d.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
|||
|
|
if v.len() >= 2 {
|
|||
|
|
let (x, y) = (
|
|||
|
|
v[0].som_tall().unwrap_or(0.0),
|
|||
|
|
v[1].som_tall().unwrap_or(1.0),
|
|||
|
|
);
|
|||
|
|
// TrueNorth peker mot nord i prosjektets koordinater.
|
|||
|
|
return x.atan2(y).to_degrees();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
0.0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Sammensatt global plassering: (x, y, z, dx, dy) der (dx, dy) er lokal x-akse i planet.
|
|||
|
|
fn global_plassering(fil: &Fil, r: u64, skala: f64) -> Option<(f64, f64, f64, f64, f64)> {
|
|||
|
|
let mut x = 0.0;
|
|||
|
|
let mut y = 0.0;
|
|||
|
|
let mut z = 0.0;
|
|||
|
|
let mut dx = 1.0;
|
|||
|
|
let mut dy = 0.0;
|
|||
|
|
let mut kjede = Vec::new();
|
|||
|
|
let mut gjeldende = Some(r);
|
|||
|
|
let mut vakt = 0;
|
|||
|
|
while let Some(p) = gjeldende {
|
|||
|
|
vakt += 1;
|
|||
|
|
if vakt > 64 {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
let e = fil.hent(p)?;
|
|||
|
|
if e.typ != "IFCLOCALPLACEMENT" {
|
|||
|
|
break;
|
|||
|
|
}
|
|||
|
|
kjede.push(p);
|
|||
|
|
gjeldende = e.arg(0).and_then(|a| a.som_ref());
|
|||
|
|
}
|
|||
|
|
for p in kjede.into_iter().rev() {
|
|||
|
|
let e = fil.hent(p)?;
|
|||
|
|
let ax = e
|
|||
|
|
.arg(1)
|
|||
|
|
.and_then(|a| a.som_ref())
|
|||
|
|
.and_then(|a| fil.hent(a))?;
|
|||
|
|
let (lx, ly, lz) = punkt(fil, ax.arg(0).and_then(|a| a.som_ref()), skala);
|
|||
|
|
let (rx, ry) = retning(fil, ax.arg(2).and_then(|a| a.som_ref())).unwrap_or((1.0, 0.0));
|
|||
|
|
// Roter det lokale punktet med foreldrenes akse og legg det til.
|
|||
|
|
let nx = x + lx * dx - ly * dy;
|
|||
|
|
let ny = y + lx * dy + ly * dx;
|
|||
|
|
x = nx;
|
|||
|
|
y = ny;
|
|||
|
|
z += lz;
|
|||
|
|
let ndx = dx * rx - dy * ry;
|
|||
|
|
let ndy = dy * rx + dx * ry;
|
|||
|
|
let lengde = (ndx * ndx + ndy * ndy).sqrt().max(1e-12);
|
|||
|
|
dx = ndx / lengde;
|
|||
|
|
dy = ndy / lengde;
|
|||
|
|
}
|
|||
|
|
Some((x, y, z, dx, dy))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn punkt(fil: &Fil, r: Option<u64>, skala: f64) -> (f64, f64, f64) {
|
|||
|
|
let Some(e) = r.and_then(|r| fil.hent(r)) else {
|
|||
|
|
return (0.0, 0.0, 0.0);
|
|||
|
|
};
|
|||
|
|
let v = e.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
|||
|
|
let g = |i: usize| v.get(i).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
|||
|
|
(g(0), g(1), g(2))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn retning(fil: &Fil, r: Option<u64>) -> Option<(f64, f64)> {
|
|||
|
|
let e = r.and_then(|r| fil.hent(r))?;
|
|||
|
|
let v = e.arg(0).map(|a| a.som_liste()).unwrap_or(&[]);
|
|||
|
|
if v.len() < 2 {
|
|||
|
|
return None;
|
|||
|
|
}
|
|||
|
|
Some((
|
|||
|
|
v[0].som_tall().unwrap_or(1.0),
|
|||
|
|
v[1].som_tall().unwrap_or(0.0),
|
|||
|
|
))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Himmelretning for en vegg: normalen står vinkelrett på veggens akse, og velges slik at
|
|||
|
|
/// den peker bort fra bygningens tyngdepunkt.
|
|||
|
|
fn asimut_av(x: f64, y: f64, dx: f64, dy: f64, cx: f64, cy: f64, sant_nord: f64) -> f64 {
|
|||
|
|
let kandidater = [(dy, -dx), (-dy, dx)];
|
|||
|
|
let ut = (x - cx, y - cy);
|
|||
|
|
let valgt = if kandidater[0].0 * ut.0 + kandidater[0].1 * ut.1
|
|||
|
|
>= kandidater[1].0 * ut.0 + kandidater[1].1 * ut.1
|
|||
|
|
{
|
|||
|
|
kandidater[0]
|
|||
|
|
} else {
|
|||
|
|
kandidater[1]
|
|||
|
|
};
|
|||
|
|
// Asimut fra nord med klokka; prosjektets y-akse peker mot nord etter TrueNorth-dreining.
|
|||
|
|
(valgt.0.atan2(valgt.1).to_degrees() + sant_nord).rem_euclid(360.0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type Egenskaper = HashMap<u64, HashMap<String, Arg>>;
|
|||
|
|
|
|||
|
|
/// Samler alle egenskaper fra `IfcRelDefinesByProperties` per objekt.
|
|||
|
|
fn samle_egenskaper(fil: &Fil) -> Egenskaper {
|
|||
|
|
let mut ut: Egenskaper = HashMap::new();
|
|||
|
|
for (_, rel) in fil.av_type("IFCRELDEFINESBYPROPERTIES") {
|
|||
|
|
let objekter = rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]);
|
|||
|
|
let Some(def) = rel
|
|||
|
|
.arg(5)
|
|||
|
|
.and_then(|a| a.som_ref())
|
|||
|
|
.and_then(|r| fil.hent(r))
|
|||
|
|
else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
if def.typ != "IFCPROPERTYSET" {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let mut verdier = HashMap::new();
|
|||
|
|
for p in def.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
|||
|
|
let Some(pe) = p.som_ref().and_then(|r| fil.hent(r)) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
if pe.typ != "IFCPROPERTYSINGLEVALUE" {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let (Some(navn), Some(v)) = (pe.arg(0).and_then(|a| a.som_tekst()), pe.arg(2)) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
verdier.insert(navn.to_string(), v.clone());
|
|||
|
|
}
|
|||
|
|
for o in objekter {
|
|||
|
|
if let Some(r) = o.som_ref() {
|
|||
|
|
ut.entry(r).or_default().extend(verdier.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
// Typeobjektenes egenskaper arves av forekomstene.
|
|||
|
|
let mut arv: Vec<(u64, HashMap<String, Arg>)> = Vec::new();
|
|||
|
|
for (_, rel) in fil.av_type("IFCRELDEFINESBYTYPE") {
|
|||
|
|
let Some(typ) = rel.arg(5).and_then(|a| a.som_ref()) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
let Some(verdier) = ut.get(&typ).cloned() else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
for o in rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]) {
|
|||
|
|
if let Some(r) = o.som_ref() {
|
|||
|
|
arv.push((r, verdier.clone()));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for (r, verdier) in arv {
|
|||
|
|
let e = ut.entry(r).or_default();
|
|||
|
|
for (k, v) in verdier {
|
|||
|
|
e.entry(k).or_insert(v);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ut
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Samler alle mengder fra `IfcElementQuantity` per objekt.
|
|||
|
|
fn samle_mengder(fil: &Fil) -> HashMap<u64, HashMap<String, f64>> {
|
|||
|
|
let mut ut: HashMap<u64, HashMap<String, f64>> = HashMap::new();
|
|||
|
|
for (_, rel) in fil.av_type("IFCRELDEFINESBYPROPERTIES") {
|
|||
|
|
let objekter = rel.arg(4).map(|a| a.som_liste()).unwrap_or(&[]);
|
|||
|
|
let Some(def) = rel
|
|||
|
|
.arg(5)
|
|||
|
|
.and_then(|a| a.som_ref())
|
|||
|
|
.and_then(|r| fil.hent(r))
|
|||
|
|
else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
if def.typ != "IFCELEMENTQUANTITY" {
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
let mut verdier = HashMap::new();
|
|||
|
|
for q in def.arg(5).map(|a| a.som_liste()).unwrap_or(&[]) {
|
|||
|
|
let Some(qe) = q.som_ref().and_then(|r| fil.hent(r)) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
let Some(navn) = qe.arg(0).and_then(|a| a.som_tekst()) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
if let Some(v) = qe.arg(3).and_then(|a| a.som_tall()) {
|
|||
|
|
verdier.insert(navn.to_string(), v);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
for o in objekter {
|
|||
|
|
if let Some(r) = o.som_ref() {
|
|||
|
|
ut.entry(r).or_default().extend(verdier.clone());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
ut
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// Kobler vinduer og dører til veggen de sitter i, via åpningene.
|
|||
|
|
fn samle_apninger(fil: &Fil) -> (HashMap<u64, u64>, HashMap<u64, u64>) {
|
|||
|
|
// åpning → vertselement
|
|||
|
|
let mut vert_for_apning: HashMap<u64, u64> = HashMap::new();
|
|||
|
|
for (_, rel) in fil.av_type("IFCRELVOIDSELEMENT") {
|
|||
|
|
if let (Some(vert), Some(apning)) = (
|
|||
|
|
rel.arg(4).and_then(|a| a.som_ref()),
|
|||
|
|
rel.arg(5).and_then(|a| a.som_ref()),
|
|||
|
|
) {
|
|||
|
|
vert_for_apning.insert(apning, vert);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
let mut vindu = HashMap::new();
|
|||
|
|
let mut dor = HashMap::new();
|
|||
|
|
for (_, rel) in fil.av_type("IFCRELFILLSELEMENT") {
|
|||
|
|
let (Some(apning), Some(element)) = (
|
|||
|
|
rel.arg(4).and_then(|a| a.som_ref()),
|
|||
|
|
rel.arg(5).and_then(|a| a.som_ref()),
|
|||
|
|
) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
let Some(vert) = vert_for_apning.get(&apning) else {
|
|||
|
|
continue;
|
|||
|
|
};
|
|||
|
|
match fil.hent(element).map(|e| e.typ.as_str()) {
|
|||
|
|
Some("IFCDOOR") => {
|
|||
|
|
dor.insert(element, *vert);
|
|||
|
|
}
|
|||
|
|
Some("IFCWINDOW") => {
|
|||
|
|
vindu.insert(element, *vert);
|
|||
|
|
}
|
|||
|
|
_ => {}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
(vindu, dor)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn egenskap(e: &Egenskaper, r: u64, navn: &str) -> Option<f64> {
|
|||
|
|
e.get(&r)?.get(navn)?.som_tall()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn u_verdi(e: &Egenskaper, r: u64) -> Option<f64> {
|
|||
|
|
[
|
|||
|
|
"ThermalTransmittance",
|
|||
|
|
"UValue",
|
|||
|
|
"U-Value",
|
|||
|
|
"Varmegjennomgangskoeffisient",
|
|||
|
|
]
|
|||
|
|
.iter()
|
|||
|
|
.find_map(|n| egenskap(e, r, n))
|
|||
|
|
.filter(|u| *u > 0.0 && *u < 20.0)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
fn er_ytre(e: &Egenskaper, r: u64) -> Option<bool> {
|
|||
|
|
let v = e.get(&r)?.get("IsExternal")?;
|
|||
|
|
match v {
|
|||
|
|
Arg::Enum(s) => Some(s == "T" || s.eq_ignore_ascii_case("true")),
|
|||
|
|
Arg::Tall(n) => Some(*n != 0.0),
|
|||
|
|
Arg::Tekst(s) => Some(s == "T" || s.eq_ignore_ascii_case("true")),
|
|||
|
|
_ => None,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[cfg(test)]
|
|||
|
|
mod tester {
|
|||
|
|
use super::*;
|
|||
|
|
|
|||
|
|
const PROVE: &str = r#"ISO-10303-21;
|
|||
|
|
HEADER;ENDSEC;
|
|||
|
|
DATA;
|
|||
|
|
#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);
|
|||
|
|
#2= IFCDIRECTION((0.,1.));
|
|||
|
|
#3= IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-5,#50,#2);
|
|||
|
|
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);
|
|||
|
|
#20= IFCLOCALPLACEMENT($,#21);
|
|||
|
|
#21= IFCAXIS2PLACEMENT3D(#22,$,$);
|
|||
|
|
#22= IFCCARTESIANPOINT((0.,0.,0.));
|
|||
|
|
#100= IFCWALLSTANDARDCASE('w1',$,'Sorvegg',$,$,#110,$,$);
|
|||
|
|
#110= IFCLOCALPLACEMENT(#20,#111);
|
|||
|
|
#111= IFCAXIS2PLACEMENT3D(#112,$,#113);
|
|||
|
|
#112= IFCCARTESIANPOINT((0.,-5.,0.));
|
|||
|
|
#113= IFCDIRECTION((1.,0.,0.));
|
|||
|
|
#120= IFCWALLSTANDARDCASE('w2',$,'Nordvegg',$,$,#130,$,$);
|
|||
|
|
#130= IFCLOCALPLACEMENT(#20,#131);
|
|||
|
|
#131= IFCAXIS2PLACEMENT3D(#132,$,#133);
|
|||
|
|
#132= IFCCARTESIANPOINT((0.,5.,0.));
|
|||
|
|
#133= IFCDIRECTION((1.,0.,0.));
|
|||
|
|
#200= IFCPROPERTYSINGLEVALUE('ThermalTransmittance',$,IFCREAL(0.18),$);
|
|||
|
|
#201= IFCPROPERTYSINGLEVALUE('IsExternal',$,IFCBOOLEAN(.T.),$);
|
|||
|
|
#202= IFCPROPERTYSET('ps',$,'Pset_WallCommon',$,(#200,#201));
|
|||
|
|
#203= IFCRELDEFINESBYPROPERTIES('r',$,$,$,(#100,#120),#202);
|
|||
|
|
#210= IFCQUANTITYAREA('NetSideArea',$,$,25.0,$);
|
|||
|
|
#211= IFCELEMENTQUANTITY('q',$,'BaseQuantities',$,$,(#210));
|
|||
|
|
#212= IFCRELDEFINESBYPROPERTIES('r2',$,$,$,(#100,#120),#211);
|
|||
|
|
#300= IFCSPACE('s',$,'Stue',$,$,#20,$,$,.ELEMENT.,.INTERNAL.,$);
|
|||
|
|
#310= IFCQUANTITYAREA('NetFloorArea',$,$,100.0,$);
|
|||
|
|
#311= IFCQUANTITYVOLUME('NetVolume',$,$,250.0,$);
|
|||
|
|
#312= IFCELEMENTQUANTITY('q2',$,'BaseQuantities',$,$,(#310,#311));
|
|||
|
|
#313= IFCRELDEFINESBYPROPERTIES('r3',$,$,$,(#300),#312);
|
|||
|
|
ENDSEC;
|
|||
|
|
END-ISO-10303-21;"#;
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn leser_vegger_areal_uverdi_og_himmelretning() {
|
|||
|
|
let r = fra_tekst(PROVE, "prove").unwrap();
|
|||
|
|
let b = &r.bygning;
|
|||
|
|
assert_eq!(b.bygning.navn, "Testhus");
|
|||
|
|
assert_eq!(b.bygning.oppvarmet_bra, 100.0);
|
|||
|
|
assert_eq!(b.bygning.oppvarmet_volum, Some(250.0));
|
|||
|
|
assert_eq!(b.flate.len(), 2);
|
|||
|
|
for f in &b.flate {
|
|||
|
|
assert_eq!(f.areal, 25.0);
|
|||
|
|
assert_eq!(f.u_verdi, Some(0.18));
|
|||
|
|
}
|
|||
|
|
// Veggen ved y = −5 vender sør (180°), den ved y = +5 vender nord (0°).
|
|||
|
|
let sor = b
|
|||
|
|
.flate
|
|||
|
|
.iter()
|
|||
|
|
.find(|f| f.navn.starts_with("Sorvegg"))
|
|||
|
|
.unwrap();
|
|||
|
|
let nord = b
|
|||
|
|
.flate
|
|||
|
|
.iter()
|
|||
|
|
.find(|f| f.navn.starts_with("Nordvegg"))
|
|||
|
|
.unwrap();
|
|||
|
|
assert!(
|
|||
|
|
(sor.orientering - 180.0).abs() < 1.0,
|
|||
|
|
"sør = {}",
|
|||
|
|
sor.orientering
|
|||
|
|
);
|
|||
|
|
assert!(
|
|||
|
|
nord.orientering < 1.0 || nord.orientering > 359.0,
|
|||
|
|
"nord = {}",
|
|||
|
|
nord.orientering
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
#[test]
|
|||
|
|
fn millimeterenhet_skaleres() {
|
|||
|
|
let t = PROVE.replace(
|
|||
|
|
"#1= IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.);",
|
|||
|
|
"#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);",
|
|||
|
|
);
|
|||
|
|
let r = fra_tekst(&t, "prove").unwrap();
|
|||
|
|
// Arealene kommer fra mengder og er alltid i m², så de endres ikke.
|
|||
|
|
assert_eq!(r.bygning.flate[0].areal, 25.0);
|
|||
|
|
}
|
|||
|
|
}
|