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:
bl
2026-09-15 20:05:20 +02:00
co-authored by Claude Opus 5
parent d02a3d9666
commit 6d87ef496f
6 changed files with 348 additions and 23 deletions
+90 -6
View File
@@ -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
View File
@@ -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}"));
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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"));
}