geodetic 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44b5221a45f63382e712aa88d033d1cb0b8165f8804833a1fd8523a9d7d8ce32
4
- data.tar.gz: 787dc88782b277262e15dde4fa1f61801894d1d34eff304dc63965147a3b10e8
3
+ metadata.gz: 0c20a680d6ffe0e6a82f3a0b7e8f6b1a428baa951b7122c0be75079f667f1099
4
+ data.tar.gz: 28031d89b1a5705cc28f6ddc751034d982e51581544f44a1d46a8792d8fd0f50
5
5
  SHA512:
6
- metadata.gz: ffb91f043c3c0c9a2bb6b44fad7952936121a09b18879350398cf260fd2be1f594f33c405df7cea0027f59c0e06679f1d4f718192d7626032a4b5fab195626f9
7
- data.tar.gz: dd922521aa23db80a5d9fdf56dbb98dcdfab811f72a126978d9b8dffe248f72f3dd83c3cae1e41d3ee2a1cf11455d70120d6c81fff7523f3aac798397250fbca
6
+ metadata.gz: 4c41de7eb8e8bdb61921dfca83310a2d5ed0978d9bd6f0f1d3f06ce33bd877070581c873396a33b80b40722ec741f93f5e80cf2afb0e90561f5a28d2dc6e7953
7
+ data.tar.gz: 8ce010a527e660a01fab3b93d8a9e4f90bceb688dd753a5a634cbf3d25ba3deaf2b8bc8038e8926767d8bd017a26e09074aa3be1d94df1813bc6407d56865ab0
data/CHANGELOG.md CHANGED
@@ -8,6 +8,81 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8
8
  ## [Unreleased]
9
9
 
10
10
 
11
+ ## [0.7.0] - 2026-03-10
12
+
13
+ ### Added
14
+
15
+ - **`Geodetic::Geos` module** — optional GEOS C library integration via `fiddle` for accelerated spatial operations
16
+ - **Library binding**: auto-discovers `libgeos_c` on macOS and Linux; uses reentrant `_r` API for thread safety
17
+ - **Predicates**: `Geos.contains?(a, b)`, `Geos.intersects?(a, b)`, `Geos.is_valid?(geom)`, `Geos.is_valid_reason(geom)`
18
+ - **Boolean operations**: `Geos.intersection(a, b)`, `Geos.difference(a, b)`, `Geos.symmetric_difference(a, b)`, `Geos.union(geom)`
19
+ - **Geometry construction**: `Geos.buffer(geom, distance)`, `Geos.buffer_with_style(geom, distance, ...)`, `Geos.convex_hull(geom)`, `Geos.simplify(geom, tolerance)`, `Geos.make_valid(geom)`
20
+ - **Measurements**: `Geos.area(geom)`, `Geos.length(geom)`, `Geos.distance(a, b)`, `Geos.nearest_points(a, b)`
21
+ - **`PreparedGeometry`**: `Geos.prepare(polygon)` builds a spatial index for O(log n) batch `contains?`/`intersects?` queries
22
+ - **Graceful degradation**: `Geos.available?` returns false when `libgeos_c` is not installed; all operations fall back to pure Ruby
23
+ - **`GEODETIC_GEOS_DISABLE` env var**: forces pure Ruby for all operations even when GEOS is installed
24
+ - **`LIBGEOS_PATH` env var**: specify a custom `libgeos_c` library path
25
+ - All GEOS operations accept any Geodetic geometry type and return standard Geodetic objects (Polygon, Path, LLA, etc.)
26
+ - **GEOS-accelerated polygon validation** — `Polygon.new` delegates self-intersection validation to GEOS when available, using O(n log n) spatial indexing vs Ruby's O(n^2) pairwise test
27
+ - **GEOS-accelerated point-in-polygon** — `polygon.includes?(point)` uses GEOS for polygons with 15+ vertices (`Polygon::GEOS_INCLUDES_THRESHOLD`); below threshold, Ruby's winding-number algorithm is faster
28
+ - **GEOS-accelerated path intersection** — `path.intersects?(other_path)` uses GEOS when available (wins at all tested sizes vs Ruby's O(n*m) brute-force)
29
+ - **Improved Ruby polygon validation** — added `validate_distinct_vertices!` and `validate_noncollinear!` checks so pure Ruby matches GEOS accuracy for degenerate polygons
30
+ - GEOS benchmark example (`examples/12_geos_benchmark.rb`) — compares Ruby vs GEOS performance across polygon validation, point-in-polygon, path intersection, PreparedGeometry batch containment, single segment (Ruby wins), and GEOS-only operations
31
+ - GEOS operations example (`examples/13_geos_operations.rb`) — 11-section demo covering boolean overlay, buffering, convex hull, simplification, validity checking, geometry repair, planar measurements, nearest points, PreparedGeometry, operation chaining, and GeoJSON/WKT export of results
32
+ - 27 GEOS tests covering all operations, predicates, PreparedGeometry, and error handling
33
+ - 3 new polygon validation tests: collinear boundary, insufficient distinct vertices, all-same-point
34
+ - Documentation: `docs/reference/geos-acceleration.md` (installation, automatic dispatch, performance expectations, API reference)
35
+
36
+ ### Changed
37
+
38
+ - Updated README with GEOS Acceleration feature, optional dependency section, and examples 12-13 in the examples table
39
+ - Updated `examples/README.md` with example 12 and 13 descriptions
40
+ - Updated `examples/sample_geometries.wkb.hex` to use valid (non-degenerate) triangles in polygon entries
41
+
42
+ ### Fixed
43
+
44
+ - WKB test data used collinear triangle `(1,2)-(3,4)-(5,6)` which is now correctly rejected; updated to valid triangle `(1,2)-(3,4)-(5,2)`
45
+ - Polygon validation error message regex in tests updated to match both Ruby and GEOS formats
46
+
47
+ ## [0.6.0] - 2026-03-10
48
+
49
+ ### Added
50
+
51
+ - **`Geodetic::WKT` module** — Well-Known Text serialization for all geometry types
52
+ - **`to_wkt(precision: 6, srid: nil)`** instance method on all 18 coordinate classes, Segment, Path, Areas::Polygon (and subclasses), Areas::Circle, Areas::BoundingBox, and Feature
53
+ - **Coordinate order**: longitude latitude (OGC Simple Features standard)
54
+ - **Z suffix**: automatically added when any point in the geometry has non-zero altitude; Z-dimensionality is uniform within each geometry
55
+ - **EWKT**: `srid:` option prepends `SRID=N;` for PostGIS compatibility
56
+ - **`WKT.parse(string)`** — parse WKT/EWKT into Geodetic objects (Point → LLA, LineString → Segment/Path, Polygon → Areas::Polygon, Multi*/GeometryCollection → Array)
57
+ - **`WKT.parse_with_srid(string)`** — returns `[object, srid]` tuple
58
+ - **`WKT.save!(path, *objects, srid:, precision:)`** — write one WKT per line
59
+ - **`WKT.load(path)`** — read WKT file into Array of Geodetic objects
60
+ - ENU/NED raise `ArgumentError` (relative systems cannot be exported)
61
+ - **`Geodetic::WKB` module** — Well-Known Binary serialization for all geometry types
62
+ - **`to_wkb(srid: nil)`** and **`to_wkb_hex(srid: nil)`** instance methods on all 18 coordinate classes, Segment, Path, Areas::Polygon (and subclasses), Areas::Circle, Areas::BoundingBox, and Feature
63
+ - **Byte order**: output is always little-endian (NDR), matching PostGIS, GEOS, RGeo, and Shapely; parser supports both LE and BE input
64
+ - **ISO WKB Z**: type code + 1000 (Point Z = 1001, LineString Z = 1002, Polygon Z = 1003) when any point has non-zero altitude
65
+ - **EWKB**: `srid:` option embeds SRID via `0x20000000` flag for PostGIS compatibility
66
+ - **`WKB.parse(input)`** — parse WKB from binary or hex string (auto-detects encoding)
67
+ - **`WKB.parse_with_srid(input)`** — returns `[object, srid]` tuple
68
+ - **Binary file I/O**: `WKB.save!(path, *objects, srid:)` / `WKB.load(path)` — framed format (4-byte LE count + size-prefixed WKB)
69
+ - **Hex file I/O**: `WKB.save_hex!(path, *objects, srid:)` / `WKB.load_hex(path)` — one hex string per line, supports `#` comments
70
+ - Supports all WKB types: Point, LineString, Polygon, MultiPoint, MultiLineString, MultiPolygon, GeometryCollection
71
+ - ENU/NED raise `ArgumentError` (relative systems cannot be exported)
72
+ - WKT example (`examples/10_wkt_serialization.rb`) — 10-section demo covering export, SRID/EWKT, Z-dimension, parsing, roundtrip, and file I/O
73
+ - WKB example (`examples/11_wkb_serialization.rb`) — 10-section demo covering export, EWKB/SRID, Z-dimension, parsing, roundtrip, and binary/hex file I/O
74
+ - WKB fixture files: `examples/sample_geometries.wkb` (9 geometries) and `examples/sample_geometries.wkb.hex` (15 geometries with comments)
75
+ - 53 WKT tests (134 assertions) and 54 WKB tests (144 assertions)
76
+ - Documentation: `docs/reference/wkt.md` and `docs/reference/wkb.md`
77
+
78
+ ### Changed
79
+
80
+ - Updated README with WKT and WKB sections, key features, and examples 10-11 in the examples table
81
+ - Updated `docs/index.md` with WKT and WKB in key features and reference links
82
+ - Updated `examples/README.md` with example 10 and 11 descriptions
83
+ - Updated `mkdocs.yml` nav with WKT and WKB reference pages
84
+ - Added `require_relative "geodetic/wkt"` and `require_relative "geodetic/wkb"` to `lib/geodetic.rb`
85
+
11
86
  ## [0.5.2] - 2026-03-10
12
87
 
13
88
  ### Added
data/README.md CHANGED
@@ -25,6 +25,9 @@
25
25
  - <strong>Vectors</strong> - Geodetic displacement (distance + bearing) with full arithmetic and Vincenty direct<br>
26
26
  - <strong>Geodetic Arithmetic</strong> - Compose geometry with operators: P1 + P2 → Segment, + P3 → Path, + Distance → Circle, * Vector → translate<br>
27
27
  - <strong>GeoJSON Export</strong> - Build FeatureCollections from any mix of objects and save to file<br>
28
+ - <strong>WKT Serialization</strong> - Well-Known Text export/import with SRID/EWKT and Z-dimension support<br>
29
+ - <strong>WKB Serialization</strong> - Well-Known Binary export/import with EWKB, SRID, hex encoding, and file I/O<br>
30
+ - <strong>GEOS Acceleration</strong> - Optional native acceleration for polygon validation, point-in-polygon, path intersection, and boolean operations<br>
28
31
  - <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
29
32
  - <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
30
33
  - <strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
@@ -63,6 +66,26 @@ brew install h3
63
66
 
64
67
  You can also set the `LIBH3_PATH` environment variable to point to a custom `libh3` location.
65
68
 
69
+ ### Optional: GEOS Spatial Acceleration
70
+
71
+ The [GEOS](https://libgeos.org/) library accelerates polygon validation, point-in-polygon tests, path intersection, and adds boolean geometry operations (intersection, difference, convex hull, etc.). Without it, all operations use pure Ruby implementations. Geodetic automatically uses GEOS when available and falls back to Ruby when it is not.
72
+
73
+ ```bash
74
+ # macOS
75
+ brew install geos
76
+
77
+ # Linux (Debian/Ubuntu)
78
+ sudo apt-get install libgeos-dev
79
+ ```
80
+
81
+ Geodetic uses GEOS selectively — only where it provides a measurable speedup. Single segment intersections stay in Ruby (FFI overhead exceeds the computation cost). For polygons with fewer than 15 vertices, point-in-polygon tests also stay in Ruby.
82
+
83
+ Set `GEODETIC_GEOS_DISABLE=1` to force pure Ruby for all operations, even when GEOS is installed. See [GEOS Acceleration](docs/reference/geos-acceleration.md) for detailed performance analysis and [example 12](examples/12_geos_benchmark.rb) for a runnable benchmark.
84
+
85
+ ```ruby
86
+ Geodetic::Geos.available? # => true when libgeos_c is found and not disabled
87
+ ```
88
+
66
89
  ## Usage
67
90
 
68
91
  ### Basic Coordinate Creation
@@ -769,6 +792,73 @@ objects = Geodetic::GeoJSON.load("map.geojson")
769
792
 
770
793
  `load` returns an Array of Geodetic objects. Features with a `"name"` or non-empty properties round-trip as `Feature` objects; bare geometries with empty properties return as raw coordinates, segments, paths, or polygons. `GeoJSON.parse(hash)` does the same from an already-parsed Hash.
771
794
 
795
+ ### WKT Serialization
796
+
797
+ `WKT` provides Well-Known Text export and import for all geometry types. WKT is the standard format used by PostGIS, RGeo, Shapely, JTS, and most GIS tools.
798
+
799
+ ```ruby
800
+ seattle.to_wkt # => "POINT(-122.3493 47.6205)"
801
+ seattle.to_wkt(srid: 4326) # => "SRID=4326;POINT(-122.3493 47.6205)"
802
+ seattle.to_wkt(precision: 2) # => "POINT(-122.35 47.62)"
803
+
804
+ Segment.new(seattle, portland).to_wkt # => "LINESTRING(-122.3493 47.6205, -122.6784 45.5152)"
805
+ route.to_wkt # => "LINESTRING(...)"
806
+ polygon.to_wkt # => "POLYGON((...))
807
+ circle.to_wkt(segments: 64) # => "POLYGON((...)) (64-gon)"
808
+ feature.to_wkt # => delegates to geometry (WKT has no properties)
809
+ ```
810
+
811
+ Altitude triggers the Z suffix. When any point has altitude, all points in that geometry get Z:
812
+
813
+ ```ruby
814
+ Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 184.0).to_wkt
815
+ # => "POINT Z(-122.35 47.62 184.0)"
816
+ ```
817
+
818
+ **File I/O and parsing:**
819
+
820
+ ```ruby
821
+ # Save to file (one WKT per line)
822
+ Geodetic::WKT.save!("shapes.wkt", seattle, segment, polygon, srid: 4326)
823
+
824
+ # Load from file
825
+ objects = Geodetic::WKT.load("shapes.wkt")
826
+ # => [Coordinate::LLA, Segment, Areas::Polygon]
827
+
828
+ # Parse a single WKT string
829
+ obj = Geodetic::WKT.parse("POINT(-122.3493 47.6205)")
830
+ obj, srid = Geodetic::WKT.parse_with_srid("SRID=4326;POLYGON((-122 47, -121 46, -123 46, -122 47))")
831
+ ```
832
+
833
+ ### WKB Serialization
834
+
835
+ `WKB` provides Well-Known Binary export and import — the binary counterpart to WKT used by PostGIS, GEOS, RGeo, and Shapely for efficient geometry storage. Output is always little-endian (NDR).
836
+
837
+ ```ruby
838
+ seattle.to_wkb # => 21-byte binary string
839
+ seattle.to_wkb_hex # => "01010000008a1f63ee5a965ec0..."
840
+ seattle.to_wkb_hex(srid: 4326) # => EWKB with embedded SRID
841
+
842
+ Segment.new(seattle, portland).to_wkb_hex # => LINESTRING hex
843
+ polygon.to_wkb_hex # => POLYGON hex
844
+ ```
845
+
846
+ **File I/O (binary and hex):**
847
+
848
+ ```ruby
849
+ # Binary format (framed: count + size-prefixed WKB)
850
+ Geodetic::WKB.save!("shapes.wkb", seattle, segment, polygon)
851
+ objects = Geodetic::WKB.load("shapes.wkb")
852
+
853
+ # Hex format (one hex string per line, supports comments)
854
+ Geodetic::WKB.save_hex!("shapes.wkb.hex", seattle, segment, polygon)
855
+ objects = Geodetic::WKB.load_hex("shapes.wkb.hex")
856
+
857
+ # Parse a single hex or binary string
858
+ obj = Geodetic::WKB.parse("01010000008a1f63ee5a965ec08195438b6ccf4740")
859
+ obj, srid = Geodetic::WKB.parse_with_srid(ewkb_hex)
860
+ ```
861
+
772
862
  ### Web Mercator Tile Coordinates
773
863
 
774
864
  ```ruby
@@ -802,6 +892,10 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
802
892
  | [`07_segments_and_shapes.rb`](examples/07_segments_and_shapes.rb) | Segment and polygon subclasses: Triangle, Rectangle, Pentagon, Hexagon, Octagon with containment, edges, and bounding boxes |
803
893
  | [`08_geodetic_arithmetic.rb`](examples/08_geodetic_arithmetic.rb) | Geodetic arithmetic: building geometry with + (Segments, Paths, Circles), Vector class (Vincenty direct, components, arithmetic, dot/cross products), translation with * (Coordinates, Segments, Paths, Circles, Polygons), and corridors |
804
894
  | [`09_geojson_export.rb`](examples/09_geojson_export.rb) | GeoJSON export: `to_geojson` on all geometry types, `GeoJSON` class for building FeatureCollections with `<<`, delete/clear, Enumerable, and `save` to file |
895
+ | [`10_wkt_serialization.rb`](examples/10_wkt_serialization.rb) | WKT serialization: `to_wkt` on all geometry types, SRID/EWKT, Z-dimension handling, parsing, and roundtrip verification |
896
+ | [`11_wkb_serialization.rb`](examples/11_wkb_serialization.rb) | WKB serialization: `to_wkb`/`to_wkb_hex` on all geometry types, EWKB/SRID, Z-dimension, parsing, roundtrip, and binary/hex file I/O |
897
+ | [`12_geos_benchmark.rb`](examples/12_geos_benchmark.rb) | GEOS performance benchmark: polygon validation, point-in-polygon, path intersection, PreparedGeometry batch containment, and GEOS-only boolean operations |
898
+ | [`13_geos_operations.rb`](examples/13_geos_operations.rb) | GEOS-only operations: boolean overlay (intersection, difference, symmetric difference, union), buffering, convex hull, simplification, validity checking, geometry repair, planar measurements, nearest points, PreparedGeometry, and operation chaining |
805
899
 
806
900
  Run any example with:
807
901
 
data/docs/index.md CHANGED
@@ -19,6 +19,8 @@
19
19
  <li><strong>Paths</strong> - Directed coordinate sequences with navigation, interpolation, closest approach, intersection, and area conversion<br>
20
20
  <li><strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
21
21
  <li><strong>GeoJSON Export</strong> - Build FeatureCollections from any mix of objects and save to file<br>
22
+ <li><strong>WKT Serialization</strong> - Well-Known Text export/import with SRID/EWKT and Z-dimension support<br>
23
+ <li><strong>WKB Serialization</strong> - Well-Known Binary export/import with EWKB, SRID, hex encoding, and file I/O<br>
22
24
  <li><strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
23
25
  <li><strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
24
26
  <li><strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
@@ -114,4 +116,6 @@ puts lla_again.to_s
114
116
  - [Vector](reference/vector.md)
115
117
  - [Arithmetic](reference/arithmetic.md)
116
118
  - [GeoJSON Export](reference/geojson.md)
119
+ - [WKT Serialization](reference/wkt.md)
120
+ - [WKB Serialization](reference/wkb.md)
117
121
  - [Map Rendering](reference/map-rendering.md)
@@ -0,0 +1,170 @@
1
+ # GEOS Acceleration
2
+
3
+ Geodetic optionally integrates with the [GEOS](https://libgeos.org/) (Geometry Engine - Open Source) C library to accelerate spatial operations. When `libgeos_c` is available, Geodetic transparently delegates performance-critical geometry operations to GEOS while keeping pure Ruby as the fallback.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # macOS
9
+ brew install geos
10
+
11
+ # Linux (Debian/Ubuntu)
12
+ sudo apt-get install libgeos-dev
13
+
14
+ # Linux (Fedora/RHEL)
15
+ sudo dnf install geos-devel
16
+ ```
17
+
18
+ Verify installation:
19
+
20
+ ```ruby
21
+ require "geodetic"
22
+ Geodetic::Geos.available? # => true
23
+ ```
24
+
25
+ ## How It Works
26
+
27
+ Geodetic uses Ruby's `fiddle` stdlib (the same approach used for H3) to call the GEOS C API directly — no compiled extensions or external gems required. The integration uses the reentrant `_r` API for thread safety.
28
+
29
+ The bridge works through WKT serialization:
30
+
31
+ 1. Geodetic objects are converted to WKT via `to_wkt`
32
+ 2. GEOS parses the WKT into its internal geometry representation
33
+ 3. GEOS performs the operation using optimized C code with spatial indexing
34
+ 4. Results are converted back to WKT and parsed into Geodetic objects
35
+
36
+ This approach keeps the integration simple and maintainable while providing significant performance gains for complex geometries.
37
+
38
+ ## Automatic Dispatch
39
+
40
+ Geodetic selectively uses GEOS only where it provides a measurable speedup. The dispatch logic is built into the existing classes:
41
+
42
+ ### Polygon Validation
43
+
44
+ `Polygon.new(boundary: [...])` automatically validates the boundary for self-intersection. GEOS uses an O(n log n) spatial index compared to Ruby's O(n^2) pairwise segment test.
45
+
46
+ - GEOS is used at **all polygon sizes** when available
47
+ - Speedup grows with vertex count: ~2x at 50 vertices, ~5x at 100, ~50x+ at 500
48
+
49
+ ### Point-in-Polygon
50
+
51
+ `polygon.includes?(point)` tests whether a point lies inside a polygon.
52
+
53
+ - GEOS is used for polygons with **15 or more vertices** (`Polygon::GEOS_INCLUDES_THRESHOLD`)
54
+ - Below 15 vertices, Ruby's winding-number algorithm is faster (FFI overhead dominates)
55
+ - Above the threshold, GEOS provides 2-10x speedup depending on polygon complexity
56
+
57
+ ### Path Intersection
58
+
59
+ `path.intersects?(other_path)` tests whether two paths cross.
60
+
61
+ - GEOS is **always used** when available (wins at all tested sizes)
62
+ - Ruby uses O(n*m) brute-force segment pair testing
63
+ - GEOS uses spatial indexing for efficient intersection detection
64
+ - Speedup is most dramatic for non-intersecting paths with overlapping bounds (worst case for Ruby): 10x at 100 points, 100x+ at 1000 points
65
+
66
+ ### Where Ruby Wins
67
+
68
+ Single segment intersection (`segment.intersects?(other_segment)`) stays in pure Ruby at all times. The FFI marshaling overhead for a single pair of line segments exceeds the computation cost. Geodetic does not use GEOS for this operation.
69
+
70
+ ## PreparedGeometry
71
+
72
+ For batch operations (testing many points against the same polygon), `PreparedGeometry` builds a spatial index once and reuses it for O(log n) queries:
73
+
74
+ ```ruby
75
+ polygon = Geodetic::Areas::Polygon.new(boundary: vertices)
76
+ prepared = Geodetic::Geos.prepare(polygon)
77
+
78
+ points.each do |pt|
79
+ prepared.contains?(pt) # O(log n) per query
80
+ end
81
+
82
+ prepared.release # free the GEOS geometry
83
+ ```
84
+
85
+ This is significantly faster than calling `polygon.includes?` in a loop, which creates a new GEOS geometry for each call.
86
+
87
+ ## GEOS-Only Operations
88
+
89
+ GEOS provides operations that have no pure Ruby equivalent in Geodetic:
90
+
91
+ ```ruby
92
+ Geos = Geodetic::Geos
93
+
94
+ # Boolean operations — return Geodetic objects
95
+ Geos.intersection(poly_a, poly_b) # area of overlap
96
+ Geos.difference(poly_a, poly_b) # A minus B
97
+ Geos.symmetric_difference(poly_a, poly_b) # area in either but not both
98
+
99
+ # Geometry analysis
100
+ Geos.convex_hull(polygon) # smallest convex polygon
101
+ Geos.simplify(polygon, tolerance) # Douglas-Peucker simplification
102
+ Geos.make_valid(polygon) # repair invalid geometry
103
+ Geos.is_valid?(polygon) # OGC validity check
104
+ Geos.is_valid_reason(polygon) # human-readable validity reason
105
+
106
+ # Measurements
107
+ Geos.area(polygon) # area in coordinate units
108
+ Geos.length(path) # length in coordinate units
109
+ Geos.distance(geom_a, geom_b) # minimum distance
110
+
111
+ # Spatial relationships
112
+ Geos.nearest_points(geom_a, geom_b) # closest point pair
113
+ Geos.contains?(polygon, point) # containment test
114
+ Geos.intersects?(geom_a, geom_b) # intersection test
115
+
116
+ # Buffering
117
+ Geos.buffer(geometry, width) # buffer zone
118
+ Geos.buffer_with_style(geometry, width, quad_segs:, cap_style:, join_style:)
119
+ ```
120
+
121
+ ## Disabling GEOS
122
+
123
+ Set the `GEODETIC_GEOS_DISABLE` environment variable to force pure Ruby for all operations:
124
+
125
+ ```bash
126
+ GEODETIC_GEOS_DISABLE=1 ruby -Ilib my_script.rb
127
+ ```
128
+
129
+ ```ruby
130
+ ENV['GEODETIC_GEOS_DISABLE'] = '1'
131
+ Geodetic::Geos.available? # => false (even when libgeos_c is installed)
132
+ ```
133
+
134
+ This is useful for:
135
+
136
+ - Benchmarking Ruby vs GEOS performance (see example 12)
137
+ - Debugging to isolate whether an issue is in GEOS or Ruby code
138
+ - Running in environments where GEOS cannot be installed
139
+
140
+ ## Performance Expectations
141
+
142
+ The following table shows representative speedups measured with [example 12](../../examples/12_geos_benchmark.rb). Actual results vary by hardware and polygon complexity.
143
+
144
+ | Operation | Size | Typical Speedup |
145
+ |-----------|------|-----------------|
146
+ | Polygon validation | 50 vertices | ~2x |
147
+ | Polygon validation | 100 vertices | ~5x |
148
+ | Polygon validation | 500 vertices | ~50x |
149
+ | Point-in-polygon | 30 vertices | ~2x |
150
+ | Point-in-polygon | 100 vertices | ~5x |
151
+ | Point-in-polygon | 500 vertices | ~10x |
152
+ | Path intersection | 100 points | ~10x |
153
+ | Path intersection | 500 points | ~50x |
154
+ | Path intersection | 1000 points | ~100x |
155
+ | Batch containment (prepared) | 1000 points × 100v | ~20x |
156
+ | Single segment | 2 points | Ruby is ~2x faster |
157
+
158
+ Run the benchmark yourself:
159
+
160
+ ```bash
161
+ ruby -Ilib examples/12_geos_benchmark.rb
162
+ ```
163
+
164
+ ## Architecture Notes
165
+
166
+ - **Thread safety**: Uses the reentrant GEOS `_r` API with per-process context initialization
167
+ - **No compiled extensions**: Uses `fiddle` from Ruby's stdlib, same pattern as the H3 integration
168
+ - **Graceful degradation**: All operations work without GEOS; it is purely an optimization
169
+ - **WKT bridge**: Geometries are serialized to WKT for transfer between Ruby and GEOS, leveraging Geodetic's existing WKT infrastructure
170
+ - **Memory management**: GEOS geometries and readers/writers are properly freed via `GEOSGeom_destroy_r`, `GEOSWKTReader_destroy_r`, etc.