1use crate::decoder::EntityDecoder;
11use crate::error::Result;
12use crate::generated::IfcType;
13use crate::schema_gen::{AttributeValue, DecodedEntity};
14
15fn pset_value_string(prop: &DecodedEntity) -> Option<String> {
21 match prop.get(2)? {
22 AttributeValue::String(s) => Some(s.clone()),
23 AttributeValue::List(items) => match (items.first(), items.get(1)) {
24 (Some(AttributeValue::String(_)), Some(AttributeValue::String(v))) => Some(v.clone()),
25 _ => None,
26 },
27 _ => None,
28 }
29}
30
31fn infer_map_unit_scale(label: &str) -> Option<f64> {
38 let n = label.to_uppercase();
39 if n.contains("US") && (n.contains("SURVEY") || n.contains("FTUS")) {
40 return Some(0.3048006096);
41 }
42 if n.contains("FOOT") || n.contains("FEET") {
43 return Some(0.3048);
44 }
45 if n.contains("MILLI") {
46 return Some(0.001);
47 }
48 if n.contains("CENTI") {
49 return Some(0.01);
50 }
51 if n.contains("DECI") {
52 return Some(0.1);
53 }
54 if n.contains("KILO") {
55 return Some(1000.0);
56 }
57 if n.contains("METRE") || n.contains("METER") {
58 return Some(1.0);
59 }
60 None
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum GeoRefSource {
70 MapConversion,
72 EPSetMapConversion,
74 SiteLocation,
76}
77
78impl GeoRefSource {
79 pub fn label(self) -> &'static str {
81 match self {
82 Self::MapConversion => "mapConversion",
83 Self::EPSetMapConversion => "ePSetMapConversion",
84 Self::SiteLocation => "siteLocation",
85 }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct GeoReference {
92 pub crs_name: Option<String>,
94 pub crs_description: Option<String>,
96 pub geodetic_datum: Option<String>,
98 pub vertical_datum: Option<String>,
100 pub map_projection: Option<String>,
102 pub map_zone: Option<String>,
104 pub map_unit: Option<String>,
108 pub map_unit_scale: Option<f64>,
111 pub source: GeoRefSource,
114 pub eastings: f64,
116 pub northings: f64,
118 pub orthogonal_height: f64,
120 pub x_axis_abscissa: f64,
122 pub x_axis_ordinate: f64,
124 pub scale: f64,
126}
127
128impl Default for GeoReference {
129 fn default() -> Self {
130 Self {
131 crs_name: None,
132 crs_description: None,
133 geodetic_datum: None,
134 vertical_datum: None,
135 map_projection: None,
136 map_zone: None,
137 map_unit: None,
138 map_unit_scale: None,
139 source: GeoRefSource::MapConversion,
140 eastings: 0.0,
141 northings: 0.0,
142 orthogonal_height: 0.0,
143 x_axis_abscissa: 1.0, x_axis_ordinate: 0.0, scale: 1.0,
146 }
147 }
148}
149
150impl GeoReference {
151 pub fn new() -> Self {
153 Self::default()
154 }
155
156 #[inline]
158 pub fn has_georef(&self) -> bool {
159 self.crs_name.is_some()
160 || self.eastings != 0.0
161 || self.northings != 0.0
162 || self.orthogonal_height != 0.0
163 }
164
165 #[inline]
167 pub fn rotation(&self) -> f64 {
168 self.x_axis_ordinate.atan2(self.x_axis_abscissa)
169 }
170
171 fn normalize_axis(&mut self) {
180 let len = self.x_axis_abscissa.hypot(self.x_axis_ordinate);
181 if len > f64::EPSILON && (len - 1.0).abs() > f64::EPSILON {
182 self.x_axis_abscissa /= len;
183 self.x_axis_ordinate /= len;
184 }
185 }
186
187 #[inline]
195 pub fn local_to_map(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
196 let cos_r = self.x_axis_abscissa;
197 let sin_r = self.x_axis_ordinate;
198 let s = self.scale;
199
200 let e = s * (cos_r * x - sin_r * y) + self.eastings;
201 let n = s * (sin_r * x + cos_r * y) + self.northings;
202 let h = s * z + self.orthogonal_height;
203
204 (e, n, h)
205 }
206
207 #[inline]
209 pub fn map_to_local(&self, e: f64, n: f64, h: f64) -> (f64, f64, f64) {
210 let cos_r = self.x_axis_abscissa;
211 let sin_r = self.x_axis_ordinate;
212 let inv_scale = if self.scale.abs() < f64::EPSILON {
214 1.0
215 } else {
216 1.0 / self.scale
217 };
218
219 let dx = e - self.eastings;
220 let dy = n - self.northings;
221
222 let x = inv_scale * (cos_r * dx + sin_r * dy);
224 let y = inv_scale * (-sin_r * dx + cos_r * dy);
225 let z = inv_scale * (h - self.orthogonal_height);
227
228 (x, y, z)
229 }
230
231 pub fn to_matrix(&self) -> [f64; 16] {
233 let cos_r = self.x_axis_abscissa;
234 let sin_r = self.x_axis_ordinate;
235 let s = self.scale;
236
237 [
239 s * cos_r,
240 s * sin_r,
241 0.0,
242 0.0,
243 -s * sin_r,
244 s * cos_r,
245 0.0,
246 0.0,
247 0.0,
248 0.0,
249 s,
251 0.0,
252 self.eastings,
253 self.northings,
254 self.orthogonal_height,
255 1.0,
256 ]
257 }
258}
259
260pub struct GeoRefExtractor;
262
263impl GeoRefExtractor {
264 pub fn extract(
269 decoder: &mut EntityDecoder,
270 entity_types: &[(u32, IfcType)],
271 ) -> Result<Option<GeoReference>> {
272 let mut map_conversion_id: Option<u32> = None;
277 let mut projected_crs_id: Option<u32> = None;
278
279 for (id, ifc_type) in entity_types {
280 match ifc_type {
281 IfcType::IfcMapConversion => {
282 if map_conversion_id.is_none() {
283 map_conversion_id = Some(*id);
284 }
285 }
286 IfcType::IfcProjectedCRS => {
287 if projected_crs_id.is_none() {
288 projected_crs_id = Some(*id);
289 }
290 }
291 _ => {}
292 }
293 }
294
295 if map_conversion_id.is_none() {
298 if let Some(georef) = Self::extract_from_pset(decoder, entity_types)? {
299 return Ok(Some(georef));
300 }
301 return Self::extract_from_site(decoder, entity_types);
302 }
303
304 let mut georef = GeoReference::new();
305 georef.source = GeoRefSource::MapConversion;
306
307 if let Some(id) = map_conversion_id {
311 let entity = decoder.decode_by_id(id)?;
312 Self::parse_map_conversion(&entity, &mut georef);
313 }
314
315 if let Some(id) = projected_crs_id {
319 let entity = decoder.decode_by_id(id)?;
320 Self::parse_projected_crs(&entity, decoder, &mut georef);
321 }
322
323 georef.normalize_axis();
324
325 if georef.has_georef() {
326 Ok(Some(georef))
327 } else {
328 Ok(None)
329 }
330 }
331
332 fn parse_map_conversion(entity: &DecodedEntity, georef: &mut GeoReference) {
334 if let Some(e) = entity.get_float(2) {
336 georef.eastings = e;
337 }
338 if let Some(n) = entity.get_float(3) {
340 georef.northings = n;
341 }
342 if let Some(h) = entity.get_float(4) {
344 georef.orthogonal_height = h;
345 }
346 if let Some(xa) = entity.get_float(5) {
348 georef.x_axis_abscissa = xa;
349 }
350 if let Some(xo) = entity.get_float(6) {
352 georef.x_axis_ordinate = xo;
353 }
354 if let Some(s) = entity.get_float(7) {
356 georef.scale = s;
357 }
358 }
359
360 fn parse_projected_crs(
362 entity: &DecodedEntity,
363 decoder: &mut EntityDecoder,
364 georef: &mut GeoReference,
365 ) {
366 if let Some(name) = entity.get_string(0) {
368 georef.crs_name = Some(name.to_string());
369 }
370 if let Some(desc) = entity.get_string(1) {
372 georef.crs_description = Some(desc.to_string());
373 }
374 if let Some(datum) = entity.get_string(2) {
376 georef.geodetic_datum = Some(datum.to_string());
377 }
378 if let Some(vdatum) = entity.get_string(3) {
380 georef.vertical_datum = Some(vdatum.to_string());
381 }
382 if let Some(proj) = entity.get_string(4) {
384 georef.map_projection = Some(proj.to_string());
385 }
386 if let Some(zone) = entity.get_string(5) {
388 georef.map_zone = Some(zone.to_string());
389 }
390 if let Some(unit_ref) = entity.get_ref(6) {
396 let mut unit_name = "METRE".to_string();
397 let mut unit_scale = 1.0_f64;
398 if let Ok(unit_entity) = decoder.decode_by_id(unit_ref) {
399 if unit_entity.ifc_type == IfcType::IfcSIUnit {
400 if let Some(prefix_attr) = unit_entity.get(2) {
402 if !prefix_attr.is_null() {
403 if let Some(prefix) = prefix_attr.as_enum() {
404 let multiplier = crate::units::get_si_prefix_multiplier(prefix);
405 if (multiplier - 1.0).abs() > f64::EPSILON {
406 unit_scale = multiplier;
407 let prefix_upper = prefix.to_ascii_uppercase();
408 unit_name = if prefix_upper == "MILLI" {
409 "MILLIMETRE".to_string()
410 } else {
411 format!("{prefix_upper}METRE")
412 };
413 }
414 }
415 }
416 }
417 }
418 }
419 georef.map_unit = Some(unit_name);
420 georef.map_unit_scale = Some(unit_scale);
421 }
422 }
423
424 fn extract_from_pset(
426 decoder: &mut EntityDecoder,
427 entity_types: &[(u32, IfcType)],
428 ) -> Result<Option<GeoReference>> {
429 let mut map_conversion_pset: Option<u32> = None;
439 let mut projected_crs_pset: Option<u32> = None;
440 for (id, ifc_type) in entity_types {
441 if *ifc_type != IfcType::IfcPropertySet {
442 continue;
443 }
444 let entity = decoder.decode_by_id(*id)?;
445 if let Some(name) = entity.get_string(2) {
446 let lower = name.to_ascii_lowercase();
447 if lower == "epset_mapconversion" && map_conversion_pset.is_none() {
448 map_conversion_pset = Some(*id);
449 } else if lower == "epset_projectedcrs" && projected_crs_pset.is_none() {
450 projected_crs_pset = Some(*id);
451 }
452 }
453 }
454
455 let Some(mc_id) = map_conversion_pset else {
456 return Ok(None);
457 };
458 let mc_entity = decoder.decode_by_id(mc_id)?;
459 Self::parse_pset_map_conversion(decoder, &mc_entity, projected_crs_pset)
460 }
461
462 fn parse_pset_map_conversion(
467 decoder: &mut EntityDecoder,
468 pset: &DecodedEntity,
469 projected_crs_pset: Option<u32>,
470 ) -> Result<Option<GeoReference>> {
471 let mut georef = GeoReference::new();
472 georef.source = GeoRefSource::EPSetMapConversion;
473 let mut target_crs: Option<String> = None;
474
475 if let Some(props_list) = pset.get_list(4) {
477 for prop_attr in props_list {
478 if let Some(prop_id) = prop_attr.as_entity_ref() {
479 let prop = decoder.decode_by_id(prop_id)?;
480 if let Some(name) = prop.get_string(0) {
482 let value = prop.get_float(2);
483 match name {
484 "Eastings" => {
485 if let Some(v) = value {
486 georef.eastings = v;
487 }
488 }
489 "Northings" => {
490 if let Some(v) = value {
491 georef.northings = v;
492 }
493 }
494 "OrthogonalHeight" => {
495 if let Some(v) = value {
496 georef.orthogonal_height = v;
497 }
498 }
499 "XAxisAbscissa" => {
500 if let Some(v) = value {
501 georef.x_axis_abscissa = v;
502 }
503 }
504 "XAxisOrdinate" => {
505 if let Some(v) = value {
506 georef.x_axis_ordinate = v;
507 }
508 }
509 "Scale" => {
510 if let Some(v) = value {
511 georef.scale = v;
512 }
513 }
514 "TargetCRS" => {
515 if let Some(v) = pset_value_string(&prop) {
516 target_crs = Some(v);
517 }
518 }
519 _ => {}
520 }
521 }
522 }
523 }
524 }
525
526 if let Some(crs_id) = projected_crs_pset {
528 let crs_entity = decoder.decode_by_id(crs_id)?;
529 Self::parse_pset_projected_crs(decoder, &crs_entity, &mut georef);
530 }
531 let crs_name_is_blank = georef
536 .crs_name
537 .as_ref()
538 .is_none_or(|name| name.trim().is_empty());
539 if crs_name_is_blank {
540 georef.crs_name = target_crs.filter(|name| !name.trim().is_empty());
541 }
542
543 georef.normalize_axis();
544
545 if georef.has_georef() {
546 Ok(Some(georef))
547 } else {
548 Ok(None)
549 }
550 }
551
552 fn parse_pset_projected_crs(
554 decoder: &mut EntityDecoder,
555 pset: &DecodedEntity,
556 georef: &mut GeoReference,
557 ) {
558 let Some(props_list) = pset.get_list(4) else {
559 return;
560 };
561 for prop_attr in props_list {
562 let Some(prop_id) = prop_attr.as_entity_ref() else {
563 continue;
564 };
565 let Ok(prop) = decoder.decode_by_id(prop_id) else {
566 continue;
567 };
568 let Some(name) = prop.get_string(0) else {
569 continue;
570 };
571 let value = pset_value_string(&prop);
572 match name {
573 "Name" => georef.crs_name = value,
574 "Description" => georef.crs_description = value,
575 "GeodeticDatum" => georef.geodetic_datum = value,
576 "VerticalDatum" => georef.vertical_datum = value,
577 "MapProjection" => georef.map_projection = value,
578 "MapZone" => georef.map_zone = value,
579 "MapUnit" => {
580 georef.map_unit_scale = value.as_deref().and_then(infer_map_unit_scale);
584 georef.map_unit = value;
585 }
586 _ => {}
587 }
588 }
589 }
590
591 fn extract_from_site(
599 decoder: &mut EntityDecoder,
600 entity_types: &[(u32, IfcType)],
601 ) -> Result<Option<GeoReference>> {
602 for (id, ifc_type) in entity_types {
603 if *ifc_type != IfcType::IfcSite {
604 continue;
605 }
606 let site = decoder.decode_by_id(*id)?;
607 let latitude = Self::compound_plane_angle_to_degrees(&site, 9);
609 let longitude = Self::compound_plane_angle_to_degrees(&site, 10);
610 let (Some(latitude), Some(longitude)) = (latitude, longitude) else {
611 continue;
612 };
613 let elevation = site.get_float(11).unwrap_or(0.0);
614
615 let mut georef = GeoReference::new();
616 georef.source = GeoRefSource::SiteLocation;
617 georef.crs_name = Some("EPSG:4326".to_string());
618 georef.crs_description = Some("Legacy IfcSite geolocation".to_string());
619 georef.geodetic_datum = Some("WGS84".to_string());
620 georef.map_projection = Some("Geographic".to_string());
621 georef.map_unit = Some("DEGREE".to_string());
622 georef.eastings = longitude;
623 georef.northings = latitude;
624 georef.orthogonal_height = elevation;
625 return Ok(Some(georef));
626 }
627 Ok(None)
628 }
629
630 fn compound_plane_angle_to_degrees(entity: &DecodedEntity, index: usize) -> Option<f64> {
635 let list = entity.get_list(index)?;
636 let mut numbers = Vec::with_capacity(4);
637 for value in list {
638 if let Some(v) = value.as_float() {
639 numbers.push(v);
640 }
641 }
642 if numbers.len() < 3 {
643 return None;
644 }
645 let millionths = numbers.get(3).copied().unwrap_or(0.0);
646 let sign = if numbers[0] < 0.0 || numbers[1] < 0.0 || numbers[2] < 0.0 || millionths < 0.0
647 {
648 -1.0
649 } else {
650 1.0
651 };
652 let degrees = numbers[0].abs();
653 let minutes = numbers[1].abs();
654 let seconds = numbers[2].abs();
655 let millionths = millionths.abs();
656 Some(sign * (degrees + minutes / 60.0 + (seconds + millionths / 1_000_000.0) / 3600.0))
657 }
658}
659
660#[derive(Debug, Clone, Default)]
662pub struct RtcOffset {
663 pub x: f64,
665 pub y: f64,
666 pub z: f64,
667}
668
669impl RtcOffset {
670 #[inline]
672 pub fn from_positions(positions: &[f32]) -> Self {
673 if positions.is_empty() {
674 return Self::default();
675 }
676
677 let count = positions.len() / 3;
678 let mut sum = (0.0f64, 0.0f64, 0.0f64);
679
680 for chunk in positions.chunks_exact(3) {
681 sum.0 += chunk[0] as f64;
682 sum.1 += chunk[1] as f64;
683 sum.2 += chunk[2] as f64;
684 }
685
686 Self {
687 x: sum.0 / count as f64,
688 y: sum.1 / count as f64,
689 z: sum.2 / count as f64,
690 }
691 }
692
693 #[inline]
695 pub fn is_significant(&self) -> bool {
696 const THRESHOLD: f64 = 10000.0; self.x.abs() > THRESHOLD || self.y.abs() > THRESHOLD || self.z.abs() > THRESHOLD
698 }
699
700 #[inline]
702 pub fn apply(&self, positions: &mut [f32]) {
703 for chunk in positions.chunks_exact_mut(3) {
704 chunk[0] = (chunk[0] as f64 - self.x) as f32;
705 chunk[1] = (chunk[1] as f64 - self.y) as f32;
706 chunk[2] = (chunk[2] as f64 - self.z) as f32;
707 }
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 #[test]
716 fn test_georef_local_to_map() {
717 let mut georef = GeoReference::new();
718 georef.eastings = 500000.0;
719 georef.northings = 5000000.0;
720 georef.orthogonal_height = 100.0;
721
722 let (e, n, h) = georef.local_to_map(10.0, 20.0, 5.0);
723 assert!((e - 500010.0).abs() < 1e-10);
724 assert!((n - 5000020.0).abs() < 1e-10);
725 assert!((h - 105.0).abs() < 1e-10);
726 }
727
728 #[test]
729 fn test_georef_map_to_local() {
730 let mut georef = GeoReference::new();
731 georef.eastings = 500000.0;
732 georef.northings = 5000000.0;
733 georef.orthogonal_height = 100.0;
734
735 let (x, y, z) = georef.map_to_local(500010.0, 5000020.0, 105.0);
736 assert!((x - 10.0).abs() < 1e-10);
737 assert!((y - 20.0).abs() < 1e-10);
738 assert!((z - 5.0).abs() < 1e-10);
739 }
740
741 #[test]
742 fn test_georef_with_rotation() {
743 let mut georef = GeoReference::new();
744 georef.eastings = 0.0;
745 georef.northings = 0.0;
746 georef.x_axis_abscissa = 0.0;
748 georef.x_axis_ordinate = 1.0;
749
750 let (e, n, _) = georef.local_to_map(10.0, 0.0, 0.0);
751 assert!(e.abs() < 1e-10);
753 assert!((n - 10.0).abs() < 1e-10);
754 }
755
756 #[test]
757 fn test_rtc_offset() {
758 let positions = vec![
759 500000.0f32,
760 5000000.0,
761 0.0,
762 500010.0,
763 5000010.0,
764 10.0,
765 500020.0,
766 5000020.0,
767 20.0,
768 ];
769
770 let offset = RtcOffset::from_positions(&positions);
771 assert!(offset.is_significant());
772 assert!((offset.x - 500010.0).abs() < 1.0);
773 assert!((offset.y - 5000010.0).abs() < 1.0);
774 }
775
776 #[test]
777 fn test_rtc_apply() {
778 let mut positions = vec![500000.0f32, 5000000.0, 0.0, 500010.0, 5000010.0, 10.0];
779
780 let offset = RtcOffset {
781 x: 500000.0,
782 y: 5000000.0,
783 z: 0.0,
784 };
785
786 offset.apply(&mut positions);
787
788 assert!((positions[0] - 0.0).abs() < 1e-5);
789 assert!((positions[1] - 0.0).abs() < 1e-5);
790 assert!((positions[3] - 10.0).abs() < 1e-5);
791 assert!((positions[4] - 10.0).abs() < 1e-5);
792 }
793}