geodetic 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -2
- data/README.md +45 -8
- data/docs/coordinate-systems/gars.md +2 -2
- data/docs/coordinate-systems/georef.md +2 -2
- data/docs/coordinate-systems/gh.md +2 -2
- data/docs/coordinate-systems/gh36.md +2 -2
- data/docs/coordinate-systems/h3.md +2 -2
- data/docs/coordinate-systems/ham.md +2 -2
- data/docs/coordinate-systems/olc.md +2 -2
- data/docs/index.md +4 -2
- data/docs/reference/areas.md +140 -14
- data/docs/reference/feature.md +2 -2
- data/docs/reference/path.md +3 -3
- data/docs/reference/segment.md +181 -0
- data/examples/02_all_coordinate_systems.rb +6 -6
- data/examples/06_path_operations.rb +2 -4
- data/examples/07_segments_and_shapes.rb +258 -0
- data/examples/README.md +19 -1
- data/lib/geodetic/areas/bounding_box.rb +56 -0
- data/lib/geodetic/areas/hexagon.rb +11 -0
- data/lib/geodetic/areas/octagon.rb +11 -0
- data/lib/geodetic/areas/pentagon.rb +11 -0
- data/lib/geodetic/areas/polygon.rb +54 -14
- data/lib/geodetic/areas/rectangle.rb +85 -35
- data/lib/geodetic/areas/regular_polygon.rb +59 -0
- data/lib/geodetic/areas/triangle.rb +180 -0
- data/lib/geodetic/areas.rb +6 -0
- data/lib/geodetic/coordinate/gh36.rb +1 -1
- data/lib/geodetic/coordinate/h3.rb +1 -1
- data/lib/geodetic/coordinate/spatial_hash.rb +2 -2
- data/lib/geodetic/path.rb +26 -153
- data/lib/geodetic/segment.rb +172 -0
- data/lib/geodetic/version.rb +1 -1
- data/lib/geodetic.rb +1 -0
- data/mkdocs.yml +1 -0
- metadata +10 -1
data/lib/geodetic/path.rb
CHANGED
|
@@ -32,7 +32,7 @@ module Geodetic
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
def segments
|
|
35
|
-
@coordinates.each_cons(2).
|
|
35
|
+
@coordinates.each_cons(2).map { |a, b| Segment.new(a, b) }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
def size
|
|
@@ -66,7 +66,7 @@ module Geodetic
|
|
|
66
66
|
def contains?(coordinate, tolerance: DEFAULT_TOLERANCE_METERS)
|
|
67
67
|
return true if include?(coordinate)
|
|
68
68
|
|
|
69
|
-
segments.any? { |
|
|
69
|
+
segments.any? { |seg| seg.contains?(coordinate, tolerance: tolerance) }
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
alias inside? contains?
|
|
@@ -146,17 +146,11 @@ module Geodetic
|
|
|
146
146
|
|
|
147
147
|
accumulated = 0.0
|
|
148
148
|
|
|
149
|
-
segments.each do |
|
|
150
|
-
seg_len =
|
|
149
|
+
segments.each do |seg|
|
|
150
|
+
seg_len = seg.length_meters
|
|
151
151
|
if accumulated + seg_len >= target_m
|
|
152
152
|
fraction = (target_m - accumulated) / seg_len
|
|
153
|
-
|
|
154
|
-
b_lla = b.is_a?(Coordinate::LLA) ? b : b.to_lla
|
|
155
|
-
return Coordinate::LLA.new(
|
|
156
|
-
lat: a_lla.lat + (b_lla.lat - a_lla.lat) * fraction,
|
|
157
|
-
lng: a_lla.lng + (b_lla.lng - a_lla.lng) * fraction,
|
|
158
|
-
alt: a_lla.alt + (b_lla.alt - a_lla.alt) * fraction
|
|
159
|
-
)
|
|
153
|
+
return seg.interpolate(fraction)
|
|
160
154
|
end
|
|
161
155
|
accumulated += seg_len
|
|
162
156
|
end
|
|
@@ -170,7 +164,7 @@ module Geodetic
|
|
|
170
164
|
lats = @coordinates.map { |c| c.is_a?(Coordinate::LLA) ? c.lat : c.to_lla.lat }
|
|
171
165
|
lngs = @coordinates.map { |c| c.is_a?(Coordinate::LLA) ? c.lng : c.to_lla.lng }
|
|
172
166
|
|
|
173
|
-
Areas::
|
|
167
|
+
Areas::BoundingBox.new(
|
|
174
168
|
nw: Coordinate::LLA.new(lat: lats.max, lng: lngs.min, alt: 0),
|
|
175
169
|
se: Coordinate::LLA.new(lat: lats.min, lng: lngs.max, alt: 0)
|
|
176
170
|
)
|
|
@@ -179,12 +173,10 @@ module Geodetic
|
|
|
179
173
|
def to_polygon
|
|
180
174
|
raise ArgumentError, "need at least 3 coordinates for a polygon" if size < 3
|
|
181
175
|
|
|
182
|
-
|
|
183
|
-
closing_a = @coordinates.last
|
|
184
|
-
closing_b = @coordinates.first
|
|
176
|
+
closing = Segment.new(@coordinates.last, @coordinates.first)
|
|
185
177
|
interior = segments[1...-1] || []
|
|
186
178
|
|
|
187
|
-
if interior.any? { |
|
|
179
|
+
if interior.any? { |seg| closing.intersects?(seg) }
|
|
188
180
|
raise ArgumentError, "closing segment intersects the path"
|
|
189
181
|
end
|
|
190
182
|
|
|
@@ -194,9 +186,9 @@ module Geodetic
|
|
|
194
186
|
def intersects?(other_path)
|
|
195
187
|
raise ArgumentError, "expected a Path" unless other_path.is_a?(Path)
|
|
196
188
|
|
|
197
|
-
segments.each do |
|
|
198
|
-
other_path.segments.each do |
|
|
199
|
-
return true if
|
|
189
|
+
segments.each do |seg1|
|
|
190
|
+
other_path.segments.each do |seg2|
|
|
191
|
+
return true if seg1.intersects?(seg2)
|
|
200
192
|
end
|
|
201
193
|
end
|
|
202
194
|
|
|
@@ -208,11 +200,11 @@ module Geodetic
|
|
|
208
200
|
end
|
|
209
201
|
|
|
210
202
|
def segment_distances
|
|
211
|
-
segments.map
|
|
203
|
+
segments.map(&:length)
|
|
212
204
|
end
|
|
213
205
|
|
|
214
206
|
def segment_bearings
|
|
215
|
-
segments.map
|
|
207
|
+
segments.map(&:bearing)
|
|
216
208
|
end
|
|
217
209
|
|
|
218
210
|
# --- Non-mutating operators ---
|
|
@@ -343,7 +335,8 @@ module Geodetic
|
|
|
343
335
|
best_dist = waypoint.distance_to(target).meters
|
|
344
336
|
|
|
345
337
|
if idx > 0
|
|
346
|
-
|
|
338
|
+
seg = Segment.new(@coordinates[idx - 1], waypoint)
|
|
339
|
+
candidate, candidate_dist = seg.project(target)
|
|
347
340
|
if candidate_dist < best_dist
|
|
348
341
|
best = candidate
|
|
349
342
|
best_dist = candidate_dist
|
|
@@ -351,7 +344,8 @@ module Geodetic
|
|
|
351
344
|
end
|
|
352
345
|
|
|
353
346
|
if idx < @coordinates.length - 1
|
|
354
|
-
|
|
347
|
+
seg = Segment.new(waypoint, @coordinates[idx + 1])
|
|
348
|
+
candidate, candidate_dist = seg.project(target)
|
|
355
349
|
if candidate_dist < best_dist
|
|
356
350
|
best = candidate
|
|
357
351
|
best_dist = candidate_dist
|
|
@@ -362,24 +356,22 @@ module Geodetic
|
|
|
362
356
|
end
|
|
363
357
|
|
|
364
358
|
def closest_points_to_boundary(boundary_coords)
|
|
365
|
-
|
|
359
|
+
boundary_segs = boundary_coords.each_cons(2).map { |a, b| Segment.new(a, b) }
|
|
366
360
|
|
|
367
361
|
best = { path_point: nil, area_point: nil, distance: Distance.new(Float::INFINITY) }
|
|
368
362
|
|
|
369
|
-
|
|
370
|
-
# For each boundary segment, project each path waypoint onto it
|
|
371
|
-
segments.each do |seg_a, seg_b|
|
|
363
|
+
segments.each do |seg|
|
|
372
364
|
boundary_coords.each do |bpt|
|
|
373
|
-
candidate, dist =
|
|
365
|
+
candidate, dist = seg.project(bpt)
|
|
374
366
|
if dist < best[:distance].meters
|
|
375
367
|
best = { path_point: candidate, area_point: bpt, distance: Distance.new(dist) }
|
|
376
368
|
end
|
|
377
369
|
end
|
|
378
370
|
end
|
|
379
371
|
|
|
380
|
-
|
|
372
|
+
boundary_segs.each do |bseg|
|
|
381
373
|
@coordinates.each do |wpt|
|
|
382
|
-
candidate, dist =
|
|
374
|
+
candidate, dist = bseg.project(wpt)
|
|
383
375
|
if dist < best[:distance].meters
|
|
384
376
|
best = { path_point: wpt, area_point: candidate, distance: Distance.new(dist) }
|
|
385
377
|
end
|
|
@@ -390,23 +382,16 @@ module Geodetic
|
|
|
390
382
|
end
|
|
391
383
|
|
|
392
384
|
def closest_points_to_circle(circle)
|
|
393
|
-
# Find closest point on path to the circle's centroid
|
|
394
385
|
path_point = closest_point_to_coordinate(circle.centroid)
|
|
395
386
|
dist_to_center = path_point.distance_to(circle.centroid).meters
|
|
396
387
|
|
|
397
|
-
# The closest point on the circle is along the line from
|
|
398
|
-
# centroid toward the path point, offset by the radius
|
|
399
388
|
if dist_to_center < 1e-6
|
|
400
|
-
# Path passes through the center — pick any point on the circle
|
|
401
389
|
area_point = circle.centroid
|
|
402
390
|
dist = circle.radius
|
|
403
391
|
elsif dist_to_center <= circle.radius
|
|
404
|
-
# Path is inside the circle
|
|
405
392
|
area_point = path_point
|
|
406
393
|
dist = 0.0
|
|
407
394
|
else
|
|
408
|
-
# Interpolate from centroid toward path_point by radius distance
|
|
409
|
-
bearing = circle.centroid.bearing_to(path_point).degrees
|
|
410
395
|
fraction = circle.radius / dist_to_center
|
|
411
396
|
c_lla = circle.centroid
|
|
412
397
|
p_lla = path_point.is_a?(Coordinate::LLA) ? path_point : path_point.to_lla
|
|
@@ -426,7 +411,7 @@ module Geodetic
|
|
|
426
411
|
case area
|
|
427
412
|
when Areas::Polygon
|
|
428
413
|
area.boundary
|
|
429
|
-
when Areas::
|
|
414
|
+
when Areas::BoundingBox
|
|
430
415
|
[area.nw,
|
|
431
416
|
Coordinate::LLA.new(lat: area.nw.lat, lng: area.se.lng, alt: 0),
|
|
432
417
|
area.se,
|
|
@@ -443,10 +428,10 @@ module Geodetic
|
|
|
443
428
|
resolve_geometry(other.geometry)
|
|
444
429
|
when Path
|
|
445
430
|
other.coordinates
|
|
446
|
-
when Areas::Polygon, Areas::
|
|
431
|
+
when Areas::Polygon, Areas::BoundingBox
|
|
447
432
|
area_boundary(other)
|
|
448
433
|
when Areas::Circle
|
|
449
|
-
other
|
|
434
|
+
other
|
|
450
435
|
else
|
|
451
436
|
other.respond_to?(:centroid) ? other.centroid : other
|
|
452
437
|
end
|
|
@@ -456,69 +441,13 @@ module Geodetic
|
|
|
456
441
|
case other
|
|
457
442
|
when Feature
|
|
458
443
|
resolve_area(other.geometry)
|
|
459
|
-
when Areas::Circle, Areas::Polygon, Areas::
|
|
444
|
+
when Areas::Circle, Areas::Polygon, Areas::BoundingBox, Path
|
|
460
445
|
other
|
|
461
446
|
else
|
|
462
447
|
raise ArgumentError, "expected an Area or Path, got #{other.class}"
|
|
463
448
|
end
|
|
464
449
|
end
|
|
465
450
|
|
|
466
|
-
# Projects target onto segment A→B using triangle geometry.
|
|
467
|
-
# Returns [closest_coordinate, distance_to_target].
|
|
468
|
-
#
|
|
469
|
-
# Given triangle A-T with known:
|
|
470
|
-
# - bearing A→B (segment heading)
|
|
471
|
-
# - bearing A→T
|
|
472
|
-
# - distance A→T
|
|
473
|
-
#
|
|
474
|
-
# The angle between A→B and A→T gives us:
|
|
475
|
-
# - along-segment distance = dist_AT * cos(angle)
|
|
476
|
-
# - perpendicular distance = dist_AT * sin(angle)
|
|
477
|
-
#
|
|
478
|
-
# If the foot of the perpendicular falls between A and B,
|
|
479
|
-
# that foot is the closest point. Otherwise the nearer
|
|
480
|
-
# endpoint wins.
|
|
481
|
-
def project_onto_segment(a, b, target)
|
|
482
|
-
seg_dist = a.distance_to(b).meters
|
|
483
|
-
dist_a_t = a.distance_to(target).meters
|
|
484
|
-
|
|
485
|
-
# Degenerate: zero-length segment or target at endpoint
|
|
486
|
-
return [a, dist_a_t] if seg_dist < 1e-6
|
|
487
|
-
return [a, 0.0] if dist_a_t < 1e-6
|
|
488
|
-
|
|
489
|
-
bearing_ab = a.bearing_to(b).degrees
|
|
490
|
-
bearing_at = a.bearing_to(target).degrees
|
|
491
|
-
|
|
492
|
-
# Angle between the segment direction and the line to target
|
|
493
|
-
angle = (bearing_at - bearing_ab).abs
|
|
494
|
-
angle = 360.0 - angle if angle > 180.0
|
|
495
|
-
angle_rad = angle * Geodetic::RAD_PER_DEG
|
|
496
|
-
|
|
497
|
-
# Distance along the segment from A to the foot of the perpendicular
|
|
498
|
-
along = dist_a_t * Math.cos(angle_rad)
|
|
499
|
-
|
|
500
|
-
# If the foot falls before A or beyond B, the closest point is an endpoint
|
|
501
|
-
if along <= 0.0
|
|
502
|
-
return [a, dist_a_t]
|
|
503
|
-
elsif along >= seg_dist
|
|
504
|
-
dist_b_t = b.distance_to(target).meters
|
|
505
|
-
return [b, dist_b_t]
|
|
506
|
-
end
|
|
507
|
-
|
|
508
|
-
# Interpolate along the segment to find the foot
|
|
509
|
-
fraction = along / seg_dist
|
|
510
|
-
a_lla = a.is_a?(Coordinate::LLA) ? a : a.to_lla
|
|
511
|
-
b_lla = b.is_a?(Coordinate::LLA) ? b : b.to_lla
|
|
512
|
-
|
|
513
|
-
foot = Coordinate::LLA.new(
|
|
514
|
-
lat: a_lla.lat + (b_lla.lat - a_lla.lat) * fraction,
|
|
515
|
-
lng: a_lla.lng + (b_lla.lng - a_lla.lng) * fraction,
|
|
516
|
-
alt: a_lla.alt + (b_lla.alt - a_lla.alt) * fraction
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
[foot, foot.distance_to(target).meters]
|
|
520
|
-
end
|
|
521
|
-
|
|
522
451
|
def resolve_point_from(other)
|
|
523
452
|
case other
|
|
524
453
|
when Feature
|
|
@@ -532,62 +461,6 @@ module Geodetic
|
|
|
532
461
|
end
|
|
533
462
|
end
|
|
534
463
|
|
|
535
|
-
def on_segment?(a, b, point, tolerance)
|
|
536
|
-
seg_dist = a.distance_to(b).meters
|
|
537
|
-
return a == point || b == point if seg_dist < 1e-6
|
|
538
|
-
|
|
539
|
-
dist_a_p = a.distance_to(point).meters
|
|
540
|
-
dist_p_b = point.distance_to(b).meters
|
|
541
|
-
|
|
542
|
-
# Point must be closer to both endpoints than the segment length
|
|
543
|
-
return false if dist_a_p > seg_dist || dist_p_b > seg_dist
|
|
544
|
-
|
|
545
|
-
# Bearing from A->P should match bearing from P->B within a
|
|
546
|
-
# delta derived from the tolerance and segment distance
|
|
547
|
-
delta = Math.atan(tolerance / seg_dist) * Geodetic::DEG_PER_RAD
|
|
548
|
-
|
|
549
|
-
bearing_ap = a.bearing_to(point).degrees
|
|
550
|
-
bearing_pb = point.bearing_to(b).degrees
|
|
551
|
-
|
|
552
|
-
(bearing_ap - bearing_pb).abs <= delta
|
|
553
|
-
end
|
|
554
|
-
|
|
555
|
-
# Checks if two segments (a1→b1) and (a2→b2) intersect using
|
|
556
|
-
# cross-product orientation tests on a flat lat/lng approximation.
|
|
557
|
-
def segments_intersect?(a1, b1, a2, b2)
|
|
558
|
-
p1 = to_flat(a1); q1 = to_flat(b1)
|
|
559
|
-
p2 = to_flat(a2); q2 = to_flat(b2)
|
|
560
|
-
|
|
561
|
-
d1 = cross_sign(p2, q2, p1)
|
|
562
|
-
d2 = cross_sign(p2, q2, q1)
|
|
563
|
-
d3 = cross_sign(p1, q1, p2)
|
|
564
|
-
d4 = cross_sign(p1, q1, q2)
|
|
565
|
-
|
|
566
|
-
return true if d1 != d2 && d3 != d4
|
|
567
|
-
|
|
568
|
-
if d1 == 0 && on_collinear?(p2, q2, p1) then return true end
|
|
569
|
-
if d2 == 0 && on_collinear?(p2, q2, q1) then return true end
|
|
570
|
-
if d3 == 0 && on_collinear?(p1, q1, p2) then return true end
|
|
571
|
-
if d4 == 0 && on_collinear?(p1, q1, q2) then return true end
|
|
572
|
-
|
|
573
|
-
false
|
|
574
|
-
end
|
|
575
|
-
|
|
576
|
-
def to_flat(coord)
|
|
577
|
-
c = coord.is_a?(Coordinate::LLA) ? coord : coord.to_lla
|
|
578
|
-
[c.lat, c.lng]
|
|
579
|
-
end
|
|
580
|
-
|
|
581
|
-
def cross_sign(a, b, c)
|
|
582
|
-
val = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
583
|
-
val > 1e-12 ? 1 : (val < -1e-12 ? -1 : 0)
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
def on_collinear?(a, b, p)
|
|
587
|
-
p[0] >= [a[0], b[0]].min && p[0] <= [a[0], b[0]].max &&
|
|
588
|
-
p[1] >= [a[1], b[1]].min && p[1] <= [a[1], b[1]].max
|
|
589
|
-
end
|
|
590
|
-
|
|
591
464
|
def dup_path
|
|
592
465
|
self.class.new(coordinates: @coordinates.dup)
|
|
593
466
|
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Geodetic
|
|
4
|
+
class Segment
|
|
5
|
+
attr_reader :start_point, :end_point
|
|
6
|
+
|
|
7
|
+
def initialize(start_point, end_point)
|
|
8
|
+
@start_point = start_point.is_a?(Coordinate::LLA) ? start_point : start_point.to_lla
|
|
9
|
+
@end_point = end_point.is_a?(Coordinate::LLA) ? end_point : end_point.to_lla
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# --- Properties ---
|
|
13
|
+
|
|
14
|
+
def length
|
|
15
|
+
@length ||= @start_point.distance_to(@end_point)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
alias distance length
|
|
19
|
+
|
|
20
|
+
def length_meters
|
|
21
|
+
length.meters
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bearing
|
|
25
|
+
@bearing ||= @start_point.bearing_to(@end_point)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def midpoint
|
|
29
|
+
@midpoint ||= interpolate(0.5)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
alias centroid midpoint
|
|
33
|
+
|
|
34
|
+
# --- Geometry ---
|
|
35
|
+
|
|
36
|
+
def reverse
|
|
37
|
+
self.class.new(@end_point, @start_point)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def interpolate(fraction)
|
|
41
|
+
Coordinate::LLA.new(
|
|
42
|
+
lat: @start_point.lat + (@end_point.lat - @start_point.lat) * fraction,
|
|
43
|
+
lng: @start_point.lng + (@end_point.lng - @start_point.lng) * fraction,
|
|
44
|
+
alt: @start_point.alt + (@end_point.alt - @start_point.alt) * fraction
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Projects a point onto this segment.
|
|
49
|
+
# Returns [closest_point_on_segment, distance_in_meters].
|
|
50
|
+
def project(point)
|
|
51
|
+
target = point.is_a?(Coordinate::LLA) ? point : point.to_lla
|
|
52
|
+
seg_len = length_meters
|
|
53
|
+
dist_a_t = @start_point.distance_to(target).meters
|
|
54
|
+
|
|
55
|
+
return [@start_point, dist_a_t] if seg_len < 1e-6
|
|
56
|
+
return [@start_point, 0.0] if dist_a_t < 1e-6
|
|
57
|
+
|
|
58
|
+
bearing_ab = bearing.degrees
|
|
59
|
+
bearing_at = @start_point.bearing_to(target).degrees
|
|
60
|
+
|
|
61
|
+
angle = (bearing_at - bearing_ab).abs
|
|
62
|
+
angle = 360.0 - angle if angle > 180.0
|
|
63
|
+
angle_rad = angle * Geodetic::RAD_PER_DEG
|
|
64
|
+
|
|
65
|
+
along = dist_a_t * Math.cos(angle_rad)
|
|
66
|
+
|
|
67
|
+
if along <= 0.0
|
|
68
|
+
return [@start_point, dist_a_t]
|
|
69
|
+
elsif along >= seg_len
|
|
70
|
+
dist_b_t = @end_point.distance_to(target).meters
|
|
71
|
+
return [@end_point, dist_b_t]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
foot = interpolate(along / seg_len)
|
|
75
|
+
[foot, foot.distance_to(target).meters]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Tests if a point lies on this segment within a tolerance (meters).
|
|
79
|
+
def contains?(point, tolerance: 10.0)
|
|
80
|
+
target = point.is_a?(Coordinate::LLA) ? point : point.to_lla
|
|
81
|
+
seg_len = length_meters
|
|
82
|
+
|
|
83
|
+
return @start_point == target || @end_point == target if seg_len < 1e-6
|
|
84
|
+
|
|
85
|
+
dist_a_p = @start_point.distance_to(target).meters
|
|
86
|
+
dist_p_b = target.distance_to(@end_point).meters
|
|
87
|
+
|
|
88
|
+
# Exact endpoint match
|
|
89
|
+
return true if dist_a_p < 1e-6 || dist_p_b < 1e-6
|
|
90
|
+
|
|
91
|
+
return false if dist_a_p > seg_len || dist_p_b > seg_len
|
|
92
|
+
|
|
93
|
+
delta = Math.atan(tolerance / seg_len) * Geodetic::DEG_PER_RAD
|
|
94
|
+
bearing_ap = @start_point.bearing_to(target).degrees
|
|
95
|
+
bearing_pb = target.bearing_to(@end_point).degrees
|
|
96
|
+
|
|
97
|
+
(bearing_ap - bearing_pb).abs <= delta
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# True if the point is a vertex (start or end point).
|
|
101
|
+
def includes?(point)
|
|
102
|
+
target = point.is_a?(Coordinate::LLA) ? point : point.to_lla
|
|
103
|
+
@start_point == target || @end_point == target
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def excludes?(point, tolerance: 10.0)
|
|
107
|
+
!contains?(point, tolerance: tolerance)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Tests if this segment intersects another segment.
|
|
111
|
+
def intersects?(other)
|
|
112
|
+
p1 = flat(@start_point); q1 = flat(@end_point)
|
|
113
|
+
p2 = flat(other.start_point); q2 = flat(other.end_point)
|
|
114
|
+
|
|
115
|
+
d1 = cross_sign(p2, q2, p1)
|
|
116
|
+
d2 = cross_sign(p2, q2, q1)
|
|
117
|
+
d3 = cross_sign(p1, q1, p2)
|
|
118
|
+
d4 = cross_sign(p1, q1, q2)
|
|
119
|
+
|
|
120
|
+
return true if d1 != d2 && d3 != d4
|
|
121
|
+
|
|
122
|
+
return true if d1 == 0 && on_collinear?(p2, q2, p1)
|
|
123
|
+
return true if d2 == 0 && on_collinear?(p2, q2, q1)
|
|
124
|
+
return true if d3 == 0 && on_collinear?(p1, q1, p2)
|
|
125
|
+
return true if d4 == 0 && on_collinear?(p1, q1, q2)
|
|
126
|
+
|
|
127
|
+
false
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# --- Conversion ---
|
|
131
|
+
|
|
132
|
+
def to_path
|
|
133
|
+
Path.new(coordinates: [@start_point, @end_point])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_a
|
|
137
|
+
[@start_point, @end_point]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# --- Equality / Display ---
|
|
141
|
+
|
|
142
|
+
def ==(other)
|
|
143
|
+
other.is_a?(Segment) &&
|
|
144
|
+
@start_point == other.start_point &&
|
|
145
|
+
@end_point == other.end_point
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def to_s
|
|
149
|
+
"Segment(#{@start_point} -> #{@end_point})"
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def inspect
|
|
153
|
+
"#<Geodetic::Segment start=#{@start_point.inspect} end=#{@end_point.inspect} length=#{length}>"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
private
|
|
157
|
+
|
|
158
|
+
def flat(coord)
|
|
159
|
+
[coord.lat, coord.lng]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def cross_sign(a, b, c)
|
|
163
|
+
val = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
164
|
+
val > 1e-12 ? 1 : (val < -1e-12 ? -1 : 0)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def on_collinear?(a, b, p)
|
|
168
|
+
p[0] >= [a[0], b[0]].min && p[0] <= [a[0], b[0]].max &&
|
|
169
|
+
p[1] >= [a[1], b[1]].min && p[1] <= [a[1], b[1]].max
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
data/lib/geodetic/version.rb
CHANGED
data/lib/geodetic.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "geodetic/bearing"
|
|
|
7
7
|
require_relative "geodetic/geoid_height"
|
|
8
8
|
require_relative "geodetic/coordinate"
|
|
9
9
|
require_relative "geodetic/areas"
|
|
10
|
+
require_relative "geodetic/segment"
|
|
10
11
|
require_relative "geodetic/path"
|
|
11
12
|
require_relative "geodetic/feature"
|
|
12
13
|
|
data/mkdocs.yml
CHANGED
|
@@ -136,6 +136,7 @@ nav:
|
|
|
136
136
|
- Datums: reference/datums.md
|
|
137
137
|
- Geoid Height: reference/geoid-height.md
|
|
138
138
|
- Areas: reference/areas.md
|
|
139
|
+
- Segment: reference/segment.md
|
|
139
140
|
- Path: reference/path.md
|
|
140
141
|
- Feature: reference/feature.md
|
|
141
142
|
- Serialization: reference/serialization.md
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: geodetic
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dewayne VanHoozer
|
|
@@ -57,6 +57,7 @@ files:
|
|
|
57
57
|
- docs/reference/geoid-height.md
|
|
58
58
|
- docs/reference/map-rendering.md
|
|
59
59
|
- docs/reference/path.md
|
|
60
|
+
- docs/reference/segment.md
|
|
60
61
|
- docs/reference/serialization.md
|
|
61
62
|
- examples/01_basic_conversions.rb
|
|
62
63
|
- examples/02_all_coordinate_systems.rb
|
|
@@ -71,13 +72,20 @@ files:
|
|
|
71
72
|
- examples/05_map_rendering/icons/park.png
|
|
72
73
|
- examples/05_map_rendering/nyc_landmarks.png
|
|
73
74
|
- examples/06_path_operations.rb
|
|
75
|
+
- examples/07_segments_and_shapes.rb
|
|
74
76
|
- examples/README.md
|
|
75
77
|
- fiddle_pointer_buffer_pool.md
|
|
76
78
|
- lib/geodetic.rb
|
|
77
79
|
- lib/geodetic/areas.rb
|
|
80
|
+
- lib/geodetic/areas/bounding_box.rb
|
|
78
81
|
- lib/geodetic/areas/circle.rb
|
|
82
|
+
- lib/geodetic/areas/hexagon.rb
|
|
83
|
+
- lib/geodetic/areas/octagon.rb
|
|
84
|
+
- lib/geodetic/areas/pentagon.rb
|
|
79
85
|
- lib/geodetic/areas/polygon.rb
|
|
80
86
|
- lib/geodetic/areas/rectangle.rb
|
|
87
|
+
- lib/geodetic/areas/regular_polygon.rb
|
|
88
|
+
- lib/geodetic/areas/triangle.rb
|
|
81
89
|
- lib/geodetic/bearing.rb
|
|
82
90
|
- lib/geodetic/coordinate.rb
|
|
83
91
|
- lib/geodetic/coordinate/bng.rb
|
|
@@ -104,6 +112,7 @@ files:
|
|
|
104
112
|
- lib/geodetic/feature.rb
|
|
105
113
|
- lib/geodetic/geoid_height.rb
|
|
106
114
|
- lib/geodetic/path.rb
|
|
115
|
+
- lib/geodetic/segment.rb
|
|
107
116
|
- lib/geodetic/version.rb
|
|
108
117
|
- mkdocs.yml
|
|
109
118
|
- sig/geodetic.rbs
|