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
225 lines
8.9 KiB
Rust
225 lines
8.9 KiB
Rust
//! 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
|
||
);
|
||
}
|
||
}
|