abscondment-rubyvor 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +56 -0
- data/Manifest.txt +30 -0
- data/README.txt +76 -0
- data/Rakefile +35 -0
- data/ext/Doc +30 -0
- data/ext/edgelist.c +204 -0
- data/ext/extconf.rb +3 -0
- data/ext/geometry.c +219 -0
- data/ext/heap.c +118 -0
- data/ext/memory.c +118 -0
- data/ext/output.c +251 -0
- data/ext/rb_cComputation.c +369 -0
- data/ext/rb_cPoint.c +35 -0
- data/ext/rb_cPriorityQueue.c +121 -0
- data/ext/ruby_vor_c.c +115 -0
- data/ext/ruby_vor_c.h +28 -0
- data/ext/vdefs.h +150 -0
- data/ext/voronoi.c +271 -0
- data/lib/ruby_vor.rb +16 -0
- data/lib/ruby_vor/computation.rb +137 -0
- data/lib/ruby_vor/geo_ruby_extensions.rb +15 -0
- data/lib/ruby_vor/point.rb +32 -0
- data/lib/ruby_vor/priority_queue.rb +84 -0
- data/lib/ruby_vor/version.rb +3 -0
- data/lib/ruby_vor/visualizer.rb +218 -0
- data/rubyvor.gemspec +36 -0
- data/test/test_computation.rb +354 -0
- data/test/test_point.rb +100 -0
- data/test/test_priority_queue.rb +129 -0
- data/test/test_voronoi_interface.rb +161 -0
- metadata +99 -0
data/lib/ruby_vor.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
$:.unshift File.join(File.dirname(__FILE__), '..', 'ext')
|
3
|
+
|
4
|
+
require 'ruby_vor/version'
|
5
|
+
require 'ruby_vor/point'
|
6
|
+
require 'ruby_vor/priority_queue'
|
7
|
+
require 'ruby_vor/computation'
|
8
|
+
require 'ruby_vor/geo_ruby_extensions'
|
9
|
+
require 'ruby_vor/visualizer'
|
10
|
+
|
11
|
+
# Require ruby_vor.so last to clobber old from_points
|
12
|
+
require 'ruby_vor_c.so'
|
13
|
+
|
14
|
+
# DOC HERE
|
15
|
+
module RubyVor
|
16
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
module RubyVor
|
2
|
+
module VDDT
|
3
|
+
class Computation
|
4
|
+
attr_reader :points, :voronoi_diagram_raw, :delaunay_triangulation_raw, :no_neighbor_response
|
5
|
+
|
6
|
+
DIST_PROC = lambda{|a,b| a.distance_from(b)}
|
7
|
+
NO_NEIGHBOR_RESPONSES = [:raise, :use_all, :ignore]
|
8
|
+
|
9
|
+
# Create a computation from an existing set of raw data.
|
10
|
+
def initialize
|
11
|
+
@points = []
|
12
|
+
|
13
|
+
@voronoi_diagram_raw = []
|
14
|
+
@delaunay_triangulation_raw = []
|
15
|
+
|
16
|
+
@nn_graph = nil
|
17
|
+
@mst = nil
|
18
|
+
|
19
|
+
@no_neighbor_response = :use_all
|
20
|
+
end
|
21
|
+
|
22
|
+
# Decided what action to take if we find a point with no neighbors
|
23
|
+
# while computing the nn_graph.
|
24
|
+
#
|
25
|
+
# Choices are:
|
26
|
+
# * :use_all - include all other nodes as potential neighbors. This choice is the default, and can lead to higher big-O lower bounds when clustering.
|
27
|
+
# * :raise - raise an error
|
28
|
+
# * :ignore - leave this node disconnected from the rest of the graph.
|
29
|
+
def no_neighbor_response=(v)
|
30
|
+
@no_neighbor_response = v if NO_NEIGHBOR_RESPONSES.include?(v)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Uses the nearest-neighbors information encapsulated by the Delaunay triangulation as a seed for clustering:
|
34
|
+
# We take the edges (there are O(n) of them, because defined by the triangulation and delete any edge above a certain distance.
|
35
|
+
#
|
36
|
+
# This method allows the caller to pass in a lambda for customizing distance calculations. For instance, to use a GeoRuby::SimpleFeatures::Point, one would:
|
37
|
+
# > cluster_by_distance(50 lambda{|a,b| a.spherical_distance(b, 3958.754)}) # this rejects edges greater than 50 miles, using spherical distance as a measure
|
38
|
+
def cluster_by_distance(max_distance, dist_proc=DIST_PROC)
|
39
|
+
clusters = []
|
40
|
+
nodes = (0..points.length-1).to_a
|
41
|
+
visited = [false] * points.length
|
42
|
+
graph = []
|
43
|
+
v = 0
|
44
|
+
|
45
|
+
nn_graph.each_with_index do |neighbors,v|
|
46
|
+
graph[v] = neighbors.select do |neighbor|
|
47
|
+
dist_proc[points[v], points[neighbor]] < max_distance
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
until nodes.empty?
|
53
|
+
v = nodes.pop
|
54
|
+
|
55
|
+
next if visited[v]
|
56
|
+
|
57
|
+
cluster = []
|
58
|
+
visited[v] = true
|
59
|
+
cluster.push(v)
|
60
|
+
|
61
|
+
children = graph[v]
|
62
|
+
until children.nil? || children.empty?
|
63
|
+
cnode = children.pop
|
64
|
+
next if cnode.nil? || visited[cnode]
|
65
|
+
|
66
|
+
visited[cnode] = true
|
67
|
+
cluster.push(cnode)
|
68
|
+
children.concat(graph[cnode])
|
69
|
+
end
|
70
|
+
|
71
|
+
clusters.push(cluster)
|
72
|
+
end
|
73
|
+
|
74
|
+
clusters
|
75
|
+
end
|
76
|
+
|
77
|
+
def cluster_by_size(sizes=[], dist_proc=DIST_PROC)
|
78
|
+
# * Take MST, and
|
79
|
+
# 1. For n in sizes (taken in descending order), delete the n most expensive edges from MST
|
80
|
+
# * TODO: use a MaxHeap?
|
81
|
+
# 2. Determine remaining connectivity using BFS as above?
|
82
|
+
# 3. Some other more efficient connectivity test?
|
83
|
+
# 4. Return {n1 => cluster, n2 => cluster} for all n.
|
84
|
+
|
85
|
+
sized_clusters = sizes.inject({}) {|h,s| h[s] = []; h}
|
86
|
+
|
87
|
+
|
88
|
+
mst = minimum_spanning_tree(dist_proc).to_a
|
89
|
+
mst.sort!{|a,b|a.last <=> b.last}
|
90
|
+
|
91
|
+
sizes = sizes.sort
|
92
|
+
last_size = 0
|
93
|
+
|
94
|
+
while current_size = sizes.shift
|
95
|
+
current_size -= 1
|
96
|
+
|
97
|
+
# Remove edge count delta
|
98
|
+
delta = current_size - last_size
|
99
|
+
mst.slice!(-delta,delta)
|
100
|
+
|
101
|
+
graph = (1..points.length).to_a.map{|v| []}
|
102
|
+
visited = [nil] * points.length
|
103
|
+
clusters = []
|
104
|
+
|
105
|
+
mst.each do |edge,weight|
|
106
|
+
graph[edge[1]].push(edge[0])
|
107
|
+
graph[edge[0]].push(edge[1])
|
108
|
+
end
|
109
|
+
|
110
|
+
for node in 0..points.length-1
|
111
|
+
next if visited[node]
|
112
|
+
|
113
|
+
cluster = [node]
|
114
|
+
visited[node] = true
|
115
|
+
|
116
|
+
neighbors = graph[node]
|
117
|
+
while v = neighbors.pop
|
118
|
+
next if visited[v]
|
119
|
+
|
120
|
+
cluster.push(v)
|
121
|
+
visited[v] = true
|
122
|
+
neighbors.concat(graph[v])
|
123
|
+
end
|
124
|
+
|
125
|
+
clusters.push(cluster)
|
126
|
+
end
|
127
|
+
|
128
|
+
sized_clusters[current_size + 1] = clusters
|
129
|
+
last_size = current_size
|
130
|
+
end
|
131
|
+
|
132
|
+
sized_clusters
|
133
|
+
end
|
134
|
+
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
if require 'geo_ruby'
|
2
|
+
# Let us call uniq on a set of Points or use one as a Hash key.
|
3
|
+
module GeoRuby
|
4
|
+
module SimpleFeatures
|
5
|
+
class Point < Geometry
|
6
|
+
def eql?(other)
|
7
|
+
self == other
|
8
|
+
end
|
9
|
+
def hash
|
10
|
+
[x,y,z,m].hash
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RubyVor
|
2
|
+
class Point
|
3
|
+
attr_reader :x, :y
|
4
|
+
def initialize(x=0.0,y=0.0)
|
5
|
+
raise TypeError, 'Must be able to convert point values into floats' unless x.respond_to?(:to_f) && y.respond_to?(:to_f)
|
6
|
+
@x = x.to_f
|
7
|
+
@y = y.to_f
|
8
|
+
end
|
9
|
+
|
10
|
+
def <=>(p)
|
11
|
+
(@x != p.x) ? @x <=> p.x : @y <=> p.y
|
12
|
+
end
|
13
|
+
|
14
|
+
def <(p)
|
15
|
+
(self <=> p) == -1
|
16
|
+
end
|
17
|
+
|
18
|
+
def >(p)
|
19
|
+
(self <=> p) == 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def ==(p)
|
23
|
+
@x == p.x && @y == p.y
|
24
|
+
end
|
25
|
+
alias :eql? :==
|
26
|
+
|
27
|
+
def to_s
|
28
|
+
"(#{@x},#{@y})"
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module RubyVor
|
2
|
+
class PriorityQueue
|
3
|
+
|
4
|
+
attr_reader :data, :size
|
5
|
+
|
6
|
+
class << self
|
7
|
+
def build_queue(max_index=-1,&block)
|
8
|
+
data = []
|
9
|
+
|
10
|
+
index = 0
|
11
|
+
loop do
|
12
|
+
x = QueueItem.new(nil, nil, nil)
|
13
|
+
|
14
|
+
yield(x)
|
15
|
+
break if !(max_index < 0 || index < max_index) || x.priority.nil?
|
16
|
+
|
17
|
+
x.index = index
|
18
|
+
data.push(x)
|
19
|
+
index += 1
|
20
|
+
end
|
21
|
+
|
22
|
+
q = new
|
23
|
+
q.instance_variable_set(:@data, data)
|
24
|
+
q.instance_variable_set(:@size, data.length)
|
25
|
+
q.heapify()
|
26
|
+
|
27
|
+
return q
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
@data = []
|
33
|
+
@size = 0
|
34
|
+
heapify()
|
35
|
+
end
|
36
|
+
|
37
|
+
def peek
|
38
|
+
@data[0]
|
39
|
+
end
|
40
|
+
|
41
|
+
def pop
|
42
|
+
return nil if @size < 1
|
43
|
+
|
44
|
+
r = @data[0]
|
45
|
+
|
46
|
+
@data[0] = @data[@size-1]
|
47
|
+
@data[0].index = 0
|
48
|
+
@data.delete_at(@size-1)
|
49
|
+
|
50
|
+
@size -= 1
|
51
|
+
|
52
|
+
percolate_down(0) if @size > 0
|
53
|
+
|
54
|
+
return r
|
55
|
+
end
|
56
|
+
|
57
|
+
def push(data, priority=data)
|
58
|
+
@size += 1
|
59
|
+
@data[@size - 1] = QueueItem.new(priority, @size - 1, data)
|
60
|
+
percolate_up(@size - 1)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Implemented in C
|
64
|
+
def reorder_queue;end
|
65
|
+
|
66
|
+
protected
|
67
|
+
|
68
|
+
# Implemented in C
|
69
|
+
def percolate_up(i);end
|
70
|
+
|
71
|
+
# Implemented in C
|
72
|
+
def percolate_down(i);end
|
73
|
+
|
74
|
+
class QueueItem
|
75
|
+
attr_accessor :priority, :index, :data
|
76
|
+
def initialize(p, i, d)
|
77
|
+
@priority = p
|
78
|
+
@index = i
|
79
|
+
@data = d
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
require 'xml/libxml' unless defined?(LibXML)
|
2
|
+
module RubyVor
|
3
|
+
class Visualizer
|
4
|
+
|
5
|
+
COLORS = %w{black red blue lime gray yellow purple orange pink}
|
6
|
+
|
7
|
+
# Support various versions of LibXML
|
8
|
+
include LibXML if defined?(LibXML)
|
9
|
+
|
10
|
+
def self.make_svg(computation, opts={})
|
11
|
+
@opts = opts = {
|
12
|
+
:name => 'vddt.svg',
|
13
|
+
:triangulation => true,
|
14
|
+
:voronoi_diagram => false,
|
15
|
+
:mst => false,
|
16
|
+
:cbd => false,
|
17
|
+
}.merge(opts)
|
18
|
+
|
19
|
+
line_colors = COLORS.clone
|
20
|
+
|
21
|
+
doc = XML::Document.new()
|
22
|
+
|
23
|
+
doc.root = XML::Node.new('svg')
|
24
|
+
doc.root['xmlns'] = 'http://www.w3.org/2000/svg'
|
25
|
+
doc.root['xml:space'] = 'preserve'
|
26
|
+
|
27
|
+
max_x = 0
|
28
|
+
min_x = Float::MAX
|
29
|
+
max_y = 0
|
30
|
+
min_y = Float::MAX
|
31
|
+
pmax_x = 0
|
32
|
+
pmin_x = Float::MAX
|
33
|
+
pmax_y = 0
|
34
|
+
pmin_y = Float::MAX
|
35
|
+
|
36
|
+
computation.points.each do |point|
|
37
|
+
max_x = point.x if point.x > max_x
|
38
|
+
min_x = point.x if point.x < min_x
|
39
|
+
max_y = point.y if point.y > max_y
|
40
|
+
min_y = point.y if point.y < min_y
|
41
|
+
pmax_x = point.x if point.x > pmax_x
|
42
|
+
pmin_x = point.x if point.x < pmin_x
|
43
|
+
pmax_y = point.y if point.y > pmax_y
|
44
|
+
pmin_y = point.y if point.y < pmin_y
|
45
|
+
end
|
46
|
+
|
47
|
+
if opts[:voronoi_diagram]
|
48
|
+
computation.voronoi_diagram_raw.each do |item|
|
49
|
+
if item.first == :v
|
50
|
+
max_x = item[1] if item[1] > max_x
|
51
|
+
min_x = item[1] if item[1] < min_x
|
52
|
+
max_y = item[2] if item[2] > max_y
|
53
|
+
min_y = item[2] if item[2] < min_y
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
opts[:offset_x] = -1.0 * min_x + 20
|
59
|
+
opts[:offset_y] = -1.0 * min_y + 20
|
60
|
+
|
61
|
+
if opts[:triangulation]
|
62
|
+
# Draw in the triangulation
|
63
|
+
color = line_colors.shift
|
64
|
+
computation.delaunay_triangulation_raw.each do |triplet|
|
65
|
+
for i in 0..2
|
66
|
+
line = line_from_points(computation.points[triplet[i % 2 + 1]], computation.points[triplet[i & 6]])
|
67
|
+
line['style'] = "stroke:#{color};stroke-width:1;"
|
68
|
+
doc.root << line
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if opts[:mst]
|
74
|
+
color = line_colors.shift
|
75
|
+
computation.minimum_spanning_tree.each do |edge, weight|
|
76
|
+
line = line_from_points(computation.points[edge[0]], computation.points[edge[1]])
|
77
|
+
line['style'] = "stroke:#{color};stroke-width:1;"
|
78
|
+
doc.root << line
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
if opts[:cbd]
|
83
|
+
mst = computation.minimum_spanning_tree
|
84
|
+
|
85
|
+
computation.cluster_by_distance(opts[:cbd]).each do |cluster|
|
86
|
+
|
87
|
+
color = new_color
|
88
|
+
min_dist = Float::MAX
|
89
|
+
set_min = false
|
90
|
+
cluster.each do |a|
|
91
|
+
cluster.each do |b|
|
92
|
+
next if a == b
|
93
|
+
k = a < b ? [a,b] : [b,a]
|
94
|
+
next unless mst.has_key?(k)
|
95
|
+
|
96
|
+
if mst[k] < min_dist
|
97
|
+
min_dist = mst[k]
|
98
|
+
set_min = true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
min_dist = 10 unless set_min
|
104
|
+
|
105
|
+
|
106
|
+
cluster.each do |v|
|
107
|
+
node = circle_from_point(computation.points[v])
|
108
|
+
node['r'] = (min_dist + 10).to_s
|
109
|
+
node['style'] = "fill:#{color};stroke:#{color};"
|
110
|
+
node['opacity'] = '0.4'
|
111
|
+
|
112
|
+
doc.root << node
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
if opts[:voronoi_diagram]
|
119
|
+
voronoi_vertices = []
|
120
|
+
draw_lines = []
|
121
|
+
draw_points = []
|
122
|
+
line_functions = []
|
123
|
+
|
124
|
+
xcut = (pmax_x + pmin_x) / 2.0
|
125
|
+
ycut = (pmax_y + pmin_y) / 2.0
|
126
|
+
|
127
|
+
color = line_colors.shift
|
128
|
+
|
129
|
+
computation.voronoi_diagram_raw.each do |item|
|
130
|
+
case item.first
|
131
|
+
when :v
|
132
|
+
# Draw a voronoi vertex
|
133
|
+
v = RubyVor::Point.new(item[1], item[2])
|
134
|
+
voronoi_vertices.push(v)
|
135
|
+
node = circle_from_point(v)
|
136
|
+
node['fill'] = 'red'
|
137
|
+
node['r'] = '2'
|
138
|
+
node['stroke'] = 'black'
|
139
|
+
node['stroke-width'] = '1'
|
140
|
+
|
141
|
+
draw_points << node
|
142
|
+
when :l
|
143
|
+
# :l a b c --> ax + by = c
|
144
|
+
a = item[1]
|
145
|
+
b = item[2]
|
146
|
+
c = item[3]
|
147
|
+
line_functions.push({ :y => lambda{|x| (c - a * x) / b},
|
148
|
+
:x => lambda{|y| (c - b * y) / a} })
|
149
|
+
when :e
|
150
|
+
if item[2] == -1 || item[3] == -1
|
151
|
+
from_vertex = voronoi_vertices[item[2] == -1 ? item[3] : item[2]]
|
152
|
+
|
153
|
+
next if from_vertex < RubyVor::Point.new(0,0)
|
154
|
+
|
155
|
+
if item[2] == -1
|
156
|
+
inf_vertex = RubyVor::Point.new(0, line_functions[item[1]][:y][0])
|
157
|
+
else
|
158
|
+
inf_vertex = RubyVor::Point.new(max_x, line_functions[item[1]][:y][max_x])
|
159
|
+
end
|
160
|
+
|
161
|
+
line = line_from_points(from_vertex, inf_vertex)
|
162
|
+
else
|
163
|
+
line = line_from_points(voronoi_vertices[item[2]], voronoi_vertices[item[3]])
|
164
|
+
end
|
165
|
+
|
166
|
+
line['style'] = "stroke:#{color};stroke-width:1;"
|
167
|
+
draw_lines << line
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
draw_lines.each {|l| doc.root << l}
|
173
|
+
draw_points.each {|p| doc.root << p}
|
174
|
+
end
|
175
|
+
|
176
|
+
# Now draw in nodes
|
177
|
+
computation.points.each do |point|
|
178
|
+
node = circle_from_point(point)
|
179
|
+
node['fill'] = 'lime'
|
180
|
+
|
181
|
+
doc.root << node
|
182
|
+
end
|
183
|
+
|
184
|
+
doc.root['width'] = (max_x + opts[:offset_x] + 50).to_s
|
185
|
+
doc.root['height'] = (max_y + opts[:offset_y] + 50).to_s
|
186
|
+
|
187
|
+
doc.save(opts[:name] || 'vddt.svg')
|
188
|
+
end
|
189
|
+
|
190
|
+
private
|
191
|
+
def self.line_from_points(a, b)
|
192
|
+
line = XML::Node.new('line')
|
193
|
+
line['x1'] = (a.x + @opts[:offset_x] + 10).to_s
|
194
|
+
line['y1'] = (a.y + @opts[:offset_y] + 10).to_s
|
195
|
+
line['x2'] = (b.x + @opts[:offset_x] + 10).to_s
|
196
|
+
line['y2'] = (b.y + @opts[:offset_y] + 10).to_s
|
197
|
+
line
|
198
|
+
end
|
199
|
+
|
200
|
+
def self.circle_from_point(point)
|
201
|
+
node = XML::Node.new('circle')
|
202
|
+
node['cx'] = (point.x + @opts[:offset_x] + 10).to_s
|
203
|
+
node['cy'] = (point.y + @opts[:offset_y] + 10).to_s
|
204
|
+
node['r'] = 5.to_s
|
205
|
+
node['stroke'] = 'black'
|
206
|
+
node['stroke-width'] = 2.to_s
|
207
|
+
node
|
208
|
+
end
|
209
|
+
|
210
|
+
def self.new_color
|
211
|
+
a = rand(256)
|
212
|
+
b = rand(256) | a
|
213
|
+
c = rand(256) ^ b
|
214
|
+
|
215
|
+
"rgb(#{[a,b,c].sort{|k,l| rand(3)-1}.join(',')})"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|