geodetic 0.3.2 → 0.4.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.
@@ -0,0 +1,181 @@
1
+ # Segment Reference
2
+
3
+ `Geodetic::Segment` represents a directed line segment between two points on the Earth's surface. It is the fundamental geometric primitive underlying `Path` segments, `Polygon` edges, and closest-approach calculations.
4
+
5
+ A Segment has a `start_point` and an `end_point`, both stored as `Coordinate::LLA`. Properties like `length`, `bearing`, and `midpoint` are computed lazily and cached.
6
+
7
+ ---
8
+
9
+ ## Great Circle Arcs
10
+
11
+ On a sphere, any two points that are not antipodal (diametrically opposite) define a great circle, and that great circle produces two arcs between them: the **minor arc** (the shorter path) and the **major arc** (the longer way around the globe).
12
+
13
+ Segment always uses the **minor arc**. All operations — `length`, `bearing`, `interpolate`, `project`, `contains?` — follow the shortest path between the two endpoints via Vincenty geodesic calculations.
14
+
15
+ For **antipodal points** (exactly opposite sides of the Earth, roughly 20,000 km apart), the great circle is degenerate: there are infinitely many paths of equal length and the bearing is undefined.
16
+
17
+ In practice, real-world segments rarely approach even a quarter of the Earth's circumference (~10,000 km), so the minor arc assumption holds for virtually all use cases.
18
+
19
+ ---
20
+
21
+ ## Constructor
22
+
23
+ ```ruby
24
+ a = Geodetic::Coordinate::LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
25
+ b = Geodetic::Coordinate::LLA.new(lat: 40.7580, lng: -73.9855, alt: 0)
26
+
27
+ seg = Geodetic::Segment.new(a, b)
28
+ ```
29
+
30
+ Accepts any coordinate type that responds to `to_lla`. Endpoints are converted to LLA on construction.
31
+
32
+ ---
33
+
34
+ ## Attributes
35
+
36
+ | Attribute | Type | Description |
37
+ |---------------|------|-------------|
38
+ | `start_point` | LLA | The starting point of the segment |
39
+ | `end_point` | LLA | The ending point of the segment |
40
+
41
+ ---
42
+
43
+ ## Properties
44
+
45
+ All properties are lazily computed and cached after first access.
46
+
47
+ | Method | Returns | Description |
48
+ |----------------|----------|-------------|
49
+ | `length` | Distance | Great-circle distance between endpoints |
50
+ | `distance` | Distance | Alias for `length` |
51
+ | `length_meters`| Float | Length in meters (convenience accessor) |
52
+ | `bearing` | Bearing | Forward azimuth from start to end |
53
+ | `midpoint` | LLA | Point at the halfway mark |
54
+
55
+ ```ruby
56
+ seg.length # => #<Geodetic::Distance 1067.45 m>
57
+ seg.distance # => #<Geodetic::Distance 1067.45 m> (alias)
58
+ seg.length_meters # => 1067.45
59
+ seg.bearing # => #<Geodetic::Bearing 1.0°>
60
+ seg.midpoint # => LLA at the midpoint
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Geometry
66
+
67
+ ### `reverse`
68
+
69
+ Returns a new Segment with start and end points swapped.
70
+
71
+ ```ruby
72
+ rev = seg.reverse
73
+ rev.start_point == seg.end_point # => true
74
+ ```
75
+
76
+ ### `interpolate(fraction)`
77
+
78
+ Returns the LLA coordinate at a given fraction (0.0 to 1.0) along the segment.
79
+
80
+ ```ruby
81
+ seg.interpolate(0.0) # => start_point
82
+ seg.interpolate(0.5) # => midpoint
83
+ seg.interpolate(1.0) # => end_point
84
+ seg.interpolate(0.25) # => quarter-way along
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Projection
90
+
91
+ ### `project(point)`
92
+
93
+ Projects a point onto the segment, returning the closest point on the segment and the distance in meters.
94
+
95
+ ```ruby
96
+ foot, distance_m = seg.project(target_point)
97
+ ```
98
+
99
+ - If the perpendicular foot falls within the segment, returns the foot and the perpendicular distance.
100
+ - If the foot falls before the start, returns `start_point`.
101
+ - If the foot falls past the end, returns `end_point`.
102
+ - Handles zero-length segments and target-at-endpoint edge cases.
103
+
104
+ This is the core geometric operation used by `Path#closest_coordinate_to`, `Path#closest_points_to`, and `Path#at_distance`.
105
+
106
+ ---
107
+
108
+ ## Membership
109
+
110
+ | Method | Description |
111
+ |--------|-------------|
112
+ | `includes?(point)` | True if the point is a vertex (start or end point) |
113
+ | `contains?(point, tolerance: 10.0)` | True if the point lies on the segment within tolerance (meters) |
114
+ | `excludes?(point, tolerance: 10.0)` | Opposite of `contains?` |
115
+
116
+ `includes?` checks vertices only. `contains?` checks whether a point lies anywhere along the line between the two endpoints using a bearing comparison with a tolerance derived from the segment length.
117
+
118
+ ```ruby
119
+ seg.includes?(seg.start_point) # => true
120
+ seg.includes?(seg.midpoint) # => false
121
+
122
+ seg.contains?(seg.midpoint) # => true
123
+ seg.contains?(far_away_point) # => false
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Intersection
129
+
130
+ ### `intersects?(other_segment)`
131
+
132
+ Tests whether two segments cross each other using cross-product orientation tests on a flat lat/lng approximation. Handles both proper intersections and collinear overlap.
133
+
134
+ ```ruby
135
+ seg1 = Geodetic::Segment.new(a, b)
136
+ seg2 = Geodetic::Segment.new(c, d)
137
+
138
+ seg1.intersects?(seg2) # => true/false
139
+ ```
140
+
141
+ Used internally by `Path#intersects?` and `Path#to_polygon` for self-intersection validation.
142
+
143
+ ---
144
+
145
+ ## Conversion
146
+
147
+ | Method | Returns | Description |
148
+ |----------|---------|-------------|
149
+ | `to_path`| Path | A two-point Path from start to end |
150
+ | `to_a` | Array | `[start_point, end_point]` |
151
+
152
+ ```ruby
153
+ seg.to_path # => #<Geodetic::Path size=2 ...>
154
+ seg.to_a # => [start_point, end_point]
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Equality and Display
160
+
161
+ ```ruby
162
+ seg1 == seg2 # true if same start and end points
163
+
164
+ seg.to_s # => "Segment(40.748400, ... -> 40.758000, ...)"
165
+ seg.inspect # => "#<Geodetic::Segment start=... end=... length=1067.45 m>"
166
+ ```
167
+
168
+ Two segments are equal if they have the same start and end points. Direction matters: `Segment.new(a, b) != Segment.new(b, a)`.
169
+
170
+ ---
171
+
172
+ ## Relationship to Path and Polygon
173
+
174
+ `Path#segments` returns an array of Segment objects:
175
+
176
+ ```ruby
177
+ route = Path.new(coordinates: [a, b, c, d])
178
+ route.segments # => [Segment(a→b), Segment(b→c), Segment(c→d)]
179
+ ```
180
+
181
+ Polygon edges are implicit segments formed by consecutive boundary points. Segment's `project`, `intersects?`, and `contains?` methods power the geometric operations in both Path and Polygon.
@@ -253,13 +253,13 @@ def demo_coordinate_systems
253
253
  puts " Lat/Lng error is from MGRS 1-meter grid precision truncation."
254
254
  puts
255
255
 
256
- # ========== Rectangle Area ==========
257
- puts "RECTANGLE AREA"
256
+ # ========== BoundingBox Area ==========
257
+ puts "BOUNDING BOX AREA"
258
258
  puts "-" * 50
259
259
 
260
260
  nw = LLA.new(lat: 47.65, lng: -122.40)
261
261
  se = LLA.new(lat: 47.60, lng: -122.30)
262
- rect = Areas::Rectangle.new(nw: nw, se: se)
262
+ rect = Areas::BoundingBox.new(nw: nw, se: se)
263
263
  puts " NW: (#{rect.nw.lat}, #{rect.nw.lng})"
264
264
  puts " SE: (#{rect.se.lat}, #{rect.se.lng})"
265
265
  puts " NE: (#{rect.ne.lat}, #{rect.ne.lng})"
@@ -268,10 +268,10 @@ def demo_coordinate_systems
268
268
  puts " Space Needle inside? #{rect.includes?(lla_coord)}"
269
269
  puts " London inside? #{rect.includes?(london_lla)}"
270
270
 
271
- # Rectangle from non-LLA coordinates
271
+ # BoundingBox from non-LLA coordinates
272
272
  nw_wm = WebMerc.from_lla(nw)
273
273
  se_wm = WebMerc.from_lla(se)
274
- rect_wm = Areas::Rectangle.new(nw: nw_wm, se: se_wm)
274
+ rect_wm = Areas::BoundingBox.new(nw: nw_wm, se: se_wm)
275
275
  puts " From WebMercator: NW=(#{rect_wm.nw.lat.round(4)}, #{rect_wm.nw.lng.round(4)})"
276
276
  puts
277
277
 
@@ -292,7 +292,7 @@ def demo_coordinate_systems
292
292
  puts "Geohash-36 (GH36)"
293
293
  puts "Geoid Height Support"
294
294
  puts
295
- puts "Areas: Circle, Polygon, Rectangle"
295
+ puts "Areas: Circle, Polygon, BoundingBox"
296
296
  puts
297
297
  puts "All coordinate systems support complete bidirectional conversions!"
298
298
  puts "Total coordinate systems implemented: 13"
@@ -59,10 +59,8 @@ puts
59
59
 
60
60
  puts "--- 3. Segment Analysis ---"
61
61
 
62
- route.segments.each_with_index do |(a, b), i|
63
- dist = a.distance_to(b)
64
- bearing = a.bearing_to(b)
65
- puts " Segment #{i + 1}: #{dist.to_km} #{bearing.to_compass(points: 8)} (#{bearing.to_s(1)})"
62
+ route.segments.each_with_index do |seg, i|
63
+ puts " Segment #{i + 1}: #{seg.length.to_km} #{seg.bearing.to_compass(points: 8)} (#{seg.bearing.to_s(1)})"
66
64
  end
67
65
 
68
66
  puts <<~TOTAL
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Demonstration of Segment and polygon subclasses (Triangle, Rectangle,
4
+ # Pentagon, Hexagon, Octagon). Shows construction, properties, predicates,
5
+ # containment testing, segment access, bounding boxes, and Feature integration.
6
+
7
+ require_relative "../lib/geodetic"
8
+
9
+ include Geodetic
10
+ LLA = Coordinate::LLA
11
+
12
+ # ── Notable locations around the National Mall, Washington DC ─────
13
+
14
+ lincoln = LLA.new(lat: 38.8893, lng: -77.0502, alt: 0)
15
+ washington = LLA.new(lat: 38.8895, lng: -77.0353, alt: 0)
16
+ capitol = LLA.new(lat: 38.8899, lng: -77.0091, alt: 0)
17
+ white_house = LLA.new(lat: 38.8977, lng: -77.0365, alt: 0)
18
+ jefferson = LLA.new(lat: 38.8814, lng: -77.0365, alt: 0)
19
+ pentagon = LLA.new(lat: 38.8719, lng: -77.0563, alt: 0)
20
+
21
+ puts "=== Segments and Shapes Demo ==="
22
+ puts
23
+
24
+ # ── 1. Segment Basics ────────────────────────────────────────────
25
+
26
+ puts "--- 1. Segment ---"
27
+ puts
28
+
29
+ seg = Segment.new(lincoln, washington)
30
+
31
+ puts <<~SEG
32
+ Lincoln Memorial -> Washington Monument
33
+ Length: #{seg.length.to_s} (#{seg.length_meters.round(1)} m)
34
+ Distance: #{seg.distance.to_s} (alias for length)
35
+ Bearing: #{seg.bearing.to_s} (#{seg.bearing.to_compass})
36
+ Midpoint: #{seg.midpoint.to_s(4)}
37
+ Centroid: #{seg.centroid.to_s(4)} (alias for midpoint)
38
+ SEG
39
+ puts
40
+
41
+ # ── 2. Segment Operations ───────────────────────────────────────
42
+
43
+ puts "--- 2. Segment Operations ---"
44
+ puts
45
+
46
+ quarter = seg.interpolate(0.25)
47
+ puts " Quarter-way point: #{quarter.to_s(4)}"
48
+ puts " Reverse bearing: #{seg.reverse.bearing.to_s}"
49
+ puts
50
+
51
+ foot, dist_m = seg.project(white_house)
52
+ puts " Project White House onto Lincoln-Washington segment:"
53
+ puts " Foot: #{foot.to_s(4)}"
54
+ puts " Distance: #{dist_m.round(1)} m from segment"
55
+ puts
56
+
57
+ puts " White House is a vertex? #{seg.includes?(white_house)}"
58
+ puts " Lincoln is a vertex? #{seg.includes?(lincoln)}"
59
+ puts " Midpoint on segment? #{seg.contains?(seg.midpoint)}"
60
+ puts " White House on segment? #{seg.contains?(white_house)}"
61
+ puts
62
+
63
+ # ── 3. Segment Intersection ─────────────────────────────────────
64
+
65
+ puts "--- 3. Segment Intersection ---"
66
+ puts
67
+
68
+ mall_ns = Segment.new(white_house, jefferson)
69
+ mall_ew = Segment.new(lincoln, capitol)
70
+
71
+ puts " N-S segment: White House -> Jefferson Memorial"
72
+ puts " E-W segment: Lincoln Memorial -> Capitol"
73
+ puts " Intersects? #{mall_ew.intersects?(mall_ns)}"
74
+ puts
75
+
76
+ parallel = Segment.new(
77
+ LLA.new(lat: 38.892, lng: -77.050, alt: 0),
78
+ LLA.new(lat: 38.892, lng: -77.010, alt: 0)
79
+ )
80
+ puts " Parallel segment (north of Mall):"
81
+ puts " Intersects E-W? #{mall_ew.intersects?(parallel)}"
82
+ puts
83
+
84
+ # ── 4. Triangle ──────────────────────────────────────────────────
85
+
86
+ puts "--- 4. Triangle ---"
87
+ puts
88
+
89
+ puts " Isosceles (center + width + height):"
90
+ tri = Areas::Triangle.new(center: washington, width: 400, height: 600, bearing: 0)
91
+ puts " Sides: #{tri.sides}"
92
+ puts " Width: #{tri.width} m"
93
+ puts " Height: #{tri.height} m"
94
+ puts " Bearing: #{tri.bearing}\u00B0"
95
+ puts " Equilateral? #{tri.equilateral?}"
96
+ puts " Isosceles? #{tri.isosceles?}"
97
+ puts " Scalene? #{tri.scalene?}"
98
+ puts " Side lengths: #{tri.side_lengths.map { |l| "#{l.round(1)} m" }.join(', ')}"
99
+ puts " Center inside? #{tri.includes?(washington)}"
100
+ puts
101
+
102
+ puts " Equilateral by side (500m):"
103
+ eq_tri = Areas::Triangle.new(center: washington, side: 500, bearing: 30)
104
+ puts " Equilateral? #{eq_tri.equilateral?}"
105
+ puts " Side lengths: #{eq_tri.side_lengths.map { |l| "#{l.round(1)} m" }.join(', ')}"
106
+ puts
107
+
108
+ puts " Equilateral by radius (300m):"
109
+ r_tri = Areas::Triangle.new(center: washington, radius: 300, bearing: 0)
110
+ puts " Equilateral? #{r_tri.equilateral?}"
111
+ puts " Width: #{r_tri.width.round(1)} m"
112
+ puts " Height: #{r_tri.height.round(1)} m"
113
+ puts
114
+
115
+ puts " Arbitrary 3 vertices:"
116
+ arb_tri = Areas::Triangle.new(vertices: [lincoln, washington, white_house])
117
+ puts " Scalene? #{arb_tri.scalene?}"
118
+ puts " Side lengths: #{arb_tri.side_lengths.map { |l| "#{l.round(1)} m" }.join(', ')}"
119
+ puts " Washington inside? #{arb_tri.includes?(washington)}"
120
+ puts
121
+
122
+ # ── 5. Rectangle ────────────────────────────────────────────────
123
+
124
+ puts "--- 5. Rectangle ---"
125
+ puts
126
+
127
+ puts " From a Segment (Lincoln -> Washington) + width:"
128
+ centerline = Segment.new(lincoln, washington)
129
+ rect = Areas::Rectangle.new(segment: centerline, width: 200)
130
+
131
+ puts " Width: #{rect.width} m"
132
+ puts " Height: #{rect.height.round(1)} m"
133
+ puts " Bearing: #{rect.bearing.round(1)}\u00B0"
134
+ puts " Center: #{rect.center.to_s(4)}"
135
+ puts " Square? #{rect.square?}"
136
+ puts " Sides: #{rect.sides}"
137
+ puts " Corners: #{rect.corners.size}"
138
+ puts " Centerline start: #{rect.centerline.start_point.to_s(4)}"
139
+ puts " Centerline end: #{rect.centerline.end_point.to_s(4)}"
140
+ puts
141
+
142
+ puts " From an array of two points + width:"
143
+ rect2 = Areas::Rectangle.new(segment: [white_house, jefferson], width: 300)
144
+ puts " Width: #{rect2.width} m"
145
+ puts " Height: #{rect2.height.round(1)} m"
146
+ puts " Bearing: #{rect2.bearing.round(1)}\u00B0"
147
+ puts " Square? #{rect2.square?}"
148
+ puts
149
+
150
+ puts " Square rectangle (width = segment distance):"
151
+ short_seg = Segment.new(lincoln, seg.interpolate(0.1))
152
+ sq = Areas::Rectangle.new(segment: short_seg, width: short_seg.distance)
153
+ puts " Width: #{sq.width.round(1)} m"
154
+ puts " Height: #{sq.height.round(1)} m"
155
+ puts " Square? #{sq.square?}"
156
+ puts
157
+
158
+ # ── 6. Pentagon, Hexagon, Octagon ────────────────────────────────
159
+
160
+ puts "--- 6. Regular Polygons ---"
161
+ puts
162
+
163
+ [
164
+ ["Pentagon", Areas::Pentagon.new(center: washington, radius: 500, bearing: 0)],
165
+ ["Hexagon", Areas::Hexagon.new(center: washington, radius: 500, bearing: 0)],
166
+ ["Octagon", Areas::Octagon.new(center: washington, radius: 500, bearing: 0)]
167
+ ].each do |name, shape|
168
+ puts " #{name}:"
169
+ puts " Sides: #{shape.sides}"
170
+ puts " Radius: #{shape.radius} m"
171
+ puts " Vertices: #{shape.boundary.size - 1} (boundary has #{shape.boundary.size} points, closed)"
172
+ puts " Centroid: #{shape.centroid.to_s(4)}"
173
+ puts " Center inside? #{shape.includes?(washington)}"
174
+ puts " Pentagon inside? #{shape.includes?(pentagon)}"
175
+ puts
176
+ end
177
+
178
+ # ── 7. Polygon Segments ─────────────────────────────────────────
179
+
180
+ puts "--- 7. Polygon Segments ---"
181
+ puts
182
+
183
+ puts " Rectangle segments (edges/border):"
184
+ rect.segments.each_with_index do |s, i|
185
+ puts " Edge #{i + 1}: #{s.length_meters.round(1)} m, bearing #{s.bearing.to_s}"
186
+ end
187
+ puts " Connected? #{rect.segments.each_cons(2).all? { |a, b| a.end_point == b.start_point }}"
188
+ puts
189
+
190
+ puts " Triangle segments:"
191
+ tri.segments.each_with_index do |s, i|
192
+ puts " Side #{i + 1}: #{s.length_meters.round(1)} m"
193
+ end
194
+ puts
195
+
196
+ # ── 8. Bounding Boxes ───────────────────────────────────────────
197
+
198
+ puts "--- 8. Bounding Boxes ---"
199
+ puts
200
+
201
+ [
202
+ ["Triangle", tri],
203
+ ["Rectangle", rect]
204
+ ].each do |name, shape|
205
+ bbox = shape.to_bounding_box
206
+ puts " #{name}:"
207
+ puts " NW: #{bbox.nw.to_s(4)}"
208
+ puts " SE: #{bbox.se.to_s(4)}"
209
+
210
+ shape_center = shape.respond_to?(:center) ? shape.center : shape.centroid
211
+ puts " Center inside bbox? #{bbox.includes?(shape_center)}"
212
+ puts
213
+ end
214
+
215
+ # ── 9. Containment ──────────────────────────────────────────────
216
+
217
+ puts "--- 9. Containment Testing ---"
218
+ puts
219
+
220
+ hex = Areas::Hexagon.new(center: washington, radius: 500)
221
+
222
+ test_points = {
223
+ "Washington Monument" => washington,
224
+ "Lincoln Memorial" => lincoln,
225
+ "White House" => white_house,
226
+ "Pentagon" => pentagon
227
+ }
228
+
229
+ puts " Hexagon (500m radius around Washington Monument):"
230
+ test_points.each do |name, point|
231
+ dist = washington.distance_to(point)
232
+ puts " #{name.ljust(22)} inside? #{hex.includes?(point).to_s.ljust(5)} (#{dist.to_s} away)"
233
+ end
234
+ puts
235
+
236
+ # ── 10. Feature Integration ─────────────────────────────────────
237
+
238
+ puts "--- 10. Feature Integration ---"
239
+ puts
240
+
241
+ seg_feature = Feature.new(label: "National Mall Axis", geometry: centerline)
242
+ tri_feature = Feature.new(label: "Warning Zone", geometry: tri, metadata: { type: "restricted" })
243
+ rect_feature = Feature.new(label: "Search Area", geometry: rect, metadata: { priority: "high" })
244
+
245
+ puts " Segment feature: #{seg_feature}"
246
+ puts " Triangle feature: #{tri_feature}"
247
+ puts " Rectangle feature: #{rect_feature}"
248
+ puts
249
+
250
+ puts " Distance from each feature to the Pentagon:"
251
+ [seg_feature, tri_feature, rect_feature].each do |f|
252
+ dist = f.distance_to(pentagon)
253
+ bearing = f.bearing_to(pentagon)
254
+ puts " #{f.label.ljust(20)} #{dist.to_s.rjust(12)} bearing #{bearing.to_s} (#{bearing.to_compass})"
255
+ end
256
+ puts
257
+
258
+ puts "=== Done ==="
data/examples/README.md CHANGED
@@ -76,10 +76,28 @@ Demonstrates the `Geodetic::Path` class with a walking route through Manhattan.
76
76
  - **Equality** comparing paths by coordinates and order
77
77
  - **Subpath extraction** with `between` and **splitting** with `split_at`
78
78
  - **Interpolation** finding coordinates at a given distance along the path with `at_distance`
79
- - **Bounding box** with `bounds` returning an `Areas::Rectangle`
79
+ - **Bounding box** with `bounds` returning an `Areas::BoundingBox`
80
80
  - **Polygon conversion** with `to_polygon` (validates no self-intersection)
81
81
  - **Path intersection** detection with `intersects?`
82
82
  - **Path-to-Path closest points** finding the nearest pair between two paths
83
83
  - **Path-to-Area closest points** for Circle and Polygon areas
84
84
  - **Reverse** to create the return route
85
85
  - **Feature integration** wrapping a Path with label and metadata
86
+
87
+ ## 07 - Segments and Shapes
88
+
89
+ Demonstrates `Geodetic::Segment` and the polygon subclasses (`Triangle`, `Rectangle`, `Pentagon`, `Hexagon`, `Octagon`) using landmarks around the National Mall in Washington DC. Covers:
90
+
91
+ - **Segment properties** including `length`/`distance`, `bearing`, `midpoint`/`centroid`
92
+ - **Segment operations** with `interpolate`, `project`, `reverse`
93
+ - **Membership testing** with `includes?` (vertex check) and `contains?` (on-segment check)
94
+ - **Intersection detection** between crossing and parallel segments
95
+ - **Triangle construction** in four modes: isosceles, equilateral by side, equilateral by radius, arbitrary vertices
96
+ - **Triangle predicates** with `equilateral?`, `isosceles?`, `scalene?`, and `side_lengths`
97
+ - **Rectangle construction** from a `Segment` centerline (or two-point array) plus width
98
+ - **Rectangle properties** with `centerline`, `center`, `height`, `bearing`, `square?`
99
+ - **Regular polygons** (Pentagon, Hexagon, Octagon) from center and radius
100
+ - **Polygon segments** accessing edges as `Segment` objects
101
+ - **Bounding boxes** via `to_bounding_box` on any polygon subclass
102
+ - **Containment testing** with `includes?` across different shapes
103
+ - **Feature integration** wrapping Segment and area geometries with labels and metadata
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../coordinate/lla'
4
+
5
+ module Geodetic
6
+ module Areas
7
+ class BoundingBox
8
+ attr_reader :nw, :se, :centroid
9
+
10
+ # Define an axis-aligned bounding box by its NW and SE corners.
11
+ # Accepts any coordinate that responds to to_lla.
12
+ #
13
+ # BoundingBox.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?(Coordinate::LLA) ? nw : nw.to_lla
19
+ @se = se.is_a?(Coordinate::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 = Coordinate::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
+ Coordinate::LLA.new(lat: @nw.lat, lng: @se.lng, alt: 0.0)
33
+ end
34
+
35
+ def sw
36
+ Coordinate::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
+
55
+ end
56
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "regular_polygon"
4
+
5
+ module Geodetic
6
+ module Areas
7
+ class Hexagon < RegularPolygon
8
+ SIDES = 6
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "regular_polygon"
4
+
5
+ module Geodetic
6
+ module Areas
7
+ class Octagon < RegularPolygon
8
+ SIDES = 8
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "regular_polygon"
4
+
5
+ module Geodetic
6
+ module Areas
7
+ class Pentagon < RegularPolygon
8
+ SIDES = 5
9
+ end
10
+ end
11
+ end