geodetic 0.2.0 → 0.3.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 +9 -4
- data/README.md +17 -3
- data/docs/coordinate-systems/h3.md +312 -0
- data/docs/coordinate-systems/index.md +23 -21
- data/docs/index.md +4 -3
- data/lib/geodetic/coordinate/enu.rb +9 -0
- data/lib/geodetic/coordinate/h3.rb +413 -0
- data/lib/geodetic/coordinate/lla.rb +8 -0
- data/lib/geodetic/coordinate/ned.rb +9 -0
- data/lib/geodetic/coordinate/state_plane.rb +9 -0
- data/lib/geodetic/coordinate.rb +1 -0
- data/lib/geodetic/version.rb +1 -1
- metadata +7 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d81eb7c69d9aff7d512562f54e2413c91c8ff10b2361c9143a8afd0c1af0ca0
|
|
4
|
+
data.tar.gz: e7fb09dd1f7d4d97f57ea48f9cacb0e6afbd30abedab1aa04cbf1153c2fe4373
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 90a2e1b8bbc1c6bdf59c7e326d4965f8ec85a16682bccba948fd1f104d7818a5fa34ee179fa00de0221cbfd0e9d9afbd5c667aac46c1a6c9c070b338618b461a
|
|
7
|
+
data.tar.gz: 85b40d6e961ec4782a9748232eed4191df1dd8a5f22df77afe68c63d64b6b23df0e8ff5ec4ae927038d60919a7825e09c2429bbd4aa8d4447b1737ff81aa4dfd
|
data/CHANGELOG.md
CHANGED
|
@@ -15,12 +15,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
15
15
|
|
|
16
16
|
### Added
|
|
17
17
|
|
|
18
|
-
- **
|
|
18
|
+
- **3 new coordinate systems** bringing the total from 15 to 18:
|
|
19
19
|
- `Geodetic::Coordinate::GEOREF` — World Geographic Reference System (aviation/military geocode with variable precision from 15-degree tiles to 0.001-minute resolution)
|
|
20
20
|
- `Geodetic::Coordinate::GARS` — Global Area Reference System (NGA standard with 30-minute cells, 15-minute quadrants, and 5-minute keypads)
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
21
|
+
- `Geodetic::Coordinate::H3` — Uber's H3 Hexagonal Hierarchical Index (16 resolution levels, hexagonal cells via `libh3` C library through `fiddle`)
|
|
22
|
+
- Full cross-system conversions for GEOREF, GARS, and H3 — all 18 coordinate systems convert to/from every other system (324 conversion paths)
|
|
23
|
+
- Spatial hash features for GEOREF, GARS, and H3: `neighbors`, `to_area`, `precision_in_meters`, `to_slug`, configurable precision
|
|
24
|
+
- H3-specific features: `grid_disk(k)`, `parent(res)`, `children(res)`, `pentagon?`, `cell_area`, `h3_index`, `resolution` (0-15)
|
|
25
|
+
- H3 `to_area` returns `Areas::Polygon` (6 vertices for hexagons, 5 for pentagons) instead of `Areas::Rectangle`
|
|
26
|
+
- H3 `neighbors` returns Array of 6 cells instead of directional Hash with 8 cardinal keys
|
|
27
|
+
- Graceful degradation: H3 raises clear error with installation instructions if `libh3` is not found; all other coordinate systems work normally
|
|
28
|
+
- Documentation pages: `docs/coordinate-systems/georef.md`, `docs/coordinate-systems/gars.md`, and `docs/coordinate-systems/h3.md`
|
|
24
29
|
|
|
25
30
|
### Changed
|
|
26
31
|
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<td width="50%" valign="top">
|
|
14
14
|
<strong>Key Features</strong><br>
|
|
15
15
|
|
|
16
|
-
- <strong>
|
|
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>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
</tr>
|
|
28
28
|
</table>
|
|
29
29
|
|
|
30
|
-
<p>Geodetic enables precise conversion between geodetic coordinate systems in Ruby. All
|
|
30
|
+
<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
31
|
|
|
32
32
|
## Installation
|
|
33
33
|
|
|
@@ -43,6 +43,20 @@ Or install directly:
|
|
|
43
43
|
gem install geodetic
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Optional: H3 Hexagonal Index
|
|
47
|
+
|
|
48
|
+
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.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# macOS
|
|
52
|
+
brew install h3
|
|
53
|
+
|
|
54
|
+
# Linux (build from source)
|
|
55
|
+
# See https://h3geo.org/docs/installation
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
You can also set the `LIBH3_PATH` environment variable to point to a custom `libh3` location.
|
|
59
|
+
|
|
46
60
|
## Usage
|
|
47
61
|
|
|
48
62
|
### Basic Coordinate Creation
|
|
@@ -542,7 +556,7 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
|
|
|
542
556
|
| Script | Description |
|
|
543
557
|
|--------|-------------|
|
|
544
558
|
| [`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
|
|
559
|
+
| [`02_all_coordinate_systems.rb`](examples/02_all_coordinate_systems.rb) | All 18 coordinate systems, cross-system chains, and areas |
|
|
546
560
|
| [`03_distance_calculations.rb`](examples/03_distance_calculations.rb) | Distance class features, unit conversions, and arithmetic |
|
|
547
561
|
| [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
|
|
548
562
|
|
|
@@ -0,0 +1,312 @@
|
|
|
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_slug`
|
|
178
|
+
|
|
179
|
+
Alias for `to_s`. H3 hex strings are already URL-safe.
|
|
180
|
+
|
|
181
|
+
### `to_a`
|
|
182
|
+
|
|
183
|
+
Returns `[lat, lng]` of the cell centroid.
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
coord.to_a # => [40.685..., -74.030...]
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### `from_string` / `from_array`
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
H3.from_string("872a1072bffffff") # from hex string
|
|
193
|
+
H3.from_array([40.689167, -74.044444]) # from [lat, lng]
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Neighbors
|
|
197
|
+
|
|
198
|
+
Returns all adjacent cells as an Array of H3 instances. Hexagons have 6 neighbors; pentagons have 5.
|
|
199
|
+
|
|
200
|
+
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.
|
|
201
|
+
|
|
202
|
+
```ruby
|
|
203
|
+
coord = H3.new("872a1072bffffff")
|
|
204
|
+
neighbors = coord.neighbors
|
|
205
|
+
# => [H3, H3, H3, H3, H3, H3]
|
|
206
|
+
|
|
207
|
+
neighbors.length # => 6
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Grid Disk
|
|
211
|
+
|
|
212
|
+
The `grid_disk(k)` method returns all cells within `k` steps. This is a generalization of `neighbors` (which is `grid_disk(1)` minus self).
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
coord.grid_disk(0) # => [self] (1 cell)
|
|
216
|
+
coord.grid_disk(1) # => [self + 6 neighbors] (7 cells)
|
|
217
|
+
coord.grid_disk(2) # => 19 cells
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Parent and Children
|
|
221
|
+
|
|
222
|
+
Navigate the H3 hierarchy by moving to coarser or finer resolution levels.
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
coord = H3.new("872a1072bffffff") # resolution 7
|
|
226
|
+
parent = coord.parent(5) # => H3 at resolution 5
|
|
227
|
+
parent.resolution # => 5
|
|
228
|
+
|
|
229
|
+
children = coord.children(8) # => Array of 7 H3 cells at resolution 8
|
|
230
|
+
children.length # => 7
|
|
231
|
+
children.first.resolution # => 8
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`parent` raises `ArgumentError` if the target resolution is not coarser (lower number). `children` raises `ArgumentError` if the target resolution is not finer (higher number).
|
|
235
|
+
|
|
236
|
+
## Area
|
|
237
|
+
|
|
238
|
+
The `to_area` method returns the hexagonal cell boundary as an `Areas::Polygon` with 6 vertices (5 for pentagons).
|
|
239
|
+
|
|
240
|
+
```ruby
|
|
241
|
+
area = coord.to_area
|
|
242
|
+
# => Geodetic::Areas::Polygon
|
|
243
|
+
|
|
244
|
+
area.includes?(coord.to_lla) # => true (centroid is inside the cell)
|
|
245
|
+
area.boundary.length # => 7 (6 vertices + closing point)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Pentagon Detection
|
|
249
|
+
|
|
250
|
+
12 cells at each resolution level are pentagons (artifacts of the icosahedral projection). These have 5 neighbors and 5 boundary vertices instead of 6.
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
coord.pentagon? # => false (most cells are hexagons)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Cell Area
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
coord.cell_area # => 5182586.98 (square meters)
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## Equality
|
|
263
|
+
|
|
264
|
+
Two H3 instances are equal if their hex strings match exactly.
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
H3.new("872a1072bffffff") == H3.new("872a1072bffffff") # => true
|
|
268
|
+
H3.new("872a1072bffffff") == H3.new(0x872a1072bffffff) # => true (integer)
|
|
269
|
+
H3.new("872a1072bffffff") == H3.new("87195da49ffffff") # => false
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
## `valid?`
|
|
273
|
+
|
|
274
|
+
Returns `true` if the H3 cell index is valid according to the H3 library.
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
coord.valid? # => true
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Universal Distance and Bearing Methods
|
|
281
|
+
|
|
282
|
+
H3 supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
|
|
283
|
+
|
|
284
|
+
```ruby
|
|
285
|
+
a = H3.new("872a1072bffffff") # Statue of Liberty area
|
|
286
|
+
b = H3.new("87195da49ffffff") # London area
|
|
287
|
+
|
|
288
|
+
a.distance_to(b) # => Distance (~5,570 km)
|
|
289
|
+
a.straight_line_distance_to(b) # => Distance
|
|
290
|
+
a.bearing_to(b) # => Bearing
|
|
291
|
+
a.elevation_to(b) # => Float (degrees)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Well-Known H3 Cells
|
|
295
|
+
|
|
296
|
+
| Location | H3 Index (res 7) | Resolution |
|
|
297
|
+
|----------|------------------|------------|
|
|
298
|
+
| Statue of Liberty | `872a1072bffffff` | 7 |
|
|
299
|
+
| London | `87195da49ffffff` | 7 |
|
|
300
|
+
| Null Island (0, 0) | `87754e64dffffff` | 7 |
|
|
301
|
+
|
|
302
|
+
## Implementation Notes
|
|
303
|
+
|
|
304
|
+
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.
|
|
305
|
+
|
|
306
|
+
The library search order is:
|
|
307
|
+
1. `LIBH3_PATH` environment variable
|
|
308
|
+
2. `/opt/homebrew/lib/libh3.dylib` (macOS ARM)
|
|
309
|
+
3. `/usr/local/lib/libh3.dylib` (macOS Intel)
|
|
310
|
+
4. `/usr/lib/libh3.so` (Linux)
|
|
311
|
+
5. `/usr/local/lib/libh3.so` (Linux local)
|
|
312
|
+
6. Architecture-specific Linux paths
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Coordinate Systems Overview
|
|
2
2
|
|
|
3
|
-
The Geodetic gem supports
|
|
3
|
+
The Geodetic gem supports 18 coordinate systems organized into six categories. All coordinate classes live under `Geodetic::Coordinate`.
|
|
4
4
|
|
|
5
5
|
## Global Systems
|
|
6
6
|
|
|
@@ -46,6 +46,7 @@ The Geodetic gem supports 17 coordinate systems organized into six categories. A
|
|
|
46
46
|
| **OLC** | `Geodetic::Coordinate::OLC` | Open Location Code (Plus Codes). Google's open system for encoding locations into short codes like `849VCWC8+R9`. Uses a 20-character alphabet with 5 paired levels of base-20 encoding plus optional grid refinement. Includes a `+` separator at position 8. Supports neighbor lookup, area extraction, and configurable precision (default 10 characters for ~14 m resolution). |
|
|
47
47
|
| **GEOREF** | `Geodetic::Coordinate::GEOREF` | World Geographic Reference System. A geocode system used in aviation and military applications that encodes positions using letter tiles (15° grid), letter degree subdivisions, and numeric minute pairs. Uses a 24-letter alphabet (A-Z excluding I and O). Supports variable precision from 15° tiles (2 chars) down to 0.01-minute resolution (12 chars). Default precision is 8 characters (1-minute resolution). |
|
|
48
48
|
| **GARS** | `Geodetic::Coordinate::GARS` | Global Area Reference System. An NGA standard that divides the world into 30-minute cells identified by a 3-digit longitude band (001-720) and 2-letter latitude band. Cells are subdivided into 15-minute quadrants (1-4) and 5-minute keypads (1-9, telephone layout). Variable precision: 5 chars (30'), 6 chars (15'), 7 chars (5'). Default precision is 7 characters. |
|
|
49
|
+
| **H3** | `Geodetic::Coordinate::H3` | H3 Hexagonal Hierarchical Index. Uber's spatial indexing system that divides the globe into hexagonal cells (and 12 pentagons) at 16 resolution levels (0-15). Each cell is a 64-bit integer displayed as a hex string. Unlike the rectangular spatial hashes, `to_area` returns an `Areas::Polygon` with 6 vertices (5 for pentagons) and `neighbors` returns an Array of 6 cells. **Requires `libh3` installed** (`brew install h3` on macOS). |
|
|
49
50
|
|
|
50
51
|
## Regional Systems
|
|
51
52
|
|
|
@@ -60,25 +61,26 @@ The Geodetic gem supports 17 coordinate systems organized into six categories. A
|
|
|
60
61
|
|
|
61
62
|
Every coordinate system can convert to every other coordinate system. The table below confirms full interoperability:
|
|
62
63
|
|
|
63
|
-
| From \ To | LLA | ECEF | UTM | ENU | NED | MGRS | USNG | WebMercator | UPS | StatePlane | BNG | GH36 | GH | HAM | OLC | GEOREF | GARS |
|
|
64
|
-
|
|
65
|
-
| **LLA** | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
66
|
-
| **ECEF** | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
67
|
-
| **UTM** | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
68
|
-
| **ENU** | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
69
|
-
| **NED** | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
70
|
-
| **MGRS** | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
71
|
-
| **USNG** | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
72
|
-
| **WebMercator**| Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
73
|
-
| **UPS** | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
74
|
-
| **StatePlane** | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y |
|
|
75
|
-
| **BNG** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y |
|
|
76
|
-
| **GH36** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y |
|
|
77
|
-
| **GH** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y |
|
|
78
|
-
| **HAM** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y |
|
|
79
|
-
| **OLC** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y |
|
|
80
|
-
| **GEOREF** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y |
|
|
81
|
-
| **GARS** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- |
|
|
64
|
+
| From \ To | LLA | ECEF | UTM | ENU | NED | MGRS | USNG | WebMercator | UPS | StatePlane | BNG | GH36 | GH | HAM | OLC | GEOREF | GARS | H3 |
|
|
65
|
+
|-----------|-----|------|-----|-----|-----|------|------|-------------|-----|------------|-----|------|----|----|-----|--------|------|-----|
|
|
66
|
+
| **LLA** | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
67
|
+
| **ECEF** | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
68
|
+
| **UTM** | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
69
|
+
| **ENU** | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
70
|
+
| **NED** | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
71
|
+
| **MGRS** | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
72
|
+
| **USNG** | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
73
|
+
| **WebMercator**| Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
74
|
+
| **UPS** | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
75
|
+
| **StatePlane** | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y | Y |
|
|
76
|
+
| **BNG** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y | Y |
|
|
77
|
+
| **GH36** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y | Y |
|
|
78
|
+
| **GH** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y | Y |
|
|
79
|
+
| **HAM** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y | Y |
|
|
80
|
+
| **OLC** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y | Y |
|
|
81
|
+
| **GEOREF** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y | Y |
|
|
82
|
+
| **GARS** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- | Y |
|
|
83
|
+
| **H3** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -- |
|
|
82
84
|
|
|
83
85
|
## Universal Distance and Bearing Calculations
|
|
84
86
|
|
|
@@ -98,6 +100,6 @@ Conversions typically route through **LLA** or **ECEF** as intermediate steps:
|
|
|
98
100
|
- **ECEF** is the intermediate for local tangent plane systems (ENU, NED), since the rotation from global Cartesian to local frames is straightforward in ECEF.
|
|
99
101
|
- **ENU and NED** convert between each other directly by reordering axes and inverting the vertical component.
|
|
100
102
|
- **MGRS and USNG** route through UTM, which in turn routes through LLA.
|
|
101
|
-
- **WebMercator, UPS, BNG, StatePlane, GH36, GH, HAM, OLC, GEOREF, and
|
|
103
|
+
- **WebMercator, UPS, BNG, StatePlane, GH36, GH, HAM, OLC, GEOREF, GARS, and H3** all convert through LLA.
|
|
102
104
|
|
|
103
105
|
For example, converting from BNG to NED follows the chain: `BNG -> LLA -> ECEF -> ENU -> NED`. The gem handles this automatically when you call a conversion method.
|
data/docs/index.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<td width="50%" valign="top">
|
|
10
10
|
<h2>Key Features</h2>
|
|
11
11
|
<lu>
|
|
12
|
-
<li><strong>
|
|
12
|
+
<li><strong>18 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC<br>
|
|
13
13
|
<li><strong>Full Bidirectional Conversions</strong> - Every system converts to and from every other system<br>
|
|
14
14
|
<li><strong>Distance Calculations</strong> - Vincenty great-circle and straight-line with unit tracking<br>
|
|
15
15
|
<li><strong>Bearing Calculations</strong> - Forward azimuth, back azimuth, compass directions, elevation angles<br>
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
</tr>
|
|
25
25
|
</table>
|
|
26
26
|
|
|
27
|
-
Geodetic is a Ruby gem for converting between geodetic coordinate systems. It provides a clean, consistent API for working with
|
|
27
|
+
Geodetic is a Ruby gem for converting between geodetic coordinate systems. It provides a clean, consistent API for working with 18 coordinate systems, 16 geodetic datums, geoid height calculations, and geographic area computations.
|
|
28
28
|
|
|
29
29
|
## Coordinate Systems
|
|
30
30
|
|
|
31
|
-
Geodetic supports full bidirectional conversion between all
|
|
31
|
+
Geodetic supports full bidirectional conversion between all 18 coordinate systems:
|
|
32
32
|
|
|
33
33
|
| System | Class | Description |
|
|
34
34
|
|--------|-------|-------------|
|
|
@@ -49,6 +49,7 @@ Geodetic supports full bidirectional conversion between all 17 coordinate system
|
|
|
49
49
|
| **OLC** | `Geodetic::Coordinate::OLC` | Open Location Code / Plus Codes (Google's location encoding) |
|
|
50
50
|
| **GEOREF** | `Geodetic::Coordinate::GEOREF` | World Geographic Reference System (aviation/military) |
|
|
51
51
|
| **GARS** | `Geodetic::Coordinate::GARS` | Global Area Reference System (NGA standard) |
|
|
52
|
+
| **H3** | `Geodetic::Coordinate::H3` | Uber's hexagonal hierarchical index (requires `libh3`) |
|
|
52
53
|
|
|
53
54
|
## Additional Features
|
|
54
55
|
|
|
@@ -212,6 +212,15 @@ module Geodetic
|
|
|
212
212
|
from_lla(lla, reference_lla)
|
|
213
213
|
end
|
|
214
214
|
|
|
215
|
+
def to_h3(reference_lla, precision: 7)
|
|
216
|
+
H3.new(to_lla(reference_lla), precision: precision)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.from_h3(h3_coord, reference_lla)
|
|
220
|
+
lla = h3_coord.to_lla
|
|
221
|
+
from_lla(lla, reference_lla)
|
|
222
|
+
end
|
|
223
|
+
|
|
215
224
|
def to_s(precision = 2)
|
|
216
225
|
precision = precision.to_i
|
|
217
226
|
if precision == 0
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# H3 (Uber's Hexagonal Hierarchical Spatial Index) Coordinate
|
|
4
|
+
#
|
|
5
|
+
# A hierarchical geospatial index that divides the globe into hexagonal cells
|
|
6
|
+
# (and 12 pentagons) at 16 resolution levels (0-15). Each cell is identified
|
|
7
|
+
# by a 64-bit integer (H3Index), typically displayed as a 15-character hex string.
|
|
8
|
+
#
|
|
9
|
+
# REQUIRES: libh3 shared library (brew install h3)
|
|
10
|
+
# Uses Ruby's fiddle (stdlib) to call H3 v4 C API — no gem dependency.
|
|
11
|
+
#
|
|
12
|
+
# Key differences from other spatial hashes:
|
|
13
|
+
# - Cells are hexagons (6 vertices), not rectangles
|
|
14
|
+
# - to_area returns Areas::Polygon, not Areas::Rectangle
|
|
15
|
+
# - neighbors returns an Array (6 cells), not a directional Hash
|
|
16
|
+
# - "precision" maps to H3 resolution (0-15), not string length
|
|
17
|
+
#
|
|
18
|
+
# Usage:
|
|
19
|
+
# H3.new("872a1072bffffff") # from hex string
|
|
20
|
+
# H3.new(0x872a1072bffffff) # from integer
|
|
21
|
+
# H3.new(lla_coord) # from any coordinate
|
|
22
|
+
# H3.new(lla_coord, precision: 9) # resolution 9
|
|
23
|
+
|
|
24
|
+
require_relative 'spatial_hash'
|
|
25
|
+
|
|
26
|
+
module Geodetic
|
|
27
|
+
module Coordinate
|
|
28
|
+
class H3 < SpatialHash
|
|
29
|
+
# --- FFI bindings to libh3 via fiddle ---
|
|
30
|
+
|
|
31
|
+
module LibH3
|
|
32
|
+
require 'fiddle'
|
|
33
|
+
|
|
34
|
+
SEARCH_PATHS = [
|
|
35
|
+
ENV['LIBH3_PATH'],
|
|
36
|
+
'/opt/homebrew/lib/libh3.dylib',
|
|
37
|
+
'/opt/homebrew/lib/libh3.1.dylib',
|
|
38
|
+
'/usr/local/lib/libh3.dylib',
|
|
39
|
+
'/usr/local/lib/libh3.1.dylib',
|
|
40
|
+
'/usr/lib/libh3.so',
|
|
41
|
+
'/usr/lib/libh3.so.1',
|
|
42
|
+
'/usr/local/lib/libh3.so',
|
|
43
|
+
'/usr/local/lib/libh3.so.1',
|
|
44
|
+
'/usr/lib/x86_64-linux-gnu/libh3.so',
|
|
45
|
+
'/usr/lib/aarch64-linux-gnu/libh3.so',
|
|
46
|
+
].compact.freeze
|
|
47
|
+
|
|
48
|
+
@handle = nil
|
|
49
|
+
@available = false
|
|
50
|
+
|
|
51
|
+
class << self
|
|
52
|
+
attr_reader :handle
|
|
53
|
+
|
|
54
|
+
def available?
|
|
55
|
+
@available
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def require_library!
|
|
59
|
+
return if @available
|
|
60
|
+
raise Geodetic::Error,
|
|
61
|
+
"libh3 not found. Install H3: brew install h3 (macOS) " \
|
|
62
|
+
"or see https://h3geo.org/docs/installation. " \
|
|
63
|
+
"Set LIBH3_PATH env var to specify a custom library path."
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
begin
|
|
68
|
+
path = SEARCH_PATHS.find { |p| File.exist?(p) }
|
|
69
|
+
if path
|
|
70
|
+
@handle = Fiddle.dlopen(path)
|
|
71
|
+
@available = true
|
|
72
|
+
end
|
|
73
|
+
rescue Fiddle::DLError
|
|
74
|
+
@available = false
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Struct sizes
|
|
78
|
+
SIZEOF_LATLNG = 2 * Fiddle::SIZEOF_DOUBLE # 16 bytes
|
|
79
|
+
SIZEOF_CELL_BOUNDARY = 8 + 10 * SIZEOF_LATLNG # 168 bytes (int + pad + 10 LatLngs)
|
|
80
|
+
SIZEOF_H3INDEX = Fiddle::SIZEOF_LONG_LONG # 8 bytes
|
|
81
|
+
SIZEOF_INT64 = Fiddle::SIZEOF_LONG_LONG # 8 bytes
|
|
82
|
+
|
|
83
|
+
# Function bindings (lazy-loaded)
|
|
84
|
+
def self.bind(name, args, ret)
|
|
85
|
+
return nil unless @available
|
|
86
|
+
ptr = @handle[name]
|
|
87
|
+
Fiddle::Function.new(ptr, args, ret)
|
|
88
|
+
rescue Fiddle::DLError
|
|
89
|
+
nil
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# H3Error latLngToCell(const LatLng *g, int res, H3Index *out)
|
|
93
|
+
F_LAT_LNG_TO_CELL = bind('latLngToCell',
|
|
94
|
+
[Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
|
|
95
|
+
Fiddle::TYPE_INT)
|
|
96
|
+
|
|
97
|
+
# H3Error cellToLatLng(H3Index h3, LatLng *g)
|
|
98
|
+
F_CELL_TO_LAT_LNG = bind('cellToLatLng',
|
|
99
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_VOIDP],
|
|
100
|
+
Fiddle::TYPE_INT)
|
|
101
|
+
|
|
102
|
+
# H3Error cellToBoundary(H3Index h3, CellBoundary *gp)
|
|
103
|
+
F_CELL_TO_BOUNDARY = bind('cellToBoundary',
|
|
104
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_VOIDP],
|
|
105
|
+
Fiddle::TYPE_INT)
|
|
106
|
+
|
|
107
|
+
# int getResolution(H3Index h)
|
|
108
|
+
F_GET_RESOLUTION = bind('getResolution',
|
|
109
|
+
[Fiddle::TYPE_LONG_LONG],
|
|
110
|
+
Fiddle::TYPE_INT)
|
|
111
|
+
|
|
112
|
+
# int isValidCell(H3Index h)
|
|
113
|
+
F_IS_VALID_CELL = bind('isValidCell',
|
|
114
|
+
[Fiddle::TYPE_LONG_LONG],
|
|
115
|
+
Fiddle::TYPE_INT)
|
|
116
|
+
|
|
117
|
+
# int isPentagon(H3Index h)
|
|
118
|
+
F_IS_PENTAGON = bind('isPentagon',
|
|
119
|
+
[Fiddle::TYPE_LONG_LONG],
|
|
120
|
+
Fiddle::TYPE_INT)
|
|
121
|
+
|
|
122
|
+
# H3Error gridDisk(H3Index origin, int k, H3Index *out)
|
|
123
|
+
F_GRID_DISK = bind('gridDisk',
|
|
124
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
|
|
125
|
+
Fiddle::TYPE_INT)
|
|
126
|
+
|
|
127
|
+
# H3Error maxGridDiskSize(int k, int64_t *out)
|
|
128
|
+
F_MAX_GRID_DISK_SIZE = bind('maxGridDiskSize',
|
|
129
|
+
[Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
|
|
130
|
+
Fiddle::TYPE_INT)
|
|
131
|
+
|
|
132
|
+
# H3Error cellAreaM2(H3Index h, double *out)
|
|
133
|
+
F_CELL_AREA_M2 = bind('cellAreaM2',
|
|
134
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_VOIDP],
|
|
135
|
+
Fiddle::TYPE_INT)
|
|
136
|
+
|
|
137
|
+
# H3Error cellToParent(H3Index h, int parentRes, H3Index *out)
|
|
138
|
+
F_CELL_TO_PARENT = bind('cellToParent',
|
|
139
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
|
|
140
|
+
Fiddle::TYPE_INT)
|
|
141
|
+
|
|
142
|
+
# H3Error cellToChildrenSize(H3Index h, int childRes, int64_t *out)
|
|
143
|
+
F_CELL_TO_CHILDREN_SIZE = bind('cellToChildrenSize',
|
|
144
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
|
|
145
|
+
Fiddle::TYPE_INT)
|
|
146
|
+
|
|
147
|
+
# H3Error cellToChildren(H3Index h, int childRes, H3Index *out)
|
|
148
|
+
F_CELL_TO_CHILDREN = bind('cellToChildren',
|
|
149
|
+
[Fiddle::TYPE_LONG_LONG, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP],
|
|
150
|
+
Fiddle::TYPE_INT)
|
|
151
|
+
|
|
152
|
+
# --- High-level wrappers ---
|
|
153
|
+
|
|
154
|
+
def self.lat_lng_to_cell(lat_rad, lng_rad, resolution)
|
|
155
|
+
require_library!
|
|
156
|
+
latlng = Fiddle::Pointer.malloc(SIZEOF_LATLNG, Fiddle::RUBY_FREE)
|
|
157
|
+
latlng[0, SIZEOF_LATLNG] = [lat_rad, lng_rad].pack('d2')
|
|
158
|
+
out = Fiddle::Pointer.malloc(SIZEOF_H3INDEX, Fiddle::RUBY_FREE)
|
|
159
|
+
err = F_LAT_LNG_TO_CELL.call(latlng, resolution, out)
|
|
160
|
+
raise ArgumentError, "H3 latLngToCell error (code #{err})" unless err == 0
|
|
161
|
+
out[0, SIZEOF_H3INDEX].unpack1('Q')
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def self.cell_to_lat_lng(h3_index)
|
|
165
|
+
require_library!
|
|
166
|
+
latlng = Fiddle::Pointer.malloc(SIZEOF_LATLNG, Fiddle::RUBY_FREE)
|
|
167
|
+
err = F_CELL_TO_LAT_LNG.call(h3_index, latlng)
|
|
168
|
+
raise ArgumentError, "H3 cellToLatLng error (code #{err})" unless err == 0
|
|
169
|
+
lat_rad, lng_rad = latlng[0, SIZEOF_LATLNG].unpack('d2')
|
|
170
|
+
{ lat: lat_rad * DEG_PER_RAD, lng: lng_rad * DEG_PER_RAD }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.cell_to_boundary(h3_index)
|
|
174
|
+
require_library!
|
|
175
|
+
cb = Fiddle::Pointer.malloc(SIZEOF_CELL_BOUNDARY, Fiddle::RUBY_FREE)
|
|
176
|
+
err = F_CELL_TO_BOUNDARY.call(h3_index, cb)
|
|
177
|
+
raise ArgumentError, "H3 cellToBoundary error (code #{err})" unless err == 0
|
|
178
|
+
num_verts = cb[0, 4].unpack1('i')
|
|
179
|
+
# Vertices start at offset 8 (4 byte int + 4 byte padding)
|
|
180
|
+
verts = cb[8, num_verts * SIZEOF_LATLNG].unpack("d#{num_verts * 2}")
|
|
181
|
+
(0...num_verts).map do |i|
|
|
182
|
+
{ lat: verts[i * 2] * DEG_PER_RAD, lng: verts[i * 2 + 1] * DEG_PER_RAD }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.get_resolution(h3_index)
|
|
187
|
+
require_library!
|
|
188
|
+
F_GET_RESOLUTION.call(h3_index)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def self.is_valid_cell(h3_index)
|
|
192
|
+
require_library!
|
|
193
|
+
F_IS_VALID_CELL.call(h3_index) == 1
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def self.is_pentagon(h3_index)
|
|
197
|
+
require_library!
|
|
198
|
+
F_IS_PENTAGON.call(h3_index) == 1
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def self.grid_disk(h3_index, k)
|
|
202
|
+
require_library!
|
|
203
|
+
size_ptr = Fiddle::Pointer.malloc(SIZEOF_INT64, Fiddle::RUBY_FREE)
|
|
204
|
+
err = F_MAX_GRID_DISK_SIZE.call(k, size_ptr)
|
|
205
|
+
raise ArgumentError, "H3 maxGridDiskSize error (code #{err})" unless err == 0
|
|
206
|
+
max_size = size_ptr[0, SIZEOF_INT64].unpack1('q')
|
|
207
|
+
|
|
208
|
+
out = Fiddle::Pointer.malloc(max_size * SIZEOF_H3INDEX, Fiddle::RUBY_FREE)
|
|
209
|
+
err = F_GRID_DISK.call(h3_index, k, out)
|
|
210
|
+
raise ArgumentError, "H3 gridDisk error (code #{err})" unless err == 0
|
|
211
|
+
|
|
212
|
+
out[0, max_size * SIZEOF_H3INDEX].unpack("Q#{max_size}").reject(&:zero?)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def self.cell_area_m2(h3_index)
|
|
216
|
+
require_library!
|
|
217
|
+
out = Fiddle::Pointer.malloc(Fiddle::SIZEOF_DOUBLE, Fiddle::RUBY_FREE)
|
|
218
|
+
err = F_CELL_AREA_M2.call(h3_index, out)
|
|
219
|
+
raise ArgumentError, "H3 cellAreaM2 error (code #{err})" unless err == 0
|
|
220
|
+
out[0, Fiddle::SIZEOF_DOUBLE].unpack1('d')
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def self.cell_to_parent(h3_index, parent_res)
|
|
224
|
+
require_library!
|
|
225
|
+
out = Fiddle::Pointer.malloc(SIZEOF_H3INDEX, Fiddle::RUBY_FREE)
|
|
226
|
+
err = F_CELL_TO_PARENT.call(h3_index, parent_res, out)
|
|
227
|
+
raise ArgumentError, "H3 cellToParent error (code #{err})" unless err == 0
|
|
228
|
+
out[0, SIZEOF_H3INDEX].unpack1('Q')
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def self.cell_to_children(h3_index, child_res)
|
|
232
|
+
require_library!
|
|
233
|
+
size_ptr = Fiddle::Pointer.malloc(SIZEOF_INT64, Fiddle::RUBY_FREE)
|
|
234
|
+
err = F_CELL_TO_CHILDREN_SIZE.call(h3_index, child_res, size_ptr)
|
|
235
|
+
raise ArgumentError, "H3 cellToChildrenSize error (code #{err})" unless err == 0
|
|
236
|
+
count = size_ptr[0, SIZEOF_INT64].unpack1('q')
|
|
237
|
+
|
|
238
|
+
out = Fiddle::Pointer.malloc(count * SIZEOF_H3INDEX, Fiddle::RUBY_FREE)
|
|
239
|
+
err = F_CELL_TO_CHILDREN.call(h3_index, child_res, out)
|
|
240
|
+
raise ArgumentError, "H3 cellToChildren error (code #{err})" unless err == 0
|
|
241
|
+
|
|
242
|
+
out[0, count * SIZEOF_H3INDEX].unpack("Q#{count}").reject(&:zero?)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# --- Class configuration ---
|
|
247
|
+
|
|
248
|
+
attr_reader :code
|
|
249
|
+
|
|
250
|
+
def self.default_precision = 7
|
|
251
|
+
def self.hash_system_name = :h3
|
|
252
|
+
|
|
253
|
+
def self.available?
|
|
254
|
+
LibH3.available?
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# --- Constructor ---
|
|
258
|
+
|
|
259
|
+
def initialize(source, precision: self.class.default_precision)
|
|
260
|
+
case source
|
|
261
|
+
when Integer
|
|
262
|
+
hex = format('%x', source)
|
|
263
|
+
validate_code!(hex)
|
|
264
|
+
set_code(hex)
|
|
265
|
+
when String
|
|
266
|
+
normalized = normalize(source.strip)
|
|
267
|
+
validate_code!(normalized)
|
|
268
|
+
set_code(normalized)
|
|
269
|
+
when LLA
|
|
270
|
+
set_code(encode(source.lat, source.lng, precision))
|
|
271
|
+
else
|
|
272
|
+
if source.respond_to?(:to_lla)
|
|
273
|
+
lla = source.to_lla
|
|
274
|
+
set_code(encode(lla.lat, lla.lng, precision))
|
|
275
|
+
else
|
|
276
|
+
raise ArgumentError,
|
|
277
|
+
"Expected an H3 hex String, Integer, or coordinate object, got #{source.class}"
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# --- H3-specific methods ---
|
|
283
|
+
|
|
284
|
+
# The 64-bit H3 cell index
|
|
285
|
+
def h3_index
|
|
286
|
+
@h3_index ||= code.to_i(16)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# H3 resolution (0-15)
|
|
290
|
+
def precision
|
|
291
|
+
@resolution ||= LibH3.get_resolution(h3_index)
|
|
292
|
+
end
|
|
293
|
+
alias_method :resolution, :precision
|
|
294
|
+
|
|
295
|
+
# Is this cell a pentagon? (12 per resolution level)
|
|
296
|
+
def pentagon?
|
|
297
|
+
LibH3.is_pentagon(h3_index)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Parent cell at a coarser resolution
|
|
301
|
+
def parent(parent_resolution)
|
|
302
|
+
raise ArgumentError, "Parent resolution must be < #{resolution}" unless parent_resolution < resolution
|
|
303
|
+
parent_idx = LibH3.cell_to_parent(h3_index, parent_resolution)
|
|
304
|
+
self.class.new(parent_idx)
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# Child cells at a finer resolution
|
|
308
|
+
def children(child_resolution)
|
|
309
|
+
raise ArgumentError, "Child resolution must be > #{resolution}" unless child_resolution > resolution
|
|
310
|
+
child_indices = LibH3.cell_to_children(h3_index, child_resolution)
|
|
311
|
+
child_indices.map { |idx| self.class.new(idx) }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# All cells within k steps (k=0 returns self, k=1 returns 7 cells, etc.)
|
|
315
|
+
def grid_disk(k = 1)
|
|
316
|
+
indices = LibH3.grid_disk(h3_index, k)
|
|
317
|
+
indices.map { |idx| self.class.new(idx) }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Cell area in square meters
|
|
321
|
+
def cell_area
|
|
322
|
+
LibH3.cell_area_m2(h3_index)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# --- Override SpatialHash methods ---
|
|
326
|
+
|
|
327
|
+
def valid?
|
|
328
|
+
LibH3.is_valid_cell(h3_index)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Returns all adjacent cells as an Array of H3 instances.
|
|
332
|
+
# Hexagons have 6 neighbors; pentagons have 5.
|
|
333
|
+
# Note: returns Array, not directional Hash (hexagons have no cardinal directions).
|
|
334
|
+
def neighbors
|
|
335
|
+
indices = LibH3.grid_disk(h3_index, 1)
|
|
336
|
+
indices.reject { |idx| idx == h3_index }.map { |idx| self.class.new(idx) }
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Returns the hexagonal cell boundary as an Areas::Polygon.
|
|
340
|
+
def to_area
|
|
341
|
+
verts = LibH3.cell_to_boundary(h3_index)
|
|
342
|
+
boundary = verts.map { |v| LLA.new(lat: v[:lat], lng: v[:lng], alt: 0.0) }
|
|
343
|
+
Areas::Polygon.new(boundary: boundary)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Returns approximate precision in meters as { lat:, lng:, area_m2: }
|
|
347
|
+
def precision_in_meters
|
|
348
|
+
area = cell_area
|
|
349
|
+
edge = Math.sqrt(area)
|
|
350
|
+
{ lat: edge, lng: edge, area_m2: area }
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def to_s(format = nil)
|
|
354
|
+
if format == :integer
|
|
355
|
+
h3_index
|
|
356
|
+
else
|
|
357
|
+
code
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def code_value
|
|
362
|
+
@code
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
protected
|
|
366
|
+
|
|
367
|
+
def normalize(string)
|
|
368
|
+
string.downcase.delete_prefix('0x')
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def set_code(value)
|
|
372
|
+
@code = value
|
|
373
|
+
@h3_index = nil
|
|
374
|
+
@resolution = nil
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
private
|
|
378
|
+
|
|
379
|
+
def encode(lat, lng, resolution = self.class.default_precision)
|
|
380
|
+
resolution = resolution.clamp(0, 15)
|
|
381
|
+
lat_rad = lat * RAD_PER_DEG
|
|
382
|
+
lng_rad = lng * RAD_PER_DEG
|
|
383
|
+
idx = LibH3.lat_lng_to_cell(lat_rad, lng_rad, resolution)
|
|
384
|
+
format('%x', idx)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def decode(code_string)
|
|
388
|
+
idx = code_string.to_i(16)
|
|
389
|
+
LibH3.cell_to_lat_lng(idx)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def decode_bounds(code_string)
|
|
393
|
+
idx = code_string.to_i(16)
|
|
394
|
+
verts = LibH3.cell_to_boundary(idx)
|
|
395
|
+
lats = verts.map { |v| v[:lat] }
|
|
396
|
+
lngs = verts.map { |v| v[:lng] }
|
|
397
|
+
{ min_lat: lats.min, max_lat: lats.max, min_lng: lngs.min, max_lng: lngs.max }
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def validate_h3!(code_string)
|
|
401
|
+
raise ArgumentError, "H3 code cannot be empty" if code_string.empty?
|
|
402
|
+
raise ArgumentError, "Invalid H3 hex string: #{code_string}" unless code_string.match?(/\A[0-9a-f]+\z/)
|
|
403
|
+
idx = code_string.to_i(16)
|
|
404
|
+
raise ArgumentError, "Invalid H3 cell index: #{code_string}" unless LibH3.is_valid_cell(idx)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
alias_method :validate_code!, :validate_h3!
|
|
408
|
+
|
|
409
|
+
register_hash_system(:h3, self, default_precision: 7)
|
|
410
|
+
Coordinate.register_class(self)
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
end
|
|
@@ -270,6 +270,14 @@ module Geodetic
|
|
|
270
270
|
gars_coord.to_lla(datum)
|
|
271
271
|
end
|
|
272
272
|
|
|
273
|
+
def to_h3(precision: 7)
|
|
274
|
+
H3.new(self, precision: precision)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def self.from_h3(h3_coord, datum = WGS84)
|
|
278
|
+
h3_coord.to_lla(datum)
|
|
279
|
+
end
|
|
280
|
+
|
|
273
281
|
def self.from_lla(lla_coord, datum = WGS84)
|
|
274
282
|
lla_coord
|
|
275
283
|
end
|
|
@@ -193,6 +193,15 @@ module Geodetic
|
|
|
193
193
|
from_lla(lla, reference_lla)
|
|
194
194
|
end
|
|
195
195
|
|
|
196
|
+
def to_h3(reference_lla, precision: 7)
|
|
197
|
+
H3.new(to_lla(reference_lla), precision: precision)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.from_h3(h3_coord, reference_lla)
|
|
201
|
+
lla = h3_coord.to_lla
|
|
202
|
+
from_lla(lla, reference_lla)
|
|
203
|
+
end
|
|
204
|
+
|
|
196
205
|
def to_s(precision = 2)
|
|
197
206
|
precision = precision.to_i
|
|
198
207
|
if precision == 0
|
|
@@ -305,6 +305,15 @@ module Geodetic
|
|
|
305
305
|
from_lla(lla_coord, zone_code, datum)
|
|
306
306
|
end
|
|
307
307
|
|
|
308
|
+
def to_h3(datum = nil, precision: 7)
|
|
309
|
+
H3.new(to_lla(datum), precision: precision)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def self.from_h3(h3_coord, zone_code, datum = WGS84)
|
|
313
|
+
lla_coord = h3_coord.to_lla(datum)
|
|
314
|
+
from_lla(lla_coord, zone_code, datum)
|
|
315
|
+
end
|
|
316
|
+
|
|
308
317
|
# Unit conversion methods
|
|
309
318
|
def to_meters
|
|
310
319
|
zone_info = ZONES[@zone_code]
|
data/lib/geodetic/coordinate.rb
CHANGED
data/lib/geodetic/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: geodetic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -9,11 +9,11 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description: A Ruby gem for converting between
|
|
12
|
+
description: A Ruby gem for converting between 18 geodetic coordinate systems (LLA,
|
|
13
13
|
ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH,
|
|
14
|
-
HAM, OLC, GEOREF, GARS) with Vincenty great-circle and ECEF Euclidean distance
|
|
15
|
-
unit-aware Distance and Bearing classes, geoid height support, and
|
|
16
|
-
operations.
|
|
14
|
+
HAM, OLC, GEOREF, GARS, H3) with Vincenty great-circle and ECEF Euclidean distance
|
|
15
|
+
calculations, unit-aware Distance and Bearing classes, geoid height support, and
|
|
16
|
+
geographic area operations.
|
|
17
17
|
email:
|
|
18
18
|
- dewayne@vanhoozer.me
|
|
19
19
|
executables: []
|
|
@@ -35,6 +35,7 @@ files:
|
|
|
35
35
|
- docs/coordinate-systems/georef.md
|
|
36
36
|
- docs/coordinate-systems/gh.md
|
|
37
37
|
- docs/coordinate-systems/gh36.md
|
|
38
|
+
- docs/coordinate-systems/h3.md
|
|
38
39
|
- docs/coordinate-systems/ham.md
|
|
39
40
|
- docs/coordinate-systems/index.md
|
|
40
41
|
- docs/coordinate-systems/lla.md
|
|
@@ -72,6 +73,7 @@ files:
|
|
|
72
73
|
- lib/geodetic/coordinate/georef.rb
|
|
73
74
|
- lib/geodetic/coordinate/gh.rb
|
|
74
75
|
- lib/geodetic/coordinate/gh36.rb
|
|
76
|
+
- lib/geodetic/coordinate/h3.rb
|
|
75
77
|
- lib/geodetic/coordinate/ham.rb
|
|
76
78
|
- lib/geodetic/coordinate/lla.rb
|
|
77
79
|
- lib/geodetic/coordinate/mgrs.rb
|