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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +15 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +471 -0
- data/Rakefile +8 -0
- data/docs/coordinate-systems/bng.md +60 -0
- data/docs/coordinate-systems/ecef.md +215 -0
- data/docs/coordinate-systems/enu.md +77 -0
- data/docs/coordinate-systems/gh36.md +192 -0
- data/docs/coordinate-systems/index.md +93 -0
- data/docs/coordinate-systems/lla.md +304 -0
- data/docs/coordinate-systems/mgrs.md +81 -0
- data/docs/coordinate-systems/ned.md +83 -0
- data/docs/coordinate-systems/state-plane.md +60 -0
- data/docs/coordinate-systems/ups.md +53 -0
- data/docs/coordinate-systems/usng.md +74 -0
- data/docs/coordinate-systems/utm.md +257 -0
- data/docs/coordinate-systems/web-mercator.md +67 -0
- data/docs/getting-started/installation.md +65 -0
- data/docs/getting-started/quick-start.md +175 -0
- data/docs/index.md +58 -0
- data/docs/reference/areas.md +195 -0
- data/docs/reference/conversions.md +351 -0
- data/docs/reference/datums.md +134 -0
- data/docs/reference/geoid-height.md +182 -0
- data/docs/reference/serialization.md +252 -0
- data/examples/01_basic_conversions.rb +187 -0
- data/examples/02_all_coordinate_systems.rb +310 -0
- data/examples/03_distance_calculations.rb +224 -0
- data/examples/04_bearing_calculations.rb +236 -0
- data/lib/geodetic/areas/circle.rb +29 -0
- data/lib/geodetic/areas/polygon.rb +57 -0
- data/lib/geodetic/areas/rectangle.rb +55 -0
- data/lib/geodetic/areas.rb +5 -0
- data/lib/geodetic/bearing.rb +94 -0
- data/lib/geodetic/coordinates/bng.rb +366 -0
- data/lib/geodetic/coordinates/ecef.rb +229 -0
- data/lib/geodetic/coordinates/enu.rb +244 -0
- data/lib/geodetic/coordinates/gh36.rb +384 -0
- data/lib/geodetic/coordinates/lla.rb +268 -0
- data/lib/geodetic/coordinates/mgrs.rb +317 -0
- data/lib/geodetic/coordinates/ned.rb +246 -0
- data/lib/geodetic/coordinates/state_plane.rb +451 -0
- data/lib/geodetic/coordinates/ups.rb +325 -0
- data/lib/geodetic/coordinates/usng.rb +274 -0
- data/lib/geodetic/coordinates/utm.rb +261 -0
- data/lib/geodetic/coordinates/web_mercator.rb +242 -0
- data/lib/geodetic/coordinates.rb +260 -0
- data/lib/geodetic/datum.rb +62 -0
- data/lib/geodetic/distance.rb +146 -0
- data/lib/geodetic/geoid_height.rb +299 -0
- data/lib/geodetic/version.rb +5 -0
- data/lib/geodetic.rb +13 -0
- data/mkdocs.yml +140 -0
- data/sig/geodetic.rbs +4 -0
- 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."
|