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
|
@@ -7,29 +7,26 @@ module Geodetic
|
|
|
7
7
|
class Polygon
|
|
8
8
|
attr_reader :boundary, :centroid
|
|
9
9
|
|
|
10
|
-
def initialize(boundary:)
|
|
10
|
+
def initialize(boundary:, validate: true)
|
|
11
11
|
raise ArgumentError, "A Polygon requires more than #{boundary.length} points on its boundary" unless boundary.length > 2
|
|
12
12
|
|
|
13
13
|
@boundary = boundary.dup
|
|
14
14
|
@boundary << boundary[0] unless boundary.first == boundary.last
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
centroid_lng = 0.0
|
|
18
|
-
area = 0.0
|
|
19
|
-
|
|
20
|
-
0.upto(@boundary.length - 2) do |i|
|
|
21
|
-
cross = @boundary[i].lng * @boundary[i + 1].lat - @boundary[i + 1].lng * @boundary[i].lat
|
|
22
|
-
area += 0.5 * cross
|
|
23
|
-
centroid_lng += (@boundary[i].lng + @boundary[i + 1].lng) * cross
|
|
24
|
-
centroid_lat += (@boundary[i].lat + @boundary[i + 1].lat) * cross
|
|
25
|
-
end
|
|
16
|
+
validate_no_self_intersection! if validate
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
compute_centroid
|
|
19
|
+
end
|
|
29
20
|
|
|
30
|
-
|
|
21
|
+
# Returns Segment objects for each edge of the polygon.
|
|
22
|
+
# Returns Segment objects for each side of the polygon.
|
|
23
|
+
def segments
|
|
24
|
+
@segments ||= @boundary.each_cons(2).map { |a, b| Segment.new(a, b) }
|
|
31
25
|
end
|
|
32
26
|
|
|
27
|
+
alias edges segments
|
|
28
|
+
alias border segments
|
|
29
|
+
|
|
33
30
|
def includes?(a_point)
|
|
34
31
|
turn_angle = 0.0
|
|
35
32
|
|
|
@@ -52,6 +49,49 @@ module Geodetic
|
|
|
52
49
|
alias_method :exclude?, :excludes?
|
|
53
50
|
alias_method :inside?, :includes?
|
|
54
51
|
alias_method :outside?, :excludes?
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def compute_centroid
|
|
56
|
+
centroid_lat = 0.0
|
|
57
|
+
centroid_lng = 0.0
|
|
58
|
+
area = 0.0
|
|
59
|
+
|
|
60
|
+
0.upto(@boundary.length - 2) do |i|
|
|
61
|
+
cross = @boundary[i].lng * @boundary[i + 1].lat - @boundary[i + 1].lng * @boundary[i].lat
|
|
62
|
+
area += 0.5 * cross
|
|
63
|
+
centroid_lng += (@boundary[i].lng + @boundary[i + 1].lng) * cross
|
|
64
|
+
centroid_lat += (@boundary[i].lat + @boundary[i + 1].lat) * cross
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if area.abs < 1e-12
|
|
68
|
+
# Degenerate polygon (collinear or self-intersecting) — fall back to mean
|
|
69
|
+
centroid_lat = @boundary[0...-1].sum(&:lat) / (@boundary.length - 1).to_f
|
|
70
|
+
centroid_lng = @boundary[0...-1].sum(&:lng) / (@boundary.length - 1).to_f
|
|
71
|
+
else
|
|
72
|
+
centroid_lng /= (6.0 * area)
|
|
73
|
+
centroid_lat /= (6.0 * area)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
@centroid = Coordinate::LLA.new(lat: centroid_lat, lng: centroid_lng, alt: 0.0)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_no_self_intersection!
|
|
80
|
+
segs = @boundary.each_cons(2).map { |a, b| Segment.new(a, b) }
|
|
81
|
+
|
|
82
|
+
segs.each_with_index do |seg_i, i|
|
|
83
|
+
segs.each_with_index do |seg_j, j|
|
|
84
|
+
# Skip same edge and adjacent edges (they share a vertex)
|
|
85
|
+
next if j <= i + 1
|
|
86
|
+
# Skip first-last pair (they share the closing vertex)
|
|
87
|
+
next if i == 0 && j == segs.length - 1
|
|
88
|
+
|
|
89
|
+
if seg_i.intersects?(seg_j)
|
|
90
|
+
raise ArgumentError, "edge #{i} intersects edge #{j} — polygon boundary must not self-intersect"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
55
95
|
end
|
|
56
96
|
end
|
|
57
97
|
end
|
|
@@ -1,55 +1,105 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
3
|
+
require_relative "polygon"
|
|
4
4
|
|
|
5
5
|
module Geodetic
|
|
6
6
|
module Areas
|
|
7
|
-
class Rectangle
|
|
8
|
-
attr_reader :
|
|
7
|
+
class Rectangle < Polygon
|
|
8
|
+
attr_reader :centerline, :width
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
# Accepts any coordinate that responds to to_lla.
|
|
10
|
+
# Construct a rectangle from a centerline Segment and a width (meters).
|
|
12
11
|
#
|
|
13
|
-
# Rectangle.new(
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
12
|
+
# Rectangle.new(segment: a_segment, width: 200)
|
|
13
|
+
# Rectangle.new(segment: [point_a, point_b], width: 200)
|
|
14
|
+
#
|
|
15
|
+
# The segment defines the centerline — height = segment.length,
|
|
16
|
+
# bearing = segment.bearing, center = segment.midpoint.
|
|
17
|
+
# Width is the perpendicular extent on each side of the centerline.
|
|
18
|
+
#
|
|
19
|
+
def initialize(segment:, width:)
|
|
20
|
+
@centerline = segment.is_a?(Segment) ? segment : Segment.new(*segment)
|
|
21
|
+
@width = width.is_a?(Distance) ? width.meters : width.to_f
|
|
22
|
+
|
|
23
|
+
raise ArgumentError, "width must be positive" unless @width > 0
|
|
24
|
+
|
|
25
|
+
super(boundary: generate_vertices, validate: false)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def sides
|
|
29
|
+
4
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Center is the midpoint of the centerline.
|
|
33
|
+
def center
|
|
34
|
+
@centerline.midpoint
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Height is the length of the centerline in meters.
|
|
38
|
+
def height
|
|
39
|
+
@centerline.length_meters
|
|
29
40
|
end
|
|
30
41
|
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
# Bearing is the direction of the centerline in degrees.
|
|
43
|
+
def bearing
|
|
44
|
+
@centerline.bearing.degrees
|
|
33
45
|
end
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
# Returns the 4 corner coordinates [front-left, front-right, back-right, back-left]
|
|
48
|
+
# relative to the bearing direction.
|
|
49
|
+
def corners
|
|
50
|
+
boundary[0..3]
|
|
37
51
|
end
|
|
38
52
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
lla.lng >= @nw.lng && lla.lng <= @se.lng
|
|
53
|
+
# True when width equals height (within tolerance).
|
|
54
|
+
def square?
|
|
55
|
+
(@width - height).abs < 1e-6
|
|
43
56
|
end
|
|
44
57
|
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
# Returns the axis-aligned bounding box that encloses this rectangle.
|
|
59
|
+
def to_bounding_box
|
|
60
|
+
lats = corners.map(&:lat)
|
|
61
|
+
lngs = corners.map(&:lng)
|
|
62
|
+
|
|
63
|
+
BoundingBox.new(
|
|
64
|
+
nw: Coordinate::LLA.new(lat: lats.max, lng: lngs.min, alt: 0),
|
|
65
|
+
se: Coordinate::LLA.new(lat: lats.min, lng: lngs.max, alt: 0)
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def generate_vertices
|
|
72
|
+
half_w = @width / 2.0
|
|
73
|
+
bearing_deg = @centerline.bearing.degrees
|
|
74
|
+
front = @centerline.end_point
|
|
75
|
+
back = @centerline.start_point
|
|
76
|
+
|
|
77
|
+
# Perpendicular bearing: +90 for right, -90 for left
|
|
78
|
+
[
|
|
79
|
+
offset_point(front, -half_w, bearing_deg + 90), # front-left
|
|
80
|
+
offset_point(front, half_w, bearing_deg + 90), # front-right
|
|
81
|
+
offset_point(back, half_w, bearing_deg + 90), # back-right
|
|
82
|
+
offset_point(back, -half_w, bearing_deg + 90) # back-left
|
|
83
|
+
]
|
|
47
84
|
end
|
|
48
85
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
# Offset a point by a signed distance along a bearing using flat-earth projection.
|
|
87
|
+
def offset_point(origin, distance_m, bearing_deg)
|
|
88
|
+
bearing_rad = bearing_deg * Geodetic::RAD_PER_DEG
|
|
89
|
+
lat_rad = origin.lat * Geodetic::RAD_PER_DEG
|
|
90
|
+
|
|
91
|
+
m_per_deg_lat = 111_320.0
|
|
92
|
+
m_per_deg_lng = 111_320.0 * Math.cos(lat_rad)
|
|
93
|
+
|
|
94
|
+
north = distance_m * Math.cos(bearing_rad) / m_per_deg_lat
|
|
95
|
+
east = distance_m * Math.sin(bearing_rad) / m_per_deg_lng
|
|
96
|
+
|
|
97
|
+
Coordinate::LLA.new(
|
|
98
|
+
lat: origin.lat + north,
|
|
99
|
+
lng: origin.lng + east,
|
|
100
|
+
alt: origin.alt
|
|
101
|
+
)
|
|
102
|
+
end
|
|
53
103
|
end
|
|
54
104
|
end
|
|
55
105
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "polygon"
|
|
4
|
+
|
|
5
|
+
module Geodetic
|
|
6
|
+
module Areas
|
|
7
|
+
# Base class for regular polygons (equal sides, equal angles).
|
|
8
|
+
# Subclasses set SIDES and inherit vertex generation.
|
|
9
|
+
class RegularPolygon < Polygon
|
|
10
|
+
attr_reader :center, :radius, :bearing
|
|
11
|
+
|
|
12
|
+
def initialize(center:, radius:, bearing: 0)
|
|
13
|
+
@center = center.is_a?(Coordinate::LLA) ? center : center.to_lla
|
|
14
|
+
@radius = radius.to_f
|
|
15
|
+
@bearing = bearing.to_f
|
|
16
|
+
|
|
17
|
+
raise ArgumentError, "radius must be positive" unless @radius > 0
|
|
18
|
+
|
|
19
|
+
super(boundary: generate_vertices, validate: false)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def sides
|
|
23
|
+
self.class::SIDES
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def generate_vertices
|
|
29
|
+
n = self.class::SIDES
|
|
30
|
+
step = 360.0 / n
|
|
31
|
+
|
|
32
|
+
n.times.map do |i|
|
|
33
|
+
angle = @bearing + step * i
|
|
34
|
+
destination(@center, @radius, angle)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Compute a destination point given start, distance (meters), and bearing (degrees).
|
|
39
|
+
# Uses a simplified flat-earth projection adequate for polygon-scale distances.
|
|
40
|
+
def destination(origin, distance_m, bearing_deg)
|
|
41
|
+
bearing_rad = bearing_deg * Geodetic::RAD_PER_DEG
|
|
42
|
+
lat_rad = origin.lat * Geodetic::RAD_PER_DEG
|
|
43
|
+
|
|
44
|
+
# Approximate meters per degree at this latitude
|
|
45
|
+
m_per_deg_lat = 111_320.0
|
|
46
|
+
m_per_deg_lng = 111_320.0 * Math.cos(lat_rad)
|
|
47
|
+
|
|
48
|
+
dlat = distance_m * Math.cos(bearing_rad) / m_per_deg_lat
|
|
49
|
+
dlng = distance_m * Math.sin(bearing_rad) / m_per_deg_lng
|
|
50
|
+
|
|
51
|
+
Coordinate::LLA.new(
|
|
52
|
+
lat: origin.lat + dlat,
|
|
53
|
+
lng: origin.lng + dlng,
|
|
54
|
+
alt: origin.alt
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "polygon"
|
|
4
|
+
|
|
5
|
+
module Geodetic
|
|
6
|
+
module Areas
|
|
7
|
+
class Triangle < Polygon
|
|
8
|
+
attr_reader :center, :width, :height, :bearing
|
|
9
|
+
|
|
10
|
+
SIDE_TOLERANCE = 5.0 # meters — accounts for flat-earth projection error
|
|
11
|
+
|
|
12
|
+
# Four construction modes:
|
|
13
|
+
#
|
|
14
|
+
# Isosceles: Triangle.new(center:, width:, height:, bearing:)
|
|
15
|
+
# Equilateral by radius: Triangle.new(center:, radius:, bearing:)
|
|
16
|
+
# Equilateral by side: Triangle.new(center:, side:, bearing:)
|
|
17
|
+
# Arbitrary 3 vertices: Triangle.new(vertices: [p1, p2, p3])
|
|
18
|
+
#
|
|
19
|
+
def initialize(center: nil, width: nil, height: nil, radius: nil, side: nil, bearing: 0, vertices: nil)
|
|
20
|
+
if vertices
|
|
21
|
+
raise ArgumentError, "vertices: cannot be combined with other shape arguments" if center || width || height || radius || side
|
|
22
|
+
|
|
23
|
+
verts = validate_vertices(vertices)
|
|
24
|
+
@center = compute_center(verts)
|
|
25
|
+
@bearing = 0.0
|
|
26
|
+
@width = 0.0
|
|
27
|
+
@height = 0.0
|
|
28
|
+
super(boundary: verts)
|
|
29
|
+
else
|
|
30
|
+
raise ArgumentError, "center is required" unless center
|
|
31
|
+
|
|
32
|
+
@center = center.is_a?(Coordinate::LLA) ? center : center.to_lla
|
|
33
|
+
@bearing = bearing.to_f
|
|
34
|
+
resolve_dimensions(width, height, radius, side)
|
|
35
|
+
|
|
36
|
+
raise ArgumentError, "width must be positive" unless @width > 0
|
|
37
|
+
raise ArgumentError, "height must be positive" unless @height > 0
|
|
38
|
+
|
|
39
|
+
super(boundary: generate_vertices, validate: false)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def sides
|
|
44
|
+
3
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns the three vertices.
|
|
48
|
+
def vertices
|
|
49
|
+
boundary[0..2]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Base length (same as width). Returns nil for arbitrary vertex triangles.
|
|
53
|
+
def base
|
|
54
|
+
@width > 0 ? @width : nil
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# True when all three side lengths are equal (within tolerance).
|
|
58
|
+
def equilateral?
|
|
59
|
+
a, b, c = side_lengths
|
|
60
|
+
(a - b).abs < SIDE_TOLERANCE &&
|
|
61
|
+
(b - c).abs < SIDE_TOLERANCE
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# True when exactly two side lengths are equal (within tolerance).
|
|
65
|
+
def isosceles?
|
|
66
|
+
a, b, c = side_lengths
|
|
67
|
+
pairs = [
|
|
68
|
+
(a - b).abs < SIDE_TOLERANCE,
|
|
69
|
+
(b - c).abs < SIDE_TOLERANCE,
|
|
70
|
+
(a - c).abs < SIDE_TOLERANCE
|
|
71
|
+
]
|
|
72
|
+
equal_count = pairs.count(true)
|
|
73
|
+
equal_count == 1
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# True when no two side lengths are equal (within tolerance).
|
|
77
|
+
def scalene?
|
|
78
|
+
!equilateral? && !isosceles?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the three side lengths in meters as [ab, bc, ca].
|
|
82
|
+
def side_lengths
|
|
83
|
+
v = vertices
|
|
84
|
+
[
|
|
85
|
+
v[0].distance_to(v[1]).meters,
|
|
86
|
+
v[1].distance_to(v[2]).meters,
|
|
87
|
+
v[2].distance_to(v[0]).meters
|
|
88
|
+
]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Returns the axis-aligned bounding box that encloses this triangle.
|
|
92
|
+
def to_bounding_box
|
|
93
|
+
lats = vertices.map(&:lat)
|
|
94
|
+
lngs = vertices.map(&:lng)
|
|
95
|
+
|
|
96
|
+
BoundingBox.new(
|
|
97
|
+
nw: Coordinate::LLA.new(lat: lats.max, lng: lngs.min, alt: 0),
|
|
98
|
+
se: Coordinate::LLA.new(lat: lats.min, lng: lngs.max, alt: 0)
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def validate_vertices(verts)
|
|
105
|
+
raise ArgumentError, "exactly 3 vertices required" unless verts.size == 3
|
|
106
|
+
|
|
107
|
+
verts.map { |v| v.is_a?(Coordinate::LLA) ? v : v.to_lla }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def compute_center(verts)
|
|
111
|
+
Coordinate::LLA.new(
|
|
112
|
+
lat: verts.sum(&:lat) / 3.0,
|
|
113
|
+
lng: verts.sum(&:lng) / 3.0,
|
|
114
|
+
alt: verts.sum(&:alt) / 3.0
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def resolve_dimensions(width, height, radius, side)
|
|
119
|
+
given = { width: width, height: height, radius: radius, side: side }
|
|
120
|
+
.compact
|
|
121
|
+
|
|
122
|
+
if given.key?(:radius)
|
|
123
|
+
raise ArgumentError, "radius cannot be combined with width, height, or side" if given.size > 1
|
|
124
|
+
|
|
125
|
+
r = radius.to_f
|
|
126
|
+
raise ArgumentError, "radius must be positive" unless r > 0
|
|
127
|
+
|
|
128
|
+
@width = r * Math.sqrt(3)
|
|
129
|
+
@height = r * 1.5
|
|
130
|
+
|
|
131
|
+
elsif given.key?(:side)
|
|
132
|
+
raise ArgumentError, "side cannot be combined with width or height" if given.size > 1
|
|
133
|
+
|
|
134
|
+
s = side.to_f
|
|
135
|
+
raise ArgumentError, "side must be positive" unless s > 0
|
|
136
|
+
|
|
137
|
+
@width = s
|
|
138
|
+
@height = s * Math.sqrt(3) / 2.0
|
|
139
|
+
|
|
140
|
+
elsif given.key?(:width) && given.key?(:height)
|
|
141
|
+
raise ArgumentError, "width/height cannot be combined with radius or side" if given.size > 2
|
|
142
|
+
|
|
143
|
+
@width = width.to_f
|
|
144
|
+
@height = height.to_f
|
|
145
|
+
|
|
146
|
+
else
|
|
147
|
+
raise ArgumentError, "provide width: + height:, radius:, side:, or vertices:"
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def generate_vertices
|
|
152
|
+
half_w = @width / 2.0
|
|
153
|
+
half_h = @height / 2.0
|
|
154
|
+
|
|
155
|
+
[
|
|
156
|
+
vertex(-half_w, -half_h), # base-left
|
|
157
|
+
vertex( half_w, -half_h), # base-right
|
|
158
|
+
vertex( 0.0, half_h) # apex
|
|
159
|
+
]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def vertex(x_offset, y_offset)
|
|
163
|
+
bearing_rad = @bearing * Geodetic::RAD_PER_DEG
|
|
164
|
+
lat_rad = @center.lat * Geodetic::RAD_PER_DEG
|
|
165
|
+
|
|
166
|
+
m_per_deg_lat = 111_320.0
|
|
167
|
+
m_per_deg_lng = 111_320.0 * Math.cos(lat_rad)
|
|
168
|
+
|
|
169
|
+
north = y_offset * Math.cos(bearing_rad) - x_offset * Math.sin(bearing_rad)
|
|
170
|
+
east = y_offset * Math.sin(bearing_rad) + x_offset * Math.cos(bearing_rad)
|
|
171
|
+
|
|
172
|
+
Coordinate::LLA.new(
|
|
173
|
+
lat: @center.lat + north / m_per_deg_lat,
|
|
174
|
+
lng: @center.lng + east / m_per_deg_lng,
|
|
175
|
+
alt: @center.alt
|
|
176
|
+
)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
data/lib/geodetic/areas.rb
CHANGED
|
@@ -2,4 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "areas/circle"
|
|
4
4
|
require_relative "areas/polygon"
|
|
5
|
+
require_relative "areas/bounding_box"
|
|
6
|
+
require_relative "areas/regular_polygon"
|
|
7
|
+
require_relative "areas/triangle"
|
|
5
8
|
require_relative "areas/rectangle"
|
|
9
|
+
require_relative "areas/pentagon"
|
|
10
|
+
require_relative "areas/hexagon"
|
|
11
|
+
require_relative "areas/octagon"
|
|
@@ -89,7 +89,7 @@ module Geodetic
|
|
|
89
89
|
bb = self.class.send(:decode_bounds, @geohash)
|
|
90
90
|
nw = LLA.new(lat: bb[:max_lat], lng: bb[:min_lng], alt: 0.0)
|
|
91
91
|
se = LLA.new(lat: bb[:min_lat], lng: bb[:max_lng], alt: 0.0)
|
|
92
|
-
Areas::
|
|
92
|
+
Areas::BoundingBox.new(nw: nw, se: se)
|
|
93
93
|
end
|
|
94
94
|
|
|
95
95
|
# Uses formula-based precision instead of bounds-based
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
#
|
|
12
12
|
# Key differences from other spatial hashes:
|
|
13
13
|
# - Cells are hexagons (6 vertices), not rectangles
|
|
14
|
-
# - to_area returns Areas::Polygon, not Areas::
|
|
14
|
+
# - to_area returns Areas::Polygon, not Areas::BoundingBox
|
|
15
15
|
# - neighbors returns an Array (6 cells), not a directional Hash
|
|
16
16
|
# - "precision" maps to H3 resolution (0-15), not string length
|
|
17
17
|
#
|
|
@@ -309,12 +309,12 @@ module Geodetic
|
|
|
309
309
|
end
|
|
310
310
|
end
|
|
311
311
|
|
|
312
|
-
# Returns the cell as an Areas::
|
|
312
|
+
# Returns the cell as an Areas::BoundingBox
|
|
313
313
|
def to_area
|
|
314
314
|
bb = decode_bounds(code_value)
|
|
315
315
|
nw = LLA.new(lat: bb[:max_lat], lng: bb[:min_lng], alt: 0.0)
|
|
316
316
|
se = LLA.new(lat: bb[:min_lat], lng: bb[:max_lng], alt: 0.0)
|
|
317
|
-
Areas::
|
|
317
|
+
Areas::BoundingBox.new(nw: nw, se: se)
|
|
318
318
|
end
|
|
319
319
|
|
|
320
320
|
# Returns precision in meters as {lat:, lng:}
|