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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -2
- data/README.md +45 -8
- data/docs/coordinate-systems/gars.md +2 -2
- data/docs/coordinate-systems/georef.md +2 -2
- data/docs/coordinate-systems/gh.md +2 -2
- data/docs/coordinate-systems/gh36.md +2 -2
- data/docs/coordinate-systems/h3.md +2 -2
- data/docs/coordinate-systems/ham.md +2 -2
- data/docs/coordinate-systems/olc.md +2 -2
- data/docs/index.md +4 -2
- data/docs/reference/areas.md +140 -14
- data/docs/reference/feature.md +2 -2
- data/docs/reference/path.md +3 -3
- data/docs/reference/segment.md +181 -0
- data/examples/02_all_coordinate_systems.rb +6 -6
- data/examples/06_path_operations.rb +2 -4
- data/examples/07_segments_and_shapes.rb +258 -0
- data/examples/README.md +19 -1
- data/lib/geodetic/areas/bounding_box.rb +56 -0
- data/lib/geodetic/areas/hexagon.rb +11 -0
- data/lib/geodetic/areas/octagon.rb +11 -0
- data/lib/geodetic/areas/pentagon.rb +11 -0
- data/lib/geodetic/areas/polygon.rb +54 -14
- data/lib/geodetic/areas/rectangle.rb +85 -35
- data/lib/geodetic/areas/regular_polygon.rb +59 -0
- data/lib/geodetic/areas/triangle.rb +180 -0
- data/lib/geodetic/areas.rb +6 -0
- data/lib/geodetic/coordinate/gh36.rb +1 -1
- data/lib/geodetic/coordinate/h3.rb +1 -1
- data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
- data/lib/geodetic/path.rb +26 -153
- data/lib/geodetic/segment.rb +172 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +1 -0
- data/mkdocs.yml +1 -0
- metadata +10 -1
|
@@ -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
|
-
# ==========
|
|
257
|
-
puts "
|
|
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::
|
|
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
|
-
#
|
|
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::
|
|
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,
|
|
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 |
|
|
63
|
-
|
|
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::
|
|
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
|