geodetic 0.0.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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/CHANGELOG.md +15 -0
  5. data/COMMITS.md +196 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +471 -0
  8. data/Rakefile +8 -0
  9. data/docs/coordinate-systems/bng.md +60 -0
  10. data/docs/coordinate-systems/ecef.md +215 -0
  11. data/docs/coordinate-systems/enu.md +77 -0
  12. data/docs/coordinate-systems/gh36.md +192 -0
  13. data/docs/coordinate-systems/index.md +93 -0
  14. data/docs/coordinate-systems/lla.md +304 -0
  15. data/docs/coordinate-systems/mgrs.md +81 -0
  16. data/docs/coordinate-systems/ned.md +83 -0
  17. data/docs/coordinate-systems/state-plane.md +60 -0
  18. data/docs/coordinate-systems/ups.md +53 -0
  19. data/docs/coordinate-systems/usng.md +74 -0
  20. data/docs/coordinate-systems/utm.md +257 -0
  21. data/docs/coordinate-systems/web-mercator.md +67 -0
  22. data/docs/getting-started/installation.md +65 -0
  23. data/docs/getting-started/quick-start.md +175 -0
  24. data/docs/index.md +58 -0
  25. data/docs/reference/areas.md +195 -0
  26. data/docs/reference/conversions.md +351 -0
  27. data/docs/reference/datums.md +134 -0
  28. data/docs/reference/geoid-height.md +182 -0
  29. data/docs/reference/serialization.md +252 -0
  30. data/examples/01_basic_conversions.rb +187 -0
  31. data/examples/02_all_coordinate_systems.rb +310 -0
  32. data/examples/03_distance_calculations.rb +224 -0
  33. data/examples/04_bearing_calculations.rb +236 -0
  34. data/lib/geodetic/areas/circle.rb +29 -0
  35. data/lib/geodetic/areas/polygon.rb +57 -0
  36. data/lib/geodetic/areas/rectangle.rb +55 -0
  37. data/lib/geodetic/areas.rb +5 -0
  38. data/lib/geodetic/bearing.rb +94 -0
  39. data/lib/geodetic/coordinates/bng.rb +366 -0
  40. data/lib/geodetic/coordinates/ecef.rb +229 -0
  41. data/lib/geodetic/coordinates/enu.rb +244 -0
  42. data/lib/geodetic/coordinates/gh36.rb +384 -0
  43. data/lib/geodetic/coordinates/lla.rb +268 -0
  44. data/lib/geodetic/coordinates/mgrs.rb +317 -0
  45. data/lib/geodetic/coordinates/ned.rb +246 -0
  46. data/lib/geodetic/coordinates/state_plane.rb +451 -0
  47. data/lib/geodetic/coordinates/ups.rb +325 -0
  48. data/lib/geodetic/coordinates/usng.rb +274 -0
  49. data/lib/geodetic/coordinates/utm.rb +261 -0
  50. data/lib/geodetic/coordinates/web_mercator.rb +242 -0
  51. data/lib/geodetic/coordinates.rb +260 -0
  52. data/lib/geodetic/datum.rb +62 -0
  53. data/lib/geodetic/distance.rb +146 -0
  54. data/lib/geodetic/geoid_height.rb +299 -0
  55. data/lib/geodetic/version.rb +5 -0
  56. data/lib/geodetic.rb +13 -0
  57. data/mkdocs.yml +140 -0
  58. data/sig/geodetic.rbs +4 -0
  59. metadata +104 -0
data/README.md ADDED
@@ -0,0 +1,471 @@
1
+ # Geodetic
2
+
3
+ A Ruby gem for converting between geodetic coordinate systems. Supports 12 coordinate systems with full bidirectional conversions, plus geoid height calculations and geographic area operations.
4
+
5
+ ## Coordinate Systems
6
+
7
+ | Class | Description |
8
+ |-------|-------------|
9
+ | `Coordinates::LLA` | Latitude, Longitude, Altitude (degrees/meters) |
10
+ | `Coordinates::ECEF` | Earth-Centered, Earth-Fixed (meters) |
11
+ | `Coordinates::UTM` | Universal Transverse Mercator |
12
+ | `Coordinates::ENU` | East, North, Up (local tangent plane) |
13
+ | `Coordinates::NED` | North, East, Down (local tangent plane) |
14
+ | `Coordinates::MGRS` | Military Grid Reference System |
15
+ | `Coordinates::USNG` | US National Grid |
16
+ | `Coordinates::WebMercator` | Web Mercator / EPSG:3857 |
17
+ | `Coordinates::UPS` | Universal Polar Stereographic |
18
+ | `Coordinates::StatePlane` | US State Plane Coordinate System |
19
+ | `Coordinates::BNG` | British National Grid |
20
+ | `Coordinates::GH36` | Geohash-36 (spatial hash, URL-friendly) |
21
+
22
+ ## Installation
23
+
24
+ Add to your Gemfile:
25
+
26
+ ```ruby
27
+ gem "geodetic"
28
+ ```
29
+
30
+ Or install directly:
31
+
32
+ ```bash
33
+ gem install geodetic
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ### Basic Coordinate Creation
39
+
40
+ All constructors use keyword arguments:
41
+
42
+ ```ruby
43
+ require "geodetic"
44
+
45
+ include Geodetic
46
+
47
+ lla = Coordinates::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
48
+ ecef = Coordinates::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
49
+ utm = Coordinates::UTM.new(easting: 548894.0, northing: 5272748.0, altitude: 184.0, zone: 10, hemisphere: "N")
50
+ enu = Coordinates::ENU.new(e: 100.0, n: 200.0, u: 50.0)
51
+ ned = Coordinates::NED.new(n: 200.0, e: 100.0, d: -50.0)
52
+ ```
53
+
54
+ ### GCS Shorthand
55
+
56
+ `GCS` is a top-level alias for `Geodetic::Coordinates`, providing a concise way to create and work with coordinates:
57
+
58
+ ```ruby
59
+ require "geodetic"
60
+
61
+ # Use GCS as a shorthand for Geodetic::Coordinates
62
+ seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
63
+ ecef = GCS::ECEF.new(x: -2304643.57, y: -3638650.07, z: 4688674.43)
64
+ ```
65
+
66
+ ### Coordinate Conversions
67
+
68
+ Every coordinate system can convert to and from every other system:
69
+
70
+ ```ruby
71
+ lla = Coordinates::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
72
+
73
+ # LLA to other systems
74
+ ecef = lla.to_ecef
75
+ utm = lla.to_utm
76
+ wm = Coordinates::WebMercator.from_lla(lla)
77
+ mgrs = Coordinates::MGRS.from_lla(lla)
78
+
79
+ # Convert back
80
+ lla_roundtrip = ecef.to_lla
81
+
82
+ # Local coordinate systems require a reference point
83
+ reference = Coordinates::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
84
+ enu = lla.to_enu(reference)
85
+ ned = lla.to_ned(reference)
86
+ ```
87
+
88
+ ### Serialization
89
+
90
+ All coordinate classes support `to_s`, `to_a`, `from_string`, and `from_array`. The `to_s` method accepts an optional precision parameter controlling the number of decimal places:
91
+
92
+ ```ruby
93
+ lla = Coordinates::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
94
+
95
+ lla.to_s # => "47.620500, -122.349300, 184.00"
96
+ lla.to_s(3) # => "47.620, -122.349, 184.00"
97
+ lla.to_s(0) # => "48, -122, 184"
98
+ lla.to_a # => [47.6205, -122.3493, 184.0]
99
+
100
+ Coordinates::LLA.from_string("47.6205, -122.3493, 184.0")
101
+ Coordinates::LLA.from_array([47.6205, -122.3493, 184.0])
102
+ ```
103
+
104
+ Default precisions by class: LLA=6, Bearing=4, all others=2. Passing `0` returns integers.
105
+
106
+ ### Validated Setters
107
+
108
+ All coordinate classes provide setter methods with type coercion and validation:
109
+
110
+ ```ruby
111
+ lla = Coordinates::LLA.new(lat: 47.0, lng: -122.0, alt: 100.0)
112
+ lla.lat = 48.0 # validates -90..90
113
+ lla.lng = -121.0 # validates -180..180
114
+ lla.alt = 200.0 # no range constraint
115
+ lla.lat = 91.0 # => ArgumentError
116
+
117
+ utm = Coordinates::UTM.new(easting: 500000.0, northing: 5000000.0, zone: 10, hemisphere: 'N')
118
+ utm.zone = 15 # validates 1..60
119
+ utm.hemisphere = 'S' # validates 'N' or 'S'
120
+ utm.easting = -1.0 # => ArgumentError
121
+
122
+ # UPS cross-validates hemisphere/zone combinations
123
+ ups = Coordinates::UPS.new(hemisphere: 'N', zone: 'Y')
124
+ ups.zone = 'Z' # valid for hemisphere 'N'
125
+ ups.zone = 'A' # => ArgumentError (rolls back)
126
+
127
+ # BNG auto-updates grid_ref when easting/northing change
128
+ bng = Coordinates::BNG.new(easting: 530000, northing: 180000)
129
+ bng.easting = 430000 # grid_ref automatically recalculated
130
+ ```
131
+
132
+ ECEF, ENU, NED, and WebMercator setters coerce to float with no range constraints. MGRS, USNG, GH36, Distance, and Bearing are immutable.
133
+
134
+ ### DMS (Degrees, Minutes, Seconds)
135
+
136
+ ```ruby
137
+ lla = Coordinates::LLA.new(lat: 37.7749, lng: -122.4192, alt: 15.0)
138
+ lla.to_dms # => "37° 46' 29.64\" N, 122° 25' 9.12\" W, 15.00 m"
139
+
140
+ Coordinates::LLA.from_dms("37° 46' 29.64\" N, 122° 25' 9.12\" W, 15.00 m")
141
+ ```
142
+
143
+ ### String-Based Coordinate Systems
144
+
145
+ MGRS and USNG use string representations:
146
+
147
+ ```ruby
148
+ mgrs = Coordinates::MGRS.new(mgrs_string: "18SUJ2337006519")
149
+ mgrs = Coordinates::MGRS.from_string("18SUJ2337006519")
150
+ mgrs.to_s # => "18SUJ2337006519"
151
+
152
+ usng = Coordinates::USNG.new(usng_string: "18T WL 12345 67890")
153
+ usng = Coordinates::USNG.from_string("18T WL 12345 67890")
154
+ usng.to_s # => "18T WL 12345 67890"
155
+ ```
156
+
157
+ ### Distance Calculations
158
+
159
+ Universal distance methods work across all coordinate types and return `Distance` objects with unit tracking and conversion.
160
+
161
+ **Instance method `distance_to`** — Vincenty great-circle distance:
162
+
163
+ ```ruby
164
+ seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
165
+ portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
166
+ sf = GCS::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
167
+
168
+ d = seattle.distance_to(portland) # => Distance (meters)
169
+ d.meters # => 235393.17
170
+ d.to_km.to_f # => 235.39
171
+ d.to_mi.to_f # => 146.28
172
+
173
+ seattle.distance_to(portland, sf) # => [Distance, Distance] (radial)
174
+ seattle.distance_to([portland, sf]) # => [Distance, Distance] (radial)
175
+ ```
176
+
177
+ **Class method `distance_between`** — consecutive chain distances:
178
+
179
+ ```ruby
180
+ GCS.distance_between(seattle, portland) # => Distance
181
+ GCS.distance_between(seattle, portland, sf) # => [Distance, Distance] (chain)
182
+ GCS.distance_between([seattle, portland, sf]) # => [Distance, Distance] (chain)
183
+ ```
184
+
185
+ **Straight-line (ECEF Euclidean) versions:**
186
+
187
+ ```ruby
188
+ seattle.straight_line_distance_to(portland) # => Distance
189
+ GCS.straight_line_distance_between(seattle, portland) # => Distance
190
+ ```
191
+
192
+ **Cross-system distances** — works between any coordinate types:
193
+
194
+ ```ruby
195
+ utm = seattle.to_utm
196
+ mgrs = GCS::MGRS.from_lla(portland)
197
+ utm.distance_to(mgrs) # => Distance
198
+ ```
199
+
200
+ > **Note:** ENU and NED are relative coordinate systems and must be converted to an absolute system before distance and bearing calculations. They retain `local_bearing_to`, `horizontal_distance_to`, and other local methods for tangent-plane operations.
201
+
202
+ ### Bearing Calculations
203
+
204
+ Universal bearing methods work across all coordinate types and return `Bearing` objects.
205
+
206
+ **Instance method `bearing_to`** — great-circle forward azimuth:
207
+
208
+ ```ruby
209
+ seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
210
+ portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
211
+
212
+ b = seattle.bearing_to(portland) # => Bearing
213
+ b.degrees # => 188.2
214
+ b.to_radians # => 3.28...
215
+ b.to_compass # => "S"
216
+ b.to_compass(points: 8) # => "S"
217
+ b.reverse # => Bearing (back azimuth)
218
+ b.to_s # => "188.2036°"
219
+ ```
220
+
221
+ **Instance method `elevation_to`** — vertical look angle:
222
+
223
+ ```ruby
224
+ a = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 0.0)
225
+ b = GCS::LLA.new(lat: 47.62, lng: -122.35, alt: 5000.0)
226
+
227
+ a.elevation_to(b) # => 89.9... (degrees, nearly straight up)
228
+ ```
229
+
230
+ **Class method `bearing_between`** — consecutive chain bearings:
231
+
232
+ ```ruby
233
+ GCS.bearing_between(seattle, portland) # => Bearing
234
+ GCS.bearing_between(seattle, portland, sf) # => [Bearing, Bearing] (chain)
235
+ ```
236
+
237
+ **Cross-system bearings** — works between any coordinate types:
238
+
239
+ ```ruby
240
+ utm = seattle.to_utm
241
+ mgrs = GCS::MGRS.from_lla(portland)
242
+ utm.bearing_to(mgrs) # => Bearing
243
+ ```
244
+
245
+ ### Bearing Class
246
+
247
+ `Bearing` wraps an azimuth angle (0-360°) with compass and radian conversions.
248
+
249
+ ```ruby
250
+ b = Geodetic::Bearing.new(225)
251
+ b.degrees # => 225.0
252
+ b.to_radians # => 3.926...
253
+ b.reverse # => Bearing (45°)
254
+ b.to_compass(points: 4) # => "W"
255
+ b.to_compass(points: 8) # => "SW"
256
+ b.to_compass(points: 16) # => "SW"
257
+ b.to_s # => "225.0000°"
258
+ b.to_s(1) # => "225.0°"
259
+ b.to_s(0) # => "225°"
260
+
261
+ # Arithmetic
262
+ b + 10 # => Bearing (235°)
263
+ b - 10 # => Bearing (215°)
264
+ Bearing.new(90) - Bearing.new(45) # => 45.0 (Float, angular difference)
265
+ ```
266
+
267
+ ### Distance Class
268
+
269
+ `Distance` tracks values internally in meters with a configurable display unit. All distance methods return `Distance` objects.
270
+
271
+ **Construction:**
272
+
273
+ ```ruby
274
+ d = Geodetic::Distance.new(1000) # 1000 meters
275
+ d = Geodetic::Distance.km(5) # 5 kilometers
276
+ d = Geodetic::Distance.mi(3) # 3 miles
277
+ d = Geodetic::Distance.ft(5280) # 5280 feet
278
+ d = Geodetic::Distance.nmi(1) # 1 nautical mile
279
+ ```
280
+
281
+ **Unit conversions** — return a new `Distance` with the same meters, different display unit:
282
+
283
+ ```ruby
284
+ d = Geodetic::Distance.new(1609.344)
285
+ d.to_km.to_f # => 1.609344
286
+ d.to_mi.to_f # => 1.0
287
+ d.to_ft.to_f # => 5280.0
288
+ d.to_nmi.to_f # => 0.869...
289
+ d.meters # => 1609.344 (always available)
290
+ ```
291
+
292
+ **Display and formatting:**
293
+
294
+ ```ruby
295
+ d = Geodetic::Distance.new(5000).to_km
296
+ d.to_f # => 5.0 (in display unit)
297
+ d.to_i # => 5
298
+ d.to_s # => "5.00 km"
299
+ d.to_s(1) # => "5.0 km"
300
+ d.to_s(0) # => "5 km"
301
+ d.inspect # => "#<Geodetic::Distance 5.00 km (5000.0 m)>"
302
+ ```
303
+
304
+ **Arithmetic** — results always in meters:
305
+
306
+ ```ruby
307
+ d1 = Geodetic::Distance.km(5)
308
+ d2 = Geodetic::Distance.mi(3)
309
+
310
+ (d1 + d2).meters # => 9828.032 (5km + 3mi in meters)
311
+ (d1 - d2).meters # => 171.968
312
+ (d1 * 2).meters # => 10000.0
313
+ (d1 / 2).meters # => 2500.0
314
+ d1 / d2 # => 1.034... (Float ratio)
315
+
316
+ # Numeric constants use the display unit
317
+ d = Geodetic::Distance.new(5000).to_km # 5 km
318
+ (d + 3).meters # => 8000.0 (3 km added)
319
+ ```
320
+
321
+ **Comparison:**
322
+
323
+ ```ruby
324
+ Geodetic::Distance.km(1) == Geodetic::Distance.new(1000) # => true
325
+ Geodetic::Distance.km(5) > Geodetic::Distance.mi(2) # => true
326
+ ```
327
+
328
+ **Supported units:** meters (m), kilometers (km), centimeters (cm), millimeters (mm), miles (mi), yards (yd), feet (ft), inches (in), nautical_miles (nmi)
329
+
330
+ ### Datums
331
+
332
+ ```ruby
333
+ wgs84 = Datum.new(name: "WGS84")
334
+ wgs84.a # => 6378137.0 (semi-major axis)
335
+ wgs84.e2 # => 0.00669437999014132 (eccentricity squared)
336
+
337
+ # Use a different datum for conversions
338
+ nad27 = Datum.new(name: "CLARKE_1866")
339
+ ecef = lla.to_ecef(nad27)
340
+
341
+ # List available datums
342
+ Datum.list
343
+ ```
344
+
345
+ ### Geoid Height
346
+
347
+ ```ruby
348
+ geoid = GeoidHeight.new(geoid_model: "EGM2008")
349
+
350
+ # Get geoid height at a location
351
+ geoid.geoid_height_at(47.6205, -122.3493)
352
+
353
+ # Convert between height datums
354
+ geoid.ellipsoidal_to_orthometric(47.6205, -122.3493, 184.0)
355
+ geoid.orthometric_to_ellipsoidal(47.6205, -122.3493, 150.0)
356
+
357
+ # Convert between vertical datums
358
+ geoid.convert_vertical_datum(47.6205, -122.3493, 184.0, "HAE", "NAVD88")
359
+ ```
360
+
361
+ The `GeoidHeightSupport` module is mixed into LLA for convenience:
362
+
363
+ ```ruby
364
+ lla = Coordinates::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
365
+ lla.geoid_height # => geoid undulation in meters
366
+ lla.orthometric_height # => height above mean sea level
367
+ ```
368
+
369
+ ### Geohash-36 (GH36)
370
+
371
+ A spatial hashing coordinate that encodes lat/lng into a compact, URL-friendly string:
372
+
373
+ ```ruby
374
+ # From a geohash string
375
+ gh36 = Coordinates::GH36.new("bdrdC26BqH")
376
+
377
+ # From any coordinate
378
+ gh36 = Coordinates::GH36.new(lla)
379
+ gh36 = lla.to_gh36(precision: 8)
380
+
381
+ # Decode back to LLA
382
+ lla = gh36.to_lla
383
+
384
+ # URL slug (the hash itself is URL-safe)
385
+ gh36.to_slug # => "bdrdC26BqH"
386
+
387
+ # Neighbor cells
388
+ gh36.neighbors # => { N: GH36, S: GH36, E: GH36, W: GH36, NE: ..., NW: ..., SE: ..., SW: ... }
389
+
390
+ # Bounding rectangle of the geohash cell
391
+ area = gh36.to_area # => Areas::Rectangle
392
+ area.includes?(gh36.to_lla) # => true
393
+
394
+ # Precision info
395
+ gh36.precision # => 10
396
+ gh36.precision_in_meters # => { lat: 0.31, lng: 0.62 }
397
+ ```
398
+
399
+ ### Geographic Areas
400
+
401
+ ```ruby
402
+ # Circle area
403
+ center = Coordinates::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
404
+ circle = Areas::Circle.new(centroid: center, radius: 1000.0) # 1km radius
405
+
406
+ # Polygon area
407
+ points = [
408
+ Coordinates::LLA.new(lat: 47.60, lng: -122.35, alt: 0.0),
409
+ Coordinates::LLA.new(lat: 47.63, lng: -122.35, alt: 0.0),
410
+ Coordinates::LLA.new(lat: 47.63, lng: -122.33, alt: 0.0),
411
+ Coordinates::LLA.new(lat: 47.60, lng: -122.33, alt: 0.0),
412
+ ]
413
+ polygon = Areas::Polygon.new(boundary: points)
414
+ polygon.centroid # => computed centroid as LLA
415
+
416
+ # Rectangle area (accepts any coordinate type)
417
+ nw = Coordinates::LLA.new(lat: 41.0, lng: -75.0)
418
+ se = Coordinates::LLA.new(lat: 40.0, lng: -74.0)
419
+ rect = Areas::Rectangle.new(nw: nw, se: se)
420
+ rect.centroid # => LLA at center
421
+ rect.ne # => computed NE corner
422
+ rect.sw # => computed SW corner
423
+ rect.includes?(point) # => true/false
424
+ ```
425
+
426
+ ### Web Mercator Tile Coordinates
427
+
428
+ ```ruby
429
+ wm = Coordinates::WebMercator.from_lla(lla)
430
+ wm.to_tile_coordinates(15) # => [x_tile, y_tile, zoom]
431
+ wm.to_pixel_coordinates(15) # => [x_pixel, y_pixel, zoom]
432
+
433
+ Coordinates::WebMercator.from_tile_coordinates(5241, 11438, 15)
434
+ ```
435
+
436
+ ## Available Datums
437
+
438
+ Airy 1830, Modified Airy, Australian National, Bessel 1841, Clarke 1866, Clarke 1880, Everest (India 1830), Everest (Brunei & E.Malaysia), Everest (W.Malaysia & Singapore), GRS 1980, Helmert 1906, Hough 1960, International 1924, South American 1969, WGS72, WGS84
439
+
440
+ ## Documentation
441
+
442
+ Full documentation is available at **[madbomber.github.io/geodetic](https://madbomber.github.io/geodetic/)**.
443
+
444
+ ## Examples
445
+
446
+ The [`examples/`](examples/) directory contains runnable demo scripts showing progressive usage:
447
+
448
+ | Script | Description |
449
+ |--------|-------------|
450
+ | [`01_basic_conversions.rb`](examples/01_basic_conversions.rb) | LLA, ECEF, UTM, ENU, NED conversions and roundtrips |
451
+ | [`02_all_coordinate_systems.rb`](examples/02_all_coordinate_systems.rb) | All 12 coordinate systems, cross-system chains, and areas |
452
+ | [`03_distance_calculations.rb`](examples/03_distance_calculations.rb) | Distance class features, unit conversions, and arithmetic |
453
+ | [`04_bearing_calculations.rb`](examples/04_bearing_calculations.rb) | Bearing class, compass directions, elevation angles, and chain bearings |
454
+
455
+ Run any example with:
456
+
457
+ ```bash
458
+ ruby -Ilib examples/01_basic_conversions.rb
459
+ ```
460
+
461
+ ## Development
462
+
463
+ ```bash
464
+ bin/setup # Install dependencies
465
+ rake test # Run tests
466
+ bin/console # Interactive console
467
+ ```
468
+
469
+ ## License
470
+
471
+ Available as open source under the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ task default: :test
@@ -0,0 +1,60 @@
1
+ # Geodetic::Coordinates::BNG
2
+
3
+ ## British National Grid
4
+
5
+ The British National Grid (BNG) is the official coordinate system for Great Britain, based on the **OSGB36 datum** and the **Airy 1830 ellipsoid**. It uses a Transverse Mercator projection centered on 2°W longitude and 49°N latitude.
6
+
7
+ ## Constructors
8
+
9
+ Create a BNG coordinate from numeric easting/northing values:
10
+
11
+ ```ruby
12
+ point = Geodetic::Coordinates::BNG.new(easting: 530000.0, northing: 180000.0)
13
+ ```
14
+
15
+ Alternatively, create from an alphanumeric grid reference string:
16
+
17
+ ```ruby
18
+ point = Geodetic::Coordinates::BNG.new(grid_ref: "TQ 300000 800000")
19
+ ```
20
+
21
+ ## Grid References
22
+
23
+ Convert a BNG coordinate to an alphanumeric grid reference at a specified precision:
24
+
25
+ ```ruby
26
+ grid_ref = point.to_grid_reference(precision)
27
+ ```
28
+
29
+ The `precision` parameter controls the number of digits and therefore the resolution of the resulting grid reference.
30
+
31
+ ## Ellipsoid and Datum
32
+
33
+ BNG uses the **Airy 1830 ellipsoid** internally as part of the OSGB36 datum. When converting between BNG and WGS84-based coordinate systems (such as GPS coordinates), an approximate **OSGB36 to WGS84 transformation** is applied. This transformation introduces small positional offsets (typically a few meters) due to the inherent differences between the two datums.
34
+
35
+ ## Validation
36
+
37
+ The `valid?` method checks that the coordinates fall within the bounds of Great Britain:
38
+
39
+ | Axis | Valid Range |
40
+ |---|---|
41
+ | Easting | 0 to 700,000 meters |
42
+ | Northing | 0 to 1,300,000 meters |
43
+
44
+ ```ruby
45
+ point.valid? # => true if within Great Britain bounds
46
+ ```
47
+
48
+ ## Utility Methods
49
+
50
+ ### Universal Distance and Bearing Methods
51
+
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
+
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)
57
+ bng_a.distance_to(bng_b) # => Distance (great-circle)
58
+ bng_a.straight_line_distance_to(bng_b) # => Distance (Euclidean)
59
+ bng_a.bearing_to(bng_b) # => Bearing (great-circle forward azimuth)
60
+ ```