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