Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7d77547ac
|
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx",
|
"approx",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
+195
-22
@@ -26,6 +26,9 @@ pub struct Importrapport {
|
|||||||
pub utelatt: Vec<(String, f64, Option<f64>)>,
|
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 VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"];
|
||||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||||
|
|
||||||
@@ -182,9 +185,59 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
plass,
|
plass,
|
||||||
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
areal: flate.map(|f| f.areal).unwrap_or(0.0),
|
||||||
normal: flate.map(|f| f.normal),
|
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),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// Tyngdepunkt av de ytre veggene bestemmer hvilken vei normalen peker.
|
||||||
let ytre_deler: Vec<&Del> = deler
|
let ytre_deler: Vec<&Del> = deler
|
||||||
.iter()
|
.iter()
|
||||||
@@ -234,8 +287,16 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
|
let forste_over = etasjer.iter().position(|e| !e.under_terreng);
|
||||||
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
|
// Terrenghøyden på IfcBuilding sier hvor mye av grunnmuren som står over bakken.
|
||||||
let terreng = terrenghoyder(&fil, skala);
|
let terreng = terrenghoyder(&fil, skala);
|
||||||
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
// 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)) => {
|
(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)
|
over_terreng_for_kjeller(referanse, terrengnivaa, forste.hoyde, kjellerdybde)
|
||||||
}
|
}
|
||||||
_ => 0.0,
|
_ => 0.0,
|
||||||
@@ -251,14 +312,14 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
|
|
||||||
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
||||||
let etasjekontekst = Etasjekontekst {
|
let etasjekontekst = Etasjekontekst {
|
||||||
nederste: (!etasjer.is_empty()).then_some(0),
|
|
||||||
kjeller: kjelleretasje,
|
kjeller: kjelleretasje,
|
||||||
forste_over,
|
forste_over,
|
||||||
|
forste_over_hoyde: forste_over.and_then(|i| etasjer.get(i)).map(|e| e.hoyde),
|
||||||
};
|
};
|
||||||
let laveste = deler
|
let laveste = deler
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
.filter(|d| PLATETYPER.contains(&d.typ.as_str()))
|
||||||
.filter_map(|d| d.plass.map(|p| p.2))
|
.map(|d| d.z)
|
||||||
.fold(f64::INFINITY, f64::min);
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
|
||||||
for d in &deler {
|
for d in &deler {
|
||||||
@@ -271,16 +332,25 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
uten_geometri += 1;
|
uten_geometri += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if dublett_av.contains_key(&d.nr) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
|
let i_kjeller = kjelleretasje.is_some() && d.etasje == kjelleretasje;
|
||||||
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
|
let Some((typ, mot)) = klassifiser(d, laveste, helning, &etasjekontekst) else {
|
||||||
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
utelatt.push((d.navn.clone(), d.areal, d.u_verdi));
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// En etasjeskiller i den laveste etasjen over terreng er grensen mot kjelleren.
|
// 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()
|
let (mot, sone) = if kjelleretasje.is_some()
|
||||||
&& !i_kjeller
|
&& !i_kjeller
|
||||||
&& d.etasje == forste_over
|
&& ved_kjellertaket
|
||||||
&& typ == FlateType::Gulv
|
&& typ == FlateType::Gulv
|
||||||
|
&& mot != Mot::Grunn
|
||||||
{
|
{
|
||||||
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
(Mot::Uoppvarmet, Some(kjellernavn.clone()))
|
||||||
} else {
|
} else {
|
||||||
@@ -324,6 +394,9 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
|
|
||||||
// ── vinduer og dører ────────────────────────────────────────────
|
// ── vinduer og dører ────────────────────────────────────────────
|
||||||
let mut vinduer: Vec<Vindu> = Vec::new();
|
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")) {
|
for (r, e) in fil.av_type("IFCWINDOW").chain(fil.av_type("IFCDOOR")) {
|
||||||
let dor = e.typ == "IFCDOOR";
|
let dor = e.typ == "IFCDOOR";
|
||||||
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
let hoyde = e.arg(8).and_then(|a| a.som_tall()).unwrap_or(0.0) * skala;
|
||||||
@@ -340,7 +413,9 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
dor_vert.get(&r)
|
dor_vert.get(&r)
|
||||||
} else {
|
} else {
|
||||||
vindu_vert.get(&r)
|
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.
|
// Vinduer i innervegger hører ikke til klimaskjermen.
|
||||||
let i_ytre_vegg = vert
|
let i_ytre_vegg = vert
|
||||||
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
.and_then(|v| deler.iter().find(|d| d.nr == *v))
|
||||||
@@ -354,6 +429,25 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
));
|
));
|
||||||
continue;
|
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
|
let asimut = vert
|
||||||
.and_then(|v| asimut_for_vegg.get(v))
|
.and_then(|v| asimut_for_vegg.get(v))
|
||||||
.copied()
|
.copied()
|
||||||
@@ -443,6 +537,44 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
"{uten_geometri} bygningsdeler er utelatt fordi geometrien ikke ga noe areal."
|
"{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() {
|
if !uten_uverdi.is_empty() {
|
||||||
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
|
// Navnene gjør det tydelig hva som må rettes i modellen. Flere elementer av samme
|
||||||
// konstruksjon nevnes én gang, med antallet.
|
// konstruksjon nevnes én gang, med antallet.
|
||||||
@@ -561,7 +693,13 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
rom.join(", ")
|
rom.join(", ")
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
let grunnmur = if terreng.is_some() {
|
let grunnmur = 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!(
|
format!(
|
||||||
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
||||||
{:.2} m under.",
|
{:.2} m under.",
|
||||||
@@ -1110,6 +1248,8 @@ struct Del {
|
|||||||
plass: Option<(f64, f64, f64, f64, f64)>,
|
plass: Option<(f64, f64, f64, f64, f64)>,
|
||||||
areal: f64,
|
areal: f64,
|
||||||
normal: Option<[f64; 3]>,
|
normal: Option<[f64; 3]>,
|
||||||
|
/// Delens høyde over byggets nullpunkt, av geometrien, m.
|
||||||
|
z: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
||||||
@@ -1257,12 +1397,13 @@ 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.
|
/// Avgjør om en bygningsdel hører til klimaskjermen, og hva den vender mot.
|
||||||
/// Etasjeinndelingen som klassifiseringen trenger.
|
/// Etasjeinndelingen som klassifiseringen trenger.
|
||||||
struct Etasjekontekst {
|
struct Etasjekontekst {
|
||||||
/// Nederste etasje i modellen.
|
|
||||||
nederste: Option<usize>,
|
|
||||||
/// Etasjen under terreng, dersom modellen har en.
|
/// Etasjen under terreng, dersom modellen har en.
|
||||||
kjeller: Option<usize>,
|
kjeller: Option<usize>,
|
||||||
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
/// Laveste etasje over terreng, der etasjeskilleren mot kjelleren ligger.
|
||||||
forste_over: Option<usize>,
|
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(
|
fn klassifiser(
|
||||||
@@ -1297,20 +1438,28 @@ fn klassifiser(
|
|||||||
if d.ytre == Some(true) {
|
if d.ytre == Some(true) {
|
||||||
return Some((FlateType::Gulv, Mot::Ute));
|
return Some((FlateType::Gulv, Mot::Ute));
|
||||||
}
|
}
|
||||||
// Mot grunnen bare i nederste etasje. Er etasjen ukjent, brukes høyden.
|
// Mot grunnen i det nederste planet, eller når navnet sier det. Høyden av geometrien
|
||||||
let nederst = match (d.etasje, e.nederste) {
|
// avgjør, siden etasjetilknytningen i en eksport kan være feil.
|
||||||
(Some(a), Some(b)) => a == b,
|
let paa_grunnen = [
|
||||||
_ => d
|
"kjellergulv",
|
||||||
.plass
|
"on grade",
|
||||||
.map(|p| (p.2 - laveste).abs() < 0.5)
|
"på grunn",
|
||||||
.unwrap_or(false),
|
"paa grunn",
|
||||||
};
|
"såle",
|
||||||
if nederst || navn.contains("kjellergulv") {
|
"sale",
|
||||||
|
];
|
||||||
|
if (d.z - laveste).abs() < 0.5 || paa_grunnen.iter().any(|o| navn.contains(o)) {
|
||||||
return Some((FlateType::Gulv, Mot::Grunn));
|
return Some((FlateType::Gulv, Mot::Grunn));
|
||||||
}
|
}
|
||||||
// Dekket i laveste etasje over en kjeller er grensen mot den uoppvarmede sonen.
|
// 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 {
|
if e.kjeller.is_some() {
|
||||||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
||||||
return None;
|
return None;
|
||||||
@@ -1678,6 +1827,11 @@ mod tester {
|
|||||||
/// To yttervegger på 5 × 2,5 m med hver sin trekantgeometri, i millimeter.
|
/// 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.
|
/// Sørveggen ligger ved y = −5 m, nordveggen ved y = +5 m.
|
||||||
fn prove() -> String {
|
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(
|
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",
|
"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",
|
||||||
);
|
);
|
||||||
@@ -1727,8 +1881,9 @@ mod tester {
|
|||||||
p + 8
|
p + 8
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
vegg(100, "Sorvegg", -5.0);
|
for (nr, navn, y) in vegger {
|
||||||
vegg(120, "Nordvegg", 5.0);
|
vegg(*nr, navn, *y);
|
||||||
|
}
|
||||||
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
s.push_str("ENDSEC;\nEND-ISO-10303-21;");
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
@@ -1774,6 +1929,24 @@ mod tester {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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]
|
#[test]
|
||||||
fn vegger_faar_himmelretning_i_navnet() {
|
fn vegger_faar_himmelretning_i_navnet() {
|
||||||
let r = fra_tekst(&prove(), "prove").unwrap();
|
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||||
|
|||||||
+17
-1
@@ -27,6 +27,9 @@ pub struct Hovedflate {
|
|||||||
pub normal: [f64; 3],
|
pub normal: [f64; 3],
|
||||||
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
/// Utstrekning i flatens plan: (u_min, u_maks, v_min, v_maks), m.
|
||||||
plan: [f64; 4],
|
plan: [f64; 4],
|
||||||
|
/// Midlere høyde over delens eget nullpunkt, m. Sier hvilken etasje delen ligger i når
|
||||||
|
/// plasseringen i filen ikke gjør det.
|
||||||
|
pub hoyde: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Nett {
|
impl Nett {
|
||||||
@@ -95,10 +98,16 @@ impl Nett {
|
|||||||
let (normal, sum) = grupper
|
let (normal, sum) = grupper
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
.max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))?;
|
||||||
|
let hoyde = if self.punkter.is_empty() {
|
||||||
|
0.0
|
||||||
|
} else {
|
||||||
|
self.punkter.iter().map(|p| p[2]).sum::<f64>() / self.punkter.len() as f64 * skala
|
||||||
|
};
|
||||||
Some(Hovedflate {
|
Some(Hovedflate {
|
||||||
areal: sum / 2.0,
|
areal: sum / 2.0,
|
||||||
normal,
|
normal,
|
||||||
plan: self.utstrekning(normal, skala),
|
plan: self.utstrekning(normal, skala),
|
||||||
|
hoyde,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,9 +182,16 @@ pub fn samlet_hovedflate(deler: &[Nett], skala: f64) -> Option<Hovedflate> {
|
|||||||
grupper.push(f);
|
grupper.push(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let areal = grupper.iter().map(|g| g.areal).sum();
|
let areal: f64 = grupper.iter().map(|g| g.areal).sum();
|
||||||
|
// Arealvektet høyde, slik at et sammensatt dekke havner der mesteparten av det ligger.
|
||||||
|
let hoyde = if areal > 0.0 {
|
||||||
|
grupper.iter().map(|g| g.hoyde * g.areal).sum::<f64>() / areal
|
||||||
|
} else {
|
||||||
|
grupper[0].hoyde
|
||||||
|
};
|
||||||
Some(Hovedflate {
|
Some(Hovedflate {
|
||||||
areal,
|
areal,
|
||||||
|
hoyde,
|
||||||
..grupper[0]
|
..grupper[0]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-1
@@ -530,9 +530,10 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet med enok {} etter NS 3031:2025 (timesmetode, \
|
"\n#v(1em)\n#text(size: 8pt, fill: mid)[Beregnet {} med enok {} etter NS 3031:2025 (timesmetode, \
|
||||||
NS-EN ISO 52016-1). Karakterskala fastsatt av Enova SF, gjeldende fra 1. januar 2026. \
|
NS-EN ISO 52016-1). Karakterskala fastsatt av Enova SF, gjeldende fra 1. januar 2026. \
|
||||||
Klimadata: TMY for norske kommuner basert på CERRA 1991–2020 (CC-BY-4.0).]\n",
|
Klimadata: TMY for norske kommuner basert på CERRA 1991–2020 (CC-BY-4.0).]\n",
|
||||||
|
i_dag(),
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
));
|
));
|
||||||
s
|
s
|
||||||
@@ -550,9 +551,34 @@ fn bunntekst(r: &Resultat) -> String {
|
|||||||
if let Some(e) = &b.energiraadgiver {
|
if let Some(e) = &b.energiraadgiver {
|
||||||
deler.push(e.clone());
|
deler.push(e.clone());
|
||||||
}
|
}
|
||||||
|
deler.push(format!("enok {} · {}", env!("CARGO_PKG_VERSION"), i_dag()));
|
||||||
deler.join(" · ")
|
deler.join(" · ")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Dagens dato som ISO-8601 i UTC.
|
||||||
|
fn i_dag() -> String {
|
||||||
|
let sekunder = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_secs() as i64)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let (aar, maaned, dag) = dato_av_dagnummer(sekunder.div_euclid(86_400));
|
||||||
|
format!("{aar:04}-{maaned:02}-{dag:02}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kalenderdato av antall døgn siden 1970-01-01, etter Howard Hinnants algoritme.
|
||||||
|
fn dato_av_dagnummer(dager: i64) -> (i64, i64, i64) {
|
||||||
|
let z = dager + 719_468;
|
||||||
|
let era = z.div_euclid(146_097);
|
||||||
|
let doe = z.rem_euclid(146_097);
|
||||||
|
let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let dag = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let maaned = if mp < 10 { mp + 3 } else { mp - 9 };
|
||||||
|
let aar = yoe + era * 400 + i64::from(maaned <= 2);
|
||||||
|
(aar, maaned, dag)
|
||||||
|
}
|
||||||
|
|
||||||
fn himmelretning(orientering: f64, helning: f64) -> String {
|
fn himmelretning(orientering: f64, helning: f64) -> String {
|
||||||
if helning < 20.0 {
|
if helning < 20.0 {
|
||||||
return "horisontal".into();
|
return "horisontal".into();
|
||||||
@@ -566,3 +592,15 @@ fn himmelretning(orientering: f64, helning: f64) -> String {
|
|||||||
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
let i = (((orientering.rem_euclid(360.0)) + 22.5) / 45.0).floor() as usize % 8;
|
||||||
retninger[i].into()
|
retninger[i].into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tester {
|
||||||
|
use super::dato_av_dagnummer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dagnummer_gir_riktig_dato() {
|
||||||
|
assert_eq!(dato_av_dagnummer(0), (1970, 1, 1));
|
||||||
|
assert_eq!(dato_av_dagnummer(19_723), (2024, 1, 1));
|
||||||
|
assert_eq!(dato_av_dagnummer(20_351), (2025, 9, 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user