Hent mye mer ut av IFC-modellen
Modellen for Nordhusstykket 5 viste seg å inneholde langt mer enn importen brukte. Etasjer - IfcBuildingStorey med høyde og navn, og tilknytningen av rom og bygningsdeler, sier hva som er hva. Etasjen «Kjeller» ligger 2,35 m under referansenivået. - En etasje under terreng settes nå opp som en egen uoppvarmet sone med sine egne vegger og gulv, og dekket over blir grensen mot den oppvarmede delen. - Oppvarmet BRA er rommene i etasjene over terreng. Rom som heter kott, bod, loft eller garasje blir flagget, siden de sjelden er oppvarmet. - Et dekke kan bare ligge mot grunnen i nederste etasje. Før ble etasjeskillere høyere opp regnet som gulv mot grunn. Egenskaper - Byggeår, bygningskategori, kommune, lekkasjetall, ventilasjonstype, varmegjenvinning, varmekilde og oppvarmet BRA leses fra et hvilket som helst egenskapssett på bygningen, tomta eller prosjektet, på norsk eller engelsk. Da kan et postformat i BIM-verktøyet bære de opplysningene IFC ikke har noen fast plass til. - Modellen viste seg allerede å ha YearOfConstruction 1950. Bare den ene verdien flytter den automatiske beregningen fra karakter E til G, siden lekkasjetallet følger byggeåret. - Masseklasse utledes av materialnavnene, og posisjonen leses av IfcSite. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
+619
-31
@@ -47,33 +47,65 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
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);
|
||||
|
||||
// ── 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();
|
||||
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 {
|
||||
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 ["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 += mengder
|
||||
.get(&r)
|
||||
.and_then(|m| m.get("NetVolume").or_else(|| m.get("GrossVolume")).copied())
|
||||
.unwrap_or(0.0);
|
||||
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 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²]: {}.",
|
||||
"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(", ")
|
||||
));
|
||||
}
|
||||
|
||||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||||
@@ -89,6 +121,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
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,
|
||||
@@ -123,14 +156,42 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
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);
|
||||
|
||||
// ── 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 = 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.
|
||||
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
||||
let etasjekontekst = Etasjekontekst {
|
||||
nederste: (!etasjer.is_empty()).then_some(0),
|
||||
kjeller: kjelleretasje,
|
||||
forste_over,
|
||||
};
|
||||
let laveste = deler
|
||||
.iter()
|
||||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||||
@@ -147,14 +208,30 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
uten_geometri += 1;
|
||||
continue;
|
||||
}
|
||||
let Some((typ, mot)) = klassifiser(d, laveste, helning) else {
|
||||
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 i den laveste etasjen over terreng er grensen mot kjelleren.
|
||||
let (mot, sone) = if kjelleretasje.is_some()
|
||||
&& !i_kjeller
|
||||
&& d.etasje == forste_over
|
||||
&& typ == FlateType::Gulv
|
||||
{
|
||||
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
||||
} else {
|
||||
(mot, None)
|
||||
};
|
||||
if d.u_verdi.is_none() {
|
||||
uten_uverdi += 1;
|
||||
}
|
||||
flater.push(Flate {
|
||||
let mal = if i_kjeller {
|
||||
&mut kjellerflater
|
||||
} else {
|
||||
&mut flater
|
||||
};
|
||||
mal.push(Flate {
|
||||
navn: d.navn.clone(),
|
||||
typ,
|
||||
areal: d.areal,
|
||||
@@ -162,20 +239,25 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
helning: Some(helning),
|
||||
u_verdi: d.u_verdi,
|
||||
lag: None,
|
||||
masseklasse: None,
|
||||
masseklasse: masseklasse_av_materialer(&fil, d.nr),
|
||||
masseplassering: Masseplassering::Fordelt,
|
||||
mot,
|
||||
b_faktor: None,
|
||||
absorptans: None,
|
||||
omkrets: None,
|
||||
dybde: None,
|
||||
over_terreng: None,
|
||||
sone: None,
|
||||
kommentar: Some("importert fra IFC".into()),
|
||||
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde),
|
||||
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(0.0),
|
||||
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();
|
||||
@@ -310,6 +392,21 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
.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 navn = fil
|
||||
.av_type("IFCBUILDING")
|
||||
.next()
|
||||
@@ -317,12 +414,71 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
.filter(|s| !s.is_empty() && s != "Default Building")
|
||||
.unwrap_or_else(|| standardnavn.to_string());
|
||||
|
||||
// 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(", ")
|
||||
)),
|
||||
});
|
||||
merknader.push(format!(
|
||||
"Etasjen «{}» ligger under terreng og er satt opp som en uoppvarmet sone med {:.0} m² gulv. \
|
||||
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; utslaget er lite, om lag \
|
||||
én prosent per meter.",
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((lat, lon)) = posisjon(&fil) {
|
||||
merknader.push(format!(
|
||||
"Modellen oppgir posisjon {lat:.4}° nord, {lon:.4}° øst. Kontroller at den stemmer, og sett \
|
||||
`kommune` under [klima] deretter."
|
||||
));
|
||||
} else {
|
||||
merknader.push(
|
||||
"Modellen oppgir ingen posisjon. Settes prosjektstedet i Vectorworks, kommer det med som \
|
||||
IfcSite og kan brukes til å velge klimadata."
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
kategori: Kategori::Smaahus,
|
||||
byggeaar: 2020,
|
||||
oppvarmet_bra: bra,
|
||||
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,
|
||||
@@ -332,16 +488,51 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
energiraadgiver: None,
|
||||
oppdragsgiver: None,
|
||||
},
|
||||
klima: Klima::default(),
|
||||
klima: Klima {
|
||||
kommune: data.kommune.clone(),
|
||||
..Klima::default()
|
||||
},
|
||||
flate: flater,
|
||||
sone: Vec::new(),
|
||||
sone: soner,
|
||||
grunn: Grunn::default(),
|
||||
vindu: vinduer,
|
||||
kuldebroer: Kuldebroer::default(),
|
||||
infiltrasjon: Infiltrasjon::default(),
|
||||
ventilasjon: Ventilasjon::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::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(),
|
||||
@@ -393,11 +584,290 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
*flater = samlet;
|
||||
}
|
||||
|
||||
/// 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 fra `IfcSite`, som grader nord og øst.
|
||||
fn posisjon(fil: &Fil) -> Option<(f64, f64)> {
|
||||
let (_, e) = fil.av_type("IFCSITE").next()?;
|
||||
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((grad(e.arg(9))?, grad(e.arg(10))?))
|
||||
}
|
||||
|
||||
/// 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)>,
|
||||
@@ -405,6 +875,72 @@ struct Del {
|
||||
normal: Option<[f64; 3]>,
|
||||
}
|
||||
|
||||
/// 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"
|
||||
@@ -479,7 +1015,22 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||||
}
|
||||
|
||||
/// 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)> {
|
||||
/// Etasjeinndelingen som klassifiseringen trenger.
|
||||
struct Etasjekontekst {
|
||||
/// Nederste etasje i modellen.
|
||||
nederste: Option<usize>,
|
||||
/// Etasjen under terreng, dersom modellen har en.
|
||||
kjeller: Option<usize>,
|
||||
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
||||
forste_over: Option<usize>,
|
||||
}
|
||||
|
||||
fn klassifiser(
|
||||
d: &Del,
|
||||
laveste: f64,
|
||||
helning: f64,
|
||||
e: &Etasjekontekst,
|
||||
) -> Option<(FlateType, Mot)> {
|
||||
let navn = d.navn.to_lowercase();
|
||||
let grunnord = [
|
||||
"kjellermur",
|
||||
@@ -502,17 +1053,26 @@ fn klassifiser(d: &Del, laveste: f64, helning: f64) -> Option<(FlateType, Mot)>
|
||||
},
|
||||
));
|
||||
}
|
||||
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.
|
||||
// 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));
|
||||
}
|
||||
// Mot grunnen bare i nederste etasje. Er etasjen ukjent, brukes høyden.
|
||||
let nederst = match (d.etasje, e.nederste) {
|
||||
(Some(a), Some(b)) => a == b,
|
||||
_ => d
|
||||
.plass
|
||||
.map(|p| (p.2 - laveste).abs() < 0.5)
|
||||
.unwrap_or(false),
|
||||
};
|
||||
if nederst || navn.contains("kjellergulv") {
|
||||
return Some((FlateType::Gulv, Mot::Grunn));
|
||||
}
|
||||
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
|
||||
if e.kjeller.is_some() && d.etasje.is_some() && d.etasje == e.forste_over {
|
||||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
||||
}
|
||||
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -912,6 +1472,34 @@ mod tester {
|
||||
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 vegg_uten_geometri_utelates() {
|
||||
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
|
||||
|
||||
Reference in New Issue
Block a user