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,310 @@
1
+ #!/usr/bin/env ruby
2
+ # Comprehensive demonstration of all coordinate systems
3
+ # Shows complete orthogonal conversions between all implemented coordinate systems
4
+
5
+ require_relative '../lib/geodetic'
6
+ require_relative '../lib/geodetic/coordinates/lla'
7
+ require_relative '../lib/geodetic/coordinates/ecef'
8
+ require_relative '../lib/geodetic/coordinates/utm'
9
+ require_relative '../lib/geodetic/coordinates/enu'
10
+ require_relative '../lib/geodetic/coordinates/ned'
11
+ require_relative '../lib/geodetic/coordinates/mgrs'
12
+ require_relative '../lib/geodetic/coordinates/web_mercator'
13
+ require_relative '../lib/geodetic/coordinates/ups'
14
+ require_relative '../lib/geodetic/coordinates/usng'
15
+ require_relative '../lib/geodetic/coordinates/state_plane'
16
+ require_relative '../lib/geodetic/coordinates/bng'
17
+ require_relative '../lib/geodetic/coordinates/gh36'
18
+ require_relative '../lib/geodetic/geoid_height'
19
+
20
+ include Geodetic
21
+ LLA = Coordinates::LLA
22
+ ECEF = Coordinates::ECEF
23
+ UTM_Coord = Coordinates::UTM
24
+ ENU = Coordinates::ENU
25
+ NED = Coordinates::NED
26
+ MGRS = Coordinates::MGRS
27
+ USNG = Coordinates::USNG
28
+ WebMerc = Coordinates::WebMercator
29
+ UPS = Coordinates::UPS
30
+ StatePlane = Coordinates::StatePlane
31
+ BNG = Coordinates::BNG
32
+ GH36 = Coordinates::GH36
33
+
34
+ def demo_coordinate_systems
35
+ puts "=" * 80
36
+ puts "COMPLETE COORDINATE SYSTEM CONVERSION DEMONSTRATION"
37
+ puts "=" * 80
38
+ puts
39
+
40
+ # Test location: Seattle Space Needle
41
+ seattle_lat = 47.6205
42
+ seattle_lng = -122.3493
43
+ seattle_alt = 184.0
44
+
45
+ puts "Reference Location: Seattle Space Needle"
46
+ puts " Latitude: #{seattle_lat} degrees"
47
+ puts " Longitude: #{seattle_lng} degrees"
48
+ puts " Altitude: #{seattle_alt} meters"
49
+ puts
50
+
51
+ # Create reference LLA coordinate
52
+ lla_coord = LLA.new(lat: seattle_lat, lng: seattle_lng, alt: seattle_alt)
53
+ reference_lla = LLA.new(lat: seattle_lat, lng: seattle_lng, alt: 0.0) # For local coordinates
54
+
55
+ puts "ALL COORDINATE SYSTEM CONVERSIONS"
56
+ puts "-" * 50
57
+
58
+ # ========== ECEF Conversion ==========
59
+ puts "Earth-Centered, Earth-Fixed (ECEF)"
60
+ ecef_coord = lla_coord.to_ecef
61
+ puts " ECEF: X=#{ecef_coord.x.round(3)}m, Y=#{ecef_coord.y.round(3)}m, Z=#{ecef_coord.z.round(3)}m"
62
+
63
+ # Round-trip test
64
+ lla_from_ecef = ecef_coord.to_lla
65
+ lat_error = (lla_from_ecef.lat - seattle_lat).abs
66
+ lng_error = (lla_from_ecef.lng - seattle_lng).abs
67
+ puts " Round-trip error: Lat=#{lat_error.round(15)}, Lng=#{lng_error.round(15)}"
68
+ puts
69
+
70
+ # ========== UTM Conversion ==========
71
+ puts "Universal Transverse Mercator (UTM)"
72
+ utm_coord = lla_coord.to_utm
73
+ puts " UTM: #{utm_coord.easting.round(3)}E #{utm_coord.northing.round(3)}N Zone #{utm_coord.zone}#{utm_coord.hemisphere}"
74
+
75
+ # Round-trip test
76
+ lla_from_utm = utm_coord.to_lla
77
+ utm_lat_error = (lla_from_utm.lat - seattle_lat).abs
78
+ utm_lng_error = (lla_from_utm.lng - seattle_lng).abs
79
+ puts " Round-trip error: Lat=#{utm_lat_error.round(8)}, Lng=#{utm_lng_error.round(8)}"
80
+ puts
81
+
82
+ # ========== ENU Conversion ==========
83
+ puts "East, North, Up (ENU) Local Coordinates"
84
+ enu_coord = lla_coord.to_enu(reference_lla)
85
+ puts " ENU: E=#{enu_coord.east.round(3)}m, N=#{enu_coord.north.round(3)}m, U=#{enu_coord.up.round(3)}m"
86
+ puts " Distance from reference: #{enu_coord.distance_to_origin.round(3)}m"
87
+ puts " Bearing from reference: #{enu_coord.bearing_from_origin.round(3)} degrees"
88
+ puts
89
+
90
+ # ========== NED Conversion ==========
91
+ puts "North, East, Down (NED) Local Coordinates"
92
+ ned_coord = lla_coord.to_ned(reference_lla)
93
+ puts " NED: N=#{ned_coord.north.round(3)}m, E=#{ned_coord.east.round(3)}m, D=#{ned_coord.down.round(3)}m"
94
+ puts " Distance from reference: #{ned_coord.distance_to_origin.round(3)}m"
95
+ puts " Elevation angle: #{ned_coord.elevation_angle.round(3)} degrees"
96
+ puts
97
+
98
+ # ========== MGRS Conversion ==========
99
+ puts "Military Grid Reference System (MGRS)"
100
+ mgrs_coord = MGRS.from_lla(lla_coord)
101
+ puts " MGRS: #{mgrs_coord}"
102
+ puts " Zone: #{mgrs_coord.grid_zone_designator}, Square: #{mgrs_coord.square_identifier}"
103
+ puts " Within square: #{mgrs_coord.easting.round(3)}E, #{mgrs_coord.northing.round(3)}N"
104
+
105
+ # Test different precisions
106
+ mgrs_10m = MGRS.from_lla(lla_coord, WGS84, 4)
107
+ mgrs_1m = MGRS.from_lla(lla_coord, WGS84, 5)
108
+ puts " 10m precision: #{mgrs_10m}"
109
+ puts " 1m precision: #{mgrs_1m}"
110
+ puts
111
+
112
+ # ========== USNG Conversion ==========
113
+ puts "US National Grid (USNG)"
114
+ usng_coord = USNG.from_lla(lla_coord)
115
+ puts " USNG: #{usng_coord}"
116
+ puts " Full format: #{usng_coord.to_full_format}"
117
+ puts " Abbreviated: #{usng_coord.to_abbreviated_format}"
118
+ puts
119
+
120
+ # ========== Web Mercator Conversion ==========
121
+ puts "Web Mercator (EPSG:3857) - Used by Google Maps, OSM"
122
+ web_merc_coord = WebMerc.from_lla(lla_coord)
123
+ puts " Web Mercator: X=#{web_merc_coord.x.round(3)}m, Y=#{web_merc_coord.y.round(3)}m"
124
+
125
+ # Test tile coordinates at different zoom levels
126
+ tile_10 = web_merc_coord.to_tile_coordinates(10)
127
+ tile_15 = web_merc_coord.to_tile_coordinates(15)
128
+ puts " Tile (zoom 10): X=#{tile_10[0]}, Y=#{tile_10[1]}"
129
+ puts " Tile (zoom 15): X=#{tile_15[0]}, Y=#{tile_15[1]}"
130
+ puts
131
+
132
+ # ========== UPS Conversion (using North Pole example) ==========
133
+ puts "Universal Polar Stereographic (UPS)"
134
+ north_pole_lla = LLA.new(lat: 89.0, lng: 0.0, alt: 0.0)
135
+ ups_coord = UPS.from_lla(north_pole_lla)
136
+ puts " UPS (North Pole): #{ups_coord.easting.round(3)}E #{ups_coord.northing.round(3)}N Zone #{ups_coord.zone}#{ups_coord.hemisphere}"
137
+ puts " Grid convergence: #{ups_coord.grid_convergence.round(6)} degrees"
138
+ puts " Scale factor: #{ups_coord.point_scale_factor.round(8)}"
139
+ puts
140
+
141
+ # ========== State Plane Conversion ==========
142
+ puts "State Plane Coordinate System (SPC)"
143
+ begin
144
+ ca_spc = StatePlane.from_lla(lla_coord, 'CA_I')
145
+ puts " California Zone I: #{ca_spc.easting.round(3)}ft, #{ca_spc.northing.round(3)}ft"
146
+ puts " State: #{ca_spc.zone_info[:state]}, Projection: #{ca_spc.zone_info[:projection]}"
147
+ puts " Units: #{ca_spc.zone_info[:units]}"
148
+
149
+ # Convert to meters
150
+ ca_spc_meters = ca_spc.to_meters
151
+ puts " In meters: #{ca_spc_meters.easting.round(3)}m, #{ca_spc_meters.northing.round(3)}m"
152
+ rescue => e
153
+ puts " State Plane conversion: #{e.message}"
154
+ end
155
+ puts
156
+
157
+ # ========== Geohash-36 Conversion ==========
158
+ puts "Geohash-36 (GH36)"
159
+ gh36_coord = GH36.new(lla_coord)
160
+ puts " GH36: #{gh36_coord.to_s}"
161
+ puts " Precision: #{gh36_coord.precision} chars"
162
+ puts " Precision in meters: lat=#{gh36_coord.precision_in_meters[:lat].round(3)}m, lng=#{gh36_coord.precision_in_meters[:lng].round(3)}m"
163
+
164
+ # URL slug
165
+ puts " URL slug: #{gh36_coord.to_slug}"
166
+
167
+ # Reduced precision
168
+ gh36_short = GH36.new(lla_coord, precision: 5)
169
+ puts " 5-char precision: #{gh36_short.to_s}"
170
+
171
+ # Round-trip test
172
+ lla_from_gh36 = gh36_coord.to_lla
173
+ gh36_lat_error = (lla_from_gh36.lat - seattle_lat).abs
174
+ gh36_lng_error = (lla_from_gh36.lng - seattle_lng).abs
175
+ puts " Round-trip error: Lat=#{gh36_lat_error.round(8)}, Lng=#{gh36_lng_error.round(8)}"
176
+
177
+ # Neighbors
178
+ neighbors = gh36_coord.neighbors
179
+ puts " Neighbors:"
180
+ neighbors.each do |dir, neighbor|
181
+ puts " #{dir}: #{neighbor.to_s}"
182
+ end
183
+
184
+ # Area (bounding rectangle)
185
+ area = gh36_coord.to_area
186
+ puts " Cell area: NW=(#{area.nw.lat.round(6)}, #{area.nw.lng.round(6)}) SE=(#{area.se.lat.round(6)}, #{area.se.lng.round(6)})"
187
+ puts " Midpoint inside cell? #{area.includes?(gh36_coord.to_lla)}"
188
+ puts
189
+
190
+ # ========== British National Grid Conversion ==========
191
+ puts "British National Grid (BNG)"
192
+ london_lla = LLA.new(lat: 51.5007, lng: -0.1246, alt: 11.0)
193
+ bng_coord = BNG.from_lla(london_lla)
194
+ puts " BNG (London): #{bng_coord.easting.round(3)}E #{bng_coord.northing.round(3)}N"
195
+ puts " Grid reference: #{bng_coord.to_grid_reference(6)}"
196
+ puts " Grid reference (low precision): #{bng_coord.to_grid_reference(0)}"
197
+ puts
198
+
199
+ # ========== Geoid Height Demonstration ==========
200
+ puts "Geoid Height and Vertical Datum Support"
201
+ geoid = GeoidHeight.new(geoid_model: 'EGM2008')
202
+
203
+ seattle_geoid_height = geoid.geoid_height_at(seattle_lat, seattle_lng)
204
+ orthometric_height = geoid.ellipsoidal_to_orthometric(seattle_lat, seattle_lng, seattle_alt)
205
+
206
+ puts " Geoid height (EGM2008): #{seattle_geoid_height.round(3)}m"
207
+ puts " Ellipsoidal height (HAE): #{seattle_alt}m"
208
+ puts " Orthometric height (MSL): #{orthometric_height.round(3)}m"
209
+
210
+ # Test different geoid models
211
+ egm96_geoid = GeoidHeight.new(geoid_model: 'EGM96')
212
+ egm96_height = egm96_geoid.geoid_height_at(seattle_lat, seattle_lng)
213
+ puts " Geoid height (EGM96): #{egm96_height.round(3)}m"
214
+ puts " Model difference: #{(seattle_geoid_height - egm96_height).abs.round(3)}m"
215
+
216
+ # Test vertical datum conversion
217
+ navd88_height = geoid.convert_vertical_datum(seattle_lat, seattle_lng, seattle_alt, 'HAE', 'NAVD88')
218
+ puts " Height in NAVD88: #{navd88_height.round(3)}m"
219
+ puts
220
+
221
+ # ========== Cross-System Conversions ==========
222
+ puts "CROSS-SYSTEM CONVERSION CHAINS"
223
+ puts "-" * 50
224
+
225
+ # Chain 1: 3D systems that preserve altitude
226
+ puts "Chain 1 (3D systems): LLA -> ECEF -> UTM -> ENU -> NED -> LLA"
227
+ c1_ecef = lla_coord.to_ecef
228
+ c1_utm = c1_ecef.to_utm
229
+ c1_enu = c1_utm.to_enu(reference_lla)
230
+ c1_ned = c1_enu.to_ned
231
+ c1_final = c1_ned.to_lla(reference_lla)
232
+
233
+ c1_lat_err = (c1_final.lat - seattle_lat).abs
234
+ c1_lng_err = (c1_final.lng - seattle_lng).abs
235
+ c1_alt_err = (c1_final.alt - seattle_alt).abs
236
+
237
+ puts " Final: #{c1_final.lat.round(8)}°, #{c1_final.lng.round(8)}°, #{c1_final.alt.round(3)}m"
238
+ puts " Error: Lat=#{c1_lat_err.round(10)}°, Lng=#{c1_lng_err.round(10)}°, Alt=#{c1_alt_err.round(6)}m"
239
+ puts
240
+
241
+ # Chain 2: 2D systems (use altitude 0.0 since these systems don't carry altitude)
242
+ lla_2d = LLA.new(lat: seattle_lat, lng: seattle_lng, alt: 0.0)
243
+
244
+ puts "Chain 2 (2D systems): LLA -> MGRS -> USNG -> Web Mercator -> LLA"
245
+ puts " Starting with altitude 0.0 (2D systems do not carry altitude)"
246
+ c2_mgrs = MGRS.from_lla(lla_2d)
247
+ c2_usng = USNG.from_mgrs(c2_mgrs)
248
+ c2_web_merc = WebMerc.from_lla(c2_usng.to_lla)
249
+ c2_final = c2_web_merc.to_lla
250
+
251
+ c2_lat_err = (c2_final.lat - seattle_lat).abs
252
+ c2_lng_err = (c2_final.lng - seattle_lng).abs
253
+
254
+ puts " Final: #{c2_final.lat.round(8)}°, #{c2_final.lng.round(8)}°, #{c2_final.alt.round(3)}m"
255
+ puts " Error: Lat=#{c2_lat_err.round(8)}° (~#{(c2_lat_err * 111320).round(2)}m), Lng=#{c2_lng_err.round(8)}° (~#{(c2_lng_err * 111320 * Math.cos(seattle_lat * Math::PI / 180)).round(2)}m), Alt=#{c2_final.alt.round(3)}m"
256
+ puts " Lat/Lng error is from MGRS 1-meter grid precision truncation."
257
+ puts
258
+
259
+ # ========== Rectangle Area ==========
260
+ puts "RECTANGLE AREA"
261
+ puts "-" * 50
262
+
263
+ nw = LLA.new(lat: 47.65, lng: -122.40)
264
+ se = LLA.new(lat: 47.60, lng: -122.30)
265
+ rect = Areas::Rectangle.new(nw: nw, se: se)
266
+ puts " NW: (#{rect.nw.lat}, #{rect.nw.lng})"
267
+ puts " SE: (#{rect.se.lat}, #{rect.se.lng})"
268
+ puts " NE: (#{rect.ne.lat}, #{rect.ne.lng})"
269
+ puts " SW: (#{rect.sw.lat}, #{rect.sw.lng})"
270
+ puts " Centroid: (#{rect.centroid.lat}, #{rect.centroid.lng})"
271
+ puts " Space Needle inside? #{rect.includes?(lla_coord)}"
272
+ puts " London inside? #{rect.includes?(london_lla)}"
273
+
274
+ # Rectangle from non-LLA coordinates
275
+ nw_wm = WebMerc.from_lla(nw)
276
+ se_wm = WebMerc.from_lla(se)
277
+ rect_wm = Areas::Rectangle.new(nw: nw_wm, se: se_wm)
278
+ puts " From WebMercator: NW=(#{rect_wm.nw.lat.round(4)}, #{rect_wm.nw.lng.round(4)})"
279
+ puts
280
+
281
+ # ========== Summary ==========
282
+ puts "COORDINATE SYSTEM SUMMARY"
283
+ puts "-" * 50
284
+ puts "LLA (Latitude, Longitude, Altitude)"
285
+ puts "ECEF (Earth-Centered, Earth-Fixed)"
286
+ puts "UTM (Universal Transverse Mercator)"
287
+ puts "ENU (East, North, Up)"
288
+ puts "NED (North, East, Down)"
289
+ puts "MGRS (Military Grid Reference System)"
290
+ puts "USNG (US National Grid)"
291
+ puts "Web Mercator (EPSG:3857)"
292
+ puts "UPS (Universal Polar Stereographic)"
293
+ puts "State Plane Coordinates"
294
+ puts "British National Grid (BNG)"
295
+ puts "Geohash-36 (GH36)"
296
+ puts "Geoid Height Support"
297
+ puts
298
+ puts "Areas: Circle, Polygon, Rectangle"
299
+ puts
300
+ puts "All coordinate systems support complete bidirectional conversions!"
301
+ puts "Total coordinate systems implemented: 13"
302
+ puts "Total conversion paths available: 156 (13 x 12)"
303
+ puts
304
+ puts "=" * 80
305
+ end
306
+
307
+ # Run the demonstration if this file is executed directly
308
+ if __FILE__ == $0
309
+ demo_coordinate_systems
310
+ end
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Demonstration of distance calculations and the Distance class
4
+ # Shows great-circle distances, straight-line distances, unit conversions,
5
+ # and arithmetic with the Distance class.
6
+
7
+ require_relative "../lib/geodetic"
8
+
9
+ Distance = Geodetic::Distance
10
+
11
+ # ── Notable locations ────────────────────────────────────────────
12
+
13
+ seattle = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
14
+ portland = GCS::LLA.new(lat: 45.5152, lng: -122.6784, alt: 0.0)
15
+ sf = GCS::LLA.new(lat: 37.7749, lng: -122.4194, alt: 0.0)
16
+ nyc = GCS::LLA.new(lat: 40.7128, lng: -74.0060, alt: 0.0)
17
+ london = GCS::LLA.new(lat: 51.5074, lng: -0.1278, alt: 0.0)
18
+
19
+ puts "=== Distance Calculations Demo ==="
20
+ puts
21
+
22
+ # ── Basic distance_to ────────────────────────────────────────────
23
+
24
+ puts "--- Great-Circle Distances (Vincenty) ---"
25
+ puts
26
+
27
+ d = seattle.distance_to(portland)
28
+ puts "Seattle -> Portland:"
29
+ puts " #{d.meters.round(2)} meters"
30
+ puts " #{d.to_km.to_f.round(2)} km"
31
+ puts " #{d.to_mi.to_f.round(2)} miles"
32
+ puts " #{d.to_nmi.to_f.round(2)} nautical miles"
33
+ puts
34
+
35
+ d = seattle.distance_to(nyc)
36
+ puts "Seattle -> New York: #{d.to_mi.to_s}"
37
+ puts
38
+
39
+ d = seattle.distance_to(london)
40
+ puts "Seattle -> London: #{d.to_km.to_s}"
41
+ puts
42
+
43
+ # ── Radial distances (one-to-many) ──────────────────────────────
44
+
45
+ puts "--- Radial Distances from Seattle ---"
46
+ puts
47
+
48
+ distances = seattle.distance_to(portland, sf, nyc, london)
49
+ labels = %w[Portland SF NYC London]
50
+
51
+ distances.each_with_index do |dist, i|
52
+ puts " -> %-10s %10.1f km (%7.1f mi)" % [labels[i], dist.to_km.to_f, dist.to_mi.to_f]
53
+ end
54
+ puts
55
+
56
+ # ── Chain distances (consecutive pairs) ─────────────────────────
57
+
58
+ puts "--- Chain Distances (consecutive legs) ---"
59
+ puts "Route: Seattle -> Portland -> SF -> NYC"
60
+ puts
61
+
62
+ legs = GCS.distance_between(seattle, portland, sf, nyc)
63
+ leg_labels = ["Seattle -> Portland", "Portland -> SF", "SF -> NYC"]
64
+
65
+ total = Distance.new(0)
66
+ legs.each_with_index do |leg, i|
67
+ total = total + leg
68
+ puts " %-20s %8.1f km" % [leg_labels[i], leg.to_km.to_f]
69
+ end
70
+ puts " %-20s %8.1f km" % ["Total", total.to_km.to_f]
71
+ puts
72
+
73
+ # ── Straight-line vs great-circle ───────────────────────────────
74
+
75
+ puts "--- Straight-Line vs Great-Circle ---"
76
+ puts
77
+
78
+ gc = seattle.distance_to(london)
79
+ sl = seattle.straight_line_distance_to(london)
80
+
81
+ puts "Seattle -> London:"
82
+ puts " Great-circle: #{gc.to_km.to_f.round(1)} km"
83
+ puts " Straight-line: #{sl.to_km.to_f.round(1)} km (through the Earth)"
84
+ puts " Difference: #{(gc - sl).to_km.to_f.round(1)} km"
85
+ puts
86
+
87
+ # ── Cross-system distances ──────────────────────────────────────
88
+
89
+ puts "--- Cross-System Distances ---"
90
+ puts
91
+
92
+ utm_seattle = seattle.to_utm
93
+ mgrs_portland = GCS::MGRS.from_lla(portland)
94
+
95
+ d = utm_seattle.distance_to(mgrs_portland)
96
+ puts "UTM(Seattle) -> MGRS(Portland): #{d.to_km.to_f.round(2)} km"
97
+
98
+ wm_sf = GCS::WebMercator.from_lla(sf)
99
+ d = wm_sf.distance_to(utm_seattle)
100
+ puts "WebMercator(SF) -> UTM(Seattle): #{d.to_mi.to_f.round(2)} mi"
101
+
102
+ gh36_nyc = GCS::GH36.new(nyc)
103
+ d = gh36_nyc.distance_to(utm_seattle)
104
+ puts "GH36(NYC) -> UTM(Seattle): #{d.to_km.to_f.round(2)} km"
105
+ puts
106
+
107
+ # ── Distance class construction ─────────────────────────────────
108
+
109
+ puts "=== Distance Class Features ==="
110
+ puts
111
+
112
+ puts "--- Construction from different units ---"
113
+ puts
114
+
115
+ examples = [
116
+ Distance.new(1000),
117
+ Distance.km(5),
118
+ Distance.mi(3),
119
+ Distance.ft(5280),
120
+ Distance.nmi(1),
121
+ ]
122
+
123
+ examples.each do |d|
124
+ puts " %-40s => %10.3f meters" % [d.inspect, d.meters]
125
+ end
126
+ puts
127
+
128
+ # ── Unit conversions ────────────────────────────────────────────
129
+
130
+ puts "--- Unit Conversions ---"
131
+ puts
132
+
133
+ d = Distance.new(1609.344)
134
+ puts "1609.344 meters is:"
135
+ puts " #{d.to_km.to_f.round(6)} km"
136
+ puts " #{d.to_mi.to_f.round(6)} miles"
137
+ puts " #{d.to_ft.to_f.round(2)} feet"
138
+ puts " #{d.to_yd.to_f.round(2)} yards"
139
+ puts " #{d.to_nmi.to_f.round(6)} nautical miles"
140
+ puts " #{d.to_cm.to_f.round(2)} cm"
141
+ puts " #{d.to_mm.to_f.round(2)} mm"
142
+ puts
143
+
144
+ # ── Display formatting ──────────────────────────────────────────
145
+
146
+ puts "--- Display Formatting ---"
147
+ puts
148
+
149
+ d = Distance.km(42.195) # marathon distance
150
+ puts <<~HEREDOC
151
+ Marathon distance:
152
+ to_s: #{d.to_s}
153
+ to_s(3): #{d.to_s(3)}
154
+ to_s(1): #{d.to_s(1)}
155
+ to_s(0): #{d.to_s(0)}
156
+ to_f: #{d.to_f}
157
+ to_i: #{d.to_i}
158
+ inspect: #{d.inspect}
159
+ in miles: #{d.to_mi.to_s}
160
+ in feet: #{d.to_ft.to_s(0)}
161
+ HEREDOC
162
+
163
+ # ── Arithmetic ──────────────────────────────────────────────────
164
+
165
+ puts "--- Arithmetic ---"
166
+ puts
167
+
168
+ d1 = Distance.km(5)
169
+ d2 = Distance.mi(3)
170
+
171
+ puts "d1 = #{d1.inspect}"
172
+ puts "d2 = #{d2.inspect}"
173
+ puts
174
+
175
+ sum = d1 + d2
176
+ puts "d1 + d2 = #{sum.to_km.to_f.round(3)} km (#{sum.meters.round(3)} m)"
177
+
178
+ diff = d1 - d2
179
+ puts "d1 - d2 = #{diff.to_km.to_f.round(3)} km (#{diff.meters.round(3)} m)"
180
+
181
+ scaled = d1 * 3
182
+ puts "d1 * 3 = #{scaled.to_km.to_f.round(3)} km"
183
+
184
+ halved = d2 / 2
185
+ puts "d2 / 2 = #{halved.to_mi.to_f.round(3)} mi"
186
+
187
+ ratio = d1 / d2
188
+ puts "d1 / d2 = #{ratio.round(6)} (ratio)"
189
+ puts
190
+
191
+ # Numeric constants use the display unit
192
+ puts "--- Numeric Constants in Display Unit ---"
193
+ puts
194
+
195
+ d = Distance.new(5000).to_km # 5 km
196
+ puts "d = #{d.inspect}"
197
+ puts "d + 3 (adds 3 km) = #{(d + 3).to_km.to_f} km"
198
+ puts "d - 2 (subs 2 km) = #{(d - 2).to_km.to_f} km"
199
+ puts "d > 4 (4 km)? #{d > 4}"
200
+ puts "d < 4 (4 km)? #{d < 4}"
201
+ puts
202
+
203
+ # ── Comparison ──────────────────────────────────────────────────
204
+
205
+ puts "--- Comparison (unit-independent) ---"
206
+ puts
207
+
208
+ a = Distance.km(1)
209
+ b = Distance.new(1000)
210
+ c = Distance.mi(1)
211
+
212
+ puts "1 km == 1000 m? #{a == b}"
213
+ puts "1 mi > 1 km? #{c > a}"
214
+ puts "1 km < 1 mi? #{a < c}"
215
+ puts
216
+
217
+ # ── Coerce (Numeric * Distance) ────────────────────────────────
218
+
219
+ d = Distance.km(10)
220
+ result = 3 * d
221
+ puts "3 * #{d.to_s} = #{result} (via coerce)"
222
+ puts
223
+
224
+ puts "=== Done ==="