ifc_lite_landxml/plan/numeric/
parcel_probe.rs1use super::{
6 measure::{finite_measure, finite_sum},
7 parcel_measure::probe_loop,
8 topology::{point_in_loop, segments_intersect},
9 LandXmlParcel, LandXmlParcelProbe, LandXmlParcelState, LandXmlPlanDocument,
10 LandXmlPlanGeometry, LandXmlPlanPoint, LandXmlPlanPointLocation, LandXmlPlanResolver,
11 ParcelProbeWork,
12};
13use crate::plan::parser::area_scale;
14
15const MAX_PARCEL_TOPOLOGY_EDGES: usize = 700;
16const MAX_PARCEL_TOPOLOGY_WORK: usize = 1_000_000;
17
18struct TopologyBudget<'a> {
19 work: usize,
20 max_work: usize,
21 cancelled: Option<&'a dyn crate::LandXmlCancellation>,
22}
23
24impl TopologyBudget<'_> {
25 fn check(&mut self) -> std::result::Result<(), crate::LandXmlError> {
26 if self
27 .cancelled
28 .is_some_and(crate::LandXmlCancellation::is_cancelled)
29 {
30 return Err(crate::LandXmlError::new(
31 crate::LandXmlDiagnosticCode::Cancelled,
32 "parcel probe cancelled",
33 ));
34 }
35 self.work = self.work.checked_add(1).ok_or_else(|| {
36 crate::LandXmlError::new(
37 crate::LandXmlDiagnosticCode::LimitExceeded,
38 "parcel topology work limit exceeded",
39 )
40 })?;
41 if self.work > self.max_work {
42 return Err(crate::LandXmlError::new(
43 crate::LandXmlDiagnosticCode::LimitExceeded,
44 "parcel topology work limit exceeded",
45 ));
46 }
47 Ok(())
48 }
49}
50
51impl ParcelProbeWork for TopologyBudget<'_> {
52 fn check(&mut self) -> std::result::Result<(), crate::LandXmlError> {
53 TopologyBudget::check(self)
54 }
55
56 fn resolve(
57 &mut self,
58 document: &LandXmlPlanDocument,
59 scope: Option<&crate::LandXmlSourceId>,
60 location: &LandXmlPlanPointLocation,
61 ) -> std::result::Result<Option<LandXmlPlanPoint>, crate::LandXmlError> {
62 document.resolve_point_with_work(scope, location, self)
63 }
64}
65
66struct BulkParcelProbe<'resolver, 'document> {
70 resolver: &'resolver mut LandXmlPlanResolver<'document>,
71 topology: TopologyBudget<'static>,
72 topology_exhausted: bool,
73}
74
75impl<'resolver, 'document> BulkParcelProbe<'resolver, 'document> {
76 fn new(resolver: &'resolver mut LandXmlPlanResolver<'document>, max_work: usize) -> Self {
77 Self {
78 resolver,
79 topology: TopologyBudget {
80 work: 0,
81 max_work,
82 cancelled: None,
83 },
84 topology_exhausted: false,
85 }
86 }
87}
88
89impl ParcelProbeWork for BulkParcelProbe<'_, '_> {
90 fn check(&mut self) -> std::result::Result<(), crate::LandXmlError> {
91 let result = self.topology.check();
92 if result
93 .as_ref()
94 .is_err_and(|error| error.code == crate::LandXmlDiagnosticCode::LimitExceeded)
95 {
96 self.topology_exhausted = true;
97 }
98 result
99 }
100
101 fn resolve(
102 &mut self,
103 _document: &LandXmlPlanDocument,
104 scope: Option<&crate::LandXmlSourceId>,
105 location: &LandXmlPlanPointLocation,
106 ) -> std::result::Result<Option<LandXmlPlanPoint>, crate::LandXmlError> {
107 self.resolver.resolve(scope, location)
108 }
109}
110
111impl LandXmlPlanDocument {
112 pub fn probe_parcel(&self, parcel: &LandXmlParcel) -> LandXmlParcelProbe {
114 self.probe_parcel_with_cancel(parcel, 1_000_000, None)
115 .unwrap_or_else(|error| preserved(parcel, &error.message))
116 }
117
118 pub fn probe_parcel_with_cancel(
120 &self,
121 parcel: &LandXmlParcel,
122 max_topology_work: usize,
123 cancelled: Option<&dyn crate::LandXmlCancellation>,
124 ) -> std::result::Result<LandXmlParcelProbe, crate::LandXmlError> {
125 let mut budget = TopologyBudget {
126 work: 0,
127 max_work: max_topology_work,
128 cancelled,
129 };
130 probe_parcel_with_work(self, parcel, &mut budget)
131 }
132
133 pub fn probe_parcels_with_resolver(
138 &self,
139 parcels: &[LandXmlParcel],
140 resolver: &mut LandXmlPlanResolver<'_>,
141 ) -> std::result::Result<Vec<LandXmlParcelProbe>, crate::LandXmlError> {
142 let mut probes = Vec::with_capacity(parcels.len());
143 for parcel in parcels {
144 let Some(max_topology_work) = parcel_topology_budget(parcel) else {
145 probes.push(preserved(parcel, "parcel topology work limit exceeded"));
146 continue;
147 };
148 let mut work = BulkParcelProbe::new(resolver, max_topology_work);
149 match probe_parcel_with_work(self, parcel, &mut work) {
150 Ok(probe) => probes.push(probe),
151 Err(_) if work.topology_exhausted => {
152 probes.push(preserved(parcel, "parcel topology work limit exceeded"));
153 }
154 Err(error)
157 if matches!(
158 error.code,
159 crate::LandXmlDiagnosticCode::LimitExceeded
160 | crate::LandXmlDiagnosticCode::Cancelled
161 ) =>
162 {
163 return Err(error);
164 }
165 Err(error) => probes.push(preserved(parcel, &error.message)),
166 }
167 }
168 Ok(probes)
169 }
170}
171
172fn parcel_topology_budget(parcel: &LandXmlParcel) -> Option<usize> {
179 let mut edges = 0usize;
180 for geometry in parcel.loops.iter().flatten() {
181 let geometry_edges = match geometry.kind {
182 super::LandXmlGeometryKind::Line => 1,
183 super::LandXmlGeometryKind::Curve => 64,
184 super::LandXmlGeometryKind::IrregularLine => {
185 geometry.intermediate_points.len().checked_add(1)?
186 }
187 };
188 edges = edges.checked_add(geometry_edges)?;
189 if edges > MAX_PARCEL_TOPOLOGY_EDGES {
190 return None;
191 }
192 }
193 let work = edges
194 .checked_mul(edges)?
195 .checked_mul(2)?
196 .checked_add(edges.checked_mul(4)?)?
197 .checked_add(64)?;
198 (work <= MAX_PARCEL_TOPOLOGY_WORK).then_some(work)
199}
200
201fn probe_parcel_with_work<W: ParcelProbeWork>(
202 document: &LandXmlPlanDocument,
203 parcel: &LandXmlParcel,
204 work: &mut W,
205) -> std::result::Result<LandXmlParcelProbe, crate::LandXmlError> {
206 if let Some(reason) = &parcel.preservation_reason {
207 return Ok(preserved(parcel, reason));
208 }
209 if parcel.loops.len() > 1
215 && parcel
216 .loops
217 .iter()
218 .flatten()
219 .any(|geometry| geometry.kind == super::LandXmlGeometryKind::Curve)
220 {
221 return Ok(preserved(parcel, "multi-loop boundary with curves"));
222 }
223 let mut perimeter = 0.0;
224 let mut loop_areas = Vec::with_capacity(parcel.loops.len());
226 let mut all_segments = Vec::new();
227 for loop_geometry in &parcel.loops {
228 if !supports_analytic_curve_loop(loop_geometry) {
229 return Ok(preserved(parcel, "unsupported curved boundary topology"));
230 }
231 let Some(loop_probe) = probe_loop(document, loop_geometry, work)? else {
232 return Ok(preserved(parcel, "open or unresolved boundary"));
233 };
234 if loop_probe.self_intersects {
235 return Ok(preserved(parcel, "self-intersecting boundary"));
236 }
237 perimeter = finite_sum(perimeter, loop_probe.perimeter)?;
238 if segments_intersect(&all_segments, &loop_probe.segments, work)? {
239 return Ok(preserved(parcel, "cross-loop or retraced boundary"));
240 }
241 let start = all_segments.len();
242 all_segments.extend(loop_probe.segments);
243 loop_areas.push((loop_probe.twice_area.abs(), start..all_segments.len()));
244 }
245 if parcel.loops.is_empty() {
246 return Ok(preserved(parcel, "missing CoordGeom boundary"));
247 }
248 let mut twice_area = 0.0;
254 for (index, (area, range)) in loop_areas.iter().enumerate() {
255 let probe_point = all_segments[range.start].0;
256 let mut is_hole = false;
257 for (other_index, (_, other)) in loop_areas.iter().enumerate() {
258 if other_index != index
259 && point_in_loop(probe_point, &all_segments[other.clone()], work)?
260 {
261 is_hole = !is_hole;
262 }
263 }
264 let signed = if is_hole { -*area } else { *area };
265 twice_area = finite_sum(twice_area, signed)?;
266 }
267 if twice_area <= 0.0 {
268 return Ok(preserved(parcel, "zero-area boundary"));
269 }
270 let coordinate_area = finite_measure(twice_area.abs() * 0.5)?;
271 let (perimeter_in_meters, area_in_square_meters, area_in_declared_square_units) =
272 match &document.units {
273 Some(units) => {
274 let square_meters =
275 finite_measure(coordinate_area * units.linear_scale_to_meters.powi(2))?;
276 let declared_scale = match parcel.declared_area_unit.as_deref() {
277 Some(unit) => match area_scale(unit) {
278 Ok(scale) => scale,
279 Err(_) => return Ok(preserved(parcel, "unsupported parcel area unit")),
280 },
281 None => document
282 .area_scale_to_square_meters
283 .unwrap_or(units.linear_scale_to_meters.powi(2)),
284 };
285 if !declared_scale.is_finite() || declared_scale <= 0.0 {
286 return Ok(preserved(parcel, "invalid declared area unit scale"));
287 }
288 (
289 Some(finite_measure(perimeter * units.linear_scale_to_meters)?),
290 Some(square_meters),
291 Some(finite_measure(square_meters / declared_scale)?),
292 )
293 }
294 None if parcel.declared_area_unit.is_some() => {
295 return Ok(preserved(
296 parcel,
297 "parcel area unit requires document linear units",
298 ));
299 }
300 None => (None, None, Some(coordinate_area)),
301 };
302 Ok(LandXmlParcelProbe {
303 state: LandXmlParcelState::Analytic,
304 perimeter_in_declared_linear_units: Some(perimeter),
305 area_in_declared_square_units,
306 declared_area: parcel.declared_area,
307 declared_perimeter: parcel.declared_perimeter,
308 perimeter_in_meters,
309 area_in_square_meters,
310 })
311}
312
313fn supports_analytic_curve_loop(geometry: &[LandXmlPlanGeometry]) -> bool {
319 let curve_count = geometry
320 .iter()
321 .filter(|item| item.kind == super::LandXmlGeometryKind::Curve)
322 .count();
323 curve_count == 0
324 || (geometry.len() == 2
325 && curve_count == 1
326 && geometry
327 .iter()
328 .any(|item| item.kind == super::LandXmlGeometryKind::Line))
329}
330
331fn preserved(parcel: &LandXmlParcel, reason: &str) -> LandXmlParcelProbe {
332 LandXmlParcelProbe {
333 state: LandXmlParcelState::PreservedOnly {
334 reason: reason.to_owned(),
335 },
336 perimeter_in_declared_linear_units: None,
337 area_in_declared_square_units: None,
338 declared_area: parcel.declared_area,
339 declared_perimeter: parcel.declared_perimeter,
340 perimeter_in_meters: None,
341 area_in_square_meters: None,
342 }
343}