ifc_lite_landxml/plan/numeric/
references.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::Ref,
7    collections::{HashMap, HashSet},
8};
9
10use super::{LandXmlPlanDocument, LandXmlPlanPoint, LandXmlPlanPointLocation, ParcelProbeWork};
11use crate::plan::model::LandXmlPlanReferenceIndex;
12use crate::LandXmlMonument;
13
14/// One document-scoped resolver used by bulk adapters. Cached failures are as
15/// important as points: dangling, ambiguous and cyclic aliases must not be
16/// retraversed for every dependent geometry record.
17pub struct LandXmlPlanResolver<'a> {
18    document: &'a LandXmlPlanDocument,
19    cache: HashMap<(Option<crate::LandXmlSourceId>, String), Option<LandXmlPlanPoint>>,
20    work: usize,
21    max_work: usize,
22}
23impl<'a> LandXmlPlanResolver<'a> {
24    pub fn new(document: &'a LandXmlPlanDocument, max_work: usize) -> Self {
25        Self {
26            document,
27            cache: HashMap::new(),
28            work: 0,
29            max_work,
30        }
31    }
32    pub fn resolve(
33        &mut self,
34        scope: Option<&crate::LandXmlSourceId>,
35        location: &LandXmlPlanPointLocation,
36    ) -> std::result::Result<Option<LandXmlPlanPoint>, crate::LandXmlError> {
37        let LandXmlPlanPointLocation::PointReference { pnt_ref } = location else {
38            return Ok(match location {
39                LandXmlPlanPointLocation::Coordinates { point, .. } => Some(*point),
40                _ => None,
41            });
42        };
43        let first = (scope.cloned(), pnt_ref.clone());
44        if let Some(value) = self.cache.get(&first) {
45            return Ok(*value);
46        }
47        let index = self
48            .document
49            .reference_index_with(|| Ok::<(), crate::LandXmlError>(()))?;
50        let mut key = first.clone();
51        let mut path = Vec::new();
52        let mut seen = HashSet::new();
53        let result = loop {
54            self.check_work()?;
55            if let Some(value) = self.cache.get(&key) {
56                break *value;
57            }
58            let Some(position) = self
59                .document
60                .lookup_reference(&index, key.0.as_ref(), &key.1)
61            else {
62                break None;
63            };
64            if !seen.insert(position) {
65                break None;
66            }
67            path.push(key.clone());
68            let Some(point) = self.document.cogo_points().get(position) else {
69                break None;
70            };
71            if let Some(value) = point.point {
72                break Some(value);
73            }
74            let Some(reference) = point.pnt_ref.clone() else {
75                break None;
76            };
77            key = (Some(point.scope_id.clone()), reference);
78        };
79        for item in path {
80            self.cache.insert(item, result);
81        }
82        self.cache.insert(first, result);
83        Ok(result)
84    }
85
86    /// Number of document-scoped resolution/topology work units consumed.
87    /// Bulk adapters use this for deterministic scaling assertions; it is not
88    /// a timing measurement.
89    pub fn work_used(&self) -> usize {
90        self.work
91    }
92
93    pub(super) fn check_work(&mut self) -> std::result::Result<(), crate::LandXmlError> {
94        self.work = self.work.checked_add(1).ok_or_else(|| {
95            crate::LandXmlError::new(
96                crate::LandXmlDiagnosticCode::LimitExceeded,
97                "LandXML bulk plan work limit exceeded",
98            )
99        })?;
100        if self.work > self.max_work {
101            return Err(crate::LandXmlError::new(
102                crate::LandXmlDiagnosticCode::LimitExceeded,
103                "LandXML bulk plan work limit exceeded",
104            ));
105        }
106        Ok(())
107    }
108}
109
110impl LandXmlPlanDocument {
111    /// Restore the same immutable reference cache a direct document parse
112    /// exposes after its plan adapters run. Stream reconstruction moves the
113    /// authored records once, so it rebuilds this cache from final records.
114    pub(crate) fn ensure_reference_index(&self) {
115        let _ = self.reference_index_with(|| Ok::<(), ()>(()));
116    }
117
118    /// Resolve a COGO reference in its producer scope, falling back only when
119    /// the document has one unambiguous name match.
120    pub fn resolve_point(
121        &self,
122        scope_id: Option<&crate::LandXmlSourceId>,
123        location: &LandXmlPlanPointLocation,
124    ) -> Option<LandXmlPlanPoint> {
125        match location {
126            LandXmlPlanPointLocation::Coordinates { point, .. } => Some(*point),
127            LandXmlPlanPointLocation::PointReference { pnt_ref } => self
128                .resolve_reference(scope_id, pnt_ref, self.cogo_points().len(), None)
129                .ok()
130                .flatten(),
131        }
132    }
133
134    /// Resolve a COGO reference while allowing hosts to interrupt alias work.
135    pub fn resolve_point_with_cancel(
136        &self,
137        scope_id: Option<&crate::LandXmlSourceId>,
138        location: &LandXmlPlanPointLocation,
139        cancelled: Option<&dyn crate::LandXmlCancellation>,
140    ) -> std::result::Result<Option<LandXmlPlanPoint>, crate::LandXmlError> {
141        match location {
142            LandXmlPlanPointLocation::Coordinates { point, .. } => Ok(Some(*point)),
143            LandXmlPlanPointLocation::PointReference { pnt_ref } => {
144                self.resolve_reference(scope_id, pnt_ref, self.cogo_points().len(), cancelled)
145            }
146        }
147    }
148
149    fn resolve_reference(
150        &self,
151        scope_id: Option<&crate::LandXmlSourceId>,
152        reference: &str,
153        budget: usize,
154        cancelled: Option<&dyn crate::LandXmlCancellation>,
155    ) -> std::result::Result<Option<LandXmlPlanPoint>, crate::LandXmlError> {
156        let index = self.reference_index_with(|| {
157            if cancelled.is_some_and(crate::LandXmlCancellation::is_cancelled) {
158                return Err(crate::LandXmlError::new(
159                    crate::LandXmlDiagnosticCode::Cancelled,
160                    "COGO reference index rebuild cancelled",
161                ));
162            }
163            Ok(())
164        })?;
165        let mut next_scope = scope_id.cloned();
166        let mut next_reference = reference;
167        let mut remaining = budget;
168        let mut seen = HashSet::new();
169        while remaining > 0 {
170            if cancelled.is_some_and(crate::LandXmlCancellation::is_cancelled) {
171                return Err(crate::LandXmlError::new(
172                    crate::LandXmlDiagnosticCode::Cancelled,
173                    "COGO reference resolution cancelled",
174                ));
175            }
176            let Some(index) = self.lookup_reference(&index, next_scope.as_ref(), next_reference)
177            else {
178                return Ok(None);
179            };
180            if !seen.insert(index) {
181                return Ok(None);
182            }
183            let Some(point) = self.cogo_points().get(index) else {
184                return Ok(None);
185            };
186            if let Some(value) = point.point {
187                return Ok(Some(value));
188            }
189            next_scope = Some(point.scope_id.clone());
190            let Some(next) = point.pnt_ref.as_deref() else {
191                return Ok(None);
192            };
193            next_reference = next;
194            remaining -= 1;
195        }
196        Ok(None)
197    }
198
199    pub(super) fn resolve_point_with_work<W: ParcelProbeWork>(
200        &self,
201        scope_id: Option<&crate::LandXmlSourceId>,
202        location: &LandXmlPlanPointLocation,
203        work: &mut W,
204    ) -> std::result::Result<Option<LandXmlPlanPoint>, crate::LandXmlError> {
205        match location {
206            LandXmlPlanPointLocation::Coordinates { point, .. } => Ok(Some(*point)),
207            LandXmlPlanPointLocation::PointReference { pnt_ref } => {
208                let index = self.reference_index_with(|| work.check())?;
209                let mut next_scope = scope_id.cloned();
210                let mut next_reference = pnt_ref.as_str();
211                let mut remaining = self.cogo_points().len();
212                let mut seen = HashSet::new();
213                while remaining > 0 {
214                    work.check()?;
215                    let Some(index) =
216                        self.lookup_reference(&index, next_scope.as_ref(), next_reference)
217                    else {
218                        return Ok(None);
219                    };
220                    if !seen.insert(index) {
221                        return Ok(None);
222                    }
223                    let Some(point) = self.cogo_points().get(index) else {
224                        return Ok(None);
225                    };
226                    if point.point.is_some() {
227                        return Ok(point.point);
228                    }
229                    next_scope = Some(point.scope_id.clone());
230                    let Some(next) = point.pnt_ref.as_deref() else {
231                        return Ok(None);
232                    };
233                    next_reference = next;
234                    remaining -= 1;
235                }
236                Ok(None)
237            }
238        }
239    }
240
241    /// Deserialize deliberately omits this derived cache. Rebuild it once on
242    /// the first lookup, polling the same cancellation/work budget used by the
243    /// caller, then retain it for later lookups. Public point mutation is
244    /// still validated by `lookup_reference` before a cached target is used.
245    fn reference_index_with<E>(
246        &self,
247        check: impl FnMut() -> std::result::Result<(), E>,
248    ) -> std::result::Result<Ref<'_, LandXmlPlanReferenceIndex>, E> {
249        if self.reference_index.borrow().is_none() {
250            let rebuilt =
251                LandXmlPlanReferenceIndex::from_points_checked(self.cogo_points(), check)?;
252            *self.reference_index.borrow_mut() = Some(rebuilt);
253        }
254        Ok(Ref::map(self.reference_index.borrow(), |index| {
255            index
256                .as_ref()
257                .expect("reference index is initialized before it is borrowed")
258        }))
259    }
260
261    fn lookup_reference(
262        &self,
263        index: &LandXmlPlanReferenceIndex,
264        scope_id: Option<&crate::LandXmlSourceId>,
265        reference: &str,
266    ) -> Option<usize> {
267        let scoped_target =
268            scope_id.and_then(|scope| index.scoped.get(&(scope.clone(), reference.to_owned())));
269        let target = scoped_target.or_else(|| index.global.get(reference));
270        if let Some(Some(target)) = target {
271            if self
272                .cogo_points()
273                .get(target.index)
274                .is_some_and(|point| point.source_id == target.source_id)
275            {
276                return Some(target.index);
277            }
278        }
279        // A complete index is immutable with respect to public document
280        // state. Do not fall back to an O(n) scan: it both bypasses the
281        // ambiguity rules and makes missing references uninterruptible.
282        None
283    }
284
285    /// Resolve a monument's direct coordinate or its scoped `pntRef`.
286    pub fn resolve_monument_point(&self, monument: &LandXmlMonument) -> Option<LandXmlPlanPoint> {
287        monument.point.or_else(|| {
288            monument.pnt_ref.as_ref().and_then(|pnt_ref| {
289                self.resolve_point(
290                    monument.point_scope_id.as_ref(),
291                    &LandXmlPlanPointLocation::PointReference {
292                        pnt_ref: pnt_ref.clone(),
293                    },
294                )
295            })
296        })
297    }
298}