Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbb1cedc5b
|
||
|
|
1e5d4f05bf
|
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.4.6"
|
version = "0.4.8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx",
|
"approx",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.4.6"
|
version = "0.4.8"
|
||||||
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"
|
||||||
|
|||||||
+5
-1
@@ -187,6 +187,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
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),
|
z: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.hoyde).unwrap_or(0.0),
|
||||||
topp: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.topp).unwrap_or(0.0),
|
topp: plass.map(|p| p.2).unwrap_or(0.0) + flate.map(|f| f.topp).unwrap_or(0.0),
|
||||||
|
omkrets: flate.map(|f| f.omkrets()).unwrap_or(0.0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,7 +392,8 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
mot,
|
mot,
|
||||||
b_faktor: None,
|
b_faktor: None,
|
||||||
absorptans: None,
|
absorptans: None,
|
||||||
omkrets: None,
|
omkrets: (typ == FlateType::Gulv && mot == Mot::Grunn && d.omkrets > 0.0)
|
||||||
|
.then_some(d.omkrets),
|
||||||
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - over_bakken),
|
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - over_bakken),
|
||||||
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(over_bakken),
|
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(over_bakken),
|
||||||
sone,
|
sone,
|
||||||
@@ -1320,6 +1322,8 @@ struct Del {
|
|||||||
z: f64,
|
z: f64,
|
||||||
/// Overkanten av delen over byggets nullpunkt, m. For en kjellervegg er det overkant mur.
|
/// Overkanten av delen over byggets nullpunkt, m. For en kjellervegg er det overkant mur.
|
||||||
topp: f64,
|
topp: f64,
|
||||||
|
/// Omkretsen av delens omriss, m. Et gulv mot grunnen trenger den for B′.
|
||||||
|
omkrets: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
/// Oversetter en fritekstverdi fra modellen til en bygningskategori.
|
||||||
|
|||||||
@@ -224,6 +224,22 @@ fn overlapper(a: &[f64; 4], b: &[f64; 4]) -> bool {
|
|||||||
dekning(a[0], a[1], b[0], b[1]) && dekning(a[2], a[3], b[2], b[3])
|
dekning(a[0], a[1], b[0], b[1]) && dekning(a[2], a[3], b[2], b[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Hovedflate {
|
||||||
|
/// Omkretsen av flatens omriss, m, regnet av utstrekningen i flatens eget plan.
|
||||||
|
///
|
||||||
|
/// Et gulv mot grunnen trenger omkretsen for B′ etter NS-EN ISO 13370. Omrisset regnes
|
||||||
|
/// som et rektangel, som stemmer godt for de fleste grunnflater.
|
||||||
|
pub fn omkrets(&self) -> f64 {
|
||||||
|
let bredde = self.plan[1] - self.plan[0];
|
||||||
|
let lengde = self.plan[3] - self.plan[2];
|
||||||
|
if bredde > 0.0 && lengde > 0.0 {
|
||||||
|
2.0 * (bredde + lengde)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Henter geometrien til et produkt via `Representation`, med én oppføring per geometridel.
|
/// Henter geometrien til et produkt via `Representation`, med én oppføring per geometridel.
|
||||||
pub fn netter_for_produkt(fil: &Fil, representasjon: Option<u64>) -> Vec<Nett> {
|
pub fn netter_for_produkt(fil: &Fil, representasjon: Option<u64>) -> Vec<Nett> {
|
||||||
let mut deler = Vec::new();
|
let mut deler = Vec::new();
|
||||||
|
|||||||
+246
-11
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::kjor::Resultat;
|
use crate::kjor::Resultat;
|
||||||
use crate::merking::skala::BOKSTAVER;
|
use crate::merking::skala::BOKSTAVER;
|
||||||
|
use crate::modell::lost::Forutsetning;
|
||||||
use crate::modell::{FlateType, Mot};
|
use crate::modell::{FlateType, Mot};
|
||||||
use crate::rapport::svg;
|
use crate::rapport::svg;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
@@ -437,7 +438,10 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
|
|
||||||
// ── inndata ─────────────────────────────────────────────────────
|
// ── inndata ─────────────────────────────────────────────────────
|
||||||
s.push_str("#pagebreak()\n\n= Inndata\n\n== Bygningskropp\n\n");
|
s.push_str("#pagebreak()\n\n= Inndata\n\n== Bygningskropp\n\n");
|
||||||
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto, auto),\n justering: (left, left, right, right, right, left),\n overskrift: (\"Bygningsdel\", \"Type\", \"Areal, m²\", \"U-verdi\", \"Orientering\", \"Mot\"),\n rader: (\n");
|
s.push_str("#tabell(\n kolonner: (1fr, auto, auto, auto, auto, auto, auto),\n justering: (left, left, right, right, right, right, left),\n overskrift: (\"Bygningsdel\", \"Type\", \"Antall\", \"Areal, m²\", \"U-verdi\", \"Orientering\", \"Mot\"),\n rader: (\n");
|
||||||
|
// Like bygningsdeler står på én linje: modellen deler gjerne den samme konstruksjonen i
|
||||||
|
// mange like vinduer eller veggfelt, og da sier antallet mer enn én rad per stykk.
|
||||||
|
let mut samlet: Vec<(String, String, String, String, usize, f64)> = Vec::new();
|
||||||
for f in &l.flater {
|
for f in &l.flater {
|
||||||
let typ = if f.glass {
|
let typ = if f.glass {
|
||||||
"Vindu/dør"
|
"Vindu/dør"
|
||||||
@@ -453,15 +457,62 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
Mot::Grunn => "grunnen",
|
Mot::Grunn => "grunnen",
|
||||||
Mot::Uoppvarmet => "uoppvarmet",
|
Mot::Uoppvarmet => "uoppvarmet",
|
||||||
};
|
};
|
||||||
|
let navn = f
|
||||||
|
.navn
|
||||||
|
.split(" #")
|
||||||
|
.next()
|
||||||
|
.unwrap_or(&f.navn)
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
let nokkel = (
|
||||||
|
navn,
|
||||||
|
typ.to_string(),
|
||||||
|
tall(f.u_verdi, 2),
|
||||||
|
himmelretning(f.orientering, f.helning),
|
||||||
|
mot.to_string(),
|
||||||
|
);
|
||||||
|
match samlet.iter_mut().find(|(n, t, u, o, _, _)| {
|
||||||
|
(n.as_str(), t.as_str(), u.as_str(), o.as_str())
|
||||||
|
== (
|
||||||
|
nokkel.0.as_str(),
|
||||||
|
nokkel.1.as_str(),
|
||||||
|
nokkel.2.as_str(),
|
||||||
|
nokkel.3.as_str(),
|
||||||
|
)
|
||||||
|
}) {
|
||||||
|
Some((_, _, _, _, antall, areal)) => {
|
||||||
|
*antall += 1;
|
||||||
|
*areal += f.areal;
|
||||||
|
}
|
||||||
|
None => samlet.push((nokkel.0, nokkel.1, nokkel.2, nokkel.3, 1, f.areal)),
|
||||||
|
}
|
||||||
|
let _ = nokkel.4;
|
||||||
|
}
|
||||||
|
for (navn, typ, u, orientering, antall, areal) in &samlet {
|
||||||
|
let mot = l
|
||||||
|
.flater
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.navn.split(" #").next().unwrap_or(&f.navn).trim() == navn)
|
||||||
|
.map(|f| match f.mot {
|
||||||
|
Mot::Ute => "det fri",
|
||||||
|
Mot::Grunn => "grunnen",
|
||||||
|
Mot::Uoppvarmet => "uoppvarmet",
|
||||||
|
})
|
||||||
|
.unwrap_or("det fri");
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
" {}\n",
|
" {}\n",
|
||||||
rad(&[
|
rad(&[
|
||||||
e(&f.navn),
|
e(navn),
|
||||||
typ.into(),
|
typ.clone(),
|
||||||
tall(f.areal, 1),
|
if *antall > 1 {
|
||||||
tall(f.u_verdi, 2),
|
antall.to_string()
|
||||||
himmelretning(f.orientering, f.helning),
|
} else {
|
||||||
mot.into()
|
String::new()
|
||||||
|
},
|
||||||
|
tall(*areal, 1),
|
||||||
|
u.clone(),
|
||||||
|
orientering.clone(),
|
||||||
|
mot.into(),
|
||||||
])
|
])
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -496,15 +547,28 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
if l.forutsetninger.is_empty() {
|
if l.forutsetninger.is_empty() {
|
||||||
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n");
|
||||||
} else {
|
} else {
|
||||||
// Faste bredder, ellers spiser de to første kolonnene plassen kilden trenger.
|
s.push_str("#tabell(\n kolonner: (1.25fr, 1fr, 1.15fr, auto, auto, auto, auto),\n justering: (left, left, left, center, center, center, left),\n overskrift: (\"Felt\", \"Verdi\", \"Gjelder\", \"Normert\", \"Byggeår\", \"Antatt\", \"Standard\"),\n rader: (\n");
|
||||||
s.push_str("#tabell(\n kolonner: (1.15fr, 0.85fr, 1.6fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
|
for f in grupper_forutsetninger(&l.forutsetninger) {
|
||||||
for f in &l.forutsetninger {
|
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
" {}\n",
|
" {}\n",
|
||||||
rad(&[e(&f.felt), e(&f.verdi), e(&f.kilde)])
|
rad(&[
|
||||||
|
e(&f.felt),
|
||||||
|
e(&f.verdi),
|
||||||
|
e(&f.gjelder),
|
||||||
|
kryss(f.normert),
|
||||||
|
kryss(f.byggeaar),
|
||||||
|
kryss(f.antatt),
|
||||||
|
e(&f.standard),
|
||||||
|
])
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
s.push_str(" ),\n)\n\n");
|
s.push_str(" ),\n)\n\n");
|
||||||
|
s.push_str(
|
||||||
|
"#merknad[*Normert* er standardverdier for bygningskategorien, som skal brukes i \
|
||||||
|
energimerket. *Byggeår* er typiske verdier for byggeperioden, brukt der modellen ikke \
|
||||||
|
oppgir noe. *Antatt* er satt av beregningen og bør kontrolleres. *Standard* viser \
|
||||||
|
hvilken standard verdien er regnet etter.]\n\n",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
for m in &r.merknader {
|
for m in &r.merknader {
|
||||||
s.push_str(&format!("#merknad[{}]\n\n", e(m)));
|
s.push_str(&format!("#merknad[{}]\n\n", e(m)));
|
||||||
@@ -563,6 +627,177 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kilden uten tall, slik at «… av konstruksjonens 3,39 W/(m²K) og 1,29 m dybde» og den
|
||||||
|
/// samme setningen for en annen vegg regnes som samme slags kilde.
|
||||||
|
fn kildenokkel(kilde: &str) -> String {
|
||||||
|
let mut ut = String::with_capacity(kilde.len());
|
||||||
|
let mut i_tall = false;
|
||||||
|
for c in kilde.chars() {
|
||||||
|
if c.is_ascii_digit() {
|
||||||
|
if !i_tall {
|
||||||
|
ut.push('#');
|
||||||
|
}
|
||||||
|
i_tall = true;
|
||||||
|
} else {
|
||||||
|
i_tall = false;
|
||||||
|
ut.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ut
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tallet fremst i en verdi som «1.353 W/(m²K)», når verdien begynner med et tall.
|
||||||
|
fn tallverdi(verdi: &str) -> Option<f64> {
|
||||||
|
let tall: String = verdi
|
||||||
|
.chars()
|
||||||
|
.take_while(|c| c.is_ascii_digit() || *c == '.' || *c == ',' || *c == '-')
|
||||||
|
.collect();
|
||||||
|
tall.replace(',', ".").parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slår to verdier med samme enhet sammen til et intervall, «1,21–1,39 W/(m²K)».
|
||||||
|
fn slaa_sammen_intervall(a: &str, b: &str) -> String {
|
||||||
|
let enhet = a.trim_start_matches(|c: char| {
|
||||||
|
c.is_ascii_digit() || c == '.' || c == ',' || c == '-' || c == '–'
|
||||||
|
});
|
||||||
|
let mut tall: Vec<f64> = Vec::new();
|
||||||
|
for del in a.split('–').chain(b.split('–')) {
|
||||||
|
if let Some(t) = tallverdi(del.trim()) {
|
||||||
|
tall.push(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let minste = tall.iter().copied().fold(f64::INFINITY, f64::min);
|
||||||
|
let storste = tall.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
if (storste - minste).abs() < 5e-3 {
|
||||||
|
format!("{minste:.2}{enhet}")
|
||||||
|
} else {
|
||||||
|
format!("{minste:.2}–{storste:.2}{enhet}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Norsk desimaltegn i tall, uten å røre tekst som «ISO 52016-1».
|
||||||
|
fn komma(verdi: &str) -> String {
|
||||||
|
let mut ut = String::with_capacity(verdi.len());
|
||||||
|
let tegn: Vec<char> = verdi.chars().collect();
|
||||||
|
for (i, c) in tegn.iter().enumerate() {
|
||||||
|
let mellom_tall = i > 0
|
||||||
|
&& i + 1 < tegn.len()
|
||||||
|
&& tegn[i - 1].is_ascii_digit()
|
||||||
|
&& tegn[i + 1].is_ascii_digit();
|
||||||
|
ut.push(if *c == '.' && mellom_tall { ',' } else { *c });
|
||||||
|
}
|
||||||
|
ut
|
||||||
|
}
|
||||||
|
|
||||||
|
fn kryss(på: bool) -> String {
|
||||||
|
if på {
|
||||||
|
"✓".into()
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// En rad i forutsetningstabellen: like forutsetninger for mange bygningsdeler slås sammen.
|
||||||
|
struct Forutsetningsrad {
|
||||||
|
felt: String,
|
||||||
|
verdi: String,
|
||||||
|
gjelder: String,
|
||||||
|
normert: bool,
|
||||||
|
byggeaar: bool,
|
||||||
|
antatt: bool,
|
||||||
|
standard: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Slår sammen forutsetninger som gjelder flere bygningsdeler, og deler kilden i kolonner.
|
||||||
|
///
|
||||||
|
/// «flate.Yttervegg nord.u_verdi» og «flate.Yttervegg sør.u_verdi» med samme verdi og kilde
|
||||||
|
/// blir til én rad «flate.u_verdi» som gjelder 2 flater.
|
||||||
|
fn grupper_forutsetninger(forutsetninger: &[Forutsetning]) -> Vec<Forutsetningsrad> {
|
||||||
|
let flertall = |prefiks: &str, antall: usize| match prefiks {
|
||||||
|
"flate" => format!("{antall} flater"),
|
||||||
|
"vindu" => format!("{antall} vinduer"),
|
||||||
|
"sone" => format!("{antall} soner"),
|
||||||
|
annet => format!("{antall} × {annet}"),
|
||||||
|
};
|
||||||
|
let mut rader: Vec<(String, String, String, Vec<String>, String)> = Vec::new();
|
||||||
|
for f in forutsetninger {
|
||||||
|
// «prefiks.navn.felt» deles i mønsteret «prefiks.felt» og navnet det gjelder.
|
||||||
|
let (prefiks, resten) = f.felt.split_once('.').unwrap_or(("", f.felt.as_str()));
|
||||||
|
let (navn, felt) = match resten.rsplit_once('.') {
|
||||||
|
Some((navn, felt)) => (navn.to_string(), felt.to_string()),
|
||||||
|
None => (String::new(), resten.to_string()),
|
||||||
|
};
|
||||||
|
let monster = if prefiks.is_empty() {
|
||||||
|
felt.clone()
|
||||||
|
} else {
|
||||||
|
format!("{prefiks}.{felt}")
|
||||||
|
};
|
||||||
|
match rader
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(m, v, k, _, _)| *m == monster && *v == f.verdi && *k == f.kilde)
|
||||||
|
{
|
||||||
|
Some((_, _, _, navn_liste, _)) => navn_liste.push(navn),
|
||||||
|
None => rader.push((
|
||||||
|
monster,
|
||||||
|
f.verdi.clone(),
|
||||||
|
f.kilde.clone(),
|
||||||
|
vec![navn],
|
||||||
|
prefiks.to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Verdier som bare skiller seg litt, som en U-verdi mot grunnen regnet for hver vegg,
|
||||||
|
// samles til ett intervall i stedet for én rad per vegg.
|
||||||
|
let mut samlet: Vec<(String, String, String, Vec<String>, String)> = Vec::new();
|
||||||
|
for (felt, verdi, kilde, navn, prefiks) in rader {
|
||||||
|
// Kilden bærer ofte tallene for den enkelte veggen; de maskeres, slik at samme
|
||||||
|
// slags kilde kjennes igjen.
|
||||||
|
let like = samlet
|
||||||
|
.iter_mut()
|
||||||
|
.find(|(f, _, k, _, _)| *f == felt && kildenokkel(k) == kildenokkel(&kilde));
|
||||||
|
match like {
|
||||||
|
Some((_, v, _, n, _)) if tallverdi(v).is_some() && tallverdi(&verdi).is_some() => {
|
||||||
|
*v = slaa_sammen_intervall(v, &verdi);
|
||||||
|
n.extend(navn);
|
||||||
|
}
|
||||||
|
_ => samlet.push((felt, verdi, kilde, navn, prefiks)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
samlet
|
||||||
|
.into_iter()
|
||||||
|
.map(|(felt, verdi, kilde, navn, prefiks)| {
|
||||||
|
let gjelder = match navn.len() {
|
||||||
|
0 => String::new(),
|
||||||
|
1 if navn[0].is_empty() => String::new(),
|
||||||
|
1 => navn[0].clone(),
|
||||||
|
n => flertall(&prefiks, n),
|
||||||
|
};
|
||||||
|
let lav = kilde.to_lowercase();
|
||||||
|
let standard = [
|
||||||
|
"NS-EN ISO 13370",
|
||||||
|
"NS-EN ISO 13789",
|
||||||
|
"ISO 52016-1",
|
||||||
|
"NS 3031",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.find(|s| kilde.contains(s))
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let normert = lav.contains("normert");
|
||||||
|
let byggeaar = lav.contains("typisk for");
|
||||||
|
Forutsetningsrad {
|
||||||
|
felt,
|
||||||
|
verdi: komma(&verdi),
|
||||||
|
gjelder,
|
||||||
|
normert,
|
||||||
|
byggeaar,
|
||||||
|
antatt: !normert && !byggeaar && standard.is_empty() || lav.contains("antatt"),
|
||||||
|
standard,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// Tekst i en Typst-strengliteral: bare omvendt skråstrek og anførselstegn skal ut.
|
/// Tekst i en Typst-strengliteral: bare omvendt skråstrek og anførselstegn skal ut.
|
||||||
fn streng(s: &str) -> String {
|
fn streng(s: &str) -> String {
|
||||||
s.replace('\\', "\\\\").replace('"', "\\\"")
|
s.replace('\\', "\\\\").replace('"', "\\\"")
|
||||||
|
|||||||
Reference in New Issue
Block a user