1use crate::decoder::EntityDecoder;
11use crate::error::Result;
12use crate::generated::IfcType;
13use crate::schema_gen::DecodedEntity;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum GeoRefSource {
22 MapConversion,
24 EPSetMapConversion,
26 SiteLocation,
28}
29
30impl GeoRefSource {
31 pub fn label(self) -> &'static str {
33 match self {
34 Self::MapConversion => "mapConversion",
35 Self::EPSetMapConversion => "ePSetMapConversion",
36 Self::SiteLocation => "siteLocation",
37 }
38 }
39}
40
41#[derive(Debug, Clone)]
43pub struct GeoReference {
44 pub crs_name: Option<String>,
46 pub crs_description: Option<String>,
48 pub geodetic_datum: Option<String>,
50 pub vertical_datum: Option<String>,
52 pub map_projection: Option<String>,
54 pub map_zone: Option<String>,
56 pub map_unit: Option<String>,
60 pub map_unit_scale: Option<f64>,
63 pub source: GeoRefSource,
66 pub eastings: f64,
68 pub northings: f64,
70 pub orthogonal_height: f64,
72 pub x_axis_abscissa: f64,
74 pub x_axis_ordinate: f64,
76 pub scale: f64,
78}
79
80impl Default for GeoReference {
81 fn default() -> Self {
82 Self {
83 crs_name: None,
84 crs_description: None,
85 geodetic_datum: None,
86 vertical_datum: None,
87 map_projection: None,
88 map_zone: None,
89 map_unit: None,
90 map_unit_scale: None,
91 source: GeoRefSource::MapConversion,
92 eastings: 0.0,
93 northings: 0.0,
94 orthogonal_height: 0.0,
95 x_axis_abscissa: 1.0, x_axis_ordinate: 0.0, scale: 1.0,
98 }
99 }
100}
101
102impl GeoReference {
103 pub fn new() -> Self {
105 Self::default()
106 }
107
108 #[inline]
110 pub fn has_georef(&self) -> bool {
111 self.crs_name.is_some()
112 || self.eastings != 0.0
113 || self.northings != 0.0
114 || self.orthogonal_height != 0.0
115 }
116
117 #[inline]
119 pub fn rotation(&self) -> f64 {
120 self.x_axis_ordinate.atan2(self.x_axis_abscissa)
121 }
122
123 fn normalize_axis(&mut self) {
132 let len = self.x_axis_abscissa.hypot(self.x_axis_ordinate);
133 if len > f64::EPSILON && (len - 1.0).abs() > f64::EPSILON {
134 self.x_axis_abscissa /= len;
135 self.x_axis_ordinate /= len;
136 }
137 }
138
139 #[inline]
147 pub fn local_to_map(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
148 let cos_r = self.x_axis_abscissa;
149 let sin_r = self.x_axis_ordinate;
150 let s = self.scale;
151
152 let e = s * (cos_r * x - sin_r * y) + self.eastings;
153 let n = s * (sin_r * x + cos_r * y) + self.northings;
154 let h = s * z + self.orthogonal_height;
155
156 (e, n, h)
157 }
158
159 #[inline]
161 pub fn map_to_local(&self, e: f64, n: f64, h: f64) -> (f64, f64, f64) {
162 let cos_r = self.x_axis_abscissa;
163 let sin_r = self.x_axis_ordinate;
164 let inv_scale = if self.scale.abs() < f64::EPSILON {
166 1.0
167 } else {
168 1.0 / self.scale
169 };
170
171 let dx = e - self.eastings;
172 let dy = n - self.northings;
173
174 let x = inv_scale * (cos_r * dx + sin_r * dy);
176 let y = inv_scale * (-sin_r * dx + cos_r * dy);
177 let z = inv_scale * (h - self.orthogonal_height);
179
180 (x, y, z)
181 }
182
183 pub fn to_matrix(&self) -> [f64; 16] {
185 let cos_r = self.x_axis_abscissa;
186 let sin_r = self.x_axis_ordinate;
187 let s = self.scale;
188
189 [
191 s * cos_r,
192 s * sin_r,
193 0.0,
194 0.0,
195 -s * sin_r,
196 s * cos_r,
197 0.0,
198 0.0,
199 0.0,
200 0.0,
201 s,
203 0.0,
204 self.eastings,
205 self.northings,
206 self.orthogonal_height,
207 1.0,
208 ]
209 }
210}
211
212pub struct GeoRefExtractor;
214
215impl GeoRefExtractor {
216 pub fn extract(
221 decoder: &mut EntityDecoder,
222 entity_types: &[(u32, IfcType)],
223 ) -> Result<Option<GeoReference>> {
224 let mut map_conversion_id: Option<u32> = None;
229 let mut projected_crs_id: Option<u32> = None;
230
231 for (id, ifc_type) in entity_types {
232 match ifc_type {
233 IfcType::IfcMapConversion => {
234 if map_conversion_id.is_none() {
235 map_conversion_id = Some(*id);
236 }
237 }
238 IfcType::IfcProjectedCRS => {
239 if projected_crs_id.is_none() {
240 projected_crs_id = Some(*id);
241 }
242 }
243 _ => {}
244 }
245 }
246
247 if map_conversion_id.is_none() {
250 if let Some(georef) = Self::extract_from_pset(decoder, entity_types)? {
251 return Ok(Some(georef));
252 }
253 return Self::extract_from_site(decoder, entity_types);
254 }
255
256 let mut georef = GeoReference::new();
257 georef.source = GeoRefSource::MapConversion;
258
259 if let Some(id) = map_conversion_id {
263 let entity = decoder.decode_by_id(id)?;
264 Self::parse_map_conversion(&entity, &mut georef);
265 }
266
267 if let Some(id) = projected_crs_id {
271 let entity = decoder.decode_by_id(id)?;
272 Self::parse_projected_crs(&entity, decoder, &mut georef);
273 }
274
275 georef.normalize_axis();
276
277 if georef.has_georef() {
278 Ok(Some(georef))
279 } else {
280 Ok(None)
281 }
282 }
283
284 fn parse_map_conversion(entity: &DecodedEntity, georef: &mut GeoReference) {
286 if let Some(e) = entity.get_float(2) {
288 georef.eastings = e;
289 }
290 if let Some(n) = entity.get_float(3) {
292 georef.northings = n;
293 }
294 if let Some(h) = entity.get_float(4) {
296 georef.orthogonal_height = h;
297 }
298 if let Some(xa) = entity.get_float(5) {
300 georef.x_axis_abscissa = xa;
301 }
302 if let Some(xo) = entity.get_float(6) {
304 georef.x_axis_ordinate = xo;
305 }
306 if let Some(s) = entity.get_float(7) {
308 georef.scale = s;
309 }
310 }
311
312 fn parse_projected_crs(
314 entity: &DecodedEntity,
315 decoder: &mut EntityDecoder,
316 georef: &mut GeoReference,
317 ) {
318 if let Some(name) = entity.get_string(0) {
320 georef.crs_name = Some(name.to_string());
321 }
322 if let Some(desc) = entity.get_string(1) {
324 georef.crs_description = Some(desc.to_string());
325 }
326 if let Some(datum) = entity.get_string(2) {
328 georef.geodetic_datum = Some(datum.to_string());
329 }
330 if let Some(vdatum) = entity.get_string(3) {
332 georef.vertical_datum = Some(vdatum.to_string());
333 }
334 if let Some(proj) = entity.get_string(4) {
336 georef.map_projection = Some(proj.to_string());
337 }
338 if let Some(zone) = entity.get_string(5) {
340 georef.map_zone = Some(zone.to_string());
341 }
342 if let Some(unit_ref) = entity.get_ref(6) {
348 let mut unit_name = "METRE".to_string();
349 let mut unit_scale = 1.0_f64;
350 if let Ok(unit_entity) = decoder.decode_by_id(unit_ref) {
351 if unit_entity.ifc_type == IfcType::IfcSIUnit {
352 if let Some(prefix_attr) = unit_entity.get(2) {
354 if !prefix_attr.is_null() {
355 if let Some(prefix) = prefix_attr.as_enum() {
356 let multiplier = crate::units::get_si_prefix_multiplier(prefix);
357 if (multiplier - 1.0).abs() > f64::EPSILON {
358 unit_scale = multiplier;
359 let prefix_upper = prefix.to_ascii_uppercase();
360 unit_name = if prefix_upper == "MILLI" {
361 "MILLIMETRE".to_string()
362 } else {
363 format!("{prefix_upper}METRE")
364 };
365 }
366 }
367 }
368 }
369 }
370 }
371 georef.map_unit = Some(unit_name);
372 georef.map_unit_scale = Some(unit_scale);
373 }
374 }
375
376 fn extract_from_pset(
378 decoder: &mut EntityDecoder,
379 entity_types: &[(u32, IfcType)],
380 ) -> Result<Option<GeoReference>> {
381 for (id, ifc_type) in entity_types {
383 if *ifc_type == IfcType::IfcPropertySet {
384 let entity = decoder.decode_by_id(*id)?;
385 if let Some(name) = entity.get_string(2) {
389 if name == "ePSet_MapConversion" || name == "EPset_MapConversion" {
390 return Self::parse_pset_map_conversion(decoder, &entity);
391 }
392 }
393 }
394 }
395 Ok(None)
396 }
397
398 fn parse_pset_map_conversion(
400 decoder: &mut EntityDecoder,
401 pset: &DecodedEntity,
402 ) -> Result<Option<GeoReference>> {
403 let mut georef = GeoReference::new();
404 georef.source = GeoRefSource::EPSetMapConversion;
405
406 if let Some(props_list) = pset.get_list(4) {
408 for prop_attr in props_list {
409 if let Some(prop_id) = prop_attr.as_entity_ref() {
410 let prop = decoder.decode_by_id(prop_id)?;
411 if let Some(name) = prop.get_string(0) {
413 let value = prop.get_float(2);
414 match name {
415 "Eastings" => {
416 if let Some(v) = value {
417 georef.eastings = v;
418 }
419 }
420 "Northings" => {
421 if let Some(v) = value {
422 georef.northings = v;
423 }
424 }
425 "OrthogonalHeight" => {
426 if let Some(v) = value {
427 georef.orthogonal_height = v;
428 }
429 }
430 "XAxisAbscissa" => {
431 if let Some(v) = value {
432 georef.x_axis_abscissa = v;
433 }
434 }
435 "XAxisOrdinate" => {
436 if let Some(v) = value {
437 georef.x_axis_ordinate = v;
438 }
439 }
440 "Scale" => {
441 if let Some(v) = value {
442 georef.scale = v;
443 }
444 }
445 _ => {}
446 }
447 }
448 }
449 }
450 }
451
452 georef.normalize_axis();
453
454 if georef.has_georef() {
455 Ok(Some(georef))
456 } else {
457 Ok(None)
458 }
459 }
460
461 fn extract_from_site(
469 decoder: &mut EntityDecoder,
470 entity_types: &[(u32, IfcType)],
471 ) -> Result<Option<GeoReference>> {
472 for (id, ifc_type) in entity_types {
473 if *ifc_type != IfcType::IfcSite {
474 continue;
475 }
476 let site = decoder.decode_by_id(*id)?;
477 let latitude = Self::compound_plane_angle_to_degrees(&site, 9);
479 let longitude = Self::compound_plane_angle_to_degrees(&site, 10);
480 let (Some(latitude), Some(longitude)) = (latitude, longitude) else {
481 continue;
482 };
483 let elevation = site.get_float(11).unwrap_or(0.0);
484
485 let mut georef = GeoReference::new();
486 georef.source = GeoRefSource::SiteLocation;
487 georef.crs_name = Some("EPSG:4326".to_string());
488 georef.crs_description = Some("Legacy IfcSite geolocation".to_string());
489 georef.geodetic_datum = Some("WGS84".to_string());
490 georef.map_projection = Some("Geographic".to_string());
491 georef.map_unit = Some("DEGREE".to_string());
492 georef.eastings = longitude;
493 georef.northings = latitude;
494 georef.orthogonal_height = elevation;
495 return Ok(Some(georef));
496 }
497 Ok(None)
498 }
499
500 fn compound_plane_angle_to_degrees(entity: &DecodedEntity, index: usize) -> Option<f64> {
505 let list = entity.get_list(index)?;
506 let mut numbers = Vec::with_capacity(4);
507 for value in list {
508 if let Some(v) = value.as_float() {
509 numbers.push(v);
510 }
511 }
512 if numbers.len() < 3 {
513 return None;
514 }
515 let millionths = numbers.get(3).copied().unwrap_or(0.0);
516 let sign = if numbers[0] < 0.0 || numbers[1] < 0.0 || numbers[2] < 0.0 || millionths < 0.0
517 {
518 -1.0
519 } else {
520 1.0
521 };
522 let degrees = numbers[0].abs();
523 let minutes = numbers[1].abs();
524 let seconds = numbers[2].abs();
525 let millionths = millionths.abs();
526 Some(sign * (degrees + minutes / 60.0 + (seconds + millionths / 1_000_000.0) / 3600.0))
527 }
528}
529
530#[derive(Debug, Clone, Default)]
532pub struct RtcOffset {
533 pub x: f64,
535 pub y: f64,
536 pub z: f64,
537}
538
539impl RtcOffset {
540 #[inline]
542 pub fn from_positions(positions: &[f32]) -> Self {
543 if positions.is_empty() {
544 return Self::default();
545 }
546
547 let count = positions.len() / 3;
548 let mut sum = (0.0f64, 0.0f64, 0.0f64);
549
550 for chunk in positions.chunks_exact(3) {
551 sum.0 += chunk[0] as f64;
552 sum.1 += chunk[1] as f64;
553 sum.2 += chunk[2] as f64;
554 }
555
556 Self {
557 x: sum.0 / count as f64,
558 y: sum.1 / count as f64,
559 z: sum.2 / count as f64,
560 }
561 }
562
563 #[inline]
565 pub fn is_significant(&self) -> bool {
566 const THRESHOLD: f64 = 10000.0; self.x.abs() > THRESHOLD || self.y.abs() > THRESHOLD || self.z.abs() > THRESHOLD
568 }
569
570 #[inline]
572 pub fn apply(&self, positions: &mut [f32]) {
573 for chunk in positions.chunks_exact_mut(3) {
574 chunk[0] = (chunk[0] as f64 - self.x) as f32;
575 chunk[1] = (chunk[1] as f64 - self.y) as f32;
576 chunk[2] = (chunk[2] as f64 - self.z) as f32;
577 }
578 }
579}
580
581#[cfg(test)]
582mod tests {
583 use super::*;
584
585 #[test]
586 fn test_georef_local_to_map() {
587 let mut georef = GeoReference::new();
588 georef.eastings = 500000.0;
589 georef.northings = 5000000.0;
590 georef.orthogonal_height = 100.0;
591
592 let (e, n, h) = georef.local_to_map(10.0, 20.0, 5.0);
593 assert!((e - 500010.0).abs() < 1e-10);
594 assert!((n - 5000020.0).abs() < 1e-10);
595 assert!((h - 105.0).abs() < 1e-10);
596 }
597
598 #[test]
599 fn test_georef_map_to_local() {
600 let mut georef = GeoReference::new();
601 georef.eastings = 500000.0;
602 georef.northings = 5000000.0;
603 georef.orthogonal_height = 100.0;
604
605 let (x, y, z) = georef.map_to_local(500010.0, 5000020.0, 105.0);
606 assert!((x - 10.0).abs() < 1e-10);
607 assert!((y - 20.0).abs() < 1e-10);
608 assert!((z - 5.0).abs() < 1e-10);
609 }
610
611 #[test]
612 fn test_georef_with_rotation() {
613 let mut georef = GeoReference::new();
614 georef.eastings = 0.0;
615 georef.northings = 0.0;
616 georef.x_axis_abscissa = 0.0;
618 georef.x_axis_ordinate = 1.0;
619
620 let (e, n, _) = georef.local_to_map(10.0, 0.0, 0.0);
621 assert!(e.abs() < 1e-10);
623 assert!((n - 10.0).abs() < 1e-10);
624 }
625
626 #[test]
627 fn test_rtc_offset() {
628 let positions = vec![
629 500000.0f32,
630 5000000.0,
631 0.0,
632 500010.0,
633 5000010.0,
634 10.0,
635 500020.0,
636 5000020.0,
637 20.0,
638 ];
639
640 let offset = RtcOffset::from_positions(&positions);
641 assert!(offset.is_significant());
642 assert!((offset.x - 500010.0).abs() < 1.0);
643 assert!((offset.y - 5000010.0).abs() < 1.0);
644 }
645
646 #[test]
647 fn test_rtc_apply() {
648 let mut positions = vec![500000.0f32, 5000000.0, 0.0, 500010.0, 5000010.0, 10.0];
649
650 let offset = RtcOffset {
651 x: 500000.0,
652 y: 5000000.0,
653 z: 0.0,
654 };
655
656 offset.apply(&mut positions);
657
658 assert!((positions[0] - 0.0).abs() < 1e-5);
659 assert!((positions[1] - 0.0).abs() < 1e-5);
660 assert!((positions[3] - 10.0).abs() < 1e-5);
661 assert!((positions[4] - 10.0).abs() < 1e-5);
662 }
663}