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,385 @@
1
+ # WKB Serialization Reference
2
+
3
+ `Geodetic::WKB` provides [Well-Known Binary](https://en.wikipedia.org/wiki/Well-known_binary_representation_of_geometry) (WKB) serialization for all Geodetic geometry types. WKB is the binary counterpart to WKT, used by PostGIS, GEOS, RGeo, Shapely, and most GIS tools for efficient geometry storage and transfer.
4
+
5
+ Every geometry type gains `to_wkb` and `to_wkb_hex` instance methods. The module also provides `WKB.parse` and `WKB.parse_with_srid` for importing WKB data back into Geodetic objects.
6
+
7
+ **Byte order:** Output is always little-endian (NDR), matching PostGIS, GEOS, RGeo, Shapely, and virtually all modern GIS tools. Big-endian (XDR) input is fully supported by the parser.
8
+
9
+ ---
10
+
11
+ ## Export
12
+
13
+ ### Coordinates → POINT
14
+
15
+ All 18 coordinate classes gain `to_wkb` and `to_wkb_hex` methods. They convert the coordinate to LLA and return WKB binary or hex-encoded output.
16
+
17
+ ```ruby
18
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
19
+
20
+ seattle.to_wkb # => 21-byte binary string
21
+ seattle.to_wkb_hex # => "01010000008a1f63ee5a965ec08195438b6ccf4740"
22
+ ```
23
+
24
+ **Altitude handling:** When altitude is non-zero, the Z type code is used (Point Z = 1001):
25
+
26
+ ```ruby
27
+ Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 184.0).to_wkb_hex
28
+ # => "01e90300008a1f63ee5a965ec08195438b6ccf47400000000000006740"
29
+ ```
30
+
31
+ **Cross-system:** Any coordinate type works — UTM, ECEF, MGRS, etc. are converted to LLA internally.
32
+
33
+ ```ruby
34
+ seattle.to_utm.to_wkb_hex # => hex string
35
+ seattle.to_ecef.to_wkb_hex # => hex string
36
+ ```
37
+
38
+ **ENU and NED:** These are relative coordinate systems. Calling `to_wkb` raises `ArgumentError`.
39
+
40
+ ---
41
+
42
+ ### Segment → LINESTRING
43
+
44
+ ```ruby
45
+ seg = Geodetic::Segment.new(seattle, portland)
46
+ seg.to_wkb # => 41-byte binary string
47
+ seg.to_wkb_hex # => hex string
48
+ ```
49
+
50
+ ---
51
+
52
+ ### Path → LINESTRING / POLYGON
53
+
54
+ Returns a LINESTRING by default. Pass `as: :polygon` for a closed POLYGON.
55
+
56
+ ```ruby
57
+ route = Geodetic::Path.new(coordinates: [seattle, portland, sf])
58
+ route.to_wkb # => LINESTRING binary
59
+ route.to_wkb(as: :polygon) # => POLYGON binary
60
+ ```
61
+
62
+ ---
63
+
64
+ ### Areas::Polygon → POLYGON
65
+
66
+ ```ruby
67
+ poly = Geodetic::Areas::Polygon.new(boundary: [a, b, c])
68
+ poly.to_wkb # => binary
69
+ poly.to_wkb_hex # => hex string
70
+ ```
71
+
72
+ All Polygon subclasses (`Triangle`, `Rectangle`, `Pentagon`, `Hexagon`, `Octagon`) inherit this method.
73
+
74
+ ---
75
+
76
+ ### Areas::Circle → POLYGON
77
+
78
+ Approximated as a regular N-gon. Default is 32 segments.
79
+
80
+ ```ruby
81
+ circle = Geodetic::Areas::Circle.new(centroid: seattle, radius: 10_000)
82
+
83
+ circle.to_wkb # => 32-gon binary (541 bytes)
84
+ circle.to_wkb(segments: 64) # => 64-gon
85
+ circle.to_wkb(segments: 8) # => 8-gon (157 bytes)
86
+ ```
87
+
88
+ ---
89
+
90
+ ### Areas::BoundingBox → POLYGON
91
+
92
+ 5 positions (4 corners + closing point).
93
+
94
+ ```ruby
95
+ bbox = Geodetic::Areas::BoundingBox.new(
96
+ nw: Geodetic::Coordinate::LLA.new(lat: 48.0, lng: -123.0, alt: 0),
97
+ se: Geodetic::Coordinate::LLA.new(lat: 46.0, lng: -121.0, alt: 0)
98
+ )
99
+ bbox.to_wkb # => 93-byte binary
100
+ bbox.to_wkb_hex # => hex string
101
+ ```
102
+
103
+ ---
104
+
105
+ ### Feature → delegates to geometry
106
+
107
+ WKB has no concept of properties or labels. `Feature#to_wkb` delegates directly to the underlying geometry.
108
+
109
+ ```ruby
110
+ f = Geodetic::Feature.new(label: "Seattle", geometry: seattle, metadata: { pop: 750_000 })
111
+ f.to_wkb_hex # => same as seattle.to_wkb_hex
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Options
117
+
118
+ All `to_wkb` and `to_wkb_hex` methods accept:
119
+
120
+ | Parameter | Default | Description |
121
+ |-----------|---------|-------------|
122
+ | `srid:` | `nil` | When set, produces EWKB with embedded SRID |
123
+
124
+ ---
125
+
126
+ ## Z-Dimension Consistency
127
+
128
+ When any point in a geometry has non-zero altitude, **all** points in that geometry use the Z type code. This follows the OGC rule that Z-dimensionality is uniform within a geometry.
129
+
130
+ ```ruby
131
+ # seattle (alt=0) + sf (alt=100) → LineString Z (type code 1002)
132
+ mixed = Geodetic::Segment.new(seattle, sf_with_altitude)
133
+ mixed.to_wkb # type code = 1002 (LineString Z)
134
+
135
+ # Both at alt=0 → LineString (type code 2)
136
+ flat = Geodetic::Segment.new(seattle, portland)
137
+ flat.to_wkb # type code = 2 (LineString)
138
+ ```
139
+
140
+ ---
141
+
142
+ ## SRID / EWKB
143
+
144
+ Pass `srid:` to any `to_wkb` or `to_wkb_hex` call to produce Extended WKB (EWKB), the format used by PostGIS:
145
+
146
+ ```ruby
147
+ seattle.to_wkb_hex(srid: 4326)
148
+ # => "0101000020e61000008a1f63ee5a965ec08195438b6ccf4740"
149
+
150
+ seg.to_wkb_hex(srid: 4326)
151
+ # => "0102000020e6100000020000008a1f63ee..."
152
+ ```
153
+
154
+ EWKB embeds the SRID in the type code using the `0x20000000` flag, followed by a 4-byte SRID value.
155
+
156
+ Common SRIDs:
157
+
158
+ | SRID | Description |
159
+ |------|-------------|
160
+ | 4326 | WGS84 geographic (lat/lng) |
161
+ | 3857 | Web Mercator |
162
+ | 32610 | UTM Zone 10N |
163
+
164
+ ---
165
+
166
+ ## File I/O
167
+
168
+ ### Binary Format
169
+
170
+ #### `WKB.save!(path, *objects, srid: nil)`
171
+
172
+ Write geometries to a binary file using a framed format: 4-byte LE count, then for each geometry a 4-byte LE size followed by raw WKB bytes. Accepts individual objects or an array.
173
+
174
+ ```ruby
175
+ Geodetic::WKB.save!("shapes.wkb", seattle, segment, polygon)
176
+ Geodetic::WKB.save!("shapes.wkb", [seattle, segment, polygon])
177
+ Geodetic::WKB.save!("shapes.wkb", seattle, polygon, srid: 4326)
178
+ ```
179
+
180
+ #### `WKB.load(path)`
181
+
182
+ Read a binary WKB file and return an Array of Geodetic objects.
183
+
184
+ ```ruby
185
+ objects = Geodetic::WKB.load("shapes.wkb")
186
+ # => [Coordinate::LLA, Segment, Areas::Polygon]
187
+ ```
188
+
189
+ ### Hex Format
190
+
191
+ #### `WKB.save_hex!(path, *objects, srid: nil)`
192
+
193
+ Write one hex-encoded WKB string per line. Human-readable and diff-friendly.
194
+
195
+ ```ruby
196
+ Geodetic::WKB.save_hex!("shapes.wkb.hex", seattle, segment, polygon)
197
+ ```
198
+
199
+ Output file:
200
+
201
+ ```
202
+ 01010000008a1f63ee5a965ec08195438b6ccf4740
203
+ 0102000000020000008a1f63ee5a965ec0...
204
+ 01030000000100000004000000...
205
+ ```
206
+
207
+ #### `WKB.load_hex(path)`
208
+
209
+ Read a hex WKB file and return an Array of Geodetic objects. Blank lines and lines starting with `#` are skipped.
210
+
211
+ ```ruby
212
+ objects = Geodetic::WKB.load_hex("shapes.wkb.hex")
213
+ # => [Coordinate::LLA, Segment, Areas::Polygon]
214
+ ```
215
+
216
+ ### Roundtrip
217
+
218
+ ```ruby
219
+ objects = [seattle, segment, polygon]
220
+
221
+ # Binary roundtrip
222
+ Geodetic::WKB.save!("data.wkb", objects)
223
+ loaded = Geodetic::WKB.load("data.wkb")
224
+
225
+ # Hex roundtrip
226
+ Geodetic::WKB.save_hex!("data.wkb.hex", objects, srid: 4326)
227
+ loaded = Geodetic::WKB.load_hex("data.wkb.hex")
228
+ ```
229
+
230
+ ---
231
+
232
+ ## Import
233
+
234
+ ### `WKB.parse(input)`
235
+
236
+ Parse a WKB binary string or hex string and return a Geodetic object (or Array for Multi*/GeometryCollection types). Auto-detects binary vs hex encoding.
237
+
238
+ ```ruby
239
+ Geodetic::WKB.parse("01010000008a1f63ee5a965ec08195438b6ccf4740")
240
+ # => Coordinate::LLA
241
+
242
+ Geodetic::WKB.parse(binary_string)
243
+ # => Coordinate::LLA
244
+ ```
245
+
246
+ ### `WKB.parse_with_srid(input)`
247
+
248
+ Parse WKB/EWKB and return both the object and the SRID:
249
+
250
+ ```ruby
251
+ obj, srid = Geodetic::WKB.parse_with_srid("0101000020e61000008a1f63ee5a965ec08195438b6ccf4740")
252
+ obj # => Coordinate::LLA
253
+ srid # => 4326
254
+
255
+ obj, srid = Geodetic::WKB.parse_with_srid(plain_wkb_hex)
256
+ srid # => nil
257
+ ```
258
+
259
+ ---
260
+
261
+ ## WKB → Geodetic Type Mapping
262
+
263
+ | WKB Type Code | WKB Type | Geodetic Type |
264
+ |---------------|----------|---------------|
265
+ | 1 | POINT | `Coordinate::LLA` |
266
+ | 2 (2 points) | LINESTRING | `Segment` |
267
+ | 2 (3+ points) | LINESTRING | `Path` |
268
+ | 3 | POLYGON | `Areas::Polygon` (outer ring only; holes are dropped) |
269
+ | 4 | MULTIPOINT | Array of `Coordinate::LLA` |
270
+ | 5 | MULTILINESTRING | Array of `Segment` or `Path` |
271
+ | 6 | MULTIPOLYGON | Array of `Areas::Polygon` |
272
+ | 7 | GEOMETRYCOLLECTION | Array of mixed types |
273
+
274
+ Z variants (type + 1000) are supported: Point Z = 1001, LineString Z = 1002, Polygon Z = 1003.
275
+
276
+ ---
277
+
278
+ ## Geometry Mapping (Export)
279
+
280
+ | Geodetic Type | WKB Type | Notes |
281
+ |---------------|----------|-------|
282
+ | Any coordinate (LLA, UTM, ECEF, ...) | POINT | Converts through LLA |
283
+ | `Segment` | LINESTRING | 2 positions |
284
+ | `Path` | LINESTRING | N positions (default) |
285
+ | `Path` (with `as: :polygon`) | POLYGON | Auto-closes the ring |
286
+ | `Areas::Polygon` (and subclasses) | POLYGON | Boundary ring already closed |
287
+ | `Areas::Circle` | POLYGON | Approximated as N-gon (default 32) |
288
+ | `Areas::BoundingBox` | POLYGON | 4 corners, closed |
289
+ | `Feature` | Delegates to geometry | Properties are lost |
290
+
291
+ ---
292
+
293
+ ## WKB Binary Structure
294
+
295
+ Each WKB geometry starts with:
296
+
297
+ | Offset | Size | Description |
298
+ |--------|------|-------------|
299
+ | 0 | 1 byte | Byte order: `0x01` = little-endian (NDR), `0x00` = big-endian (XDR) |
300
+ | 1 | 4 bytes | Type code (uint32) |
301
+ | 5 | 4 bytes | SRID (uint32, only in EWKB when SRID flag is set) |
302
+ | 5 or 9 | varies | Geometry data (IEEE 754 doubles) |
303
+
304
+ **Type codes:**
305
+
306
+ | Code | Type | Z Code |
307
+ |------|------|--------|
308
+ | 1 | Point | 1001 |
309
+ | 2 | LineString | 1002 |
310
+ | 3 | Polygon | 1003 |
311
+ | 4 | MultiPoint | 1004 |
312
+ | 5 | MultiLineString | 1005 |
313
+ | 6 | MultiPolygon | 1006 |
314
+ | 7 | GeometryCollection | 1007 |
315
+
316
+ **EWKB flags** (OR'd into type code):
317
+
318
+ | Flag | Value | Description |
319
+ |------|-------|-------------|
320
+ | SRID | `0x20000000` | SRID follows the type code |
321
+ | Z | `0x80000000` | Coordinates include Z values |
322
+
323
+ ---
324
+
325
+ ## Roundtrip Example
326
+
327
+ ```ruby
328
+ require "geodetic"
329
+
330
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0)
331
+
332
+ # Export
333
+ hex = seattle.to_wkb_hex(srid: 4326)
334
+ # => "0101000020e61000008a1f63ee5a965ec08195438b6ccf4740"
335
+
336
+ # Import
337
+ obj, srid = Geodetic::WKB.parse_with_srid(hex)
338
+ obj.lat # => 47.6205
339
+ obj.lng # => -122.3493
340
+ srid # => 4326
341
+
342
+ # Re-export
343
+ obj.to_wkb_hex(srid: srid) == hex # => true
344
+ ```
345
+
346
+ ---
347
+
348
+ ## Integration with PostGIS and RGeo
349
+
350
+ WKB is the preferred format for exchanging geometry with spatial databases:
351
+
352
+ ```ruby
353
+ # Writing to PostGIS (hex format)
354
+ hex = polygon.to_wkb_hex(srid: 4326)
355
+ ActiveRecord::Base.connection.execute(
356
+ "INSERT INTO regions (geom) VALUES (ST_GeomFromWKB(decode('#{hex}', 'hex'), 4326))"
357
+ )
358
+
359
+ # Or using EWKB directly
360
+ ewkb_hex = polygon.to_wkb_hex(srid: 4326)
361
+ ActiveRecord::Base.connection.execute(
362
+ "INSERT INTO regions (geom) VALUES ('#{ewkb_hex}')"
363
+ )
364
+
365
+ # Reading from PostGIS
366
+ row = ActiveRecord::Base.connection.select_one("SELECT ST_AsEWKB(geom)::text FROM regions LIMIT 1")
367
+ obj, srid = Geodetic::WKB.parse_with_srid(row["st_asewkb"])
368
+ ```
369
+
370
+ WKB binary is also accepted by RGeo's `WKRep::WKBParser`, Shapely's `wkb.loads()`, JTS, GEOS, and virtually every GIS library.
371
+
372
+ ---
373
+
374
+ ## WKB vs WKT
375
+
376
+ | | WKB | WKT |
377
+ |---|-----|-----|
378
+ | **Format** | Binary | Text |
379
+ | **Size** | Compact | Larger |
380
+ | **Human-readable** | No (use hex) | Yes |
381
+ | **Speed** | Faster to parse | Slower |
382
+ | **Use case** | Database storage, wire transfer | Debugging, config files, SQL |
383
+ | **SRID support** | EWKB flag | EWKT prefix |
384
+
385
+ Both formats support the same geometry types and Z-dimension handling.