winding-polygon 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +2 -0
- data/Rakefile +1 -0
- data/lib/winding-polygon.rb +15 -0
- data/lib/winding-polygon/avltree.rb +287 -0
- data/lib/winding-polygon/event_queue.rb +34 -0
- data/lib/winding-polygon/point.rb +40 -0
- data/lib/winding-polygon/polygon.rb +114 -0
- data/lib/winding-polygon/segment.rb +48 -0
- data/lib/winding-polygon/sweep_line.rb +138 -0
- data/lib/winding-polygon/version.rb +3 -0
- data/spec/event_queue_spec.rb +15 -0
- data/spec/point_spec.rb +43 -0
- data/spec/polygon_spec.rb +189 -0
- data/spec/segment_spec.rb +20 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/sweep_line_spec.rb +22 -0
- data/spec/winding-polygon_spec.rb +27 -0
- data/test/test_avltree.rb +142 -0
- data/winding-polygon.gemspec +25 -0
- metadata +34 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4d430cfe91bb9a92623cb8f79b9ad4b7592b5db5
|
4
|
+
data.tar.gz: 1bcfa936807da46199b6c7a71b1faa0c64e057c8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fe5ee6fb05e9f98dd6cf4b2785043d91de374460f66cf35de8b8911e1b541c2f56cbb8b0fc681f533dbcb617304879b53434984bd0d2412411fb93c5d954506d
|
7
|
+
data.tar.gz: 06e207c51824273756dc7504826b0e2fb513805b2c4c24d5e398b22ab6376cd08080bae5763837489c2793c7d594aa8d57a87ae4b2ba23347387fc02d7b3fbd4
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 TODO: Write your name
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require "winding-polygon/version"
|
2
|
+
|
3
|
+
module WindingPolygon
|
4
|
+
|
5
|
+
def self.decompose(input_polygon, output_polygons)
|
6
|
+
output_polygons = Array.new if output_polygons.nil?
|
7
|
+
result = input_polygon.decompose
|
8
|
+
if result.instance_of?(Array) && result.size==1 && !output_polygons.include?(input_polygon)
|
9
|
+
output_polygons.concat(result)
|
10
|
+
else
|
11
|
+
decompose(result[0],output_polygons )
|
12
|
+
decompose(result[1],output_polygons )
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
class AVLTree
|
2
|
+
attr_accessor :root
|
3
|
+
def initialize(array = [])
|
4
|
+
#create root
|
5
|
+
@root = nil
|
6
|
+
array.each do |n|
|
7
|
+
insert n
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def insert(v)
|
12
|
+
i_node = create_node(v)
|
13
|
+
|
14
|
+
prev_node = nil
|
15
|
+
curr_node = @root
|
16
|
+
while !curr_node.nil?
|
17
|
+
prev_node = curr_node
|
18
|
+
if i_node.value < curr_node.value
|
19
|
+
curr_node = curr_node.left
|
20
|
+
else
|
21
|
+
curr_node = curr_node.right
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if prev_node.nil?
|
26
|
+
@root = i_node
|
27
|
+
else
|
28
|
+
i_node.parent = prev_node
|
29
|
+
if i_node.value < prev_node.value
|
30
|
+
prev_node.left = i_node
|
31
|
+
else
|
32
|
+
prev_node.right = i_node
|
33
|
+
end
|
34
|
+
end
|
35
|
+
i_node.update_height
|
36
|
+
@root = i_node.balance
|
37
|
+
i_node
|
38
|
+
end
|
39
|
+
|
40
|
+
def search(v)
|
41
|
+
search_node(@root, v)
|
42
|
+
end
|
43
|
+
|
44
|
+
def delete(v)
|
45
|
+
d = search(v)
|
46
|
+
return nil if d.nil?
|
47
|
+
|
48
|
+
if d.left == nil or d.right == nil
|
49
|
+
n = d
|
50
|
+
else #both children exist
|
51
|
+
n = d.next
|
52
|
+
end
|
53
|
+
|
54
|
+
if !n.left.nil?
|
55
|
+
b = n.left
|
56
|
+
else
|
57
|
+
b = n.right
|
58
|
+
end
|
59
|
+
|
60
|
+
b.parent = n.parent unless b.nil?
|
61
|
+
|
62
|
+
if n.parent.nil?
|
63
|
+
@root = b
|
64
|
+
elsif n == n.parent.left
|
65
|
+
n.parent.left = b
|
66
|
+
else
|
67
|
+
n.parent.right = b
|
68
|
+
end
|
69
|
+
|
70
|
+
d.value = n.value unless n == d
|
71
|
+
|
72
|
+
if b.nil?
|
73
|
+
n.parent.update_height if !n.parent.nil?
|
74
|
+
@root = n.balance
|
75
|
+
else
|
76
|
+
b.update_height
|
77
|
+
@root = b.balance
|
78
|
+
end
|
79
|
+
n
|
80
|
+
end
|
81
|
+
|
82
|
+
def print
|
83
|
+
result = sort
|
84
|
+
puts result.inspect
|
85
|
+
end
|
86
|
+
|
87
|
+
def sort
|
88
|
+
result = Array.new
|
89
|
+
sort_node(@root, result )
|
90
|
+
result
|
91
|
+
end
|
92
|
+
|
93
|
+
def min
|
94
|
+
@root.min
|
95
|
+
end
|
96
|
+
|
97
|
+
def max
|
98
|
+
@root.max
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
private
|
104
|
+
def create_node(v = nil)
|
105
|
+
n = AVLNode.new(:value => v)
|
106
|
+
end
|
107
|
+
|
108
|
+
def sort_node(n, array)
|
109
|
+
if !n.nil?
|
110
|
+
sort_node(n.left, array)
|
111
|
+
array.push(n.value)
|
112
|
+
sort_node(n.right, array)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def search_node(n, v)
|
117
|
+
if !n.nil?
|
118
|
+
if n.value == v
|
119
|
+
return n
|
120
|
+
elsif v < n.value
|
121
|
+
search_node(n.left,v)
|
122
|
+
else
|
123
|
+
search_node(n.right,v)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class AVLNode
|
130
|
+
BAL_H = 1
|
131
|
+
attr_accessor :height, :left, :right, :value, :parent
|
132
|
+
def initialize args
|
133
|
+
@height = 0
|
134
|
+
@left = nil
|
135
|
+
@right = nil
|
136
|
+
@value = nil
|
137
|
+
@parent = nil
|
138
|
+
args.each do |k,v|
|
139
|
+
instance_variable_set("@#{k}", v) unless v.nil?
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def balance
|
144
|
+
rotate if difference.abs > BAL_H
|
145
|
+
return self if @parent.nil?
|
146
|
+
@parent.balance
|
147
|
+
end
|
148
|
+
|
149
|
+
def update_height
|
150
|
+
l_height = @left.nil? ? 0 : @left.height
|
151
|
+
r_height = @right.nil? ? 0 : @right.height
|
152
|
+
@height = ((l_height > r_height) ? l_height : r_height) + 1
|
153
|
+
@parent.update_height unless @parent.nil?
|
154
|
+
end
|
155
|
+
|
156
|
+
def rotate
|
157
|
+
if difference >= BAL_H
|
158
|
+
#check if children should rotate too
|
159
|
+
@right.rotate if @right.difference <= -BAL_H
|
160
|
+
rotate_left
|
161
|
+
elsif difference <= - BAL_H
|
162
|
+
#check if children should rotate too
|
163
|
+
@left.rotate if @left.difference >= BAL_H
|
164
|
+
rotate_right
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def rotate_left
|
169
|
+
#the old right is now the root
|
170
|
+
root = @right
|
171
|
+
root.parent = @parent
|
172
|
+
#update the parent to point to the new root
|
173
|
+
if not @parent.nil?
|
174
|
+
if @parent.right == self
|
175
|
+
@parent.right = root
|
176
|
+
else
|
177
|
+
@parent.left = root
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
#this node's right is now the root's left
|
182
|
+
@right = root.left
|
183
|
+
root.left.parent = self unless root.left.nil?
|
184
|
+
|
185
|
+
#the root is now the parent of this node
|
186
|
+
@parent = root
|
187
|
+
#this node is now the root's left
|
188
|
+
root.left = self
|
189
|
+
root.left.update_height
|
190
|
+
root
|
191
|
+
end
|
192
|
+
|
193
|
+
def rotate_right
|
194
|
+
root = @left
|
195
|
+
root.parent = @parent
|
196
|
+
#update the parent to point to the new root
|
197
|
+
if not @parent.nil?
|
198
|
+
if @parent.right == self
|
199
|
+
@parent.right = root
|
200
|
+
else
|
201
|
+
@parent.left = root
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
@left = root.right
|
206
|
+
root.right.parent = self unless root.right.nil?
|
207
|
+
|
208
|
+
@parent = root
|
209
|
+
root.right = self
|
210
|
+
root.right.update_height
|
211
|
+
root
|
212
|
+
end
|
213
|
+
|
214
|
+
def difference
|
215
|
+
r_height = @right.nil? ? 0 : @right.height
|
216
|
+
l_height = @left.nil? ? 0 : @left.height
|
217
|
+
r_height - l_height
|
218
|
+
end
|
219
|
+
|
220
|
+
def next
|
221
|
+
if not @right.nil?
|
222
|
+
@right.min
|
223
|
+
else
|
224
|
+
curr_node = self
|
225
|
+
p_node = @parent
|
226
|
+
while p_node != nil && curr_node == p_node.right
|
227
|
+
curr_node = p_node
|
228
|
+
p_node = p_node.parent
|
229
|
+
end
|
230
|
+
p_node
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def prev
|
235
|
+
if not @left.nil?
|
236
|
+
@left.max
|
237
|
+
else
|
238
|
+
curr_node = self
|
239
|
+
p_node = @parent
|
240
|
+
while p_node != nil && curr_node == p_node.left
|
241
|
+
curr_node = p_node
|
242
|
+
p_node = p_node.parent
|
243
|
+
end
|
244
|
+
p_node
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
def min
|
250
|
+
node = self
|
251
|
+
while !node.left.nil?
|
252
|
+
node = node.left
|
253
|
+
end
|
254
|
+
node
|
255
|
+
end
|
256
|
+
|
257
|
+
def max
|
258
|
+
node = self
|
259
|
+
while !node.right.nil?
|
260
|
+
node = node.right
|
261
|
+
end
|
262
|
+
node
|
263
|
+
end
|
264
|
+
|
265
|
+
def print_node
|
266
|
+
out_s = "\t\t\t\t\t\n"
|
267
|
+
out_s += "\t\t#{@parent}\t\t\n"
|
268
|
+
out_s += "#{@height}\t\t|#{@value}|\t\t\n"
|
269
|
+
out_s += "\t#{@left}\t\t#{@right}\t\n"
|
270
|
+
out_s += "\t\t\t\t\t\n"
|
271
|
+
puts out_s
|
272
|
+
end
|
273
|
+
|
274
|
+
def print
|
275
|
+
out_s = "#{@value} - "
|
276
|
+
instance_variables.each do |i|
|
277
|
+
out_s += "(#{i}: #{instance_variable_get(i.to_sym)})"
|
278
|
+
end
|
279
|
+
puts out_s
|
280
|
+
end
|
281
|
+
|
282
|
+
def to_s
|
283
|
+
@value.to_s
|
284
|
+
end
|
285
|
+
|
286
|
+
end
|
287
|
+
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class EventQueue
|
2
|
+
attr_reader :number_of_events
|
3
|
+
attr_reader :events
|
4
|
+
attr_reader :individual_vertices
|
5
|
+
|
6
|
+
def initialize(polygon)
|
7
|
+
#last vertex in geojson is equal to first vertex
|
8
|
+
@individual_vertices = polygon.vertices.length - 1
|
9
|
+
#2 per edge - last event looping back to 0 is handled by +1 below
|
10
|
+
@number_of_events = 2 * (@individual_vertices)
|
11
|
+
@events = []
|
12
|
+
|
13
|
+
#build up 2 'events' per edge. One for left vertex, one for right.
|
14
|
+
for i in 0..@individual_vertices-1
|
15
|
+
a = 2*i
|
16
|
+
b = 2*i+1
|
17
|
+
@events[a] = {:edge=>i}
|
18
|
+
@events[b] = {:edge=>i}
|
19
|
+
@events[a][:vertex] = polygon.vertices[i]
|
20
|
+
@events[b][:vertex] = polygon.vertices[i+1]
|
21
|
+
if @events[a][:vertex].compare(@events[b][:vertex]) < 0
|
22
|
+
@events[a][:type] = 'left'
|
23
|
+
@events[b][:type] = 'right'
|
24
|
+
else
|
25
|
+
@events[a][:type] = 'right'
|
26
|
+
@events[b][:type] = 'left'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# sort events lexicographically
|
31
|
+
@events.sort!{|a,b| a[:vertex].compare(b[:vertex])}
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Point
|
2
|
+
|
3
|
+
attr_reader :x
|
4
|
+
attr_reader :y
|
5
|
+
|
6
|
+
def initialize(x,y)
|
7
|
+
@x=x
|
8
|
+
@y=y
|
9
|
+
end
|
10
|
+
|
11
|
+
# Determines the xy lexicographical order of two points
|
12
|
+
def compare other_point
|
13
|
+
raise Exception.new("Self is x=#{@x},y=#{@y}, the other_point is nil") if other_point.nil?
|
14
|
+
# x-coord first
|
15
|
+
return 1 if @x > other_point.x
|
16
|
+
return -1 if @x < other_point.x
|
17
|
+
|
18
|
+
# y-coord second
|
19
|
+
return 1 if @y > other_point.y
|
20
|
+
return -1 if @y < other_point.y
|
21
|
+
|
22
|
+
# they are the same point
|
23
|
+
return 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def == (other_point)
|
27
|
+
@x==other_point.x && @y==other_point.y
|
28
|
+
end
|
29
|
+
|
30
|
+
# tests if point is Left|On|Right of the line P0 to P1.
|
31
|
+
#
|
32
|
+
# returns:
|
33
|
+
# >0 for left of the line
|
34
|
+
# 0 for on the line
|
35
|
+
# <0 for right of the line
|
36
|
+
def is_left p0, p1
|
37
|
+
return (p1.x - p0.x) * (@y - p0.y) - (@x - p0.x) * (p1.y - p0.y)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
class Polygon
|
2
|
+
attr_reader :vertices
|
3
|
+
attr_reader :intersection_points
|
4
|
+
|
5
|
+
def initialize(points)
|
6
|
+
@vertices = points
|
7
|
+
@intersection_points = Array.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def is_simple
|
11
|
+
event_queue = EventQueue.new(self)
|
12
|
+
sweep_line = SweepLine.new(self)
|
13
|
+
|
14
|
+
# This loop processes all events in the sorted queue
|
15
|
+
# Events are only left or right vertices
|
16
|
+
e = event_queue.events.shift
|
17
|
+
while !e.nil? do
|
18
|
+
|
19
|
+
if e[:type] == 'left'
|
20
|
+
s = sweep_line.add(e)
|
21
|
+
|
22
|
+
return false if !sweep_line.intersect(s, s.above).nil?
|
23
|
+
return false if !sweep_line.intersect(s, s.below).nil?
|
24
|
+
else
|
25
|
+
s = sweep_line.find(e)
|
26
|
+
return false if !sweep_line.intersect(s.above, s.below).nil?
|
27
|
+
sweep_line.remove(s)
|
28
|
+
end
|
29
|
+
|
30
|
+
e = event_queue.events.shift
|
31
|
+
end
|
32
|
+
|
33
|
+
return true
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_intersection_points
|
37
|
+
event_queue = EventQueue.new(self)
|
38
|
+
sweep_line = SweepLine.new(self)
|
39
|
+
|
40
|
+
# This loop processes all events in the sorted queue
|
41
|
+
# Events are only left or right vertices
|
42
|
+
e = event_queue.events.shift
|
43
|
+
while !e.nil? do
|
44
|
+
|
45
|
+
if e[:type] == 'left'
|
46
|
+
s = sweep_line.add(e)
|
47
|
+
|
48
|
+
add_to_intersection_point_collection(sweep_line.intersect(s, s.above))
|
49
|
+
add_to_intersection_point_collection(sweep_line.intersect(s, s.below))
|
50
|
+
|
51
|
+
else
|
52
|
+
s = sweep_line.find(e)
|
53
|
+
add_to_intersection_point_collection(sweep_line.intersect(s.above, s.below))
|
54
|
+
sweep_line.remove(s)
|
55
|
+
end
|
56
|
+
|
57
|
+
e = event_queue.events.shift
|
58
|
+
end
|
59
|
+
|
60
|
+
return nil if intersection_points.size==0
|
61
|
+
@intersection_points
|
62
|
+
end
|
63
|
+
|
64
|
+
def add_to_intersection_point_collection(point)
|
65
|
+
@intersection_points << point if !point.nil? && !@intersection_points.any?{|p| p.compare(point)==0}
|
66
|
+
end
|
67
|
+
|
68
|
+
def get_first_intersection_point_hash
|
69
|
+
event_queue = EventQueue.new(self)
|
70
|
+
sweep_line = SweepLine.new(self)
|
71
|
+
|
72
|
+
# This loop processes all events in the sorted queue
|
73
|
+
# Events are only left or right vertices
|
74
|
+
e = event_queue.events.shift
|
75
|
+
while !e.nil? do
|
76
|
+
if e[:type] == 'left'
|
77
|
+
s = sweep_line.add(e)
|
78
|
+
|
79
|
+
point = sweep_line.intersect(s, s.above)
|
80
|
+
return point_hash_with_edge_info(point, s, s.above) if !point.nil?
|
81
|
+
|
82
|
+
point = sweep_line.intersect(s, s.below)
|
83
|
+
return point_hash_with_edge_info(point, s, s.below) if !point.nil?
|
84
|
+
else
|
85
|
+
s = sweep_line.find(e)
|
86
|
+
point = sweep_line.intersect(s.above, s.below)
|
87
|
+
return point_hash_with_edge_info(point, s.above, s.below) if !point.nil?
|
88
|
+
sweep_line.remove(s)
|
89
|
+
end
|
90
|
+
e = event_queue.events.shift
|
91
|
+
end
|
92
|
+
return nil
|
93
|
+
end
|
94
|
+
|
95
|
+
def decompose
|
96
|
+
intersection_point = get_first_intersection_point_hash
|
97
|
+
return [self] if intersection_point.nil?
|
98
|
+
first_polygon = Polygon.new((@vertices[0..intersection_point[:edge1]]<<intersection_point[:point]).concat( @vertices[intersection_point[:edge2]+1,@vertices.length-intersection_point[:edge2]]))
|
99
|
+
second_polygon = Polygon.new(@vertices[intersection_point[:edge1]+1..intersection_point[:edge2]].insert(0,intersection_point[:point])<<intersection_point[:point])
|
100
|
+
return [first_polygon,second_polygon]
|
101
|
+
end
|
102
|
+
|
103
|
+
def point_hash_with_edge_info(point, s1, s2)
|
104
|
+
edges=[s1.edge, s2.edge].sort!
|
105
|
+
{:point => point, :edge1 => edges[0], :edge2 => edges[1]}
|
106
|
+
end
|
107
|
+
|
108
|
+
def ==(other_polygon)
|
109
|
+
for i in 0..@vertices.size-1
|
110
|
+
return false if @vertices[i] != other_polygon.vertices[i]
|
111
|
+
end
|
112
|
+
return true
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
#A container class for segments (or edges) of the polygon to test
|
2
|
+
#Allows storage and retrieval from the Balanced Binary Tree
|
3
|
+
class Segment
|
4
|
+
attr_reader :edge
|
5
|
+
attr_accessor :left_point
|
6
|
+
attr_accessor :right_point
|
7
|
+
attr_accessor :above
|
8
|
+
attr_accessor :below
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(event)
|
12
|
+
@edge = event[:edge]
|
13
|
+
end
|
14
|
+
|
15
|
+
def <(other_segment)
|
16
|
+
return true if @edge<other_segment.edge
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
|
20
|
+
def >(other_segment)
|
21
|
+
return true if @edge>other_segment.edge
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
def == (other_segment)
|
26
|
+
@edge == other_segment.edge && @left_point == other_segment.left_point && @right_point == other_segment.right_point
|
27
|
+
end
|
28
|
+
|
29
|
+
def to_s
|
30
|
+
return "edge:#{@edge.to_s}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def intersection_point_with(other_segment)
|
34
|
+
numerator = (other_segment.left_point.y - @left_point.y) * (other_segment.left_point.x - other_segment.right_point.x) -
|
35
|
+
(other_segment.left_point.y - other_segment.right_point.y) * (other_segment.left_point.x - @left_point.x)
|
36
|
+
|
37
|
+
denominator = (@right_point.y - @left_point.y) * (other_segment.left_point.x - other_segment.right_point.x) -
|
38
|
+
(other_segment.left_point.y - other_segment.right_point.y) * (@right_point.x - @left_point.x)
|
39
|
+
|
40
|
+
t = numerator.to_f / denominator
|
41
|
+
|
42
|
+
x = @left_point.x + t * (@right_point.x - @left_point.x)
|
43
|
+
y = @left_point.y + t * (@right_point.y - @left_point.y)
|
44
|
+
|
45
|
+
Point.new(x, y)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
class SweepLine
|
2
|
+
attr_reader :tree
|
3
|
+
attr_reader :polygon
|
4
|
+
|
5
|
+
def initialize(polygon)
|
6
|
+
@tree = AVLTree.new
|
7
|
+
@polygon = polygon
|
8
|
+
end
|
9
|
+
|
10
|
+
def add(event)
|
11
|
+
# build up segment data
|
12
|
+
seg = Segment.new(event)
|
13
|
+
p1 = @polygon.vertices[seg.edge]
|
14
|
+
p2 = @polygon.vertices[seg.edge + 1]
|
15
|
+
|
16
|
+
# if it is being added, then it must be a LEFT edge event
|
17
|
+
# but need to determine which endpoint is the left one first
|
18
|
+
|
19
|
+
if p1.compare(p2) < 0
|
20
|
+
seg.left_point = p1
|
21
|
+
seg.right_point = p2
|
22
|
+
else
|
23
|
+
seg.left_point = p2
|
24
|
+
seg.right_point = p1
|
25
|
+
end
|
26
|
+
|
27
|
+
# Add node to tree and setup linkages to "above" and "below"
|
28
|
+
# edges as per algorithm
|
29
|
+
nd = @tree.insert(seg)
|
30
|
+
|
31
|
+
nx = nd.next
|
32
|
+
np = nd.prev
|
33
|
+
|
34
|
+
if !nx.nil?
|
35
|
+
seg.above = nx.value
|
36
|
+
seg.above.below = seg
|
37
|
+
end
|
38
|
+
|
39
|
+
if !np.nil?
|
40
|
+
seg.below = np.value
|
41
|
+
seg.below.above = seg
|
42
|
+
end
|
43
|
+
return seg
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
def find(event)
|
48
|
+
# need a segment to find it in the tree
|
49
|
+
seg = Segment.new(event)
|
50
|
+
p1 = @polygon.vertices[seg.edge]
|
51
|
+
p2 = @polygon.vertices[seg.edge + 1]
|
52
|
+
|
53
|
+
# if it is being added, then it must be a LEFT edge event
|
54
|
+
# but need to determine which endpoint is the left one first
|
55
|
+
if p1.compare(p2) < 0
|
56
|
+
seg.left_point = p1
|
57
|
+
seg.right_point = p2
|
58
|
+
else
|
59
|
+
seg.left_point = p2
|
60
|
+
seg.right_point = p1
|
61
|
+
end
|
62
|
+
|
63
|
+
node = @tree.search(seg)
|
64
|
+
|
65
|
+
return nil if node.nil?
|
66
|
+
|
67
|
+
node.value
|
68
|
+
end
|
69
|
+
|
70
|
+
def remove(seg)
|
71
|
+
nd = @tree.search(seg)
|
72
|
+
return if nd.nil?
|
73
|
+
|
74
|
+
nx = nd.next
|
75
|
+
nx.value.below = seg.below if !nx.nil?
|
76
|
+
|
77
|
+
np = nd.prev
|
78
|
+
np.value.above = seg.above if !np.nil?
|
79
|
+
|
80
|
+
@tree.delete(seg)
|
81
|
+
end
|
82
|
+
|
83
|
+
def switch(s1,s2)
|
84
|
+
nd1 = @tree.search(s1)
|
85
|
+
return if nd1.nil?
|
86
|
+
|
87
|
+
nd2 = @tree.search(s2)
|
88
|
+
return if nd2.nil?
|
89
|
+
|
90
|
+
nx1 = nd1.next
|
91
|
+
nx1.value.below = nd2.value if !nx1.nil?
|
92
|
+
|
93
|
+
np1 = nd1.prev
|
94
|
+
np1.value.above = nd2.value if !np1.nil?
|
95
|
+
|
96
|
+
nx2 = nd2.next
|
97
|
+
nx2.value.below = nd1.value if !nx2.nil?
|
98
|
+
|
99
|
+
np2 = nd2.prev
|
100
|
+
np2.value.above = nd1.value if !np2.nil?
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
#test intersect of 2 segments and return: nil when none, point when intersecting
|
105
|
+
def intersect(s1,s2)
|
106
|
+
# no intersect if either segment doesn't existend
|
107
|
+
return nil if s1.nil? || s2.nil?
|
108
|
+
|
109
|
+
# check for consecutive edges in polygon
|
110
|
+
e1 = s1.edge
|
111
|
+
e2 = s2.edge
|
112
|
+
|
113
|
+
# no non-simple intersect since consecutive
|
114
|
+
polygon_edges = @polygon.vertices.length-1
|
115
|
+
return nil if (((e1+1)%polygon_edges === e2) || (e1 === (e2+1)%polygon_edges))
|
116
|
+
|
117
|
+
|
118
|
+
#test for existence of an intersect point
|
119
|
+
#s2 left point sign
|
120
|
+
lsign = s2.left_point.is_left(s1.left_point, s1.right_point)
|
121
|
+
#s2 right point sign
|
122
|
+
rsign = s2.right_point.is_left(s1.left_point, s1.right_point)
|
123
|
+
|
124
|
+
# s2 endpoints have same sign relative to s1 => on same side => no intersect is possible
|
125
|
+
return nil if (lsign * rsign > 0)
|
126
|
+
|
127
|
+
# s1 left point sign
|
128
|
+
lsign = s1.left_point.is_left(s2.left_point, s2.right_point)
|
129
|
+
#s1 right point sign
|
130
|
+
rsign = s1.right_point.is_left(s2.left_point, s2.right_point)
|
131
|
+
|
132
|
+
# s1 endpoints have same sign relative to s2 => on same side => no intersect is possible
|
133
|
+
return nil if (lsign * rsign > 0)
|
134
|
+
|
135
|
+
#segments s1 and s2 straddle. Intersect exists.
|
136
|
+
s1.intersection_point_with(s2)
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "EventQueue" do
|
4
|
+
|
5
|
+
it "test can create an EventQueue" do
|
6
|
+
|
7
|
+
points = JSON.parse("[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]")
|
8
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
9
|
+
event_queue = EventQueue.new(polygon)
|
10
|
+
|
11
|
+
event_queue.events.length.should == 8
|
12
|
+
event_queue.events.length.should == event_queue.number_of_events
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
data/spec/point_spec.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Point" do
|
4
|
+
|
5
|
+
it 'test less than comparator' do
|
6
|
+
p0 = Point.new(1,1)
|
7
|
+
p1 = Point.new(3,3)
|
8
|
+
p0.compare(p1).should == -1
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'test more than comparator' do
|
12
|
+
p0 = Point.new(1,1)
|
13
|
+
p1 = Point.new(3,3)
|
14
|
+
p1.compare(p0).should == 1
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'test equality' do
|
18
|
+
p0 = Point.new(1,1)
|
19
|
+
p0.compare(p0).should == 0
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'test left of line' do
|
23
|
+
p0 = Point.new(1,1)
|
24
|
+
p1 = Point.new(3,3)
|
25
|
+
p2 = Point.new(1,3)
|
26
|
+
p2.is_left(p0,p1).should > 0
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'test right of line' do
|
30
|
+
p0 = Point.new(1,1)
|
31
|
+
p1 = Point.new(3,3)
|
32
|
+
p2 = Point.new(3,1)
|
33
|
+
p2.is_left(p0,p1).should < 0
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'test on of line' do
|
37
|
+
p0 = Point.new(1,1)
|
38
|
+
p1 = Point.new(3,3)
|
39
|
+
p2 = Point.new(2,2)
|
40
|
+
p2.is_left(p0,p1).should == 0
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Polygon" do
|
4
|
+
|
5
|
+
it 'test can build a polygon from an array of points' do
|
6
|
+
|
7
|
+
points = JSON.parse("[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]")
|
8
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
9
|
+
|
10
|
+
polygon.vertices.length.should == points.length
|
11
|
+
polygon.vertices[0].x.should == points[0][0]
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'test is polygon simple 1' do
|
16
|
+
|
17
|
+
points = JSON.parse("[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]")
|
18
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
19
|
+
|
20
|
+
polygon.is_simple.should == true
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'test is polygon simple 2' do
|
25
|
+
|
26
|
+
points = JSON.parse("[[2.0, 2.0], [1.0, 2.0], [1.0, 1.0], [2.0, 1.0], [3.0, 1.0], [3.0, 2.0], [2.0, 2.0]]")
|
27
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
28
|
+
|
29
|
+
polygon.is_simple.should == true
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'test is polygon simple 3' do
|
34
|
+
|
35
|
+
points = JSON.parse("[[0, 0], [0, 1], [1, 1], [1, 0], [0, 0]]")
|
36
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
37
|
+
|
38
|
+
polygon.is_simple.should == true
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'test is polygon simple 4' do
|
43
|
+
|
44
|
+
points = JSON.parse("[[0.0, 0.0], [0.0, 3.0], [3.0, 3.0], [3.0, 0.0], [0.0, 0.0]]")
|
45
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
46
|
+
|
47
|
+
polygon.is_simple.should == true
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'test is polygon simple 5' do
|
52
|
+
|
53
|
+
points = JSON.parse("[[2.0, 2.0], [2.0, 3.0], [3.0, 3.0], [4.0, 3.0], [4.0, 2.0], [2.0, 2.0]]")
|
54
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
55
|
+
|
56
|
+
polygon.is_simple.should == true
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'test is practical polygon simple 6 (clockwise)' do
|
61
|
+
|
62
|
+
points = JSON.parse("[[-110.84096146619905, 32.206464264983815],[-110.8254844632264, 32.206404507969239],[-110.82564474991523, 32.2065185890533],[-110.8278203157768, 32.211149692139429],[-110.82920840010047, 32.217701622983441],[110.82990196985523,32.220975019031748],[-110.84096004697494,32.221011891029775],[-110.84096146619905,32.206464264983815]]")
|
63
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
64
|
+
|
65
|
+
polygon.is_simple.should == true
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'test is practical polygon simple 7 (counter clockwise)' do
|
70
|
+
|
71
|
+
points = JSON.parse("[[-118.845766, 34.013743],[-118.384777,34.013743],[-118.384777, 34.335579],[-118.845766, 34.335579],[-118.845766, 34.013743]]")
|
72
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
73
|
+
|
74
|
+
polygon.is_simple.should == true
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
it 'get_intersection_point_hash test for practical polygon simple 6 (clockwise)' do
|
80
|
+
|
81
|
+
points = JSON.parse("[[-110.84096146619905, 32.206464264983815],[-110.8254844632264, 32.206404507969239],[-110.82564474991523, 32.2065185890533],[-110.8278203157768, 32.211149692139429],[-110.82920840010047, 32.217701622983441],[110.82990196985523,32.220975019031748],[-110.84096004697494,32.221011891029775],[-110.84096146619905,32.206464264983815]]")
|
82
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
83
|
+
|
84
|
+
polygon.get_first_intersection_point_hash.should be_nil
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'get_intersection_point_hash test for practical polygon simple 6 (counter clockwise)' do
|
89
|
+
|
90
|
+
points = JSON.parse("[[-118.845766, 34.013743],[-118.384777,34.013743],[-118.384777, 34.335579],[-118.845766, 34.335579],[-118.845766, 34.013743]]")
|
91
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
92
|
+
|
93
|
+
polygon.get_first_intersection_point_hash.should be_nil
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
it 'test is practical polygon simple 7 (counter clockwise)' do
|
98
|
+
|
99
|
+
points = JSON.parse("[[-118.845766, 34.013743],[-118.384777,34.013743],[-118.384777, 34.335579],[-118.845766, 34.335579],[-118.845766, 34.013743]]")
|
100
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
101
|
+
|
102
|
+
polygon.is_simple.should == true
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
|
107
|
+
it 'test is complex polygon 1' do
|
108
|
+
|
109
|
+
points = JSON.parse("[[100.0, 0.0],[101.0, 1.0],[100.0, 1.0],[101.0, 0.0], [100.0, 0.0]]")
|
110
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
111
|
+
|
112
|
+
polygon.is_simple.should == false
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'test is complex polygon 2' do
|
117
|
+
|
118
|
+
points = JSON.parse("[[2.0, 2.0], [3.0, 2.0], [3.0, 3.0], [4.0, 2.0], [2.0, 2.0]]")
|
119
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
120
|
+
|
121
|
+
polygon.is_simple.should == false
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'test is complex polygon 3' do
|
126
|
+
|
127
|
+
points = JSON.parse("[[0.0, 0.0], [3.0, 3.0], [0.0, 3.0], [3.0, 0.0], [0.0, 0.0]]")
|
128
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
129
|
+
|
130
|
+
polygon.is_simple.should == false
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
|
135
|
+
it 'test is practical complex polygon 4' do
|
136
|
+
|
137
|
+
points = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-98.4609375000036,40.3051841980949]]")
|
138
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
139
|
+
|
140
|
+
polygon.is_simple.should == false
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'get complex polygon 3 intersection points' do
|
145
|
+
|
146
|
+
points = JSON.parse("[[0.0, 0.0], [3.0, 3.0], [0.0, 3.0], [3.0, 0.0], [0.0, 0.0]]")
|
147
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
148
|
+
|
149
|
+
polygon.get_intersection_points[0].should == Point.new(1.5,1.5)
|
150
|
+
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'get practical complex polygon 4 intersection points' do
|
154
|
+
|
155
|
+
points = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-98.4609375000036,40.3051841980949]]")
|
156
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
157
|
+
|
158
|
+
polygon.get_intersection_points[0].should == Point.new(-97.39951856124108,38.97265129300353)
|
159
|
+
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'get_intersection_point_hash test for practical complex polygon' do
|
163
|
+
|
164
|
+
points = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-98.4609375000036,40.3051841980949]]")
|
165
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
166
|
+
|
167
|
+
polygon.get_first_intersection_point_hash.should == {:point=>Point.new(-97.39951856124108,38.97265129300353),:edge1=>1,:edge2=>3 }
|
168
|
+
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'should decompose a practical complex polygon into two simple polygons' do
|
172
|
+
|
173
|
+
points = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-98.4609375000036,40.3051841980949]]")
|
174
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
175
|
+
|
176
|
+
multi_polygons = polygon.decompose
|
177
|
+
multi_polygons.should_not be_nil
|
178
|
+
|
179
|
+
points1 = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-97.39951856124108,38.97265129300353],[-98.4609375000036,40.3051841980949]]")
|
180
|
+
polygon1 = Polygon.new(points1.map{|item| Point.new(item[0],item[1])})
|
181
|
+
multi_polygons[0].should ==polygon1
|
182
|
+
|
183
|
+
points2 = JSON.parse("[[-97.39951856124108,38.97265129300353],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-97.39951856124108,38.97265129300353]]")
|
184
|
+
polygon2 = Polygon.new(points2.map{|item| Point.new(item[0],item[1])})
|
185
|
+
multi_polygons[1].should ==polygon2
|
186
|
+
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Segment" do
|
4
|
+
|
5
|
+
it "test_segments_intersect_at_the_endpoint" do
|
6
|
+
|
7
|
+
segment1 = Segment.new({:edge=>0})
|
8
|
+
segment1.left_point = Point.new(0,0)
|
9
|
+
segment1.right_point = Point.new(2,2)
|
10
|
+
|
11
|
+
segment2 = Segment.new({:edge=>1})
|
12
|
+
segment2.left_point = Point.new(0,2)
|
13
|
+
segment2.right_point = Point.new(2,2)
|
14
|
+
|
15
|
+
intersection_point = segment1.intersection_point_with(segment2)
|
16
|
+
intersection_point.x.should ==2
|
17
|
+
intersection_point.y.should ==2
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../../lib/winding-polygon', __FILE__)
|
3
|
+
#Dir["../../lib/winding-polygon/*.rb"].each {|file| require file }
|
4
|
+
require 'avltree.rb'
|
5
|
+
require 'event_queue'
|
6
|
+
require 'point'
|
7
|
+
require 'polygon'
|
8
|
+
require 'segment'
|
9
|
+
require 'sweep_line'
|
10
|
+
require 'winding-polygon'
|
11
|
+
require 'json'
|
12
|
+
require "rspec"
|
13
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "SweepLine" do
|
4
|
+
|
5
|
+
it "test can find" do
|
6
|
+
points = JSON.parse("[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]]")
|
7
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
8
|
+
sweep_line = SweepLine.new(polygon)
|
9
|
+
event_queue = EventQueue.new(polygon)
|
10
|
+
|
11
|
+
event = event_queue.events.pop
|
12
|
+
while !event.nil?
|
13
|
+
sweep_line.add(event) if event[:type]=='left'
|
14
|
+
event = event_queue.events.pop
|
15
|
+
end
|
16
|
+
|
17
|
+
for i in 0..3 do
|
18
|
+
sweep_line.find({:edge=>i}).should_not be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe WindingPolygon do
|
4
|
+
it 'should have a version number' do
|
5
|
+
WindingPolygon::VERSION.should_not be_nil
|
6
|
+
end
|
7
|
+
|
8
|
+
it 'should decompose a practical complex polygon into two simple polygons' do
|
9
|
+
|
10
|
+
points = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-98.4609375000036,40.3051841980949]]")
|
11
|
+
polygon = Polygon.new(points.map{|item| Point.new(item[0],item[1])})
|
12
|
+
|
13
|
+
multi_polygons = []
|
14
|
+
WindingPolygon.decompose(polygon,multi_polygons)
|
15
|
+
multi_polygons.should_not be_nil
|
16
|
+
|
17
|
+
points1 = JSON.parse("[[-98.4609375000036,40.3051841980949],[-98.4609375000036,38.057277897745],[-97.39951856124108,38.97265129300353],[-98.4609375000036,40.3051841980949]]")
|
18
|
+
polygon1 = Polygon.new(points1.map{|item| Point.new(item[0],item[1])})
|
19
|
+
multi_polygons[0].should ==polygon1
|
20
|
+
|
21
|
+
points2 = JSON.parse("[[-97.39951856124108,38.97265129300353],[-96.0878906250017,40.1038062719331],[-96.6152343750031,37.9880405545487],[-97.39951856124108,38.97265129300353]]")
|
22
|
+
polygon2 = Polygon.new(points2.map{|item| Point.new(item[0],item[1])})
|
23
|
+
multi_polygons[1].should ==polygon2
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
require '../lib/winding-polygon/avltree'
|
3
|
+
|
4
|
+
class TestAVLTree < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def setup
|
7
|
+
#
|
8
|
+
# 2
|
9
|
+
# 1 4
|
10
|
+
# 3 5
|
11
|
+
#
|
12
|
+
@tree = AVLTree.new [1,2,5,4,3]
|
13
|
+
#
|
14
|
+
# 4
|
15
|
+
# 2 6
|
16
|
+
# 1 3 5 7
|
17
|
+
#
|
18
|
+
@rot_tree = AVLTree.new [4,2,6,3,1,5,7]
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_sort
|
22
|
+
assert_equal "[1, 2, 3, 4, 5]", @tree.sort.inspect
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_search
|
26
|
+
assert_not_nil @tree.search(1)
|
27
|
+
assert_not_nil @tree.search(4)
|
28
|
+
assert_nil @tree.search(0)
|
29
|
+
assert_nil @tree.search(6)
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_min
|
33
|
+
assert_equal 1, @tree.min.value
|
34
|
+
assert_equal 1, @tree.search(1).min.value
|
35
|
+
assert_equal 3, @tree.search(4).min.value
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_max
|
39
|
+
assert_equal 5, @tree.max.value
|
40
|
+
assert_equal 5, @tree.search(2).max.value
|
41
|
+
assert_equal 5, @tree.search(4).max.value
|
42
|
+
assert_equal 5, @tree.search(5).max.value
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_next
|
46
|
+
assert_equal 2, @tree.search(1).next.value
|
47
|
+
assert_equal 3, @tree.search(2).next.value
|
48
|
+
assert_nil @tree.search(5).next
|
49
|
+
end
|
50
|
+
|
51
|
+
def test_prev
|
52
|
+
assert_nil @tree.search(1).prev
|
53
|
+
assert_equal 4, @tree.search(5).prev.value
|
54
|
+
assert_equal 3, @tree.search(4).prev.value
|
55
|
+
assert_equal 2, @tree.search(3).prev.value
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_delete
|
59
|
+
|
60
|
+
@tree.delete 2
|
61
|
+
assert_equal "[1, 3, 4, 5]", @tree.sort.inspect
|
62
|
+
@tree.delete 4
|
63
|
+
assert_equal "[1, 3, 5]", @tree.sort.inspect
|
64
|
+
@tree.delete 3
|
65
|
+
assert_equal "[1, 5]", @tree.sort.inspect
|
66
|
+
@tree.delete 1
|
67
|
+
assert_equal "[5]", @tree.sort.inspect
|
68
|
+
|
69
|
+
@rot_tree.delete 1
|
70
|
+
assert_equal 4.to_s, @rot_tree.root.to_s
|
71
|
+
@rot_tree.delete 2
|
72
|
+
assert_equal 4.to_s, @rot_tree.root.to_s
|
73
|
+
@rot_tree.delete 3
|
74
|
+
#should have to rotate
|
75
|
+
assert_equal 6.to_s, @rot_tree.root.to_s
|
76
|
+
assert_equal 4.to_s, @rot_tree.root.left.to_s
|
77
|
+
assert_equal 7.to_s, @rot_tree.root.right.to_s
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_rotate_left
|
81
|
+
root = @rot_tree.root
|
82
|
+
t_node = @rot_tree.search(4)
|
83
|
+
s_node = @rot_tree.search(6)
|
84
|
+
|
85
|
+
assert_equal 4.to_s, root.to_s
|
86
|
+
assert_equal t_node.to_s, root.to_s
|
87
|
+
assert_equal s_node.to_s, t_node.right.to_s
|
88
|
+
|
89
|
+
t_node.rotate_left
|
90
|
+
|
91
|
+
t_node = @rot_tree.search(4)
|
92
|
+
root = t_node.parent
|
93
|
+
|
94
|
+
#root
|
95
|
+
assert_equal nil.to_s, root.parent.to_s
|
96
|
+
assert_equal 7.to_s, root.right.to_s
|
97
|
+
assert_equal t_node.to_s, root.left.to_s
|
98
|
+
|
99
|
+
#t_node
|
100
|
+
assert_equal root.to_s, t_node.parent.to_s
|
101
|
+
assert_equal 2.to_s, t_node.left.to_s
|
102
|
+
assert_equal 5.to_s, t_node.right.to_s
|
103
|
+
end
|
104
|
+
|
105
|
+
def test_rotate_right
|
106
|
+
root = @rot_tree.root
|
107
|
+
t_node = @rot_tree.search(4)
|
108
|
+
s_node = @rot_tree.search(2)
|
109
|
+
|
110
|
+
#root.print
|
111
|
+
#t_node.print
|
112
|
+
#s_node.print
|
113
|
+
|
114
|
+
assert_equal 4.to_s, root.to_s
|
115
|
+
assert_equal t_node.to_s, root.to_s
|
116
|
+
assert_equal s_node.to_s, t_node.left.to_s
|
117
|
+
|
118
|
+
t_node.rotate_right
|
119
|
+
|
120
|
+
t_node = @rot_tree.search(4)
|
121
|
+
root = t_node.parent
|
122
|
+
|
123
|
+
#root
|
124
|
+
assert_equal nil.to_s, root.parent.to_s
|
125
|
+
assert_equal 4.to_s, root.right.to_s
|
126
|
+
|
127
|
+
#t_node
|
128
|
+
assert_equal root.to_s, t_node.parent.to_s
|
129
|
+
assert_equal 3.to_s, t_node.left.to_s
|
130
|
+
assert_equal 6.to_s, t_node.right.to_s
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_balance
|
134
|
+
#@tree.root.print
|
135
|
+
#@tree.search(1).print
|
136
|
+
#@tree.search(2).print
|
137
|
+
#@tree.search(5).print
|
138
|
+
#@tree.search(4).print
|
139
|
+
#@tree.search(3).print
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'winding-polygon/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "winding-polygon"
|
8
|
+
gem.version = WindingPolygon::VERSION
|
9
|
+
gem.authors = ["Martin Xu"]
|
10
|
+
gem.email = ["mxu2008@gmail.com"]
|
11
|
+
gem.description = %q{Use Bentley-Ottmann algorithm to solve self-intersecting polygon issue }
|
12
|
+
gem.summary = %q{Detect intersecting points and decompose it into multi-polygons}
|
13
|
+
gem.homepage = ""
|
14
|
+
gem.license = "MIT"
|
15
|
+
|
16
|
+
gem.add_development_dependency "rspec", "~> 2.6"
|
17
|
+
gem.add_development_dependency "test-unit"
|
18
|
+
|
19
|
+
gem.files = `git ls-files`.split($/)
|
20
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
21
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
22
|
+
gem.require_paths = ["lib"]
|
23
|
+
|
24
|
+
gem.add_development_dependency "rake"
|
25
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: winding-polygon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Martin Xu
|
@@ -58,7 +58,30 @@ email:
|
|
58
58
|
executables: []
|
59
59
|
extensions: []
|
60
60
|
extra_rdoc_files: []
|
61
|
-
files:
|
61
|
+
files:
|
62
|
+
- .gitignore
|
63
|
+
- .rspec
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- lib/winding-polygon.rb
|
69
|
+
- lib/winding-polygon/avltree.rb
|
70
|
+
- lib/winding-polygon/event_queue.rb
|
71
|
+
- lib/winding-polygon/point.rb
|
72
|
+
- lib/winding-polygon/polygon.rb
|
73
|
+
- lib/winding-polygon/segment.rb
|
74
|
+
- lib/winding-polygon/sweep_line.rb
|
75
|
+
- lib/winding-polygon/version.rb
|
76
|
+
- spec/event_queue_spec.rb
|
77
|
+
- spec/point_spec.rb
|
78
|
+
- spec/polygon_spec.rb
|
79
|
+
- spec/segment_spec.rb
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
- spec/sweep_line_spec.rb
|
82
|
+
- spec/winding-polygon_spec.rb
|
83
|
+
- test/test_avltree.rb
|
84
|
+
- winding-polygon.gemspec
|
62
85
|
homepage: ''
|
63
86
|
licenses:
|
64
87
|
- MIT
|
@@ -83,4 +106,12 @@ rubygems_version: 2.0.3
|
|
83
106
|
signing_key:
|
84
107
|
specification_version: 4
|
85
108
|
summary: Detect intersecting points and decompose it into multi-polygons
|
86
|
-
test_files:
|
109
|
+
test_files:
|
110
|
+
- spec/event_queue_spec.rb
|
111
|
+
- spec/point_spec.rb
|
112
|
+
- spec/polygon_spec.rb
|
113
|
+
- spec/segment_spec.rb
|
114
|
+
- spec/spec_helper.rb
|
115
|
+
- spec/sweep_line_spec.rb
|
116
|
+
- spec/winding-polygon_spec.rb
|
117
|
+
- test/test_avltree.rb
|