Skip to main content

topology_checker_core/
topology.rs

1use crate::engine::line_graph::{InputMode, LineGraph};
2use crate::{
3    Error, Layer, LayerId, Rule, RuleId, RuleSpec, Violation, ViolationKind, model::Parts, rules,
4};
5use rayon::prelude::*;
6use serde::Serialize;
7use std::{
8    collections::BTreeMap,
9    fmt,
10    sync::{Arc, Mutex, OnceLock},
11};
12type GraphSlot = Arc<OnceLock<Result<Arc<LineGraph>, Error>>>;
13
14/// One rule's violations, with deterministic counts and feature provenance.
15#[derive(Debug, Clone, Serialize)]
16#[non_exhaustive]
17pub struct RuleReport {
18    /// Registered rule with its topology-scoped rule and layer IDs.
19    pub spec: RuleSpec,
20    /// Primary input layer name.
21    pub layer: String,
22    /// Secondary input layer name for a binary rule.
23    pub other: Option<String>,
24    /// Number of aggregated violations per kind, in kind order.
25    pub counts: BTreeMap<ViolationKind, usize>,
26    /// Deterministically ordered violations, serialized as `errors`.
27    /// Several parts may contribute to one violation.
28    #[serde(rename = "errors")]
29    pub violations: Vec<Violation>,
30}
31impl RuleReport {
32    /// Return the name of one of this rule's input layers, as referenced by its violations.
33    #[must_use]
34    pub fn layer_name(&self, id: LayerId) -> Option<&str> {
35        if id == self.spec.layer {
36            Some(&self.layer)
37        } else if Some(id) == self.spec.other {
38            self.other.as_deref()
39        } else {
40            None
41        }
42    }
43}
44/// A non-fatal input condition found while preparing a registered layer.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46#[serde(tag = "kind", rename_all = "kebab-case")]
47#[non_exhaustive]
48pub enum Warning {
49    /// Degenerate parts or rings were skipped while cleaning a layer.
50    DegenerateParts {
51        /// Topology-scoped layer ID.
52        layer: LayerId,
53        /// Caller-supplied layer name, which need not be unique.
54        name: String,
55        /// Number of skipped parts and rings.
56        skipped: usize,
57    },
58}
59impl fmt::Display for Warning {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            Self::DegenerateParts { name, skipped, .. } => {
63                write!(
64                    f,
65                    "layer {name}: skipped {skipped} degenerate parts or rings"
66                )
67            }
68        }
69    }
70}
71/// A successful validation result, which may contain topology violations.
72#[derive(Debug, Clone, Serialize)]
73#[non_exhaustive]
74pub struct Report {
75    /// Planar tolerance in input coordinate units.
76    pub tolerance: f64,
77    /// Degenerate-part warnings in layer registration order.
78    pub warnings: Vec<Warning>,
79    /// Rule results in rule registration order, independent of parallel scheduling.
80    /// The result for `RuleId` `i` is at index `i`.
81    pub rules: Vec<RuleReport>,
82}
83impl Report {
84    /// Return whether every registered rule has no violations.
85    #[must_use]
86    pub fn is_valid(&self) -> bool {
87        self.rules.iter().all(|r| r.violations.is_empty())
88    }
89    /// Count aggregated violations across all registered rules.
90    #[must_use]
91    pub fn violation_count(&self) -> usize {
92        self.rules.iter().map(|r| r.violations.len()).sum()
93    }
94    /// Iterate violations in rule order and then each rule's deterministic order.
95    pub fn violations(&self) -> impl Iterator<Item = &Violation> {
96        self.rules.iter().flat_map(|r| &r.violations)
97    }
98    /// Return one rule's result. IDs must originate from the topology that
99    /// produced this report.
100    #[must_use]
101    pub fn rule(&self, id: RuleId) -> Option<&RuleReport> {
102        self.rules.get(id.0)
103    }
104}
105/// An existing layer identity or an owned layer to register atomically with a rule.
106#[derive(Debug)]
107pub enum LayerInput {
108    /// Reuse a layer registered in this topology.
109    Existing(LayerId),
110    /// Register a fresh owned snapshot.
111    New(Box<Layer>),
112}
113
114/// Shared planar layers and ordered rules with reusable, thread-safe geometry caches.
115#[derive(Debug)]
116pub struct Topology {
117    tolerance: f64,
118    layers: Vec<Arc<Layer>>,
119    specs: Vec<RuleSpec>,
120    graphs: Mutex<BTreeMap<Vec<(LayerId, InputMode)>, GraphSlot>>,
121}
122impl Topology {
123    /// Create an empty topology with tolerance in input coordinate units.
124    /// Zero requests exact comparisons; no CRS transformation is performed.
125    ///
126    /// # Errors
127    /// Returns [`Error::Tolerance`] for a negative or non-finite tolerance.
128    pub fn new(tolerance: f64) -> Result<Self, Error> {
129        if !tolerance.is_finite() || tolerance < 0. {
130            return Err(Error::Tolerance(tolerance));
131        }
132        Ok(Self {
133            tolerance,
134            layers: Vec::new(),
135            specs: Vec::new(),
136            graphs: Mutex::new(BTreeMap::new()),
137        })
138    }
139    /// Return the configured planar tolerance.
140    #[must_use]
141    pub fn tolerance(&self) -> f64 {
142        self.tolerance
143    }
144    /// Register a layer without validating it, returning a topology-scoped ID.
145    /// Pass an `Arc<Layer>` to share one layer, and its cached views, between topologies.
146    pub fn add_layer(&mut self, layer: impl Into<Arc<Layer>>) -> LayerId {
147        let id = LayerId(self.layers.len());
148        self.layers.push(layer.into());
149        id
150    }
151    /// Borrow a registered layer. IDs must originate from this topology.
152    ///
153    /// # Errors
154    /// Returns [`Error::UnknownLayer`] when the index is not registered.
155    pub fn layer(&self, id: LayerId) -> Result<&Layer, Error> {
156        self.layers
157            .get(id.0)
158            .map(AsRef::as_ref)
159            .ok_or(Error::UnknownLayer(id))
160    }
161    /// Iterate layers and their IDs in registration order.
162    pub fn layers(&self) -> impl Iterator<Item = (LayerId, &Layer)> {
163        self.layers
164            .iter()
165            .enumerate()
166            .map(|(i, l)| (LayerId(i), l.as_ref()))
167    }
168    /// Iterate registered rules in registration order.
169    pub fn rules(&self) -> impl ExactSizeIterator<Item = RuleSpec> {
170        self.specs.iter().copied()
171    }
172    /// Remove every registered rule, keeping layers and their cached line graphs, so a
173    /// different rule set can be validated without rebuilding them. Rule IDs restart at
174    /// zero; IDs from earlier registrations must not be reused.
175    pub fn clear_rules(&mut self) {
176        self.specs.clear();
177    }
178    pub(crate) fn parts(&self, id: LayerId) -> Result<&Parts, Error> {
179        let layer = self.layer(id)?;
180        layer.parts().map_err(|e| e.at(id, layer.name()))
181    }
182    pub(crate) fn graph(&self, layers: Vec<LayerId>) -> Result<Arc<LineGraph>, Error> {
183        self.graph_inputs(
184            layers
185                .into_iter()
186                .map(|id| (id, InputMode::Linework))
187                .collect(),
188        )
189    }
190    pub(crate) fn graph_inputs(
191        &self,
192        mut layers: Vec<(LayerId, InputMode)>,
193    ) -> Result<Arc<LineGraph>, Error> {
194        layers.sort_unstable();
195        layers.dedup();
196        // A poisoned map still contains valid Arc/OnceLock entries: initialization
197        // happens outside this lock, and no partially built graph is stored in it.
198        let slot = Arc::clone(
199            self.graphs
200                .lock()
201                .unwrap_or_else(std::sync::PoisonError::into_inner)
202                .entry(layers.clone())
203                .or_default(),
204        );
205        slot.get_or_init(|| LineGraph::build(self, &layers).map(Arc::new))
206            .as_ref()
207            .map(Arc::clone)
208            .map_err(Clone::clone)
209    }
210    /// Register a rule after validating input families, finite coordinates and FIDs.
211    /// Unary rules require no secondary layer; binary rules require a distinct one.
212    /// Rules run in parallel, but reports retain registration order.
213    /// The returned spec's `id` locates this rule's result with [`Report::rule`].
214    ///
215    /// # Errors
216    /// Returns input-count, same-layer, unknown-layer, geometry-kind, duplicate-FID
217    /// or non-finite-coordinate errors. Failed registration does not add a rule.
218    pub fn add_rule(
219        &mut self,
220        rule: Rule,
221        layer: LayerId,
222        other: Option<LayerId>,
223    ) -> Result<RuleSpec, Error> {
224        self.register_rule(
225            rule,
226            LayerInput::Existing(layer),
227            other.map(LayerInput::Existing),
228        )
229    }
230    /// Validate and commit owned inputs and a rule as one transaction.
231    /// Layer caches do not depend on registration IDs, so validation work done
232    /// here is reused after commit.
233    ///
234    /// # Errors
235    /// Returns arity, same-layer, unknown-layer, geometry-kind, duplicate-FID or
236    /// coordinate errors before committing any new layer or rule.
237    pub fn register_rule(
238        &mut self,
239        rule: Rule,
240        layer: LayerInput,
241        other: Option<LayerInput>,
242    ) -> Result<RuleSpec, Error> {
243        let (primary_kind, other_kind) = rule.inputs();
244        rule.check_input_count(other.is_some())?;
245        if let (LayerInput::Existing(a), Some(LayerInput::Existing(b))) = (&layer, &other)
246            && a == b
247        {
248            return Err(Error::SameLayer { rule, layer: *a });
249        }
250        let mut next = self.layers.len();
251        let mut check = |input: &LayerInput, kind| -> Result<LayerId, Error> {
252            let (id, value) = match input {
253                LayerInput::Existing(id) => (*id, self.layer(*id)?),
254                LayerInput::New(value) => {
255                    let id = LayerId(next);
256                    next += 1;
257                    (id, value.as_ref())
258                }
259            };
260            value.check_kind(id, kind)?;
261            value.parts().map_err(|e| e.at(id, value.name()))?;
262            Ok(id)
263        };
264        let primary_id = check(&layer, primary_kind)?;
265        let other_id = other
266            .as_ref()
267            .zip(other_kind)
268            .map(|(input, kind)| check(input, kind))
269            .transpose()?;
270        for input in [Some(layer), other].into_iter().flatten() {
271            if let LayerInput::New(value) = input {
272                self.layers.push(Arc::from(value));
273            }
274        }
275        let spec = RuleSpec {
276            id: RuleId(self.specs.len()),
277            rule,
278            layer: primary_id,
279            other: other_id,
280        };
281        self.specs.push(spec);
282        Ok(spec)
283    }
284    /// Validate cached part views and report skipped degenerate parts without running rules.
285    ///
286    /// # Errors
287    /// Returns duplicate-FID or non-finite-coordinate errors from any registered layer,
288    /// including layers that are not used by a rule.
289    pub fn warnings(&self) -> Result<Vec<Warning>, Error> {
290        let mut warnings = Vec::new();
291        for (id, layer) in self.layers() {
292            let skipped = self.parts(id)?.skipped;
293            if skipped > 0 {
294                warnings.push(Warning::DegenerateParts {
295                    layer: id,
296                    name: layer.name().into(),
297                    skipped,
298                });
299            }
300        }
301        Ok(warnings)
302    }
303    /// Validate all layers and execute registered rules, reusing cached views.
304    /// Topology violations are a successful report, not operational errors.
305    ///
306    /// # Errors
307    /// Returns invalid-layer errors from deferred validation or operational errors
308    /// encountered while building rule inputs. No partial report is returned.
309    pub fn validate(&self) -> Result<Report, Error> {
310        let warnings = self.warnings()?;
311        let rules = self
312            .specs
313            .par_iter()
314            .map(|&spec| {
315                let violations = rules::run(self, spec)?;
316                let mut counts = BTreeMap::new();
317                for v in &violations {
318                    *counts.entry(v.kind).or_default() += 1;
319                }
320                Ok(RuleReport {
321                    spec,
322                    layer: self.layer(spec.layer)?.name().into(),
323                    other: spec
324                        .other
325                        .map(|id| self.layer(id).map(|l| l.name().to_string()))
326                        .transpose()?,
327                    counts,
328                    violations,
329                })
330            })
331            .collect::<Result<Vec<_>, Error>>()?;
332        Ok(Report {
333            tolerance: self.tolerance,
334            warnings,
335            rules,
336        })
337    }
338}
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343    use geo::{LineString, Point};
344
345    #[test]
346    fn transactions_reject_invalid_layers_without_consuming_ids_or_rules() {
347        for bad in [
348            Layer::from_features("duplicates", vec![(4, Point::new(0., 0.).into()); 2]),
349            Layer::from_geometries("nonfinite", [Point::new(f64::INFINITY, 0.).into()]),
350            Layer::from_geometries(
351                "wrong kind",
352                [LineString::from(vec![(0., 0.), (1., 0.)]).into()],
353            ),
354        ] {
355            let mut topology = Topology::new(0.).unwrap();
356            let original = topology.add_layer(Layer::from_geometries("original", []));
357            let new = LayerInput::New(Box::new(Layer::from_geometries(
358                "pending",
359                [Point::new(0., 0.).into()],
360            )));
361            assert!(
362                topology
363                    .register_rule(
364                        Rule::PointMustNotOverlapWith,
365                        new,
366                        Some(LayerInput::New(Box::new(bad))),
367                    )
368                    .is_err()
369            );
370            assert_eq!(topology.layers().count(), 1);
371            assert!(topology.validate().unwrap().rules.is_empty());
372            assert!(topology.warnings().unwrap().is_empty());
373            let spec = topology
374                .register_rule(
375                    Rule::PointMustNotOverlapWith,
376                    LayerInput::Existing(original),
377                    Some(LayerInput::New(Box::new(Layer::from_geometries(
378                        "next",
379                        [],
380                    )))),
381                )
382                .unwrap();
383            assert_eq!(spec.other.unwrap().index(), 1);
384        }
385    }
386
387    #[test]
388    fn transaction_caches_keep_final_ids_for_each_input_order() {
389        for reuse_primary in [false, true] {
390            let mut topology = Topology::new(0.).unwrap();
391            topology.add_layer(Layer::from_geometries("unused", []));
392            let existing = topology.add_layer(Layer::from_geometries(
393                "existing",
394                [Point::new(0., 0.).into()],
395            ));
396            let fresh = LayerInput::New(Box::new(Layer::from_geometries(
397                "fresh",
398                [Point::new(0., 0.).into()],
399            )));
400            let (primary, other) = if reuse_primary {
401                (LayerInput::Existing(existing), fresh)
402            } else {
403                (fresh, LayerInput::Existing(existing))
404            };
405            let spec = topology
406                .register_rule(Rule::PointMustNotOverlapWith, primary, Some(other))
407                .unwrap();
408            let report = topology.validate().unwrap();
409            let error = &report.rules[0].violations[0];
410            assert_eq!(error.origin.unwrap().layer, spec.layer);
411            assert_eq!(error.destination.unwrap().layer, spec.other.unwrap());
412            assert_eq!(topology.layers().count(), 3);
413        }
414    }
415}