geodetic 0.6.0 → 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 +4 -4
- data/CHANGELOG.md +36 -0
- data/README.md +23 -0
- data/docs/reference/geos-acceleration.md +170 -0
- data/examples/12_geos_benchmark.rb +258 -0
- data/examples/13_geos_operations.rb +538 -0
- data/examples/README.md +52 -0
- data/examples/sample_geometries.wkb.hex +6 -6
- data/lib/geodetic/areas/polygon.rb +66 -10
- data/lib/geodetic/geos.rb +547 -0
- data/lib/geodetic/path.rb +20 -8
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +1 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0c20a680d6ffe0e6a82f3a0b7e8f6b1a428baa951b7122c0be75079f667f1099
|
|
4
|
+
data.tar.gz: 28031d89b1a5705cc28f6ddc751034d982e51581544f44a1d46a8792d8fd0f50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4c41de7eb8e8bdb61921dfca83310a2d5ed0978d9bd6f0f1d3f06ce33bd877070581c873396a33b80b40722ec741f93f5e80cf2afb0e90561f5a28d2dc6e7953
|
|
7
|
+
data.tar.gz: 8ce010a527e660a01fab3b93d8a9e4f90bceb688dd753a5a634cbf3d25ba3deaf2b8bc8038e8926767d8bd017a26e09074aa3be1d94df1813bc6407d56865ab0
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,42 @@ 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
|
+
|
|
11
47
|
## [0.6.0] - 2026-03-10
|
|
12
48
|
|
|
13
49
|
### Added
|
data/README.md
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
- <strong>GeoJSON Export</strong> - Build FeatureCollections from any mix of objects and save to file<br>
|
|
28
28
|
- <strong>WKT Serialization</strong> - Well-Known Text export/import with SRID/EWKT and Z-dimension support<br>
|
|
29
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>
|
|
30
31
|
- <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
|
|
31
32
|
- <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
|
|
32
33
|
- <strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
|
|
@@ -65,6 +66,26 @@ brew install h3
|
|
|
65
66
|
|
|
66
67
|
You can also set the `LIBH3_PATH` environment variable to point to a custom `libh3` location.
|
|
67
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
|
+
|
|
68
89
|
## Usage
|
|
69
90
|
|
|
70
91
|
### Basic Coordinate Creation
|
|
@@ -873,6 +894,8 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
|
|
|
873
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 |
|
|
874
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 |
|
|
875
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 |
|
|
876
899
|
|
|
877
900
|
Run any example with:
|
|
878
901
|
|
|
@@ -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.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# GEOS Performance Benchmark
|
|
5
|
+
#
|
|
6
|
+
# Compares pure Ruby spatial operations against GEOS-accelerated versions.
|
|
7
|
+
# Uses GEODETIC_GEOS_DISABLE env var to force Ruby-only execution, then
|
|
8
|
+
# runs the same operations with GEOS enabled.
|
|
9
|
+
#
|
|
10
|
+
# Requires: brew install geos
|
|
11
|
+
# Run: ruby -Ilib examples/12_geos_benchmark.rb
|
|
12
|
+
|
|
13
|
+
require_relative '../lib/geodetic'
|
|
14
|
+
require 'benchmark'
|
|
15
|
+
|
|
16
|
+
LLA = Geodetic::Coordinate::LLA
|
|
17
|
+
Polygon = Geodetic::Areas::Polygon
|
|
18
|
+
Path = Geodetic::Path
|
|
19
|
+
Segment = Geodetic::Segment
|
|
20
|
+
Geos = Geodetic::Geos
|
|
21
|
+
|
|
22
|
+
unless Geos::LibGEOS.available?
|
|
23
|
+
abort "libgeos_c not found. Install with: brew install geos"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def make_polygon(n_vertices, center_lat: 40.0, center_lng: -74.0, radius_deg: 1.0)
|
|
31
|
+
step = 360.0 / n_vertices
|
|
32
|
+
boundary = n_vertices.times.map do |i|
|
|
33
|
+
angle = i * step * Geodetic::RAD_PER_DEG
|
|
34
|
+
LLA.new(
|
|
35
|
+
lat: center_lat + radius_deg * Math.sin(angle),
|
|
36
|
+
lng: center_lng + radius_deg * Math.cos(angle),
|
|
37
|
+
alt: 0.0
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
Polygon.new(boundary: boundary)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def make_path(n_points, start_lat: 40.0, start_lng: -74.0, step_deg: 0.01, zigzag: 0.001)
|
|
44
|
+
coords = n_points.times.map do |i|
|
|
45
|
+
LLA.new(
|
|
46
|
+
lat: start_lat + (i.even? ? zigzag : -zigzag),
|
|
47
|
+
lng: start_lng + i * step_deg,
|
|
48
|
+
alt: 0.0
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
Path.new(coordinates: coords)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def section(title)
|
|
55
|
+
puts
|
|
56
|
+
puts "=" * 70
|
|
57
|
+
puts title
|
|
58
|
+
puts "=" * 70
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def speedup(ruby_time, geos_time)
|
|
62
|
+
return "N/A" if geos_time == 0
|
|
63
|
+
ratio = ruby_time / geos_time
|
|
64
|
+
format("%.1fx", ratio)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# Collect timings for Ruby-only vs GEOS
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
results = []
|
|
72
|
+
|
|
73
|
+
# --- 1. Polygon validation ------------------------------------------------
|
|
74
|
+
|
|
75
|
+
section "1. POLYGON VALIDATION"
|
|
76
|
+
puts " Ruby uses O(n^2) pairwise segment tests."
|
|
77
|
+
puts " GEOS uses an O(n log n) spatial index."
|
|
78
|
+
|
|
79
|
+
[50, 100, 500].each do |n|
|
|
80
|
+
boundary = n.times.map do |i|
|
|
81
|
+
angle = i * (360.0 / n) * Geodetic::RAD_PER_DEG
|
|
82
|
+
LLA.new(lat: 40.0 + Math.sin(angle), lng: -74.0 + Math.cos(angle), alt: 0.0)
|
|
83
|
+
end
|
|
84
|
+
iterations = n <= 100 ? 500 : 50
|
|
85
|
+
|
|
86
|
+
puts "\n #{n} vertices, #{iterations} iterations:"
|
|
87
|
+
|
|
88
|
+
# Ruby-only (disable GEOS)
|
|
89
|
+
ENV['GEODETIC_GEOS_DISABLE'] = '1'
|
|
90
|
+
ruby_time = Benchmark.realtime do
|
|
91
|
+
iterations.times { Polygon.new(boundary: boundary.dup) }
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# GEOS-enabled
|
|
95
|
+
ENV.delete('GEODETIC_GEOS_DISABLE')
|
|
96
|
+
geos_time = Benchmark.realtime do
|
|
97
|
+
iterations.times { Polygon.new(boundary: boundary.dup) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
puts format(" Ruby: %8.4fs", ruby_time)
|
|
101
|
+
puts format(" GEOS: %8.4fs (%s faster)", geos_time, speedup(ruby_time, geos_time))
|
|
102
|
+
results << { test: "Validation #{n}v", ruby: ruby_time, geos: geos_time }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# --- 2. Point-in-polygon --------------------------------------------------
|
|
106
|
+
|
|
107
|
+
section "2. POINT-IN-POLYGON"
|
|
108
|
+
puts " Ruby uses winding-number with bearing calculations."
|
|
109
|
+
puts " GEOS uses computational geometry contains test."
|
|
110
|
+
puts " Threshold: polygons with >= #{Polygon::GEOS_INCLUDES_THRESHOLD} vertices use GEOS."
|
|
111
|
+
|
|
112
|
+
test_point = LLA.new(lat: 40.0, lng: -74.0, alt: 0.0)
|
|
113
|
+
|
|
114
|
+
[8, 30, 100, 500].each do |n|
|
|
115
|
+
polygon = make_polygon(n)
|
|
116
|
+
iterations = n <= 100 ? 5_000 : 1_000
|
|
117
|
+
|
|
118
|
+
above = n >= Polygon::GEOS_INCLUDES_THRESHOLD
|
|
119
|
+
label = above ? "GEOS" : "Ruby (below threshold)"
|
|
120
|
+
puts "\n #{n} vertices -> #{label}, #{iterations} iterations:"
|
|
121
|
+
|
|
122
|
+
ENV['GEODETIC_GEOS_DISABLE'] = '1'
|
|
123
|
+
ruby_time = Benchmark.realtime do
|
|
124
|
+
iterations.times { polygon.includes?(test_point) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
ENV.delete('GEODETIC_GEOS_DISABLE')
|
|
128
|
+
geos_time = Benchmark.realtime do
|
|
129
|
+
iterations.times { polygon.includes?(test_point) }
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
puts format(" Ruby: %8.4fs", ruby_time)
|
|
133
|
+
puts format(" GEOS: %8.4fs (%s faster)", geos_time, speedup(ruby_time, geos_time))
|
|
134
|
+
results << { test: "Includes #{n}v", ruby: ruby_time, geos: geos_time }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# --- 3. Path intersection -------------------------------------------------
|
|
138
|
+
|
|
139
|
+
section "3. PATH INTERSECTION"
|
|
140
|
+
puts " Ruby uses O(n*m) pairwise segment tests."
|
|
141
|
+
puts " GEOS uses spatial indexing for fast intersection."
|
|
142
|
+
puts " Non-intersecting paths with overlapping bounds (worst case for Ruby)."
|
|
143
|
+
|
|
144
|
+
[100, 500, 1000].each do |n|
|
|
145
|
+
# Two zigzag paths: bounds overlap but segments don't cross.
|
|
146
|
+
# Ruby must check all O(n*m) segment pairs before returning false.
|
|
147
|
+
path1 = make_path(n, start_lat: 40.0, zigzag: 0.05)
|
|
148
|
+
path2 = make_path(n, start_lat: 40.08, zigzag: 0.05)
|
|
149
|
+
iterations = [5000 / n, 5].max
|
|
150
|
+
|
|
151
|
+
puts "\n #{n} points per path, #{iterations} iterations:"
|
|
152
|
+
|
|
153
|
+
ENV['GEODETIC_GEOS_DISABLE'] = '1'
|
|
154
|
+
ruby_time = Benchmark.realtime do
|
|
155
|
+
iterations.times { path1.intersects?(path2) }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
ENV.delete('GEODETIC_GEOS_DISABLE')
|
|
159
|
+
geos_time = Benchmark.realtime do
|
|
160
|
+
iterations.times { path1.intersects?(path2) }
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
puts format(" Ruby: %8.4fs", ruby_time)
|
|
164
|
+
puts format(" GEOS: %8.4fs (%s faster)", geos_time, speedup(ruby_time, geos_time))
|
|
165
|
+
results << { test: "Path intersect #{n}pt", ruby: ruby_time, geos: geos_time }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# --- 4. Batch containment with PreparedGeometry ----------------------------
|
|
169
|
+
|
|
170
|
+
section "4. BATCH CONTAINMENT (PreparedGeometry)"
|
|
171
|
+
puts " Testing 1,000 random points against a 100-vertex polygon."
|
|
172
|
+
puts " PreparedGeometry builds a spatial index once, then queries are O(log n)."
|
|
173
|
+
|
|
174
|
+
srand(42) # reproducible random points
|
|
175
|
+
polygon = make_polygon(100)
|
|
176
|
+
test_points = 1_000.times.map do
|
|
177
|
+
LLA.new(lat: 39.0 + rand * 2.0, lng: -75.0 + rand * 2.0, alt: 0.0)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
ENV['GEODETIC_GEOS_DISABLE'] = '1'
|
|
181
|
+
ruby_time = Benchmark.realtime do
|
|
182
|
+
test_points.each { |pt| polygon.includes?(pt) }
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
ENV.delete('GEODETIC_GEOS_DISABLE')
|
|
186
|
+
geos_one_shot_time = Benchmark.realtime do
|
|
187
|
+
test_points.each { |pt| polygon.includes?(pt) }
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
prepared_time = Benchmark.realtime do
|
|
191
|
+
prepared = Geos.prepare(polygon)
|
|
192
|
+
test_points.each { |pt| prepared.contains?(pt) }
|
|
193
|
+
prepared.release
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
puts format("\n Ruby: %8.4fs", ruby_time)
|
|
197
|
+
puts format(" GEOS one-shot: %8.4fs (%s faster)", geos_one_shot_time, speedup(ruby_time, geos_one_shot_time))
|
|
198
|
+
puts format(" GEOS prepared: %8.4fs (%s faster)", prepared_time, speedup(ruby_time, prepared_time))
|
|
199
|
+
results << { test: "Batch 1k points", ruby: ruby_time, geos: prepared_time }
|
|
200
|
+
|
|
201
|
+
# --- 5. Segment intersection (Ruby wins) ----------------------------------
|
|
202
|
+
|
|
203
|
+
section "5. SINGLE SEGMENT INTERSECTION (Ruby wins here)"
|
|
204
|
+
puts " For trivial operations, FFI overhead exceeds the computation cost."
|
|
205
|
+
puts " Geodetic correctly keeps Ruby for this case."
|
|
206
|
+
|
|
207
|
+
seg1 = Segment.new(
|
|
208
|
+
LLA.new(lat: 0.0, lng: 0.0, alt: 0.0),
|
|
209
|
+
LLA.new(lat: 2.0, lng: 2.0, alt: 0.0)
|
|
210
|
+
)
|
|
211
|
+
seg2 = Segment.new(
|
|
212
|
+
LLA.new(lat: 0.0, lng: 2.0, alt: 0.0),
|
|
213
|
+
LLA.new(lat: 2.0, lng: 0.0, alt: 0.0)
|
|
214
|
+
)
|
|
215
|
+
iterations = 50_000
|
|
216
|
+
|
|
217
|
+
ruby_time = Benchmark.realtime { iterations.times { seg1.intersects?(seg2) } }
|
|
218
|
+
geos_time = Benchmark.realtime { iterations.times { Geos.intersects?(seg1, seg2) } }
|
|
219
|
+
|
|
220
|
+
puts format("\n Ruby: %8.4fs", ruby_time)
|
|
221
|
+
puts format(" GEOS: %8.4fs (Ruby is %s faster)", geos_time, speedup(geos_time, ruby_time))
|
|
222
|
+
|
|
223
|
+
# --- 6. GEOS-only operations ----------------------------------------------
|
|
224
|
+
|
|
225
|
+
section "6. GEOS-ONLY OPERATIONS (no Ruby equivalent)"
|
|
226
|
+
puts " These capabilities are only available when GEOS is installed."
|
|
227
|
+
|
|
228
|
+
poly_a = make_polygon(100, center_lat: 40.0, center_lng: -74.0, radius_deg: 1.0)
|
|
229
|
+
poly_b = make_polygon(100, center_lat: 40.5, center_lng: -73.5, radius_deg: 1.0)
|
|
230
|
+
iterations = 500
|
|
231
|
+
|
|
232
|
+
puts "\n Two 100-vertex polygons, #{iterations} iterations each:"
|
|
233
|
+
Benchmark.bm(22) do |x|
|
|
234
|
+
x.report(" intersection") { iterations.times { Geos.intersection(poly_a, poly_b) } }
|
|
235
|
+
x.report(" difference") { iterations.times { Geos.difference(poly_a, poly_b) } }
|
|
236
|
+
x.report(" symmetric_diff") { iterations.times { Geos.symmetric_difference(poly_a, poly_b) } }
|
|
237
|
+
x.report(" convex_hull") { iterations.times { Geos.convex_hull(poly_a) } }
|
|
238
|
+
x.report(" simplify(0.01)") { iterations.times { Geos.simplify(poly_a, 0.01) } }
|
|
239
|
+
x.report(" is_valid?") { iterations.times { Geos.is_valid?(poly_a) } }
|
|
240
|
+
x.report(" area") { iterations.times { Geos.area(poly_a) } }
|
|
241
|
+
x.report(" nearest_points") { iterations.times { Geos.nearest_points(poly_a, poly_b) } }
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# --- Summary ---------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
section "SUMMARY"
|
|
247
|
+
puts
|
|
248
|
+
puts format(" %-25s %10s %10s %10s", "Test", "Ruby", "GEOS", "Speedup")
|
|
249
|
+
puts " " + "-" * 57
|
|
250
|
+
|
|
251
|
+
results.each do |r|
|
|
252
|
+
puts format(" %-25s %9.4fs %9.4fs %10s",
|
|
253
|
+
r[:test], r[:ruby], r[:geos], speedup(r[:ruby], r[:geos]))
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
puts
|
|
257
|
+
puts " Note: GEOS acceleration is automatic when libgeos_c is installed."
|
|
258
|
+
puts " Set GEODETIC_GEOS_DISABLE=1 to force pure Ruby for all operations."
|