Skip to main content

geo_repair/
lib.rs

1//! Validate and repair invalid OGC GIS geometries in Rust.
2//!
3//! Detects and fixes geometry defects — self-intersections, unclosed rings,
4//! degenerate shapes, NaN coordinates, and more — using algorithms selected
5//! by geometry type. The **Structure** strategy (default) mirrors GEOS's
6//! ST_MakeValid algorithm; the **Arrange** strategy uses CDT-based repair
7//! as a robust fallback for complex topologies. Passes 2490/2490 GEOS XML
8//! validation tests, with parallel batch performance **0.30× GEOS** (3.3×
9//! faster) on 1.58M data set polygons.
10//!
11//! # Quick start
12//!
13//! ```toml
14//! [dependencies]
15//! geo-repair = "0.12"
16//! ```
17//!
18//! ```rust
19//! # use geo::{Geometry, Point};
20//! # let geometry = Geometry::Point(Point::new(0.0, 0.0));
21//! use geo_repair::{is_valid, validate, MakeValid, ValidateAndFix};
22//! use geo_repair::{read_wkb, write_wkb, read_wkt, write_wkt};
23//!
24//! // Check validity
25//! let result = validate(&geometry);
26//! if !result.valid {
27//!     for err in &result.errors {
28//!         eprintln!("  {err}");
29//!     }
30//! }
31//!
32//! // Fix invalid geometry
33//! let fixed = geometry.make_valid();
34//!
35//! // Combined validate-and-fix
36//! let (result, fixed) = geometry.validate_and_fix();
37//!
38//! // WKB roundtrip
39//! let bytes: Vec<u8> = write_wkb(&geometry);
40//! let geom = read_wkb(&bytes).unwrap();
41//!
42//! // WKT roundtrip
43//! let text: String = write_wkt(&geometry);
44//! let geom = read_wkt(&text).unwrap();
45//! ```
46//!
47//! ## With method selection
48//!
49//! ```rust
50//! # use geo::{Geometry, Point};
51//! # let geometry = Geometry::Point(Point::new(0.0, 0.0));
52//! use geo_repair::{MakeValid, MakeValidConfig, PolyMethod};
53//!
54//! let config = MakeValidConfig {
55//!     poly_method: PolyMethod::Arrange,
56//!     keep_collapsed: false,
57//!     ..Default::default()
58//! };
59//! let fixed = geometry.make_valid_with_config(&config);
60//! ```
61//!
62//! ## WKB I/O (built-in, no dependencies)
63//!
64//! ```rust
65//! # use geo::{Geometry, Point};
66//! # let geom = Geometry::Point(Point::new(0.0, 0.0));
67//! # let concat_buffer = vec![];
68//! use geo_repair::{read_wkb, write_wkb, read_wkb_concat};
69//!
70//! let wkb: Vec<u8> = write_wkb(&geom);
71//! let geom = read_wkb(&wkb).unwrap();
72//! let geoms: Vec<Geometry<f64>> = read_wkb_concat(&concat_buffer).unwrap();
73//! ```
74//!
75//! ## WKT I/O (built-in, no dependencies)
76//!
77//! ```rust
78//! # use geo::{Geometry, Point};
79//! # let geom = Geometry::Point(Point::new(0.0, 0.0));
80//! use geo_repair::{read_wkt, write_wkt};
81//!
82//! let wkt: String = write_wkt(&geom);
83//! let geom = read_wkt(&wkt).unwrap();
84//! ```
85//!
86//! ## Binary format loading (custom `.bin` format, fast bulk I/O)
87//!
88//! ```rust,no_run
89//! use geo_repair::load_bin;
90//!
91//! let polys: Vec<geo::Polygon<f64>> = load_bin("dataset.bin").unwrap();
92//! ```
93//!
94//! # Feature flags
95//!
96//! | Feature | Description | Default |
97//! |---------|-------------|---------|
98//! | `std` | Standard library + file I/O. Disable for no_std builds. | yes |
99//! | `arrange` | CDT-based polygon repair (requires `spade`) | yes |
100//! | `structure` | Structure-based fast-path repair | yes |
101//! | `parallel` | Rayon parallel processing (non-WASM) | yes |
102//! | `simd` | AVX2-accelerated orientation tests (x86_64) | yes |
103//! | `simd-portable` | Portable SIMD via `core::simd` (nightly) | no |
104//! | `validate` | OGC validation predicates | yes |
105//! | `memmap` | Memory-mapped binary file loading | no* |
106//! | `wasm` | WASM browser fetch (synchronous XHR) | no |
107//! | `mimalloc` | Use mimalloc global allocator | yes |
108//! | `io-shp` | Shapefile format backend | no |
109//! | `io-wkb` | No-op (WKB is always compiled in) | — |
110//! | `io-wkt` | No-op (WKT is always compiled in) | — |
111//! | `io-csv` | CSV format backend | no |
112//! | `io-gml` | GML/XML format backend | no |
113//! | `io-gpkg` | GeoPackage format backend (not WASM) | no |
114//! | `io-all` | All opt-in backends except gpkg | no |
115//! | `io-all-native` | All opt-in backends including gpkg | no |
116//! | `ffi` | C-compatible FFI bindings | no |
117//! | `python` | Python bindings via PyO3 | no |
118//! | `proj` | CRS transformation via PROJ | no |
119//! | `serde` | Geometry serde support | no |
120//! | `bench-geos` | GEOS comparison benchmarks (static — MSVC, no LTO) | no |
121//! | `bench-geos-system` | GEOS comparison benchmarks (system — conda LLVM, full LTO) | no |
122//!
123//! *`memmap` was default in 0.10 but moved to opt-in in 0.11.
124//!
125//! # Platform support
126//!
127//! | Platform | Core | SIMD | I/O | Parallel | Python |
128//! |----------|------|------|-----|----------|--------|
129//! | x86_64 Windows/Linux/macOS | Yes | Yes (AVX2) | Yes | Yes | Yes |
130//! | aarch64 macOS/Linux | Yes | Scalar | Yes | Yes | Yes |
131//! | WASM32 | Yes | Scalar | In-memory only | No | No |
132//! | no_std (embedded) | Yes | Scalar | No | No | No |
133//!
134//! AVX2 requires `RUSTFLAGS="-C target-cpu=native"` at build time. Falls
135//! back to scalar on CPUs without AVX2 or non-x86_64 targets.
136//!
137//! # no_std
138//!
139//! Disable the `std` feature for no_std builds. Core validation, repair,
140//! and WKB parsing work without std. File I/O, parallel processing, and
141//! Python/FFI bindings require std:
142//!
143//! ```shell
144//! cargo check --no-default-features --features arrange,structure,simd
145//! ```
146//!
147//! # Validation
148//!
149//! The [`GeoValidation`] trait checks 18 OGC validity rules using Shewchuk
150//! adaptive-precision orientation tests (via the `robust` crate):
151//!
152//! | Rule | Applies to |
153//! |------|-----------|
154//! | Coordinate finiteness | All geometries |
155//! | Ring closure | Polygon rings |
156//! | Ring minimum vertices (≥4) | Polygon rings |
157//! | Ring self-intersection | Polygon rings |
158//! | Pinch points (non-consecutive duplicates) | Rings |
159//! | Hole containment (inside shell) | Polygon |
160//! | No nested holes | Polygon |
161//! | Interior ring connectivity | Polygon |
162//! | Ring orientation (exterior CCW, interior CW) | Polygon |
163//! | Non-collinear rings | Polygon |
164//! | Consecutive duplicates | Lines/rings |
165//! | Duplicate rings | Polygon |
166//! | Duplicate points | MultiPoint |
167//! | Duplicate lines | MultiLineString |
168//! | Non-zero-length lines | Line |
169//! | Non-degenerate exterior | Polygon |
170//! | Simplicity (no interior intersections) | LineString, MultiLineString |
171//! | Nesting depth limit | GeometryCollection |
172//!
173//! ```rust
174//! # use geo::{Geometry, Point};
175//! # let geom = Geometry::Point(Point::new(0.0, 0.0));
176//! use geo_repair::{is_valid, validate, validate_reason, GeoValidation, ValidationResult};
177//!
178//! let ok: bool = geom.is_valid();
179//! let ok2: bool = is_valid(&geom);
180//!
181//! let result: ValidationResult = validate(&geom);
182//! let reason: String = validate_reason(&geom);
183//! ```
184//!
185//! # Polygon repair strategies
186//!
187//! | Strategy | Approach | Strengths | Weaknesses |
188//! |----------|----------|-----------|------------|
189//! | **Arrange** (CDT) | Constrained Delaunay triangulation → face labeling → ring extraction | Handles any topology. No self-intersection limit. | Slower, especially on large rings. Requires `spade`. |
190//! | **Structure** (fast path) | Planar graph extraction → face walking → winding-number assembly | 10-100× faster for valid/simple inputs. No external deps. | Falls back on complex topologies (many holes, nested self-intersections). |
191//!
192//! **Auto** (default) tries Structure first and falls back to Arrange.
193//! The repair pipeline enforces OGC-correct winding order (CCW exterior,
194//! CW interior) on all output.
195//!
196//! # CRS support
197//!
198//! The [`Crs`] type stores EPSG codes and provides CRS-aware tolerance heuristics:
199//! - Geographic (lon/lat): `1e-10` degrees
200//! - Projected (metres): `1e-6` metres
201//! - Unknown: `1e-12`
202//!
203//! CRS is set directly on [`MakeValidConfig`]:
204//!
205//! ```rust
206//! use geo_repair::{Crs, MakeValidConfig};
207//!
208//! let config = MakeValidConfig {
209//!     crs: Some(Crs::from_epsg(4326)),
210//!     ..Default::default()
211//! };
212//! ```
213//!
214//! # I/O
215//!
216//! GeoRepair provides format-agnostic dispatch and individual backends:
217//!
218//! ```rust,no_run
219//! # use geo::{Geometry, Point};
220//! # let geom = Geometry::Point(Point::new(0.0, 0.0));
221//! # let concat_buffer = vec![];
222//! use geo_repair::{
223//!     diagnose_file, load, load_bin, read_wkb, read_wkb_concat, read_wkb_from,
224//!     read_ewkb, write_ewkb, EwkbGeometry, EwkbDims,
225//!     read_wkt, read_wkt_from, write_wkt, write_wkt_to, infer_wkt_type,
226//!     write_wkb, write_wkb_to, write_wkb_with_opts, Endianness, WriteOptions,
227//!     repair_file, save, MakeValidConfig,
228//! };
229//!
230//! let geoms = load("input.wkb").unwrap();
231//! let geoms = read_wkb_concat(&concat_buffer).unwrap();
232//!
233//! for result in diagnose_file("input.bin").unwrap() {
234//!     println!("{}", result.reason());
235//! }
236//!
237//! repair_file("invalid.wkb", "fixed.wkb", &MakeValidConfig::default()).unwrap();
238//! save("output.wkt", &geoms[0]).unwrap();
239//!
240//! // Standard LE WKB
241//! let wkb: Vec<u8> = write_wkb(&geom);
242//! // Big-endian WKB
243//! let be_wkb: Vec<u8> = write_wkb_with_opts(&geom, &WriteOptions { endianness: Endianness::BigEndian });
244//! // Write to any io::Write target
245//! write_wkb_to(&geom, &mut std::io::stdout()).unwrap();
246//! // Read from any io::Read source
247//! let geom = read_wkb_from(&wkb[..]).unwrap();
248//!
249//! // EWKB with SRID and Z/M preservation
250//! let ewkb = EwkbGeometry {
251//!     geometry: geom.clone(),
252//!     srid: Some(4326),
253//!     dims: EwkbDims::XYZ,
254//!     extra_coords: vec![100.0],
255//! };
256//! let ewkb_bytes = write_ewkb(&ewkb);
257//! let back = read_ewkb(&ewkb_bytes).unwrap();
258//!
259//! // WKT with streaming I/O
260//! let wkt: String = write_wkt(&geom);
261//! let geom = read_wkt(&wkt).unwrap();
262//! write_wkt_to(&geom, &mut std::io::stdout()).unwrap();
263//! let geom = read_wkt_from(wkt.as_bytes()).unwrap();
264//!
265//! // Peek at WKT type without parsing
266//! let (type_name, _dims) = infer_wkt_type("POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0))").unwrap();
267//!
268//! let polys = load_bin("dataset.bin").unwrap();
269//! ```
270//!
271//! | Extension | Format | Backend |
272//! |-----------|--------|---------|
273//! | `.wkb` / `.wks` | WKB (LE/BE, EWKB SRID, Z/M variants, io::Read/Write) | Zero-dep built-in |
274//! | `.bin` | Custom binary bulk polygon format | Zero-dep built-in |
275//! | `.shp` | Shapefile | `io-shp` feature |
276//! | `.wkt` | WKT (io::Read/Write, type inference) | Zero-dep built-in |
277//! | `.csv` | CSV with WKT geometry | `io-csv` feature |
278//! | `.gml` | GML/XML | `io-gml` feature |
279//! | `.gpkg` | GeoPackage (SQLite) | `io-gpkg` feature |
280//!
281//! # Geometry type coverage
282//!
283//! | Geometry | Repair approach |
284//! |----------|----------------|
285//! | `Polygon` / `MultiPolygon` | Structure fast path or Arrange CDT fallback |
286//! | `LineString` / `MultiLineString` | NaN filtering, duplicate removal, self-intersection noding |
287//! | `Line` | Zero-length and NaN detection |
288//! | `Point` / `MultiPoint` | NaN/Inf filtering, deduplication |
289//! | `Rect` / `Triangle` | Basic degeneracy checks |
290//! | `GeometryCollection` | Recursive repair of children |
291//!
292//! # Known limitations
293//!
294//! - **CDT arranger may panic** on certain degenerate inputs (all-collinear
295//!   exterior rings, coordinates near f64::MAX). This is a known limitation
296//!   of `spade`.
297//! - **OGC compliance** is a key goal but not yet formally certified. The
298//!   validation module checks 18 OGC predicates and passes 2490/2490 GEOS XML
299//!   tests.
300//! - **GeometryCollection cross-component intersection** is not validated.
301//! - **Z/M coordinate consistency** is not validated.
302//!
303//! # License
304//!
305//! Apache-2.0
306#![cfg_attr(feature = "simd-portable", feature(portable_simd))]
307
308
309/// Compile-time guard: ensures `cfg(feature = "rstar")` is active when any
310/// rstar-dependent feature is enabled. Prevents silent O(n²) regression when
311/// `dep:rstar` is used in Cargo.toml feature lists instead of the explicit
312/// `rstar = ["dep:rstar"]` feature alias.
313#[cfg(any(feature = "arrange", feature = "structure", feature = "validate"))]
314const _: () = {
315    #[cfg(not(feature = "rstar"))]
316    compile_error!(
317        "cfg(feature = \"rstar\") must be set when rstar-dependent features are active.\n  \
318         Fix: replace `dep:rstar` with `rstar` in Cargo.toml feature lists. The pattern is:\n  \
319         rstar = [\"dep:rstar\"]  # explicit feature alias\n  \
320         foo = [\"rstar\"]          # not foo = [\"dep:rstar\"]"
321    );
322};
323
324/// Core configuration types for geometry repair.
325pub mod core;
326/// Coordinate reference system (CRS) handling and transformation.
327pub mod crs;
328/// Double-double arithmetic for robust geometric computations.
329pub mod dd;
330/// Feature metadata associated with geometries.
331pub mod feature;
332/// Geometry I/O: WKB, binary format, and format-dispatch helpers.
333pub mod io;
334/// Geometry repair implementation via the [`MakeValid`] trait.
335pub mod make_valid;
336/// Ring orientation utilities (CW/CCW winding).
337pub mod orient;
338/// Coordinate snapping to a precision grid.
339pub mod snap;
340pub(crate) mod util;
341/// OGC Simple Features geometry validation predicates.
342pub mod validation;
343/// Z/M coordinate value preservation through the repair pipeline.
344pub mod zm;
345
346#[cfg(feature = "arrange")]
347/// CDT-based polygon repair for complex topologies (Arrange strategy).
348pub mod arrange;
349/// Segment noding: intersection detection, snap-rounding, and validation.
350pub mod noding;
351/// Geometry precision reduction with topology preservation.
352pub mod reduce;
353#[cfg(feature = "structure")]
354/// GEOS-compatible fast-path polygon repair via planar graph extraction.
355pub mod structure;
356
357#[cfg(feature = "ffi")]
358#[path = "bindings/ffi.rs"]
359/// C-compatible FFI bindings for geo-repair.
360pub mod ffi;
361#[cfg(all(feature = "parallel", not(target_arch = "wasm32")))]
362/// Rayon-based parallel batch geometry repair.
363pub mod parallel;
364#[cfg(feature = "python")]
365#[path = "bindings/python.rs"]
366/// Python bindings via PyO3.
367pub mod python;
368#[cfg(feature = "simd")]
369/// AVX2-accelerated geometric predicates (x86_64).
370pub mod simd;
371
372/// Repair configuration, error types, and polygon method selection.
373pub use core::{MakeValidConfig, MakeValidError, PolyMethod};
374/// Coordinate reference system wrapper.
375pub use crs::Crs;
376/// A feature combining geometry with optional attributes and CRS.
377pub use feature::Feature;
378pub use io::{
379    diagnose_file, infer_wkt_type, load, load_bin, load_bin_stream, read_ewkb, read_wkb,
380    read_wkb_concat, read_wkb_from, read_wkt, read_wkt_from, repair_file, save, write_ewkb,
381    write_wkb, write_wkb_to, write_wkb_with_opts, write_wkt, write_wkt_to, Endianness, EwkbDims,
382    EwkbGeometry, WkbError, WktError, WriteOptions,
383};
384/// Trait for repairing invalid geometries.
385pub use make_valid::MakeValid;
386#[cfg(any(feature = "arrange", feature = "structure"))]
387/// Combines validation and repair in a single step, returning errors for
388/// violations that could not be automatically repaired.
389pub use make_valid::ValidateAndFix;
390/// Coordinate snapping functions.
391pub use snap::{snap_coord, snap_coord_default, snap_line, snap_lines, DEFAULT_GRID};
392/// OGC validation predicates and result types.
393pub use validation::{
394    is_valid, validate, validate_reason, GeoValidation, GeometryValidationError, ValidationResult,
395};
396
397#[cfg(feature = "mimalloc")]
398#[global_allocator]
399static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
400
401/// Profile each step of the structure fast path on a sample of polygons.
402/// Prints timing breakdown and pass rates to stderr.
403#[cfg(all(feature = "arrange", feature = "structure"))]
404pub fn profile_structure_fastpath(polys: &[geo::Polygon<f64>], sample: usize) {
405    use geo::LinesIter;
406    use std::time::Instant;
407
408    let n = polys.len().min(sample);
409    let mut t_basic = 0.0f64;
410    let mut t_collect = 0.0f64;
411    let mut t_int = 0.0f64;
412    let mut t_holes = 0.0f64;
413    let mut n_basic_pass = 0usize;
414    let mut n_int_pass = 0usize;
415    let mut n_holes_pass = 0usize;
416
417    for p in &polys[..n] {
418        let t0 = Instant::now();
419        let basic = arrange::poly_has_basic_form(p);
420        t_basic += t0.elapsed().as_secs_f64();
421        if !basic {
422            continue;
423        }
424        n_basic_pass += 1;
425
426        let t0 = Instant::now();
427        let lines: Vec<_> = p.lines_iter().collect();
428        t_collect += t0.elapsed().as_secs_f64();
429        if lines.is_empty() {
430            continue;
431        }
432
433        let t0 = Instant::now();
434        let no_int = arrange::prep::has_no_intersections(&lines);
435        t_int += t0.elapsed().as_secs_f64();
436        if !no_int {
437            continue;
438        }
439        n_int_pass += 1;
440
441        let t0 = Instant::now();
442        let holes = arrange::holes_are_valid(p);
443        t_holes += t0.elapsed().as_secs_f64();
444        if !holes {
445            continue;
446        }
447        n_holes_pass += 1;
448    }
449
450    let total = t_basic + t_collect + t_int + t_holes;
451    eprintln!("\n═══ Structure fast path profile (n={n}) ═══");
452    eprintln!(
453        "  poly_has_basic_form:  {:.3}s ({:.1}%)",
454        t_basic,
455        t_basic / total * 100.0
456    );
457    eprintln!(
458        "  lines collect:        {:.3}s ({:.1}%)",
459        t_collect,
460        t_collect / total * 100.0
461    );
462    eprintln!(
463        "  has_no_intersections: {:.3}s ({:.1}%)",
464        t_int,
465        t_int / total * 100.0
466    );
467    eprintln!(
468        "  holes_are_valid:      {:.3}s ({:.1}%)",
469        t_holes,
470        t_holes / total * 100.0
471    );
472    eprintln!("  ─────");
473    eprintln!("  total:                {:.3}s", total);
474    eprintln!(
475        "  basic form pass:      {n_basic_pass}/{n} ({:.1}%)",
476        n_basic_pass as f64 / n as f64 * 100.0
477    );
478    eprintln!(
479        "  no intersections:     {n_int_pass}/{n} ({:.1}%)",
480        n_int_pass as f64 / n as f64 * 100.0
481    );
482    eprintln!(
483        "  holes valid:          {n_holes_pass}/{n} ({:.1}%)",
484        n_holes_pass as f64 / n as f64 * 100.0
485    );
486    eprintln!();
487}