Lag bygning.toml av IFC-modellen og en tilleggsfil
«enok import modell.ifc --tillegg prosjekt.toml» fletter inn det modellen ikke beskriver, som varmeanlegg og tiltak, og rettelser der den mangler noe. Tilleggsfilen kan holde rom utenfor oppvarmet BRA, og et navn med «*» treffer alle elementer det passer på, også i scenarioer. Importen er nå deterministisk: entitetene leses i fast rekkefølge, helningen arealvektes når flater slås sammen, og skrå tak får alltid normalen oppover. Vegger får himmelretningen i navnet, slik at tilleggsfiler kan vise til dem. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
@@ -68,6 +68,17 @@ bygning med store vindusflater fått urealistisk høye innetemperaturer om våre
|
||||
lagret solvarme ville kunstig redusert oppvarmingsbehovet. Grensen settes til den normerte
|
||||
settpunkttemperaturen for kjøling, og luftingen koster ingen energi.
|
||||
|
||||
**Ventilasjon.** Varmegjenvinneren reguleres ned når den ellers ville varmet tilluften over
|
||||
settpunktet. Et anlegg med `kjolebatteri = true` holder tilluften på normert temperatur i
|
||||
driftstiden, slik DiBKs modellbygg for energirammene er regnet, og energien føres som
|
||||
ventilasjonskjøling.
|
||||
|
||||
**Bygningskategorier.** Normerte laster og driftstider er lagt inn for småhus, boligblokk,
|
||||
barnehage, kontorbygg, skolebygg, idrettsbygg og kulturbygg. Driftstidene har timer per døgn,
|
||||
dager per uke og uker per år; lastene gjelder bare på driftsdager, og ukene uten drift legges
|
||||
i ferier. For sykehus, sykehjem, hotell, forretningsbygg, universitet og lett industri er det
|
||||
ikke funnet offentlige kilder, og de krever en egen fil i `--data KATALOG/normerte/`.
|
||||
|
||||
**Tiltak.** Scenarier i samme fil gir før-og-etter-tall og karakterer i én tabell.
|
||||
|
||||
## Installasjon
|
||||
@@ -85,6 +96,7 @@ Typst må være installert for PDF-rapporten. Uten Typst kjører du med `--ingen
|
||||
| --- | --- |
|
||||
| `enok beregn FIL.toml` | Beregner og skriver rapport, CSV og JSON til `ut/<navn>/` |
|
||||
| `enok import FIL` | Leser IFC, gbXML eller regneark og skriver `bygning.toml` |
|
||||
| `enok import FIL --tillegg prosjekt.toml` | Som over, med en tilleggsfil flettet inn (se nedenfor) |
|
||||
| `enok klima --liste` | Lister alle kommuner med klimadata |
|
||||
| `enok klima --kommune Oslo` | Viser nøkkeltall for ett klimasted |
|
||||
| `enok skala smaahus --areal 140` | Viser karakterskalaen med arealkorreksjon |
|
||||
@@ -154,6 +166,7 @@ over_terreng = 0.6 # høyden som står over terreng
|
||||
|
||||
[ventilasjon]
|
||||
type = "naturlig" # naturlig | avtrekk | balansert
|
||||
# kjolebatteri = true # balansert anlegg med kjølebatteri
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "el_kjel" # direkte_el, luft_luft_vp, vaeske_vann_vp, vedovn, fjernvarme, …
|
||||
@@ -167,6 +180,7 @@ endringer.vindu = [{ navn = "Vinduer sør", u_verdi = 0.80, g_verdi = 0.50 }]
|
||||
|
||||
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.
|
||||
Et navn med `*` treffer alle elementer det passer på, for eksempel `navn = "Yttervegg*"`.
|
||||
|
||||
Ferdige eksempler ligger i `eksempler/`.
|
||||
|
||||
@@ -178,6 +192,53 @@ Ferdige eksempler ligger i `eksempler/`.
|
||||
| gbXML | Verktøy som eksporterer gbXML | Rom, flater, åpninger, konstruksjoner med lag |
|
||||
| Regneark | *Fil → Eksporter → Export Worksheet* | Enkel CSV med én rad per bygningsdel |
|
||||
|
||||
### Modellen og en tilleggsfil
|
||||
|
||||
Modellen bør være kilden til alt den kan beskrive. Det den ikke beskriver, som varmeanlegg,
|
||||
lekkasjetall og tiltak, og rettelser der den mangler noe, legges i en tilleggsfil som flettes
|
||||
inn ved hver import etter de samme reglene som scenarioene:
|
||||
|
||||
```sh
|
||||
enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
|
||||
enok beregn bygning.toml
|
||||
```
|
||||
|
||||
```toml
|
||||
# prosjekt.toml
|
||||
[import]
|
||||
uoppvarmede_rom = ["Kott", "Bod"] # holdes utenfor oppvarmet BRA
|
||||
|
||||
[[flate]]
|
||||
navn = "Yttervegg*" # alle ytterveggene, uansett himmelretning
|
||||
masseklasse = "lett"
|
||||
|
||||
[[flate]]
|
||||
navn = "Roof Slab" # skråtaket mangler U-verdi i modellen
|
||||
u_verdi = 0.55
|
||||
|
||||
[[vindu]]
|
||||
navn = "V-*"
|
||||
g_verdi = 0.76
|
||||
|
||||
[[sone]]
|
||||
navn = "Kjeller"
|
||||
andel_distribusjonstap = 1.0
|
||||
|
||||
[[sone.flate]]
|
||||
navn = "Kjellermur*"
|
||||
over_terreng = 0.6
|
||||
|
||||
[[oppvarming.kilde]]
|
||||
type = "el_kjel"
|
||||
andel_rom = 1.0
|
||||
andel_tappevann = 1.0
|
||||
```
|
||||
|
||||
Flatene får navn etter konstruksjonen i modellen, og vegger får himmelretningen etter navnet
|
||||
(«Yttervegg, sør»), slik at tilleggsfilen kan vise til dem. Importen er deterministisk: samme
|
||||
modell og tilleggsfil gir alltid samme `bygning.toml`. Et mønster som ikke treffer noe, nevnes
|
||||
i merknadene, og et vanlig navn som ikke finnes i modellen, legges til som ny oppføring.
|
||||
|
||||
### Hva enok henter ut av en IFC-modell
|
||||
|
||||
| Opplysning | Hvor den kommer fra |
|
||||
@@ -267,8 +328,9 @@ vertskjøreren `fish` på samme måte som de andre prosjektene:
|
||||
|
||||
### Energirapport fra et byggeprosjekt
|
||||
|
||||
Et prosjektlager med sin egen IFC-modell og `bygning.toml` trenger ikke Rust. Det henter
|
||||
binærfilen fra enoks siste utgivelse:
|
||||
Et prosjektlager med sin egen IFC-modell og tilleggsfil trenger ikke Rust. Det henter
|
||||
binærfilen fra enoks siste utgivelse. Én jobb lager `bygning.toml` av modellen, og en annen
|
||||
beregner rapporten av den:
|
||||
|
||||
```yaml
|
||||
name: Energirapport
|
||||
@@ -276,14 +338,14 @@ on:
|
||||
release:
|
||||
types: [published]
|
||||
jobs:
|
||||
rapport:
|
||||
bygningsmodell:
|
||||
runs-on: fish
|
||||
defaults:
|
||||
run:
|
||||
shell: fish {0}
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v6
|
||||
- name: Hent enok og lag rapporten
|
||||
- name: Hent enok og lag bygning.toml
|
||||
env:
|
||||
TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
@@ -293,6 +355,27 @@ jobs:
|
||||
curl -sSL --fail -o enok --header "Authorization: token $TOKEN" \
|
||||
"$GITHUB_SERVER_URL/klingenbergbygg/enok/releases/download/$TAG/enok"
|
||||
chmod +x enok
|
||||
./enok import modell.ifc --tillegg prosjekt.toml --ut bygning.toml
|
||||
- uses: https://gitea.com/actions/upload-artifact@v4
|
||||
with:
|
||||
name: bygningsmodell
|
||||
path: |
|
||||
bygning.toml
|
||||
enok
|
||||
rapport:
|
||||
needs: bygningsmodell
|
||||
runs-on: fish
|
||||
defaults:
|
||||
run:
|
||||
shell: fish {0}
|
||||
steps:
|
||||
- uses: https://gitea.com/actions/checkout@v6
|
||||
- uses: https://gitea.com/actions/download-artifact@v4
|
||||
with:
|
||||
name: bygningsmodell
|
||||
- name: Lag rapporten
|
||||
run: |
|
||||
chmod +x enok
|
||||
./enok beregn bygning.toml --ut dist
|
||||
```
|
||||
|
||||
@@ -310,12 +393,13 @@ Motoren er kontrollert mot uavhengige referanser:
|
||||
| Varmebehov som funksjon av temperaturdifferanse | eksakt lineært |
|
||||
| Varmetapsbudsjett for TEK17-referansebygget | identisk med publisert tabell, 0,69 W/(m²K) |
|
||||
| Normerte energiposter for småhus | varmtvann 25,1, belysning 11,4, teknisk utstyr 17,5 kWh/(m²·år) |
|
||||
| Faste energiposter for yrkesbyggene | belysning, utstyr og varmtvann treffer energibudsjettene i kildene |
|
||||
|
||||
### Kjent avvik
|
||||
|
||||
Standard Norges lanseringseksempel for NS 3031:2025 oppgir 46,2 kWh/(m²·år) romoppvarming og
|
||||
106,4 kWh/(m²·år) netto energibehov for TEK17-referansebygget beregnet med Simien 8. enok
|
||||
beregner 27,2 og 86,7 for den samme bygningen.
|
||||
beregner 27,1 og 86,6 for den samme bygningen.
|
||||
|
||||
Varmetapsbudsjettet, de normerte energipostene, solinnstrålingen og energibalansen stemmer
|
||||
hver for seg, så avviket ligger i hvor stor del av solvarmen som nyttiggjøres. NS 3031:2025
|
||||
@@ -336,7 +420,7 @@ Alle normerte verdier ligger i `data/` og kan overstyres med `--data KATALOG`.
|
||||
| --- | --- |
|
||||
| `skala.toml` | Energikarakterskalaen fra 1.1.2026, Enova SF via Multiconsult-notatet «Grunnlag for ny energikarakterskala», 19.12.2025 |
|
||||
| `vekting.toml` | Vektingsfaktorer etter energimerkeforskriften § 10a med NVEs veileder |
|
||||
| `normerte/*.toml` | Normerte laster, driftstider og settpunkt. Utgangspunktet er NS 3031:2014 Tillegg A; varmtvann er oppdatert til NS 3031:2025-verdien for småhus. Timesprofiler fra NS 3031:2025 kan legges inn i `profil_*`-feltene |
|
||||
| `normerte/*.toml` | Normerte laster, driftstider og settpunkt. Utgangspunktet er NS 3031:2014 Tillegg A; varmtvann for boliger er oppdatert til NS 3031:2025-verdien for småhus. Tillegg A er ikke offentlig, så yrkesbyggverdiene er hentet fra publiserte Simien-utskrifter og DiBK-utredninger. Hver fil oppgir kildene og hvilke verdier som er antatt. Timesprofiler fra NS 3031:2025 kan legges inn i `profil_*`-feltene |
|
||||
| `varmepumpe_spf.toml` | Veiledende årsvarmefaktorer, NS 3031:2014 tabell B.9-3 |
|
||||
| `materialer.toml` | Materialdata og typiske U-verdier per byggeperiode |
|
||||
| `klimakorreksjon.csv` | Klimakorrigeringsfaktorer, Standard Norge SN/K 34, doi 10.5281/zenodo.15041587 |
|
||||
|
||||
+92
-3
@@ -11,6 +11,7 @@
|
||||
|
||||
use crate::inndata::ifcgeom;
|
||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||
use crate::inndata::toml_inn::Importvalg;
|
||||
use crate::klima::geo;
|
||||
use crate::modell::*;
|
||||
use crate::Result;
|
||||
@@ -29,17 +30,26 @@ const VEGGTYPER: [&str; 3] = ["IFCWALL", "IFCWALLSTANDARDCASE", "IFCCURTAINWALL"
|
||||
const PLATETYPER: [&str; 3] = ["IFCSLAB", "IFCROOF", "IFCSLABSTANDARDCASE"];
|
||||
|
||||
pub fn les(sti: &Path) -> Result<Importrapport> {
|
||||
les_med(sti, &Importvalg::default())
|
||||
}
|
||||
|
||||
pub fn les_med(sti: &Path, valg: &Importvalg) -> Result<Importrapport> {
|
||||
let raa = std::fs::read(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let tekst = String::from_utf8_lossy(&raa);
|
||||
fra_tekst(
|
||||
fra_tekst_med(
|
||||
&tekst,
|
||||
sti.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("Bygning"),
|
||||
valg,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
fra_tekst_med(tekst, standardnavn, &Importvalg::default())
|
||||
}
|
||||
|
||||
pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Result<Importrapport> {
|
||||
let fil = step::les(tekst);
|
||||
let mut merknader = Vec::new();
|
||||
|
||||
@@ -88,6 +98,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
let mut rom: Vec<(String, f64)> = Vec::new();
|
||||
let mut kjellerrom: Vec<(String, f64)> = Vec::new();
|
||||
let mut mistenkelige: Vec<String> = Vec::new();
|
||||
let mut uoppvarmede: 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);
|
||||
@@ -111,6 +122,14 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
kjellervolum += romvolum;
|
||||
} else {
|
||||
let lavt = navn.to_lowercase();
|
||||
if valg
|
||||
.uoppvarmede_rom
|
||||
.iter()
|
||||
.any(|u| lavt.contains(&u.to_lowercase()))
|
||||
{
|
||||
uoppvarmede.push(format!("{navn} {areal:.1} m²"));
|
||||
continue;
|
||||
}
|
||||
if ["kott", "bod", "loft", "garasje", "carport", "uoppvarmet"]
|
||||
.iter()
|
||||
.any(|o| lavt.contains(o))
|
||||
@@ -136,6 +155,12 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
mistenkelige.join(", ")
|
||||
));
|
||||
}
|
||||
if !uoppvarmede.is_empty() {
|
||||
merknader.push(format!(
|
||||
"Disse rommene er uoppvarmet etter tilleggsfilen og holdt utenfor oppvarmet BRA: {}.",
|
||||
uoppvarmede.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
// ── plassering og geometri for alle bygningsdeler ───────────────
|
||||
let typer: Vec<&str> = VEGGTYPER.iter().chain(PLATETYPER.iter()).copied().collect();
|
||||
@@ -398,6 +423,10 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
));
|
||||
}
|
||||
|
||||
// Tilleggsfiler og scenarioer viser til flatene med navn, så navnene må være entydige.
|
||||
unike_navn(&mut flater);
|
||||
unike_navn(&mut kjellerflater);
|
||||
|
||||
// ── merknader ───────────────────────────────────────────────────
|
||||
if bra <= 0.0 {
|
||||
bra = flater
|
||||
@@ -571,6 +600,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
||||
}
|
||||
};
|
||||
|
||||
vinduer.sort_by(|a, b| a.navn.cmp(&b.navn));
|
||||
|
||||
let bygning = Bygning {
|
||||
bygning: BygningInfo {
|
||||
navn,
|
||||
@@ -668,9 +699,13 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
for f in flater.drain(..) {
|
||||
match samlet.iter_mut().find(|g| nokkel(g) == nokkel(&f)) {
|
||||
Some(g) => {
|
||||
// Arealvektet himmelretning, slik at summen peker riktig vei.
|
||||
// Arealvektet himmelretning og helning, slik at summen peker riktig vei og ikke
|
||||
// avhenger av rekkefølgen elementene kommer i.
|
||||
let sum = g.areal + f.areal;
|
||||
g.orientering = (g.orientering * g.areal + f.orientering * f.areal) / sum;
|
||||
if let (Some(a), Some(b)) = (g.helning, f.helning) {
|
||||
g.helning = Some((a * g.areal + b * f.areal) / sum);
|
||||
}
|
||||
g.areal = sum;
|
||||
}
|
||||
None => samlet.push(f),
|
||||
@@ -689,6 +724,36 @@ fn slaa_sammen(flater: &mut Vec<Flate>) {
|
||||
*flater = samlet;
|
||||
}
|
||||
|
||||
/// Gjør flatenavnene entydige. Vegger får himmelretningen etter konstruksjonsnavnet, og andre
|
||||
/// flater som fortsatt deler navn, nummereres.
|
||||
fn unike_navn(flater: &mut [Flate]) {
|
||||
// Fast rekkefølge, slik at samme modell alltid gir samme fil og samme nummerering.
|
||||
flater.sort_by(|a, b| {
|
||||
(a.typ as u8, &a.navn)
|
||||
.cmp(&(b.typ as u8, &b.navn))
|
||||
.then(a.orientering.total_cmp(&b.orientering))
|
||||
.then(b.areal.total_cmp(&a.areal))
|
||||
});
|
||||
for f in flater.iter_mut().filter(|f| f.typ == FlateType::Vegg) {
|
||||
f.navn = format!("{}, {}", f.navn, himmelretning(f.orientering));
|
||||
}
|
||||
let mut antall: HashMap<String, usize> = HashMap::new();
|
||||
for f in flater.iter_mut() {
|
||||
let n = antall.entry(f.navn.clone()).or_insert(0);
|
||||
*n += 1;
|
||||
if *n > 1 {
|
||||
f.navn = format!("{} ({n})", f.navn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn himmelretning(grader: f64) -> &'static str {
|
||||
const RETNINGER: [&str; 8] = [
|
||||
"nord", "nordøst", "øst", "sørøst", "sør", "sørvest", "vest", "nordvest",
|
||||
];
|
||||
RETNINGER[((grader / 45.0).round() as i64).rem_euclid(8) as usize]
|
||||
}
|
||||
|
||||
/// En etasje i modellen, med navn og høyde over byggets referansenivå.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Etasje {
|
||||
@@ -1150,7 +1215,10 @@ fn retning_av(d: &Del, cx: f64, cy: f64, sant_nord: f64) -> (f64, f64) {
|
||||
let mut g = [n[0] * dx - n[1] * dy, n[0] * dy + n[1] * dx, n[2]];
|
||||
|
||||
let vannrett = (g[0] * g[0] + g[1] * g[1]).sqrt();
|
||||
if vannrett > 0.2 {
|
||||
// Et skrått tak har også en vannrett komponent, men skal alltid ha normalen oppover;
|
||||
// ellers kan undersiden bli hovedflaten og gi helning 180° minus takvinkelen.
|
||||
let skraa_plate = PLATETYPER.contains(&d.typ.as_str()) && g[2].abs() > 0.1;
|
||||
if vannrett > 0.2 && !skraa_plate {
|
||||
// Veggflate: velg den normalen som peker bort fra bygningens tyngdepunkt.
|
||||
if let Some((x, y, _, _, _)) = d.plass {
|
||||
if g[0] * (x - cx) + g[1] * (y - cy) < 0.0 {
|
||||
@@ -1686,6 +1754,27 @@ mod tester {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vegger_faar_himmelretning_i_navnet() {
|
||||
let r = fra_tekst(&prove(), "prove").unwrap();
|
||||
let mut navn: Vec<&str> = r.bygning.flate.iter().map(|f| f.navn.as_str()).collect();
|
||||
navn.sort();
|
||||
assert_eq!(navn, vec!["Nordvegg, nord", "Sorvegg, sør"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn uoppvarmede_rom_fra_tilleggsfilen_holdes_utenfor_bra() {
|
||||
let valg = Importvalg {
|
||||
uoppvarmede_rom: vec!["STUE".into()],
|
||||
};
|
||||
let r = fra_tekst_med(&prove(), "prove", &valg).unwrap();
|
||||
assert_ne!(r.bygning.bygning.oppvarmet_bra, 100.0);
|
||||
assert!(r
|
||||
.merknader
|
||||
.iter()
|
||||
.any(|m| m.contains("uoppvarmet etter tilleggsfilen")));
|
||||
}
|
||||
|
||||
/// Lengdeenheten skal hentes gjennom prosjektets enhetsoppsett, ikke ved å lete
|
||||
/// gjennom alle enheter i filen. Prøvefilen har både millimeter og meter.
|
||||
#[test]
|
||||
|
||||
+3
-1
@@ -7,4 +7,6 @@ pub mod regneark;
|
||||
pub mod step;
|
||||
pub mod toml_inn;
|
||||
|
||||
pub use toml_inn::{les_bygning, slaa_sammen_scenario};
|
||||
pub use toml_inn::{
|
||||
flett_tillegg, les_bygning, les_tillegg, slaa_sammen_scenario, Importvalg, Tillegg,
|
||||
};
|
||||
|
||||
+4
-3
@@ -3,7 +3,7 @@
|
||||
//! Filen består av linjer på formen `#12=IFCWALL('guid',#3,'Navn',...);`. Denne modulen
|
||||
//! deler filen i entiteter med type og argumenter, uten å tolke IFC-skjemaet.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Arg {
|
||||
@@ -58,7 +58,8 @@ impl Entitet {
|
||||
}
|
||||
|
||||
pub struct Fil {
|
||||
pub entiteter: HashMap<u64, Entitet>,
|
||||
/// Sortert på entitetsnummer, slik at gjennomløp alltid kommer i samme rekkefølge.
|
||||
pub entiteter: BTreeMap<u64, Entitet>,
|
||||
}
|
||||
|
||||
impl Fil {
|
||||
@@ -83,7 +84,7 @@ impl Fil {
|
||||
|
||||
/// Deler en STEP-fil i entiteter.
|
||||
pub fn les(tekst: &str) -> Fil {
|
||||
let mut entiteter = HashMap::new();
|
||||
let mut entiteter = BTreeMap::new();
|
||||
let mut setning = String::new();
|
||||
let mut i_streng = false;
|
||||
let mut i_data = false;
|
||||
|
||||
+123
-6
@@ -1,8 +1,9 @@
|
||||
//! Leser `bygning.toml` og fletter inn scenarioendringer.
|
||||
//! Leser `bygning.toml` og fletter inn scenarioendringer og tilleggsfiler.
|
||||
|
||||
use crate::modell::{Bygning, Scenario};
|
||||
use crate::Result;
|
||||
use anyhow::Context;
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
use toml::Value;
|
||||
|
||||
@@ -11,19 +12,84 @@ pub fn les_bygning(sti: &Path) -> Result<Bygning> {
|
||||
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))
|
||||
}
|
||||
|
||||
/// Valg for selve importen, fra tabellen `[import]` i en tilleggsfil.
|
||||
#[derive(Deserialize, Default, Clone, Debug)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Importvalg {
|
||||
/// Rom som står uoppvarmet og holdes utenfor oppvarmet BRA. Et rom treffes når navnet
|
||||
/// inneholder en av tekstene, uten hensyn til store og små bokstaver.
|
||||
#[serde(default)]
|
||||
pub uoppvarmede_rom: Vec<String>,
|
||||
}
|
||||
|
||||
/// En tilleggsfil til en importert modell: det modellen ikke beskriver, som drift, varmeanlegg
|
||||
/// og scenarioer, og rettelser til det den beskriver.
|
||||
pub struct Tillegg {
|
||||
pub endringer: Value,
|
||||
pub import: Importvalg,
|
||||
}
|
||||
|
||||
pub fn les_tillegg(sti: &Path) -> Result<Tillegg> {
|
||||
let tekst = std::fs::read_to_string(sti).with_context(|| format!("leser {}", sti.display()))?;
|
||||
let mut endringer: Value =
|
||||
toml::from_str(&tekst).with_context(|| format!("tolker {}", sti.display()))?;
|
||||
let import = match endringer.as_table_mut().and_then(|t| t.remove("import")) {
|
||||
Some(v) => v
|
||||
.try_into()
|
||||
.with_context(|| format!("tolker [import] i {}", sti.display()))?,
|
||||
None => Importvalg::default(),
|
||||
};
|
||||
Ok(Tillegg { endringer, import })
|
||||
}
|
||||
|
||||
/// Fletter en tilleggsfil inn i en importert bygning, etter de samme reglene som scenarioene.
|
||||
///
|
||||
/// Returnerer også navnemønstrene med `*` som ikke traff noen oppføring, slik at en tilleggsfil
|
||||
/// som har gått ut av takt med modellen blir synlig.
|
||||
pub fn flett_tillegg(b: &Bygning, tillegg: &Value) -> Result<(Bygning, Vec<String>)> {
|
||||
let mut grunn = Value::try_from(b).context("serialiserer den importerte bygningen")?;
|
||||
let mut bom = Vec::new();
|
||||
flett(&mut grunn, tillegg, &mut bom);
|
||||
let bygning = grunn.try_into().context(
|
||||
"tilleggsfilen gir en ugyldig bygningsbeskrivelse. Et navn som ikke finnes i modellen, \
|
||||
legges til som ny oppføring og må da være komplett",
|
||||
)?;
|
||||
Ok((bygning, bom))
|
||||
}
|
||||
|
||||
/// Lager en ny bygning der scenarioets endringer er flettet inn.
|
||||
///
|
||||
/// Tabeller flettes rekursivt. Tabellarrayer (`flate`, `vindu`, `oppvarming.kilde` …) matches
|
||||
/// på feltet `navn` når det finnes, ellers på `type`; elementer som ikke finnes fra før legges
|
||||
/// til, og `slett = true` fjerner et element. Andre verdier erstattes.
|
||||
/// til, og `slett = true` fjerner et element. Et navn med `*` er et mønster som treffer alle
|
||||
/// oppføringer det passer på, for eksempel `"Yttervegg*"`. Andre verdier erstattes.
|
||||
pub fn slaa_sammen_scenario(b: &Bygning, s: &Scenario) -> Result<Bygning> {
|
||||
let mut grunn = Value::try_from(b).context("serialiserer bygningen for scenario")?;
|
||||
flett(&mut grunn, &s.endringer);
|
||||
flett(&mut grunn, &s.endringer, &mut Vec::new());
|
||||
grunn
|
||||
.try_into()
|
||||
.with_context(|| format!("scenarioet «{}» gir en ugyldig bygningsbeskrivelse", s.navn))
|
||||
}
|
||||
|
||||
/// Om `navn` passer på mønsteret, der `*` står for vilkårlig tekst.
|
||||
fn passer(moenster: &str, navn: &str) -> bool {
|
||||
let deler: Vec<&str> = moenster.split('*').collect();
|
||||
let (forst, sist) = (deler[0], deler[deler.len() - 1]);
|
||||
if deler.len() == 1 {
|
||||
return moenster == navn;
|
||||
}
|
||||
let Some(mut rest) = navn.strip_prefix(forst) else {
|
||||
return false;
|
||||
};
|
||||
for del in &deler[1..deler.len() - 1] {
|
||||
match rest.find(del) {
|
||||
Some(i) => rest = &rest[i + del.len()..],
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
rest.ends_with(sist)
|
||||
}
|
||||
|
||||
fn navnenokkel(v: &Value) -> Option<String> {
|
||||
let t = v.as_table()?;
|
||||
t.get("navn")
|
||||
@@ -39,7 +105,7 @@ fn skal_slettes(v: &Value) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn flett(grunn: &mut Value, over: &Value) {
|
||||
fn flett(grunn: &mut Value, over: &Value, bom: &mut Vec<String>) {
|
||||
match (grunn, over) {
|
||||
(Value::Table(g), Value::Table(o)) => {
|
||||
for (k, v) in o {
|
||||
@@ -47,7 +113,7 @@ fn flett(grunn: &mut Value, over: &Value) {
|
||||
continue;
|
||||
}
|
||||
match g.get_mut(k) {
|
||||
Some(eksisterende) => flett(eksisterende, v),
|
||||
Some(eksisterende) => flett(eksisterende, v, bom),
|
||||
None => {
|
||||
g.insert(k.clone(), v.clone());
|
||||
}
|
||||
@@ -57,6 +123,25 @@ fn flett(grunn: &mut Value, over: &Value) {
|
||||
(Value::Array(g), Value::Array(o)) => {
|
||||
for element in o {
|
||||
match navnenokkel(element) {
|
||||
Some(moenster) if moenster.contains('*') => {
|
||||
let treffer =
|
||||
|e: &Value| navnenokkel(e).is_some_and(|n| passer(&moenster, &n));
|
||||
let antall = g.iter().filter(|e| treffer(e)).count();
|
||||
if antall == 0 {
|
||||
bom.push(moenster.clone());
|
||||
} else if skal_slettes(element) {
|
||||
g.retain(|e| !treffer(e));
|
||||
} else {
|
||||
// Mønsteret skal ikke erstatte navnene det treffer.
|
||||
let mut endring = element.clone();
|
||||
if let Some(t) = endring.as_table_mut() {
|
||||
t.remove("navn");
|
||||
}
|
||||
for e in g.iter_mut().filter(|e| treffer(e)) {
|
||||
flett(e, &endring, bom);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(navn) => {
|
||||
let treff = g
|
||||
.iter()
|
||||
@@ -65,7 +150,7 @@ fn flett(grunn: &mut Value, over: &Value) {
|
||||
Some(i) if skal_slettes(element) => {
|
||||
g.remove(i);
|
||||
}
|
||||
Some(i) => flett(&mut g[i], element),
|
||||
Some(i) => flett(&mut g[i], element, bom),
|
||||
None if skal_slettes(element) => {}
|
||||
None => g.push(element.clone()),
|
||||
}
|
||||
@@ -133,6 +218,38 @@ u_verdi = 0.8
|
||||
assert_eq!(navn, vec!["Yttervegg", "Nytt tak"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moenster_i_navn_treffer_flere_oppforinger() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
let mut grunn = Value::try_from(&b).unwrap();
|
||||
let mut bom = Vec::new();
|
||||
let over: Value = toml::from_str(
|
||||
r#"flate = [{ navn = "*", masseklasse = "lett" }, { navn = "Yttervegg*", u_verdi = 0.3 }, { navn = "Kjeller*", u_verdi = 0.2 }]"#,
|
||||
)
|
||||
.unwrap();
|
||||
flett(&mut grunn, &over, &mut bom);
|
||||
let ny: Bygning = grunn.try_into().unwrap();
|
||||
assert_eq!(ny.flate.len(), 2);
|
||||
assert_eq!(ny.flate[0].navn, "Yttervegg");
|
||||
assert_eq!(ny.flate[0].u_verdi, Some(0.3));
|
||||
assert_eq!(ny.flate[1].u_verdi, Some(0.8));
|
||||
assert!(ny.flate.iter().all(|f| f.masseklasse.is_some()));
|
||||
assert_eq!(bom, vec!["Kjeller*".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moenster_passer() {
|
||||
assert!(passer("V-2*", "V-2 #3306"));
|
||||
assert!(passer("*sør", "Yttervegg, sør"));
|
||||
assert!(passer(
|
||||
"Kjellermur*uisolert*",
|
||||
"Kjellermur 30 cm uisolert, øst"
|
||||
));
|
||||
assert!(!passer("V-2*", "VD-2 #11162"));
|
||||
assert!(passer("Tak", "Tak"));
|
||||
assert!(!passer("Tak", "Taket"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scenario_endrer_skalarfelt() {
|
||||
let b: Bygning = toml::from_str(GRUNN).unwrap();
|
||||
|
||||
+36
-4
@@ -62,6 +62,10 @@ struct Beregn {
|
||||
struct Import {
|
||||
/// gbXML-, IFC- eller CSV-fil eksportert fra BIM-verktøyet.
|
||||
fil: PathBuf,
|
||||
/// Tilleggsfil som flettes inn i den importerte bygningen: det modellen ikke beskriver
|
||||
/// (drift, varmeanlegg, scenarioer) og rettelser til det den beskriver.
|
||||
#[arg(short, long, value_name = "FIL")]
|
||||
tillegg: Option<PathBuf>,
|
||||
/// Utdatafil. Standard: samme navn med endelsen .toml.
|
||||
#[arg(short, long, value_name = "FIL")]
|
||||
ut: Option<PathBuf>,
|
||||
@@ -217,13 +221,18 @@ fn import(a: Import) -> Result<()> {
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
let (bygning, merknader) = match endelse.as_str() {
|
||||
let tillegg = a.tillegg.as_deref().map(inndata::les_tillegg).transpose()?;
|
||||
let valg = tillegg
|
||||
.as_ref()
|
||||
.map(|t| t.import.clone())
|
||||
.unwrap_or_default();
|
||||
let (bygning, mut merknader) = match endelse.as_str() {
|
||||
"gbxml" | "xml" => {
|
||||
let r = inndata::gbxml::les(&a.fil)?;
|
||||
(r.bygning, r.merknader)
|
||||
}
|
||||
"ifc" => {
|
||||
let r = inndata::ifc::les(&a.fil)?;
|
||||
let r = inndata::ifc::les_med(&a.fil, &valg)?;
|
||||
(r.bygning, r.merknader)
|
||||
}
|
||||
"csv" => {
|
||||
@@ -234,10 +243,33 @@ fn import(a: Import) -> Result<()> {
|
||||
"ukjent filtype «{annen}». enok importerer .gbxml, .ifc og .csv (regneark fra Vectorworks)"
|
||||
),
|
||||
};
|
||||
if endelse != "ifc" && !valg.uoppvarmede_rom.is_empty() {
|
||||
merknader.push("uoppvarmede_rom i tilleggsfilen brukes bare ved import fra IFC.".into());
|
||||
}
|
||||
let (bygning, mut innhold) = match (&tillegg, &a.tillegg) {
|
||||
(Some(t), Some(sti)) => {
|
||||
let (b, bom) = inndata::flett_tillegg(&bygning, &t.endringer)?;
|
||||
for m in bom {
|
||||
merknader.push(format!(
|
||||
"Navnemønsteret «{m}» i tilleggsfilen traff ingen oppføring i modellen."
|
||||
));
|
||||
}
|
||||
let hode = format!(
|
||||
"# Generert av enok fra «{}» og tilleggsfilen «{}».\n\
|
||||
# Rett i modellen eller i tilleggsfilen; denne filen skrives på nytt ved hver import.\n\
|
||||
# Merknader fra importen:\n",
|
||||
a.fil.display(),
|
||||
sti.display()
|
||||
);
|
||||
(b, hode)
|
||||
}
|
||||
_ => (
|
||||
bygning,
|
||||
String::from("# Importert med enok. Fyll ut feltene som er merket nedenfor.\n"),
|
||||
),
|
||||
};
|
||||
let ut = a.ut.unwrap_or_else(|| a.fil.with_extension("toml"));
|
||||
let tekst = toml::to_string_pretty(&bygning).context("serialiserer bygningen til TOML")?;
|
||||
let mut innhold =
|
||||
String::from("# Importert med enok. Fyll ut feltene som er merket nedenfor.\n");
|
||||
for m in &merknader {
|
||||
innhold.push_str(&format!("# {m}\n"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user