diff --git a/Cargo.lock b/Cargo.lock index bed5aae..800949c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,7 +222,7 @@ dependencies = [ [[package]] name = "enok" -version = "0.4.1" +version = "0.4.2" dependencies = [ "anyhow", "approx", diff --git a/Cargo.toml b/Cargo.toml index 811becc..d9fac8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "enok" -version = "0.4.1" +version = "0.4.2" edition = "2021" description = "Årlig energiberegning og energirapport for boliger etter NS 3031 (timesmetode, ISO 52016-1)" license = "MIT" diff --git a/README.md b/README.md index 14c237b..9076465 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,16 @@ navn = "Nye vinduer" 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 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*"`. diff --git a/src/inndata/gbxml.rs b/src/inndata/gbxml.rs index b96cc05..b8bd61a 100644 --- a/src/inndata/gbxml.rs +++ b/src/inndata/gbxml.rs @@ -596,6 +596,7 @@ fn bygg( lufting: Lufting::default(), drift: Drift::default(), scenario: Vec::new(), + rapport: Rapport::default(), }; Ok(Importrapport { bygning, merknader }) } diff --git a/src/inndata/ifc.rs b/src/inndata/ifc.rs index 118f5af..36bd471 100644 --- a/src/inndata/ifc.rs +++ b/src/inndata/ifc.rs @@ -833,6 +833,7 @@ pub fn fra_tekst_med(tekst: &str, standardnavn: &str, valg: &Importvalg) -> Resu lufting: Lufting::default(), drift: Drift::default(), scenario: Vec::new(), + rapport: Rapport::default(), }; Ok(Importrapport { bygning, diff --git a/src/inndata/regneark.rs b/src/inndata/regneark.rs index 50a8ff0..6d8a2df 100644 --- a/src/inndata/regneark.rs +++ b/src/inndata/regneark.rs @@ -215,6 +215,7 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result { lufting: Lufting::default(), drift: Drift::default(), scenario: Vec::new(), + rapport: Rapport::default(), }; Ok(Importrapport { bygning, merknader }) } diff --git a/src/modell/mod.rs b/src/modell/mod.rs index c657c74..6545c55 100644 --- a/src/modell/mod.rs +++ b/src/modell/mod.rs @@ -82,6 +82,26 @@ pub struct Bygning { pub drift: Drift, #[serde(default, skip_serializing_if = "Vec::is_empty")] pub scenario: Vec, + #[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, + /// Aksentfarge, for eksempel «#880000». Utelatt: enoks egen. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub farge: Option, + /// Avsenderlinje nederst på hver side, for eksempel firma og organisasjonsnummer. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub avsender: Option, + /// Skrift for overskrifter. Utelatt: enoks egen. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub skrift_overskrift: Option, } #[derive(Deserialize, Serialize, Clone, Debug)] diff --git a/src/rapport/typst_ut.rs b/src/rapport/typst_ut.rs index fd51430..3845334 100644 --- a/src/rapport/typst_ut.rs +++ b/src/rapport/typst_ut.rs @@ -66,6 +66,12 @@ fn rad(celler: &[String]) -> String { pub fn skriv(r: &Resultat, katalog: &Path, scenarier: &[(String, Resultat)]) -> Result<()> { std::fs::create_dir_all(katalog)?; 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-varighet.svg"), svg::varighetskurve(r))?; std::fs::write(katalog.join("fig-karakter.svg"), svg::karakterstige(r))?; @@ -100,13 +106,30 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String { let mut s = String::new(); s.push_str("#import \"stil.typ\": *\n\n"); - s.push_str(&format!( - "#show: oppsett.with(tittel: \"{} — energirapport\", bunntekst: \"{}\")\n\n", - e(&l.navn), - e(&bunntekst(r)) - )); + let rap = &r.bygning.rapport; + let mut valg = format!( + "tittel: \"{} — energirapport\", bunntekst: \"{}\"", + 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 ───────────────────────────────────────────────────── + if let Some(navn) = logo_navn(r) { + s.push_str(&format!( + "#align(right, image(\"{}\", height: 1.7cm))\n\n#v(-0.9cm)\n", + streng(&navn) + )); + } s.push_str(&format!( "#v(1.2cm)\n#text(size: 26pt, weight: \"bold\")[Energirapport]\n\n\ #v(-0.3em)\n#text(size: 15pt, fill: mid)[{}]\n\n", @@ -473,7 +496,8 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String { if l.forutsetninger.is_empty() { s.push_str("Alle inndata er oppgitt i bygningsbeskrivelsen.\n\n"); } else { - s.push_str("#tabell(\n kolonner: (auto, auto, 1fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n"); + // Faste bredder, ellers spiser de to første kolonnene plassen kilden trenger. + s.push_str("#tabell(\n kolonner: (1.15fr, 0.85fr, 1.6fr),\n justering: (left, left, left),\n overskrift: (\"Felt\", \"Verdi\", \"Kilde\"),\n rader: (\n"); for f in &l.forutsetninger { s.push_str(&format!( " {}\n", @@ -539,6 +563,24 @@ fn kilde(r: &Resultat, scenarier: &[(String, Resultat)]) -> String { s } +/// 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 { + 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 { let b = &r.bygning.bygning; let mut deler = Vec::new(); diff --git a/typst/stil.typ b/typst/stil.typ index 8e3495e..42d726f 100644 --- a/typst/stil.typ +++ b/typst/stil.typ @@ -7,11 +7,32 @@ #let lys = rgb("#f4f1ee") #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-body = ("Libertinus Serif", "DejaVu Serif", "Noto Serif") #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( paper: "a4", margin: (top: 2.4cm, bottom: 2.4cm, left: 2.2cm, right: 2.2cm), @@ -20,32 +41,36 @@ grid( columns: (1fr, auto), 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()], ) line(length: 100%, stroke: 0.4pt + kantlinje) } }, 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) 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] } }, ) set text(font: font-body, size: 10pt, fill: moerk, lang: "nb") set par(justify: true, leading: 0.62em) - show heading: it => { - set text(font: font-heading, fill: aksent) + show heading: it => context { + set text(font: overskriftsskrift(), fill: aksent-valgt.get()) 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") block(above: 1.6em, below: 0.7em)[ #it.body #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") @@ -55,7 +80,8 @@ } // 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 } block(breakable: true, table( columns: kolonner, @@ -99,3 +125,4 @@ stroke: (left: 2pt + aksent-lys), text(size: 9pt)[#innhold], ) +