Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cbb1cedc5b
|
||
|
|
1e5d4f05bf
|
||
|
|
133152742f
|
||
|
|
959db648a9
|
||
|
|
3dff2dc273
|
||
|
|
b19162d9a7
|
||
|
|
c89561b0f5
|
||
|
|
e3f37afe36
|
||
|
|
cabd8500ea
|
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.4.1"
|
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.1"
|
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"
|
||||||
|
|||||||
@@ -180,6 +180,16 @@ navn = "Nye vinduer"
|
|||||||
endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
|
endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Rapporten kan følge husets eller firmaets egne dokumenter:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[rapport]
|
||||||
|
logo = "logo.svg" # vises øverst på forsiden
|
||||||
|
farge = "#880000" # aksentfarge i overskrifter og tabeller
|
||||||
|
avsender = "Firma · Adresse · Org.nr. 000 000 000"
|
||||||
|
skrift_overskrift = "Futura Now Headline"
|
||||||
|
```
|
||||||
|
|
||||||
Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister
|
Scenarier flettes inn i bygningsbeskrivelsen: tabeller flettes rekursivt, elementer i lister
|
||||||
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
|
matches på `navn` eller `type`, nye elementer legges til, og `slett = true` fjerner et element.
|
||||||
Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`.
|
Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`.
|
||||||
@@ -310,6 +320,19 @@ Mangler en bygningsdel U-verdi i modellen, sier importen hvilke det gjelder, og
|
|||||||
bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste
|
bruker en typisk verdi for byggeåret. Det gjelder også vinduer og dører, som i de fleste
|
||||||
eksportene ikke har U-verdi.
|
eksportene ikke har U-verdi.
|
||||||
|
|
||||||
|
Importen tåler noen vanlige feil i eksporten:
|
||||||
|
|
||||||
|
- **Deler som ligger dobbelt.** Legger eksporten den samme veggen eller det samme dekket i
|
||||||
|
flere etasjer, telles den én gang. Av kopiene brukes den som hører til den etasjen høyden
|
||||||
|
tilsier, og vinduene i de andre finner fortsatt veggen sin.
|
||||||
|
- **Feil etasje.** Hva som er gulv mot grunnen, dekke over kjelleren eller etasjeskiller inne
|
||||||
|
i bygningen, avgjøres av høyden geometrien har, ikke av hvilken etasje delen er lagt i.
|
||||||
|
- **Terrengmodell utenfor bakkenivå.** Ligger terrenget over gulvet i første etasje, brukes
|
||||||
|
det ikke, og importen sier fra. Oppgi da `over_terreng` på kjellerveggene.
|
||||||
|
|
||||||
|
Importen sier også hvilke deler som ikke er regnet med i klimaskjermen, som innervegger og
|
||||||
|
etasjeskillere, slik at det er lett å se om noe mangler.
|
||||||
|
|
||||||
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
|
Med de åtte feltene på plass er importen fra IFC nær nok komplett til at beregningen står på
|
||||||
modellen alene.
|
modellen alene.
|
||||||
|
|
||||||
@@ -382,7 +405,7 @@ jobs:
|
|||||||
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
|
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
|
||||||
chmod +x enok
|
chmod +x enok
|
||||||
./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
|
./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
|
||||||
- uses: https://gitea.com/actions/upload-artifact@v4
|
- uses: https://gitea.com/actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: bygningsmodell
|
name: bygningsmodell
|
||||||
path: |
|
path: |
|
||||||
@@ -396,7 +419,7 @@ jobs:
|
|||||||
shell: fish {0}
|
shell: fish {0}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://gitea.com/actions/checkout@v6
|
- uses: https://gitea.com/actions/checkout@v6
|
||||||
- uses: https://gitea.com/actions/download-artifact@v4
|
- uses: https://gitea.com/actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: bygningsmodell
|
name: bygningsmodell
|
||||||
- name: Lag rapporten
|
- name: Lag rapporten
|
||||||
|
|||||||
@@ -596,6 +596,7 @@ fn bygg(
|
|||||||
lufting: Lufting::default(),
|
lufting: Lufting::default(),
|
||||||
drift: Drift::default(),
|
drift: Drift::default(),
|
||||||
scenario: Vec::new(),
|
scenario: Vec::new(),
|
||||||
|
rapport: Rapport::default(),
|
||||||
};
|
};
|
||||||
Ok(Importrapport { bygning, merknader })
|
Ok(Importrapport { bygning, merknader })
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-16
@@ -186,6 +186,8 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
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),
|
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),
|
||||||
|
omkrets: flate.map(|f| f.omkrets()).unwrap_or(0.0),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +288,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
// Den laveste etasjen over terreng, der etasjeskilleren mot kjelleren ligger.
|
// Den laveste etasjen over terreng, der etasjeskilleren mot kjelleren ligger.
|
||||||
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 terrengmodell = terrengpunkter(&fil, skala);
|
||||||
let terreng = terrenghoyder(&fil, skala);
|
let terreng = terrenghoyder(&fil, skala);
|
||||||
// Ligger terrengmodellen over gulvet i første etasje, er den ikke lagt i bakkenivå, og da
|
// 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.
|
// sier den ingenting om hvor mye av grunnmuren som er nedgravd.
|
||||||
@@ -308,6 +311,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
let mut utelatt: Vec<(String, f64, Option<f64>)> = Vec::new();
|
||||||
let mut uten_uverdi: Vec<String> = Vec::new();
|
let mut uten_uverdi: Vec<String> = Vec::new();
|
||||||
let mut uten_geometri = 0usize;
|
let mut uten_geometri = 0usize;
|
||||||
|
let mut grunnmurshoyder: Vec<f64> = Vec::new();
|
||||||
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
let mut asimut_for_vegg: HashMap<u64, f64> = HashMap::new();
|
||||||
|
|
||||||
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
// Laveste etasje og laveste gulvnivå skiller dekke mot grunn fra etasjeskillere.
|
||||||
@@ -359,6 +363,17 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
if d.u_verdi.is_none() {
|
if d.u_verdi.is_none() {
|
||||||
uten_uverdi.push(d.navn.clone());
|
uten_uverdi.push(d.navn.clone());
|
||||||
}
|
}
|
||||||
|
// Terrengmodellen sier hvor høyt bakken ligger akkurat der denne veggen står, så en
|
||||||
|
// skrånende tomt gir forskjellig høyde over bakken på hver side av huset.
|
||||||
|
// Høyden måles fra overkant mur, ikke fra gulvet over: dekket selv står ikke i bakken.
|
||||||
|
let over_bakken = d
|
||||||
|
.plass
|
||||||
|
.and_then(|(x, y, _, _, _)| terreng_ved(&terrengmodell, x, y))
|
||||||
|
.map(|z| (d.topp - z).clamp(0.0, kjellerdybde))
|
||||||
|
.unwrap_or(kjeller_over_terreng);
|
||||||
|
if i_kjeller && mot == Mot::Grunn && typ == FlateType::Vegg {
|
||||||
|
grunnmurshoyder.push(over_bakken);
|
||||||
|
}
|
||||||
let mal = if i_kjeller {
|
let mal = if i_kjeller {
|
||||||
&mut kjellerflater
|
&mut kjellerflater
|
||||||
} else {
|
} else {
|
||||||
@@ -377,9 +392,10 @@ 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)
|
||||||
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - kjeller_over_terreng),
|
.then_some(d.omkrets),
|
||||||
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(kjeller_over_terreng),
|
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - over_bakken),
|
||||||
|
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(over_bakken),
|
||||||
sone,
|
sone,
|
||||||
kommentar: d
|
kommentar: d
|
||||||
.etasje
|
.etasje
|
||||||
@@ -693,7 +709,21 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
rom.join(", ")
|
rom.join(", ")
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
let grunnmur = if terreng_over_gulvet {
|
let grunnmur = if !grunnmurshoyder.is_empty() && !terrengmodell.is_empty() {
|
||||||
|
let laveste = grunnmurshoyder
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.fold(f64::INFINITY, f64::min);
|
||||||
|
let hoyeste = grunnmurshoyder
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max);
|
||||||
|
let snitt = grunnmurshoyder.iter().sum::<f64>() / grunnmurshoyder.len() as f64;
|
||||||
|
format!(
|
||||||
|
"Terrengmodellen i filen gir hver kjellervegg sin egen høyde over bakken: \
|
||||||
|
{laveste:.2}–{hoyeste:.2} m, i snitt {snitt:.2} m. Resten av veggen er regnet nedgravd."
|
||||||
|
)
|
||||||
|
} else if terreng_over_gulvet {
|
||||||
format!(
|
format!(
|
||||||
"Terrengmodellen ligger over gulvet i første etasje, så den er ikke brukt. \
|
"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 \
|
Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Legg \
|
||||||
@@ -833,6 +863,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu
|
|||||||
lufting: Lufting::default(),
|
lufting: Lufting::default(),
|
||||||
drift: Drift::default(),
|
drift: Drift::default(),
|
||||||
scenario: Vec::new(),
|
scenario: Vec::new(),
|
||||||
|
rapport: Rapport::default(),
|
||||||
};
|
};
|
||||||
Ok(Importrapport {
|
Ok(Importrapport {
|
||||||
bygning,
|
bygning,
|
||||||
@@ -1223,6 +1254,45 @@ fn terrenghoyder(fil: &Fil, skala: f64) -> Option<(f64, f64)> {
|
|||||||
Some((referanse, terreng))
|
Some((referanse, terreng))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Punktene i terrengmodellen, i byggets koordinater, m.
|
||||||
|
///
|
||||||
|
/// Vectorworks legger terrenget som et `IfcGeographicElement` med typen TERRAIN, eller på
|
||||||
|
/// tomta. Punktene brukes til å finne bakkenivået ved hver enkelt kjellervegg.
|
||||||
|
fn terrengpunkter(fil: &Fil, skala: f64) -> Vec<[f64; 3]> {
|
||||||
|
let mut ut = Vec::new();
|
||||||
|
for (_, e) in fil
|
||||||
|
.av_type("IFCGEOGRAPHICELEMENT")
|
||||||
|
.chain(fil.av_type("IFCSITE"))
|
||||||
|
{
|
||||||
|
let (px, py, pz) = e
|
||||||
|
.arg(5)
|
||||||
|
.and_then(|a| a.som_ref())
|
||||||
|
.and_then(|p| global_plassering(fil, p, skala))
|
||||||
|
.map(|(x, y, z, _, _)| (x, y, z))
|
||||||
|
.unwrap_or((0.0, 0.0, 0.0));
|
||||||
|
for nett in ifcgeom::netter_for_produkt(fil, e.arg(6).and_then(|a| a.som_ref())) {
|
||||||
|
for p in &nett.punkter {
|
||||||
|
ut.push([p[0] * skala + px, p[1] * skala + py, p[2] * skala + pz]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ut
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bakkenivået ved et punkt, som snittet av de nærmeste punktene i terrengmodellen, m.
|
||||||
|
fn terreng_ved(punkter: &[[f64; 3]], x: f64, y: f64) -> Option<f64> {
|
||||||
|
if punkter.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut avstand: Vec<(f64, f64)> = punkter
|
||||||
|
.iter()
|
||||||
|
.map(|p| ((p[0] - x).powi(2) + (p[1] - y).powi(2), p[2]))
|
||||||
|
.collect();
|
||||||
|
avstand.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||||
|
let antall = avstand.len().min(8);
|
||||||
|
Some(avstand[..antall].iter().map(|(_, z)| z).sum::<f64>() / antall as f64)
|
||||||
|
}
|
||||||
|
|
||||||
/// Hvor mye av grunnmuren som står over bakken, m.
|
/// Hvor mye av grunnmuren som står over bakken, m.
|
||||||
///
|
///
|
||||||
/// Toppen av grunnmuren ligger ved gulvet i første etasje over kjelleren. Den står over
|
/// Toppen av grunnmuren ligger ved gulvet i første etasje over kjelleren. Den står over
|
||||||
@@ -1250,6 +1320,10 @@ struct Del {
|
|||||||
normal: Option<[f64; 3]>,
|
normal: Option<[f64; 3]>,
|
||||||
/// Delens høyde over byggets nullpunkt, av geometrien, m.
|
/// Delens høyde over byggets nullpunkt, av geometrien, m.
|
||||||
z: f64,
|
z: f64,
|
||||||
|
/// Overkanten av delen over byggets nullpunkt, m. For en kjellervegg er det overkant mur.
|
||||||
|
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.
|
||||||
@@ -1438,20 +1512,15 @@ 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 i det nederste planet, eller når navnet sier det. Høyden av geometrien
|
// Høyden av geometrien avgjør, siden etasjetilknytningen i en eksport kan være feil.
|
||||||
// avgjør, siden etasjetilknytningen i en eksport kan være feil.
|
// Et dekke som ligger på bakken sier det i navnet. Ellers er et dekke like under
|
||||||
let paa_grunnen = [
|
// gulvet i første etasje himlingen over kjelleren, og det nederste dekket ligger mot
|
||||||
"kjellergulv",
|
// grunnen. «Kjellergulv» er ikke nok til å si at dekket ligger på bakken: dekket over
|
||||||
"on grade",
|
// kjelleren bærer ofte det samme navnet.
|
||||||
"på grunn",
|
let paa_grunnen = ["on grade", "på grunn", "paa grunn", "såle", "sale"];
|
||||||
"paa grunn",
|
if paa_grunnen.iter().any(|o| navn.contains(o)) {
|
||||||
"såle",
|
|
||||||
"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.
|
|
||||||
if e.kjeller.is_some() {
|
if e.kjeller.is_some() {
|
||||||
let ved_forste_over = e
|
let ved_forste_over = e
|
||||||
.forste_over_hoyde
|
.forste_over_hoyde
|
||||||
@@ -1461,6 +1530,9 @@ fn klassifiser(
|
|||||||
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
return Some((FlateType::Gulv, Mot::Uoppvarmet));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (d.z - laveste).abs() < 0.5 {
|
||||||
|
return Some((FlateType::Gulv, Mot::Grunn));
|
||||||
|
}
|
||||||
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
// Dekker lenger opp er etasjeskillere inne i bygningen.
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -2094,6 +2166,20 @@ mod tester {
|
|||||||
assert!(r.merknader.iter().any(|m| m.contains("standardplassering")));
|
assert!(r.merknader.iter().any(|m| m.contains("standardplassering")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn terrenget_leses_av_de_naermeste_punktene() {
|
||||||
|
// Et terreng som faller mot øst: bakken er høyere ved vestveggen enn ved østveggen.
|
||||||
|
let punkter: Vec<[f64; 3]> = (0..10)
|
||||||
|
.flat_map(|i| (0..10).map(move |j| [i as f64, j as f64, -1.0 - 0.2 * i as f64]))
|
||||||
|
.collect();
|
||||||
|
let vest = terreng_ved(&punkter, 0.5, 5.0).unwrap();
|
||||||
|
let ost = terreng_ved(&punkter, 8.5, 5.0).unwrap();
|
||||||
|
assert!(vest > ost, "vest {vest}, øst {ost}");
|
||||||
|
assert!((vest + 1.1).abs() < 0.2, "vest {vest}");
|
||||||
|
assert!((ost + 2.7).abs() < 0.2, "øst {ost}");
|
||||||
|
assert_eq!(terreng_ved(&[], 0.0, 0.0), None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn grunnmur_over_terreng_av_hoydene() {
|
fn grunnmur_over_terreng_av_hoydene() {
|
||||||
// ±0 ligger 131,2 moh., terrenget 130,6 moh. og første etasje på ±0: 0,6 m over bakken.
|
// ±0 ligger 131,2 moh., terrenget 130,6 moh. og første etasje på ±0: 0,6 m over bakken.
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ pub struct Hovedflate {
|
|||||||
/// Midlere høyde over delens eget nullpunkt, m. Sier hvilken etasje delen ligger i når
|
/// Midlere høyde over delens eget nullpunkt, m. Sier hvilken etasje delen ligger i når
|
||||||
/// plasseringen i filen ikke gjør det.
|
/// plasseringen i filen ikke gjør det.
|
||||||
pub hoyde: f64,
|
pub hoyde: f64,
|
||||||
|
/// Øverste punkt over delens eget nullpunkt, m. For en kjellervegg er det overkant mur.
|
||||||
|
pub topp: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Nett {
|
impl Nett {
|
||||||
@@ -103,11 +105,18 @@ impl Nett {
|
|||||||
} else {
|
} else {
|
||||||
self.punkter.iter().map(|p| p[2]).sum::<f64>() / self.punkter.len() as f64 * skala
|
self.punkter.iter().map(|p| p[2]).sum::<f64>() / self.punkter.len() as f64 * skala
|
||||||
};
|
};
|
||||||
|
let topp = self
|
||||||
|
.punkter
|
||||||
|
.iter()
|
||||||
|
.map(|p| p[2])
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max)
|
||||||
|
* 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,
|
hoyde,
|
||||||
|
topp: if topp.is_finite() { topp } else { 0.0 },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +198,14 @@ pub fn samlet_hovedflate(deler: &[Nett], skala: f64) -> Option<Hovedflate> {
|
|||||||
} else {
|
} else {
|
||||||
grupper[0].hoyde
|
grupper[0].hoyde
|
||||||
};
|
};
|
||||||
|
let topp = grupper
|
||||||
|
.iter()
|
||||||
|
.map(|g| g.topp)
|
||||||
|
.fold(f64::NEG_INFINITY, f64::max);
|
||||||
Some(Hovedflate {
|
Some(Hovedflate {
|
||||||
areal,
|
areal,
|
||||||
hoyde,
|
hoyde,
|
||||||
|
topp,
|
||||||
..grupper[0]
|
..grupper[0]
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -210,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();
|
||||||
@@ -562,6 +592,9 @@ mod tester {
|
|||||||
let h = n.hovedflate(1.0).unwrap();
|
let h = n.hovedflate(1.0).unwrap();
|
||||||
assert!((h.areal - 12.0).abs() < 1e-9, "areal ble {}", h.areal);
|
assert!((h.areal - 12.0).abs() < 1e-9, "areal ble {}", h.areal);
|
||||||
assert!(h.normal[2].abs() > 0.99, "normal ble {:?}", h.normal);
|
assert!(h.normal[2].abs() > 0.99, "normal ble {:?}", h.normal);
|
||||||
|
// Midlere høyde er halve tykkelsen, og overkanten er tykkelsen.
|
||||||
|
assert!((h.hoyde - z / 2.0).abs() < 1e-9, "høyde ble {}", h.hoyde);
|
||||||
|
assert!((h.topp - z).abs() < 1e-9, "topp ble {}", h.topp);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Et hull i flaten skal trekkes fra, siden hullet mangler trekanter.
|
/// Et hull i flaten skal trekkes fra, siden hullet mangler trekanter.
|
||||||
|
|||||||
@@ -215,6 +215,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
lufting: Lufting::default(),
|
lufting: Lufting::default(),
|
||||||
drift: Drift::default(),
|
drift: Drift::default(),
|
||||||
scenario: Vec::new(),
|
scenario: Vec::new(),
|
||||||
|
rapport: Rapport::default(),
|
||||||
};
|
};
|
||||||
Ok(Importrapport { bygning, merknader })
|
Ok(Importrapport { bygning, merknader })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,26 @@ pub struct Bygning {
|
|||||||
pub drift: Drift,
|
pub drift: Drift,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
pub scenario: Vec<Scenario>,
|
pub scenario: Vec<Scenario>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub rapport: Rapport,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Utseendet på rapporten, slik at den kan følge firmaets øvrige dokumenter.
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct Rapport {
|
||||||
|
/// Logo på forsiden. Filen kopieres til utdatakatalogen ved siden av rapporten.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub logo: Option<PathBuf>,
|
||||||
|
/// Aksentfarge, for eksempel «#880000». Utelatt: enoks egen.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub farge: Option<String>,
|
||||||
|
/// Avsenderlinje nederst på hver side, for eksempel firma og organisasjonsnummer.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub avsender: Option<String>,
|
||||||
|
/// Skrift for overskrifter. Utelatt: enoks egen.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub skrift_overskrift: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
|||||||
+293
-16
@@ -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;
|
||||||
@@ -66,6 +67,12 @@ fn rad(celler: &[String]) -> String {
|
|||||||
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> {
|
||||||
std::fs::create_dir_all(katalog)?;
|
std::fs::create_dir_all(katalog)?;
|
||||||
std::fs::write(katalog.join("stil.typ"), STIL)?;
|
std::fs::write(katalog.join("stil.typ"), STIL)?;
|
||||||
|
// Logoen følger med, slik at rapporten kan kompileres for seg selv senere. Mangler
|
||||||
|
// filen, lages rapporten uten den.
|
||||||
|
if let (Some(logo), Some(navn)) = (&r.bygning.rapport.logo, logo_navn(r)) {
|
||||||
|
std::fs::copy(logo, katalog.join(navn))
|
||||||
|
.with_context(|| format!("kopierer logoen {}", logo.display()))?;
|
||||||
|
}
|
||||||
std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?;
|
std::fs::write(katalog.join("fig-maaned.svg"), svg::maanedsdiagram(r))?;
|
||||||
std::fs::write(katalog.join("fig-varighet.svg"), svg::varighetskurve(r))?;
|
std::fs::write(katalog.join("fig-varighet.svg"), svg::varighetskurve(r))?;
|
||||||
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
|
std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?;
|
||||||
@@ -100,15 +107,32 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String {
|
|||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
|
|
||||||
s.push_str("#import \"stil.typ\": *\n\n");
|
s.push_str("#import \"stil.typ\": *\n\n");
|
||||||
s.push_str(&format!(
|
let rap = &r.bygning.rapport;
|
||||||
"#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n",
|
let mut valg = format!(
|
||||||
e(&l.navn),
|
"tittel: \"{} — energirapport\", bunntekst: \"{}\"",
|
||||||
e(&bunntekst(r))
|
streng(&l.navn),
|
||||||
));
|
streng(&bunntekst(r))
|
||||||
|
);
|
||||||
|
if let Some(a) = &rap.avsender {
|
||||||
|
valg.push_str(&format!(", avsender: \"{}\"", streng(a)));
|
||||||
|
}
|
||||||
|
if let Some(f) = &rap.farge {
|
||||||
|
valg.push_str(&format!(", farge: rgb(\"{}\")", streng(f)));
|
||||||
|
}
|
||||||
|
if let Some(sk) = &rap.skrift_overskrift {
|
||||||
|
valg.push_str(&format!(", skrift: \"{}\"", streng(sk)));
|
||||||
|
}
|
||||||
|
s.push_str(&format!("#show: oppsett.with({valg})\n\n"));
|
||||||
|
|
||||||
// ── forside ─────────────────────────────────────────────────────
|
// ── forside ─────────────────────────────────────────────────────
|
||||||
|
if let Some(navn) = logo_navn(r) {
|
||||||
s.push_str(&format!(
|
s.push_str(&format!(
|
||||||
"#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\
|
"#align(right, image(\"{}\", height: 1.7cm))\n\n#v(-0.9cm)\n",
|
||||||
|
streng(&navn)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
s.push_str(&format!(
|
||||||
|
"#v(1.2cm)\n#forsidetittel[Energirapport]\n\n\
|
||||||
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
#v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n",
|
||||||
e(&l.navn)
|
e(&l.navn)
|
||||||
));
|
));
|
||||||
@@ -414,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"
|
||||||
@@ -430,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(),
|
||||||
])
|
])
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@@ -473,14 +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 {
|
||||||
s.push_str("#tabell(\n kolonner: (auto, auto, 1fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n");
|
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");
|
||||||
for f in &l.forutsetninger {
|
for f in grupper_forutsetninger(&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)));
|
||||||
@@ -539,6 +627,195 @@ 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.
|
||||||
|
fn streng(s: &str) -> String {
|
||||||
|
s.replace('\\', "\\\\").replace('"', "\\\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filnavnet på logoen, når den er oppgitt og finnes.
|
||||||
|
fn logo_navn(r: &Resultat) -> Option<String> {
|
||||||
|
let logo = r.bygning.rapport.logo.as_ref()?;
|
||||||
|
if !logo.exists() {
|
||||||
|
eprintln!(
|
||||||
|
" ! rapport.logo: fant ikke {}; rapporten lages uten logo.",
|
||||||
|
logo.display()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(logo.file_name()?.to_str()?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
fn bunntekst(r: &Resultat) -> String {
|
fn bunntekst(r: &Resultat) -> String {
|
||||||
let b = &r.bygning.bygning;
|
let b = &r.bygning.bygning;
|
||||||
let mut deler = Vec::new();
|
let mut deler = Vec::new();
|
||||||
|
|||||||
+45
-8
@@ -7,11 +7,32 @@
|
|||||||
#let lys = rgb("#f4f1ee")
|
#let lys = rgb("#f4f1ee")
|
||||||
#let kantlinje = rgb("#ddd4cc")
|
#let kantlinje = rgb("#ddd4cc")
|
||||||
|
|
||||||
|
// Aksentfarge og overskriftsskrift kan settes av rapporten, slik at den kan følge
|
||||||
|
// firmaets øvrige dokumenter. Verdiene under er enoks egne.
|
||||||
|
#let aksent-valgt = state("aksent-valgt", aksent)
|
||||||
|
#let skrift-valgt = state("skrift-valgt", none)
|
||||||
|
|
||||||
#let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
#let font-heading = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||||
#let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
#let font-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif")
|
||||||
#let font-tall = ("DejaVu Sans Mono", "Liberation Mono")
|
#let font-tall = ("DejaVu Sans Mono", "Liberation Mono")
|
||||||
|
|
||||||
#let oppsett(tittel: "", undertittel: "", bunntekst: "", doc) = {
|
/// Skriften rapporten har bedt om, ellers enoks egen.
|
||||||
|
#let overskriftsskrift() = {
|
||||||
|
let valgt = skrift-valgt.get()
|
||||||
|
if valgt == none { font-heading } else { (valgt,) + font-heading }
|
||||||
|
}
|
||||||
|
|
||||||
|
#let oppsett(
|
||||||
|
tittel: "",
|
||||||
|
undertittel: "",
|
||||||
|
bunntekst: "",
|
||||||
|
avsender: "",
|
||||||
|
farge: none,
|
||||||
|
skrift: none,
|
||||||
|
doc,
|
||||||
|
) = {
|
||||||
|
aksent-valgt.update(if farge == none { aksent } else { farge })
|
||||||
|
skrift-valgt.update(skrift)
|
||||||
set page(
|
set page(
|
||||||
paper: "a4",
|
paper: "a4",
|
||||||
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
|
margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm),
|
||||||
@@ -20,32 +41,36 @@
|
|||||||
grid(
|
grid(
|
||||||
columns: (1fr, auto),
|
columns: (1fr, auto),
|
||||||
align: (left + horizon, right + horizon),
|
align: (left + horizon, right + horizon),
|
||||||
text(font: font-heading, size: 8pt, fill: mid)[#tittel],
|
text(font: overskriftsskrift(), size: 8pt, fill: mid)[#tittel],
|
||||||
text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()],
|
text(font: font-body, size: 8pt, fill: mid)[#counter(page).display()],
|
||||||
)
|
)
|
||||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
footer: context {
|
footer: context {
|
||||||
if counter(page).get().first() > 1 and bunntekst != "" {
|
if counter(page).get().first() > 1 and (bunntekst != "" or avsender != "") {
|
||||||
line(length: 100%, stroke: 0.4pt + kantlinje)
|
line(length: 100%, stroke: 0.4pt + kantlinje)
|
||||||
v(3pt)
|
v(3pt)
|
||||||
|
if avsender != "" {
|
||||||
|
text(font: font-body, size: 7.5pt, fill: mid)[#avsender]
|
||||||
|
linebreak()
|
||||||
|
}
|
||||||
text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst]
|
text(font: font-body, size: 7.5pt, fill: mid)[#bunntekst]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
|
set text(font: font-body, size: 10pt, fill: moerk, lang: "nb")
|
||||||
set par(justify: true, leading: 0.62em)
|
set par(justify: true, leading: 0.62em)
|
||||||
show heading: it => {
|
show heading: it => context {
|
||||||
set text(font: font-heading, fill: aksent)
|
set text(font: overskriftsskrift(), fill: aksent-valgt.get())
|
||||||
block(above: 1.2em, below: 0.6em, it)
|
block(above: 1.2em, below: 0.6em, it)
|
||||||
}
|
}
|
||||||
show heading.where(level: 1): it => {
|
show heading.where(level: 1): it => context {
|
||||||
set text(size: 15pt, weight: "bold")
|
set text(size: 15pt, weight: "bold")
|
||||||
block(above: 1.6em, below: 0.7em)[
|
block(above: 1.6em, below: 0.7em)[
|
||||||
#it.body
|
#it.body
|
||||||
#v(-0.45em)
|
#v(-0.45em)
|
||||||
#line(length: 100%, stroke: 1pt + aksent)
|
#line(length: 100%, stroke: 1pt + aksent-valgt.get())
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
show heading.where(level: 2): set text(size: 11.5pt, weight: "bold")
|
show heading.where(level: 2): set text(size: 11.5pt, weight: "bold")
|
||||||
@@ -55,7 +80,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Tabell med stripete rader og linje under overskriften.
|
// Tabell med stripete rader og linje under overskriften.
|
||||||
#let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = {
|
#let tabell(kolonner: (), overskrift: (), rader: (), justering: none, sum-rader: ()) = context {
|
||||||
|
let aksent = aksent-valgt.get()
|
||||||
let j = if justering == none { kolonner.map(_ => right) } else { justering }
|
let j = if justering == none { kolonner.map(_ => right) } else { justering }
|
||||||
block(breakable: true, table(
|
block(breakable: true, table(
|
||||||
columns: kolonner,
|
columns: kolonner,
|
||||||
@@ -71,6 +97,16 @@
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tittelen på forsiden, i samme skrift og farge som overskriftene.
|
||||||
|
#let forsidetittel(tekst) = context {
|
||||||
|
text(
|
||||||
|
font: overskriftsskrift(),
|
||||||
|
size: 26pt,
|
||||||
|
weight: "bold",
|
||||||
|
fill: aksent-valgt.get(),
|
||||||
|
)[#tekst]
|
||||||
|
}
|
||||||
|
|
||||||
// Stor karakterrute på forsiden.
|
// Stor karakterrute på forsiden.
|
||||||
#let karakterrute(bokstav, verdi, farge) = {
|
#let karakterrute(bokstav, verdi, farge) = {
|
||||||
box(
|
box(
|
||||||
@@ -99,3 +135,4 @@
|
|||||||
stroke: (left: 2pt + aksent-lys),
|
stroke: (left: 2pt + aksent-lys),
|
||||||
text(size: 9pt)[#innhold],
|
text(size: 9pt)[#innhold],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user