Velg klimasted ut fra georefereringen, og rett adresse fra IFC
En ny eksport av Nordhusstykket 5 viste hva mer modellen kan bære, og avdekket feil. Posisjon og klimasted - IfcMapConversion med IfcProjectedCRS gjøres om til breddegrad og lengdegrad. EUREF89 og WGS 84 UTM, NN2000-varianter og Web Mercator forstås. Omregningen er kontrollert mot PROJ. - Vectorworks' standardplassering i Washington, som står når ingenting er satt, kjennes igjen og brukes ikke. - Ny `posisjon` under [klima]. Uten kommune velger beregningen nærmeste av de 358 klimastedene i TMY-arkivet, og sier i rapporten hvor langt unna det ligger. Opplysninger fra modellen - Adressen hentes fra IfcBuilding eller IfcSite. Før ble den første postadressen i filen brukt, og det var firmaets adresse i forfatteropplysningene. - Energirådgiver fra person og organisasjon i forfatteropplysningene. - Byggets navn fra prosjektet når bygningen heter «Default Building». - Referansehøyde og terrenghøyde på bygningen gir hvor mye av grunnmuren som står over bakken. - IsLandmarked gir en merknad om at vernede bygninger er unntatt energiattestplikten. En eksport uten bygningsdeler stopper nå med en feilmelding som peker på lagtilknytningen i eksportdialogen, i stedet for å skrive en tom bygning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeH3y8fwTaTudaD88P57o
This commit is contained in:
Generated
+1
-1
@@ -222,7 +222,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx",
|
"approx",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "enok"
|
name = "enok"
|
||||||
version = "0.2.0"
|
version = "0.3.0"
|
||||||
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"
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ byggeaar = 1948
|
|||||||
oppvarmet_bra = 140.0
|
oppvarmet_bra = 140.0
|
||||||
|
|
||||||
[klima]
|
[klima]
|
||||||
kommune = "Indre Østfold"
|
kommune = "Indre Østfold" # eller posisjon = [59.63, 11.01] for nærmeste klimasted
|
||||||
|
|
||||||
[[flate]]
|
[[flate]]
|
||||||
navn = "Yttervegg sør"
|
navn = "Yttervegg sør"
|
||||||
@@ -191,7 +191,12 @@ Ferdige eksempler ligger i `eksempler/`.
|
|||||||
| Uoppvarmet kjeller | En etasje under referansenivået blir satt opp som egen sone, med sine egne vegger og gulv, og dekket over som grense mot den oppvarmede delen |
|
| Uoppvarmet kjeller | En etasje under referansenivået blir satt opp som egen sone, med sine egne vegger og gulv, og dekket over som grense mot den oppvarmede delen |
|
||||||
| Oppvarmet BRA | Rommene i etasjene over terreng. Rom som heter kott, bod, loft eller garasje blir flagget |
|
| Oppvarmet BRA | Rommene i etasjene over terreng. Rom som heter kott, bod, loft eller garasje blir flagget |
|
||||||
| Vindus- og dørareal | `OverallHeight` og `OverallWidth`, og hvilken vegg de sitter i |
|
| Vindus- og dørareal | `OverallHeight` og `OverallWidth`, og hvilken vegg de sitter i |
|
||||||
| Posisjon | `IfcSite` med breddegrad og lengdegrad |
|
| Posisjon | `IfcMapConversion` med `IfcProjectedCRS`, ellers `IfcSite`. Uten oppgitt kommune velger beregningen nærmeste klimasted. EUREF89 UTM og Web Mercator forstås |
|
||||||
|
| Grunnmur over terreng | `ElevationOfRefHeight` og `ElevationOfTerrain` på bygningen |
|
||||||
|
| Adresse | `BuildingAddress` på bygningen, eller `SiteAddress` på tomta. Adressen i forfatteropplysningene er firmaets og brukes ikke |
|
||||||
|
| Energirådgiver | Person og organisasjon i forfatteropplysningene |
|
||||||
|
| Byggets navn | Bygningens navn, eller prosjektets når bygningen heter «Default Building» |
|
||||||
|
| Vernet bygning | `IsLandmarked`. Vernede bygninger er unntatt plikten til energiattest |
|
||||||
|
|
||||||
Himmelretningene og arealene bør likevel kontrolleres mot tegningen. Importen skriver merknader
|
Himmelretningene og arealene bør likevel kontrolleres mot tegningen. Importen skriver merknader
|
||||||
øverst i TOML-filen om hva den måtte anta.
|
øverst i TOML-filen om hva den måtte anta.
|
||||||
@@ -221,15 +226,24 @@ egenskapssett på `IfcBuilding`, `IfcSite` eller `IfcProject`:
|
|||||||
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.
|
||||||
|
|
||||||
Fire ting i selve modellen er verdt å gjøre riktig:
|
Dette i selve modellen er verdt å gjøre riktig, omtrent i rekkefølge etter hva det betyr:
|
||||||
|
|
||||||
- **Sett prosjektstedet.** Da får `IfcSite` en posisjon, og klimadataene kan velges etter den.
|
- **Ta med designlagene i eksporten.** I «Export IFC Project» må lagene være knyttet til
|
||||||
Uten det står Vectorworks' standardverdi, som ligger i Washington.
|
etasjer under «Layer Mapping». Mangler de, blir filen bare prosjektstruktur uten vegger,
|
||||||
|
dekker, vinduer eller rom. enok stopper da med en feilmelding i stedet for å skrive en tom
|
||||||
|
bygning.
|
||||||
|
- **Georeferer tegningen, eller sett prosjektstedet.** Da får modellen koordinater, og
|
||||||
|
beregningen velger selv nærmeste av de 358 klimastedene. Uten det står Vectorworks'
|
||||||
|
standardverdi i Washington, og den blir oversett.
|
||||||
- **Merk yttervegger med `IsExternal`.** I modellen for Nordhusstykket 5 er bare tolv av
|
- **Merk yttervegger med `IsExternal`.** I modellen for Nordhusstykket 5 er bare tolv av
|
||||||
hundre og fem vegger merket, og kjellermurene er ikke blant dem.
|
hundre og fem vegger merket, og kjellermurene er ikke blant dem.
|
||||||
- **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke.
|
- **Fyll inn U-verdi for tak og vinduer.** Veggene har den, takflatene og vinduene ikke.
|
||||||
- **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den
|
- **Gi etasjene navn.** «Kjeller», «1. etg.» og «2. etg.» er nok til at enok finner den
|
||||||
uoppvarmede kjelleren og regner oppvarmet areal riktig.
|
uoppvarmede kjelleren og regner oppvarmet areal riktig.
|
||||||
|
- **Sett referansehøyde og terrenghøyde på bygningen.** Da regner enok ut hvor mye av
|
||||||
|
grunnmuren som står over bakken. Uten dem regnes hele muren som nedgravd.
|
||||||
|
- **Legg byggets adresse på bygningen.** Adressen i forfatteropplysningene er firmaets, og
|
||||||
|
brukes ikke som byggets adresse.
|
||||||
|
|
||||||
Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet:
|
Regnearkformatet er bevisst enkelt, slik at det kan settes opp som en rapport i BIM-verktøyet:
|
||||||
|
|
||||||
|
|||||||
+365
-44
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
use crate::inndata::ifcgeom;
|
use crate::inndata::ifcgeom;
|
||||||
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
use crate::inndata::step::{self, Arg, Entitet, Fil};
|
||||||
|
use crate::klima::geo;
|
||||||
use crate::modell::*;
|
use crate::modell::*;
|
||||||
use crate::Result;
|
use crate::Result;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
@@ -49,6 +50,35 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
let (vindu_vert, dor_vert) = samle_apninger(&fil);
|
||||||
let (etasjer, etasje_for) = les_etasjer(&fil, skala);
|
let (etasjer, etasje_for) = les_etasjer(&fil, skala);
|
||||||
|
|
||||||
|
// En eksport uten bygningsdeler gir ingen klimaskjerm å regne på. Det skjer typisk når
|
||||||
|
// designlagene ikke er knyttet til etasjer i eksportdialogen, og da er det bedre å stoppe
|
||||||
|
// enn å skrive en tom bygningsbeskrivelse.
|
||||||
|
let antall_deler: usize = [
|
||||||
|
"IFCWALL",
|
||||||
|
"IFCWALLSTANDARDCASE",
|
||||||
|
"IFCCURTAINWALL",
|
||||||
|
"IFCSLAB",
|
||||||
|
"IFCROOF",
|
||||||
|
"IFCWINDOW",
|
||||||
|
"IFCDOOR",
|
||||||
|
"IFCSPACE",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.map(|t| fil.av_type(t).count())
|
||||||
|
.sum();
|
||||||
|
if antall_deler == 0 {
|
||||||
|
anyhow::bail!(
|
||||||
|
"IFC-filen har ingen bygningsdeler: ingen vegger, dekker, tak, vinduer, dører eller rom. \
|
||||||
|
Den inneholder bare prosjektstrukturen{}. I Vectorworks skjer dette gjerne når designlagene \
|
||||||
|
ikke er med under «Layer Mapping» i «Export IFC Project».",
|
||||||
|
if etasjer.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" med {} etasjer", etasjer.len())
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── oppvarmet areal fra rommene ─────────────────────────────────
|
// ── oppvarmet areal fra rommene ─────────────────────────────────
|
||||||
// Rom i en etasje under terreng hører til kjelleren og ikke til oppvarmet BRA.
|
// Rom i en etasje under terreng hører til kjelleren og ikke til oppvarmet BRA.
|
||||||
let mut bra = 0.0;
|
let mut bra = 0.0;
|
||||||
@@ -177,6 +207,14 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
// 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.
|
||||||
|
let terreng = terrenghoyder(&fil, skala);
|
||||||
|
let kjeller_over_terreng = match (terreng, forste_over.and_then(|i| etasjer.get(i))) {
|
||||||
|
(Some((referanse, terrengnivaa)), Some(forste)) => {
|
||||||
|
over_terreng_for_kjeller(referanse, terrengnivaa, forste.hoyde, kjellerdybde)
|
||||||
|
}
|
||||||
|
_ => 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
// ── klimaskjerm ─────────────────────────────────────────────────
|
// ── klimaskjerm ─────────────────────────────────────────────────
|
||||||
let mut kjellerflater: Vec<Flate> = Vec::new();
|
let mut kjellerflater: Vec<Flate> = Vec::new();
|
||||||
@@ -245,8 +283,8 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
b_faktor: None,
|
b_faktor: None,
|
||||||
absorptans: None,
|
absorptans: None,
|
||||||
omkrets: None,
|
omkrets: None,
|
||||||
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde),
|
dybde: (i_kjeller && mot == Mot::Grunn).then_some(kjellerdybde - kjeller_over_terreng),
|
||||||
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(0.0),
|
over_terreng: (i_kjeller && mot == Mot::Grunn).then_some(kjeller_over_terreng),
|
||||||
sone,
|
sone,
|
||||||
kommentar: d
|
kommentar: d
|
||||||
.etasje
|
.etasje
|
||||||
@@ -407,13 +445,45 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ubrukelig = |s: &str| {
|
||||||
|
let s = s.trim();
|
||||||
|
s.is_empty() || s == "Default Building" || s == "Vectorworks Project"
|
||||||
|
};
|
||||||
|
// Bygningens navn, ellers prosjektets langnavn eller navn, ellers filnavnet.
|
||||||
let navn = fil
|
let navn = fil
|
||||||
.av_type("IFCBUILDING")
|
.av_type("IFCBUILDING")
|
||||||
.next()
|
.next()
|
||||||
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
.and_then(|(_, e)| e.navn().map(|s| s.to_string()))
|
||||||
.filter(|s| !s.is_empty() && s != "Default Building")
|
.filter(|s| !ubrukelig(s))
|
||||||
|
.or_else(|| {
|
||||||
|
fil.av_type("IFCPROJECT").next().and_then(|(_, p)| {
|
||||||
|
[p.arg(5), p.arg(2)]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|a| a.som_tekst())
|
||||||
|
.find(|s| !ubrukelig(s))
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
})
|
||||||
|
})
|
||||||
.unwrap_or_else(|| standardnavn.to_string());
|
.unwrap_or_else(|| standardnavn.to_string());
|
||||||
|
|
||||||
|
// Vernede bygninger er unntatt plikten til å ha energiattest.
|
||||||
|
let vernet = fil
|
||||||
|
.av_type("IFCBUILDING")
|
||||||
|
.next()
|
||||||
|
.and_then(|(r, _)| egenskaper.get(&r))
|
||||||
|
.and_then(|e| e.get("IsLandmarked"))
|
||||||
|
.map(|a| matches!(a.som_tekst(), Some("T") | Some("TRUE") | Some("true")))
|
||||||
|
.unwrap_or(false);
|
||||||
|
if vernet {
|
||||||
|
merknader.push(
|
||||||
|
"Bygningen er merket som vernet i modellen. Vernede bygninger etter kulturminneloven og \
|
||||||
|
plan- og bygningsloven er unntatt plikten til å ha energiattest, men beregningen kan \
|
||||||
|
fortsatt brukes til å vurdere tiltak."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Kjellersonen settes sammen av flatene i etasjen under terreng.
|
// Kjellersonen settes sammen av flatene i etasjen under terreng.
|
||||||
let mut soner: Vec<Sone> = Vec::new();
|
let mut soner: Vec<Sone> = Vec::new();
|
||||||
if let Some(i) = kjelleretasje {
|
if let Some(i) = kjelleretasje {
|
||||||
@@ -442,11 +512,22 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
rom.join(", ")
|
rom.join(", ")
|
||||||
)),
|
)),
|
||||||
});
|
});
|
||||||
|
let grunnmur = if terreng.is_some() {
|
||||||
|
format!(
|
||||||
|
"Etter terrenghøyden i modellen står grunnmuren {kjeller_over_terreng:.2} m over bakken og \
|
||||||
|
{:.2} m under.",
|
||||||
|
kjellerdybde - kjeller_over_terreng
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Står en del av \
|
||||||
|
grunnmuren over bakken, sett `over_terreng` i meter på veggene, eller sett terrenghøyden \
|
||||||
|
på bygningen i modellen. Utslaget er lite, om lag én prosent per meter."
|
||||||
|
)
|
||||||
|
};
|
||||||
merknader.push(format!(
|
merknader.push(format!(
|
||||||
"Etasjen «{}» ligger under terreng og er satt opp som en uoppvarmet sone med {:.0} m² gulv. \
|
"Etasjen «{}» ligger under terreng og er satt opp som en uoppvarmet sone med {:.0} m² gulv. \
|
||||||
Kjellerveggene er regnet {kjellerdybde:.1} m under terreng uten noe over. Står en del av \
|
{grunnmur}",
|
||||||
grunnmuren over bakken, sett `over_terreng` i meter på veggene; utslaget er lite, om lag \
|
|
||||||
én prosent per meter.",
|
|
||||||
etasjer[i].navn, kjellerareal
|
etasjer[i].navn, kjellerareal
|
||||||
));
|
));
|
||||||
merknader.push("Er kjelleren oppvarmet likevel, slett [[sone]] og legg flatene inn i klimaskjermen. \
|
merknader.push("Er kjelleren oppvarmet likevel, slett [[sone]] og legg flatene inn i klimaskjermen. \
|
||||||
@@ -454,18 +535,41 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some((lat, lon)) = posisjon(&fil) {
|
let klimaposisjon = match posisjon(&fil) {
|
||||||
merknader.push(format!(
|
Some(p) if p.er_standardplassering() => {
|
||||||
"Modellen oppgir posisjon {lat:.4}° nord, {lon:.4}° øst. Kontroller at den stemmer, og sett \
|
merknader.push(
|
||||||
`kommune` under [klima] deretter."
|
"Modellen står på Vectorworks' standardplassering i Washington, så posisjonen er ikke \
|
||||||
));
|
brukt. Georefereres tegningen eller settes prosjektstedet, velger beregningen \
|
||||||
} else {
|
klimasted ut fra den."
|
||||||
merknader.push(
|
.into(),
|
||||||
"Modellen oppgir ingen posisjon. Settes prosjektstedet i Vectorworks, kommer det med som \
|
);
|
||||||
IfcSite og kan brukes til å velge klimadata."
|
None
|
||||||
.into(),
|
}
|
||||||
);
|
Some(p) if !p.i_norge() => {
|
||||||
}
|
merknader.push(format!(
|
||||||
|
"Modellen oppgir posisjon {:.4}° nord, {:.4}° øst, som ligger utenfor Norge. \
|
||||||
|
Posisjonen er ikke brukt til å velge klimasted.",
|
||||||
|
p.bredde, p.lengde
|
||||||
|
));
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Some(p) => {
|
||||||
|
merknader.push(format!(
|
||||||
|
"Modellen oppgir posisjon {:.4}° nord, {:.4}° øst. Uten kommune velger beregningen \
|
||||||
|
klimastedet nærmest denne.",
|
||||||
|
p.bredde, p.lengde
|
||||||
|
));
|
||||||
|
Some([p.bredde, p.lengde])
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
merknader.push(
|
||||||
|
"Modellen oppgir ingen posisjon. Georefereres tegningen eller settes prosjektstedet, \
|
||||||
|
velger beregningen klimasted ut fra den."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let bygning = Bygning {
|
let bygning = Bygning {
|
||||||
bygning: BygningInfo {
|
bygning: BygningInfo {
|
||||||
@@ -485,11 +589,12 @@ pub fn fra_tekst(tekst: &str, standardnavn: &str) -> Result<Importrapport> {
|
|||||||
adresse: adresse(&fil),
|
adresse: adresse(&fil),
|
||||||
gnr_bnr: None,
|
gnr_bnr: None,
|
||||||
beskrivelse: Some("Importert fra IFC".into()),
|
beskrivelse: Some("Importert fra IFC".into()),
|
||||||
energiraadgiver: None,
|
energiraadgiver: forfatter(&fil),
|
||||||
oppdragsgiver: None,
|
oppdragsgiver: None,
|
||||||
},
|
},
|
||||||
klima: Klima {
|
klima: Klima {
|
||||||
kommune: data.kommune.clone(),
|
kommune: data.kommune.clone(),
|
||||||
|
posisjon: klimaposisjon,
|
||||||
..Klima::default()
|
..Klima::default()
|
||||||
},
|
},
|
||||||
flate: flater,
|
flate: flater,
|
||||||
@@ -847,19 +952,66 @@ fn les_byggdata(fil: &Fil, egenskaper: &Egenskaper) -> (Byggdata, Vec<String>) {
|
|||||||
(d, funnet)
|
(d, funnet)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Byggets posisjon fra `IfcSite`, som grader nord og øst.
|
/// Byggets posisjon.
|
||||||
fn posisjon(fil: &Fil) -> Option<(f64, f64)> {
|
///
|
||||||
let (_, e) = fil.av_type("IFCSITE").next()?;
|
/// Georeferering gjennom `IfcMapConversion` og `IfcProjectedCRS` brukes først, siden den er
|
||||||
let grad = |a: Option<&Arg>| -> Option<f64> {
|
/// mest presis. Ellers brukes breddegrad og lengdegrad på `IfcSite`. Vectorworks fyller begge
|
||||||
let l = a?.som_liste();
|
/// med en standardplassering i Washington når ingenting er satt, og den velges bare når ingen
|
||||||
if l.is_empty() {
|
/// annen posisjon finnes, slik at den kan kjennes igjen og oversees.
|
||||||
return None;
|
fn posisjon(fil: &Fil) -> Option<geo::Posisjon> {
|
||||||
}
|
let fra_kart = fil.av_type("IFCMAPCONVERSION").next().and_then(|(_, m)| {
|
||||||
let g = |i: usize| l.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
let crs = m
|
||||||
let verdi = g(0).abs() + g(1).abs() / 60.0 + g(2).abs() / 3600.0 + g(3).abs() / 3.6e9;
|
.arg(1)
|
||||||
Some(if g(0) < 0.0 { -verdi } else { verdi })
|
.and_then(|a| a.som_ref())
|
||||||
};
|
.and_then(|r| fil.hent(r))?;
|
||||||
Some((grad(e.arg(9))?, grad(e.arg(10))?))
|
let navn = crs.arg(0).and_then(|a| a.som_tekst())?;
|
||||||
|
let x = m.arg(2).and_then(|a| a.som_tall())?;
|
||||||
|
let y = m.arg(3).and_then(|a| a.som_tall())?;
|
||||||
|
geo::fra_koordinatsystem(navn, x, y)
|
||||||
|
});
|
||||||
|
let fra_tomt = fil.av_type("IFCSITE").next().and_then(|(_, e)| {
|
||||||
|
let grad = |a: Option<&Arg>| -> Option<f64> {
|
||||||
|
let l = a?.som_liste();
|
||||||
|
if l.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let g = |i: usize| l.get(i).and_then(|x| x.som_tall()).unwrap_or(0.0);
|
||||||
|
let verdi = g(0).abs() + g(1).abs() / 60.0 + g(2).abs() / 3600.0 + g(3).abs() / 3.6e9;
|
||||||
|
Some(if g(0) < 0.0 { -verdi } else { verdi })
|
||||||
|
};
|
||||||
|
Some(geo::Posisjon {
|
||||||
|
bredde: grad(e.arg(9))?,
|
||||||
|
lengde: grad(e.arg(10))?,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
[fra_kart, fra_tomt]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.find(|p| !p.er_standardplassering())
|
||||||
|
.or(fra_kart)
|
||||||
|
.or(fra_tomt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Referansehøyden og terrenghøyden på bygningen, begge over havet, m.
|
||||||
|
fn terrenghoyder(fil: &Fil, skala: f64) -> Option<(f64, f64)> {
|
||||||
|
let (_, e) = fil.av_type("IFCBUILDING").next()?;
|
||||||
|
let referanse = e.arg(9).and_then(|a| a.som_tall())? * skala;
|
||||||
|
let terreng = e.arg(10).and_then(|a| a.som_tall())? * skala;
|
||||||
|
Some((referanse, terreng))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// terrenget med differansen mellom etasjens høyde over havet og terrengets, begrenset til
|
||||||
|
/// kjellerens høyde.
|
||||||
|
fn over_terreng_for_kjeller(
|
||||||
|
referanse: f64,
|
||||||
|
terreng: f64,
|
||||||
|
forste_etasje: f64,
|
||||||
|
kjellerdybde: f64,
|
||||||
|
) -> f64 {
|
||||||
|
((referanse + forste_etasje) - terreng).clamp(0.0, kjellerdybde.max(0.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// En bygningsdel slik den er lest av IFC-filen.
|
/// En bygningsdel slik den er lest av IFC-filen.
|
||||||
@@ -1097,23 +1249,93 @@ fn navn_av(e: &Entitet, r: u64, standard: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Byggets adresse fra `IfcBuilding.BuildingAddress`, eller fra tomta.
|
||||||
|
///
|
||||||
|
/// Adresser knyttet til personer og organisasjoner er forfatterens, ikke byggets, og brukes
|
||||||
|
/// ikke. Plassholdere uten gate eller poststed, som Vectorworks setter inn, hoppes over.
|
||||||
fn adresse(fil: &Fil) -> Option<String> {
|
fn adresse(fil: &Fil) -> Option<String> {
|
||||||
let (_, e) = fil.av_type("IFCPOSTALADDRESS").next()?;
|
let bygning = fil
|
||||||
|
.av_type("IFCBUILDING")
|
||||||
|
.filter_map(|(_, e)| e.arg(11).and_then(|a| a.som_ref()));
|
||||||
|
let tomt = fil
|
||||||
|
.av_type("IFCSITE")
|
||||||
|
.filter_map(|(_, e)| e.arg(13).and_then(|a| a.som_ref()));
|
||||||
|
bygning
|
||||||
|
.chain(tomt)
|
||||||
|
.filter_map(|r| fil.hent(r))
|
||||||
|
.filter(|e| e.typ == "IFCPOSTALADDRESS")
|
||||||
|
.find_map(formater_adresse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `IfcPostalAddress` har feltene Purpose, Description, UserDefinedPurpose, InternalLocation,
|
||||||
|
/// AddressLines, PostalBox, Town, Region, PostalCode og Country.
|
||||||
|
fn formater_adresse(e: &Entitet) -> Option<String> {
|
||||||
let linjer: Vec<String> = e
|
let linjer: Vec<String> = e
|
||||||
.arg(4)?
|
.arg(4)
|
||||||
.som_liste()
|
.map(|a| a.som_liste())
|
||||||
|
.unwrap_or(&[])
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|a| a.som_tekst().map(|s| s.to_string()))
|
.filter_map(|a| a.som_tekst())
|
||||||
|
.map(|s| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
.collect();
|
.collect();
|
||||||
let by = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("");
|
let postnummer = e.arg(8).and_then(|a| a.som_tekst()).unwrap_or("").trim();
|
||||||
let mut ut = linjer.join(", ");
|
let sted = e.arg(6).and_then(|a| a.som_tekst()).unwrap_or("").trim();
|
||||||
if !by.is_empty() {
|
if linjer.is_empty() && sted.is_empty() {
|
||||||
if !ut.is_empty() {
|
return None;
|
||||||
ut.push_str(", ");
|
}
|
||||||
}
|
let poststed: Vec<&str> = [postnummer, sted]
|
||||||
ut.push_str(by);
|
.into_iter()
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect();
|
||||||
|
Some(
|
||||||
|
linjer
|
||||||
|
.into_iter()
|
||||||
|
.chain((!poststed.is_empty()).then(|| poststed.join(" ")))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", "),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Forfatteren av modellen, som person og organisasjon.
|
||||||
|
///
|
||||||
|
/// I en modell laget for energirådgivning er det energirådgiveren. Vectorworks' egen
|
||||||
|
/// organisasjon, som står når ingenting er fylt inn, tas ikke med.
|
||||||
|
fn forfatter(fil: &Fil) -> Option<String> {
|
||||||
|
let (_, po) = fil.av_type("IFCPERSONANDORGANIZATION").next()?;
|
||||||
|
let person = po
|
||||||
|
.arg(0)
|
||||||
|
.and_then(|a| a.som_ref())
|
||||||
|
.and_then(|r| fil.hent(r));
|
||||||
|
let organisasjon = po
|
||||||
|
.arg(1)
|
||||||
|
.and_then(|a| a.som_ref())
|
||||||
|
.and_then(|r| fil.hent(r));
|
||||||
|
// IfcPerson: Identification, FamilyName, GivenName, …
|
||||||
|
let personnavn = person
|
||||||
|
.map(|p| {
|
||||||
|
[p.arg(2), p.arg(1)]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|a| a.som_tekst())
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
})
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
// IfcOrganization: Identification, Name, …
|
||||||
|
let organisasjonsnavn = organisasjon
|
||||||
|
.and_then(|o| o.arg(1).and_then(|a| a.som_tekst()))
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|s| !s.is_empty() && !s.to_lowercase().contains("vectorworks"))
|
||||||
|
.map(str::to_string);
|
||||||
|
match (personnavn, organisasjonsnavn) {
|
||||||
|
(Some(p), Some(o)) => Some(format!("{p}, {o}")),
|
||||||
|
(Some(p), None) => Some(p),
|
||||||
|
(None, Some(o)) => Some(o),
|
||||||
|
(None, None) => None,
|
||||||
}
|
}
|
||||||
(!ut.is_empty()).then_some(ut)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
/// Prosjektets dreining mot sant nord, i grader med klokka.
|
||||||
@@ -1500,6 +1722,105 @@ mod tester {
|
|||||||
assert!(r.merknader.iter().any(|m| m.contains("tatt i bruk")));
|
assert!(r.merknader.iter().any(|m| m.contains("tatt i bruk")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn eksport_uten_bygningsdeler_gir_tydelig_feil() {
|
||||||
|
let tom = "ISO-10303-21;\nHEADER;ENDSEC;\nDATA;\n\
|
||||||
|
#1= IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.);\n\
|
||||||
|
#2= IFCUNITASSIGNMENT((#1));\n\
|
||||||
|
#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,$,#2);\n\
|
||||||
|
#10= IFCBUILDING('b',$,'Hus',$,$,$,$,$,.ELEMENT.,$,$,$);\n\
|
||||||
|
#11= IFCBUILDINGSTOREY('k',$,'Kjeller',$,$,$,$,$,.ELEMENT.,-2350.);\n\
|
||||||
|
ENDSEC;\nEND-ISO-10303-21;";
|
||||||
|
let feil = fra_tekst(tom, "tom")
|
||||||
|
.err()
|
||||||
|
.expect("en tom eksport skal gi feil");
|
||||||
|
let tekst = feil.to_string();
|
||||||
|
assert!(tekst.contains("ingen bygningsdeler"), "{tekst}");
|
||||||
|
assert!(tekst.contains("1 etasjer"), "{tekst}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adressen på firmaet som laget modellen skal ikke bli byggets adresse.
|
||||||
|
#[test]
|
||||||
|
fn adressen_hentes_fra_bygningen_og_ikke_fra_forfatteren() {
|
||||||
|
let t = prove().replace(
|
||||||
|
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
|
||||||
|
"#501= IFCPOSTALADDRESS($,$,$,$,('Hob\\X\\F8lveien 6'),$,'Tomter',$,'1825','Norge');\n\
|
||||||
|
#502= IFCORGANIZATION('976015711','Klingenberg Bygg',$,$,(#501));\n\
|
||||||
|
#503= IFCPERSON($,'Lynghaug','Fredrik',$,$,$,$,$);\n\
|
||||||
|
#504= IFCPERSONANDORGANIZATION(#503,#502,$);\n\
|
||||||
|
#500= IFCPOSTALADDRESS($,$,$,$,('Nordhusstykket 5'),$,'Tomter',$,'1825','Norge');\n\
|
||||||
|
#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
|
||||||
|
);
|
||||||
|
let r = fra_tekst(&t, "prove").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
r.bygning.bygning.adresse.as_deref(),
|
||||||
|
Some("Nordhusstykket 5, 1825 Tomter")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
r.bygning.bygning.energiraadgiver.as_deref(),
|
||||||
|
Some("Fredrik Lynghaug, Klingenberg Bygg")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plassholderadresse_og_standardnavn_brukes_ikke() {
|
||||||
|
let t = prove()
|
||||||
|
.replace(
|
||||||
|
"#10= IFCBUILDING('b',$,'Testhus',$,$,#20,$,$,.ELEMENT.,$,$,$);",
|
||||||
|
"#500= IFCPOSTALADDRESS($,$,$,'Vectorworks Architect',$,$,$,$,$,$);\n\
|
||||||
|
#10= IFCBUILDING('b',$,'Default Building',$,$,#20,$,$,.ELEMENT.,$,$,#500);",
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
"#3= IFCPROJECT('p',$,'Prosjekt',$,$,$,$,(#40),#2);",
|
||||||
|
"#3= IFCPROJECT('p',$,'Nordhusstykket 5','Energianalyse',$,'Nordhusstykket 5','Eksisterende',(#40),#2);",
|
||||||
|
);
|
||||||
|
let r = fra_tekst(&t, "prove").unwrap();
|
||||||
|
assert_eq!(r.bygning.bygning.adresse, None);
|
||||||
|
assert_eq!(r.bygning.bygning.navn, "Nordhusstykket 5");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn georeferering_i_utm_gir_posisjon_for_klimavalg() {
|
||||||
|
let t = prove().replace(
|
||||||
|
"#4= IFCDIRECTION((0.,1.));",
|
||||||
|
"#4= IFCDIRECTION((0.,1.));\n\
|
||||||
|
#600= IFCPROJECTEDCRS('EPSG:25832','ETRS89 / UTM zone 32N','ETRS89',$,$,$,$);\n\
|
||||||
|
#601= IFCMAPCONVERSION(#40,#600,613351.291,6611921.980,0.,1.,0.,$);",
|
||||||
|
);
|
||||||
|
let r = fra_tekst(&t, "prove").unwrap();
|
||||||
|
let [b, l] = r
|
||||||
|
.bygning
|
||||||
|
.klima
|
||||||
|
.posisjon
|
||||||
|
.expect("posisjon fra georefereringen");
|
||||||
|
assert!(
|
||||||
|
(b - 59.63).abs() < 1e-4 && (l - 11.01).abs() < 1e-4,
|
||||||
|
"{b}, {l}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vectorworks_standardplassering_brukes_ikke() {
|
||||||
|
let t = prove().replace(
|
||||||
|
"#4= IFCDIRECTION((0.,1.));",
|
||||||
|
"#4= IFCDIRECTION((0.,1.));\n\
|
||||||
|
#600= IFCPROJECTEDCRS('EPSG:3857',$,'WGS 84',$,$,$,$);\n\
|
||||||
|
#601= IFCMAPCONVERSION(#40,#600,-8575524.35228859,4705852.25746973,0.,1.,0.,$);",
|
||||||
|
);
|
||||||
|
let r = fra_tekst(&t, "prove").unwrap();
|
||||||
|
assert_eq!(r.bygning.klima.posisjon, None);
|
||||||
|
assert!(r.merknader.iter().any(|m| m.contains("standardplassering")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
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.
|
||||||
|
assert!((over_terreng_for_kjeller(131.2, 130.6, 0.0, 2.35) - 0.6).abs() < 1e-9);
|
||||||
|
// Aldri mer enn kjellerhøyden, og aldri under null.
|
||||||
|
assert_eq!(over_terreng_for_kjeller(131.2, 125.0, 0.0, 2.35), 2.35);
|
||||||
|
assert_eq!(over_terreng_for_kjeller(130.0, 131.0, 0.0, 2.35), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vegg_uten_geometri_utelates() {
|
fn vegg_uten_geometri_utelates() {
|
||||||
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
|
let uten = prove().replace("'Body','Tessellation'", "'Axis','Curve2D'");
|
||||||
|
|||||||
+22
-5
@@ -112,11 +112,14 @@ pub fn hent_klima(b: &Bygning) -> Result<(Vaerdata, String, bool)> {
|
|||||||
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
||||||
return Ok((v, navn, er_referanse));
|
return Ok((v, navn, er_referanse));
|
||||||
}
|
}
|
||||||
let sted = b
|
// Uten kommune, men med posisjon, velges klimastedet nærmest posisjonen.
|
||||||
.klima
|
let sted = match (&b.klima.kommune, b.klima.posisjon) {
|
||||||
.kommune
|
(Some(k), _) => k.clone(),
|
||||||
.clone()
|
(None, Some([bredde, lengde])) => {
|
||||||
.unwrap_or_else(|| REFERANSEKLIMA.to_string());
|
tmy::naermeste_kommune(&crate::klima::geo::Posisjon { bredde, lengde })?.0
|
||||||
|
}
|
||||||
|
(None, None) => REFERANSEKLIMA.to_string(),
|
||||||
|
};
|
||||||
let er_referanse = crate::data::normaliser_stedsnavn(&sted)
|
let er_referanse = crate::data::normaliser_stedsnavn(&sted)
|
||||||
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
== crate::data::normaliser_stedsnavn(REFERANSEKLIMA);
|
||||||
let v = tmy::for_kommune(&sted)?;
|
let v = tmy::for_kommune(&sted)?;
|
||||||
@@ -140,6 +143,20 @@ pub fn kjor_med_klima(
|
|||||||
let varmetap = Varmetapsbudsjett::beregn(&lost);
|
let varmetap = Varmetapsbudsjett::beregn(&lost);
|
||||||
let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?;
|
let systemer_oppsett = systemer::sett_opp_systemer(b, &lost, kilde)?;
|
||||||
let mut merknader = systemer_oppsett.merknader.clone();
|
let mut merknader = systemer_oppsett.merknader.clone();
|
||||||
|
if b.klima.kommune.is_none() && b.klima.epw.is_none() {
|
||||||
|
if let Some([bredde, lengde]) = b.klima.posisjon {
|
||||||
|
let bygg = crate::klima::geo::Posisjon { bredde, lengde };
|
||||||
|
let stasjon = crate::klima::geo::Posisjon {
|
||||||
|
bredde: vaerdata.bredde,
|
||||||
|
lengde: vaerdata.lengde,
|
||||||
|
};
|
||||||
|
merknader.push(format!(
|
||||||
|
"Klimastedet {klimanavn} er valgt ut fra posisjonen {bredde:.4}° nord, {lengde:.4}° øst, \
|
||||||
|
{:.0} km unna. Sett `kommune` under [klima] for å velge et annet.",
|
||||||
|
bygg.avstand_km(&stasjon)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
merknader.extend(
|
merknader.extend(
|
||||||
crate::modell::kontroll::kontroller(b)
|
crate::modell::kontroll::kontroller(b)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
//! Posisjon fra BIM-modellen, og valg av klimasted ut fra den.
|
||||||
|
//!
|
||||||
|
//! En IFC-modell kan oppgi hvor bygningen står på to måter: som breddegrad og lengdegrad på
|
||||||
|
//! `IfcSite`, eller som prosjiserte koordinater gjennom `IfcMapConversion` og
|
||||||
|
//! `IfcProjectedCRS`. I Norge er det siste normalt EUREF89 UTM sone 32, 33 eller 35. Denne
|
||||||
|
//! modulen gjør begge om til breddegrad og lengdegrad, slik at nærmeste klimasted kan velges
|
||||||
|
//! automatisk.
|
||||||
|
|
||||||
|
use std::f64::consts::FRAC_PI_2;
|
||||||
|
|
||||||
|
/// Store halvakse for GRS80, som EUREF89 bygger på, m.
|
||||||
|
const A: f64 = 6_378_137.0;
|
||||||
|
/// Flattrykning for GRS80.
|
||||||
|
const F: f64 = 1.0 / 298.257_222_101;
|
||||||
|
/// Målestokkfaktor langs sentralmeridianen i UTM.
|
||||||
|
const K0: f64 = 0.9996;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct Posisjon {
|
||||||
|
/// Breddegrad, grader nord.
|
||||||
|
pub bredde: f64,
|
||||||
|
/// Lengdegrad, grader øst.
|
||||||
|
pub lengde: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Posisjon {
|
||||||
|
/// Avstand langs jordoverflaten, km.
|
||||||
|
pub fn avstand_km(&self, annen: &Posisjon) -> f64 {
|
||||||
|
let (b1, b2) = (self.bredde.to_radians(), annen.bredde.to_radians());
|
||||||
|
let db = b2 - b1;
|
||||||
|
let dl = (annen.lengde - self.lengde).to_radians();
|
||||||
|
let h = (db / 2.0).sin().powi(2) + b1.cos() * b2.cos() * (dl / 2.0).sin().powi(2);
|
||||||
|
2.0 * 6371.0 * h.sqrt().asin()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sant for plasseringen Vectorworks setter når prosjektstedet ikke er angitt, og for
|
||||||
|
/// nullpunktet. Begge betyder at modellen i praksis ikke oppgir noen posisjon.
|
||||||
|
pub fn er_standardplassering(&self) -> bool {
|
||||||
|
let washington =
|
||||||
|
(self.bredde - 38.8895).abs() < 0.05 && (self.lengde + 77.0353).abs() < 0.05;
|
||||||
|
let null = self.bredde.abs() < 1e-6 && self.lengde.abs() < 1e-6;
|
||||||
|
washington || null
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Grovt: innenfor fastlands-Norge og Svalbard.
|
||||||
|
pub fn i_norge(&self) -> bool {
|
||||||
|
(57.5..=81.5).contains(&self.bredde) && (4.0..=32.0).contains(&self.lengde)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fra Web Mercator (EPSG:3857), som Vectorworks bruker når ingen annen projeksjon er valgt.
|
||||||
|
pub fn fra_web_mercator(x: f64, y: f64) -> Posisjon {
|
||||||
|
Posisjon {
|
||||||
|
bredde: (2.0 * (y / A).exp().atan() - FRAC_PI_2).to_degrees(),
|
||||||
|
lengde: (x / A).to_degrees(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fra UTM på nordlige halvkule, med GRS80-ellipsoiden som EUREF89 bruker.
|
||||||
|
///
|
||||||
|
/// Inversformlene er de vanlige seriene etter Snyder, «Map Projections – A Working Manual»,
|
||||||
|
/// som gir centimeternøyaktighet innenfor en sone.
|
||||||
|
pub fn fra_utm(sone: u32, x: f64, y: f64) -> Posisjon {
|
||||||
|
let e2 = F * (2.0 - F);
|
||||||
|
let ep2 = e2 / (1.0 - e2);
|
||||||
|
let e1 = (1.0 - (1.0 - e2).sqrt()) / (1.0 + (1.0 - e2).sqrt());
|
||||||
|
|
||||||
|
let m = y / K0;
|
||||||
|
let mu = m / (A * (1.0 - e2 / 4.0 - 3.0 * e2 * e2 / 64.0 - 5.0 * e2.powi(3) / 256.0));
|
||||||
|
let fi1 = mu
|
||||||
|
+ (3.0 * e1 / 2.0 - 27.0 * e1.powi(3) / 32.0) * (2.0 * mu).sin()
|
||||||
|
+ (21.0 * e1 * e1 / 16.0 - 55.0 * e1.powi(4) / 32.0) * (4.0 * mu).sin()
|
||||||
|
+ (151.0 * e1.powi(3) / 96.0) * (6.0 * mu).sin()
|
||||||
|
+ (1097.0 * e1.powi(4) / 512.0) * (8.0 * mu).sin();
|
||||||
|
|
||||||
|
let (s, c, t) = (fi1.sin(), fi1.cos(), fi1.tan());
|
||||||
|
let n1 = A / (1.0 - e2 * s * s).sqrt();
|
||||||
|
let t1 = t * t;
|
||||||
|
let c1 = ep2 * c * c;
|
||||||
|
let r1 = A * (1.0 - e2) / (1.0 - e2 * s * s).powf(1.5);
|
||||||
|
let d = (x - 500_000.0) / (n1 * K0);
|
||||||
|
|
||||||
|
let bredde = fi1
|
||||||
|
- (n1 * t / r1)
|
||||||
|
* (d * d / 2.0
|
||||||
|
- (5.0 + 3.0 * t1 + 10.0 * c1 - 4.0 * c1 * c1 - 9.0 * ep2) * d.powi(4) / 24.0
|
||||||
|
+ (61.0 + 90.0 * t1 + 298.0 * c1 + 45.0 * t1 * t1 - 252.0 * ep2 - 3.0 * c1 * c1)
|
||||||
|
* d.powi(6)
|
||||||
|
/ 720.0);
|
||||||
|
let sentralmeridian = (sone as f64 - 1.0) * 6.0 - 180.0 + 3.0;
|
||||||
|
let lengde = sentralmeridian
|
||||||
|
+ ((d - (1.0 + 2.0 * t1 + c1) * d.powi(3) / 6.0
|
||||||
|
+ (5.0 - 2.0 * c1 + 28.0 * t1 - 3.0 * c1 * c1 + 8.0 * ep2 + 24.0 * t1 * t1)
|
||||||
|
* d.powi(5)
|
||||||
|
/ 120.0)
|
||||||
|
/ c)
|
||||||
|
.to_degrees();
|
||||||
|
Posisjon {
|
||||||
|
bredde: bredde.to_degrees(),
|
||||||
|
lengde,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gjør koordinater i et navngitt koordinatsystem om til posisjon.
|
||||||
|
///
|
||||||
|
/// `navn` er slik det står i `IfcProjectedCRS`, for eksempel «EPSG:25832». Koordinatsystemer
|
||||||
|
/// enok ikke kjenner gir `None`, og da brukes ikke posisjonen.
|
||||||
|
pub fn fra_koordinatsystem(navn: &str, x: f64, y: f64) -> Option<Posisjon> {
|
||||||
|
let stor = navn.to_uppercase();
|
||||||
|
let kode: Option<u32> = stor.find("EPSG").and_then(|i| {
|
||||||
|
let siffer: String = stor[i + 4..]
|
||||||
|
.chars()
|
||||||
|
.skip_while(|c| !c.is_ascii_digit())
|
||||||
|
.take_while(|c| c.is_ascii_digit())
|
||||||
|
.collect();
|
||||||
|
siffer.parse().ok()
|
||||||
|
});
|
||||||
|
match kode {
|
||||||
|
Some(3857) | Some(900_913) => Some(fra_web_mercator(x, y)),
|
||||||
|
// Geografiske koordinater: østverdien er lengdegrad og nordverdien breddegrad.
|
||||||
|
Some(4326) | Some(4258) => Some(Posisjon {
|
||||||
|
bredde: y,
|
||||||
|
lengde: x,
|
||||||
|
}),
|
||||||
|
// ETRS89 / EUREF89 UTM sone 28–38.
|
||||||
|
Some(k @ 25828..=25838) => Some(fra_utm(k - 25800, x, y)),
|
||||||
|
// WGS 84 UTM nord, sone 28–38.
|
||||||
|
Some(k @ 32628..=32638) => Some(fra_utm(k - 32600, x, y)),
|
||||||
|
// ETRS89 / UTM med NN2000-høyder, som Kartverket bruker: 5972 er sone 32, 5973 sone 33,
|
||||||
|
// 5975 sone 35.
|
||||||
|
Some(k @ 5972..=5976) => Some(fra_utm(k - 5940, x, y)),
|
||||||
|
_ => {
|
||||||
|
// Uten EPSG-kode: kjenn igjen «UTM32», «UTM 33N» og lignende.
|
||||||
|
let i = stor.find("UTM")?;
|
||||||
|
let sone: String = stor[i + 3..]
|
||||||
|
.chars()
|
||||||
|
.skip_while(|c| !c.is_ascii_digit())
|
||||||
|
.take_while(|c| c.is_ascii_digit())
|
||||||
|
.collect();
|
||||||
|
let sone: u32 = sone.parse().ok()?;
|
||||||
|
(1..=60).contains(&sone).then(|| fra_utm(sone, x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tester {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn web_mercator_fra_vectorworks_gir_washington() {
|
||||||
|
// Verdiene i IfcMapConversion når prosjektstedet ikke er satt i Vectorworks.
|
||||||
|
let p = fra_web_mercator(-8_575_524.352_288_59, 4_705_852.257_469_73);
|
||||||
|
assert!((p.bredde - 38.8895).abs() < 1e-3, "bredde {}", p.bredde);
|
||||||
|
assert!((p.lengde + 77.0352).abs() < 1e-3, "lengde {}", p.lengde);
|
||||||
|
assert!(p.er_standardplassering());
|
||||||
|
assert!(!p.i_norge());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Referanseverdier fra pyproj (PROJ), transformert fra EPSG:4326.
|
||||||
|
#[test]
|
||||||
|
fn utm_og_web_mercator_stemmer_med_proj() {
|
||||||
|
// (sone, øst, nord, breddegrad, lengdegrad, toleranse i grader)
|
||||||
|
//
|
||||||
|
// Innenfor sonen og et stykke utenfor er avviket under 1e-5 grader, om lag én meter.
|
||||||
|
// Tromsø i sone 32 ligger nesten 10 grader utenfor sonen, der rekkeutviklingen gir
|
||||||
|
// om lag 1,3 m avvik. Det er langt innenfor hva valg av klimasted trenger, men viser
|
||||||
|
// hvor grensen går.
|
||||||
|
let utm = [
|
||||||
|
(32, 613_351.291, 6_611_921.980, 59.6300, 11.0100, 1e-5),
|
||||||
|
(32, 596_946.981, 6_642_824.393, 59.9115, 10.7336, 1e-5),
|
||||||
|
(33, 275_055.615, 6_616_968.071, 59.6300, 11.0100, 1e-5),
|
||||||
|
(33, 653_421.188, 7_731_721.083, 69.6492, 18.9553, 1e-5),
|
||||||
|
(35, 188_546.727, 7_747_295.774, 69.6492, 18.9553, 1e-5),
|
||||||
|
(32, 884_909.622, 7_758_204.184, 69.6492, 18.9553, 1e-4),
|
||||||
|
];
|
||||||
|
for (sone, x, y, b, l, toleranse) in utm {
|
||||||
|
let p = fra_utm(sone, x, y);
|
||||||
|
assert!(
|
||||||
|
(p.bredde - b).abs() < toleranse && (p.lengde - l).abs() < toleranse,
|
||||||
|
"sone {sone} ({x}, {y}) ga {p:?}, forventet {b}, {l}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let p = fra_web_mercator(1_225_627.594, 8_317_818.189);
|
||||||
|
assert!(
|
||||||
|
(p.bredde - 59.63).abs() < 1e-6 && (p.lengde - 11.01).abs() < 1e-6,
|
||||||
|
"{p:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sentralmeridianen_gir_eksakt_lengdegrad() {
|
||||||
|
// Østverdi 500 000 ligger på sonens sentralmeridian, 9° øst for sone 32.
|
||||||
|
let p = fra_utm(32, 500_000.0, 6_600_000.0);
|
||||||
|
assert!((p.lengde - 9.0).abs() < 1e-9, "lengde {}", p.lengde);
|
||||||
|
let q = fra_utm(33, 500_000.0, 6_600_000.0);
|
||||||
|
assert!((q.lengde - 15.0).abs() < 1e-9, "lengde {}", q.lengde);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ekvator_paa_sentralmeridianen() {
|
||||||
|
let p = fra_utm(32, 500_000.0, 0.0);
|
||||||
|
assert!(p.bredde.abs() < 1e-9 && (p.lengde - 9.0).abs() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ukjent_koordinatsystem_gir_ingen_posisjon() {
|
||||||
|
assert!(fra_koordinatsystem("EPSG:2154", 650_000.0, 6_860_000.0).is_none());
|
||||||
|
assert!(fra_koordinatsystem("lokalt", 1.0, 2.0).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn epsg_koder_tolkes() {
|
||||||
|
let a = fra_koordinatsystem("EPSG:25832", 500_000.0, 6_600_000.0).unwrap();
|
||||||
|
let b = fra_koordinatsystem("ETRS89 / UTM zone 32N", 500_000.0, 6_600_000.0).unwrap();
|
||||||
|
assert!((a.lengde - 9.0).abs() < 1e-9 && (b.lengde - 9.0).abs() < 1e-9);
|
||||||
|
let c = fra_koordinatsystem("EPSG:5973", 500_000.0, 6_600_000.0).unwrap();
|
||||||
|
assert!(
|
||||||
|
(c.lengde - 15.0).abs() < 1e-9,
|
||||||
|
"5973 er sone 33, fikk {}",
|
||||||
|
c.lengde
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
//! Klimadata: TMY-filer (EPW) for norske kommuner, solberegning og klimakorrigering.
|
//! Klimadata: TMY-filer (EPW) for norske kommuner, solberegning og klimakorrigering.
|
||||||
|
|
||||||
pub mod epw;
|
pub mod epw;
|
||||||
|
pub mod geo;
|
||||||
pub mod korreksjon;
|
pub mod korreksjon;
|
||||||
pub mod sol;
|
pub mod sol;
|
||||||
pub mod tmy;
|
pub mod tmy;
|
||||||
|
|||||||
@@ -76,6 +76,36 @@ fn kommune_av_filnavn(navn: &str) -> Option<String> {
|
|||||||
Some(base.to_string())
|
Some(base.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Klimastedet nærmest en posisjon, og avstanden dit i km.
|
||||||
|
///
|
||||||
|
/// Hver TMY-fil oppgir i første linje koordinatene for punktet dataene gjelder. Bare starten
|
||||||
|
/// av hver fil pakkes ut, så oppslaget går raskt selv om arkivet er stort.
|
||||||
|
pub fn naermeste_kommune(p: &crate::klima::geo::Posisjon) -> Result<(String, f64)> {
|
||||||
|
let sti = sikre_arkiv()?;
|
||||||
|
let fil = std::fs::File::open(&sti)?;
|
||||||
|
let mut zip = zip::ZipArchive::new(fil)?;
|
||||||
|
let mut best: Option<(String, f64)> = None;
|
||||||
|
for i in 0..zip.len() {
|
||||||
|
let oppf = zip.by_index(i)?;
|
||||||
|
let Some(kommune) = kommune_av_filnavn(oppf.name()) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut hode = Vec::with_capacity(512);
|
||||||
|
oppf.take(512).read_to_end(&mut hode)?;
|
||||||
|
let linje = crate::klima::epw::avkod(&hode);
|
||||||
|
let felt: Vec<&str> = linje.lines().next().unwrap_or("").split(',').collect();
|
||||||
|
let tall = |i: usize| felt.get(i).and_then(|s| s.trim().parse::<f64>().ok());
|
||||||
|
let (Some(bredde), Some(lengde)) = (tall(6), tall(7)) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let d = p.avstand_km(&crate::klima::geo::Posisjon { bredde, lengde });
|
||||||
|
if best.as_ref().is_none_or(|(_, bd)| d < *bd) {
|
||||||
|
best = Some((kommune, d));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
best.ok_or_else(|| anyhow!("TMY-arkivet inneholder ingen klimafiler med koordinater"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både
|
/// Henter værdata for en kommune. Navnet sammenlignes normalisert, slik at både
|
||||||
/// «Indre Østfold» og «Indre Ostfold» treffer samme fil.
|
/// «Indre Østfold» og «Indre Ostfold» treffer samme fil.
|
||||||
pub fn for_kommune(kommune: &str) -> Result<Vaerdata> {
|
pub fn for_kommune(kommune: &str) -> Result<Vaerdata> {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ pub fn kontroller(b: &Bygning) -> Vec<Mangel> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if b.klima.kommune.is_none() && b.klima.epw.is_none() {
|
if b.klima.kommune.is_none() && b.klima.epw.is_none() && b.klima.posisjon.is_none() {
|
||||||
ut.push(Mangel {
|
ut.push(Mangel {
|
||||||
felt: "klima.kommune",
|
felt: "klima.kommune",
|
||||||
melding: "Kommune er ikke oppgitt, så beregningen bruker referanseklimaet Oslo. \
|
melding: "Kommune er ikke oppgitt, så beregningen bruker referanseklimaet Oslo. \
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ fn en() -> u32 {
|
|||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
|
||||||
pub struct Klima {
|
pub struct Klima {
|
||||||
|
/// Byggets posisjon som [breddegrad, lengdegrad]. Er kommune ikke oppgitt, velges
|
||||||
|
/// klimastedet nærmest denne posisjonen.
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub posisjon: Option<[f64; 2]>,
|
||||||
/// Kommune for lokalt klima (TMY-fil fra Zenodo 14779899). Utelatt: bare Oslo.
|
/// Kommune for lokalt klima (TMY-fil fra Zenodo 14779899). Utelatt: bare Oslo.
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub kommune: Option<String>,
|
pub kommune: Option<String>,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
//! Kontroll av valg av klimasted. Testene bruker TMY-arkivet og laster det ned ved første kjøring.
|
||||||
|
|
||||||
|
use enok::data::normaliser_stedsnavn;
|
||||||
|
use enok::klima::geo::Posisjon;
|
||||||
|
use enok::klima::tmy::naermeste_kommune;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn naermeste_klimasted_for_tomter_er_indre_ostfold() {
|
||||||
|
let (kommune, km) = naermeste_kommune(&Posisjon {
|
||||||
|
bredde: 59.63,
|
||||||
|
lengde: 11.01,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
normaliser_stedsnavn(&kommune),
|
||||||
|
"indreostfold",
|
||||||
|
"fikk {kommune}"
|
||||||
|
);
|
||||||
|
assert!(km < 25.0, "{km} km");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn naermeste_klimasted_for_oslo_raadhus_er_oslo() {
|
||||||
|
let (kommune, _) = naermeste_kommune(&Posisjon {
|
||||||
|
bredde: 59.9115,
|
||||||
|
lengde: 10.7336,
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(normaliser_stedsnavn(&kommune), "oslo", "fikk {kommune}");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user