ifc_lite_landxml/plan/
model.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
5use std::{
6    cell::RefCell,
7    collections::{BTreeMap, HashMap},
8};
9
10use serde::{Deserialize, Serialize};
11
12use crate::{LandXmlSourceId, LandXmlUnits};
13
14pub type LandXmlProperties = BTreeMap<String, String>;
15
16/// A coordinate in LandXML's authored northing/easting/elevation order.
17#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
18pub struct LandXmlPlanPoint {
19    pub northing: f64,
20    pub easting: f64,
21    pub elevation: Option<f64>,
22}
23
24/// A plan geometry endpoint is either authored directly or a COGO reference.
25#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
26#[serde(tag = "kind", rename_all = "snake_case")]
27pub enum LandXmlPlanPointLocation {
28    Coordinates {
29        point: LandXmlPlanPoint,
30        /// A legal `pntRef` retained alongside authored coordinates. Numeric
31        /// consumers use the coordinates; the reference remains inspectable.
32        pnt_ref: Option<String>,
33    },
34    PointReference {
35        pnt_ref: String,
36    },
37}
38
39/// A COGO point's namespace-local identity is distinct from its display name.
40#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
41pub struct LandXmlCgPoint {
42    pub source_id: LandXmlSourceId,
43    pub scope_id: LandXmlSourceId,
44    pub ordinal: usize,
45    pub name: Option<String>,
46    pub code: Option<String>,
47    pub description: Option<String>,
48    pub point: Option<LandXmlPlanPoint>,
49    pub pnt_ref: Option<String>,
50    pub properties: LandXmlProperties,
51}
52
53/// Lookup tables are built while parsing, so reference resolution never scans
54/// every COGO point for each endpoint or alias hop.
55#[derive(Clone, Debug, Default, PartialEq)]
56pub(crate) struct LandXmlPlanReferenceIndex {
57    pub(crate) scoped: HashMap<(LandXmlSourceId, String), Option<ReferenceTarget>>,
58    pub(crate) global: HashMap<String, Option<ReferenceTarget>>,
59}
60
61#[derive(Clone, Debug, PartialEq)]
62pub(crate) struct ReferenceTarget {
63    pub(crate) index: usize,
64    pub(crate) source_id: LandXmlSourceId,
65}
66
67impl LandXmlPlanReferenceIndex {
68    pub(crate) fn insert(&mut self, point: &LandXmlCgPoint) {
69        self.insert_at(point, point.ordinal.saturating_sub(1));
70    }
71
72    fn insert_at(&mut self, point: &LandXmlCgPoint, index: usize) {
73        // `pntRef` is an authored COGO identity, not an implementation
74        // source-id or a record ordinal. Accepting synthetic provenance here
75        // would allow a display name to collide with an internal id.
76        let mut keys = Vec::new();
77        if let Some(name) = &point.name {
78            keys.push(name.clone());
79        }
80        if let Some(oid) = point.properties.get("oID") {
81            keys.push(oid.clone());
82        }
83        keys.sort();
84        keys.dedup();
85        let target = ReferenceTarget {
86            index,
87            source_id: point.source_id.clone(),
88        };
89        for key in keys {
90            self.scoped
91                .entry((point.scope_id.clone(), key.clone()))
92                .and_modify(|slot| *slot = None)
93                .or_insert(Some(target.clone()));
94            self.global
95                .entry(key)
96                .and_modify(|slot| *slot = None)
97                .or_insert(Some(target.clone()));
98        }
99    }
100
101    pub(crate) fn from_points_checked<E>(
102        points: &[LandXmlCgPoint],
103        mut check: impl FnMut() -> std::result::Result<(), E>,
104    ) -> std::result::Result<Self, E> {
105        let mut index = Self::default();
106        for (position, point) in points.iter().enumerate() {
107            check()?;
108            index.insert_at(point, position);
109        }
110        Ok(index)
111    }
112}
113
114/// A monument retains the source point reference instead of resolving it away.
115#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
116pub struct LandXmlMonument {
117    pub source_id: LandXmlSourceId,
118    /// The COGO scope active when this exact `Monument` was authored.
119    pub point_scope_id: Option<LandXmlSourceId>,
120    pub ordinal: usize,
121    pub name: Option<String>,
122    pub code: Option<String>,
123    pub description: Option<String>,
124    pub pnt_ref: Option<String>,
125    pub point: Option<LandXmlPlanPoint>,
126    pub properties: LandXmlProperties,
127}
128
129#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub enum LandXmlGeometryKind {
132    Line,
133    Curve,
134    IrregularLine,
135}
136
137/// An analytic `CoordGeom` primitive, never a tessellated substitute.
138#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
139pub struct LandXmlPlanGeometry {
140    pub source_id: LandXmlSourceId,
141    pub ordinal: usize,
142    pub kind: LandXmlGeometryKind,
143    pub point_scope_id: Option<LandXmlSourceId>,
144    pub start: LandXmlPlanPointLocation,
145    pub end: LandXmlPlanPointLocation,
146    pub center: Option<LandXmlPlanPointLocation>,
147    pub pi: Option<LandXmlPlanPointLocation>,
148    pub intermediate_points: Vec<LandXmlPlanPoint>,
149    pub rotation: Option<String>,
150    pub radius: Option<f64>,
151    pub declared_length: Option<f64>,
152    pub properties: LandXmlProperties,
153}
154
155#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
156pub struct LandXmlPlanFeature {
157    pub source_id: LandXmlSourceId,
158    pub ordinal: usize,
159    pub name: Option<String>,
160    pub code: Option<String>,
161    pub description: Option<String>,
162    pub properties: LandXmlProperties,
163    pub locations: Vec<LandXmlPlanPointLocation>,
164    pub geometry: Vec<LandXmlPlanGeometry>,
165}
166
167#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
168pub struct LandXmlParcel {
169    pub source_id: LandXmlSourceId,
170    pub ordinal: usize,
171    pub name: Option<String>,
172    pub code: Option<String>,
173    pub description: Option<String>,
174    pub title: Option<String>,
175    pub declared_area: Option<f64>,
176    pub declared_perimeter: Option<f64>,
177    pub declared_area_unit: Option<String>,
178    pub properties: LandXmlProperties,
179    /// Each `CoordGeom` is a distinct legal boundary loop.
180    pub loops: Vec<Vec<LandXmlPlanGeometry>>,
181    /// Set when a source primitive is malformed. The parcel remains
182    /// inspectable but is never promoted to a filled analytic boundary.
183    pub preservation_reason: Option<String>,
184}
185
186/// Whether a parcel can be probed without inventing a filled boundary.
187#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
188#[serde(tag = "kind", rename_all = "snake_case")]
189pub enum LandXmlParcelState {
190    Analytic,
191    PreservedOnly { reason: String },
192}
193
194/// Measurements remain in the document's declared coordinate units.
195#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
196pub struct LandXmlParcelProbe {
197    pub state: LandXmlParcelState,
198    pub perimeter_in_declared_linear_units: Option<f64>,
199    pub area_in_declared_square_units: Option<f64>,
200    pub declared_area: Option<f64>,
201    pub declared_perimeter: Option<f64>,
202    pub perimeter_in_meters: Option<f64>,
203    pub area_in_square_meters: Option<f64>,
204}
205
206/// Bounded source identifiers for host-side navigation and batching.
207#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
208pub struct LandXmlPlanSourceBatch {
209    pub source_ids: Vec<LandXmlSourceId>,
210}
211
212/// Durable COGO and plan records joined to the canonical LandXML document.
213#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
214pub struct LandXmlPlanDocument {
215    /// Namespace-selected grammar used to parse this source, independent from
216    /// the producer's declared version string.
217    pub schema: String,
218    pub version: String,
219    /// Compatibility observations retained for direct plan-parser consumers.
220    pub capability_diagnostics: Vec<crate::LandXmlCapabilityDiagnostic>,
221    pub units: Option<LandXmlUnits>,
222    pub area_unit: Option<String>,
223    pub area_scale_to_square_meters: Option<f64>,
224    pub(crate) cogo_points: Vec<LandXmlCgPoint>,
225    pub monuments: Vec<LandXmlMonument>,
226    pub plan_features: Vec<LandXmlPlanFeature>,
227    pub parcels: Vec<LandXmlParcel>,
228    pub warnings: Vec<String>,
229    /// `None` only after deserialization; an empty index is still a completed
230    /// cache for a source with no COGO points.
231    #[serde(skip)]
232    pub(crate) reference_index: RefCell<Option<LandXmlPlanReferenceIndex>>,
233}
234
235impl LandXmlPlanDocument {
236    /// COGO records are immutable through this view so the derived resolver
237    /// index cannot become stale behind a public `Vec` mutation.
238    pub fn cogo_points(&self) -> &[LandXmlCgPoint] {
239        &self.cogo_points
240    }
241
242    /// Replace authored COGO records and invalidate their resolver index.
243    pub fn replace_cogo_points(&mut self, cogo_points: Vec<LandXmlCgPoint>) {
244        self.cogo_points = cogo_points;
245        *self.reference_index.get_mut() = None;
246    }
247}