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.
@@ -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