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