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::schema::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 { 1.0 } else { 1.0 / self.scale };
99
100        let dx = e - self.eastings;
101        let dy = n - self.northings;
102
103        // Inverse rotation: transpose of rotation matrix
104        let x = inv_scale * (cos_r * dx + sin_r * dy);
105        let y = inv_scale * (-sin_r * dx + cos_r * dy);
106        let z = h - self.orthogonal_height;
107
108        (x, y, z)
109    }
110
111    /// Get 4x4 transformation matrix (column-major for OpenGL/WebGL)
112    pub fn to_matrix(&self) -> [f64; 16] {
113        let cos_r = self.x_axis_abscissa;
114        let sin_r = self.x_axis_ordinate;
115        let s = self.scale;
116
117        // Column-major 4x4 matrix
118        [
119            s * cos_r,  s * sin_r,  0.0, 0.0,
120            -s * sin_r, s * cos_r,  0.0, 0.0,
121            0.0,        0.0,        1.0, 0.0,
122            self.eastings, self.northings, self.orthogonal_height, 1.0,
123        ]
124    }
125}
126
127/// Extract georeferencing from IFC content
128pub struct GeoRefExtractor;
129
130impl GeoRefExtractor {
131    /// Extract georeferencing from decoder
132    pub fn extract(decoder: &mut EntityDecoder, entity_types: &[(u32, IfcType)]) -> Result<Option<GeoReference>> {
133        // Find IfcMapConversion and IfcProjectedCRS entities
134        let mut map_conversion_id: Option<u32> = None;
135        let mut projected_crs_id: Option<u32> = None;
136
137        for (id, ifc_type) in entity_types {
138            match ifc_type {
139                IfcType::IfcMapConversion => {
140                    map_conversion_id = Some(*id);
141                }
142                IfcType::IfcProjectedCRS => {
143                    projected_crs_id = Some(*id);
144                }
145                _ => {}
146            }
147        }
148
149        // If no map conversion, try IFC2X3 property set fallback
150        if map_conversion_id.is_none() {
151            return Self::extract_from_pset(decoder, entity_types);
152        }
153
154        let mut georef = GeoReference::new();
155
156        // Parse IfcMapConversion
157        // Attributes: SourceCRS, TargetCRS, Eastings, Northings, OrthogonalHeight,
158        //             XAxisAbscissa, XAxisOrdinate, Scale
159        if let Some(id) = map_conversion_id {
160            let entity = decoder.decode_by_id(id)?;
161            Self::parse_map_conversion(&entity, &mut georef);
162        }
163
164        // Parse IfcProjectedCRS
165        // Attributes: Name, Description, GeodeticDatum, VerticalDatum,
166        //             MapProjection, MapZone, MapUnit
167        if let Some(id) = projected_crs_id {
168            let entity = decoder.decode_by_id(id)?;
169            Self::parse_projected_crs(&entity, &mut georef);
170        }
171
172        if georef.has_georef() {
173            Ok(Some(georef))
174        } else {
175            Ok(None)
176        }
177    }
178
179    /// Parse IfcMapConversion entity
180    fn parse_map_conversion(entity: &DecodedEntity, georef: &mut GeoReference) {
181        // Index 2: Eastings
182        if let Some(e) = entity.get_float(2) {
183            georef.eastings = e;
184        }
185        // Index 3: Northings
186        if let Some(n) = entity.get_float(3) {
187            georef.northings = n;
188        }
189        // Index 4: OrthogonalHeight
190        if let Some(h) = entity.get_float(4) {
191            georef.orthogonal_height = h;
192        }
193        // Index 5: XAxisAbscissa (optional)
194        if let Some(xa) = entity.get_float(5) {
195            georef.x_axis_abscissa = xa;
196        }
197        // Index 6: XAxisOrdinate (optional)
198        if let Some(xo) = entity.get_float(6) {
199            georef.x_axis_ordinate = xo;
200        }
201        // Index 7: Scale (optional, default 1.0)
202        if let Some(s) = entity.get_float(7) {
203            georef.scale = s;
204        }
205    }
206
207    /// Parse IfcProjectedCRS entity
208    fn parse_projected_crs(entity: &DecodedEntity, georef: &mut GeoReference) {
209        // Index 0: Name (e.g., "EPSG:32632")
210        if let Some(name) = entity.get_string(0) {
211            georef.crs_name = Some(name.to_string());
212        }
213        // Index 2: GeodeticDatum
214        if let Some(datum) = entity.get_string(2) {
215            georef.geodetic_datum = Some(datum.to_string());
216        }
217        // Index 3: VerticalDatum
218        if let Some(vdatum) = entity.get_string(3) {
219            georef.vertical_datum = Some(vdatum.to_string());
220        }
221        // Index 4: MapProjection
222        if let Some(proj) = entity.get_string(4) {
223            georef.map_projection = Some(proj.to_string());
224        }
225    }
226
227    /// Extract from IFC2X3 property sets (fallback)
228    fn extract_from_pset(
229        decoder: &mut EntityDecoder,
230        entity_types: &[(u32, IfcType)],
231    ) -> Result<Option<GeoReference>> {
232        // Find IfcPropertySet with name ePSet_MapConversion
233        for (id, ifc_type) in entity_types {
234            if *ifc_type == IfcType::IfcPropertySet {
235                let entity = decoder.decode_by_id(*id)?;
236                if let Some(name) = entity.get_string(0) {
237                    if name == "ePSet_MapConversion" || name == "EPset_MapConversion" {
238                        return Self::parse_pset_map_conversion(decoder, &entity);
239                    }
240                }
241            }
242        }
243        Ok(None)
244    }
245
246    /// Parse ePSet_MapConversion property set
247    fn parse_pset_map_conversion(
248        decoder: &mut EntityDecoder,
249        pset: &DecodedEntity,
250    ) -> Result<Option<GeoReference>> {
251        let mut georef = GeoReference::new();
252
253        // HasProperties is typically at index 4
254        if let Some(props_list) = pset.get_list(4) {
255            for prop_attr in props_list {
256                if let Some(prop_id) = prop_attr.as_entity_ref() {
257                    let prop = decoder.decode_by_id(prop_id)?;
258                    // IfcPropertySingleValue: Name (0), Description (1), NominalValue (2)
259                    if let Some(name) = prop.get_string(0) {
260                        let value = prop.get_float(2);
261                        match name {
262                            "Eastings" => {
263                                if let Some(v) = value {
264                                    georef.eastings = v;
265                                }
266                            }
267                            "Northings" => {
268                                if let Some(v) = value {
269                                    georef.northings = v;
270                                }
271                            }
272                            "OrthogonalHeight" => {
273                                if let Some(v) = value {
274                                    georef.orthogonal_height = v;
275                                }
276                            }
277                            "XAxisAbscissa" => {
278                                if let Some(v) = value {
279                                    georef.x_axis_abscissa = v;
280                                }
281                            }
282                            "XAxisOrdinate" => {
283                                if let Some(v) = value {
284                                    georef.x_axis_ordinate = v;
285                                }
286                            }
287                            "Scale" => {
288                                if let Some(v) = value {
289                                    georef.scale = v;
290                                }
291                            }
292                            _ => {}
293                        }
294                    }
295                }
296            }
297        }
298
299        if georef.has_georef() {
300            Ok(Some(georef))
301        } else {
302            Ok(None)
303        }
304    }
305}
306
307/// RTC (Relative-To-Center) coordinate handler for large coordinates
308#[derive(Debug, Clone, Default)]
309pub struct RtcOffset {
310    /// Center offset (subtracted from all coordinates)
311    pub x: f64,
312    pub y: f64,
313    pub z: f64,
314}
315
316impl RtcOffset {
317    /// Create from centroid of positions
318    #[inline]
319    pub fn from_positions(positions: &[f32]) -> Self {
320        if positions.is_empty() {
321            return Self::default();
322        }
323
324        let count = positions.len() / 3;
325        let mut sum = (0.0f64, 0.0f64, 0.0f64);
326
327        for chunk in positions.chunks_exact(3) {
328            sum.0 += chunk[0] as f64;
329            sum.1 += chunk[1] as f64;
330            sum.2 += chunk[2] as f64;
331        }
332
333        Self {
334            x: sum.0 / count as f64,
335            y: sum.1 / count as f64,
336            z: sum.2 / count as f64,
337        }
338    }
339
340    /// Check if offset is significant (>10km from origin)
341    #[inline]
342    pub fn is_significant(&self) -> bool {
343        const THRESHOLD: f64 = 10000.0; // 10km
344        self.x.abs() > THRESHOLD || self.y.abs() > THRESHOLD || self.z.abs() > THRESHOLD
345    }
346
347    /// Apply offset to positions in-place
348    #[inline]
349    pub fn apply(&self, positions: &mut [f32]) {
350        for chunk in positions.chunks_exact_mut(3) {
351            chunk[0] = (chunk[0] as f64 - self.x) as f32;
352            chunk[1] = (chunk[1] as f64 - self.y) as f32;
353            chunk[2] = (chunk[2] as f64 - self.z) as f32;
354        }
355    }
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361
362    #[test]
363    fn test_georef_local_to_map() {
364        let mut georef = GeoReference::new();
365        georef.eastings = 500000.0;
366        georef.northings = 5000000.0;
367        georef.orthogonal_height = 100.0;
368
369        let (e, n, h) = georef.local_to_map(10.0, 20.0, 5.0);
370        assert!((e - 500010.0).abs() < 1e-10);
371        assert!((n - 5000020.0).abs() < 1e-10);
372        assert!((h - 105.0).abs() < 1e-10);
373    }
374
375    #[test]
376    fn test_georef_map_to_local() {
377        let mut georef = GeoReference::new();
378        georef.eastings = 500000.0;
379        georef.northings = 5000000.0;
380        georef.orthogonal_height = 100.0;
381
382        let (x, y, z) = georef.map_to_local(500010.0, 5000020.0, 105.0);
383        assert!((x - 10.0).abs() < 1e-10);
384        assert!((y - 20.0).abs() < 1e-10);
385        assert!((z - 5.0).abs() < 1e-10);
386    }
387
388    #[test]
389    fn test_georef_with_rotation() {
390        let mut georef = GeoReference::new();
391        georef.eastings = 0.0;
392        georef.northings = 0.0;
393        // 90 degree rotation
394        georef.x_axis_abscissa = 0.0;
395        georef.x_axis_ordinate = 1.0;
396
397        let (e, n, _) = georef.local_to_map(10.0, 0.0, 0.0);
398        // After 90 degree rotation: (10, 0) -> (0, 10)
399        assert!(e.abs() < 1e-10);
400        assert!((n - 10.0).abs() < 1e-10);
401    }
402
403    #[test]
404    fn test_rtc_offset() {
405        let positions = vec![
406            500000.0f32, 5000000.0, 0.0,
407            500010.0, 5000010.0, 10.0,
408            500020.0, 5000020.0, 20.0,
409        ];
410
411        let offset = RtcOffset::from_positions(&positions);
412        assert!(offset.is_significant());
413        assert!((offset.x - 500010.0).abs() < 1.0);
414        assert!((offset.y - 5000010.0).abs() < 1.0);
415    }
416
417    #[test]
418    fn test_rtc_apply() {
419        let mut positions = vec![
420            500000.0f32, 5000000.0, 0.0,
421            500010.0, 5000010.0, 10.0,
422        ];
423
424        let offset = RtcOffset {
425            x: 500000.0,
426            y: 5000000.0,
427            z: 0.0,
428        };
429
430        offset.apply(&mut positions);
431
432        assert!((positions[0] - 0.0).abs() < 1e-5);
433        assert!((positions[1] - 0.0).abs() < 1e-5);
434        assert!((positions[3] - 10.0).abs() < 1e-5);
435        assert!((positions[4] - 10.0).abs() < 1e-5);
436    }
437}