geodetic 0.1.0 → 0.2.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -4
  3. data/README.md +5 -5
  4. data/docs/coordinate-systems/bng.md +5 -5
  5. data/docs/coordinate-systems/ecef.md +23 -23
  6. data/docs/coordinate-systems/enu.md +3 -3
  7. data/docs/coordinate-systems/gars.md +246 -0
  8. data/docs/coordinate-systems/georef.md +221 -0
  9. data/docs/coordinate-systems/gh.md +7 -7
  10. data/docs/coordinate-systems/gh36.md +6 -6
  11. data/docs/coordinate-systems/ham.md +6 -6
  12. data/docs/coordinate-systems/index.md +38 -34
  13. data/docs/coordinate-systems/lla.md +26 -26
  14. data/docs/coordinate-systems/mgrs.md +3 -3
  15. data/docs/coordinate-systems/ned.md +3 -3
  16. data/docs/coordinate-systems/olc.md +6 -6
  17. data/docs/coordinate-systems/state-plane.md +2 -2
  18. data/docs/coordinate-systems/ups.md +4 -4
  19. data/docs/coordinate-systems/usng.md +2 -2
  20. data/docs/coordinate-systems/utm.md +23 -23
  21. data/docs/coordinate-systems/web-mercator.md +7 -7
  22. data/docs/getting-started/installation.md +17 -17
  23. data/docs/getting-started/quick-start.md +8 -8
  24. data/docs/index.md +21 -19
  25. data/docs/reference/areas.md +15 -15
  26. data/docs/reference/conversions.md +31 -31
  27. data/docs/reference/geoid-height.md +5 -5
  28. data/docs/reference/serialization.md +44 -44
  29. data/examples/01_basic_conversions.rb +10 -10
  30. data/examples/02_all_coordinate_systems.rb +24 -24
  31. data/lib/geodetic/areas/circle.rb +1 -1
  32. data/lib/geodetic/areas/polygon.rb +2 -2
  33. data/lib/geodetic/areas/rectangle.rb +6 -6
  34. data/lib/geodetic/{coordinates → coordinate}/bng.rb +3 -37
  35. data/lib/geodetic/{coordinates → coordinate}/ecef.rb +3 -33
  36. data/lib/geodetic/{coordinates → coordinate}/enu.rb +21 -1
  37. data/lib/geodetic/coordinate/gars.rb +233 -0
  38. data/lib/geodetic/coordinate/georef.rb +204 -0
  39. data/lib/geodetic/coordinate/gh.rb +161 -0
  40. data/lib/geodetic/{coordinates → coordinate}/gh36.rb +28 -187
  41. data/lib/geodetic/coordinate/ham.rb +226 -0
  42. data/lib/geodetic/{coordinates → coordinate}/lla.rb +23 -1
  43. data/lib/geodetic/{coordinates → coordinate}/mgrs.rb +3 -33
  44. data/lib/geodetic/{coordinates → coordinate}/ned.rb +21 -1
  45. data/lib/geodetic/{coordinates → coordinate}/olc.rb +19 -225
  46. data/lib/geodetic/coordinate/spatial_hash.rb +342 -0
  47. data/lib/geodetic/{coordinates → coordinate}/state_plane.rb +21 -1
  48. data/lib/geodetic/{coordinates → coordinate}/ups.rb +3 -37
  49. data/lib/geodetic/{coordinates → coordinate}/usng.rb +3 -33
  50. data/lib/geodetic/{coordinates → coordinate}/utm.rb +3 -33
  51. data/lib/geodetic/{coordinates → coordinate}/web_mercator.rb +3 -33
  52. data/lib/geodetic/{coordinates.rb → coordinate.rb} +61 -45
  53. data/lib/geodetic/version.rb +1 -1
  54. data/lib/geodetic.rb +1 -1
  55. data/spatial_hash_idea.md +241 -0
  56. metadata +27 -20
  57. data/lib/geodetic/coordinates/gh.rb +0 -372
  58. data/lib/geodetic/coordinates/ham.rb +0 -435
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b413145dfecf16a20a9c7ac563811ed5cc2ae5784ae07a246a88d589704173b
4
- data.tar.gz: 64b5d90e49b653a42ba469248dbcd4723e4b561f59475862a20ba4c7ce28366e
3
+ metadata.gz: 782ba82d99727dfa9a075fcdfb9dea89f631ebc78e0e105293745c64f77f9dae
4
+ data.tar.gz: 59e9c844470d77026a9929c8b9ffd01a5d065fd491d8efb67106b0903267c681
5
5
  SHA512:
6
- metadata.gz: e5902317105ba68039818520b7a0b0ca6938e0f349b3123efe2e0d100a16f670ed0bc7571292f5691f1fcba20e7e0644baea192bf91306da972725a5984d3e0f
7
- data.tar.gz: '08dd192001b74b25a46303fc61572bd1b5da7899d0b995a8538bece0f3ef616db9a340c9e8c542a5577d09f2e4c8cade454dada0a275404b7b1c6bab3bc4278d'
6
+ metadata.gz: 38543deb9e8d62aa20b4a90c9602b0e97806d7d2f55dd9d308a138b9aea2b033e6f51656a9d247ce750d5abcd5298f170825a3f0e4f013a63198bc467e1f3d8c
7
+ data.tar.gz: fcb62fac876d350bd784d08bb17d45b7c14218a31ff6cfcce4be7e384e3f9a6a2c711ae3731b4a4241e5b34c062a8ddcdc2fb8ce6786ac114ff35d872ad8a7f3
data/CHANGELOG.md CHANGED
@@ -10,15 +10,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
10
10
 
11
11
  ## [Unreleased]
12
12
 
13
+
14
+ ## [0.2.0] - 2026-03-08
15
+
16
+ ### Added
17
+
18
+ - **2 new coordinate systems** bringing the total from 15 to 17:
19
+ - `Geodetic::Coordinate::GEOREF` — World Geographic Reference System (aviation/military geocode with variable precision from 15-degree tiles to 0.001-minute resolution)
20
+ - `Geodetic::Coordinate::GARS` — Global Area Reference System (NGA standard with 30-minute cells, 15-minute quadrants, and 5-minute keypads)
21
+ - Full cross-system conversions for GEOREF and GARS — all 17 coordinate systems convert to/from every other system (289 conversion paths)
22
+ - Spatial hash features for GEOREF and GARS: `neighbors`, `to_area`, `precision_in_meters`, `to_slug`, configurable precision
23
+ - Documentation pages: `docs/coordinate-systems/georef.md` and `docs/coordinate-systems/gars.md`
24
+
25
+ ### Changed
26
+
27
+ - **Namespace renamed**: `Geodetic::Coordinates` is now `Geodetic::Coordinate` (singular)
28
+ - **SpatialHash base class** (`lib/geodetic/coordinate/spatial_hash.rb`) — GH36, GH, HAM, OLC, GEOREF, and GARS now inherit from a shared base class that provides common behavior (neighbors, to_area, precision_in_meters, serialization, encoding/decoding contract)
29
+ - **Auto-generated hash conversions** — `SpatialHash.generate_hash_conversions_for` replaces 232 lines of hand-written boilerplate `to_gh`/`from_gh`/`to_ham`/`from_ham`/etc. methods across 7 coordinate classes
30
+ - **Self-registration** — each coordinate class calls `Coordinate.register_class(self)` at load time; `ALL_COORD_CLASSES` is populated from the registry instead of a manual list
31
+
13
32
  ## [0.1.0] - 2026-03-08
14
33
 
15
34
  ### Added
16
35
 
17
36
  - **4 new coordinate systems** bringing the total from 11 to 15:
18
- - `Geodetic::Coordinates::GH36` — Geohash-36 (radix-36 spatial hash, URL-friendly)
19
- - `Geodetic::Coordinates::GH` — Geohash base-32 (standard geohash, supported by Elasticsearch, Redis, PostGIS)
20
- - `Geodetic::Coordinates::HAM` — Maidenhead Locator System (amateur radio grid squares)
21
- - `Geodetic::Coordinates::OLC` — Open Location Code / Plus Codes (Google's location encoding)
37
+ - `Geodetic::Coordinate::GH36` — Geohash-36 (radix-36 spatial hash, URL-friendly)
38
+ - `Geodetic::Coordinate::GH` — Geohash base-32 (standard geohash, supported by Elasticsearch, Redis, PostGIS)
39
+ - `Geodetic::Coordinate::HAM` — Maidenhead Locator System (amateur radio grid squares)
40
+ - `Geodetic::Coordinate::OLC` — Open Location Code / Plus Codes (Google's location encoding)
22
41
  - **Full cross-system conversions** — all 15 coordinate systems convert to/from every other system (225 conversion paths)
23
42
  - **Spatial hash features** for GH36, GH, HAM, and OLC:
24
43
  - `neighbors` — returns all 8 adjacent grid cells
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>15 Coordinate Systems</strong> - LLA, ECEF, UTM, ENU, NED, MGRS, USNG, Web Mercator, UPS, State Plane, BNG, GH36, GH, HAM, OLC<br>
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>
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 15 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>
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
31
 
32
32
  ## Installation
33
33
 
@@ -63,12 +63,12 @@ ned = Coordinates::NED.new(n: 200.0, e: 100.0, d: -50.0)
63
63
 
64
64
  ### GCS Shorthand
65
65
 
66
- `GCS` is a top-level alias for `Geodetic::Coordinates`, providing a concise way to create and work with coordinates:
66
+ `GCS` is a top-level alias for `Geodetic::Coordinate`, providing a concise way to create and work with coordinates:
67
67
 
68
68
  ```ruby
69
69
  require "geodetic"
70
70
 
71
- # Use GCS as a shorthand for Geodetic::Coordinates
71
+ # Use GCS as a shorthand for Geodetic::Coordinate
72
72
  seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
73
73
  ecef = GCS::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
74
74
  ```
@@ -542,7 +542,7 @@ The [`examples/`](examples/) directory contains runnable demo scripts showing pr
542
542
  | Script | Description |
543
543
  |--------|-------------|
544
544
  | [`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 15 coordinate systems, cross-system chains, and areas |
545
+ | [`02_all_coordinate_systems.rb`](examples/02_all_coordinate_systems.rb) | All 17 coordinate systems, cross-system chains, and areas |
546
546
  | [`03_distance_calculations.rb`](examples/03_distance_calculations.rb) | Distance class features, unit conversions, and arithmetic |
547
547
  | [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
548
548
 
@@ -1,4 +1,4 @@
1
- # Geodetic::Coordinates::BNG
1
+ # Geodetic::Coordinate::BNG
2
2
 
3
3
  ## British National Grid
4
4
 
@@ -9,13 +9,13 @@ The British National Grid (BNG) is the official coordinate system for Great Brit
9
9
  Create a BNG coordinate from numeric easting/northing values:
10
10
 
11
11
  ```ruby
12
- point = Geodetic::Coordinates::BNG.new(easting: 530000.0, northing: 180000.0)
12
+ point = Geodetic::Coordinate::BNG.new(easting: 530000.0, northing: 180000.0)
13
13
  ```
14
14
 
15
15
  Alternatively, create from an alphanumeric grid reference string:
16
16
 
17
17
  ```ruby
18
- point = Geodetic::Coordinates::BNG.new(grid_ref: "TQ 300000 800000")
18
+ point = Geodetic::Coordinate::BNG.new(grid_ref: "TQ 300000 800000")
19
19
  ```
20
20
 
21
21
  ## Grid References
@@ -52,8 +52,8 @@ point.valid? # => true if within Great Britain bounds
52
52
  The universal `distance_to` method computes the Vincenty great-circle distance to any other coordinate type, returning a `Distance` object. The `straight_line_distance_to` method computes the Euclidean distance in ECEF space. The universal `bearing_to` method computes the great-circle forward azimuth, returning a `Bearing` object. All accept single or multiple targets.
53
53
 
54
54
  ```ruby
55
- bng_a = Geodetic::Coordinates::BNG.new(easting: 530000.0, northing: 180000.0)
56
- bng_b = Geodetic::Coordinates::BNG.new(easting: 540000.0, northing: 190000.0)
55
+ bng_a = Geodetic::Coordinate::BNG.new(easting: 530000.0, northing: 180000.0)
56
+ bng_b = Geodetic::Coordinate::BNG.new(easting: 540000.0, northing: 190000.0)
57
57
  bng_a.distance_to(bng_b) # => Distance (great-circle)
58
58
  bng_a.straight_line_distance_to(bng_b) # => Distance (Euclidean)
59
59
  bng_a.bearing_to(bng_b) # => Bearing (great-circle forward azimuth)
@@ -1,4 +1,4 @@
1
- # Geodetic::Coordinates::ECEF
1
+ # Geodetic::Coordinate::ECEF
2
2
 
3
3
  Earth-Centered, Earth-Fixed -- a Cartesian coordinate system with its origin at the center of mass of the Earth. The X axis points toward the intersection of the Prime Meridian and the Equator, the Y axis points toward 90 degrees East longitude on the Equator, and the Z axis points toward the North Pole. All values are in meters.
4
4
 
@@ -7,7 +7,7 @@ ECEF is useful for satellite positioning, radar tracking, and any application re
7
7
  ## Constructor
8
8
 
9
9
  ```ruby
10
- Geodetic::Coordinates::ECEF.new(x: 0.0, y: 0.0, z: 0.0)
10
+ Geodetic::Coordinate::ECEF.new(x: 0.0, y: 0.0, z: 0.0)
11
11
  ```
12
12
 
13
13
  | Parameter | Type | Default | Description |
@@ -35,9 +35,9 @@ All conversion methods accept an optional `datum` parameter (defaults to `Geodet
35
35
  Converts to geodetic Latitude, Longitude, Altitude coordinates using an iterative algorithm. The iteration converges when both latitude and altitude changes are below `1e-12`, with a maximum of 100 iterations.
36
36
 
37
37
  ```ruby
38
- ecef = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
38
+ ecef = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
39
39
  lla = ecef.to_lla
40
- # => Geodetic::Coordinates::LLA
40
+ # => Geodetic::Coordinate::LLA
41
41
  ```
42
42
 
43
43
  ### ECEF.from_lla(lla, datum = WGS84)
@@ -45,7 +45,7 @@ lla = ecef.to_lla
45
45
  Creates an ECEF from an LLA instance. Raises `ArgumentError` if the argument is not an `LLA`.
46
46
 
47
47
  ```ruby
48
- ecef = Geodetic::Coordinates::ECEF.from_lla(lla)
48
+ ecef = Geodetic::Coordinate::ECEF.from_lla(lla)
49
49
  ```
50
50
 
51
51
  ### to_utm(datum = WGS84)
@@ -54,7 +54,7 @@ Converts to Universal Transverse Mercator coordinates. Internally converts to LL
54
54
 
55
55
  ```ruby
56
56
  utm = ecef.to_utm
57
- # => Geodetic::Coordinates::UTM
57
+ # => Geodetic::Coordinate::UTM
58
58
  ```
59
59
 
60
60
  ### ECEF.from_utm(utm, datum = WGS84)
@@ -66,10 +66,10 @@ Creates an ECEF from a UTM instance. Raises `ArgumentError` if the argument is n
66
66
  Converts to East-North-Up local tangent plane coordinates relative to a reference ECEF position. If `reference_lla` is not provided, it is computed from `reference_ecef` via `to_lla`.
67
67
 
68
68
  ```ruby
69
- ref_ecef = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
70
- point_ecef = Geodetic::Coordinates::ECEF.new(x: 1130740.0, y: -4828573.0, z: 3991580.0)
69
+ ref_ecef = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
70
+ point_ecef = Geodetic::Coordinate::ECEF.new(x: 1130740.0, y: -4828573.0, z: 3991580.0)
71
71
  enu = point_ecef.to_enu(ref_ecef)
72
- # => Geodetic::Coordinates::ENU
72
+ # => Geodetic::Coordinate::ENU
73
73
  ```
74
74
 
75
75
  Raises `ArgumentError` if `reference_ecef` is not an `ECEF`.
@@ -84,7 +84,7 @@ Converts to North-East-Down local tangent plane coordinates. Internally converts
84
84
 
85
85
  ```ruby
86
86
  ned = point_ecef.to_ned(ref_ecef)
87
- # => Geodetic::Coordinates::NED
87
+ # => Geodetic::Coordinate::NED
88
88
  ```
89
89
 
90
90
  ### ECEF.from_ned(ned, reference_ecef, reference_lla = nil)
@@ -98,7 +98,7 @@ Creates an ECEF from a NED instance and a reference ECEF origin. Raises `Argumen
98
98
  Returns a comma-separated string of `x, y, z`.
99
99
 
100
100
  ```ruby
101
- ecef = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
101
+ ecef = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
102
102
  ecef.to_s
103
103
  # => "1130730.0, -4828583.0, 3991570.0"
104
104
  ```
@@ -117,7 +117,7 @@ ecef.to_a
117
117
  Parses a comma-separated string into an ECEF.
118
118
 
119
119
  ```ruby
120
- ecef = Geodetic::Coordinates::ECEF.from_string("1130730.0, -4828583.0, 3991570.0")
120
+ ecef = Geodetic::Coordinate::ECEF.from_string("1130730.0, -4828583.0, 3991570.0")
121
121
  ```
122
122
 
123
123
  ### ECEF.from_array(array)
@@ -125,7 +125,7 @@ ecef = Geodetic::Coordinates::ECEF.from_string("1130730.0, -4828583.0, 3991570.0
125
125
  Creates an ECEF from a three-element array `[x, y, z]`.
126
126
 
127
127
  ```ruby
128
- ecef = Geodetic::Coordinates::ECEF.from_array([1130730.0, -4828583.0, 3991570.0])
128
+ ecef = Geodetic::Coordinate::ECEF.from_array([1130730.0, -4828583.0, 3991570.0])
129
129
  ```
130
130
 
131
131
  ## Additional Methods
@@ -135,8 +135,8 @@ ecef = Geodetic::Coordinates::ECEF.from_array([1130730.0, -4828583.0, 3991570.0]
135
135
  Compares two ECEF instances for approximate equality. Returns `true` if the absolute difference for each of `x`, `y`, and `z` is `<= 1e-6` meters. Returns `false` if `other` is not an `ECEF`.
136
136
 
137
137
  ```ruby
138
- a = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
139
- b = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
138
+ a = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
139
+ b = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
140
140
  a == b
141
141
  # => true
142
142
  ```
@@ -146,8 +146,8 @@ a == b
146
146
  Computes the Vincenty great-circle distance to one or more other coordinates. Accepts any coordinate type (coordinates are converted to LLA internally). Returns a `Distance` for a single target or an Array of `Distance` objects for multiple targets (radial distances from the receiver).
147
147
 
148
148
  ```ruby
149
- a = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
150
- b = Geodetic::Coordinates::ECEF.new(x: 1130740.0, y: -4828573.0, z: 3991580.0)
149
+ a = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
150
+ b = Geodetic::Coordinate::ECEF.new(x: 1130740.0, y: -4828573.0, z: 3991580.0)
151
151
  a.distance_to(b)
152
152
  # => Distance (meters, great-circle distance)
153
153
  ```
@@ -169,7 +169,7 @@ a.straight_line_distance_to(b)
169
169
  require 'geodetic'
170
170
 
171
171
  # Create an ECEF coordinate
172
- ecef = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
172
+ ecef = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
173
173
 
174
174
  # Convert to LLA and back
175
175
  lla = ecef.to_lla
@@ -181,8 +181,8 @@ ecef == ecef_roundtrip
181
181
  ### Distance between two points
182
182
 
183
183
  ```ruby
184
- station_a = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
185
- station_b = Geodetic::Coordinates::ECEF.new(x: 1131000.0, y: -4828300.0, z: 3991800.0)
184
+ station_a = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
185
+ station_b = Geodetic::Coordinate::ECEF.new(x: 1131000.0, y: -4828300.0, z: 3991800.0)
186
186
 
187
187
  # Great-circle distance (Vincenty)
188
188
  distance = station_a.distance_to(station_b)
@@ -196,8 +196,8 @@ puts "Straight-line distance: #{straight.meters} meters"
196
196
  ### Local tangent plane from ECEF
197
197
 
198
198
  ```ruby
199
- origin = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
200
- target = Geodetic::Coordinates::ECEF.new(x: 1130740.0, y: -4828573.0, z: 3991580.0)
199
+ origin = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
200
+ target = Geodetic::Coordinate::ECEF.new(x: 1130740.0, y: -4828573.0, z: 3991580.0)
201
201
 
202
202
  # Provide reference LLA to avoid recomputing it
203
203
  ref_lla = origin.to_lla
@@ -210,6 +210,6 @@ ned = target.to_ned(origin, ref_lla)
210
210
 
211
211
  ```ruby
212
212
  clarke66 = Geodetic::Datum.new(name: 'CLARKE_1866')
213
- ecef = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
213
+ ecef = Geodetic::Coordinate::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
214
214
  lla = ecef.to_lla(clarke66)
215
215
  ```
@@ -1,4 +1,4 @@
1
- # Geodetic::Coordinates::ENU - East, North, Up
1
+ # Geodetic::Coordinate::ENU - East, North, Up
2
2
 
3
3
  ## Overview
4
4
 
@@ -52,7 +52,7 @@ The one exception is the conversion between ENU and NED, which does not require
52
52
  ENU is a relative coordinate system. The universal `distance_to`, `straight_line_distance_to`, `bearing_to`, and `elevation_to` methods raise `ArgumentError` because ENU cannot be converted to an absolute system without a reference point. Convert to an absolute system first:
53
53
 
54
54
  ```ruby
55
- ref = Geodetic::Coordinates::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
55
+ ref = Geodetic::Coordinate::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
56
56
  lla = enu.to_lla(ref)
57
57
  lla.distance_to(other_lla) # Vincenty great-circle distance
58
58
  lla.bearing_to(other_lla) # Great-circle forward azimuth (Bearing object)
@@ -65,7 +65,7 @@ Bearing is measured in **degrees from north**, clockwise, in the range **0-360**
65
65
  ## Example
66
66
 
67
67
  ```ruby
68
- point = Geodetic::Coordinates::ENU.new(e: 100.0, n: 200.0, u: 50.0)
68
+ point = Geodetic::Coordinate::ENU.new(e: 100.0, n: 200.0, u: 50.0)
69
69
 
70
70
  point.east # => 100.0
71
71
  point.north # => 200.0
@@ -0,0 +1,246 @@
1
+ # Geodetic::Coordinate::GARS
2
+
3
+ ## Global Area Reference System
4
+
5
+ GARS is a standardized geospatial reference system developed by the National Geospatial-Intelligence Agency (NGA). It divides the Earth into hierarchical grid cells at three precision levels, designed for military targeting, air defense, and joint operations.
6
+
7
+ The encoding uses a false coordinate origin that shifts longitude by +180 and latitude by +90. Cells are identified by a combination of numeric longitude bands, alphabetic latitude bands, and optional subdivision digits:
8
+
9
+ | Level | Characters | Size | Description |
10
+ |-------|-----------|------|-------------|
11
+ | **30-minute cell** | 5 (NNNll) | 30' x 30' | 3-digit lon band + 2-letter lat band |
12
+ | **Quadrant** | 6 (NNNllq) | 15' x 15' | + quadrant digit 1-4 |
13
+ | **Keypad** | 7 (NNNllqk) | 5' x 5' | + keypad digit 1-9 |
14
+
15
+ ### Quadrant Layout
16
+
17
+ Within each 30-minute cell, quadrants are numbered:
18
+
19
+ ```
20
+ +---+---+
21
+ | 1 | 2 | (north)
22
+ +---+---+
23
+ | 3 | 4 | (south)
24
+ +---+---+
25
+ ```
26
+
27
+ ### Keypad Layout
28
+
29
+ Within each quadrant, keypads follow telephone-style numbering:
30
+
31
+ ```
32
+ +---+---+---+
33
+ | 1 | 2 | 3 | (north)
34
+ +---+---+---+
35
+ | 4 | 5 | 6 |
36
+ +---+---+---+
37
+ | 7 | 8 | 9 | (south)
38
+ +---+---+---+
39
+ ```
40
+
41
+ GARS is a **2D coordinate system** (no altitude). Conversions to/from other systems go through LLA as the intermediary. Each code represents a rectangular cell; the coordinate's point value is the cell's midpoint.
42
+
43
+ ## Constructor
44
+
45
+ ```ruby
46
+ # From a GARS string
47
+ coord = Geodetic::Coordinate::GARS.new("006AG39")
48
+
49
+ # From any coordinate (converts via LLA)
50
+ coord = Geodetic::Coordinate::GARS.new(lla_coord)
51
+ coord = Geodetic::Coordinate::GARS.new(utm_coord, precision: 6)
52
+ ```
53
+
54
+ | Parameter | Type | Default | Description |
55
+ |-------------|-------------------|---------|--------------------------------------|
56
+ | `source` | String or Coord | -- | A GARS string or any coordinate object |
57
+ | `precision` | Integer | 7 | Code length: 5, 6, or 7 |
58
+
59
+ Raises `ArgumentError` if the source string is empty, has an invalid length, contains an out-of-range longitude band (must be 001-720), invalid latitude letters (I and O excluded), invalid quadrant digit (must be 1-4), or invalid keypad digit (must be 1-9). Letter input is case-insensitive (normalized to uppercase).
60
+
61
+ ## Attributes
62
+
63
+ | Attribute | Type | Access | Description |
64
+ |-----------|--------|-----------|------------------------|
65
+ | `code` | String | read-only | The GARS code string |
66
+
67
+ GARS is **immutable** -- there are no setter methods.
68
+
69
+ ## Precision
70
+
71
+ The precision (code length) determines the size of the encoded cell:
72
+
73
+ | Length | Level | Approximate Cell Size |
74
+ |--------|-------|----------------------|
75
+ | 5 | 30-minute cell | ~55.6 km x 55.6 km |
76
+ | 6 | 15-minute quadrant | ~27.8 km x 27.8 km |
77
+ | 7 | 5-minute keypad | ~9.3 km x 9.3 km |
78
+
79
+ ```ruby
80
+ coord.precision # => 7 (code length)
81
+ coord.precision_in_meters # => { lat: ~9260, lng: ~... }
82
+ ```
83
+
84
+ ## Conversions
85
+
86
+ All conversions chain through LLA. The datum parameter defaults to `Geodetic::WGS84`.
87
+
88
+ ### Instance Methods
89
+
90
+ ```ruby
91
+ coord.to_lla # => LLA (midpoint of the cell)
92
+ coord.to_ecef
93
+ coord.to_utm
94
+ coord.to_enu(reference_lla)
95
+ coord.to_ned(reference_lla)
96
+ coord.to_mgrs
97
+ coord.to_usng
98
+ coord.to_web_mercator
99
+ coord.to_ups
100
+ coord.to_state_plane(zone_code)
101
+ coord.to_bng
102
+ coord.to_gh36
103
+ coord.to_gh
104
+ coord.to_ham
105
+ coord.to_olc
106
+ coord.to_georef
107
+ ```
108
+
109
+ ### Class Methods
110
+
111
+ ```ruby
112
+ GARS.from_lla(lla_coord)
113
+ GARS.from_ecef(ecef_coord)
114
+ GARS.from_utm(utm_coord)
115
+ GARS.from_web_mercator(wm_coord)
116
+ GARS.from_gh(gh_coord)
117
+ GARS.from_georef(georef_coord)
118
+ # ... and all other coordinate systems
119
+ ```
120
+
121
+ ### LLA Convenience Methods
122
+
123
+ ```ruby
124
+ lla = Geodetic::Coordinate::LLA.new(lat: 40.7128, lng: -74.0060)
125
+ gars = lla.to_gars # default precision 7
126
+ gars = lla.to_gars(precision: 5) # 30-minute cell only
127
+
128
+ lla = Geodetic::Coordinate::LLA.from_gars(gars)
129
+ ```
130
+
131
+ ## Serialization
132
+
133
+ ### `to_s(truncate_to = nil)`
134
+
135
+ Returns the GARS string. An optional integer truncates to that precision (re-encodes from decoded coordinates).
136
+
137
+ ```ruby
138
+ coord = GARS.new("006AG39")
139
+ coord.to_s # => "006AG39"
140
+ coord.to_s(6) # => "006AG3"
141
+ coord.to_s(5) # => "006AG"
142
+ ```
143
+
144
+ ### `to_slug`
145
+
146
+ Alias for `to_s`. GARS codes are already URL-safe.
147
+
148
+ ### `to_a`
149
+
150
+ Returns `[lat, lng]` of the cell midpoint.
151
+
152
+ ```ruby
153
+ coord.to_a # => [lat, lng]
154
+ ```
155
+
156
+ ### `from_string` / `from_array`
157
+
158
+ ```ruby
159
+ GARS.from_string("006AG39") # from GARS string
160
+ GARS.from_array([40.0, -74.0]) # from [lat, lng]
161
+ ```
162
+
163
+ ## Neighbors
164
+
165
+ Returns all 8 adjacent cells as GARS instances.
166
+
167
+ ```ruby
168
+ coord = GARS.new("361HN35")
169
+ neighbors = coord.neighbors
170
+ # => { N: GARS, S: GARS, E: GARS, W: GARS, NE: GARS, NW: GARS, SE: GARS, SW: GARS }
171
+
172
+ neighbors[:N].to_lla.lat > coord.to_lla.lat # => true
173
+ neighbors[:E].to_lla.lng > coord.to_lla.lng # => true
174
+ ```
175
+
176
+ Neighbors preserve the same precision as the original code. Latitude is clamped to valid range near the poles; longitude wraps at the antimeridian.
177
+
178
+ ## Area
179
+
180
+ The `to_area` method returns the GARS cell as a `Geodetic::Areas::Rectangle`.
181
+
182
+ ```ruby
183
+ area = coord.to_area
184
+ # => Geodetic::Areas::Rectangle
185
+
186
+ area.includes?(coord.to_lla) # => true (midpoint is inside the cell)
187
+ area.nw # => LLA (northwest corner)
188
+ area.se # => LLA (southeast corner)
189
+ ```
190
+
191
+ ## Equality
192
+
193
+ Two GARS instances are equal if their code strings match exactly.
194
+
195
+ ```ruby
196
+ GARS.new("006AG39") == GARS.new("006AG39") # => true
197
+ GARS.new("006AG39") == GARS.new("006AG38") # => false
198
+ ```
199
+
200
+ ## `valid?`
201
+
202
+ Returns `true` if the code has a valid length (5, 6, or 7), a longitude band between 001-720, valid latitude letters (A-Z excluding I and O), a valid quadrant digit (1-4), and a valid keypad digit (1-9).
203
+
204
+ ```ruby
205
+ coord.valid? # => true
206
+ ```
207
+
208
+ ## Universal Distance and Bearing Methods
209
+
210
+ GARS supports all universal distance and bearing methods via the `DistanceMethods` and `BearingMethods` mixins:
211
+
212
+ ```ruby
213
+ a = GARS.new("361HN35")
214
+ b = GARS.new("212LX43")
215
+
216
+ a.distance_to(b) # => Distance
217
+ a.straight_line_distance_to(b) # => Distance
218
+ a.bearing_to(b) # => Bearing
219
+ a.elevation_to(b) # => Float (degrees)
220
+ ```
221
+
222
+ ## Encoding Details
223
+
224
+ ### Longitude Bands
225
+
226
+ 720 bands of 0.5 degrees each, numbered 001-720 from west to east:
227
+ - Band 001: -180.0 to -179.5
228
+ - Band 361: 0.0 to 0.5
229
+ - Band 720: 179.5 to 180.0
230
+
231
+ ### Latitude Bands
232
+
233
+ 360 bands of 0.5 degrees each, encoded as 2-letter pairs (AA-QZ) from south to north:
234
+ - AA: -90.0 to -89.5
235
+ - HN: 0.0 to 0.5
236
+ - QZ: 89.5 to 90.0
237
+
238
+ The 24-letter alphabet (A-Z, excluding I and O) gives 24 x 15 = 360 valid combinations.
239
+
240
+ ## Well-Known GARS Codes
241
+
242
+ | Location | GARS Code | Description |
243
+ |----------|-----------|-------------|
244
+ | Null Island (0, 0) | 361HN | 30-minute cell at equator/prime meridian |
245
+ | New York City | 212LX43 | 5-minute cell |
246
+ | Western boundary | 001xx | Longitude -180.0 |