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 +4 -4
- data/CHANGELOG.md +75 -0
- data/README.md +94 -0
- data/docs/index.md +4 -0
- data/docs/reference/geos-acceleration.md +170 -0
- data/docs/reference/wkb.md +385 -0
- data/docs/reference/wkt.md +354 -0
- data/examples/10_wkt_serialization.rb +248 -0
- data/examples/11_wkb_serialization.rb +261 -0
- data/examples/12_geos_benchmark.rb +258 -0
- data/examples/13_geos_operations.rb +538 -0
- data/examples/README.md +82 -0
- data/examples/geodetic_demo.wkb +0 -0
- data/examples/geodetic_demo.wkt +5 -0
- data/examples/geodetic_demo_output.wkb.hex +5 -0
- data/examples/sample_geometries.wkb +0 -0
- data/examples/sample_geometries.wkb.hex +60 -0
- 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/wkb.rb +360 -0
- data/lib/geodetic/wkt.rb +313 -0
- data/lib/geodetic.rb +3 -0
- data/mkdocs.yml +2 -0
- metadata +16 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# WKT Serialization Reference
|
|
2
|
+
|
|
3
|
+
`Geodetic::WKT` provides [Well-Known Text](https://en.wikipedia.org/wiki/Well-known_text_representation_of_geometry) (WKT) serialization for all Geodetic geometry types. WKT is the standard text format used by PostGIS, RGeo, Shapely, JTS, GEOS, and most GIS tools.
|
|
4
|
+
|
|
5
|
+
Every geometry type gains a `to_wkt` instance method. The module also provides `WKT.parse` and `WKT.parse_with_srid` for importing WKT strings back into Geodetic objects.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Export
|
|
10
|
+
|
|
11
|
+
### Coordinates → POINT
|
|
12
|
+
|
|
13
|
+
All 18 coordinate classes gain a `to_wkt` method. It converts the coordinate to LLA and returns a WKT POINT string.
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
|
|
17
|
+
seattle.to_wkt
|
|
18
|
+
# => "POINT(-122.3493 47.6205)"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Altitude handling:** When altitude is non-zero, the Z suffix is added:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 184.0).to_wkt
|
|
25
|
+
# => "POINT Z(-122.35 47.62 184.0)"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**Cross-system:** Any coordinate type works — UTM, ECEF, MGRS, etc. are converted to LLA internally.
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
seattle.to_utm.to_wkt # => "POINT(-122.3493 47.6205)"
|
|
32
|
+
seattle.to_ecef.to_wkt # => "POINT(-122.3493 47.6205)"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**ENU and NED:** These are relative coordinate systems. Calling `to_wkt` raises `ArgumentError`.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
enu = Geodetic::Coordinate::ENU.new(e: 100, n: 200, u: 10)
|
|
39
|
+
enu.to_wkt # => ArgumentError
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Options
|
|
43
|
+
|
|
44
|
+
All `to_wkt` methods accept these keyword arguments:
|
|
45
|
+
|
|
46
|
+
| Parameter | Default | Description |
|
|
47
|
+
|-----------|---------|-------------|
|
|
48
|
+
| `precision:` | `6` | Number of decimal places for coordinates |
|
|
49
|
+
| `srid:` | `nil` | When set, prepends `SRID=N;` (EWKT format) |
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
seattle.to_wkt(precision: 2)
|
|
53
|
+
# => "POINT(-122.35 47.62)"
|
|
54
|
+
|
|
55
|
+
seattle.to_wkt(srid: 4326)
|
|
56
|
+
# => "SRID=4326;POINT(-122.3493 47.6205)"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
### Segment → LINESTRING
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
seg = Geodetic::Segment.new(seattle, portland)
|
|
65
|
+
seg.to_wkt
|
|
66
|
+
# => "LINESTRING(-122.3493 47.6205, -122.6784 45.5152)"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### Path → LINESTRING / POLYGON
|
|
72
|
+
|
|
73
|
+
Returns a LINESTRING by default. Pass `as: :polygon` for a closed POLYGON.
|
|
74
|
+
|
|
75
|
+
```ruby
|
|
76
|
+
route = Geodetic::Path.new(coordinates: [seattle, portland, sf])
|
|
77
|
+
route.to_wkt
|
|
78
|
+
# => "LINESTRING(-122.3493 47.6205, -122.6784 45.5152, -122.4194 37.7749)"
|
|
79
|
+
|
|
80
|
+
route.to_wkt(as: :polygon)
|
|
81
|
+
# => "POLYGON((-122.3493 47.6205, -122.6784 45.5152, -122.4194 37.7749, -122.3493 47.6205))"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Edge cases:**
|
|
85
|
+
|
|
86
|
+
| Condition | Behavior |
|
|
87
|
+
|-----------|----------|
|
|
88
|
+
| Empty path | Raises `ArgumentError` |
|
|
89
|
+
| 1 coordinate (line_string) | Raises `ArgumentError` |
|
|
90
|
+
| 2 coordinates (polygon) | Raises `ArgumentError` |
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
### Areas::Polygon → POLYGON
|
|
95
|
+
|
|
96
|
+
```ruby
|
|
97
|
+
poly = Geodetic::Areas::Polygon.new(boundary: [a, b, c])
|
|
98
|
+
poly.to_wkt
|
|
99
|
+
# => "POLYGON((-122.5 47.7, -122.1 47.5, -122.4 47.3, -122.5 47.7))"
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
All Polygon subclasses (`Triangle`, `Rectangle`, `Pentagon`, `Hexagon`, `Octagon`) inherit this method.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
### Areas::Circle → POLYGON
|
|
107
|
+
|
|
108
|
+
Approximated as a regular N-gon. Default is 32 segments.
|
|
109
|
+
|
|
110
|
+
```ruby
|
|
111
|
+
circle = Geodetic::Areas::Circle.new(centroid: seattle, radius: 10_000)
|
|
112
|
+
|
|
113
|
+
circle.to_wkt # => 32-gon
|
|
114
|
+
circle.to_wkt(segments: 64) # => 64-gon
|
|
115
|
+
circle.to_wkt(segments: 8) # => 8-gon
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### Areas::BoundingBox → POLYGON
|
|
121
|
+
|
|
122
|
+
5 positions (4 corners + closing point).
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
bbox = Geodetic::Areas::BoundingBox.new(
|
|
126
|
+
nw: Geodetic::Coordinate::LLA.new(lat: 48.0, lng: -123.0, alt: 0),
|
|
127
|
+
se: Geodetic::Coordinate::LLA.new(lat: 46.0, lng: -121.0, alt: 0)
|
|
128
|
+
)
|
|
129
|
+
bbox.to_wkt
|
|
130
|
+
# => "POLYGON((-123.0 48.0, -121.0 48.0, -121.0 46.0, -123.0 46.0, -123.0 48.0))"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Feature → delegates to geometry
|
|
136
|
+
|
|
137
|
+
WKT has no concept of properties or labels. `Feature#to_wkt` delegates directly to the underlying geometry.
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
f = Geodetic::Feature.new(label: "Seattle", geometry: seattle, metadata: { pop: 750_000 })
|
|
141
|
+
f.to_wkt
|
|
142
|
+
# => "POINT(-122.3493 47.6205)"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Z-Dimension Consistency
|
|
148
|
+
|
|
149
|
+
When any point in a geometry has non-zero altitude, **all** points in that geometry use the Z suffix. This follows the OGC rule that Z-dimensionality is uniform within a geometry.
|
|
150
|
+
|
|
151
|
+
```ruby
|
|
152
|
+
# seattle has alt=0, sf has alt=100
|
|
153
|
+
path = Geodetic::Path.new(coordinates: [seattle, sf])
|
|
154
|
+
path.to_wkt
|
|
155
|
+
# => "LINESTRING Z(-122.3493 47.6205 0.0, -122.4194 37.7749 100.0)"
|
|
156
|
+
|
|
157
|
+
# Both at alt=0 → no Z
|
|
158
|
+
path2 = Geodetic::Path.new(coordinates: [seattle, portland])
|
|
159
|
+
path2.to_wkt
|
|
160
|
+
# => "LINESTRING(-122.3493 47.6205, -122.6784 45.5152)"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## SRID / EWKT
|
|
166
|
+
|
|
167
|
+
Pass `srid:` to any `to_wkt` call to produce Extended WKT (EWKT), the format used by PostGIS:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
seattle.to_wkt(srid: 4326)
|
|
171
|
+
# => "SRID=4326;POINT(-122.3493 47.6205)"
|
|
172
|
+
|
|
173
|
+
seg.to_wkt(srid: 4326)
|
|
174
|
+
# => "SRID=4326;LINESTRING(-122.3493 47.6205, -122.6784 45.5152)"
|
|
175
|
+
|
|
176
|
+
poly.to_wkt(srid: 4326)
|
|
177
|
+
# => "SRID=4326;POLYGON((...))
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Common SRIDs:
|
|
181
|
+
|
|
182
|
+
| SRID | Description |
|
|
183
|
+
|------|-------------|
|
|
184
|
+
| 4326 | WGS84 geographic (lat/lng) |
|
|
185
|
+
| 3857 | Web Mercator |
|
|
186
|
+
| 32610 | UTM Zone 10N |
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## File I/O
|
|
191
|
+
|
|
192
|
+
### `WKT.save!(path, *objects, srid: nil, precision: 6)`
|
|
193
|
+
|
|
194
|
+
Write one WKT string per line. Accepts individual objects or an array.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
Geodetic::WKT.save!("shapes.wkt", seattle, segment, polygon)
|
|
198
|
+
Geodetic::WKT.save!("shapes.wkt", [seattle, segment, polygon])
|
|
199
|
+
Geodetic::WKT.save!("shapes.wkt", seattle, polygon, srid: 4326, precision: 2)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Output file (one geometry per line):
|
|
203
|
+
|
|
204
|
+
```
|
|
205
|
+
POINT(-122.3493 47.6205)
|
|
206
|
+
LINESTRING(-122.3493 47.6205, -122.6784 45.5152)
|
|
207
|
+
POLYGON((-122.0 47.0, -121.0 46.0, -123.0 46.0, -122.0 47.0))
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### `WKT.load(path)`
|
|
211
|
+
|
|
212
|
+
Read a WKT file and return an Array of Geodetic objects. Blank lines are skipped.
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
objects = Geodetic::WKT.load("shapes.wkt")
|
|
216
|
+
# => [Coordinate::LLA, Segment, Areas::Polygon]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Roundtrip:**
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
Geodetic::WKT.save!("data.wkt", seattle, portland, polygon, srid: 4326)
|
|
223
|
+
objects = Geodetic::WKT.load("data.wkt")
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Import
|
|
229
|
+
|
|
230
|
+
### `WKT.parse(string)`
|
|
231
|
+
|
|
232
|
+
Parse a WKT string and return a Geodetic object (or Array for Multi*/GeometryCollection types).
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
Geodetic::WKT.parse("POINT(-122.3493 47.6205)")
|
|
236
|
+
# => Coordinate::LLA
|
|
237
|
+
|
|
238
|
+
Geodetic::WKT.parse("LINESTRING(-122.35 47.62, -122.68 45.52)")
|
|
239
|
+
# => Segment (2 points) or Path (3+ points)
|
|
240
|
+
|
|
241
|
+
Geodetic::WKT.parse("POLYGON((-122.0 47.0, -121.0 46.0, -123.0 46.0, -122.0 47.0))")
|
|
242
|
+
# => Areas::Polygon
|
|
243
|
+
|
|
244
|
+
Geodetic::WKT.parse("MULTIPOINT((-122.35 47.62), (-122.68 45.52))")
|
|
245
|
+
# => [LLA, LLA]
|
|
246
|
+
|
|
247
|
+
Geodetic::WKT.parse("GEOMETRYCOLLECTION(POINT(-122.35 47.62), LINESTRING(...))")
|
|
248
|
+
# => [LLA, Segment]
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
SRID prefixes are silently stripped:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
Geodetic::WKT.parse("SRID=4326;POINT(-122.35 47.62)")
|
|
255
|
+
# => Coordinate::LLA (SRID discarded)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `WKT.parse_with_srid(string)`
|
|
259
|
+
|
|
260
|
+
Parse a WKT/EWKT string and return both the object and the SRID:
|
|
261
|
+
|
|
262
|
+
```ruby
|
|
263
|
+
obj, srid = Geodetic::WKT.parse_with_srid("SRID=4326;POINT(-122.3493 47.6205)")
|
|
264
|
+
obj # => Coordinate::LLA
|
|
265
|
+
srid # => 4326
|
|
266
|
+
|
|
267
|
+
obj, srid = Geodetic::WKT.parse_with_srid("POINT(-122.3493 47.6205)")
|
|
268
|
+
srid # => nil
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## WKT → Geodetic Type Mapping
|
|
274
|
+
|
|
275
|
+
| WKT Type | Geodetic Type |
|
|
276
|
+
|----------|---------------|
|
|
277
|
+
| POINT | `Coordinate::LLA` |
|
|
278
|
+
| LINESTRING (2 points) | `Segment` |
|
|
279
|
+
| LINESTRING (3+ points) | `Path` |
|
|
280
|
+
| POLYGON | `Areas::Polygon` (outer ring only; holes are dropped) |
|
|
281
|
+
| MULTIPOINT | Array of `Coordinate::LLA` |
|
|
282
|
+
| MULTILINESTRING | Array of `Segment` or `Path` |
|
|
283
|
+
| MULTIPOLYGON | Array of `Areas::Polygon` |
|
|
284
|
+
| GEOMETRYCOLLECTION | Array of mixed types |
|
|
285
|
+
|
|
286
|
+
All types support the Z suffix for 3D coordinates.
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Geometry Mapping (Export)
|
|
291
|
+
|
|
292
|
+
| Geodetic Type | WKT Type | Notes |
|
|
293
|
+
|---------------|----------|-------|
|
|
294
|
+
| Any coordinate (LLA, UTM, ECEF, ...) | POINT | Converts through LLA |
|
|
295
|
+
| `Segment` | LINESTRING | 2 positions |
|
|
296
|
+
| `Path` | LINESTRING | N positions (default) |
|
|
297
|
+
| `Path` (with `as: :polygon`) | POLYGON | Auto-closes the ring |
|
|
298
|
+
| `Areas::Polygon` (and subclasses) | POLYGON | Boundary ring already closed |
|
|
299
|
+
| `Areas::Circle` | POLYGON | Approximated as N-gon (default 32) |
|
|
300
|
+
| `Areas::BoundingBox` | POLYGON | 4 corners, closed |
|
|
301
|
+
| `Feature` | Delegates to geometry | Properties are lost |
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Roundtrip Example
|
|
306
|
+
|
|
307
|
+
```ruby
|
|
308
|
+
require "geodetic"
|
|
309
|
+
|
|
310
|
+
seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0)
|
|
311
|
+
|
|
312
|
+
# Export
|
|
313
|
+
wkt = seattle.to_wkt(srid: 4326)
|
|
314
|
+
# => "SRID=4326;POINT(-122.3493 47.6205)"
|
|
315
|
+
|
|
316
|
+
# Import
|
|
317
|
+
obj, srid = Geodetic::WKT.parse_with_srid(wkt)
|
|
318
|
+
obj.lat # => 47.6205
|
|
319
|
+
obj.lng # => -122.3493
|
|
320
|
+
srid # => 4326
|
|
321
|
+
|
|
322
|
+
# Re-export
|
|
323
|
+
obj.to_wkt(srid: srid) == wkt # => true
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## WKT Specification Notes
|
|
329
|
+
|
|
330
|
+
- **Coordinate order** is `longitude latitude` (same as GeoJSON), per OGC Simple Features.
|
|
331
|
+
- **Z suffix** indicates 3D coordinates: `POINT Z(lng lat alt)`.
|
|
332
|
+
- **Polygon rings** are closed (first point = last point).
|
|
333
|
+
- **EWKT** (Extended WKT) prepends `SRID=N;` — this is a PostGIS extension, not part of the OGC standard.
|
|
334
|
+
- **No properties** — unlike GeoJSON, WKT is geometry-only. Feature labels and metadata are not preserved.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Integration with PostGIS and RGeo
|
|
339
|
+
|
|
340
|
+
WKT is the lingua franca for exchanging geometry between Ruby and spatial databases:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Writing to PostGIS
|
|
344
|
+
wkt = polygon.to_wkt(srid: 4326)
|
|
345
|
+
ActiveRecord::Base.connection.execute(
|
|
346
|
+
"INSERT INTO regions (geom) VALUES (ST_GeomFromEWKT('#{wkt}'))"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Reading from PostGIS
|
|
350
|
+
row = ActiveRecord::Base.connection.select_one("SELECT ST_AsEWKT(geom) FROM regions LIMIT 1")
|
|
351
|
+
obj, srid = Geodetic::WKT.parse_with_srid(row["st_asewkt"])
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
WKT strings are also accepted by RGeo's `WKRep::WKTParser`, Shapely's `loads()`, JTS, GEOS, and virtually every GIS library.
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstration of WKT (Well-Known Text) Serialization
|
|
4
|
+
# Shows export, import, roundtrip, SRID/EWKT, and Z-dimension handling.
|
|
5
|
+
|
|
6
|
+
require_relative "../lib/geodetic"
|
|
7
|
+
|
|
8
|
+
include Geodetic
|
|
9
|
+
|
|
10
|
+
LLA = Coordinate::LLA
|
|
11
|
+
Distance = Geodetic::Distance
|
|
12
|
+
|
|
13
|
+
puts "=== WKT Serialization Demo ==="
|
|
14
|
+
puts
|
|
15
|
+
|
|
16
|
+
# ── 1. Coordinate → POINT ──────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
puts "--- 1. Coordinate → POINT ---"
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
seattle = LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
|
|
22
|
+
puts " seattle.to_wkt"
|
|
23
|
+
puts " => #{seattle.to_wkt}"
|
|
24
|
+
puts
|
|
25
|
+
|
|
26
|
+
# Works from any coordinate system — converts through LLA
|
|
27
|
+
utm = seattle.to_utm
|
|
28
|
+
puts " seattle.to_utm.to_wkt"
|
|
29
|
+
puts " => #{utm.to_wkt}"
|
|
30
|
+
puts
|
|
31
|
+
|
|
32
|
+
# Altitude triggers Z suffix
|
|
33
|
+
space_needle = LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
|
|
34
|
+
puts " With altitude:"
|
|
35
|
+
puts " => #{space_needle.to_wkt}"
|
|
36
|
+
puts
|
|
37
|
+
|
|
38
|
+
# Custom precision
|
|
39
|
+
puts " Custom precision (2):"
|
|
40
|
+
puts " => #{seattle.to_wkt(precision: 2)}"
|
|
41
|
+
puts
|
|
42
|
+
|
|
43
|
+
# ── 2. Segment → LINESTRING ────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
puts "--- 2. Segment → LINESTRING ---"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
portland = LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
|
|
49
|
+
seg = Segment.new(seattle, portland)
|
|
50
|
+
puts " #{seg.to_wkt}"
|
|
51
|
+
puts
|
|
52
|
+
|
|
53
|
+
# ── 3. Path → LINESTRING or POLYGON ────────────────────────────────
|
|
54
|
+
|
|
55
|
+
puts "--- 3. Path → LINESTRING / POLYGON ---"
|
|
56
|
+
puts
|
|
57
|
+
|
|
58
|
+
sf = LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
|
|
59
|
+
la = LLA.new(lat: 34.0522, lng: -118.2437, alt: 0.0)
|
|
60
|
+
route = Path.new(coordinates: [seattle, portland, sf, la])
|
|
61
|
+
puts " Path as LINESTRING:"
|
|
62
|
+
puts " #{route.to_wkt}"
|
|
63
|
+
puts
|
|
64
|
+
|
|
65
|
+
triangle_path = Path.new(coordinates: [
|
|
66
|
+
LLA.new(lat: 47.0, lng: -122.5, alt: 0),
|
|
67
|
+
LLA.new(lat: 46.0, lng: -121.0, alt: 0),
|
|
68
|
+
LLA.new(lat: 46.0, lng: -123.0, alt: 0)
|
|
69
|
+
])
|
|
70
|
+
puts " Path as POLYGON:"
|
|
71
|
+
puts " #{triangle_path.to_wkt(as: :polygon)}"
|
|
72
|
+
puts
|
|
73
|
+
|
|
74
|
+
# ── 4. Areas → POLYGON ─────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
puts "--- 4. Areas → POLYGON ---"
|
|
77
|
+
puts
|
|
78
|
+
|
|
79
|
+
a = LLA.new(lat: 47.7, lng: -122.5, alt: 0)
|
|
80
|
+
b = LLA.new(lat: 47.5, lng: -122.1, alt: 0)
|
|
81
|
+
c = LLA.new(lat: 47.3, lng: -122.4, alt: 0)
|
|
82
|
+
poly = Areas::Polygon.new(boundary: [a, b, c])
|
|
83
|
+
puts " Polygon: #{poly.to_wkt(precision: 1)}"
|
|
84
|
+
|
|
85
|
+
circle = Areas::Circle.new(centroid: seattle, radius: 10_000)
|
|
86
|
+
circle_wkt = circle.to_wkt(segments: 8, precision: 4)
|
|
87
|
+
puts " Circle (8-gon): #{circle_wkt[0..60]}..."
|
|
88
|
+
|
|
89
|
+
bbox = Areas::BoundingBox.new(
|
|
90
|
+
nw: LLA.new(lat: 48.0, lng: -123.0, alt: 0),
|
|
91
|
+
se: LLA.new(lat: 46.0, lng: -121.0, alt: 0)
|
|
92
|
+
)
|
|
93
|
+
puts " BoundingBox: #{bbox.to_wkt(precision: 1)}"
|
|
94
|
+
puts
|
|
95
|
+
|
|
96
|
+
# ── 5. Feature delegates to geometry ───────────────────────────────
|
|
97
|
+
|
|
98
|
+
puts "--- 5. Feature → delegates to geometry ---"
|
|
99
|
+
puts
|
|
100
|
+
|
|
101
|
+
city = Feature.new(label: "Seattle", geometry: seattle, metadata: { pop: 750_000 })
|
|
102
|
+
puts " Feature('Seattle').to_wkt"
|
|
103
|
+
puts " => #{city.to_wkt}"
|
|
104
|
+
puts " (WKT has no properties; label/metadata are lost)"
|
|
105
|
+
puts
|
|
106
|
+
|
|
107
|
+
# ── 6. SRID / EWKT ─────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
puts "--- 6. SRID / EWKT ---"
|
|
110
|
+
puts
|
|
111
|
+
|
|
112
|
+
puts " POINT with SRID:"
|
|
113
|
+
puts " => #{seattle.to_wkt(srid: 4326)}"
|
|
114
|
+
|
|
115
|
+
puts " LINESTRING with SRID:"
|
|
116
|
+
puts " => #{seg.to_wkt(srid: 4326)}"
|
|
117
|
+
|
|
118
|
+
puts " POLYGON with SRID:"
|
|
119
|
+
puts " => #{poly.to_wkt(srid: 4326, precision: 1)}"
|
|
120
|
+
puts
|
|
121
|
+
|
|
122
|
+
# ── 7. Z-dimension consistency ──────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
puts "--- 7. Z-dimension consistency ---"
|
|
125
|
+
puts
|
|
126
|
+
|
|
127
|
+
# When ANY point has altitude, ALL points get Z
|
|
128
|
+
mixed = Path.new(coordinates: [seattle, space_needle])
|
|
129
|
+
puts " seattle (alt=0) + space_needle (alt=184):"
|
|
130
|
+
puts " => #{mixed.to_wkt}"
|
|
131
|
+
puts " (Both coordinates get Z even though seattle has alt=0)"
|
|
132
|
+
puts
|
|
133
|
+
|
|
134
|
+
no_alt = Path.new(coordinates: [seattle, portland])
|
|
135
|
+
puts " seattle + portland (both alt=0):"
|
|
136
|
+
puts " => #{no_alt.to_wkt}"
|
|
137
|
+
puts " (No Z suffix when all altitudes are zero)"
|
|
138
|
+
puts
|
|
139
|
+
|
|
140
|
+
# ── 8. Parsing WKT ─────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
puts "--- 8. Parsing WKT ---"
|
|
143
|
+
puts
|
|
144
|
+
|
|
145
|
+
wkt_strings = [
|
|
146
|
+
"POINT(-122.3493 47.6205)",
|
|
147
|
+
"POINT Z(-122.3493 47.6205 184.0)",
|
|
148
|
+
"LINESTRING(-122.3493 47.6205, -122.6784 45.5152)",
|
|
149
|
+
"POLYGON((-122.0 47.0, -121.0 46.0, -123.0 46.0, -122.0 47.0))",
|
|
150
|
+
"MULTIPOINT((-122.35 47.62), (-122.68 45.52))",
|
|
151
|
+
"GEOMETRYCOLLECTION(POINT(-122.35 47.62), LINESTRING(-122.35 47.62, -122.68 45.52))"
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
wkt_strings.each do |wkt|
|
|
155
|
+
result = WKT.parse(wkt)
|
|
156
|
+
type = wkt[/\A\w+/]
|
|
157
|
+
if result.is_a?(Array)
|
|
158
|
+
puts " #{type} → #{result.map { |r| r.class.name.split('::').last }.join(', ')}"
|
|
159
|
+
else
|
|
160
|
+
puts " #{type} → #{result.class.name.split('::').last}"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
puts
|
|
164
|
+
|
|
165
|
+
# Parsing EWKT with SRID
|
|
166
|
+
ewkt = "SRID=4326;POINT(-122.3493 47.6205)"
|
|
167
|
+
obj, srid = WKT.parse_with_srid(ewkt)
|
|
168
|
+
puts " EWKT: #{ewkt}"
|
|
169
|
+
puts " → object: #{obj.class.name.split('::').last}(#{obj.to_s(4)}), srid: #{srid}"
|
|
170
|
+
puts
|
|
171
|
+
|
|
172
|
+
# ── 9. Roundtrip ───────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
puts "--- 9. Roundtrip (export → parse → export) ---"
|
|
175
|
+
puts
|
|
176
|
+
|
|
177
|
+
objects = {
|
|
178
|
+
"POINT" => seattle,
|
|
179
|
+
"POINT Z" => space_needle,
|
|
180
|
+
"LINESTRING" => Segment.new(seattle, portland),
|
|
181
|
+
"PATH" => Path.new(coordinates: [seattle, portland, sf]),
|
|
182
|
+
"POLYGON" => poly,
|
|
183
|
+
"BBOX" => bbox,
|
|
184
|
+
"SRID" => seattle # will use srid: 4326
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
objects.each do |label, obj|
|
|
188
|
+
srid = label == "SRID" ? 4326 : nil
|
|
189
|
+
original_wkt = obj.to_wkt(srid: srid)
|
|
190
|
+
|
|
191
|
+
if srid
|
|
192
|
+
parsed, parsed_srid = WKT.parse_with_srid(original_wkt)
|
|
193
|
+
roundtrip_wkt = parsed.to_wkt(srid: parsed_srid)
|
|
194
|
+
else
|
|
195
|
+
parsed = WKT.parse(original_wkt)
|
|
196
|
+
roundtrip_wkt = parsed.to_wkt
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
match = original_wkt == roundtrip_wkt ? "MATCH" : "DIFF"
|
|
200
|
+
puts " #{label.ljust(12)} #{match} #{original_wkt[0..55]}#{"..." if original_wkt.length > 55}"
|
|
201
|
+
end
|
|
202
|
+
puts
|
|
203
|
+
|
|
204
|
+
# ── 10. File I/O (save & load) ──────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
puts "--- 10. File I/O (save & load) ---"
|
|
207
|
+
puts
|
|
208
|
+
|
|
209
|
+
output_path = File.join(__dir__, "geodetic_demo.wkt")
|
|
210
|
+
|
|
211
|
+
save_objects = [seattle, space_needle, seg, poly, bbox]
|
|
212
|
+
WKT.save!(output_path, save_objects)
|
|
213
|
+
puts " Saved #{save_objects.length} objects to: #{output_path}"
|
|
214
|
+
puts " File size: #{File.size(output_path)} bytes"
|
|
215
|
+
puts
|
|
216
|
+
|
|
217
|
+
puts " File contents:"
|
|
218
|
+
File.readlines(output_path, chomp: true).each_with_index do |line, i|
|
|
219
|
+
puts " #{i + 1}: #{line}"
|
|
220
|
+
end
|
|
221
|
+
puts
|
|
222
|
+
|
|
223
|
+
loaded = WKT.load(output_path)
|
|
224
|
+
puts " Loaded #{loaded.length} objects:"
|
|
225
|
+
loaded.each do |obj|
|
|
226
|
+
puts " #{obj.class.name.split('::').last}: #{obj.to_wkt(precision: 4)[0..60]}#{"..." if obj.to_wkt(precision: 4).length > 60}"
|
|
227
|
+
end
|
|
228
|
+
puts
|
|
229
|
+
|
|
230
|
+
# Verify roundtrip
|
|
231
|
+
puts " Roundtrip check:"
|
|
232
|
+
save_objects.zip(loaded).each_with_index do |(orig, back), i|
|
|
233
|
+
match = orig.to_wkt == back.to_wkt ? "MATCH" : "DIFF"
|
|
234
|
+
puts " object #{i + 1}: #{match}"
|
|
235
|
+
end
|
|
236
|
+
puts
|
|
237
|
+
|
|
238
|
+
# Save with SRID
|
|
239
|
+
srid_path = File.join(__dir__, "geodetic_demo_srid.wkt")
|
|
240
|
+
WKT.save!(srid_path, seattle, portland, srid: 4326, precision: 4)
|
|
241
|
+
puts " Saved with SRID=4326 to: #{srid_path}"
|
|
242
|
+
File.readlines(srid_path, chomp: true).each { |line| puts " #{line}" }
|
|
243
|
+
puts
|
|
244
|
+
|
|
245
|
+
# Clean up the SRID file
|
|
246
|
+
File.delete(srid_path) if File.exist?(srid_path)
|
|
247
|
+
|
|
248
|
+
puts "=== Done ==="
|