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
@@ -0,0 +1,182 @@
1
+ # Geoid Height Reference
2
+
3
+ ## Geodetic::GeoidHeight
4
+
5
+ Provides conversion between ellipsoidal heights (height above the reference ellipsoid) and orthometric heights (height above the geoid / mean sea level). Supports multiple geoid models and vertical datums.
6
+
7
+ ### Constructor
8
+
9
+ ```ruby
10
+ geoid = Geodetic::GeoidHeight.new(
11
+ geoid_model: 'EGM2008', # default
12
+ interpolation_method: 'bilinear' # default
13
+ )
14
+ ```
15
+
16
+ Raises a RuntimeError if the geoid model is not recognized.
17
+
18
+ ### Attributes
19
+
20
+ | Attribute | Type | Description |
21
+ |-----------|------|-------------|
22
+ | `geoid_model` | String | The active geoid model name |
23
+ | `interpolation_method` | String | Interpolation method for grid lookups |
24
+
25
+ ---
26
+
27
+ ## Geoid Models
28
+
29
+ | Model | Full Name | Resolution (arc-min) | Accuracy (m) | Epoch | Region |
30
+ |-------|-----------|---------------------|---------------|-------|--------|
31
+ | `EGM96` | Earth Gravitational Model 1996 | 15.0 | 1.0 | 1996 | Global |
32
+ | `EGM2008` | Earth Gravitational Model 2008 | 2.5 | 0.5 | 2008 | Global |
33
+ | `GEOID18` | GEOID18 (CONUS) | 1.0 | 0.1 | 2018 | CONUS |
34
+ | `GEOID12B` | GEOID12B (CONUS) | 1.0 | 0.15 | 2012 | CONUS |
35
+
36
+ Regional models (GEOID18, GEOID12B) fall back to global models for positions outside their coverage area.
37
+
38
+ ---
39
+
40
+ ## Vertical Datums
41
+
42
+ | Datum | Full Name | Region | Type | Reference Geoid |
43
+ |-------|-----------|--------|------|-----------------|
44
+ | `NAVD88` | North American Vertical Datum of 1988 | North America | Orthometric | GEOID18 |
45
+ | `NGVD29` | National Geodetic Vertical Datum of 1929 | United States | Orthometric | GEOID12B |
46
+ | `MSL` | Mean Sea Level | Global | Orthometric | EGM2008 |
47
+ | `HAE` | Height Above Ellipsoid | Global | Ellipsoidal | (none) |
48
+
49
+ ---
50
+
51
+ ## Instance Methods
52
+
53
+ ### `geoid_height_at(lat, lng)`
54
+
55
+ Returns the geoid undulation (separation between ellipsoid and geoid) at the given latitude and longitude, in meters.
56
+
57
+ ```ruby
58
+ geoid = Geodetic::GeoidHeight.new(geoid_model: 'EGM2008')
59
+ height = geoid.geoid_height_at(38.8977, -77.0365)
60
+ ```
61
+
62
+ ### `ellipsoidal_to_orthometric(lat, lng, ellipsoidal_height)`
63
+
64
+ Converts an ellipsoidal height to an orthometric height by subtracting the geoid undulation.
65
+
66
+ ```ruby
67
+ orthometric = geoid.ellipsoidal_to_orthometric(38.8977, -77.0365, 100.0)
68
+ ```
69
+
70
+ ### `orthometric_to_ellipsoidal(lat, lng, orthometric_height)`
71
+
72
+ Converts an orthometric height to an ellipsoidal height by adding the geoid undulation.
73
+
74
+ ```ruby
75
+ ellipsoidal = geoid.orthometric_to_ellipsoidal(38.8977, -77.0365, 65.0)
76
+ ```
77
+
78
+ ### `convert_vertical_datum(lat, lng, height, from_datum, to_datum)`
79
+
80
+ Converts a height value between any two vertical datums. Handles the intermediate conversion through ellipsoidal height when both datums are orthometric.
81
+
82
+ ```ruby
83
+ navd88_height = geoid.convert_vertical_datum(
84
+ 38.8977, -77.0365, 100.0,
85
+ 'HAE', # from Height Above Ellipsoid
86
+ 'NAVD88' # to NAVD88 orthometric
87
+ )
88
+ ```
89
+
90
+ ### `in_coverage?(lat, lng)`
91
+
92
+ Returns `true` if the given position falls within the coverage area of the current geoid model. Global models always return `true`. CONUS models check for approximate continental US bounds.
93
+
94
+ ```ruby
95
+ geoid = Geodetic::GeoidHeight.new(geoid_model: 'GEOID18')
96
+ geoid.in_coverage?(40.0, -100.0) # => true (within CONUS)
97
+ geoid.in_coverage?(51.5, -0.1) # => false (London, outside CONUS)
98
+ ```
99
+
100
+ ### `accuracy_estimate(lat, lng)`
101
+
102
+ Returns the estimated accuracy of the geoid model at the given position, in meters. Regional models return their base accuracy within coverage and 3x the base accuracy outside coverage.
103
+
104
+ ```ruby
105
+ geoid.accuracy_estimate(40.0, -100.0) # => 0.1 (GEOID18 within CONUS)
106
+ geoid.accuracy_estimate(51.5, -0.1) # => 0.3 (GEOID18 outside CONUS)
107
+ ```
108
+
109
+ ### `model_info`
110
+
111
+ Returns the full information hash for the current geoid model.
112
+
113
+ ```ruby
114
+ geoid.model_info
115
+ # => { name: "Earth Gravitational Model 2008", resolution: 2.5, accuracy: 0.5, epoch: 2008 }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Class Methods
121
+
122
+ ### `GeoidHeight.available_models`
123
+
124
+ Returns an array of available geoid model names.
125
+
126
+ ```ruby
127
+ Geodetic::GeoidHeight.available_models
128
+ # => ["EGM96", "EGM2008", "GEOID18", "GEOID12B"]
129
+ ```
130
+
131
+ ### `GeoidHeight.available_vertical_datums`
132
+
133
+ Returns an array of available vertical datum names.
134
+
135
+ ```ruby
136
+ Geodetic::GeoidHeight.available_vertical_datums
137
+ # => ["NAVD88", "NGVD29", "MSL", "HAE"]
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Geodetic::GeoidHeightSupport (Mixin)
143
+
144
+ The `GeoidHeightSupport` module is included in `Geodetic::Coordinates::LLA`, adding geoid-related methods directly to LLA instances.
145
+
146
+ ### Mixin Methods
147
+
148
+ #### `geoid_height(geoid_model = 'EGM2008')`
149
+
150
+ Returns the geoid undulation at the LLA position.
151
+
152
+ ```ruby
153
+ point = Geodetic::Coordinates::LLA.new(lat: 38.8977, lng: -77.0365, alt: 100.0)
154
+ point.geoid_height # Uses EGM2008
155
+ point.geoid_height('GEOID18') # Uses GEOID18
156
+ ```
157
+
158
+ #### `orthometric_height(geoid_model = 'EGM2008')`
159
+
160
+ Returns the orthometric height (height above geoid) by subtracting the geoid undulation from the LLA altitude.
161
+
162
+ ```ruby
163
+ point.orthometric_height # alt minus geoid undulation
164
+ ```
165
+
166
+ #### `convert_height_datum(from_datum, to_datum, geoid_model = 'EGM2008')`
167
+
168
+ Returns a new LLA with the altitude converted between vertical datums. The original object is not modified.
169
+
170
+ ```ruby
171
+ wgs84_point = Geodetic::Coordinates::LLA.new(lat: 40.0, lng: -100.0, alt: 300.0)
172
+ navd88_point = wgs84_point.convert_height_datum('HAE', 'NAVD88')
173
+ ```
174
+
175
+ ### Class Extension
176
+
177
+ The mixin also extends the including class with a `with_geoid_height` class method and a `geoid_model` reader, though these are primarily for internal use:
178
+
179
+ ```ruby
180
+ Geodetic::Coordinates::LLA.with_geoid_height('GEOID18')
181
+ Geodetic::Coordinates::LLA.geoid_model # => 'GEOID18'
182
+ ```
@@ -0,0 +1,252 @@
1
+ # Serialization Reference
2
+
3
+ All coordinate classes provide methods for converting to and from string and array representations. This enables storage, transmission, and reconstruction of coordinate objects.
4
+
5
+ ---
6
+
7
+ ## Common Serialization Methods
8
+
9
+ Every coordinate class implements these four methods:
10
+
11
+ | Method | Direction | Description |
12
+ |--------|-----------|-------------|
13
+ | `to_s(precision)` | Export | Returns a comma-separated string with controlled decimal places |
14
+ | `self.from_string(string)` | Import | Parses a comma-separated string into a new instance |
15
+ | `to_a` | Export | Returns an array of component values (full precision) |
16
+ | `self.from_array(array)` | Import | Constructs a new instance from an array |
17
+
18
+ ### Precision Parameter
19
+
20
+ All `to_s` methods accept an optional `precision` parameter controlling the number of decimal places. Each class has a sensible default:
21
+
22
+ | Class | Default Precision | Notes |
23
+ |-------|------------------|-------|
24
+ | LLA | 6 | Altitude capped at min(precision, 2) |
25
+ | Bearing | 4 | |
26
+ | All others | 2 | Meters-based coordinates |
27
+
28
+ Passing `0` returns integer values (no decimal point). MGRS and USNG are string-based and do not accept a precision parameter.
29
+
30
+ ```ruby
31
+ lla = Geodetic::Coordinates::LLA.new(lat: 47.6205, lng: -122.3493, alt: 184.0)
32
+ lla.to_s # => "47.620500, -122.349300, 184.00"
33
+ lla.to_s(3) # => "47.620, -122.349, 184.00"
34
+ lla.to_s(0) # => "48, -122, 184"
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Format Reference by Class
40
+
41
+ ### LLA
42
+
43
+ ```ruby
44
+ point = Geodetic::Coordinates::LLA.new(lat: 38.8977, lng: -77.0365, alt: 100.0)
45
+
46
+ point.to_s # => "38.897700, -77.036500, 100.00"
47
+ point.to_s(2) # => "38.90, -77.04, 100.00"
48
+ point.to_a # => [38.8977, -77.0365, 100.0]
49
+
50
+ Geodetic::Coordinates::LLA.from_string("38.8977, -77.0365, 100.0")
51
+ Geodetic::Coordinates::LLA.from_array([38.8977, -77.0365, 100.0])
52
+ ```
53
+
54
+ **LLA-specific: DMS format**
55
+
56
+ LLA also supports degrees-minutes-seconds notation:
57
+
58
+ ```ruby
59
+ point.to_dms
60
+ # => "38 53' 51.72\" N, 77 2' 11.40\" W, 100.00 m"
61
+
62
+ Geodetic::Coordinates::LLA.from_dms("38 53' 51.72\" N, 77 2' 11.40\" W, 100.0 m")
63
+ ```
64
+
65
+ The DMS string format is: `DD MM' SS.ss" H, DDD MM' SS.ss" H, ALT m` where H is N/S for latitude and E/W for longitude. The altitude portion is optional in `from_dms` (defaults to 0.0).
66
+
67
+ ### ECEF
68
+
69
+ ```ruby
70
+ point = Geodetic::Coordinates::ECEF.new(x: 1130730.0, y: -4828583.0, z: 3991570.0)
71
+
72
+ point.to_s # => "1130730.00, -4828583.00, 3991570.00"
73
+ point.to_s(0) # => "1130730, -4828583, 3991570"
74
+ point.to_a # => [1130730.0, -4828583.0, 3991570.0]
75
+
76
+ Geodetic::Coordinates::ECEF.from_string("1130730.0, -4828583.0, 3991570.0")
77
+ Geodetic::Coordinates::ECEF.from_array([1130730.0, -4828583.0, 3991570.0])
78
+ ```
79
+
80
+ ### UTM
81
+
82
+ ```ruby
83
+ point = Geodetic::Coordinates::UTM.new(easting: 323394.0, northing: 4307396.0, altitude: 100.0, zone: 18, hemisphere: 'N')
84
+
85
+ point.to_s # => "323394.00, 4307396.00, 100.00, 18, N"
86
+ point.to_a # => [323394.0, 4307396.0, 100.0, 18, "N"]
87
+
88
+ Geodetic::Coordinates::UTM.from_string("323394.0, 4307396.0, 100.0, 18, N")
89
+ Geodetic::Coordinates::UTM.from_array([323394.0, 4307396.0, 100.0, 18, "N"])
90
+ ```
91
+
92
+ Note: The array and string formats include all five components: easting, northing, altitude, zone number, and hemisphere.
93
+
94
+ ### ENU
95
+
96
+ ```ruby
97
+ point = Geodetic::Coordinates::ENU.new(e: 100.0, n: 200.0, u: 50.0)
98
+
99
+ point.to_s # => "100.00, 200.00, 50.00"
100
+ point.to_a # => [100.0, 200.0, 50.0]
101
+
102
+ Geodetic::Coordinates::ENU.from_string("100.0, 200.0, 50.0")
103
+ Geodetic::Coordinates::ENU.from_array([100.0, 200.0, 50.0])
104
+ ```
105
+
106
+ ### NED
107
+
108
+ ```ruby
109
+ point = Geodetic::Coordinates::NED.new(n: 200.0, e: 100.0, d: -50.0)
110
+
111
+ point.to_s # => "200.00, 100.00, -50.00"
112
+ point.to_a # => [200.0, 100.0, -50.0]
113
+
114
+ Geodetic::Coordinates::NED.from_string("200.0, 100.0, -50.0")
115
+ Geodetic::Coordinates::NED.from_array([200.0, 100.0, -50.0])
116
+ ```
117
+
118
+ ### WebMercator
119
+
120
+ ```ruby
121
+ point = Geodetic::Coordinates::WebMercator.new(x: -8575605.0, y: 4707175.0)
122
+
123
+ point.to_s # => "-8575605.00, 4707175.00"
124
+ point.to_a # => [-8575605.0, 4707175.0]
125
+
126
+ Geodetic::Coordinates::WebMercator.from_string("-8575605.0, 4707175.0")
127
+ Geodetic::Coordinates::WebMercator.from_array([-8575605.0, 4707175.0])
128
+ ```
129
+
130
+ ### UPS
131
+
132
+ ```ruby
133
+ point = Geodetic::Coordinates::UPS.new(easting: 2000000.0, northing: 2000000.0, hemisphere: 'N', zone: 'Y')
134
+
135
+ point.to_s # => "2000000.00, 2000000.00, N, Y"
136
+ point.to_a # => [2000000.0, 2000000.0, "N", "Y"]
137
+
138
+ Geodetic::Coordinates::UPS.from_string("2000000.0, 2000000.0, N, Y")
139
+ Geodetic::Coordinates::UPS.from_array([2000000.0, 2000000.0, "N", "Y"])
140
+ ```
141
+
142
+ ### BNG
143
+
144
+ ```ruby
145
+ point = Geodetic::Coordinates::BNG.new(easting: 530000.0, northing: 180000.0)
146
+
147
+ point.to_s # => "530000.00, 180000.00"
148
+ point.to_a # => [530000.0, 180000.0]
149
+
150
+ Geodetic::Coordinates::BNG.from_string("530000.0, 180000.0")
151
+ Geodetic::Coordinates::BNG.from_array([530000.0, 180000.0])
152
+ ```
153
+
154
+ BNG also supports grid reference notation via the constructor:
155
+
156
+ ```ruby
157
+ point = Geodetic::Coordinates::BNG.new(grid_ref: "TQ 30 80")
158
+ point.to_grid_reference(6) # => "TQ 300000 800000"
159
+ point.to_grid_reference(0) # => "TQ" (grid square only)
160
+ ```
161
+
162
+ ### StatePlane
163
+
164
+ ```ruby
165
+ point = Geodetic::Coordinates::StatePlane.new(easting: 2000000.0, northing: 500000.0, zone_code: 'CA_I')
166
+
167
+ point.to_s # => "2000000.00, 500000.00, CA_I"
168
+ point.to_a # => [2000000.0, 500000.0, "CA_I"]
169
+
170
+ Geodetic::Coordinates::StatePlane.from_string("2000000.0, 500000.0, CA_I")
171
+ Geodetic::Coordinates::StatePlane.from_array([2000000.0, 500000.0, "CA_I"])
172
+ ```
173
+
174
+ ---
175
+
176
+ ## MGRS and USNG (String-Based Formats)
177
+
178
+ MGRS and USNG use alphanumeric grid references rather than numeric arrays. They support `to_s` and `from_string` but do not provide `to_a` / `from_array`.
179
+
180
+ ### MGRS
181
+
182
+ ```ruby
183
+ mgrs = Geodetic::Coordinates::MGRS.new(mgrs_string: "18SUJ2034706880")
184
+ mgrs.to_s # => "18SUJ2034706880"
185
+
186
+ Geodetic::Coordinates::MGRS.from_string("18SUJ2034706880")
187
+ ```
188
+
189
+ The MGRS string format is: `{zone_number}{zone_letter}{square_id}{easting}{northing}` with no spaces. Precision varies based on the number of coordinate digits (0 to 5 pairs).
190
+
191
+ ### USNG
192
+
193
+ ```ruby
194
+ usng = Geodetic::Coordinates::USNG.new(usng_string: "18S UJ 20347 06880")
195
+ usng.to_s # => "18S UJ 20347 06880"
196
+
197
+ Geodetic::Coordinates::USNG.from_string("18S UJ 20347 06880")
198
+ ```
199
+
200
+ USNG uses the same underlying format as MGRS but separates components with spaces for readability. Both spaced and non-spaced formats are accepted by `from_string`. USNG also provides:
201
+
202
+ ```ruby
203
+ usng.to_full_format # => "18S UJ 20347 06880"
204
+ usng.to_abbreviated_format # => "18S UJ 20347 6880"
205
+ ```
206
+
207
+ ---
208
+
209
+ ## Roundtrip Examples
210
+
211
+ String and array serialization support full roundtrip fidelity:
212
+
213
+ ```ruby
214
+ # LLA roundtrip via string
215
+ original = Geodetic::Coordinates::LLA.new(lat: 40.7128, lng: -74.0060, alt: 10.0)
216
+ restored = Geodetic::Coordinates::LLA.from_string(original.to_s)
217
+ original == restored # => true
218
+
219
+ # ECEF roundtrip via array
220
+ original = Geodetic::Coordinates::ECEF.new(x: 1334000.0, y: -4654000.0, z: 4138000.0)
221
+ restored = Geodetic::Coordinates::ECEF.from_array(original.to_a)
222
+ original == restored # => true
223
+
224
+ # UTM roundtrip via string
225
+ original = Geodetic::Coordinates::UTM.new(easting: 583960.0, northing: 4507523.0, altitude: 10.0, zone: 18, hemisphere: 'N')
226
+ restored = Geodetic::Coordinates::UTM.from_string(original.to_s)
227
+ original == restored # => true
228
+
229
+ # LLA roundtrip via DMS
230
+ original = Geodetic::Coordinates::LLA.new(lat: 38.8977, lng: -77.0365, alt: 100.0)
231
+ dms_string = original.to_dms
232
+ restored = Geodetic::Coordinates::LLA.from_dms(dms_string)
233
+ # Note: DMS roundtrip has minor floating-point precision differences
234
+ ```
235
+
236
+ ---
237
+
238
+ ## Summary Table
239
+
240
+ | Class | `to_s` Format | Default Precision | `to_a` Elements | Extra Formats |
241
+ |-------|--------------|-------------------|-----------------|---------------|
242
+ | LLA | `lat, lng, alt` | 6 (alt: 2) | `[lat, lng, alt]` | `to_dms` / `from_dms` |
243
+ | ECEF | `x, y, z` | 2 | `[x, y, z]` | -- |
244
+ | UTM | `easting, northing, alt, zone, hemisphere` | 2 | `[easting, northing, alt, zone, hemisphere]` | -- |
245
+ | ENU | `e, n, u` | 2 | `[e, n, u]` | -- |
246
+ | NED | `n, e, d` | 2 | `[n, e, d]` | -- |
247
+ | WebMercator | `x, y` | 2 | `[x, y]` | -- |
248
+ | UPS | `easting, northing, hemisphere, zone` | 2 | `[easting, northing, hemisphere, zone]` | -- |
249
+ | BNG | `easting, northing` | 2 | `[easting, northing]` | `to_grid_reference` / `grid_ref:` constructor |
250
+ | StatePlane | `easting, northing, zone_code` | 2 | `[easting, northing, zone_code]` | -- |
251
+ | MGRS | `grid_zone+square+coords` | n/a | (not available) | String-based only |
252
+ | USNG | `grid_zone square coords` | n/a | (not available) | `to_full_format`, `to_abbreviated_format` |
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Demonstration of orthogonal coordinate system conversions
4
+ # Shows all coordinate systems converting to/from each other
5
+
6
+ require_relative '../lib/geodetic'
7
+ require_relative '../lib/geodetic/coordinates/lla'
8
+ require_relative '../lib/geodetic/coordinates/ecef'
9
+ require_relative '../lib/geodetic/coordinates/enu'
10
+ require_relative '../lib/geodetic/coordinates/ned'
11
+ require_relative '../lib/geodetic/coordinates/utm'
12
+
13
+ include Geodetic
14
+ LLA = Coordinates::LLA
15
+ ECEF = Coordinates::ECEF
16
+ ENU = Coordinates::ENU
17
+ NED = Coordinates::NED
18
+ UTM = Coordinates::UTM
19
+
20
+ puts "=== Orthogonal Coordinate System Conversions Demo ==="
21
+ puts
22
+
23
+ # Test location: Seattle Space Needle
24
+ seattle_lat = 47.6205
25
+ seattle_lng = -122.3493
26
+ seattle_alt = 184.0
27
+
28
+ puts "Original Test Point (Seattle Space Needle):"
29
+ puts "Latitude: #{seattle_lat}"
30
+ puts "Longitude: #{seattle_lng}"
31
+ puts "Altitude: #{seattle_alt}m"
32
+ puts
33
+
34
+ # Create initial LLA coordinate
35
+ lla = LLA.new(lat: seattle_lat, lng: seattle_lng, alt: seattle_alt)
36
+ puts "1. LLA Coordinate: #{lla}"
37
+
38
+ # LLA -> ECEF
39
+ ecef = lla.to_ecef
40
+ puts "2. LLA -> ECEF: #{ecef}"
41
+
42
+ # ECEF -> LLA (roundtrip test)
43
+ lla_back = ecef.to_lla
44
+ puts "3. ECEF -> LLA: #{lla_back}"
45
+ lat_error = (seattle_lat - lla_back.lat).abs
46
+ lng_error = (seattle_lng - lla_back.lng).abs
47
+ alt_error = (seattle_alt - lla_back.alt).abs
48
+ puts " Roundtrip errors: lat=#{lat_error}, lng=#{lng_error}, alt=#{alt_error}"
49
+ puts
50
+
51
+ # LLA -> UTM
52
+ utm = lla.to_utm
53
+ puts "4. LLA -> UTM: #{utm}"
54
+
55
+ # UTM -> LLA (roundtrip test)
56
+ lla_from_utm = utm.to_lla
57
+ puts "5. UTM -> LLA: #{lla_from_utm}"
58
+ puts
59
+
60
+ # Reference point for local coordinates (slightly offset)
61
+ ref_lla = LLA.new(lat: seattle_lat - 0.01, lng: seattle_lng + 0.01, alt: seattle_alt - 100)
62
+ puts "Reference point for local coordinates:"
63
+ puts "6. Reference LLA: #{ref_lla}"
64
+
65
+ # LLA -> ENU
66
+ enu = lla.to_enu(ref_lla)
67
+ puts "7. LLA -> ENU: #{enu}"
68
+ puts " (East: #{enu.e.round(2)}m, North: #{enu.n.round(2)}m, Up: #{enu.u.round(2)}m)"
69
+
70
+ # ENU -> LLA (roundtrip test)
71
+ lla_from_enu = enu.to_lla(ref_lla)
72
+ puts "8. ENU -> LLA: #{lla_from_enu}"
73
+
74
+ # LLA -> NED
75
+ ned = lla.to_ned(ref_lla)
76
+ puts "9. LLA -> NED: #{ned}"
77
+ puts " (North: #{ned.n.round(2)}m, East: #{ned.e.round(2)}m, Down: #{ned.d.round(2)}m)"
78
+
79
+ # NED -> LLA (roundtrip test)
80
+ lla_from_ned = ned.to_lla(ref_lla)
81
+ puts "10. NED -> LLA: #{lla_from_ned}"
82
+ puts
83
+
84
+ # Cross-conversions between local coordinate systems
85
+ puts "=== Local Coordinate Cross-Conversions ==="
86
+
87
+ # ENU <-> NED
88
+ ned_from_enu = enu.to_ned
89
+ puts "11. ENU -> NED: #{ned_from_enu}"
90
+ enu_from_ned = ned.to_enu
91
+ puts "12. NED -> ENU: #{enu_from_ned}"
92
+
93
+ # Verify ENU <-> NED consistency
94
+ enu_error_e = (enu.e - enu_from_ned.e).abs
95
+ enu_error_n = (enu.n - enu_from_ned.n).abs
96
+ enu_error_u = (enu.u - enu_from_ned.u).abs
97
+ puts " ENU roundtrip errors: e=#{enu_error_e}, n=#{enu_error_n}, u=#{enu_error_u}"
98
+ puts
99
+
100
+ # Chain conversions to test system consistency
101
+ puts "=== Chain Conversion Test (LLA -> ECEF -> ENU -> NED -> ECEF -> LLA) ==="
102
+
103
+ ref_ecef = ref_lla.to_ecef
104
+
105
+ step1_ecef = lla.to_ecef
106
+ puts "13. LLA -> ECEF: #{step1_ecef}"
107
+
108
+ step2_enu = step1_ecef.to_enu(ref_ecef, ref_lla)
109
+ puts "14. ECEF -> ENU: #{step2_enu}"
110
+
111
+ step3_ned = step2_enu.to_ned
112
+ puts "15. ENU -> NED: #{step3_ned}"
113
+
114
+ step4_ecef = step3_ned.to_ecef(ref_ecef, ref_lla)
115
+ puts "16. NED -> ECEF: #{step4_ecef}"
116
+
117
+ step5_lla = step4_ecef.to_lla
118
+ puts "17. ECEF -> LLA: #{step5_lla}"
119
+
120
+ # Check final accuracy
121
+ final_lat_error = (seattle_lat - step5_lla.lat).abs
122
+ final_lng_error = (seattle_lng - step5_lla.lng).abs
123
+ final_alt_error = (seattle_alt - step5_lla.alt).abs
124
+ puts " Chain conversion errors: lat=#{final_lat_error}, lng=#{final_lng_error}, alt=#{final_alt_error}"
125
+ puts
126
+
127
+ # Distance calculations
128
+ puts "=== Distance and Bearing Calculations ==="
129
+
130
+ # Create another test point (Pioneer Square, Seattle)
131
+ pioneer_lla = LLA.new(lat: 47.6097, lng: -122.3331, alt: 56.0)
132
+ puts "18. Second point (Pioneer Square): #{pioneer_lla}"
133
+
134
+ # ECEF distance
135
+ pioneer_ecef = pioneer_lla.to_ecef
136
+ ecef_distance = ecef.distance_to(pioneer_ecef)
137
+ puts "19. ECEF distance: #{ecef_distance.to_s}"
138
+
139
+ # ENU distance and bearing
140
+ pioneer_enu = pioneer_lla.to_enu(lla) # Using first point as reference
141
+ enu_origin = ENU.new(e: 0, n: 0, u: 0)
142
+ enu_distance = enu_origin.distance_to_origin
143
+ enu_bearing = enu_origin.local_bearing_to(pioneer_enu)
144
+ horizontal_distance = enu_origin.horizontal_distance_to(pioneer_enu)
145
+
146
+ puts "20. ENU distance to origin: #{enu_distance.round(2)}m"
147
+ puts "21. ENU bearing: #{enu_bearing.round(1)} degrees"
148
+ puts "22. Horizontal distance: #{horizontal_distance.round(2)}m"
149
+
150
+ # UTM distance (same zone)
151
+ pioneer_utm = pioneer_lla.to_utm
152
+ utm_distance = utm.distance_to(pioneer_utm)
153
+ puts "23. UTM distance: #{utm_distance.to_s}"
154
+
155
+ puts
156
+ puts "=== All Coordinate Systems Summary ==="
157
+ puts "LLA: #{lla}"
158
+ puts "ECEF: #{ecef}"
159
+ puts "UTM: #{utm}"
160
+ puts "ENU: #{enu} (relative to reference)"
161
+ puts "NED: #{ned} (relative to reference)"
162
+
163
+ puts
164
+ puts "=== to_s Precision Control ==="
165
+ puts
166
+ puts "All to_s methods accept an optional precision parameter:"
167
+ puts
168
+ puts "LLA (default=6):"
169
+ puts " to_s => #{lla.to_s}"
170
+ puts " to_s(3) => #{lla.to_s(3)}"
171
+ puts " to_s(0) => #{lla.to_s(0)}"
172
+ puts
173
+ puts "ECEF (default=2):"
174
+ puts " to_s => #{ecef.to_s}"
175
+ puts " to_s(0) => #{ecef.to_s(0)}"
176
+ puts
177
+ puts "UTM (default=2):"
178
+ puts " to_s => #{utm.to_s}"
179
+ puts " to_s(0) => #{utm.to_s(0)}"
180
+ puts
181
+ puts "ENU (default=2):"
182
+ puts " to_s => #{enu.to_s}"
183
+ puts " to_s(4) => #{enu.to_s(4)}"
184
+ puts " to_s(0) => #{enu.to_s(0)}"
185
+ puts
186
+ puts "All coordinate systems successfully demonstrate complete orthogonal conversions!"
187
+ puts "Each system can convert to and from every other system with high precision."