winding-polygon 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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
|