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,236 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstration of bearing calculations and the Bearing class
|
|
4
|
+
# Shows great-circle bearings, compass directions, elevation angles,
|
|
5
|
+
# cross-system bearings, and Bearing class features.
|
|
6
|
+
|
|
7
|
+
require_relative "../lib/geodetic"
|
|
8
|
+
|
|
9
|
+
Bearing = Geodetic::Bearing
|
|
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 "=== Bearing Calculations Demo ==="
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
# ── Basic bearing_to ──────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
puts "--- Great-Circle Bearings (Forward Azimuth) ---"
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
b = seattle.bearing_to(portland)
|
|
28
|
+
puts <<~HEREDOC
|
|
29
|
+
Seattle -> Portland:
|
|
30
|
+
#{b.to_s} (default: 4 decimals)
|
|
31
|
+
#{b.to_s(2)} (2 decimals)
|
|
32
|
+
#{b.to_s(0)} (integer)
|
|
33
|
+
#{b.to_compass(points: 16)} (16-point compass)
|
|
34
|
+
#{b.to_radians.round(4)} radians
|
|
35
|
+
Back azimuth: #{b.reverse.to_s}
|
|
36
|
+
HEREDOC
|
|
37
|
+
|
|
38
|
+
b = seattle.bearing_to(nyc)
|
|
39
|
+
puts "Seattle -> NYC: #{b.to_s} (#{b.to_compass(points: 8)})"
|
|
40
|
+
|
|
41
|
+
b = seattle.bearing_to(london)
|
|
42
|
+
puts "Seattle -> London: #{b.to_s} (#{b.to_compass(points: 8)})"
|
|
43
|
+
|
|
44
|
+
b = seattle.bearing_to(sf)
|
|
45
|
+
puts "Seattle -> SF: #{b.to_s} (#{b.to_compass(points: 8)})"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
# ── Compass directions at different resolutions ──────────────────
|
|
49
|
+
|
|
50
|
+
puts "--- Compass Resolution Comparison ---"
|
|
51
|
+
puts
|
|
52
|
+
|
|
53
|
+
targets = { "Portland" => portland, "SF" => sf, "NYC" => nyc, "London" => london }
|
|
54
|
+
|
|
55
|
+
puts " %-10s %12s %4s %4s %4s" % ["Target", "Degrees", "4pt", "8pt", "16pt"]
|
|
56
|
+
puts " " + "-" * 42
|
|
57
|
+
|
|
58
|
+
targets.each do |name, target|
|
|
59
|
+
b = seattle.bearing_to(target)
|
|
60
|
+
puts " %-10s %12s %4s %4s %4s" % [
|
|
61
|
+
name, b.to_s, b.to_compass(points: 4),
|
|
62
|
+
b.to_compass(points: 8), b.to_compass(points: 16)
|
|
63
|
+
]
|
|
64
|
+
end
|
|
65
|
+
puts
|
|
66
|
+
|
|
67
|
+
# ── Elevation angles ─────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
puts "--- Elevation Angles ---"
|
|
70
|
+
puts
|
|
71
|
+
|
|
72
|
+
ground = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 0.0)
|
|
73
|
+
hilltop = GCS::LLA.new(lat: 47.6205, lng: -122.3493, alt: 5000.0)
|
|
74
|
+
plane = GCS::LLA.new(lat: 47.6300, lng: -122.3400, alt: 10000.0)
|
|
75
|
+
nearby = GCS::LLA.new(lat: 47.6210, lng: -122.3490, alt: 100.0)
|
|
76
|
+
|
|
77
|
+
puts " From ground (0m) to directly above (5000m):"
|
|
78
|
+
puts " Elevation: #{ground.elevation_to(hilltop).round(2)}° (nearly straight up)"
|
|
79
|
+
puts
|
|
80
|
+
puts " From ground to airplane (10km alt, ~1km away):"
|
|
81
|
+
puts " Elevation: #{ground.elevation_to(plane).round(2)}°"
|
|
82
|
+
puts " Bearing: #{ground.bearing_to(plane).to_s}"
|
|
83
|
+
puts
|
|
84
|
+
puts " From ground to nearby hilltop (100m alt, ~60m away):"
|
|
85
|
+
puts " Elevation: #{ground.elevation_to(nearby).round(2)}°"
|
|
86
|
+
puts " Bearing: #{ground.bearing_to(nearby).to_s}"
|
|
87
|
+
puts
|
|
88
|
+
|
|
89
|
+
# ── Chain bearings ────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
puts "--- Chain Bearings (Consecutive Legs) ---"
|
|
92
|
+
puts "Route: Seattle -> Portland -> SF -> NYC -> London"
|
|
93
|
+
puts
|
|
94
|
+
|
|
95
|
+
bearings = GCS.bearing_between(seattle, portland, sf, nyc, london)
|
|
96
|
+
legs = [
|
|
97
|
+
"Seattle -> Portland",
|
|
98
|
+
"Portland -> SF",
|
|
99
|
+
"SF -> NYC",
|
|
100
|
+
"NYC -> London"
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
legs.each_with_index do |label, i|
|
|
104
|
+
b = bearings[i]
|
|
105
|
+
puts " %-22s %12s %-3s (reverse: %s)" % [
|
|
106
|
+
label, b.to_s, b.to_compass(points: 8), b.reverse.to_s
|
|
107
|
+
]
|
|
108
|
+
end
|
|
109
|
+
puts
|
|
110
|
+
|
|
111
|
+
# ── Cross-system bearings ────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
puts "--- Cross-System Bearings ---"
|
|
114
|
+
puts
|
|
115
|
+
|
|
116
|
+
utm_seattle = seattle.to_utm
|
|
117
|
+
mgrs_portland = GCS::MGRS.from_lla(portland)
|
|
118
|
+
wm_sf = GCS::WebMercator.from_lla(sf)
|
|
119
|
+
|
|
120
|
+
b = utm_seattle.bearing_to(mgrs_portland)
|
|
121
|
+
puts " UTM(Seattle) -> MGRS(Portland): #{b.to_s} (#{b.to_compass})"
|
|
122
|
+
|
|
123
|
+
b = mgrs_portland.bearing_to(wm_sf)
|
|
124
|
+
puts " MGRS(Portland) -> WebMercator(SF): #{b.to_s} (#{b.to_compass})"
|
|
125
|
+
|
|
126
|
+
b = wm_sf.bearing_to(utm_seattle)
|
|
127
|
+
puts " WebMercator(SF) -> UTM(Seattle): #{b.to_s} (#{b.to_compass})"
|
|
128
|
+
|
|
129
|
+
gh36_nyc = GCS::GH36.new(nyc)
|
|
130
|
+
b = gh36_nyc.bearing_to(utm_seattle)
|
|
131
|
+
puts " GH36(NYC) -> UTM(Seattle): #{b.to_s} (#{b.to_compass})"
|
|
132
|
+
puts
|
|
133
|
+
|
|
134
|
+
# ── Local bearings (ENU/NED) ─────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
puts "--- Local Tangent Plane Bearings (ENU / NED) ---"
|
|
137
|
+
puts
|
|
138
|
+
|
|
139
|
+
enu_origin = GCS::ENU.new(e: 0.0, n: 0.0, u: 0.0)
|
|
140
|
+
enu_ne = GCS::ENU.new(e: 100.0, n: 100.0, u: 0.0)
|
|
141
|
+
enu_south = GCS::ENU.new(e: 0.0, n: -200.0, u: 50.0)
|
|
142
|
+
|
|
143
|
+
puts " ENU local_bearing_to:"
|
|
144
|
+
puts " Origin -> NE point (100,100): #{enu_origin.local_bearing_to(enu_ne).round(2)}°"
|
|
145
|
+
puts " Origin -> South point (0,-200): #{enu_origin.local_bearing_to(enu_south).round(2)}°"
|
|
146
|
+
puts
|
|
147
|
+
|
|
148
|
+
ned_origin = GCS::NED.new(n: 0.0, e: 0.0, d: 0.0)
|
|
149
|
+
ned_above = GCS::NED.new(n: 100.0, e: 0.0, d: -100.0)
|
|
150
|
+
ned_below = GCS::NED.new(n: 100.0, e: 0.0, d: 100.0)
|
|
151
|
+
|
|
152
|
+
puts " NED local_bearing_to / local_elevation_angle_to:"
|
|
153
|
+
puts " Origin -> N+Up: bearing #{ned_origin.local_bearing_to(ned_above).round(2)}°, elevation #{ned_origin.local_elevation_angle_to(ned_above).round(2)}°"
|
|
154
|
+
puts " Origin -> N+Down: bearing #{ned_origin.local_bearing_to(ned_below).round(2)}°, elevation #{ned_origin.local_elevation_angle_to(ned_below).round(2)}°"
|
|
155
|
+
puts
|
|
156
|
+
|
|
157
|
+
# ── Bearing class features ───────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
puts "=== Bearing Class Features ==="
|
|
160
|
+
puts
|
|
161
|
+
|
|
162
|
+
# ── Construction and normalization ────────────────────────────────
|
|
163
|
+
|
|
164
|
+
puts "--- Construction (auto-normalizes to 0-360) ---"
|
|
165
|
+
puts
|
|
166
|
+
|
|
167
|
+
[0, 90, -90, 450, 720, -180].each do |deg|
|
|
168
|
+
puts " Bearing.new(%-4d) => %s" % [deg, Bearing.new(deg).to_s]
|
|
169
|
+
end
|
|
170
|
+
puts
|
|
171
|
+
|
|
172
|
+
# ── Reverse (back azimuth) ───────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
puts "--- Reverse (Back Azimuth) ---"
|
|
175
|
+
puts
|
|
176
|
+
|
|
177
|
+
[0, 45, 90, 180, 270, 315].each do |deg|
|
|
178
|
+
b = Bearing.new(deg)
|
|
179
|
+
puts " %-10s reverse => %s" % [b.to_s, b.reverse.to_s]
|
|
180
|
+
end
|
|
181
|
+
puts
|
|
182
|
+
|
|
183
|
+
# ── Arithmetic ───────────────────────────────────────────────────
|
|
184
|
+
|
|
185
|
+
puts "--- Arithmetic ---"
|
|
186
|
+
puts
|
|
187
|
+
|
|
188
|
+
b = Bearing.new(350)
|
|
189
|
+
puts " b = #{b.to_s}"
|
|
190
|
+
puts " b + 20 = #{(b + 20).to_s} (wraps past 360)"
|
|
191
|
+
puts " b - 10 = #{(b - 10).to_s}"
|
|
192
|
+
puts
|
|
193
|
+
|
|
194
|
+
b1 = Bearing.new(90)
|
|
195
|
+
b2 = Bearing.new(45)
|
|
196
|
+
puts " Bearing(90) - Bearing(45) = #{(b1 - b2)} (Float, angular difference)"
|
|
197
|
+
puts " Bearing(45) - Bearing(90) = #{(b2 - b1)} (negative difference)"
|
|
198
|
+
puts
|
|
199
|
+
|
|
200
|
+
# ── Comparison ───────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
puts "--- Comparison ---"
|
|
203
|
+
puts
|
|
204
|
+
|
|
205
|
+
puts " Bearing(90) == Bearing(90)? #{Bearing.new(90) == Bearing.new(90)}"
|
|
206
|
+
puts " Bearing(90) == Bearing(450)? #{Bearing.new(90) == Bearing.new(450)}"
|
|
207
|
+
puts " Bearing(45) < Bearing(90)? #{Bearing.new(45) < Bearing.new(90)}"
|
|
208
|
+
puts " Bearing(270) > 180? #{Bearing.new(270) > 180}"
|
|
209
|
+
puts
|
|
210
|
+
|
|
211
|
+
# ── Combined distance and bearing ────────────────────────────────
|
|
212
|
+
|
|
213
|
+
puts "=== Combined Distance + Bearing ==="
|
|
214
|
+
puts
|
|
215
|
+
|
|
216
|
+
puts " %-22s %10s %12s %6s" % ["Leg", "Distance", "Bearing", "Dir"]
|
|
217
|
+
puts " " + "-" * 54
|
|
218
|
+
|
|
219
|
+
route = [
|
|
220
|
+
["Seattle", seattle],
|
|
221
|
+
["Portland", portland],
|
|
222
|
+
["SF", sf],
|
|
223
|
+
["NYC", nyc],
|
|
224
|
+
["London", london]
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
route.each_cons(2) do |(name_a, coord_a), (name_b, coord_b)|
|
|
228
|
+
d = coord_a.distance_to(coord_b)
|
|
229
|
+
b = coord_a.bearing_to(coord_b)
|
|
230
|
+
label = "#{name_a} -> #{name_b}"
|
|
231
|
+
km_str = "%.1f km" % d.to_km.to_f
|
|
232
|
+
puts " %-22s %10s %12s %6s" % [label, km_str, b.to_s, b.to_compass(points: 8)]
|
|
233
|
+
end
|
|
234
|
+
puts
|
|
235
|
+
|
|
236
|
+
puts "=== Done ==="
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../coordinates/lla'
|
|
4
|
+
|
|
5
|
+
module Geodetic
|
|
6
|
+
module Areas
|
|
7
|
+
class Circle
|
|
8
|
+
attr_reader :centroid, :radius
|
|
9
|
+
|
|
10
|
+
def initialize(centroid:, radius:)
|
|
11
|
+
@centroid = centroid # LLA point
|
|
12
|
+
@radius = radius # in units of meters
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def includes?(a_point)
|
|
16
|
+
@centroid.distance_to(a_point).meters <= @radius
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def excludes?(a_point)
|
|
20
|
+
!includes?(a_point)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias_method :include?, :includes?
|
|
24
|
+
alias_method :exclude?, :excludes?
|
|
25
|
+
alias_method :inside?, :includes?
|
|
26
|
+
alias_method :outside?, :excludes?
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../coordinates/lla'
|
|
4
|
+
|
|
5
|
+
module Geodetic
|
|
6
|
+
module Areas
|
|
7
|
+
class Polygon
|
|
8
|
+
attr_reader :boundary, :centroid
|
|
9
|
+
|
|
10
|
+
def initialize(boundary:)
|
|
11
|
+
raise ArgumentError, "A Polygon requires more than #{boundary.length} points on its boundary" unless boundary.length > 2
|
|
12
|
+
|
|
13
|
+
@boundary = boundary.dup
|
|
14
|
+
@boundary << boundary[0] unless boundary.first == boundary.last
|
|
15
|
+
|
|
16
|
+
centroid_lat = 0.0
|
|
17
|
+
centroid_lng = 0.0
|
|
18
|
+
area = 0.0
|
|
19
|
+
|
|
20
|
+
0.upto(@boundary.length - 2) do |i|
|
|
21
|
+
cross = @boundary[i].lng * @boundary[i + 1].lat - @boundary[i + 1].lng * @boundary[i].lat
|
|
22
|
+
area += 0.5 * cross
|
|
23
|
+
centroid_lng += (@boundary[i].lng + @boundary[i + 1].lng) * cross
|
|
24
|
+
centroid_lat += (@boundary[i].lat + @boundary[i + 1].lat) * cross
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
centroid_lng /= (6.0 * area)
|
|
28
|
+
centroid_lat /= (6.0 * area)
|
|
29
|
+
|
|
30
|
+
@centroid = Coordinates::LLA.new(lat: centroid_lat, lng: centroid_lng, alt: 0.0)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def includes?(a_point)
|
|
34
|
+
turn_angle = 0.0
|
|
35
|
+
|
|
36
|
+
(@boundary.length - 2).times do |index|
|
|
37
|
+
return true if @boundary[index] == a_point
|
|
38
|
+
|
|
39
|
+
d_turn_angle = a_point.bearing_to(@boundary[index + 1]) - a_point.bearing_to(@boundary[index])
|
|
40
|
+
d_turn_angle += (d_turn_angle > 0.0 ? -360.0 : 360.0) if d_turn_angle.abs > 180.0
|
|
41
|
+
turn_angle += d_turn_angle
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
turn_angle.abs > 180.0
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def excludes?(a_point)
|
|
48
|
+
!includes?(a_point)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
alias_method :include?, :includes?
|
|
52
|
+
alias_method :exclude?, :excludes?
|
|
53
|
+
alias_method :inside?, :includes?
|
|
54
|
+
alias_method :outside?, :excludes?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../coordinates/lla'
|
|
4
|
+
|
|
5
|
+
module Geodetic
|
|
6
|
+
module Areas
|
|
7
|
+
class Rectangle
|
|
8
|
+
attr_reader :nw, :se, :centroid
|
|
9
|
+
|
|
10
|
+
# Define an axis-aligned rectangle by its NW and SE corners.
|
|
11
|
+
# Accepts any coordinate that responds to to_lla.
|
|
12
|
+
#
|
|
13
|
+
# Rectangle.new(
|
|
14
|
+
# nw: LLA.new(lat: 41.0, lng: -75.0),
|
|
15
|
+
# se: LLA.new(lat: 40.0, lng: -74.0)
|
|
16
|
+
# )
|
|
17
|
+
def initialize(nw:, se:)
|
|
18
|
+
@nw = nw.is_a?(Coordinates::LLA) ? nw : nw.to_lla
|
|
19
|
+
@se = se.is_a?(Coordinates::LLA) ? se : se.to_lla
|
|
20
|
+
|
|
21
|
+
raise ArgumentError, "NW corner must have higher latitude than SE corner" if @nw.lat < @se.lat
|
|
22
|
+
raise ArgumentError, "NW corner must have lower longitude than SE corner" if @nw.lng > @se.lng
|
|
23
|
+
|
|
24
|
+
@centroid = Coordinates::LLA.new(
|
|
25
|
+
lat: (@nw.lat + @se.lat) / 2.0,
|
|
26
|
+
lng: (@nw.lng + @se.lng) / 2.0,
|
|
27
|
+
alt: 0.0
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def ne
|
|
32
|
+
Coordinates::LLA.new(lat: @nw.lat, lng: @se.lng, alt: 0.0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def sw
|
|
36
|
+
Coordinates::LLA.new(lat: @se.lat, lng: @nw.lng, alt: 0.0)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def includes?(a_point)
|
|
40
|
+
lla = a_point.respond_to?(:to_lla) ? a_point.to_lla : a_point
|
|
41
|
+
lla.lat >= @se.lat && lla.lat <= @nw.lat &&
|
|
42
|
+
lla.lng >= @nw.lng && lla.lng <= @se.lng
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def excludes?(a_point)
|
|
46
|
+
!includes?(a_point)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
alias_method :include?, :includes?
|
|
50
|
+
alias_method :exclude?, :excludes?
|
|
51
|
+
alias_method :inside?, :includes?
|
|
52
|
+
alias_method :outside?, :excludes?
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Geodetic
|
|
4
|
+
class Bearing
|
|
5
|
+
include Comparable
|
|
6
|
+
|
|
7
|
+
COMPASS_16 = %w[N NNE NE ENE E ESE SE SSE S SSW SW WSW W WNW NW NNW].freeze
|
|
8
|
+
COMPASS_8 = %w[N NE E SE S SW W NW].freeze
|
|
9
|
+
COMPASS_4 = %w[N E S W].freeze
|
|
10
|
+
|
|
11
|
+
attr_reader :degrees
|
|
12
|
+
|
|
13
|
+
def initialize(degrees)
|
|
14
|
+
@degrees = degrees.to_f % 360.0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_f
|
|
18
|
+
@degrees
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_i
|
|
22
|
+
@degrees.to_i
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def to_radians
|
|
26
|
+
@degrees * RAD_PER_DEG
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Back azimuth (reverse bearing)
|
|
30
|
+
def reverse
|
|
31
|
+
Bearing.new(@degrees + 180.0)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Compass direction string.
|
|
35
|
+
# points: 4 (N/E/S/W), 8 (N/NE/E/...), or 16 (N/NNE/NE/ENE/...)
|
|
36
|
+
def to_compass(points: 16)
|
|
37
|
+
case points
|
|
38
|
+
when 4 then COMPASS_4[(((@degrees + 45.0) % 360.0) / 90.0).to_i]
|
|
39
|
+
when 8 then COMPASS_8[(((@degrees + 22.5) % 360.0) / 45.0).to_i]
|
|
40
|
+
when 16 then COMPASS_16[(((@degrees + 11.25) % 360.0) / 22.5).to_i]
|
|
41
|
+
else raise ArgumentError, "points must be 4, 8, or 16"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def to_s(precision = 4)
|
|
46
|
+
precision = precision.to_i
|
|
47
|
+
if precision == 0
|
|
48
|
+
"#{@degrees.round}°"
|
|
49
|
+
else
|
|
50
|
+
format("%.#{precision}f°", @degrees)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def inspect
|
|
55
|
+
"#<Geodetic::Bearing #{to_s} (#{to_compass})>"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def <=>(other)
|
|
59
|
+
case other
|
|
60
|
+
when Bearing then @degrees <=> other.degrees
|
|
61
|
+
when Numeric then @degrees <=> other.to_f
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Bearing - Bearing => Float (angular difference)
|
|
66
|
+
# Bearing - Numeric => Bearing (subtract degrees)
|
|
67
|
+
def -(other)
|
|
68
|
+
case other
|
|
69
|
+
when Bearing then @degrees - other.degrees
|
|
70
|
+
when Numeric then Bearing.new(@degrees - other)
|
|
71
|
+
else raise ArgumentError, "Cannot subtract #{other.class} from Bearing"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Bearing + Numeric => Bearing
|
|
76
|
+
def +(other)
|
|
77
|
+
case other
|
|
78
|
+
when Numeric then Bearing.new(@degrees + other)
|
|
79
|
+
else raise ArgumentError, "Cannot add #{other.class} to Bearing"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def coerce(other)
|
|
84
|
+
case other
|
|
85
|
+
when Numeric then [other, @degrees]
|
|
86
|
+
else raise TypeError, "#{other.class} can't be coerced into Bearing"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def zero?
|
|
91
|
+
@degrees == 0.0
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|