geodetic 0.3.1 → 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 +66 -0
- data/README.md +87 -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 +7 -3
- data/docs/reference/areas.md +140 -14
- data/docs/reference/feature.md +4 -3
- data/docs/reference/path.md +269 -0
- data/docs/reference/segment.md +181 -0
- data/examples/02_all_coordinate_systems.rb +6 -6
- data/examples/06_path_operations.rb +366 -0
- data/examples/07_segments_and_shapes.rb +258 -0
- data/examples/README.md +41 -0
- 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/feature.rb +10 -2
- data/lib/geodetic/path.rb +472 -0
- data/lib/geodetic/segment.rb +172 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +2 -0
- data/mkdocs.yml +2 -0
- metadata +13 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
# Demonstration of the Path class
|
|
4
|
+
# Shows construction, navigation, mutation, closest-approach
|
|
5
|
+
# calculations, containment testing, Enumerable, subpaths,
|
|
6
|
+
# interpolation, bounding boxes, intersection, path-to-path
|
|
7
|
+
# and path-to-area operations, and Feature integration.
|
|
8
|
+
|
|
9
|
+
require_relative "../lib/geodetic"
|
|
10
|
+
|
|
11
|
+
include Geodetic
|
|
12
|
+
LLA = Coordinate::LLA
|
|
13
|
+
Distance = Geodetic::Distance
|
|
14
|
+
|
|
15
|
+
# ── Define waypoints along a hiking route through Manhattan ────
|
|
16
|
+
|
|
17
|
+
battery_park = LLA.new(lat: 40.7033, lng: -74.0170, alt: 0)
|
|
18
|
+
wall_street = LLA.new(lat: 40.7074, lng: -74.0113, alt: 0)
|
|
19
|
+
brooklyn_bridge = LLA.new(lat: 40.7061, lng: -73.9969, alt: 0)
|
|
20
|
+
city_hall = LLA.new(lat: 40.7128, lng: -74.0060, alt: 0)
|
|
21
|
+
soho = LLA.new(lat: 40.7233, lng: -73.9985, alt: 0)
|
|
22
|
+
union_square = LLA.new(lat: 40.7359, lng: -73.9911, alt: 0)
|
|
23
|
+
empire_state = LLA.new(lat: 40.7484, lng: -73.9857, alt: 0)
|
|
24
|
+
times_square = LLA.new(lat: 40.7580, lng: -73.9855, alt: 0)
|
|
25
|
+
central_park = LLA.new(lat: 40.7829, lng: -73.9654, alt: 0)
|
|
26
|
+
|
|
27
|
+
# ── 1. Construction ────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
puts "=== Path Operations Demo ==="
|
|
30
|
+
puts
|
|
31
|
+
puts "--- 1. Construction ---"
|
|
32
|
+
|
|
33
|
+
# From an array of coordinates
|
|
34
|
+
route = Path.new(coordinates: [
|
|
35
|
+
battery_park, wall_street, brooklyn_bridge,
|
|
36
|
+
city_hall, soho, union_square, empire_state
|
|
37
|
+
])
|
|
38
|
+
|
|
39
|
+
puts <<~CONSTRUCTION
|
|
40
|
+
Route has #{route.size} waypoints
|
|
41
|
+
Start: #{route.first.to_s(4)}
|
|
42
|
+
End: #{route.last.to_s(4)}
|
|
43
|
+
CONSTRUCTION
|
|
44
|
+
puts
|
|
45
|
+
|
|
46
|
+
# ── 2. Navigation ─────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
puts "--- 2. Navigation ---"
|
|
49
|
+
|
|
50
|
+
puts <<~NAV
|
|
51
|
+
After Wall Street: #{route.next(wall_street)&.to_s(4) || 'nil'}
|
|
52
|
+
Before SoHo: #{route.prev(soho)&.to_s(4) || 'nil'}
|
|
53
|
+
Start has no prev: #{route.prev(battery_park).inspect}
|
|
54
|
+
End has no next: #{route.next(empire_state).inspect}
|
|
55
|
+
NAV
|
|
56
|
+
puts
|
|
57
|
+
|
|
58
|
+
# ── 3. Segments, distances, and bearings ──────────────────────
|
|
59
|
+
|
|
60
|
+
puts "--- 3. Segment Analysis ---"
|
|
61
|
+
|
|
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)})"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
puts <<~TOTAL
|
|
67
|
+
|
|
68
|
+
Total route distance: #{route.total_distance.to_km}
|
|
69
|
+
TOTAL
|
|
70
|
+
puts
|
|
71
|
+
|
|
72
|
+
# ── 4. Mutation: building a path incrementally ────────────────
|
|
73
|
+
|
|
74
|
+
puts "--- 4. Mutation ---"
|
|
75
|
+
|
|
76
|
+
# Start empty, build with <<
|
|
77
|
+
trail = Path.new
|
|
78
|
+
trail << battery_park << wall_street << city_hall
|
|
79
|
+
puts "Built with <<: #{trail.size} waypoints"
|
|
80
|
+
|
|
81
|
+
# Prepend with >>
|
|
82
|
+
trail >> LLA.new(lat: 40.6892, lng: -74.0445, alt: 0) # Statue of Liberty ferry
|
|
83
|
+
puts "After >> prepend: #{trail.size} waypoints, starts at #{trail.first.to_s(4)}"
|
|
84
|
+
|
|
85
|
+
# Insert between waypoints
|
|
86
|
+
trail.insert(brooklyn_bridge, after: wall_street)
|
|
87
|
+
puts "After insert: #{trail.size} waypoints"
|
|
88
|
+
|
|
89
|
+
# Non-mutating + returns a new path
|
|
90
|
+
extended = trail + soho
|
|
91
|
+
puts "Original trail: #{trail.size} waypoints (unchanged)"
|
|
92
|
+
puts "Extended trail: #{extended.size} waypoints (new path)"
|
|
93
|
+
|
|
94
|
+
# Delete a waypoint
|
|
95
|
+
trail.delete(wall_street)
|
|
96
|
+
puts "After delete: #{trail.size} waypoints"
|
|
97
|
+
puts
|
|
98
|
+
|
|
99
|
+
# ── 5. Path + Path, Path - Path ───────────────────────────────
|
|
100
|
+
|
|
101
|
+
puts "--- 5. Path + Path, Path - Path ---"
|
|
102
|
+
|
|
103
|
+
downtown = Path.new(coordinates: [battery_park, wall_street, brooklyn_bridge])
|
|
104
|
+
uptown = Path.new(coordinates: [city_hall, soho, union_square])
|
|
105
|
+
|
|
106
|
+
# Concatenate two paths
|
|
107
|
+
combined = downtown + uptown
|
|
108
|
+
puts " Downtown (#{downtown.size}) + Uptown (#{uptown.size}) = Combined (#{combined.size})"
|
|
109
|
+
|
|
110
|
+
# Subtract a path's coordinates from another
|
|
111
|
+
trimmed = combined - uptown
|
|
112
|
+
puts " Combined - Uptown = #{trimmed.size} waypoints: #{trimmed.map { |c| c.to_s(4) }.join(', ')}"
|
|
113
|
+
puts
|
|
114
|
+
|
|
115
|
+
# ── 6. Closest approach ───────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
puts "--- 6. Closest Approach ---"
|
|
118
|
+
|
|
119
|
+
# The Flatiron Building is not on our route — where do we pass closest?
|
|
120
|
+
flatiron = LLA.new(lat: 40.7411, lng: -73.9897, alt: 0)
|
|
121
|
+
|
|
122
|
+
closest = route.closest_coordinate_to(flatiron)
|
|
123
|
+
dist = route.distance_to(flatiron)
|
|
124
|
+
bearing = route.bearing_to(flatiron)
|
|
125
|
+
|
|
126
|
+
puts <<~APPROACH
|
|
127
|
+
Target: Flatiron Building (#{flatiron.to_s(4)})
|
|
128
|
+
Nearest waypoint: #{route.nearest_waypoint(flatiron).to_s(4)}
|
|
129
|
+
Closest approach: #{closest.to_s(4)}
|
|
130
|
+
Distance: #{dist.to_km}
|
|
131
|
+
Bearing: #{bearing.to_s(1)} (#{bearing.to_compass(points: 8)})
|
|
132
|
+
APPROACH
|
|
133
|
+
|
|
134
|
+
# Compare waypoint-only vs geometric projection
|
|
135
|
+
wp_dist = route.nearest_waypoint(flatiron).distance_to(flatiron)
|
|
136
|
+
puts <<~COMPARE
|
|
137
|
+
Waypoint-only distance: #{wp_dist.to_km}
|
|
138
|
+
Projected distance: #{dist.to_km}
|
|
139
|
+
Improvement: #{Distance.new(wp_dist.meters - dist.meters)}
|
|
140
|
+
COMPARE
|
|
141
|
+
puts
|
|
142
|
+
|
|
143
|
+
# ── 7. Containment testing ─────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
puts "--- 7. Containment ---"
|
|
146
|
+
|
|
147
|
+
# Waypoint check
|
|
148
|
+
puts " Union Square is a waypoint? #{route.includes?(union_square)}"
|
|
149
|
+
puts " Flatiron is a waypoint? #{route.includes?(flatiron)}"
|
|
150
|
+
|
|
151
|
+
# On-segment check (is a point on the path within tolerance?)
|
|
152
|
+
midpoint_lat = (union_square.lat + empire_state.lat) / 2.0
|
|
153
|
+
midpoint_lng = (union_square.lng + empire_state.lng) / 2.0
|
|
154
|
+
on_path = LLA.new(lat: midpoint_lat, lng: midpoint_lng, alt: 0)
|
|
155
|
+
off_path = LLA.new(lat: midpoint_lat + 0.01, lng: midpoint_lng, alt: 0)
|
|
156
|
+
|
|
157
|
+
puts <<~CONTAINMENT
|
|
158
|
+
Midpoint on segment? #{route.contains?(on_path)}
|
|
159
|
+
Point 1km off path? #{route.contains?(off_path)}
|
|
160
|
+
Point 1km off (500m tol)? #{route.contains?(off_path, tolerance: 500)}
|
|
161
|
+
CONTAINMENT
|
|
162
|
+
puts
|
|
163
|
+
|
|
164
|
+
# ── 8. Enumerable ──────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
puts "--- 8. Enumerable ---"
|
|
167
|
+
|
|
168
|
+
# Path includes Enumerable — use map, select, any?, etc.
|
|
169
|
+
latitudes = route.map { |c| c.lat.round(4) }
|
|
170
|
+
puts " Latitudes: #{latitudes.join(', ')}"
|
|
171
|
+
|
|
172
|
+
northernmost = route.max_by { |c| c.lat }
|
|
173
|
+
puts " Northernmost waypoint: #{northernmost.to_s(4)}"
|
|
174
|
+
|
|
175
|
+
above_40_72 = route.select { |c| c.lat > 40.72 }
|
|
176
|
+
puts " Waypoints above 40.72°N: #{above_40_72.size}"
|
|
177
|
+
puts
|
|
178
|
+
|
|
179
|
+
# ── 9. Equality ────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
puts "--- 9. Equality ---"
|
|
182
|
+
|
|
183
|
+
p1 = Path.new(coordinates: [battery_park, wall_street, city_hall])
|
|
184
|
+
p2 = Path.new(coordinates: [battery_park, wall_street, city_hall])
|
|
185
|
+
p3 = p1.reverse
|
|
186
|
+
|
|
187
|
+
puts <<~EQUALITY
|
|
188
|
+
Same coordinates, same order: #{p1 == p2}
|
|
189
|
+
Same coordinates, reversed order: #{p1 == p3}
|
|
190
|
+
Path vs reversed path: #{p1 == p1.reverse}
|
|
191
|
+
EQUALITY
|
|
192
|
+
puts
|
|
193
|
+
|
|
194
|
+
# ── 10. Subpath (between) ─────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
puts "--- 10. Subpath (between) ---"
|
|
197
|
+
|
|
198
|
+
sub = route.between(wall_street, union_square)
|
|
199
|
+
puts <<~SUBPATH
|
|
200
|
+
Full route: #{route.size} waypoints, #{route.total_distance.to_km}
|
|
201
|
+
Subpath: #{sub.size} waypoints (Wall Street → Union Square)
|
|
202
|
+
Sub-distance: #{sub.total_distance.to_km}
|
|
203
|
+
SUBPATH
|
|
204
|
+
puts
|
|
205
|
+
|
|
206
|
+
# ── 11. Split ──────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
puts "--- 11. Split ---"
|
|
209
|
+
|
|
210
|
+
left, right = route.split_at(city_hall)
|
|
211
|
+
puts <<~SPLIT
|
|
212
|
+
Split at City Hall:
|
|
213
|
+
Left half: #{left.size} waypoints (#{left.first.to_s(4)} → #{left.last.to_s(4)})
|
|
214
|
+
Right half: #{right.size} waypoints (#{right.first.to_s(4)} → #{right.last.to_s(4)})
|
|
215
|
+
Shared point: #{left.last == right.first}
|
|
216
|
+
SPLIT
|
|
217
|
+
puts
|
|
218
|
+
|
|
219
|
+
# ── 12. Interpolation (at_distance) ───────────────────────────
|
|
220
|
+
|
|
221
|
+
puts "--- 12. Interpolation ---"
|
|
222
|
+
|
|
223
|
+
total = route.total_distance
|
|
224
|
+
quarter = route.at_distance(Distance.new(total.meters * 0.25))
|
|
225
|
+
halfway = route.at_distance(Distance.new(total.meters * 0.50))
|
|
226
|
+
three_qtr = route.at_distance(Distance.new(total.meters * 0.75))
|
|
227
|
+
|
|
228
|
+
puts <<~INTERP
|
|
229
|
+
Total route: #{total.to_km}
|
|
230
|
+
At 25%: #{quarter.to_s(4)}
|
|
231
|
+
At 50%: #{halfway.to_s(4)}
|
|
232
|
+
At 75%: #{three_qtr.to_s(4)}
|
|
233
|
+
INTERP
|
|
234
|
+
puts
|
|
235
|
+
|
|
236
|
+
# ── 13. Bounding Box ──────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
puts "--- 13. Bounding Box ---"
|
|
239
|
+
|
|
240
|
+
bbox = route.bounds
|
|
241
|
+
puts <<~BOUNDS
|
|
242
|
+
NW corner: #{bbox.nw.to_s(4)}
|
|
243
|
+
SE corner: #{bbox.se.to_s(4)}
|
|
244
|
+
Centroid: #{bbox.centroid.to_s(4)}
|
|
245
|
+
Flatiron inside bounds? #{bbox.includes?(flatiron)}
|
|
246
|
+
Central Park in bounds? #{bbox.includes?(central_park)}
|
|
247
|
+
BOUNDS
|
|
248
|
+
puts
|
|
249
|
+
|
|
250
|
+
# ── 14. To Polygon ────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
puts "--- 14. To Polygon ---"
|
|
253
|
+
|
|
254
|
+
# A triangular path can be closed into a polygon
|
|
255
|
+
triangle = Path.new(coordinates: [battery_park, brooklyn_bridge, empire_state])
|
|
256
|
+
poly = triangle.to_polygon
|
|
257
|
+
puts <<~POLYGON
|
|
258
|
+
Triangle path: #{triangle.size} waypoints
|
|
259
|
+
Polygon boundary: #{poly.boundary.size} points (closed)
|
|
260
|
+
City Hall inside triangle? #{poly.includes?(city_hall)}
|
|
261
|
+
Central Park inside? #{poly.includes?(central_park)}
|
|
262
|
+
POLYGON
|
|
263
|
+
puts
|
|
264
|
+
|
|
265
|
+
# ── 15. Intersection ──────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
puts "--- 15. Path Intersection ---"
|
|
268
|
+
|
|
269
|
+
# A crosstown path that crosses our uptown route
|
|
270
|
+
crosstown_west = LLA.new(lat: 40.7350, lng: -74.0050, alt: 0)
|
|
271
|
+
crosstown_east = LLA.new(lat: 40.7350, lng: -73.9750, alt: 0)
|
|
272
|
+
crosstown = Path.new(coordinates: [crosstown_west, crosstown_east])
|
|
273
|
+
|
|
274
|
+
# A path that runs parallel, never crossing
|
|
275
|
+
parallel_west = LLA.new(lat: 40.7000, lng: -74.0200, alt: 0)
|
|
276
|
+
parallel_east = LLA.new(lat: 40.7000, lng: -73.9800, alt: 0)
|
|
277
|
+
parallel = Path.new(coordinates: [parallel_west, parallel_east])
|
|
278
|
+
|
|
279
|
+
puts <<~INTERSECT
|
|
280
|
+
Route intersects crosstown? #{route.intersects?(crosstown)}
|
|
281
|
+
Route intersects parallel? #{route.intersects?(parallel)}
|
|
282
|
+
INTERSECT
|
|
283
|
+
puts
|
|
284
|
+
|
|
285
|
+
# ── 16. Path-to-Path closest points ──────────────────────────
|
|
286
|
+
|
|
287
|
+
puts "--- 16. Path-to-Path Closest Points ---"
|
|
288
|
+
|
|
289
|
+
west_side = Path.new(coordinates: [
|
|
290
|
+
LLA.new(lat: 40.7100, lng: -74.0150, alt: 0),
|
|
291
|
+
LLA.new(lat: 40.7500, lng: -74.0050, alt: 0)
|
|
292
|
+
])
|
|
293
|
+
|
|
294
|
+
result = route.closest_points_to(west_side)
|
|
295
|
+
puts <<~P2P
|
|
296
|
+
Route closest point: #{result[:path_point].to_s(4)}
|
|
297
|
+
West Side closest point: #{result[:area_point].to_s(4)}
|
|
298
|
+
Distance between: #{result[:distance].to_km}
|
|
299
|
+
P2P
|
|
300
|
+
puts
|
|
301
|
+
|
|
302
|
+
# ── 17. Path-to-Area closest points ──────────────────────────
|
|
303
|
+
|
|
304
|
+
puts "--- 17. Path-to-Area Closest Points ---"
|
|
305
|
+
|
|
306
|
+
# Distance from route to a circular area around Central Park
|
|
307
|
+
park_zone = Areas::Circle.new(centroid: central_park, radius: 500)
|
|
308
|
+
circle_result = route.closest_points_to(park_zone)
|
|
309
|
+
|
|
310
|
+
puts <<~AREA
|
|
311
|
+
Central Park zone (500m radius):
|
|
312
|
+
Path closest point: #{circle_result[:path_point].to_s(4)}
|
|
313
|
+
Area closest point: #{circle_result[:area_point].to_s(4)}
|
|
314
|
+
Distance to zone: #{circle_result[:distance].to_km}
|
|
315
|
+
AREA
|
|
316
|
+
|
|
317
|
+
# Distance from route to a polygon
|
|
318
|
+
park_poly = Areas::Polygon.new(boundary: [
|
|
319
|
+
LLA.new(lat: 40.800, lng: -73.958, alt: 0),
|
|
320
|
+
LLA.new(lat: 40.800, lng: -73.949, alt: 0),
|
|
321
|
+
LLA.new(lat: 40.764, lng: -73.973, alt: 0),
|
|
322
|
+
LLA.new(lat: 40.764, lng: -73.981, alt: 0)
|
|
323
|
+
])
|
|
324
|
+
|
|
325
|
+
poly_result = route.closest_points_to(park_poly)
|
|
326
|
+
puts <<~POLY
|
|
327
|
+
Central Park polygon:
|
|
328
|
+
Path closest point: #{poly_result[:path_point].to_s(4)}
|
|
329
|
+
Area closest point: #{poly_result[:area_point].to_s(4)}
|
|
330
|
+
Distance to polygon: #{poly_result[:distance].to_km}
|
|
331
|
+
POLY
|
|
332
|
+
puts
|
|
333
|
+
|
|
334
|
+
# ── 18. Reverse ────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
puts "--- 18. Reverse ---"
|
|
337
|
+
|
|
338
|
+
return_route = route.reverse
|
|
339
|
+
puts <<~REVERSE
|
|
340
|
+
Original: #{route.first.to_s(4)} -> #{route.last.to_s(4)}
|
|
341
|
+
Reversed: #{return_route.first.to_s(4)} -> #{return_route.last.to_s(4)}
|
|
342
|
+
Same distance: #{route.total_distance.to_km} vs #{return_route.total_distance.to_km}
|
|
343
|
+
REVERSE
|
|
344
|
+
puts
|
|
345
|
+
|
|
346
|
+
# ── 19. Feature integration ───────────────────────────────────
|
|
347
|
+
|
|
348
|
+
puts "--- 19. Feature Integration ---"
|
|
349
|
+
|
|
350
|
+
hiking_route = Feature.new(
|
|
351
|
+
label: "Manhattan Walking Tour",
|
|
352
|
+
geometry: route,
|
|
353
|
+
metadata: { type: "walking", difficulty: "easy" }
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
statue = Feature.new(
|
|
357
|
+
label: "Statue of Liberty",
|
|
358
|
+
geometry: LLA.new(lat: 40.6892, lng: -74.0445, alt: 0),
|
|
359
|
+
metadata: { category: "monument" }
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
puts <<~FEATURE
|
|
363
|
+
Route: #{hiking_route.label} (#{hiking_route.metadata[:type]})
|
|
364
|
+
Closest approach to #{statue.label}: #{hiking_route.distance_to(statue).to_km}
|
|
365
|
+
Bearing to #{statue.label}: #{hiking_route.bearing_to(statue).to_s(1)}
|
|
366
|
+
FEATURE
|
|
@@ -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
|
@@ -60,3 +60,44 @@ CLI flags:
|
|
|
60
60
|
```
|
|
61
61
|
|
|
62
62
|
Output: `examples/05_map_rendering/nyc_landmarks.png`
|
|
63
|
+
|
|
64
|
+
## 06 - Path Operations
|
|
65
|
+
|
|
66
|
+
Demonstrates the `Geodetic::Path` class with a walking route through Manhattan. Covers:
|
|
67
|
+
|
|
68
|
+
- **Construction** from arrays and incremental building with `<<` and `>>`
|
|
69
|
+
- **Navigation** with `first`, `last`, `next`, `prev`
|
|
70
|
+
- **Segment analysis** with distances and bearings for each leg
|
|
71
|
+
- **Mutation** with `insert`, `delete`, `+`, `-`
|
|
72
|
+
- **Path arithmetic** combining paths with `+`, `<<`, `>>` and removing with `-`
|
|
73
|
+
- **Closest approach** using geometric projection to find the nearest point on the path to an off-path target
|
|
74
|
+
- **Containment testing** with `includes?` (waypoint check) and `contains?` (on-segment check)
|
|
75
|
+
- **Enumerable** iteration with `map`, `select`, `max_by`
|
|
76
|
+
- **Equality** comparing paths by coordinates and order
|
|
77
|
+
- **Subpath extraction** with `between` and **splitting** with `split_at`
|
|
78
|
+
- **Interpolation** finding coordinates at a given distance along the path with `at_distance`
|
|
79
|
+
- **Bounding box** with `bounds` returning an `Areas::BoundingBox`
|
|
80
|
+
- **Polygon conversion** with `to_polygon` (validates no self-intersection)
|
|
81
|
+
- **Path intersection** detection with `intersects?`
|
|
82
|
+
- **Path-to-Path closest points** finding the nearest pair between two paths
|
|
83
|
+
- **Path-to-Area closest points** for Circle and Polygon areas
|
|
84
|
+
- **Reverse** to create the return route
|
|
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
|