geodetic 0.3.2 → 0.5.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -2
  3. data/README.md +121 -8
  4. data/docs/coordinate-systems/gars.md +2 -2
  5. data/docs/coordinate-systems/georef.md +2 -2
  6. data/docs/coordinate-systems/gh.md +2 -2
  7. data/docs/coordinate-systems/gh36.md +2 -2
  8. data/docs/coordinate-systems/h3.md +2 -2
  9. data/docs/coordinate-systems/ham.md +2 -2
  10. data/docs/coordinate-systems/olc.md +2 -2
  11. data/docs/index.md +4 -2
  12. data/docs/reference/areas.md +140 -14
  13. data/docs/reference/arithmetic.md +368 -0
  14. data/docs/reference/feature.md +2 -2
  15. data/docs/reference/path.md +3 -3
  16. data/docs/reference/segment.md +181 -0
  17. data/docs/reference/vector.md +256 -0
  18. data/examples/02_all_coordinate_systems.rb +6 -6
  19. data/examples/06_path_operations.rb +2 -4
  20. data/examples/07_segments_and_shapes.rb +258 -0
  21. data/examples/08_geodetic_arithmetic.rb +393 -0
  22. data/examples/README.md +35 -1
  23. data/lib/geodetic/areas/bounding_box.rb +56 -0
  24. data/lib/geodetic/areas/circle.rb +8 -0
  25. data/lib/geodetic/areas/hexagon.rb +11 -0
  26. data/lib/geodetic/areas/octagon.rb +11 -0
  27. data/lib/geodetic/areas/pentagon.rb +11 -0
  28. data/lib/geodetic/areas/polygon.rb +64 -14
  29. data/lib/geodetic/areas/rectangle.rb +85 -35
  30. data/lib/geodetic/areas/regular_polygon.rb +59 -0
  31. data/lib/geodetic/areas/triangle.rb +180 -0
  32. data/lib/geodetic/areas.rb +6 -0
  33. data/lib/geodetic/coordinate/gh36.rb +1 -1
  34. data/lib/geodetic/coordinate/h3.rb +1 -1
  35. data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
  36. data/lib/geodetic/coordinate.rb +26 -1
  37. data/lib/geodetic/distance.rb +5 -1
  38. data/lib/geodetic/path.rb +85 -153
  39. data/lib/geodetic/segment.rb +193 -0
  40. data/lib/geodetic/vector.rb +242 -0
  41. data/lib/geodetic/version.rb +1 -1
  42. data/lib/geodetic.rb +2 -0
  43. data/mkdocs.yml +1 -0
  44. metadata +14 -1
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Geodetic
4
+ class Vector
5
+ include Comparable
6
+
7
+ attr_reader :distance, :bearing
8
+
9
+ def initialize(distance:, bearing:)
10
+ @distance = distance.is_a?(Distance) ? distance : Distance.new(distance.to_f)
11
+ @bearing = bearing.is_a?(Bearing) ? bearing : Bearing.new(bearing.to_f)
12
+ end
13
+
14
+ # --- Components (meters) ---
15
+
16
+ def north
17
+ @distance.meters * Math.cos(@bearing.to_radians)
18
+ end
19
+
20
+ def east
21
+ @distance.meters * Math.sin(@bearing.to_radians)
22
+ end
23
+
24
+ # --- Factory ---
25
+
26
+ ZERO_TOLERANCE = 1e-9
27
+
28
+ def self.from_components(north:, east:)
29
+ meters = Math.sqrt(north**2 + east**2)
30
+
31
+ if meters < ZERO_TOLERANCE
32
+ new(distance: Distance.new(0.0), bearing: Bearing.new(0.0))
33
+ else
34
+ new(distance: Distance.new(meters), bearing: Bearing.new(Math.atan2(east, north) * DEG_PER_RAD))
35
+ end
36
+ end
37
+
38
+ def self.from_segment(segment)
39
+ new(distance: segment.length, bearing: segment.bearing)
40
+ end
41
+
42
+ # --- Arithmetic ---
43
+
44
+ def +(other)
45
+ case other
46
+ when Vector
47
+ self.class.from_components(north: north + other.north, east: east + other.east)
48
+ when Segment
49
+ new_start = reverse.destination_from(other.start_point)
50
+ Path.new(coordinates: [new_start, other.start_point, other.end_point])
51
+ when ->(o) { Coordinate.systems.any? { |k| o.is_a?(k) } }
52
+ Segment.new(reverse.destination_from(other), other)
53
+ else
54
+ raise ArgumentError, "Cannot add #{other.class} to Vector"
55
+ end
56
+ end
57
+
58
+ def -(other)
59
+ case other
60
+ when Vector
61
+ self.class.from_components(north: north - other.north, east: east - other.east)
62
+ else
63
+ raise ArgumentError, "Cannot subtract #{other.class} from Vector"
64
+ end
65
+ end
66
+
67
+ def *(scalar)
68
+ raise ArgumentError, "Cannot multiply Vector by #{scalar.class}" unless scalar.is_a?(Numeric)
69
+
70
+ if scalar < 0
71
+ self.class.new(distance: Distance.new(@distance.meters * scalar.abs), bearing: @bearing.reverse)
72
+ else
73
+ self.class.new(distance: Distance.new(@distance.meters * scalar), bearing: @bearing)
74
+ end
75
+ end
76
+
77
+ def /(scalar)
78
+ raise ArgumentError, "Cannot divide Vector by #{scalar.class}" unless scalar.is_a?(Numeric)
79
+ raise ZeroDivisionError, "Cannot divide Vector by zero" if scalar == 0
80
+
81
+ self * (1.0 / scalar)
82
+ end
83
+
84
+ def -@
85
+ self.class.new(distance: @distance, bearing: @bearing.reverse)
86
+ end
87
+
88
+ def coerce(other)
89
+ case other
90
+ when Numeric then [ScalarCoerce.new(other), self]
91
+ else raise TypeError, "#{other.class} can't be coerced into Vector"
92
+ end
93
+ end
94
+
95
+ # @private
96
+ ScalarCoerce = Struct.new(:value) do
97
+ def *(vector)
98
+ vector * value
99
+ end
100
+ end
101
+
102
+ # --- Properties ---
103
+
104
+ def magnitude
105
+ @distance.meters
106
+ end
107
+
108
+ def zero?
109
+ @distance.zero?
110
+ end
111
+
112
+ def reverse
113
+ -self
114
+ end
115
+
116
+ alias inverse reverse
117
+
118
+ def normalize
119
+ return self if zero?
120
+
121
+ self.class.new(distance: Distance.new(1.0), bearing: @bearing)
122
+ end
123
+
124
+ def dot(other)
125
+ raise ArgumentError, "Cannot dot Vector with #{other.class}" unless other.is_a?(Vector)
126
+
127
+ north * other.north + east * other.east
128
+ end
129
+
130
+ def cross(other)
131
+ raise ArgumentError, "Cannot cross Vector with #{other.class}" unless other.is_a?(Vector)
132
+
133
+ north * other.east - east * other.north
134
+ end
135
+
136
+ def angle_between(other)
137
+ raise ArgumentError, "expected a Vector" unless other.is_a?(Vector)
138
+
139
+ Bearing.new(other.bearing.degrees - @bearing.degrees)
140
+ end
141
+
142
+ # --- Vincenty direct ---
143
+
144
+ def destination_from(origin)
145
+ lla = origin.is_a?(Coordinate::LLA) ? origin : origin.to_lla
146
+
147
+ a = WGS84.a
148
+ f = WGS84.f
149
+ b = WGS84.b
150
+ s = @distance.meters
151
+
152
+ return lla if s == 0.0
153
+
154
+ alpha1 = @bearing.degrees * RAD_PER_DEG
155
+ sin_alpha1 = Math.sin(alpha1)
156
+ cos_alpha1 = Math.cos(alpha1)
157
+
158
+ tan_u1 = (1 - f) * Math.tan(lla.lat * RAD_PER_DEG)
159
+ cos_u1 = 1.0 / Math.sqrt(1 + tan_u1**2)
160
+ sin_u1 = tan_u1 * cos_u1
161
+
162
+ sigma1 = Math.atan2(tan_u1, cos_alpha1)
163
+ sin_alpha = cos_u1 * sin_alpha1
164
+ cos2_alpha = 1 - sin_alpha**2
165
+
166
+ u_sq = cos2_alpha * (a**2 - b**2) / b**2
167
+ aa = 1 + u_sq / 16384 * (4096 + u_sq * (-768 + u_sq * (320 - 175 * u_sq)))
168
+ bb = u_sq / 1024 * (256 + u_sq * (-128 + u_sq * (74 - 47 * u_sq)))
169
+
170
+ sigma = s / (b * aa)
171
+
172
+ sin_sigma = 0.0
173
+ cos_sigma = 0.0
174
+ cos_2sigma_m = 0.0
175
+
176
+ 100.times do
177
+ cos_2sigma_m = Math.cos(2 * sigma1 + sigma)
178
+ sin_sigma = Math.sin(sigma)
179
+ cos_sigma = Math.cos(sigma)
180
+
181
+ delta_sigma = bb * sin_sigma * (cos_2sigma_m + bb / 4 *
182
+ (cos_sigma * (-1 + 2 * cos_2sigma_m**2) -
183
+ bb / 6 * cos_2sigma_m * (-3 + 4 * sin_sigma**2) * (-3 + 4 * cos_2sigma_m**2)))
184
+
185
+ sigma_prev = sigma
186
+ sigma = s / (b * aa) + delta_sigma
187
+
188
+ break if (sigma - sigma_prev).abs < 1e-12
189
+ end
190
+
191
+ sin_sigma = Math.sin(sigma)
192
+ cos_sigma = Math.cos(sigma)
193
+ cos_2sigma_m = Math.cos(2 * sigma1 + sigma)
194
+
195
+ lat2 = Math.atan2(
196
+ sin_u1 * cos_sigma + cos_u1 * sin_sigma * cos_alpha1,
197
+ (1 - f) * Math.sqrt(sin_alpha**2 +
198
+ (sin_u1 * sin_sigma - cos_u1 * cos_sigma * cos_alpha1)**2)
199
+ )
200
+
201
+ lambda_val = Math.atan2(
202
+ sin_sigma * sin_alpha1,
203
+ cos_u1 * cos_sigma - sin_u1 * sin_sigma * cos_alpha1
204
+ )
205
+
206
+ c = f / 16 * cos2_alpha * (4 + f * (4 - 3 * cos2_alpha))
207
+
208
+ l = lambda_val - (1 - c) * f * sin_alpha *
209
+ (sigma + c * sin_sigma * (cos_2sigma_m + c * cos_sigma * (-1 + 2 * cos_2sigma_m**2)))
210
+
211
+ Coordinate::LLA.new(
212
+ lat: lat2 * DEG_PER_RAD,
213
+ lng: lla.lng + l * DEG_PER_RAD,
214
+ alt: lla.alt
215
+ )
216
+ end
217
+
218
+ # --- Comparison ---
219
+
220
+ def <=>(other)
221
+ case other
222
+ when Vector then @distance <=> other.distance
223
+ end
224
+ end
225
+
226
+ def ==(other)
227
+ other.is_a?(Vector) &&
228
+ @distance == other.distance &&
229
+ @bearing == other.bearing
230
+ end
231
+
232
+ # --- Display ---
233
+
234
+ def to_s
235
+ "Vector(#{@distance}, #{@bearing})"
236
+ end
237
+
238
+ def inspect
239
+ "#<Geodetic::Vector distance=#{@distance.inspect} bearing=#{@bearing.inspect}>"
240
+ end
241
+ end
242
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Geodetic
4
- VERSION = "0.3.2"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/geodetic.rb CHANGED
@@ -7,6 +7,8 @@ require_relative "geodetic/bearing"
7
7
  require_relative "geodetic/geoid_height"
8
8
  require_relative "geodetic/coordinate"
9
9
  require_relative "geodetic/areas"
10
+ require_relative "geodetic/vector"
11
+ require_relative "geodetic/segment"
10
12
  require_relative "geodetic/path"
11
13
  require_relative "geodetic/feature"
12
14
 
data/mkdocs.yml CHANGED
@@ -136,6 +136,7 @@ nav:
136
136
  - Datums: reference/datums.md
137
137
  - Geoid Height: reference/geoid-height.md
138
138
  - Areas: reference/areas.md
139
+ - Segment: reference/segment.md
139
140
  - Path: reference/path.md
140
141
  - Feature: reference/feature.md
141
142
  - Serialization: reference/serialization.md
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: geodetic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -51,13 +51,16 @@ files:
51
51
  - docs/getting-started/quick-start.md
52
52
  - docs/index.md
53
53
  - docs/reference/areas.md
54
+ - docs/reference/arithmetic.md
54
55
  - docs/reference/conversions.md
55
56
  - docs/reference/datums.md
56
57
  - docs/reference/feature.md
57
58
  - docs/reference/geoid-height.md
58
59
  - docs/reference/map-rendering.md
59
60
  - docs/reference/path.md
61
+ - docs/reference/segment.md
60
62
  - docs/reference/serialization.md
63
+ - docs/reference/vector.md
61
64
  - examples/01_basic_conversions.rb
62
65
  - examples/02_all_coordinate_systems.rb
63
66
  - examples/03_distance_calculations.rb
@@ -71,13 +74,21 @@ files:
71
74
  - examples/05_map_rendering/icons/park.png
72
75
  - examples/05_map_rendering/nyc_landmarks.png
73
76
  - examples/06_path_operations.rb
77
+ - examples/07_segments_and_shapes.rb
78
+ - examples/08_geodetic_arithmetic.rb
74
79
  - examples/README.md
75
80
  - fiddle_pointer_buffer_pool.md
76
81
  - lib/geodetic.rb
77
82
  - lib/geodetic/areas.rb
83
+ - lib/geodetic/areas/bounding_box.rb
78
84
  - lib/geodetic/areas/circle.rb
85
+ - lib/geodetic/areas/hexagon.rb
86
+ - lib/geodetic/areas/octagon.rb
87
+ - lib/geodetic/areas/pentagon.rb
79
88
  - lib/geodetic/areas/polygon.rb
80
89
  - lib/geodetic/areas/rectangle.rb
90
+ - lib/geodetic/areas/regular_polygon.rb
91
+ - lib/geodetic/areas/triangle.rb
81
92
  - lib/geodetic/bearing.rb
82
93
  - lib/geodetic/coordinate.rb
83
94
  - lib/geodetic/coordinate/bng.rb
@@ -104,6 +115,8 @@ files:
104
115
  - lib/geodetic/feature.rb
105
116
  - lib/geodetic/geoid_height.rb
106
117
  - lib/geodetic/path.rb
118
+ - lib/geodetic/segment.rb
119
+ - lib/geodetic/vector.rb
107
120
  - lib/geodetic/version.rb
108
121
  - mkdocs.yml
109
122
  - sig/geodetic.rbs