geodetic 0.2.0 → 0.3.1

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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -0
  3. data/README.md +83 -28
  4. data/docs/coordinate-systems/gars.md +0 -4
  5. data/docs/coordinate-systems/georef.md +0 -4
  6. data/docs/coordinate-systems/gh.md +0 -4
  7. data/docs/coordinate-systems/gh36.md +0 -4
  8. data/docs/coordinate-systems/h3.md +308 -0
  9. data/docs/coordinate-systems/ham.md +0 -4
  10. data/docs/coordinate-systems/index.md +25 -23
  11. data/docs/coordinate-systems/olc.md +0 -4
  12. data/docs/index.md +7 -3
  13. data/docs/reference/conversions.md +15 -15
  14. data/docs/reference/feature.md +116 -0
  15. data/docs/reference/map-rendering.md +32 -0
  16. data/docs/reference/serialization.md +4 -4
  17. data/examples/02_all_coordinate_systems.rb +0 -3
  18. data/examples/03_distance_calculations.rb +1 -0
  19. data/examples/04_bearing_calculations.rb +1 -0
  20. data/examples/05_map_rendering/.gitignore +2 -0
  21. data/examples/05_map_rendering/demo.rb +264 -0
  22. data/examples/05_map_rendering/icons/bridge.png +0 -0
  23. data/examples/05_map_rendering/icons/building.png +0 -0
  24. data/examples/05_map_rendering/icons/landmark.png +0 -0
  25. data/examples/05_map_rendering/icons/monument.png +0 -0
  26. data/examples/05_map_rendering/icons/park.png +0 -0
  27. data/examples/05_map_rendering/nyc_landmarks.png +0 -0
  28. data/examples/README.md +62 -0
  29. data/fiddle_pointer_buffer_pool.md +119 -0
  30. data/lib/geodetic/coordinate/bng.rb +14 -33
  31. data/lib/geodetic/coordinate/ecef.rb +5 -1
  32. data/lib/geodetic/coordinate/enu.rb +13 -0
  33. data/lib/geodetic/coordinate/gars.rb +2 -3
  34. data/lib/geodetic/coordinate/georef.rb +2 -3
  35. data/lib/geodetic/coordinate/gh.rb +2 -4
  36. data/lib/geodetic/coordinate/gh36.rb +4 -5
  37. data/lib/geodetic/coordinate/h3.rb +412 -0
  38. data/lib/geodetic/coordinate/ham.rb +2 -3
  39. data/lib/geodetic/coordinate/lla.rb +15 -1
  40. data/lib/geodetic/coordinate/mgrs.rb +1 -1
  41. data/lib/geodetic/coordinate/ned.rb +13 -0
  42. data/lib/geodetic/coordinate/olc.rb +0 -1
  43. data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
  44. data/lib/geodetic/coordinate/state_plane.rb +9 -0
  45. data/lib/geodetic/coordinate/ups.rb +1 -1
  46. data/lib/geodetic/coordinate/usng.rb +1 -1
  47. data/lib/geodetic/coordinate/utm.rb +1 -1
  48. data/lib/geodetic/coordinate/web_mercator.rb +1 -1
  49. data/lib/geodetic/coordinate.rb +31 -26
  50. data/lib/geodetic/feature.rb +44 -0
  51. data/lib/geodetic/geoid_height.rb +11 -6
  52. data/lib/geodetic/version.rb +1 -1
  53. data/lib/geodetic.rb +1 -0
  54. data/mkdocs.yml +2 -0
  55. metadata +20 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 782ba82d99727dfa9a075fcdfb9dea89f631ebc78e0e105293745c64f77f9dae
4
- data.tar.gz: 59e9c844470d77026a9929c8b9ffd01a5d065fd491d8efb67106b0903267c681
3
+ metadata.gz: 13b2b50f8ff03c2093abf1a2454f3db38942f9c32918eb23aab30b6c45a4146b
4
+ data.tar.gz: 2b3d5a7f325e2766196d2997e9c1781f0425abe016edeeb2fe4f155fc6a43f13
5
5
  SHA512:
6
- metadata.gz: 38543deb9e8d62aa20b4a90c9602b0e97806d7d2f55dd9d308a138b9aea2b033e6f51656a9d247ce750d5abcd5298f170825a3f0e4f013a63198bc467e1f3d8c
7
- data.tar.gz: fcb62fac876d350bd784d08bb17d45b7c14218a31ff6cfcce4be7e384e3f9a6a2c711ae3731b4a4241e5b34c062a8ddcdc2fb8ce6786ac114ff35d872ad8a7f3
6
+ metadata.gz: 1d0378c9df0e04f9bb3de688303abae35857c4fbc594a26094a983417baa55004ef7f7f4cef7c8db7511b8fc769d0a368420e7601cb04e12eaf887b324e51e61
7
+ data.tar.gz: c20dd6fa952579d8d035e1afd146d7337caa087efd71df4db17dcc3918b8cb4c561237f1cb5820167d9f84a64209da48a1dd38d0b46a5bccf0e3ce6959aa06d3
data/CHANGELOG.md CHANGED
@@ -11,6 +11,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
11
11
  ## [Unreleased]
12
12
 
13
13
 
14
+ ## [0.3.1] - 2026-03-09
15
+
16
+ ### Added
17
+
18
+ - **`Geodetic::Feature` class** — wraps any coordinate or area geometry with a `label` and `metadata` hash; delegates `distance_to` and `bearing_to` to the underlying geometry, using the centroid for area geometries
19
+ - **Map rendering example** (`examples/05_map_rendering/`) — renders NYC landmarks on a raster map using [libgd-gis](https://rubygems.org/gems/libgd-gis), demonstrating Feature objects, polygon overlays, bearing arrows, icon compositing, and light/dark theme support
20
+ - `examples/README.md` describing all five example scripts
21
+ - Documentation: `docs/reference/feature.md` (Feature reference) and `docs/reference/map-rendering.md` (libgd-gis integration guide)
22
+
23
+ ### Changed
24
+
25
+ - Updated README, `docs/index.md`, and mkdocs nav to include Feature class and map rendering example
26
+
27
+ ## [0.3.0] - 2026-03-08
28
+
29
+ ### Added
30
+
31
+ - **H3 Hexagonal Hierarchical Index** (`Geodetic::Coordinate::H3`) — Uber's spatial indexing system, bringing total to 18 coordinate systems (324 conversion paths)
32
+ - H3 uses Ruby's `fiddle` to call the H3 v4 C API directly — no gem dependency beyond `fiddle`
33
+ - H3-specific features: `grid_disk(k)`, `parent(res)`, `children(res)`, `pentagon?`, `cell_area`, `h3_index`, `resolution` (0-15)
34
+ - H3 `to_area` returns `Areas::Polygon` (6 vertices for hexagons, 5 for pentagons) instead of `Areas::Rectangle`
35
+ - H3 `neighbors` returns Array of 6 cells instead of directional Hash with 8 cardinal keys
36
+ - Graceful degradation: H3 raises clear error with installation instructions if `libh3` is not found; all other coordinate systems work normally
37
+ - `H3.available?` class method to check for libh3 at runtime
38
+ - Documentation page: `docs/coordinate-systems/h3.md`
39
+
40
+ ### Changed
41
+
42
+ - Updated all documentation to reflect 18 coordinate systems (README, docs, gemspec, CLAUDE.md)
43
+
14
44
  ## [0.2.0] - 2026-03-08
15
45
 
16
46
  ### Added
data/README.md CHANGED
@@ -13,12 +13,13 @@
13
13
  <td width="50%" valign="top">
14
14
  <strong>Key Features</strong><br>
15
15
 
16
- - <strong>17 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC, GEOREF, GARS<br>
16
+ - <strong>18 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC, GEOREF, GARS, H3<br>
17
17
  - <strong>Full Bidirectional Conversions</strong> - Every system converts to and from every other system<br>
18
18
  - <strong>Distance Calculations</strong> - Vincenty great-circle and straight-line with unit tracking<br>
19
19
  - <strong>Bearing Calculations</strong> - Forward azimuth, back azimuth, compass directions, elevation angles<br>
20
20
  - <strong>Geoid Height Support</strong> - EGM96, EGM2008, GEOID18, GEOID12B models<br>
21
21
  - <strong>Geographic Areas</strong> - Circle, Polygon, and Rectangle with point-in-area tests<br>
22
+ - <strong>Features</strong> - Named geometry wrapper with metadata and delegated distance/bearing<br>
22
23
  - <strong>Validated Setters</strong> - Type coercion and range validation on all coordinate attributes<br>
23
24
  - <strong>Serialization</strong> - to_s(precision), to_a, from_string, from_array, DMS format<br>
24
25
  - <strong>Multiple Datums</strong> - WGS84, Clarke 1866, GRS 1980, Airy 1830, and more<br>
@@ -27,7 +28,7 @@
27
28
  </tr>
28
29
  </table>
29
30
 
30
- <p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All 17 coordinate systems support complete bidirectional conversions with high precision. Review the <a href="https://madbomber.github.io/geodetic/">full documentation website</a> and explore the <a href="examples/">runnable examples</a>.</p>
31
+ <p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All 18 coordinate systems support complete bidirectional conversions with high precision. Review the <a href="https://madbomber.github.io/geodetic/">full documentation website</a> and explore the <a href="examples/">runnable examples</a>.</p>
31
32
 
32
33
  ## Installation
33
34
 
@@ -43,6 +44,20 @@ Or install directly:
43
44
  gem install geodetic
44
45
  ```
45
46
 
47
+ ### Optional: H3 Hexagonal Index
48
+
49
+ The H3 coordinate system requires Uber's [H3 C library](https://h3geo.org/) installed on your system. Without it, all other 17 coordinate systems work normally; H3 operations will raise a helpful error.
50
+
51
+ ```bash
52
+ # macOS
53
+ brew install h3
54
+
55
+ # Linux (build from source)
56
+ # See https://h3geo.org/docs/installation
57
+ ```
58
+
59
+ You can also set the `LIBH3_PATH` environment variable to point to a custom `libh3` location.
60
+
46
61
  ## Usage
47
62
 
48
63
  ### Basic Coordinate Creation
@@ -63,14 +78,29 @@ ned = Coordinates::NED.new(n: 200.0, e: 100.0, d: -50.0)
63
78
 
64
79
  ### GCS Shorthand
65
80
 
66
- `GCS` is a top-level alias for `Geodetic::Coordinate`, providing a concise way to create and work with coordinates:
81
+ For convenience, you can define a short alias in your application:
67
82
 
68
83
  ```ruby
69
84
  require "geodetic"
70
85
 
71
- # Use GCS as a shorthand for Geodetic::Coordinate
72
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
73
- ecef = GCS::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
86
+ GCS = Geodetic::Coordinate
87
+
88
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
89
+ ecef = Geodetic::Coordinate::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
90
+ ```
91
+
92
+ ### Discovering Coordinate Systems
93
+
94
+ List all available coordinate systems at runtime:
95
+
96
+ ```ruby
97
+ Geodetic::Coordinate.systems
98
+ # => [Geodetic::Coordinate::LLA, Geodetic::Coordinate::ECEF, Geodetic::Coordinate::UTM, ...]
99
+
100
+ # Get short names
101
+ Geodetic::Coordinate.systems.map { |c| c.name.split('::').last }
102
+ # => ["LLA", "ECEF", "UTM", "ENU", "NED", "MGRS", "USNG", "WebMercator",
103
+ # "UPS", "StatePlane", "BNG", "GH36", "GH", "HAM", "OLC", "GEOREF", "GARS", "H3"]
74
104
  ```
75
105
 
76
106
  ### Coordinate Conversions
@@ -171,9 +201,9 @@ Universal distance methods work across all coordinate types and return `Distance
171
201
  **Instance method `distance_to`** — Vincenty great-circle distance:
172
202
 
173
203
  ```ruby
174
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
175
- portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
176
- sf = GCS::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
204
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
205
+ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
206
+ sf = Geodetic::Coordinate::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
177
207
 
178
208
  d = seattle.distance_to(portland) # => Distance (meters)
179
209
  d.meters # => 235393.17
@@ -187,23 +217,23 @@ seattle.distance_to([portland, sf]) # => [Distance, Distance] (radial)
187
217
  **Class method `distance_between`** — consecutive chain distances:
188
218
 
189
219
  ```ruby
190
- GCS.distance_between(seattle, portland) # => Distance
191
- GCS.distance_between(seattle, portland, sf) # => [Distance, Distance] (chain)
192
- GCS.distance_between([seattle, portland, sf]) # => [Distance, Distance] (chain)
220
+ Geodetic::Coordinate.distance_between(seattle, portland) # => Distance
221
+ Geodetic::Coordinate.distance_between(seattle, portland, sf) # => [Distance, Distance] (chain)
222
+ Geodetic::Coordinate.distance_between([seattle, portland, sf]) # => [Distance, Distance] (chain)
193
223
  ```
194
224
 
195
225
  **Straight-line (ECEF Euclidean) versions:**
196
226
 
197
227
  ```ruby
198
228
  seattle.straight_line_distance_to(portland) # => Distance
199
- GCS.straight_line_distance_between(seattle, portland) # => Distance
229
+ Geodetic::Coordinate.straight_line_distance_between(seattle, portland) # => Distance
200
230
  ```
201
231
 
202
232
  **Cross-system distances** — works between any coordinate types:
203
233
 
204
234
  ```ruby
205
235
  utm = seattle.to_utm
206
- mgrs = GCS::MGRS.from_lla(portland)
236
+ mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
207
237
  utm.distance_to(mgrs) # => Distance
208
238
  ```
209
239
 
@@ -216,8 +246,8 @@ Universal bearing methods work across all coordinate types and return `Bearing`
216
246
  **Instance method `bearing_to`** — great-circle forward azimuth:
217
247
 
218
248
  ```ruby
219
- seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
220
- portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
249
+ seattle = Geodetic::Coordinate::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
250
+ portland = Geodetic::Coordinate::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
221
251
 
222
252
  b = seattle.bearing_to(portland) # => Bearing
223
253
  b.degrees # => 188.2
@@ -231,8 +261,8 @@ b.to_s # => "188.2036°"
231
261
  **Instance method `elevation_to`** — vertical look angle:
232
262
 
233
263
  ```ruby
234
- a = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
235
- b = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 5000.0)
264
+ a = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
265
+ b = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 5000.0)
236
266
 
237
267
  a.elevation_to(b) # => 89.9... (degrees, nearly straight up)
238
268
  ```
@@ -240,15 +270,15 @@ a.elevation_to(b) # => 89.9... (degrees, nearly straight up)
240
270
  **Class method `bearing_between`** — consecutive chain bearings:
241
271
 
242
272
  ```ruby
243
- GCS.bearing_between(seattle, portland) # => Bearing
244
- GCS.bearing_between(seattle, portland, sf) # => [Bearing, Bearing] (chain)
273
+ Geodetic::Coordinate.bearing_between(seattle, portland) # => Bearing
274
+ Geodetic::Coordinate.bearing_between(seattle, portland, sf) # => [Bearing, Bearing] (chain)
245
275
  ```
246
276
 
247
277
  **Cross-system bearings** — works between any coordinate types:
248
278
 
249
279
  ```ruby
250
280
  utm = seattle.to_utm
251
- mgrs = GCS::MGRS.from_lla(portland)
281
+ mgrs = Geodetic::Coordinate::MGRS.from_lla(portland)
252
282
  utm.bearing_to(mgrs) # => Bearing
253
283
  ```
254
284
 
@@ -391,9 +421,6 @@ gh36 = lla.to_gh36(precision: 8)
391
421
  # Decode back to LLA
392
422
  lla = gh36.to_lla
393
423
 
394
- # URL slug (the hash itself is URL-safe)
395
- gh36.to_slug # => "bdrdC26BqH"
396
-
397
424
  # Neighbor cells
398
425
  gh36.neighbors # => { N: GH36, S: GH36, E: GH36, W: GH36, NE: ..., NW: ..., SE: ..., SW: ... }
399
426
 
@@ -421,9 +448,6 @@ gh = lla.to_gh(precision: 8)
421
448
  # Decode back to LLA
422
449
  lla = gh.to_lla
423
450
 
424
- # URL slug (the hash itself is URL-safe)
425
- gh.to_slug # => "dr5ru7"
426
-
427
451
  # Neighbor cells
428
452
  gh.neighbors # => { N: GH, S: GH, E: GH, W: GH, NE: ..., NW: ..., SE: ..., SW: ... }
429
453
 
@@ -517,6 +541,36 @@ rect.sw # => computed SW corner
517
541
  rect.includes?(point) # => true/false
518
542
  ```
519
543
 
544
+ ### Features
545
+
546
+ `Feature` wraps a geometry (any coordinate or area) with a label and a metadata hash. It delegates `distance_to` and `bearing_to` to its geometry, using the centroid for area geometries.
547
+
548
+ ```ruby
549
+ liberty = Feature.new(
550
+ label: "Statue of Liberty",
551
+ geometry: Coordinates::LLA.new(lat: 40.6892, lng: -74.0445, alt: 0),
552
+ metadata: { category: "monument", year: 1886 }
553
+ )
554
+
555
+ empire = Feature.new(
556
+ label: "Empire State Building",
557
+ geometry: Coordinates::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0),
558
+ metadata: { category: "building", floors: 102 }
559
+ )
560
+
561
+ liberty.distance_to(empire).to_km # => "8.24 km"
562
+ liberty.bearing_to(empire).degrees # => 36.99
563
+
564
+ # Area geometries use the centroid for distance/bearing
565
+ park = Feature.new(
566
+ label: "Central Park",
567
+ geometry: Areas::Polygon.new(boundary: [...])
568
+ )
569
+ park.distance_to(liberty).to_km # => "12.47 km"
570
+ ```
571
+
572
+ All three attributes (`label`, `geometry`, `metadata`) are mutable.
573
+
520
574
  ### Web Mercator Tile Coordinates
521
575
 
522
576
  ```ruby
@@ -542,9 +596,10 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
542
596
  | Script | Description |
543
597
  |--------|-------------|
544
598
  | [`01_basic_conversions.rb`](examples/01_basic_conversions.rb) | LLA, ECEF, UTM, ENU, NED conversions and roundtrips |
545
- | [`02_all_coordinate_systems.rb`](examples/02_all_coordinate_systems.rb) | All 17 coordinate systems, cross-system chains, and areas |
599
+ | [`02_all_coordinate_systems.rb`](examples/02_all_coordinate_systems.rb) | All 18 coordinate systems, cross-system chains, and areas |
546
600
  | [`03_distance_calculations.rb`](examples/03_distance_calculations.rb) | Distance class features, unit conversions, and arithmetic |
547
601
  | [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
602
+ | [`05_map_rendering/`](examples/05_map_rendering/) | Render landmarks on a raster map with Feature objects, polygon areas, bearing arrows, and icons using [libgd-gis](https://rubygems.org/gems/libgd-gis) |
548
603
 
549
604
  Run any example with:
550
605
 
@@ -141,10 +141,6 @@ coord.to_s(6) # => "006AG3"
141
141
  coord.to_s(5) # => "006AG"
142
142
  ```
143
143
 
144
- ### `to_slug`
145
-
146
- Alias for `to_s`. GARS codes are already URL-safe.
147
-
148
144
  ### `to_a`
149
145
 
150
146
  Returns `[lat, lng]` of the cell midpoint.
@@ -122,10 +122,6 @@ coord.to_s(4) # => "GJPJ"
122
122
  coord.to_s(2) # => "GJ"
123
123
  ```
124
124
 
125
- ### `to_slug`
126
-
127
- Alias for `to_s`. GEOREF codes are already URL-safe.
128
-
129
125
  ### `to_a`
130
126
 
131
127
  Returns `[lat, lng]` of the cell midpoint.
@@ -110,10 +110,6 @@ coord.to_s # => "dr5ru7c5g200"
110
110
  coord.to_s(6) # => "dr5ru7"
111
111
  ```
112
112
 
113
- ### `to_slug`
114
-
115
- Alias for `to_s`. The geohash is already URL-safe.
116
-
117
113
  ### `to_a`
118
114
 
119
115
  Returns `[lat, lng]` of the cell midpoint.
@@ -105,10 +105,6 @@ coord.to_s # => "bdrdC26BqH"
105
105
  coord.to_s(5) # => "bdrdC"
106
106
  ```
107
107
 
108
- ### `to_slug`
109
-
110
- Alias for `to_s`. The geohash is already URL-safe.
111
-
112
108
  ### `to_a`
113
109
 
114
110
  Returns `[lat, lng]` of the cell midpoint.
@@ -0,0 +1,308 @@
1
+ # Geodetic::Coordinate::H3
2
+
3
+ ## H3 Hexagonal Hierarchical Index
4
+
5
+ H3 is Uber's hierarchical geospatial indexing system that divides the globe into hexagonal cells (and 12 pentagons per resolution level) using an icosahedron projection. Each cell is identified by a 64-bit integer, typically displayed as a 15-character hex string like `872a1072bffffff`.
6
+
7
+ **H3 requires the `libh3` C library** installed on your system. Without it, all other coordinate systems work normally; H3 operations raise a clear error message with installation instructions.
8
+
9
+ ### Prerequisites
10
+
11
+ ```bash
12
+ # macOS (Homebrew)
13
+ brew install h3
14
+
15
+ # Linux (build from source)
16
+ git clone https://github.com/uber/h3.git
17
+ cd h3
18
+ cmake -B build -DCMAKE_INSTALL_PREFIX=/usr/local
19
+ cmake --build build
20
+ sudo cmake --install build
21
+ ```
22
+
23
+ You can also set the `LIBH3_PATH` environment variable to specify a custom library path:
24
+
25
+ ```bash
26
+ export LIBH3_PATH=/path/to/libh3.dylib
27
+ ```
28
+
29
+ Geodetic searches these paths automatically:
30
+ - `/opt/homebrew/lib/libh3.dylib` (macOS ARM Homebrew)
31
+ - `/usr/local/lib/libh3.dylib` (macOS Intel Homebrew)
32
+ - `/usr/lib/libh3.so` (Linux system)
33
+ - `/usr/local/lib/libh3.so` (Linux local install)
34
+
35
+ ### Key Differences from Other Spatial Hashes
36
+
37
+ | Feature | GH/OLC/GARS/GEOREF/HAM | H3 |
38
+ |---------|------------------------|-----|
39
+ | Cell shape | Rectangle | Hexagon (6 vertices) |
40
+ | `to_area` returns | `Areas::Rectangle` | `Areas::Polygon` |
41
+ | `neighbors` returns | Hash with 8 cardinal keys | Array of 6 cells |
42
+ | Code format | String | 64-bit integer (hex string) |
43
+ | Dependency | None (pure Ruby) | `libh3` (C library via fiddle) |
44
+
45
+ H3 is a **2D coordinate system** (no altitude). Conversions to/from other systems go through LLA as the intermediary. Each hex string represents a hexagonal cell; the coordinate's point value is the cell's centroid.
46
+
47
+ ## Constructor
48
+
49
+ ```ruby
50
+ # From a hex string
51
+ coord = Geodetic::Coordinate::H3.new("872a1072bffffff")
52
+
53
+ # From a hex string with 0x prefix
54
+ coord = Geodetic::Coordinate::H3.new("0x872a1072bffffff")
55
+
56
+ # From a 64-bit integer
57
+ coord = Geodetic::Coordinate::H3.new(0x872a1072bffffff)
58
+
59
+ # From any coordinate (converts via LLA)
60
+ coord = Geodetic::Coordinate::H3.new(lla_coord)
61
+ coord = Geodetic::Coordinate::H3.new(utm_coord, precision: 9)
62
+ ```
63
+
64
+ | Parameter | Type | Default | Description |
65
+ |-------------|-------------------------|---------|--------------------------------------------|
66
+ | `source` | String, Integer, or Coord | -- | An H3 hex string, integer, or coordinate |
67
+ | `precision` | Integer | 7 | H3 resolution level (0-15) |
68
+
69
+ Raises `ArgumentError` if the source string is empty, contains invalid hex characters, or does not represent a valid H3 cell index. String input is case-insensitive (normalized to lowercase). The `0x` prefix is stripped automatically.
70
+
71
+ ## Attributes
72
+
73
+ | Attribute | Type | Access | Description |
74
+ |------------|---------|-----------|----------------------------------|
75
+ | `code` | String | read-only | The hex string representation |
76
+ | `h3_index` | Integer | read-only | The 64-bit H3 cell index |
77
+
78
+ H3 is **immutable** -- there are no setter methods.
79
+
80
+ ## Resolution
81
+
82
+ H3 uses "resolution" (0-15) instead of string-length precision. Higher resolution means smaller cells.
83
+
84
+ | Resolution | Approximate Cell Area | Approximate Edge Length |
85
+ |------------|----------------------|------------------------|
86
+ | 0 | 4,357,449 km^2 | 1,108 km |
87
+ | 1 | 609,788 km^2 | 419 km |
88
+ | 2 | 86,801 km^2 | 158 km |
89
+ | 3 | 12,393 km^2 | 60 km |
90
+ | 4 | 1,770 km^2 | 23 km |
91
+ | 5 | 252 km^2 | 8.5 km |
92
+ | 6 | 36 km^2 | 3.2 km |
93
+ | 7 | 5.2 km^2 (default) | 1.2 km |
94
+ | 8 | 0.74 km^2 | 461 m |
95
+ | 9 | 0.105 km^2 | 174 m |
96
+ | 10 | 0.015 km^2 | 66 m |
97
+ | 11 | 0.002 km^2 | 25 m |
98
+ | 12 | 307 m^2 | 9.4 m |
99
+ | 13 | 43 m^2 | 3.6 m |
100
+ | 14 | 6.2 m^2 | 1.3 m |
101
+ | 15 | 0.9 m^2 | 0.5 m |
102
+
103
+ ```ruby
104
+ coord.resolution # => 7 (alias: coord.precision)
105
+ coord.cell_area # => 5182586.98 (square meters)
106
+ coord.precision_in_meters # => { lat: ~2276, lng: ~2276, area_m2: ~5182586 }
107
+ ```
108
+
109
+ ## Checking Availability
110
+
111
+ ```ruby
112
+ Geodetic::Coordinate::H3.available? # => true if libh3 is found
113
+ ```
114
+
115
+ ## Conversions
116
+
117
+ All conversions chain through LLA. The datum parameter defaults to `Geodetic::WGS84`.
118
+
119
+ ### Instance Methods
120
+
121
+ ```ruby
122
+ coord.to_lla # => LLA (centroid of the cell)
123
+ coord.to_ecef
124
+ coord.to_utm
125
+ coord.to_enu(reference_lla)
126
+ coord.to_ned(reference_lla)
127
+ coord.to_mgrs
128
+ coord.to_usng
129
+ coord.to_web_mercator
130
+ coord.to_ups
131
+ coord.to_state_plane(zone_code)
132
+ coord.to_bng
133
+ coord.to_gh36
134
+ coord.to_gh
135
+ coord.to_ham
136
+ coord.to_olc
137
+ coord.to_georef
138
+ coord.to_gars
139
+ ```
140
+
141
+ ### Class Methods
142
+
143
+ ```ruby
144
+ H3.from_lla(lla_coord)
145
+ H3.from_ecef(ecef_coord)
146
+ H3.from_utm(utm_coord)
147
+ H3.from_web_mercator(wm_coord)
148
+ H3.from_gh(gh_coord)
149
+ H3.from_georef(georef_coord)
150
+ H3.from_gars(gars_coord)
151
+ # ... and all other coordinate systems
152
+ ```
153
+
154
+ ### LLA Convenience Methods
155
+
156
+ ```ruby
157
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.689167, lng: -74.044444)
158
+ h3 = lla.to_h3 # default resolution 7
159
+ h3 = lla.to_h3(precision: 9) # resolution 9
160
+
161
+ lla = Geodetic::Coordinate::LLA.from_h3(h3)
162
+ ```
163
+
164
+ ## Serialization
165
+
166
+ ### `to_s(format = nil)`
167
+
168
+ Returns the hex string. Pass `:integer` to get the 64-bit integer value.
169
+
170
+ ```ruby
171
+ coord = H3.new("872a1072bffffff")
172
+ coord.to_s # => "872a1072bffffff"
173
+ coord.to_s(:integer) # => 608693941536498687
174
+ coord.h3_index # => 608693941536498687
175
+ ```
176
+
177
+ ### `to_a`
178
+
179
+ Returns `[lat, lng]` of the cell centroid.
180
+
181
+ ```ruby
182
+ coord.to_a # => [40.685..., -74.030...]
183
+ ```
184
+
185
+ ### `from_string` / `from_array`
186
+
187
+ ```ruby
188
+ H3.from_string("872a1072bffffff") # from hex string
189
+ H3.from_array([40.689167, -74.044444]) # from [lat, lng]
190
+ ```
191
+
192
+ ## Neighbors
193
+
194
+ Returns all adjacent cells as an Array of H3 instances. Hexagons have 6 neighbors; pentagons have 5.
195
+
196
+ Note: unlike the rectangular spatial hashes which return a directional Hash (`:N`, `:S`, etc.), H3 returns a flat Array because hexagonal cells do not have cardinal directions.
197
+
198
+ ```ruby
199
+ coord = H3.new("872a1072bffffff")
200
+ neighbors = coord.neighbors
201
+ # => [H3, H3, H3, H3, H3, H3]
202
+
203
+ neighbors.length # => 6
204
+ ```
205
+
206
+ ## Grid Disk
207
+
208
+ The `grid_disk(k)` method returns all cells within `k` steps. This is a generalization of `neighbors` (which is `grid_disk(1)` minus self).
209
+
210
+ ```ruby
211
+ coord.grid_disk(0) # => [self] (1 cell)
212
+ coord.grid_disk(1) # => [self + 6 neighbors] (7 cells)
213
+ coord.grid_disk(2) # => 19 cells
214
+ ```
215
+
216
+ ## Parent and Children
217
+
218
+ Navigate the H3 hierarchy by moving to coarser or finer resolution levels.
219
+
220
+ ```ruby
221
+ coord = H3.new("872a1072bffffff") # resolution 7
222
+ parent = coord.parent(5) # => H3 at resolution 5
223
+ parent.resolution # => 5
224
+
225
+ children = coord.children(8) # => Array of 7 H3 cells at resolution 8
226
+ children.length # => 7
227
+ children.first.resolution # => 8
228
+ ```
229
+
230
+ `parent` raises `ArgumentError` if the target resolution is not coarser (lower number). `children` raises `ArgumentError` if the target resolution is not finer (higher number).
231
+
232
+ ## Area
233
+
234
+ The `to_area` method returns the hexagonal cell boundary as an `Areas::Polygon` with 6 vertices (5 for pentagons).
235
+
236
+ ```ruby
237
+ area = coord.to_area
238
+ # => Geodetic::Areas::Polygon
239
+
240
+ area.includes?(coord.to_lla) # => true (centroid is inside the cell)
241
+ area.boundary.length # => 7 (6 vertices + closing point)
242
+ ```
243
+
244
+ ## Pentagon Detection
245
+
246
+ 12 cells at each resolution level are pentagons (artifacts of the icosahedral projection). These have 5 neighbors and 5 boundary vertices instead of 6.
247
+
248
+ ```ruby
249
+ coord.pentagon? # => false (most cells are hexagons)
250
+ ```
251
+
252
+ ## Cell Area
253
+
254
+ ```ruby
255
+ coord.cell_area # => 5182586.98 (square meters)
256
+ ```
257
+
258
+ ## Equality
259
+
260
+ Two H3 instances are equal if their hex strings match exactly.
261
+
262
+ ```ruby
263
+ H3.new("872a1072bffffff") == H3.new("872a1072bffffff") # => true
264
+ H3.new("872a1072bffffff") == H3.new(0x872a1072bffffff) # => true (integer)
265
+ H3.new("872a1072bffffff") == H3.new("87195da49ffffff") # => false
266
+ ```
267
+
268
+ ## `valid?`
269
+
270
+ Returns `true` if the H3 cell index is valid according to the H3 library.
271
+
272
+ ```ruby
273
+ coord.valid? # => true
274
+ ```
275
+
276
+ ## Universal Distance and Bearing Methods
277
+
278
+ H3 supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
279
+
280
+ ```ruby
281
+ a = H3.new("872a1072bffffff") # Statue of Liberty area
282
+ b = H3.new("87195da49ffffff") # London area
283
+
284
+ a.distance_to(b) # => Distance (~5,570 km)
285
+ a.straight_line_distance_to(b) # => Distance
286
+ a.bearing_to(b) # => Bearing
287
+ a.elevation_to(b) # => Float (degrees)
288
+ ```
289
+
290
+ ## Well-Known H3 Cells
291
+
292
+ | Location | H3 Index (res 7) | Resolution |
293
+ |----------|------------------|------------|
294
+ | Statue of Liberty | `872a1072bffffff` | 7 |
295
+ | London | `87195da49ffffff` | 7 |
296
+ | Null Island (0, 0) | `87754e64dffffff` | 7 |
297
+
298
+ ## Implementation Notes
299
+
300
+ Geodetic uses Ruby's `fiddle` (part of the standard library) to call the H3 v4 C API directly. No gem dependency beyond `fiddle` is required. The H3 C library must be installed separately.
301
+
302
+ The library search order is:
303
+ 1. `LIBH3_PATH` environment variable
304
+ 2. `/opt/homebrew/lib/libh3.dylib` (macOS ARM)
305
+ 3. `/usr/local/lib/libh3.dylib` (macOS Intel)
306
+ 4. `/usr/lib/libh3.so` (Linux)
307
+ 5. `/usr/local/lib/libh3.so` (Linux local)
308
+ 6. Architecture-specific Linux paths
@@ -118,10 +118,6 @@ coord.to_s(6) # => "FN31pr"
118
118
  coord.to_s(4) # => "FN31"
119
119
  ```
120
120
 
121
- ### `to_slug`
122
-
123
- Alias for `to_s`. The locator is already URL-safe.
124
-
125
121
  ### `to_a`
126
122
 
127
123
  Returns `[lat, lng]` of the cell midpoint.