Skip to main content

ifc_lite_core/
georef.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! IFC Georeferencing Support
6//!
7//! Handles IfcMapConversion and IfcProjectedCRS for coordinate transformations.
8//! Supports both IFC4 native entities and IFC2X3 ePSet_MapConversion fallback.
9
10use crate::decoder::EntityDecoder;
11use crate::error::Result;
12use crate::generated::IfcType;
13use crate::schema_gen::DecodedEntity;
14
15/// Georeferencing information extracted from IFC model
16#[derive(Debug, Clone)]
17pub struct GeoReference {
18    /// CRS name (e.g., "EPSG:32632")
19    pub crs_name: Option<String>,
20    /// Geodetic datum (e.g., "WGS84")
21    pub geodetic_datum: Option<String>,
22    /// Vertical datum (e.g., "NAVD88")
23    pub vertical_datum: Option<String>,
24    /// Map projection (e.g., "UTM Zone 32N")
25    pub map_projection: Option<String>,
26    /// False easting (X offset to map CRS)
27    pub eastings: f64,
28    /// False northing (Y offset to map CRS)
29    pub northings: f64,
30    /// Orthogonal height (Z offset)
31    pub orthogonal_height: f64,
32    /// X-axis abscissa (cos of rotation angle)
33    pub x_axis_abscissa: f64,
34    /// X-axis ordinate (sin of rotation angle)
35    pub x_axis_ordinate: f64,
36    /// Scale factor (default 1.0)
37    pub scale: f64,
38}
39
40impl Default for GeoReference {
41    fn default() -> Self {
42        Self {
43            crs_name: None,
44            geodetic_datum: None,
45            vertical_datum: None,
46            map_projection: None,
47            eastings: 0.0,
48            northings: 0.0,
49            orthogonal_height: 0.0,
50            x_axis_abscissa: 1.0, // No rotation (cos(0) = 1)
51            x_axis_ordinate: 0.0, // No rotation (sin(0) = 0)
52            scale: 1.0,
53        }
54    }
55}
56
57impl GeoReference {
58    /// Create new georeferencing info with defaults
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Check if georeferencing is present
64    #[inline]
65    pub fn has_georef(&self) -> bool {
66        self.crs_name.is_some()
67            || self.eastings != 0.0
68            || self.northings != 0.0
69            || self.orthogonal_height != 0.0
70    }
71
72    /// Get rotation angle in radians
73    #[inline]
74    pub fn rotation(&self) -> f64 {
75        self.x_axis_ordinate.atan2(self.x_axis_abscissa)
76    }
77
78    /// Transform local coordinates to map coordinates
79    #[inline]
80    pub fn local_to_map(&self, x: f64, y: f64, z: f64) -> (f64, f64, f64) {
81        let cos_r = self.x_axis_abscissa;
82        let sin_r = self.x_axis_ordinate;
83        let s = self.scale;
84
85        let e = s * (cos_r * x - sin_r * y) + self.eastings;
86        let n = s * (sin_r * x + cos_r * y) + self.northings;
87        let h = z + self.orthogonal_height;
88
89        (e, n, h)
90    }
91
92    /// Transform map coordinates to local coordinates
93    #[inline]
94    pub fn map_to_local(&self, e: f64, n: f64, h: f64) -> (f64, f64, f64) {
95        let cos_r = self.x_axis_abscissa;
96        let sin_r = self.x_axis_ordinate;
97        // Guard against division by zero
98        let inv_scale = if self.scale.abs() < f64::EPSILON {
99            1.0
100        } else {
101            1.0 / self.scale
102        };
103
104        let dx = e - self.eastings;
105        let dy = n - self.northings;
106
107        // Inverse rotation: transpose of rotation matrix
108        let x = inv_scale * (cos_r * dx + sin_r * dy);
109        let y = inv_scale * (-sin_r * dx + cos_r * dy);
110        let z = h - self.orthogonal_height;
111
112        (x, y, z)
113    }
114
115    /// Get 4x4 transformation matrix (column-major for OpenGL/WebGL)
116    pub fn to_matrix(&self) -> [f64; 16] {
117        let cos_r = self.x_axis_abscissa;
118        let sin_r = self.x_axis_ordinate;
119        let s = self.scale;
120
121        // Column-major 4x4 matrix
122        [
123            s * cos_r,
124            s * sin_r,
125            0.0,
126            0.0,
127            -s * sin_r,
128            s * cos_r,
129            0.0,
130            0.0,
131            0.0,
132            0.0,
133            1.0,
134            0.0,
135            self.eastings,
136            self.northings,
137            self.orthogonal_height,
138            1.0,
139        ]
140    }
141}
142
143/// Extract georeferencing from IFC content
144pub struct GeoRefExtractor;
145
146impl GeoRefExtractor {
147    /// Extract georeferencing from decoder
148    pub fn extract(
149        decoder: &mut EntityDecoder,
150        entity_types: &[(u32, IfcType)],
151    ) -> Result<Option<GeoReference>> {
152        // Find IfcMapConversion and IfcProjectedCRS entities
153        let mut map_conversion_id: Option<u32> = None;
154        let mut projected_crs_id: Option<u32> = None;
155
156        for (id, ifc_type) in entity_types {
157            match ifc_type {
158                IfcType::IfcMapConversion => {
159                    map_conversion_id = Some(*id);
160                }
161                IfcType::IfcProjectedCRS => {
162                    projected_crs_id = Some(*id);
163                }
164                _ => {}
165            }
166        }
167
168        // If no map conversion, try IFC2X3 property set fallback
169        if map_conversion_id.is_none() {
170            return Self::extract_from_pset(decoder, entity_types);
171        }
172
173        let mut georef = GeoReference::new();
174
175        // Parse IfcMapConversion
176        // Attributes: SourceCRS, TargetCRS, Eastings, Northings, OrthogonalHeight,
177        //             XAxisAbscissa, XAxisOrdinate, Scale
178        if let Some(id) = map_conversion_id {
179            let entity = decoder.decode_by_id(id)?;
180            Self::parse_map_conversion(&entity, &mut georef);
181        }
182
183        // Parse IfcProjectedCRS
184        // Attributes: Name, Description, GeodeticDatum, VerticalDatum,
185        //             MapProjection, MapZone, MapUnit
186        if let Some(id) = projected_crs_id {
187            let entity = decoder.decode_by_id(id)?;
188            Self::parse_projected_crs(&entity, &mut georef);
189        }
190
191        if georef.has_georef() {
192            Ok(Some(georef))
193        } else {
194            Ok(None)
195        }
196    }
197
198    /// Parse IfcMapConversion entity
199    fn parse_map_conversion(entity: &DecodedEntity, georef: &mut GeoReference) {
200        // Index 2: Eastings
201        if let Some(e) = entity.get_float(2) {
202            georef.eastings = e;
203        }
204        // Index 3: Northings
205        if let Some(n) = entity.get_float(3) {
206            georef.northings = n;
207        }
208        // Index 4: OrthogonalHeight
209        if let Some(h) = entity.get_float(4) {
210            georef.orthogonal_height = h;
211        }
212        // Index 5: XAxisAbscissa (optional)
213        if let Some(xa) = entity.get_float(5) {
214            georef.x_axis_abscissa = xa;
215        }
216        // Index 6: XAxisOrdinate (optional)
217        if let Some(xo) = entity.get_float(6) {
218            georef.x_axis_ordinate = xo;
219        }
220        // Index 7: Scale (optional, default 1.0)
221        if let Some(s) = entity.get_float(7) {
222            georef.scale = s;
223        }
224    }
225
226    /// Parse IfcProjectedCRS entity
227    fn parse_projected_crs(entity: &DecodedEntity, georef: &mut GeoReference) {
228        // Index 0: Name (e.g., "EPSG:32632")
229        if let Some(name) = entity.get_string(0) {
230            georef.crs_name = Some(name.to_string());
231        }
232        // Index 2: GeodeticDatum
233        if let Some(datum) = entity.get_string(2) {
234            georef.geodetic_datum = Some(datum.to_string());
235        }
236        // Index 3: VerticalDatum
237        if let Some(vdatum) = entity.get_string(3) {
238            georef.vertical_datum = Some(vdatum.to_string());
239        }
240        // Index 4: MapProjection
241        if let Some(proj) = entity.get_string(4) {
242            georef.map_projection = Some(proj.to_string());
243        }
244    }
245
246    /// Extract from IFC2X3 property sets (fallback)
247    fn extract_from_pset(
248        decoder: &mut EntityDecoder,
249        entity_types: &[(u32, IfcType)],
250    ) -> Result<Option<GeoReference>> {
251        // Find IfcPropertySet with name ePSet_MapConversion
252        for (id, ifc_type) in entity_types {
253            if *ifc_type == IfcType::IfcPropertySet {
254                let entity = decoder.decode_by_id(*id)?;
255                if let Some(name) = entity.get_string(0) {
256                    if name == "ePSet_MapConversion" || name == "EPset_MapConversion" {
257                        return Self::parse_pset_map_conversion(decoder, &entity);
258                    }
259                }
260            }
261        }
262        Ok(None)
263    }
264
265    /// Parse ePSet_MapConversion property set
266    fn parse_pset_map_conversion(
267        decoder: &mut EntityDecoder,
268        pset: &DecodedEntity,
269    ) -> Result<Option<GeoReference>> {
270        let mut georef = GeoReference::new();
271
272        // HasProperties is typically at index 4
273        if let Some(props_list) = pset.get_list(4) {
274            for prop_attr in props_list {
275                if let Some(prop_id) = prop_attr.as_entity_ref() {
276                    let prop = decoder.decode_by_id(prop_id)?;
277                    // IfcPropertySingleValue: Name (0), Description (1), NominalValue (2)
278                    if let Some(name) = prop.get_string(0) {
279                        let value = prop.get_float(2);
280                        match name {
281                            "Eastings" => {
282                                if let Some(v) = value {
283                                    georef.eastings = v;
284                                }
285                            }
286                            "Northings" => {
287                                if let Some(v) = value {
288                                    georef.northings = v;
289                                }
290                            }
291                            "OrthogonalHeight" => {
292                                if let Some(v) = value {
293                                    georef.orthogonal_height = v;
294                                }
295                            }
296                            "XAxisAbscissa" => {
297                                if let Some(v) = value {
298                                    georef.x_axis_abscissa = v;
299                                }
300                            }
301                            "XAxisOrdinate" => {
302                                if let Some(v) = value {
303                                    georef.x_axis_ordinate = v;
304                                }
305                            }
306                            "Scale" => {
307                                if let Some(v) = value {
308                                    georef.scale = v;
309                                }
310                            }
311                            _ => {}
312                        }
313                    }
314                }
315            }
316        }
317
318        if georef.has_georef() {
319            Ok(Some(georef))
320        } else {
321            Ok(None)
322        }
323    }
324}
325
326/// RTC (Relative-To-Center) coordinate handler for large coordinates
327#[derive(Debug, Clone, Default)]
328pub struct RtcOffset {
329    /// Center offset (subtracted from all coordinates)
330    pub x: f64,
331    pub y: f64,
332    pub z: f64,
333}
334
335impl RtcOffset {
336    /// Create from centroid of positions
337    #[inline]
338    pub fn from_positions(positions: &[f32]) -> Self {
339        if positions.is_empty() {
340            return Self::default();
341        }
342
343        let count = positions.len() / 3;
344        let mut sum = (0.0f64, 0.0f64, 0.0f64);
345
346        for chunk in positions.chunks_exact(3) {
347            sum.0 += chunk[0] as f64;
348            sum.1 += chunk[1] as f64;
349            sum.2 += chunk[2] as f64;
350        }
351
352        Self {
353            x: sum.0 / count as f64,
354            y: sum.1 / count as f64,
355            z: sum.2 / count as f64,
356        }
357    }
358
359    /// Check if offset is significant (>10km from origin)
360    #[inline]
361    pub fn is_significant(&self) -> bool {
362        const THRESHOLD: f64 = 10000.0; // 10km
363        self.x.abs() > THRESHOLD || self.y.abs() > THRESHOLD || self.z.abs() > THRESHOLD
364    }
365
366    /// Apply offset to positions in-place
367    #[inline]
368    pub fn apply(&self, positions: &mut [f32]) {
369        for chunk in positions.chunks_exact_mut(3) {
370            chunk[0] = (chunk[0] as f64 - self.x) as f32;
371            chunk[1] = (chunk[1] as f64 - self.y) as f32;
372            chunk[2] = (chunk[2] as f64 - self.z) as f32;
373        }
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    use super::*;
380
381    #[test]
382    fn test_georef_local_to_map() {
383        let mut georef = GeoReference::new();
384        georef.eastings = 500000.0;
385        georef.northings = 5000000.0;
386        georef.orthogonal_height = 100.0;
387
388        let (e, n, h) = georef.local_to_map(10.0, 20.0, 5.0);
389        assert!((e - 500010.0).abs() < 1e-10);
390        assert!((n - 5000020.0).abs() < 1e-10);
391        assert!((h - 105.0).abs() < 1e-10);
392    }
393
394    #[test]
395    fn test_georef_map_to_local() {
396        let mut georef = GeoReference::new();
397        georef.eastings = 500000.0;
398        georef.northings = 5000000.0;
399        georef.orthogonal_height = 100.0;
400
401        let (x, y, z) = georef.map_to_local(500010.0, 5000020.0, 105.0);
402        assert!((x - 10.0).abs() < 1e-10);
403        assert!((y - 20.0).abs() < 1e-10);
404        assert!((z - 5.0).abs() < 1e-10);
405    }
406
407    #[test]
408    fn test_georef_with_rotation() {
409        let mut georef = GeoReference::new();
410        georef.eastings = 0.0;
411        georef.northings = 0.0;
412        // 90 degree rotation
413        georef.x_axis_abscissa = 0.0;
414        georef.x_axis_ordinate = 1.0;
415
416        let (e, n, _) = georef.local_to_map(10.0, 0.0, 0.0);
417        // After 90 degree rotation: (10, 0) -> (0, 10)
418        assert!(e.abs() < 1e-10);
419        assert!((n - 10.0).abs() < 1e-10);
420    }
421
422    #[test]
423    fn test_rtc_offset() {
424        let positions = vec![
425            500000.0f32,
426            5000000.0,
427            0.0,
428            500010.0,
429            5000010.0,
430            10.0,
431            500020.0,
432            5000020.0,
433            20.0,
434        ];
435
436        let offset = RtcOffset::from_positions(&positions);
437        assert!(offset.is_significant());
438        assert!((offset.x - 500010.0).abs() < 1.0);
439        assert!((offset.y - 5000010.0).abs() < 1.0);
440    }
441
442    #[test]
443    fn test_rtc_apply() {
444        let mut positions = vec![500000.0f32, 5000000.0, 0.0, 500010.0, 5000010.0, 10.0];
445
446        let offset = RtcOffset {
447            x: 500000.0,
448            y: 5000000.0,
449            z: 0.0,
450        };
451
452        offset.apply(&mut positions);
453
454        assert!((positions[0] - 0.0).abs() < 1e-5);
455        assert!((positions[1] - 0.0).abs() < 1e-5);
456        assert!((positions[3] - 10.0).abs() < 1e-5);
457        assert!((positions[4] - 10.0).abs() < 1e-5);
458    }
459}