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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -2
- data/README.md +121 -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/arithmetic.md +368 -0
- data/docs/reference/feature.md +2 -2
- data/docs/reference/path.md +3 -3
- data/docs/reference/segment.md +181 -0
- data/docs/reference/vector.md +256 -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/08_geodetic_arithmetic.rb +393 -0
- data/examples/README.md +35 -1
- data/lib/geodetic/areas/bounding_box.rb +56 -0
- data/lib/geodetic/areas/circle.rb +8 -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 +64 -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/coordinate.rb +26 -1
- data/lib/geodetic/distance.rb +5 -1
- data/lib/geodetic/path.rb +85 -153
- data/lib/geodetic/segment.rb +193 -0
- data/lib/geodetic/vector.rb +242 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +2 -0
- data/mkdocs.yml +1 -0
- metadata +14 -1
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# Vector Reference
|
|
2
|
+
|
|
3
|
+
`Geodetic::Vector` represents a geodetic displacement — a combination of distance (magnitude) and bearing (direction). It is the fundamental type for expressing "how far and which way" between two points on Earth.
|
|
4
|
+
|
|
5
|
+
A Vector solves the **Vincenty direct problem**: given an origin point, a bearing, and a distance, compute the destination point on the WGS84 ellipsoid.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Constructor
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
# From numeric values (meters and degrees)
|
|
13
|
+
v = Geodetic::Vector.new(distance: 10_000, bearing: 90.0)
|
|
14
|
+
|
|
15
|
+
# From Distance and Bearing objects
|
|
16
|
+
v = Geodetic::Vector.new(
|
|
17
|
+
distance: Geodetic::Distance.km(10),
|
|
18
|
+
bearing: Geodetic::Bearing.new(90.0)
|
|
19
|
+
)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Numeric arguments are automatically coerced: `distance` wraps into `Distance.new(value)` and `bearing` wraps into `Bearing.new(value)`.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Attributes
|
|
27
|
+
|
|
28
|
+
| Attribute | Type | Description |
|
|
29
|
+
|------------|----------|-------------|
|
|
30
|
+
| `distance` | Distance | The magnitude of the vector |
|
|
31
|
+
| `bearing` | Bearing | The direction of the vector (0-360 degrees from north) |
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Components
|
|
36
|
+
|
|
37
|
+
A Vector decomposes into north/east components in meters, treating bearing as an angle measured clockwise from north.
|
|
38
|
+
|
|
39
|
+
| Method | Returns | Description |
|
|
40
|
+
|--------|---------|-------------|
|
|
41
|
+
| `north` | Float | North component: `distance * cos(bearing)` |
|
|
42
|
+
| `east` | Float | East component: `distance * sin(bearing)` |
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
v = Geodetic::Vector.new(distance: 1000, bearing: 45.0)
|
|
46
|
+
v.north # => 707.107 (meters north)
|
|
47
|
+
v.east # => 707.107 (meters east)
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
A due-north vector has a full north component and zero east component. A due-east vector has zero north and full east.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Factory Methods
|
|
55
|
+
|
|
56
|
+
### `Vector.from_components(north:, east:)`
|
|
57
|
+
|
|
58
|
+
Reconstruct a Vector from north/east components in meters.
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
v = Geodetic::Vector.from_components(north: 1000, east: 0)
|
|
62
|
+
v.bearing.degrees # => 0.0 (due north)
|
|
63
|
+
v.distance.meters # => 1000.0
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Near-zero magnitudes (< 1e-9 meters) snap to a clean zero vector to avoid floating-point artifacts.
|
|
67
|
+
|
|
68
|
+
### `Vector.from_segment(segment)`
|
|
69
|
+
|
|
70
|
+
Create a Vector from an existing Segment's length and bearing.
|
|
71
|
+
|
|
72
|
+
```ruby
|
|
73
|
+
seg = Geodetic::Segment.new(seattle, portland)
|
|
74
|
+
v = Geodetic::Vector.from_segment(seg)
|
|
75
|
+
v.distance.meters # => same as seg.length_meters
|
|
76
|
+
v.bearing.degrees # => same as seg.bearing.degrees
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Also available as `segment.to_vector`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Vincenty Direct
|
|
84
|
+
|
|
85
|
+
### `destination_from(origin)`
|
|
86
|
+
|
|
87
|
+
Given an origin coordinate, compute the destination point by traveling the vector's distance along the vector's bearing. Uses the full Vincenty direct formula on the WGS84 ellipsoid.
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
v = Geodetic::Vector.new(distance: 100_000, bearing: 45.0)
|
|
91
|
+
dest = v.destination_from(seattle) # => LLA 100km northeast of Seattle
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Accepts any coordinate type — non-LLA inputs are converted automatically. A zero-distance vector returns the origin unchanged.
|
|
95
|
+
|
|
96
|
+
**Round-trip accuracy:**
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
v = Geodetic::Vector.new(distance: 50_000, bearing: 135.0)
|
|
100
|
+
dest = v.destination_from(origin)
|
|
101
|
+
origin.distance_to(dest).meters # => 50000.0 (within ~1 meter)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Arithmetic
|
|
107
|
+
|
|
108
|
+
Vector arithmetic uses north/east component decomposition. The result is always a new Vector.
|
|
109
|
+
|
|
110
|
+
### Vector + Vector
|
|
111
|
+
|
|
112
|
+
Component-wise addition. Combines two displacements into a single resultant vector.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
north = Geodetic::Vector.new(distance: 1000, bearing: 0.0)
|
|
116
|
+
east = Geodetic::Vector.new(distance: 1000, bearing: 90.0)
|
|
117
|
+
result = north + east
|
|
118
|
+
result.bearing.degrees # => 45.0
|
|
119
|
+
result.distance.meters # => 1414.21
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Vector - Vector
|
|
123
|
+
|
|
124
|
+
Component-wise subtraction.
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
v1 = Geodetic::Vector.new(distance: 1000, bearing: 45.0)
|
|
128
|
+
result = v1 - v1
|
|
129
|
+
result.zero? # => true
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Vector * Scalar
|
|
133
|
+
|
|
134
|
+
Scales the distance. Negative scalars reverse the bearing.
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
v = Geodetic::Vector.new(distance: 1000, bearing: 45.0)
|
|
138
|
+
(v * 3).distance.meters # => 3000.0, bearing 45
|
|
139
|
+
(v * -1).bearing.degrees # => 225.0 (reversed)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`Scalar * Vector` also works via `coerce`:
|
|
143
|
+
|
|
144
|
+
```ruby
|
|
145
|
+
2 * v # => Vector with double the distance
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Vector / Scalar
|
|
149
|
+
|
|
150
|
+
Scales the distance down. Raises `ZeroDivisionError` for zero. Negative divisors reverse the bearing.
|
|
151
|
+
|
|
152
|
+
```ruby
|
|
153
|
+
v = Geodetic::Vector.new(distance: 3000, bearing: 90.0)
|
|
154
|
+
(v / 3).distance.meters # => 1000.0
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Unary Minus
|
|
158
|
+
|
|
159
|
+
Same distance, reversed bearing.
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
v = Geodetic::Vector.new(distance: 1000, bearing: 45.0)
|
|
163
|
+
(-v).bearing.degrees # => 225.0
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Identity: V + (-V)
|
|
167
|
+
|
|
168
|
+
Opposite vectors cancel to zero:
|
|
169
|
+
|
|
170
|
+
```ruby
|
|
171
|
+
v = Geodetic::Vector.new(distance: 1000, bearing: 45.0)
|
|
172
|
+
result = v + (-v)
|
|
173
|
+
result.zero? # => true
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## Products
|
|
179
|
+
|
|
180
|
+
### `dot(other)`
|
|
181
|
+
|
|
182
|
+
Scalar dot product using north/east components. Returns a Float.
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
north = Geodetic::Vector.new(distance: 100, bearing: 0.0)
|
|
186
|
+
east = Geodetic::Vector.new(distance: 100, bearing: 90.0)
|
|
187
|
+
|
|
188
|
+
north.dot(north) # => 10000.0 (parallel)
|
|
189
|
+
north.dot(east) # => 0.0 (perpendicular)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `cross(other)`
|
|
193
|
+
|
|
194
|
+
2D cross product (scalar). Returns a Float. Positive means `other` is to the left of `self`.
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
north.cross(east) # => 10000.0
|
|
198
|
+
east.cross(north) # => -10000.0
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `angle_between(other)`
|
|
202
|
+
|
|
203
|
+
Returns a Bearing representing the angular difference.
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
north = Geodetic::Vector.new(distance: 100, bearing: 0.0)
|
|
207
|
+
east = Geodetic::Vector.new(distance: 100, bearing: 90.0)
|
|
208
|
+
north.angle_between(east).degrees # => 90.0
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Properties
|
|
214
|
+
|
|
215
|
+
| Method | Returns | Description |
|
|
216
|
+
|-------------|----------|-------------|
|
|
217
|
+
| `magnitude` | Float | Distance in meters |
|
|
218
|
+
| `zero?` | Boolean | True if distance is zero |
|
|
219
|
+
| `normalize` | Vector | Unit vector (1 meter) in the same bearing |
|
|
220
|
+
| `reverse` | Vector | Same distance, opposite bearing (alias for `-self`) |
|
|
221
|
+
| `inverse` | Vector | Alias for `reverse` |
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
v = Geodetic::Vector.new(distance: 5000, bearing: 135.0)
|
|
225
|
+
v.magnitude # => 5000.0
|
|
226
|
+
v.normalize.magnitude # => 1.0
|
|
227
|
+
v.reverse.bearing.degrees # => 315.0
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Comparison
|
|
233
|
+
|
|
234
|
+
Vectors include `Comparable`, ordered by distance (magnitude). Two vectors are equal (`==`) only if both distance and bearing match.
|
|
235
|
+
|
|
236
|
+
```ruby
|
|
237
|
+
short = Geodetic::Vector.new(distance: 100, bearing: 0.0)
|
|
238
|
+
long = Geodetic::Vector.new(distance: 200, bearing: 0.0)
|
|
239
|
+
short < long # => true
|
|
240
|
+
|
|
241
|
+
# Same magnitude, different bearing
|
|
242
|
+
a = Geodetic::Vector.new(distance: 100, bearing: 0.0)
|
|
243
|
+
b = Geodetic::Vector.new(distance: 100, bearing: 90.0)
|
|
244
|
+
(a <=> b) == 0 # => true (same magnitude)
|
|
245
|
+
a == b # => false (different bearing)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Display
|
|
251
|
+
|
|
252
|
+
```ruby
|
|
253
|
+
v = Geodetic::Vector.new(distance: 1000, bearing: 90.0)
|
|
254
|
+
v.to_s # => "Vector(1000.00 m, 90.0000°)"
|
|
255
|
+
v.inspect # => "#<Geodetic::Vector distance=#<Geodetic::Distance ...> bearing=#<Geodetic::Bearing ...>>"
|
|
256
|
+
```
|
|
@@ -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 ==="
|