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.
@@ -0,0 +1,538 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Demonstration of GEOS-Only Operations
4
+ # Shows geometry operations provided by the GEOS C library that have no
5
+ # pure Ruby equivalent in Geodetic: boolean operations, buffering,
6
+ # convex hull, simplification, validation repair, measurements, and
7
+ # PreparedGeometry batch queries.
8
+ #
9
+ # Requires: brew install geos
10
+ # Run: ruby -Ilib examples/13_geos_operations.rb
11
+
12
+ require_relative "../lib/geodetic"
13
+
14
+ include Geodetic
15
+
16
+ LLA = Coordinate::LLA
17
+ Polygon = Areas::Polygon
18
+ Geos = Geodetic::Geos
19
+
20
+ unless Geos.available?
21
+ if ENV.key?('GEODETIC_GEOS_DISABLE')
22
+ abort "GEODETIC_GEOS_DISABLE is set. Unset it to run this demo:\n unset GEODETIC_GEOS_DISABLE"
23
+ else
24
+ abort "libgeos_c not found. Install with: brew install geos"
25
+ end
26
+ end
27
+
28
+ def section(title)
29
+ puts
30
+ puts "=" * 70
31
+ puts title
32
+ puts "=" * 70
33
+ end
34
+
35
+ def subsection(title)
36
+ puts
37
+ puts " --- #{title} ---"
38
+ end
39
+
40
+ def fmt_polygon(poly)
41
+ if poly.is_a?(Array)
42
+ poly.map { |p| fmt_polygon(p) }.join("\n ")
43
+ elsif poly.is_a?(Polygon)
44
+ n = poly.boundary.length - 1
45
+ centroid = poly.centroid
46
+ "Polygon(#{n} vertices, centroid: #{centroid.lat.round(4)}, #{centroid.lng.round(4)})"
47
+ else
48
+ poly.to_s
49
+ end
50
+ end
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Build two overlapping polygons around NYC
54
+ # ---------------------------------------------------------------------------
55
+
56
+ # Lower Manhattan polygon (roughly)
57
+ lower = Polygon.new(boundary: [
58
+ LLA.new(lat: 40.700, lng: -74.020, alt: 0),
59
+ LLA.new(lat: 40.700, lng: -73.970, alt: 0),
60
+ LLA.new(lat: 40.730, lng: -73.970, alt: 0),
61
+ LLA.new(lat: 40.730, lng: -74.020, alt: 0),
62
+ ])
63
+
64
+ # Midtown Manhattan polygon (overlaps northern edge of lower)
65
+ midtown = Polygon.new(boundary: [
66
+ LLA.new(lat: 40.720, lng: -74.010, alt: 0),
67
+ LLA.new(lat: 40.720, lng: -73.960, alt: 0),
68
+ LLA.new(lat: 40.760, lng: -73.960, alt: 0),
69
+ LLA.new(lat: 40.760, lng: -74.010, alt: 0),
70
+ ])
71
+
72
+ puts <<~HEREDOC
73
+ === GEOS-Only Operations Demo ===
74
+
75
+ These operations require the GEOS C library and have no pure Ruby
76
+ equivalent in Geodetic. They enable computational geometry workflows
77
+ like boolean overlay, buffering, simplification, and spatial indexing.
78
+
79
+ Two overlapping rectangles around Manhattan:
80
+ Lower: 40.700-40.730 lat, -74.020 to -73.970 lng
81
+ Midtown: 40.720-40.760 lat, -74.010 to -73.960 lng
82
+ Overlap: 40.720-40.730 lat, -74.010 to -73.970 lng
83
+ HEREDOC
84
+
85
+ # ── 1. Boolean Operations ────────────────────────────────────────────
86
+
87
+ section "1. BOOLEAN OPERATIONS"
88
+
89
+ subsection "Intersection (area of overlap)"
90
+ overlap = Geos.intersection(lower, midtown)
91
+ puts " Geos.intersection(lower, midtown)"
92
+ puts " => #{fmt_polygon(overlap)}"
93
+ if overlap.is_a?(Polygon)
94
+ puts " Boundary:"
95
+ overlap.boundary[0...-1].each do |pt|
96
+ puts " (#{pt.lat.round(4)}, #{pt.lng.round(4)})"
97
+ end
98
+ end
99
+
100
+ subsection "Difference (lower minus midtown)"
101
+ diff = Geos.difference(lower, midtown)
102
+ puts " Geos.difference(lower, midtown)"
103
+ puts " => #{fmt_polygon(diff)}"
104
+ puts " The part of Lower Manhattan not covered by Midtown."
105
+
106
+ subsection "Difference (midtown minus lower)"
107
+ diff2 = Geos.difference(midtown, lower)
108
+ puts " Geos.difference(midtown, lower)"
109
+ puts " => #{fmt_polygon(diff2)}"
110
+ puts " The part of Midtown not covered by Lower Manhattan."
111
+
112
+ subsection "Symmetric Difference (in one but not both)"
113
+ sym = Geos.symmetric_difference(lower, midtown)
114
+ puts " Geos.symmetric_difference(lower, midtown)"
115
+ puts " => #{fmt_polygon(sym)}"
116
+ puts " Everything except the overlap — the XOR of the two polygons."
117
+
118
+ subsection "Union (merge into one polygon)"
119
+ merged = Geos.union(lower)
120
+ puts " Geos.union(lower)"
121
+ puts " => #{fmt_polygon(merged)}"
122
+ puts " Dissolves internal boundaries (useful for merging multi-polygons)."
123
+
124
+ # ── 2. Buffering ─────────────────────────────────────────────────────
125
+
126
+ section "2. BUFFERING"
127
+
128
+ puts <<~HEREDOC
129
+
130
+ Buffer creates a polygon at a fixed distance around any geometry.
131
+ Distance is in coordinate units (degrees for LLA).
132
+ ~0.001 degrees ≈ 111 meters at the equator.
133
+ HEREDOC
134
+
135
+ # Buffer a point to create a circle-like polygon
136
+ point = LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
137
+ buffered = Geos.buffer(point, 0.005, quad_segs: 16)
138
+
139
+ puts " Point buffer (Empire State Building, radius ~555m):"
140
+ puts " Geos.buffer(point, 0.005, quad_segs: 16)"
141
+ if buffered.is_a?(Polygon)
142
+ puts " => #{fmt_polygon(buffered)}"
143
+ end
144
+
145
+ # Buffer a path to create a corridor
146
+ path = Path.new(coordinates: [
147
+ LLA.new(lat: 40.7484, lng: -73.9857, alt: 0),
148
+ LLA.new(lat: 40.7580, lng: -73.9855, alt: 0),
149
+ LLA.new(lat: 40.7614, lng: -73.9776, alt: 0),
150
+ ])
151
+ corridor = Geos.buffer(path, 0.002)
152
+
153
+ puts
154
+ puts " Path buffer (corridor along a 3-point route, width ~222m):"
155
+ puts " Geos.buffer(path, 0.002)"
156
+ if corridor.is_a?(Polygon)
157
+ puts " => #{fmt_polygon(corridor)}"
158
+ end
159
+
160
+ subsection "Buffer with style options"
161
+ flat_buf = Geos.buffer_with_style(path, 0.002,
162
+ quad_segs: 8, end_cap_style: 2, join_style: 2)
163
+
164
+ puts " Geos.buffer_with_style(path, 0.002,"
165
+ puts " end_cap_style: 2, # flat caps"
166
+ puts " join_style: 2) # mitre joins"
167
+ if flat_buf.is_a?(Polygon)
168
+ puts " => #{fmt_polygon(flat_buf)}"
169
+ end
170
+
171
+ # ── 3. Convex Hull ───────────────────────────────────────────────────
172
+
173
+ section "3. CONVEX HULL"
174
+
175
+ puts <<~HEREDOC
176
+
177
+ Convex hull is the smallest convex polygon enclosing a geometry.
178
+ Think of stretching a rubber band around all the points.
179
+ HEREDOC
180
+
181
+ # Scatter some NYC landmarks
182
+ landmarks = [
183
+ LLA.new(lat: 40.6892, lng: -74.0445, alt: 0), # Statue of Liberty
184
+ LLA.new(lat: 40.7484, lng: -73.9857, alt: 0), # Empire State
185
+ LLA.new(lat: 40.7580, lng: -73.9855, alt: 0), # Rockefeller Center
186
+ LLA.new(lat: 40.7614, lng: -73.9776, alt: 0), # Central Park South
187
+ LLA.new(lat: 40.7128, lng: -74.0060, alt: 0), # One World Trade
188
+ LLA.new(lat: 40.6413, lng: -73.7781, alt: 0), # JFK Airport
189
+ ]
190
+ landmark_names = [
191
+ "Statue of Liberty", "Empire State", "Rockefeller Center",
192
+ "Central Park South", "One World Trade", "JFK Airport",
193
+ ]
194
+
195
+ # Build a path from the landmarks so GEOS sees all the points
196
+ landmark_path = Path.new(coordinates: landmarks)
197
+ hull = Geos.convex_hull(landmark_path)
198
+
199
+ puts " Input: 6 NYC landmarks"
200
+ landmarks.each_with_index do |pt, i|
201
+ puts " #{landmark_names[i]}: (#{pt.lat.round(4)}, #{pt.lng.round(4)})"
202
+ end
203
+ puts
204
+ puts " Geos.convex_hull(landmark_path)"
205
+ if hull.is_a?(Polygon)
206
+ puts " => #{fmt_polygon(hull)}"
207
+ puts " Hull vertices:"
208
+ hull.boundary[0...-1].each do |pt|
209
+ puts " (#{pt.lat.round(4)}, #{pt.lng.round(4)})"
210
+ end
211
+ end
212
+
213
+ # ── 4. Simplification ────────────────────────────────────────────────
214
+
215
+ section "4. SIMPLIFICATION (Douglas-Peucker)"
216
+
217
+ puts <<~HEREDOC
218
+
219
+ Simplify reduces vertices while preserving shape within a tolerance.
220
+ Uses the Douglas-Peucker algorithm. Tolerance is in coordinate units.
221
+ HEREDOC
222
+
223
+ # Create a detailed polygon (50-vertex circle approximation)
224
+ step = 360.0 / 50
225
+ detailed = Polygon.new(boundary: 50.times.map { |i|
226
+ angle = i * step * Geodetic::RAD_PER_DEG
227
+ LLA.new(
228
+ lat: 40.75 + 0.02 * Math.sin(angle),
229
+ lng: -73.99 + 0.02 * Math.cos(angle),
230
+ alt: 0.0
231
+ )
232
+ })
233
+
234
+ puts " Original polygon: #{detailed.boundary.length - 1} vertices"
235
+
236
+ [0.001, 0.005, 0.01].each do |tol|
237
+ simplified = Geos.simplify(detailed, tol)
238
+ if simplified.is_a?(Polygon)
239
+ n = simplified.boundary.length - 1
240
+ puts " Geos.simplify(polygon, #{tol}) => #{n} vertices"
241
+ end
242
+ end
243
+
244
+ # ── 5. Validity Checking ─────────────────────────────────────────────
245
+
246
+ section "5. VALIDITY CHECKING"
247
+
248
+ puts <<~HEREDOC
249
+
250
+ GEOS checks geometry against OGC validity rules and can explain
251
+ exactly why a geometry is invalid.
252
+ HEREDOC
253
+
254
+ puts " Valid polygon:"
255
+ puts " Geos.is_valid?(lower) => #{Geos.is_valid?(lower)}"
256
+ puts " Geos.is_valid_reason(lower) => \"#{Geos.is_valid_reason(lower)}\""
257
+
258
+ # Feed a bowtie (self-intersecting) polygon directly to GEOS via its
259
+ # WKT string, bypassing Geodetic's Polygon constructor which would reject it.
260
+ puts
261
+ puts " Self-intersecting polygon (bowtie):"
262
+ puts " Vertices cross at the center, forming a figure-8."
263
+ bowtie_wkt = "POLYGON((0 0, 2 2, 2 0, 0 2, 0 0))"
264
+ bowtie_geom = Geodetic::Geos::LibGEOS.wkt_to_geom(bowtie_wkt)
265
+ begin
266
+ valid = Geodetic::Geos::LibGEOS::F_IS_VALID.call(
267
+ Geodetic::Geos::LibGEOS.context, bowtie_geom) == 1
268
+ reason_ptr = Geodetic::Geos::LibGEOS::F_IS_VALID_REASON.call(
269
+ Geodetic::Geos::LibGEOS.context, bowtie_geom)
270
+ reason = reason_ptr.to_s
271
+ Geodetic::Geos::LibGEOS::F_GEOS_FREE.call(Geodetic::Geos::LibGEOS.context, reason_ptr)
272
+ puts " Geos.is_valid?(bowtie) => #{valid}"
273
+ puts " Geos.is_valid_reason(bowtie) => \"#{reason}\""
274
+ ensure
275
+ # keep bowtie_geom alive for make_valid below
276
+ end
277
+
278
+ # ── 6. Make Valid (Geometry Repair) ───────────────────────────────────
279
+
280
+ section "6. MAKE VALID (Geometry Repair)"
281
+
282
+ puts <<~HEREDOC
283
+
284
+ make_valid repairs invalid geometries — fixing self-intersections,
285
+ ring ordering, and other OGC violations. The result is always valid.
286
+ HEREDOC
287
+
288
+ puts " Repairing the bowtie polygon:"
289
+ repaired_geom = Geodetic::Geos::LibGEOS::F_MAKE_VALID.call(
290
+ Geodetic::Geos::LibGEOS.context, bowtie_geom)
291
+ repaired_wkt = Geodetic::Geos::LibGEOS.geom_to_wkt(repaired_geom, precision: 6)
292
+ repaired_valid = Geodetic::Geos::LibGEOS::F_IS_VALID.call(
293
+ Geodetic::Geos::LibGEOS.context, repaired_geom) == 1
294
+ puts " Geos.make_valid(bowtie)"
295
+ puts " => #{repaired_wkt}"
296
+ puts " Geos.is_valid?(repaired) => #{repaired_valid}"
297
+ puts " GEOS splits the self-intersecting bowtie into valid geometry."
298
+ Geodetic::Geos::LibGEOS.destroy_geom(repaired_geom)
299
+ Geodetic::Geos::LibGEOS.destroy_geom(bowtie_geom)
300
+
301
+ # ── 7. Planar Measurements ───────────────────────────────────────────
302
+
303
+ section "7. PLANAR MEASUREMENTS"
304
+
305
+ puts <<~HEREDOC
306
+
307
+ GEOS computes area, length, and minimum distance in coordinate units.
308
+ For LLA geometries, units are square degrees (area) and degrees
309
+ (length/distance). These are planar approximations — use Geodetic's
310
+ Vincenty-based distance_to for geodesic accuracy.
311
+ HEREDOC
312
+
313
+ puts " Area:"
314
+ puts " Geos.area(lower) => #{Geos.area(lower).round(8)} sq degrees"
315
+ puts " Geos.area(midtown) => #{Geos.area(midtown).round(8)} sq degrees"
316
+ if overlap.is_a?(Polygon)
317
+ puts " Geos.area(overlap) => #{Geos.area(overlap).round(8)} sq degrees"
318
+ end
319
+
320
+ puts
321
+ puts " Perimeter (length of boundary):"
322
+ puts " Geos.length(lower) => #{Geos.length(lower).round(6)} degrees"
323
+ puts " Geos.length(midtown) => #{Geos.length(midtown).round(6)} degrees"
324
+
325
+ puts
326
+ puts " Minimum distance between non-overlapping geometries:"
327
+
328
+ brooklyn = Polygon.new(boundary: [
329
+ LLA.new(lat: 40.630, lng: -73.990, alt: 0),
330
+ LLA.new(lat: 40.630, lng: -73.940, alt: 0),
331
+ LLA.new(lat: 40.660, lng: -73.940, alt: 0),
332
+ LLA.new(lat: 40.660, lng: -73.990, alt: 0),
333
+ ])
334
+ dist = Geos.distance(lower, brooklyn)
335
+ puts " Geos.distance(lower_manhattan, brooklyn) => #{dist.round(6)} degrees"
336
+ puts " (Approx #{(dist * 111_000).round(0)} meters at this latitude)"
337
+
338
+ # ── 8. Nearest Points ────────────────────────────────────────────────
339
+
340
+ section "8. NEAREST POINTS"
341
+
342
+ puts <<~HEREDOC
343
+
344
+ nearest_points finds the closest pair of points between two
345
+ geometries — one point on each. Returns [LLA, LLA].
346
+ HEREDOC
347
+
348
+ pts = Geos.nearest_points(lower, brooklyn)
349
+ puts " Geos.nearest_points(lower_manhattan, brooklyn)"
350
+ puts " Point on Lower Manhattan: (#{pts[0].lat.round(6)}, #{pts[0].lng.round(6)})"
351
+ puts " Point on Brooklyn: (#{pts[1].lat.round(6)}, #{pts[1].lng.round(6)})"
352
+
353
+ geodesic_dist = pts[0].distance_to(pts[1])
354
+ puts " Geodesic distance between them: #{geodesic_dist.to_s(0)}"
355
+
356
+ # ── 9. PreparedGeometry (Batch Spatial Index) ─────────────────────────
357
+
358
+ section "9. PREPARED GEOMETRY (Batch Spatial Index)"
359
+
360
+ puts <<~HEREDOC
361
+
362
+ PreparedGeometry builds a spatial index once, then subsequent
363
+ contains?/intersects? queries run in O(log n). Essential for
364
+ testing many points against the same polygon.
365
+ HEREDOC
366
+
367
+ # Build a 100-vertex polygon
368
+ step = 360.0 / 100
369
+ big_poly = Polygon.new(boundary: 100.times.map { |i|
370
+ angle = i * step * Geodetic::RAD_PER_DEG
371
+ LLA.new(
372
+ lat: 40.75 + 0.05 * Math.sin(angle),
373
+ lng: -73.99 + 0.05 * Math.cos(angle),
374
+ alt: 0.0
375
+ )
376
+ })
377
+
378
+ # Generate test points
379
+ srand(42)
380
+ test_points = 20.times.map do
381
+ LLA.new(lat: 40.69 + rand * 0.12, lng: -74.05 + rand * 0.12, alt: 0.0)
382
+ end
383
+
384
+ prepared = Geos.prepare(big_poly)
385
+
386
+ inside = 0
387
+ outside = 0
388
+ test_points.each do |pt|
389
+ if prepared.contains?(pt)
390
+ inside += 1
391
+ else
392
+ outside += 1
393
+ end
394
+ end
395
+
396
+ puts " Polygon: 100 vertices, radius ~0.05 degrees"
397
+ puts " Test points: #{test_points.length}"
398
+ puts " Inside: #{inside}"
399
+ puts " Outside: #{outside}"
400
+ puts
401
+ puts " PreparedGeometry also supports intersects?:"
402
+ seg = Segment.new(
403
+ LLA.new(lat: 40.70, lng: -74.00, alt: 0),
404
+ LLA.new(lat: 40.80, lng: -73.98, alt: 0)
405
+ )
406
+ puts " prepared.intersects?(segment) => #{prepared.intersects?(seg)}"
407
+
408
+ prepared.release
409
+ puts " prepared.release # free GEOS memory"
410
+
411
+ # ── 10. Chaining Operations ──────────────────────────────────────────
412
+
413
+ section "10. CHAINING OPERATIONS"
414
+
415
+ puts <<~HEREDOC
416
+
417
+ GEOS operations return Geodetic objects, so results chain naturally
418
+ with other GEOS calls or with Geodetic's Ruby methods.
419
+ HEREDOC
420
+
421
+ puts " Workflow: intersect two polygons, buffer the result, compute hull"
422
+ puts
423
+
424
+ # Step 1: Intersection
425
+ result = Geos.intersection(lower, midtown)
426
+ puts " 1. overlap = Geos.intersection(lower, midtown)"
427
+ puts " => #{fmt_polygon(result)}"
428
+
429
+ # Step 2: Buffer the intersection
430
+ if result.is_a?(Polygon)
431
+ buffered_overlap = Geos.buffer(result, 0.005)
432
+ puts " 2. expanded = Geos.buffer(overlap, 0.005)"
433
+ puts " => #{fmt_polygon(buffered_overlap)}"
434
+
435
+ # Step 3: Convex hull of the buffered result
436
+ hull2 = Geos.convex_hull(buffered_overlap)
437
+ puts " 3. hull = Geos.convex_hull(expanded)"
438
+ puts " => #{fmt_polygon(hull2)}"
439
+
440
+ # Step 4: Compute area
441
+ area = Geos.area(hull2)
442
+ puts " 4. Geos.area(hull) => #{area.round(8)} sq degrees"
443
+
444
+ # Step 5: Validate
445
+ puts " 5. Geos.is_valid?(hull) => #{Geos.is_valid?(hull2)}"
446
+
447
+ # Step 6: Use Geodetic methods on the result
448
+ if hull2.is_a?(Polygon)
449
+ puts " 6. hull.centroid => (#{hull2.centroid.lat.round(4)}, #{hull2.centroid.lng.round(4)})"
450
+ puts " hull.includes?(Empire State) => #{hull2.includes?(point)}"
451
+ end
452
+ end
453
+
454
+ # ── 11. GeoJSON Export ────────────────────────────────────────────────
455
+
456
+ section "11. GEOJSON EXPORT OF GEOS RESULTS"
457
+
458
+ puts <<~HEREDOC
459
+
460
+ GEOS results are standard Geodetic objects, so they work with
461
+ GeoJSON export, WKT, WKB, and all other serialization.
462
+ HEREDOC
463
+
464
+ gj = GeoJSON.new
465
+ gj << Feature.new(label: "Lower Manhattan", geometry: lower,
466
+ metadata: { role: "input" })
467
+ gj << Feature.new(label: "Midtown", geometry: midtown,
468
+ metadata: { role: "input" })
469
+ if result.is_a?(Polygon)
470
+ gj << Feature.new(label: "Overlap", geometry: result,
471
+ metadata: { role: "intersection" })
472
+ end
473
+ hull_result = Geos.convex_hull(landmark_path)
474
+ if hull_result.is_a?(Polygon)
475
+ gj << Feature.new(label: "Landmark Hull", geometry: hull_result,
476
+ metadata: { role: "convex_hull" })
477
+ end
478
+
479
+ puts " GeoJSON FeatureCollection with #{gj.size} features:"
480
+ gj.each do |obj|
481
+ if obj.is_a?(Feature)
482
+ puts " - #{obj.label} (#{obj.metadata[:role]})"
483
+ end
484
+ end
485
+ puts
486
+ puts " gj.to_json => #{gj.to_json.length} characters"
487
+
488
+ puts
489
+ puts " WKT of the intersection:"
490
+ if result.is_a?(Polygon)
491
+ wkt = result.to_wkt(precision: 4)
492
+ puts " #{wkt}"
493
+ end
494
+
495
+ # ── Summary ───────────────────────────────────────────────────────────
496
+
497
+ section "SUMMARY"
498
+
499
+ puts <<~HEREDOC
500
+
501
+ GEOS-only operations demonstrated:
502
+
503
+ Boolean operations:
504
+ Geos.intersection(a, b) Area of overlap
505
+ Geos.difference(a, b) A minus B
506
+ Geos.symmetric_difference(a, b) XOR of two geometries
507
+ Geos.union(a) Dissolve internal boundaries
508
+
509
+ Geometry construction:
510
+ Geos.buffer(geom, distance) Buffer zone around any geometry
511
+ Geos.buffer_with_style(...) Buffer with cap/join style control
512
+ Geos.convex_hull(geom) Smallest enclosing convex polygon
513
+ Geos.simplify(geom, tolerance) Douglas-Peucker vertex reduction
514
+
515
+ Validation and repair:
516
+ Geos.is_valid?(geom) OGC validity check
517
+ Geos.is_valid_reason(geom) Human-readable invalidity reason
518
+ Geos.make_valid(geom) Repair invalid geometries
519
+
520
+ Measurements:
521
+ Geos.area(geom) Planar area (sq degrees for LLA)
522
+ Geos.length(geom) Planar perimeter/length
523
+ Geos.distance(a, b) Minimum planar distance
524
+ Geos.nearest_points(a, b) Closest point pair [LLA, LLA]
525
+
526
+ Batch spatial indexing:
527
+ Geos.prepare(polygon) Build spatial index
528
+ prepared.contains?(point) O(log n) containment
529
+ prepared.intersects?(geom) O(log n) intersection
530
+ prepared.release Free GEOS memory
531
+
532
+ All results are standard Geodetic objects — they work with to_wkt,
533
+ to_wkb, to_geojson, distance_to, bearing_to, includes?, and every
534
+ other Geodetic method.
535
+
536
+ Install GEOS: brew install geos
537
+ See also: ruby -Ilib examples/12_geos_benchmark.rb
538
+ HEREDOC
data/examples/README.md CHANGED
@@ -131,3 +131,85 @@ Demonstrates the `Geodetic::GeoJSON` class for building and exporting GeoJSON Fe
131
131
  - **Delete and clear** removing individual objects or emptying the collection
132
132
  - **Enumerable** iteration over collected objects
133
133
  - **Export** via `to_h` (Ruby Hash), `to_json`/`to_json(pretty: true)` (JSON string), and `save(path, pretty:)` (file output)
134
+
135
+ ## 10 - WKT Serialization
136
+
137
+ Demonstrates `Geodetic::WKT` for Well-Known Text export and import, the standard geometry format used by PostGIS, RGeo, and most GIS tools. Covers:
138
+
139
+ - **Coordinate → POINT** with `to_wkt` on any coordinate system, including altitude (Z suffix)
140
+ - **Segment → LINESTRING** exporting two-point directed segments
141
+ - **Path → LINESTRING** and optional `to_wkt(as: :polygon)` for closed paths
142
+ - **Areas → POLYGON** for `Polygon`, `Circle` (N-gon approximation with configurable `segments:`), and `BoundingBox`
143
+ - **Feature → delegates to geometry** (WKT has no properties concept)
144
+ - **SRID / EWKT** with `to_wkt(srid: 4326)` producing PostGIS-compatible Extended WKT
145
+ - **Z-dimension consistency** where any non-zero altitude triggers Z on all points in the geometry
146
+ - **Parsing** with `WKT.parse` (returns Geodetic objects) and `WKT.parse_with_srid` (returns object + SRID)
147
+ - **Roundtrip** verification showing export → parse → re-export produces identical WKT strings
148
+ - **File I/O** with `WKT.save` writing one WKT per line and `WKT.load` reading them back, including SRID support and full file roundtrip verification
149
+
150
+ ## 11 - WKB Serialization
151
+
152
+ Demonstrates `Geodetic::WKB` for Well-Known Binary export and import, the binary counterpart to WKT used by PostGIS, GEOS, RGeo, and Shapely for efficient geometry storage. Covers:
153
+
154
+ - **Coordinate → POINT** with `to_wkb` and `to_wkb_hex` on any coordinate system, including altitude (Z type code)
155
+ - **Segment → LINESTRING** exporting two-point directed segments
156
+ - **Path → LINESTRING** and optional `to_wkb(as: :polygon)` for closed paths
157
+ - **Areas → POLYGON** for `Polygon`, `Circle` (N-gon approximation with configurable `segments:`), and `BoundingBox`
158
+ - **Feature → delegates to geometry** (WKB has no properties concept)
159
+ - **SRID / EWKB** with `to_wkb_hex(srid: 4326)` producing PostGIS-compatible Extended WKB
160
+ - **Z-dimension consistency** where any non-zero altitude triggers Z on all points in the geometry
161
+ - **Parsing** with `WKB.parse` (auto-detects binary vs hex) and `WKB.parse_with_srid` (returns object + SRID)
162
+ - **Roundtrip** verification showing export → parse → re-export produces identical hex strings
163
+ - **File I/O** with `WKB.save!`/`WKB.load` for binary format and `WKB.save_hex!`/`WKB.load_hex` for hex format, including fixture file loading and full roundtrip verification
164
+
165
+ ## 12 - GEOS Benchmark
166
+
167
+ Benchmarks pure Ruby spatial operations against GEOS-accelerated versions. Requires `libgeos_c` installed (`brew install geos` on macOS). Uses the `GEODETIC_GEOS_DISABLE` environment variable to toggle between Ruby and GEOS within the same run. Covers:
168
+
169
+ - **Polygon validation** comparing Ruby's O(n^2) pairwise segment test against GEOS's O(n log n) spatial index at 50, 100, and 500 vertices
170
+ - **Point-in-polygon** comparing Ruby's winding-number algorithm against GEOS's contains test, showing the crossover threshold at 15 vertices
171
+ - **Path intersection** comparing Ruby's O(n*m) brute-force segment checks against GEOS's spatial indexing for non-intersecting paths with overlapping bounds
172
+ - **Batch containment** with `PreparedGeometry` testing 1,000 random points against a 100-vertex polygon, comparing Ruby, GEOS one-shot, and GEOS prepared (spatial index built once)
173
+ - **Single segment intersection** demonstrating where Ruby wins due to FFI overhead exceeding the computation cost
174
+ - **GEOS-only operations** benchmarking `intersection`, `difference`, `symmetric_difference`, `convex_hull`, `simplify`, `is_valid?`, `area`, and `nearest_points` — capabilities with no pure Ruby equivalent
175
+ - **Summary table** with side-by-side timings and speedup ratios
176
+
177
+ Prerequisites:
178
+
179
+ ```bash
180
+ brew install geos # macOS
181
+ ```
182
+
183
+ Run:
184
+
185
+ ```bash
186
+ ruby -Ilib examples/12_geos_benchmark.rb
187
+ ```
188
+
189
+ ## 13 - GEOS Operations
190
+
191
+ Demonstrates GEOS-only operations — capabilities provided by the GEOS C library that have no pure Ruby equivalent in Geodetic. Uses two overlapping rectangles around Manhattan as the primary test geometries. Covers:
192
+
193
+ - **Boolean operations** with `intersection` (area of overlap), `difference` (A minus B), `symmetric_difference` (XOR), and `union` (dissolve internal boundaries)
194
+ - **Buffering** with `buffer` (create polygon zones around points and paths) and `buffer_with_style` (flat caps, mitre joins)
195
+ - **Convex hull** computing the smallest convex polygon enclosing a set of NYC landmarks
196
+ - **Simplification** using the Douglas-Peucker algorithm at multiple tolerance levels (50 vertices reduced to 16, 8, or 4)
197
+ - **Validity checking** with `is_valid?` and `is_valid_reason` on valid and self-intersecting (bowtie) polygons
198
+ - **Geometry repair** with `make_valid` splitting a self-intersecting bowtie into two valid triangles
199
+ - **Planar measurements** with `area`, `length`, `distance` in coordinate units
200
+ - **Nearest points** finding the closest point pair between two separated polygons
201
+ - **PreparedGeometry** batch spatial indexing for O(log n) `contains?` and `intersects?` queries
202
+ - **Chaining operations** combining GEOS results with other GEOS calls and Geodetic methods
203
+ - **Serialization** exporting GEOS results to GeoJSON and WKT
204
+
205
+ Prerequisites:
206
+
207
+ ```bash
208
+ brew install geos # macOS
209
+ ```
210
+
211
+ Run:
212
+
213
+ ```bash
214
+ ruby -Ilib examples/13_geos_operations.rb
215
+ ```
Binary file
@@ -0,0 +1,5 @@
1
+ POINT(-122.3493 47.6205)
2
+ POINT Z(-122.3493 47.6205 184.0)
3
+ LINESTRING(-122.3493 47.6205, -122.6784 45.5152)
4
+ POLYGON((-122.5 47.7, -122.1 47.5, -122.4 47.3, -122.5 47.7))
5
+ POLYGON((-123.0 48.0, -121.0 48.0, -121.0 46.0, -123.0 46.0, -123.0 48.0))
@@ -0,0 +1,5 @@
1
+ 01010000008a1f63ee5a965ec08195438b6ccf4740
2
+ 01e90300008a1f63ee5a965ec08195438b6ccf47400000000000006740
3
+ 0102000000020000008a1f63ee5a965ec08195438b6ccf4740cf66d5e76aab5ec01973d712f2c14640
4
+ 010300000001000000040000000000000000a05ec09a99999999d947406666666666865ec00000000000c047409a99999999995ec06666666666a647400000000000a05ec09a99999999d94740
5
+ 010300000001000000050000000000000000c05ec000000000000048400000000000405ec000000000000048400000000000405ec000000000000047400000000000c05ec000000000000047400000000000c05ec00000000000004840
Binary file