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:
+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();
|
||||
|
||||
Reference in New Issue
Block a user