easy_geometry 0.1.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 +7 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +26 -0
- data/README +2 -0
- data/easy_geometry.gemspec +17 -0
- data/lib/.ruby-version +1 -0
- data/lib/easy_geometry.rb +12 -0
- data/lib/easy_geometry/d2/line.rb +73 -0
- data/lib/easy_geometry/d2/linear_entity.rb +355 -0
- data/lib/easy_geometry/d2/point.rb +193 -0
- data/lib/easy_geometry/d2/polygon.rb +638 -0
- data/lib/easy_geometry/d2/ray.rb +111 -0
- data/lib/easy_geometry/d2/segment.rb +119 -0
- data/lib/easy_geometry/d2/triangle.rb +375 -0
- data/lib/easy_geometry/d2/vector.rb +71 -0
- data/spec/d2/line_spec.rb +339 -0
- data/spec/d2/point_spec.rb +211 -0
- data/spec/d2/polygon_spec.rb +291 -0
- data/spec/d2/ray_spec.rb +439 -0
- data/spec/d2/segment_spec.rb +453 -0
- data/spec/d2/triangle_spec.rb +224 -0
- data/spec/d2/vector_spec.rb +91 -0
- data/spec/spec_helper.rb +101 -0
- metadata +88 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
module EasyGeometry
|
|
2
|
+
module D2
|
|
3
|
+
# A point in a 2-dimensional Euclidean space.
|
|
4
|
+
class Point
|
|
5
|
+
attr_reader :x, :y
|
|
6
|
+
|
|
7
|
+
EQUITY_TOLERANCE = 0.0000000000001
|
|
8
|
+
|
|
9
|
+
def initialize(x, y)
|
|
10
|
+
@x = x; @y = y
|
|
11
|
+
|
|
12
|
+
validate!
|
|
13
|
+
converting_to_rational!
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Project the point 'a' onto the line between the origin
|
|
17
|
+
# and point 'b' along the normal direction.
|
|
18
|
+
#
|
|
19
|
+
# Parameters:
|
|
20
|
+
# Point, Point
|
|
21
|
+
#
|
|
22
|
+
# Returns:
|
|
23
|
+
# Point
|
|
24
|
+
#
|
|
25
|
+
def self.project(a, b)
|
|
26
|
+
unless a.is_a?(Point) && b.is_a?(Point)
|
|
27
|
+
raise TypeError, "Project between #{ a.class } and #{ b.class } is not defined"
|
|
28
|
+
end
|
|
29
|
+
raise ArgumentError, "Cannot project to the zero vector" if b.zero?
|
|
30
|
+
|
|
31
|
+
b * a.dot(b) / b.dot(b)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns:
|
|
35
|
+
# true if there exists a line that contains `points`,
|
|
36
|
+
# or if no points are given.
|
|
37
|
+
# false otherwise.
|
|
38
|
+
#
|
|
39
|
+
def self.is_collinear?(*points)
|
|
40
|
+
# raise TypeError, 'Args should be a Points' unless points.detect { |p| !p.is_a?(Point) }.nil?
|
|
41
|
+
Point.affine_rank(*points.uniq) <= 1
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The affine rank of a set of points is the dimension
|
|
45
|
+
# of the smallest affine space containing all the points.
|
|
46
|
+
#
|
|
47
|
+
# For example, if the points lie on a line (and are not all
|
|
48
|
+
# the same) their affine rank is 1.
|
|
49
|
+
# If the points lie on a plane but not a line, their affine rank is 2.
|
|
50
|
+
# By convention, the empty set has affine rank -1.
|
|
51
|
+
def self.affine_rank(*points)
|
|
52
|
+
raise TypeError, 'Args should be a Points' unless points.detect { |p| !p.is_a?(Point) }.nil?
|
|
53
|
+
return -1 if points.length == 0
|
|
54
|
+
|
|
55
|
+
origin = points[0]
|
|
56
|
+
points = points[1..-1].map {|p| p - origin}
|
|
57
|
+
|
|
58
|
+
Matrix[*points.map {|p| [p.x, p.y]}].rank
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Dot product, also known as inner product or scalar product.
|
|
62
|
+
def dot(other)
|
|
63
|
+
raise TypeError, "Scalar (dot) product between Point and #{ other.class } is not defined" unless other.is_a?(Point)
|
|
64
|
+
x * other.x + y * other.y
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# True if every coordinate is zero, False if any coordinate is not zero.
|
|
68
|
+
def zero?
|
|
69
|
+
return true if x.zero? && y.zero?
|
|
70
|
+
return false
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Compare self and other Point.
|
|
74
|
+
def ==(other)
|
|
75
|
+
return false unless other.is_a?(Point)
|
|
76
|
+
(x - other.x).abs < EQUITY_TOLERANCE && (y - other.y).abs < EQUITY_TOLERANCE
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Subtraction of two points.
|
|
80
|
+
def -(other)
|
|
81
|
+
raise TypeError, "Subtract between Point and #{ other.class } is not defined" unless other.is_a?(Point)
|
|
82
|
+
Point.new(self.x - other.x, self.y - other.y)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Addition of two points.
|
|
86
|
+
def +(other)
|
|
87
|
+
raise TypeError, "Addition between Point and #{ other.class } is not defined" unless other.is_a?(Point)
|
|
88
|
+
Point.new(self.x + other.x, self.y + other.y)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Multiplication of point and number.
|
|
92
|
+
def *(scalar)
|
|
93
|
+
raise TypeError, "Multiplication between Point and #{ scalar.class } is not defined" unless scalar.is_a?(Numeric)
|
|
94
|
+
Point.new(x * scalar, y * scalar)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Dividing of point and number.
|
|
98
|
+
def /(scalar)
|
|
99
|
+
raise TypeError, "Dividing between Point and #{ scalar.class } is not defined" unless scalar.is_a?(Numeric)
|
|
100
|
+
Point.new(x / scalar, y / scalar)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def <=>(other)
|
|
104
|
+
return self.y <=> other.y if self.x == other.x
|
|
105
|
+
self.x <=> other.x
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns the distance between this point and the origin.
|
|
109
|
+
def abs
|
|
110
|
+
self.distance(Point.new(0, 0))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Distance between self and another geometry entity.
|
|
114
|
+
#
|
|
115
|
+
# Parameters:
|
|
116
|
+
# geometry_entity
|
|
117
|
+
#
|
|
118
|
+
# Returns:
|
|
119
|
+
# int
|
|
120
|
+
#
|
|
121
|
+
def distance(other)
|
|
122
|
+
if other.is_a?(Point)
|
|
123
|
+
return distance_between_points(self, other)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if other.respond_to?(:distance)
|
|
127
|
+
return other.distance(self)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
raise TypeError, "Distance between Point and #{ other.class } is not defined"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Intersection between point and another geometry entity.
|
|
134
|
+
#
|
|
135
|
+
# Parameters:
|
|
136
|
+
# geometry_entity
|
|
137
|
+
#
|
|
138
|
+
# Returns:
|
|
139
|
+
# Array of Points
|
|
140
|
+
#
|
|
141
|
+
def intersection(other)
|
|
142
|
+
if other.is_a?(Point)
|
|
143
|
+
return points_intersection(self, other)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
if other.respond_to?(:intersection)
|
|
147
|
+
return other.intersection(self)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
raise TypeError, "Intersection between Point and #{ other.class } is not defined"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# The midpoint between self and another point.
|
|
154
|
+
#
|
|
155
|
+
# Parameters:
|
|
156
|
+
# Point
|
|
157
|
+
#
|
|
158
|
+
# Returns:
|
|
159
|
+
# Point
|
|
160
|
+
#
|
|
161
|
+
def midpoint(other)
|
|
162
|
+
raise TypeError, "Midpoint between Point and #{ other.class } is not defined" unless other.is_a?(Point)
|
|
163
|
+
|
|
164
|
+
Point.new(
|
|
165
|
+
(self.x + other.x) / 2,
|
|
166
|
+
(self.y + other.y) / 2
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
private
|
|
171
|
+
|
|
172
|
+
def points_intersection(p1, p2)
|
|
173
|
+
return [p1] if p1 == p2
|
|
174
|
+
return []
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def distance_between_points(p1, p2)
|
|
178
|
+
# AB = √(x2 - x1)**2 + (y2 - y1)**2
|
|
179
|
+
Math.sqrt( (p2.x - p1.x)**2 + (p2.y - p1.y)**2 )
|
|
180
|
+
# Math.hypot((p2.x - p1.x), (p2.y - p1.y))
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def validate!
|
|
184
|
+
raise TypeError, 'Coords should be numbers' if !x.is_a?(Numeric) || !y.is_a?(Numeric)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def converting_to_rational!
|
|
188
|
+
@x = Rational(x.to_s) unless x.is_a?(Rational)
|
|
189
|
+
@y = Rational(y.to_s) unless y.is_a?(Rational)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
module EasyGeometry
|
|
2
|
+
module D2
|
|
3
|
+
# A two-dimensional polygon.
|
|
4
|
+
# A simple polygon in space. Can be constructed from a sequence of points
|
|
5
|
+
#
|
|
6
|
+
# Polygons are treated as closed paths rather than 2D areas so
|
|
7
|
+
# some calculations can be be negative or positive (e.g., area)
|
|
8
|
+
# based on the orientation of the points.
|
|
9
|
+
|
|
10
|
+
# Any consecutive identical points are reduced to a single point
|
|
11
|
+
# and any points collinear and between two points will be removed
|
|
12
|
+
# unless they are needed to define an explicit intersection (see specs).
|
|
13
|
+
|
|
14
|
+
# Must be at least 4 points
|
|
15
|
+
class Polygon
|
|
16
|
+
attr_reader :vertices
|
|
17
|
+
|
|
18
|
+
def initialize(*args)
|
|
19
|
+
@vertices = preprocessing_args(args)
|
|
20
|
+
remove_consecutive_duplicates
|
|
21
|
+
remove_collinear_points
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Return True/False for cw/ccw orientation.
|
|
25
|
+
def self.is_right?(a, b, c)
|
|
26
|
+
raise TypeError, 'Must pass only Point objects' unless a.is_a?(Point)
|
|
27
|
+
raise TypeError, 'Must pass only Point objects' unless b.is_a?(Point)
|
|
28
|
+
raise TypeError, 'Must pass only Point objects' unless c.is_a?(Point)
|
|
29
|
+
|
|
30
|
+
ba = b - a
|
|
31
|
+
ca = c - a
|
|
32
|
+
t_area = ba.x * ca.y - ca.x * ba.y
|
|
33
|
+
|
|
34
|
+
t_area <= 0
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns True if self and other are the same mathematical entities
|
|
38
|
+
def ==(other)
|
|
39
|
+
return false unless other.is_a?(Polygon)
|
|
40
|
+
self.hashable_content == other.hashable_content
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The area of the polygon.
|
|
44
|
+
# The area calculation can be positive or negative based on the
|
|
45
|
+
# orientation of the points. If any side of the polygon crosses
|
|
46
|
+
# any other side, there will be areas having opposite signs.
|
|
47
|
+
def area
|
|
48
|
+
return @area if defined?(@area)
|
|
49
|
+
|
|
50
|
+
sum = 0.0
|
|
51
|
+
(0...vertices.length).each do |i|
|
|
52
|
+
prev = vertices[i - 1]
|
|
53
|
+
curr = vertices[i]
|
|
54
|
+
|
|
55
|
+
sum += ((prev.x * curr.y) - (prev.y * curr.x))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@area = sum / 2
|
|
59
|
+
@area
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The perimeter of the polygon.
|
|
63
|
+
def perimeter
|
|
64
|
+
return @perimeter if defined?(@perimeter)
|
|
65
|
+
|
|
66
|
+
@perimeter = 0.0
|
|
67
|
+
(0...vertices.length).each do |i|
|
|
68
|
+
@perimeter += vertices[i - 1].distance(vertices[i])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
@perimeter
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The centroid of the polygon.
|
|
75
|
+
#
|
|
76
|
+
# Returns
|
|
77
|
+
# Point
|
|
78
|
+
#
|
|
79
|
+
def centroid
|
|
80
|
+
return @centroid if defined?(@centroid)
|
|
81
|
+
|
|
82
|
+
cx, cy = 0, 0
|
|
83
|
+
|
|
84
|
+
(0...vertices.length).each do |i|
|
|
85
|
+
prev = vertices[i - 1]
|
|
86
|
+
curr = vertices[i]
|
|
87
|
+
|
|
88
|
+
v = prev.x * curr.y - curr.x * prev.y
|
|
89
|
+
cx += v * (prev.x + curr.x)
|
|
90
|
+
cy += v * (prev.y + curr.y)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
@centroid = Point.new(Rational(cx, 6 * self.area), Rational(cy, 6 * self.area))
|
|
94
|
+
@centroid
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The directed line segments that form the sides of the polygon.
|
|
98
|
+
#
|
|
99
|
+
# Returns
|
|
100
|
+
# Array of Segments
|
|
101
|
+
#
|
|
102
|
+
def sides
|
|
103
|
+
return @sides if defined?(@sides)
|
|
104
|
+
|
|
105
|
+
@sides = []
|
|
106
|
+
(-vertices.length...0).each do |i|
|
|
107
|
+
@sides << Segment.new(vertices[i], vertices[i + 1])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
@sides
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Return an array (xmin, ymin, xmax, ymax) representing the bounding
|
|
114
|
+
# rectangle for the geometric figure.
|
|
115
|
+
#
|
|
116
|
+
# Returns:
|
|
117
|
+
# array
|
|
118
|
+
#
|
|
119
|
+
def bounds
|
|
120
|
+
return @bounds if defined?(@bounds)
|
|
121
|
+
|
|
122
|
+
xs = vertices.map(&:x)
|
|
123
|
+
ys = vertices.map(&:y)
|
|
124
|
+
@bounds = [xs.min, ys.min, xs.max, ys.max]
|
|
125
|
+
|
|
126
|
+
@bounds
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Is the polygon convex?
|
|
130
|
+
# A polygon is convex if all its interior angles are less than 180
|
|
131
|
+
# degrees and there are no intersections between sides.
|
|
132
|
+
#
|
|
133
|
+
# Returns
|
|
134
|
+
# True if this polygon is convex
|
|
135
|
+
# False otherwise.
|
|
136
|
+
#
|
|
137
|
+
def is_convex?
|
|
138
|
+
cw = Polygon.is_right?(vertices[-2], vertices[-1], vertices[0])
|
|
139
|
+
(1...vertices.length).each do |i|
|
|
140
|
+
if cw ^ Polygon.is_right?(vertices[i - 2], vertices[i - 1], vertices[i])
|
|
141
|
+
return false
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# check for intersecting sides
|
|
146
|
+
sides = self.sides
|
|
147
|
+
sides.each_with_index do |si, i|
|
|
148
|
+
points = [si.p1, si.p2]
|
|
149
|
+
|
|
150
|
+
first_number = 0
|
|
151
|
+
first_number = 1 if i == sides.length - 1
|
|
152
|
+
(first_number...i - 1).each do |j|
|
|
153
|
+
sj = sides[j]
|
|
154
|
+
if !points.include?(sj.p1) && !points.include?(sj.p2)
|
|
155
|
+
hit = si.intersection(sj)
|
|
156
|
+
return false if !hit.empty?
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
return true
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Return True if p is enclosed by (is inside of) self, False otherwise.
|
|
165
|
+
# Being on the border of self is considered False.
|
|
166
|
+
#
|
|
167
|
+
# Parameters:
|
|
168
|
+
# Point
|
|
169
|
+
#
|
|
170
|
+
# Returns:
|
|
171
|
+
# bool
|
|
172
|
+
#
|
|
173
|
+
# http://paulbourke.net/geometry/polygonmesh/#insidepoly
|
|
174
|
+
def is_encloses_point?(point)
|
|
175
|
+
point = Point.new(point[0], point[1]) if point.is_a?(Array)
|
|
176
|
+
raise TypeError, 'Must pass only Point objects' unless point.is_a?(Point)
|
|
177
|
+
|
|
178
|
+
return false if vertices.include?(point)
|
|
179
|
+
|
|
180
|
+
sides.each do |s|
|
|
181
|
+
return false if s.contains?(point)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# move to point, checking that the result is numeric
|
|
185
|
+
lit = []
|
|
186
|
+
vertices.each do |v|
|
|
187
|
+
lit << v - point
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
poly = Polygon.new(*lit)
|
|
191
|
+
# polygon closure is assumed in the following test but Polygon removes duplicate pts so
|
|
192
|
+
# the last point has to be added so all sides are computed. Using Polygon.sides is
|
|
193
|
+
# not good since Segments are unordered.
|
|
194
|
+
args = poly.vertices
|
|
195
|
+
indices = (-args.length..0).to_a
|
|
196
|
+
|
|
197
|
+
if poly.is_convex?
|
|
198
|
+
orientation = nil
|
|
199
|
+
indices.each do |i|
|
|
200
|
+
a = args[i]
|
|
201
|
+
b = args[i + 1]
|
|
202
|
+
test = ((-a.y)*(b.x - a.x) - (-a.x)*(b.y - a.y)) < 0
|
|
203
|
+
|
|
204
|
+
if orientation.nil?
|
|
205
|
+
orientation = test
|
|
206
|
+
elsif test != orientation
|
|
207
|
+
return false
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
return true
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
hit_odd = false
|
|
215
|
+
p1x, p1y = args[0].x, args[0].y
|
|
216
|
+
indices[1..-1].each do |i|
|
|
217
|
+
p2x, p2y = args[i].x, args[i].y
|
|
218
|
+
|
|
219
|
+
if [p1y, p2y].min < 0 && [p1y, p2y].max >= 0 &&
|
|
220
|
+
[p1x, p2x].max >= 0 && p1y != p2y
|
|
221
|
+
|
|
222
|
+
xinters = (-p1y)*(p2x - p1x)/(p2y - p1y) + p1x
|
|
223
|
+
hit_odd = !hit_odd if p1x == p2x or 0 <= xinters
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
p1x, p1y = p2x, p2y
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
return hit_odd
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# The intersection of polygon and geometry entity.
|
|
233
|
+
#
|
|
234
|
+
# The intersection may be empty and can contain individual Points and
|
|
235
|
+
# complete Line Segments.
|
|
236
|
+
def intersection(other)
|
|
237
|
+
intersection_result = []
|
|
238
|
+
|
|
239
|
+
if other.is_a?(Polygon)
|
|
240
|
+
k = other.sides
|
|
241
|
+
else
|
|
242
|
+
k = [other]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
self.sides.each do |side|
|
|
246
|
+
k.each do |side1|
|
|
247
|
+
intersection_result += side.intersection(side1)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
intersection_result.uniq! do |a|
|
|
252
|
+
if a.is_a?(Point)
|
|
253
|
+
[a.x, a.y]
|
|
254
|
+
else
|
|
255
|
+
[a.p1, a.p2].sort_by {|p| [p.x, p.y]}
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
points = []; segments = []
|
|
259
|
+
|
|
260
|
+
intersection_result.each do |entity|
|
|
261
|
+
points << entity if entity.is_a?(Point)
|
|
262
|
+
segments << entity if entity.is_a?(Segment)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
if !points.empty? && !segments.empty?
|
|
266
|
+
points_in_segments = []
|
|
267
|
+
|
|
268
|
+
points.each do |point|
|
|
269
|
+
segments.each do |segment|
|
|
270
|
+
points_in_segments << point if segment.contains?(point)
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
points_in_segments.uniq! {|a| [a.x, a.y]}
|
|
275
|
+
if !points_in_segments.empty?
|
|
276
|
+
points_in_segments.each do |p|
|
|
277
|
+
points.delete(p)
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
return points.sort + segments.sort
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
return intersection_result.sort
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Returns the shortest distance between self and other.
|
|
288
|
+
#
|
|
289
|
+
# If other is a point, then self does not need to be convex.
|
|
290
|
+
# If other is another polygon self and other must be convex.
|
|
291
|
+
def distance(other)
|
|
292
|
+
other = Point.new(other[0], other[1]) if other.is_a?(Array)
|
|
293
|
+
|
|
294
|
+
if other.is_a?(Point)
|
|
295
|
+
dist = BigDecimal('Infinity')
|
|
296
|
+
|
|
297
|
+
sides.each do |side|
|
|
298
|
+
current = side.distance(other)
|
|
299
|
+
if current == 0
|
|
300
|
+
return 0
|
|
301
|
+
elsif current < dist
|
|
302
|
+
dist = current
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
return dist
|
|
307
|
+
|
|
308
|
+
elsif other.is_a?(Polygon) && self.is_convex? && other.is_convex?
|
|
309
|
+
return do_poly_distance(other)
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
raise TypeError, "Distance not handled for #{ other.class }"
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def hashable_content
|
|
316
|
+
d = {}
|
|
317
|
+
|
|
318
|
+
s1 = ref_list(self.vertices, d)
|
|
319
|
+
r_nor = rotate_left(s1, least_rotation(s1))
|
|
320
|
+
|
|
321
|
+
s2 = ref_list(self.vertices.reverse, d)
|
|
322
|
+
r_rev = rotate_left(s2, least_rotation(s2))
|
|
323
|
+
|
|
324
|
+
if (r_nor <=> r_rev) == -1
|
|
325
|
+
r = r_nor
|
|
326
|
+
else
|
|
327
|
+
r = r_rev
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
r.map {|order| d[order]}
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
private
|
|
334
|
+
|
|
335
|
+
# Calculates the least distance between the exteriors of two
|
|
336
|
+
# convex polygons e1 and e2. Does not check for the convexity
|
|
337
|
+
# of the polygons as this is checked by Polygon.#distance.
|
|
338
|
+
#
|
|
339
|
+
# Method:
|
|
340
|
+
# [1] http://cgm.cs.mcgill.ca/~orm/mind2p.html
|
|
341
|
+
# Uses rotating calipers:
|
|
342
|
+
# [2] https://en.wikipedia.org/wiki/Rotating_calipers
|
|
343
|
+
# and antipodal points:
|
|
344
|
+
# [3] https://en.wikipedia.org/wiki/Antipodal_point
|
|
345
|
+
def do_poly_distance(e2)
|
|
346
|
+
e1 = self
|
|
347
|
+
|
|
348
|
+
# Tests for a possible intersection between the polygons and outputs a warning
|
|
349
|
+
e1_center = e1.centroid
|
|
350
|
+
e2_center = e2.centroid
|
|
351
|
+
e1_max_radius = Rational(0)
|
|
352
|
+
e2_max_radius = Rational(0)
|
|
353
|
+
|
|
354
|
+
e1.vertices.each do |vertex|
|
|
355
|
+
r = e1_center.distance(vertex)
|
|
356
|
+
e1_max_radius = r if e1_max_radius < r
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
e2.vertices.each do |vertex|
|
|
360
|
+
r = e2_center.distance(vertex)
|
|
361
|
+
e2_max_radius = r if e2_max_radius < r
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
center_dist = e1_center.distance(e2_center)
|
|
365
|
+
if center_dist <= e1_max_radius + e2_max_radius
|
|
366
|
+
puts "Polygons may intersect producing erroneous output"
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Find the upper rightmost vertex of e1 and the lowest leftmost vertex of e2
|
|
370
|
+
e1_ymax = e1.vertices.first
|
|
371
|
+
e2_ymin = e2.vertices.first
|
|
372
|
+
|
|
373
|
+
e1.vertices.each do |vertex|
|
|
374
|
+
if vertex.y > e1_ymax.y || (vertex.y == e1_ymax.y && vertex.x > e1_ymax.x)
|
|
375
|
+
e1_ymax = vertex
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
e2.vertices.each do |vertex|
|
|
380
|
+
if vertex.y < e2_ymin.y || (vertex.y == e2_ymin.y && vertex.x < e2_ymin.x)
|
|
381
|
+
e2_ymin = vertex
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
min_dist = e1_ymax.distance(e2_ymin)
|
|
386
|
+
|
|
387
|
+
# Produce a dictionary with vertices of e1 as the keys and, for each vertex, the points
|
|
388
|
+
# to which the vertex is connected as its value. The same is then done for e2.
|
|
389
|
+
|
|
390
|
+
e1_connections = {}
|
|
391
|
+
e2_connections = {}
|
|
392
|
+
|
|
393
|
+
e1.sides.each do |side|
|
|
394
|
+
if e1_connections[side.p1].nil?
|
|
395
|
+
e1_connections[side.p1] = [side.p2]
|
|
396
|
+
else
|
|
397
|
+
e1_connections[side.p1] << side.p2
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
if e1_connections[side.p2].nil?
|
|
401
|
+
e1_connections[side.p2] = [side.p1]
|
|
402
|
+
else
|
|
403
|
+
e1_connections[side.p2] << side.p1
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
e2.sides.each do |side|
|
|
408
|
+
if e2_connections[side.p1].nil?
|
|
409
|
+
e2_connections[side.p1] = [side.p2]
|
|
410
|
+
else
|
|
411
|
+
e2_connections[side.p1] << side.p2
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if e2_connections[side.p2].nil?
|
|
415
|
+
e2_connections[side.p2] = [side.p1]
|
|
416
|
+
else
|
|
417
|
+
e2_connections[side.p2] << side.p1
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
e1_current = e1_ymax
|
|
422
|
+
e2_current = e2_ymin
|
|
423
|
+
support_line = Line.new([0, 0], [1, 0])
|
|
424
|
+
|
|
425
|
+
# Determine which point in e1 and e2 will be selected after e2_ymin and e1_ymax,
|
|
426
|
+
# this information combined with the above produced dictionaries determines the
|
|
427
|
+
# path that will be taken around the polygons
|
|
428
|
+
|
|
429
|
+
point1 = e1_connections[e1_ymax][0]
|
|
430
|
+
point2 = e1_connections[e1_ymax][1]
|
|
431
|
+
angle1 = support_line.angle_between(Line.new(e1_ymax, point1))
|
|
432
|
+
angle2 = support_line.angle_between(Line.new(e1_ymax, point2))
|
|
433
|
+
|
|
434
|
+
if angle1 < angle2
|
|
435
|
+
e1_next = point1
|
|
436
|
+
elsif angle2 < angle1
|
|
437
|
+
e1_next = point2
|
|
438
|
+
elsif e1_ymax.distance(point1) > e1_ymax.distance(point2)
|
|
439
|
+
e1_next = point2
|
|
440
|
+
else
|
|
441
|
+
e1_next = point1
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
point1 = e2_connections[e2_ymin][0]
|
|
445
|
+
point2 = e2_connections[e2_ymin][1]
|
|
446
|
+
angle1 = support_line.angle_between(Line.new(e2_ymin, point1))
|
|
447
|
+
angle2 = support_line.angle_between(Line.new(e2_ymin, point2))
|
|
448
|
+
|
|
449
|
+
if angle1 > angle2
|
|
450
|
+
e2_next = point1
|
|
451
|
+
elsif angle2 > angle1
|
|
452
|
+
e2_next = point2
|
|
453
|
+
elsif e2_ymin.distance(point1) > e2_ymin.distance(point2)
|
|
454
|
+
e2_next = point2
|
|
455
|
+
else
|
|
456
|
+
e2_next = point1
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
# Loop which determines the distance between anti-podal pairs and updates the
|
|
460
|
+
# minimum distance accordingly. It repeats until it reaches the starting position.
|
|
461
|
+
|
|
462
|
+
while true
|
|
463
|
+
e1_angle = support_line.angle_between(Line.new(e1_current, e1_next))
|
|
464
|
+
e2_angle = Math::PI - support_line.angle_between(Line.new(e2_current, e2_next))
|
|
465
|
+
|
|
466
|
+
if e1_angle < e2_angle
|
|
467
|
+
support_line = Line.new(e1_current, e1_next)
|
|
468
|
+
e1_segment = Segment.new(e1_current, e1_next)
|
|
469
|
+
min_dist_current = e1_segment.distance(e2_current)
|
|
470
|
+
|
|
471
|
+
if min_dist_current < min_dist
|
|
472
|
+
min_dist = min_dist_current
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
if e1_connections[e1_next][0] != e1_current
|
|
476
|
+
e1_current = e1_next
|
|
477
|
+
e1_next = e1_connections[e1_next][0]
|
|
478
|
+
else
|
|
479
|
+
e1_current = e1_next
|
|
480
|
+
e1_next = e1_connections[e1_next][1]
|
|
481
|
+
end
|
|
482
|
+
elsif e1_angle > e2_angle
|
|
483
|
+
support_line = Line.new(e2_next, e2_current)
|
|
484
|
+
e2_segment = Segment.new(e2_current, e2_next)
|
|
485
|
+
min_dist_current = e2_segment.distance(e1_current)
|
|
486
|
+
|
|
487
|
+
if min_dist_current < min_dist
|
|
488
|
+
min_dist = min_dist_current
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
if e2_connections[e2_next][0] != e2_current
|
|
492
|
+
e2_current = e2_next
|
|
493
|
+
e2_next = e2_connections[e2_next][0]
|
|
494
|
+
else
|
|
495
|
+
e2_current = e2_next
|
|
496
|
+
e2_next = e2_connections[e2_next][1]
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
else
|
|
500
|
+
support_line = Line.new(e1_current, e1_next)
|
|
501
|
+
e1_segment = Segment.new(e1_current, e1_next)
|
|
502
|
+
e2_segment = Segment.new(e2_current, e2_next)
|
|
503
|
+
min1 = e1_segment.distance(e2_next)
|
|
504
|
+
min2 = e2_segment.distance(e1_next)
|
|
505
|
+
|
|
506
|
+
min_dist_current = [min1, min2].min
|
|
507
|
+
|
|
508
|
+
if min_dist_current < min_dist
|
|
509
|
+
min_dist = min_dist_current
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
if e1_connections[e1_next][0] != e1_current
|
|
513
|
+
e1_current = e1_next
|
|
514
|
+
e1_next = e1_connections[e1_next][0]
|
|
515
|
+
else
|
|
516
|
+
e1_current = e1_next
|
|
517
|
+
e1_next = e1_connections[e1_next][1]
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
if e2_connections[e2_next][0] != e2_current
|
|
521
|
+
e2_current = e2_next
|
|
522
|
+
e2_next = e2_connections[e2_next][0]
|
|
523
|
+
else
|
|
524
|
+
e2_current = e2_next
|
|
525
|
+
e2_next = e2_connections[e2_next][1]
|
|
526
|
+
end
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
break if e1_current == e1_ymax && e2_current == e2_ymin
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
return min_dist
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def ref_list(point_list, d)
|
|
536
|
+
kee = {}
|
|
537
|
+
|
|
538
|
+
point_list.sort_by {|p| [p.x, p.y]}.each_with_index do |p, i|
|
|
539
|
+
kee[p] = i
|
|
540
|
+
d[i] = p
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
point_list.map {|p| kee[p]}
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Returns the number of steps of left rotation required to
|
|
547
|
+
# obtain lexicographically minimal array.
|
|
548
|
+
# https://en.wikipedia.org/wiki/Lexicographically_minimal_string_rotation
|
|
549
|
+
def least_rotation(x)
|
|
550
|
+
s = x + x # Concatenate arrays to it self to avoid modular arithmetic
|
|
551
|
+
f = [-1] * s.length # Failure function
|
|
552
|
+
k = 0 # Least rotation of array found so far
|
|
553
|
+
|
|
554
|
+
(1...s.length).each do |j|
|
|
555
|
+
sj = s[j]
|
|
556
|
+
i = f[j - k - 1]
|
|
557
|
+
|
|
558
|
+
while i != -1 && sj != s[k + i + 1]
|
|
559
|
+
if sj < s[k + i + 1]
|
|
560
|
+
k = j-i-1
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
i = f[i]
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
if sj != s[k + i + 1]
|
|
567
|
+
if sj < s[k]
|
|
568
|
+
k = j
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
f[j - k] = -1
|
|
572
|
+
else
|
|
573
|
+
f[j - k] = i + 1
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
return k
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
# Left rotates a list x by the number of steps specified in y.
|
|
582
|
+
def rotate_left(x, y)
|
|
583
|
+
return [] if x.length == 0
|
|
584
|
+
|
|
585
|
+
y = y % x.length
|
|
586
|
+
x[y..-1] + x[0...y]
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
# preprocessing_args - convert coordinates to points if necessary.
|
|
590
|
+
def preprocessing_args(args)
|
|
591
|
+
args.map do |v|
|
|
592
|
+
if v.is_a?(Array) && v.length == 2
|
|
593
|
+
Point.new(*v)
|
|
594
|
+
elsif v.is_a?(Point)
|
|
595
|
+
v
|
|
596
|
+
else
|
|
597
|
+
raise TypeError, "Arguments should be arrays with coordinates or Points."
|
|
598
|
+
end
|
|
599
|
+
end
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def remove_consecutive_duplicates
|
|
603
|
+
nodup = []
|
|
604
|
+
@vertices.each do |p|
|
|
605
|
+
next if !nodup.empty? && p == nodup[-1]
|
|
606
|
+
nodup << p
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
if nodup.length > 1 && nodup[-1] == nodup[0]
|
|
610
|
+
nodup.pop # last point was same as first
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
@vertices = nodup
|
|
614
|
+
validate
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def remove_collinear_points
|
|
618
|
+
i = 0
|
|
619
|
+
while i < vertices.length
|
|
620
|
+
a, b, c = vertices[i], vertices[i - 1], vertices[i - 2]
|
|
621
|
+
if Point.is_collinear?(a, b, c)
|
|
622
|
+
vertices.delete_at(i - 1)
|
|
623
|
+
vertices.delete_at(i - 2) if a == c
|
|
624
|
+
else
|
|
625
|
+
i += 1
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
validate
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def validate
|
|
633
|
+
raise ArgumentError, 'Number of vertices should be more than 2' if vertices.length < 3
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|