1use crate::{GeometryKind, LayerId};
2use serde::{Deserialize, Serialize};
3use std::{fmt, str::FromStr};
4
5macro_rules! registry {
6 ($(($variant:ident, $name:literal, $a:ident, $b:expr, $description:literal, $summary:literal, $arcgis:expr, $qgis:expr)),* $(,)?) => {
7 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
9 #[cfg_attr(feature = "ts", derive(ts_rs::TS))]
10 #[non_exhaustive]
11 pub enum Rule { $( #[doc = $description] #[serde(rename = $name)] $variant, )* }
12 impl Rule {
13 pub const ALL: &'static [Self] = &[$(Self::$variant),*];
15 #[must_use]
17 pub const fn name(self) -> &'static str { match self { $(Self::$variant => $name),* } }
18 #[must_use]
20 pub const fn inputs(self) -> (GeometryKind, Option<GeometryKind>) { match self { $(Self::$variant => (GeometryKind::$a, $b)),* } }
21 #[must_use]
23 pub const fn description(self) -> &'static str { match self { $(Self::$variant => $description),* } }
24 #[must_use]
26 pub const fn summary(self) -> &'static str { match self { $(Self::$variant => $summary),* } }
27 #[must_use]
29 pub const fn arcgis(self) -> Option<&'static str> { match self { $(Self::$variant => $arcgis),* } }
30 #[must_use]
32 pub const fn qgis(self) -> Option<&'static str> { match self { $(Self::$variant => $qgis),* } }
33 }
34 }
35}
36use GeometryKind::{Any, Line, Point, Polygon};
37registry! {
38 (PointMustNotOverlap, "point/must-not-overlap", Point, None, "Report each pair of coincident points within tolerance once.", "Find duplicate points within the distance tolerance.", Some("Must Be Disjoint"), Some("Must not have duplicates")),
39 (PointMustNotOverlapWith, "point/must-not-overlap-with", Point, Some(Point), "Report coincident point pairs across two layers.", "Find duplicate points across two layers within the distance tolerance.", None, None),
40 (PointMustBeInside, "point/must-be-inside", Point, Some(Polygon), "Report points not strictly inside any polygon; boundary distances at or below tolerance are outside.", "Find points outside the polygons, including points on or within the distance tolerance of their edges.", Some("Must Be Properly Inside Polygons"), Some("Must be inside")),
41 (LineMustNotHaveDangles, "line/must-not-have-dangles", Line, None, "Report part endpoints with one incident use after noding and clustering.", "Find unconnected line endpoints.", Some("Must Not Have Dangles"), Some("Must not have dangles")),
42 (LineMustNotHavePseudoNodes, "line/must-not-have-pseudo-nodes", Line, None, "Report nodes with exactly two endpoint uses from distinct features. Closed loops are valid.", "Find joins connecting exactly two distinct features at their endpoints. Closed loops are allowed.", Some("Must Not Have Pseudo Nodes"), Some("Must not have pseudo nodes")),
43 (LineMustNotOverlap, "line/must-not-overlap", Line, None, "Report shared linework between distinct features, once per feature pair. Self-overlaps are excluded.", "Find shared line segments between distinct features. Crossings and overlaps within one feature are excluded.", Some("Must Not Overlap"), None),
44 (LineMustNotOverlapWith, "line/must-not-overlap-with", Line, Some(Line), "Report shared linework across two layers, once per feature pair.", "Find shared line segments across two layers. Crossings are excluded.", Some("Must Not Overlap With"), None),
45 (LineMustNotSelfOverlap, "line/must-not-self-overlap", Line, None, "Report linework traversed repeatedly by one feature, including its multiple parts.", "Find line segments traced more than once by the same feature, including across its parts.", Some("Must Not Self-Overlap"), None),
46 (LineMustNotIntersect, "line/must-not-intersect", Line, None, "Report overlaps and interior-to-interior intersections between distinct features. Endpoint touches and self violations are excluded.", "Find overlaps and crossings between distinct features away from their endpoints. Endpoint touches are allowed.", Some("Must Not Intersect"), None),
47 (LineMustNotIntersectWith, "line/must-not-intersect-with", Line, Some(Line), "Report overlaps and interior-to-interior intersections across two layers.", "Find overlaps and crossings across two layers away from their endpoints. Endpoint touches are allowed.", Some("Must Not Intersect With"), None),
48 (LineMustNotIntersectOrTouchInterior, "line/must-not-intersect-or-touch-interior", Line, None, "Report overlaps and meetings between distinct features where at least one feature is interior.", "Find overlaps, crossings and endpoint touches on the interior of another feature. Touches only at both features' endpoints are allowed.", Some("Must Not Intersect Or Touch Interior"), None),
49 (LineMustNotIntersectOrTouchInteriorWith, "line/must-not-intersect-or-touch-interior-with", Line, Some(Line), "Report overlaps and meetings across two layers where at least one feature is interior.", "Find overlaps, crossings and endpoint touches on a line's interior across two layers. Touches only at both features' endpoints are allowed.", Some("Must Not Intersect Or Touch Interior With"), None),
50 (LineMustNotSelfIntersect, "line/must-not-self-intersect", Line, None, "Report self-overlaps and true self-crossings. Closed loops and self-touches are valid.", "Find overlaps and crossings within one feature. Closed loops and self-touches are allowed.", Some("Must Not Self-Intersect"), None),
51 (LineMustBeInside, "line/must-be-inside", Line, Some(Polygon), "Report pieces outside the polygon union longer than tolerance. Polygon boundaries are covered.", "Find line pieces outside the polygons that are longer than the distance tolerance. Lines on polygon edges are allowed.", Some("Must Be Inside"), None),
52 (PolygonMustNotOverlap, "polygon/must-not-overlap", Polygon, None, "Report polygon intersection areas once per distinct feature pair, excluding slivers below tolerance.", "Find shared areas between distinct polygon features, ignoring slivers below the tolerance.", Some("Must Not Overlap"), Some("Must not overlap")),
53 (PolygonMustNotOverlapWith, "polygon/must-not-overlap-with", Polygon, Some(Polygon), "Report polygon intersection areas across two layers, excluding slivers below tolerance.", "Find shared polygon areas across two layers, ignoring slivers below the tolerance.", Some("Must Not Overlap With"), Some("Must not overlap with")),
54 (PolygonMustNotHaveGaps, "polygon/must-not-have-gaps", Polygon, None, "Report union interior rings as gaps and exterior rings as outer-boundary. Ignore sliver holes below tolerance.", "Find enclosed gaps between polygons, ignoring sliver gaps below the tolerance. Also report outer boundaries separately.", Some("Must Not Have Gaps"), Some("Must not have gaps")),
55 (GeometryMustNotBeMultipart, "geometry/must-not-be-multipart", Any, None, "Report features containing more than one part. A single-part Multi geometry is valid.", "Find features with more than one part. A Multi geometry with only one part is allowed.", Some("Must Be Single Part (lines)"), Some("Must not have multi-part geometries")),
56 (PointMustBeCoveredByLines, "point/must-be-covered-by-lines", Point, Some(Line), "Every point must be within tolerance of linework, including endpoints.", "Find points without covering linework, including line endpoints.", Some("Must Be Covered By (Point-Line)"), Some("Must be covered by")),
57 (PointMustBeCoveredByLineEndpoints, "point/must-be-covered-by-line-endpoints", Point, Some(Line), "Every point must be within tolerance of an open line part endpoint. Closed parts have no endpoints.", "Find points without a covering open line endpoint. Closed line parts have no endpoints.", Some("Must Be Covered By Endpoint Of"), Some("Must be covered by endpoints of")),
58 (PointMustBeCoveredByPolygonBoundaries, "point/must-be-covered-by-polygon-boundaries", Point, Some(Polygon), "Every point must be within tolerance of an individual polygon exterior or hole boundary, including shared edges.", "Find points away from polygon edges, including holes and shared parcel edges.", Some("Must Be Covered By Boundary Of (Point-Area)"), None),
59 (LineEndpointsMustBeCoveredByPoints, "line/endpoints-must-be-covered-by-points", Line, Some(Point), "Every open line part endpoint must be within tolerance of a point. Closed parts have no endpoints.", "Find open line endpoints without a covering point. Closed line parts have no endpoints.", Some("Endpoint Must Be Covered By"), Some("End points must be covered by")),
60 (LineMustBeCoveredByPolygonBoundaries, "line/must-be-covered-by-polygon-boundaries", Line, Some(Polygon), "Report line pieces not covered by individual polygon boundaries, longer than tolerance. Includes hole and shared boundaries.", "Find line pieces that do not follow polygon edges, including holes and shared edges.", Some("Must Be Covered By Boundary Of (Line-Area)"), None),
61 (PolygonBoundariesMustBeCoveredByLines, "polygon/boundaries-must-be-covered-by-lines", Polygon, Some(Line), "Report exterior and hole boundary pieces without covering linework, longer than tolerance.", "Find polygon edge pieces without covering linework, including hole edges.", Some("Boundary Must Be Covered By (Area-Line)"), None),
62 (PolygonMustContainPoint, "polygon/must-contain-point", Polygon, Some(Point), "Every polygon feature must strictly contain all retained parts of at least one point feature. Count distinct FIDs.", "Find polygons without a strictly interior point feature. All parts of a point feature must qualify.", Some("Contains Point"), Some("Must contain")),
63 (PolygonMustContainOnePoint, "polygon/must-contain-one-point", Polygon, Some(Point), "Every polygon feature must strictly contain exactly one point feature, and every point feature must belong to exactly one polygon feature. Count distinct FIDs.", "Find polygons and point features without a one-to-one interior assignment. Count feature IDs, including multipart features.", Some("Contains One Point"), None),
64 (LineMustNotCrossPolygonBoundaries, "line/must-not-cross-polygon-boundaries", Line, Some(Polygon), "Report entry and exit crossings of individual exterior and hole boundaries. Tangencies, endpoint contacts and shared segments are allowed.", "Find lines entering or leaving polygons through exterior or hole edges. Tangencies, endpoint touches and shared segments are allowed.", None, None),
65 (GeometryMustNotBeContainedBy, "geometry/must-not-be-contained-by", Any, Some(Any), "Report whole cleaned features topologically contained by individual target features, excluding mutual containment. No tolerance buffer is used.", "Find whole features contained by an individual feature in the other layer. Equal geometries are allowed. No tolerance buffer is used.", None, Some("Check Geometry (Contained)")),
66 (PointMustBeCoveredByPolygons, "point/must-be-covered-by-polygons", Point, Some(Polygon), "Every point must be inside polygon areas or within tolerance of their boundaries.", "Find points outside polygon areas and farther than the tolerance from their edges. Boundary points are allowed.", None, None),
67 (PointMustNotBeInsidePolygons, "point/must-not-be-inside-polygons", Point, Some(Polygon), "Report strictly interior points. Points within tolerance of polygon boundaries are allowed.", "Find points strictly inside polygons. Points on or within tolerance of their edges are allowed.", None, None),
68 (PointMustNotIntersectLines, "point/must-not-intersect-lines", Point, Some(Line), "Report point contacts within tolerance anywhere on linework, including endpoints.", "Find points touching linework anywhere, including endpoints, within tolerance.", None, None),
69 (PointMustNotIntersectPolygons, "point/must-not-intersect-polygons", Point, Some(Polygon), "Report points inside polygon areas or within tolerance of their boundaries.", "Find points inside polygons or touching their edges within tolerance.", None, None),
70 (LineMustBeProperlyInsidePolygons, "line/must-be-properly-inside-polygons", Line, Some(Polygon), "Every cleaned line part must be inside the polygon union and farther than tolerance from its boundary. Report the whole failing part.", "Find whole line parts that leave polygon coverage or come within tolerance of its boundary.", None, None),
71 (LineMustNotOverlapWithPolygons, "line/must-not-overlap-with-polygons", Line, Some(Polygon), "Report line pieces inside individual polygon areas or along their boundaries, longer than tolerance. Isolated contacts are allowed.", "Find line pieces inside polygons or along their edges, longer than tolerance. Isolated contacts are allowed.", None, None),
72 (LineMustNotOverlapPolygonBoundaries, "line/must-not-overlap-polygon-boundaries", Line, Some(Polygon), "Report shared individual polygon boundary segments longer than tolerance. Crossings and isolated contacts are allowed.", "Find line pieces shared with polygon edges, longer than tolerance. Crossings and isolated contacts are allowed.", None, None),
73 (LineMustNotIntersectPolygons, "line/must-not-intersect-polygons", Line, Some(Polygon), "Report interior pieces, shared boundary pieces and isolated contacts with individual polygons. Boundary proximity within tolerance counts as contact.", "Find any line contact with polygons, including interior pieces, shared edges and isolated touches within tolerance.", None, None),
74 (LineEndpointsMustBeCoveredByPolygonBoundaries, "line/endpoints-must-be-covered-by-polygon-boundaries", Line, Some(Polygon), "Every open line part endpoint must be within tolerance of an exterior or hole boundary. Closed parts have no endpoints.", "Find open line endpoints away from polygon exterior or hole edges. Closed line parts have no endpoints.", None, None),
75 (PolygonMustNotContainPoints, "polygon/must-not-contain-points", Polygon, Some(Point), "Report strictly contained point parts with polygon origin provenance. Points within tolerance of boundaries are allowed.", "Find strictly contained points, with the polygon as the primary feature. Boundary points are allowed.", None, None),
76}
77impl Rule {
78 pub fn check_input_count(self, other: bool) -> Result<(), crate::Error> {
85 let binary = self.inputs().1.is_some();
86 if other == binary {
87 Ok(())
88 } else {
89 Err(crate::Error::Inputs {
90 rule: self,
91 expected: if binary { 2 } else { 1 },
92 })
93 }
94 }
95}
96impl fmt::Display for Rule {
97 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.write_str(self.name())
99 }
100}
101impl FromStr for Rule {
102 type Err = ParseRuleError;
103 fn from_str(value: &str) -> Result<Self, Self::Err> {
108 Self::ALL
109 .iter()
110 .copied()
111 .find(|r| r.name() == value)
112 .ok_or_else(|| ParseRuleError { name: value.into() })
113 }
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
118#[error("unknown rule {name}")]
119pub struct ParseRuleError {
120 name: String,
121}
122impl ParseRuleError {
123 #[must_use]
125 pub fn name(&self) -> &str {
126 &self.name
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
133pub struct RuleId(pub(crate) usize);
134impl RuleId {
135 #[must_use]
137 pub fn index(self) -> usize {
138 self.0
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
144#[non_exhaustive]
145pub struct RuleSpec {
146 #[serde(skip)]
148 pub id: RuleId,
149 pub rule: Rule,
151 pub layer: LayerId,
153 pub other: Option<LayerId>,
155}