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,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 ==="