bribera-rubyvor 0.0.2 → 0.1.1
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.
- data/History.txt +45 -0
- data/Manifest.txt +12 -2
- data/README.txt +32 -1
- data/Rakefile +2 -3
- data/ext/extconf.rb +2 -2
- data/ext/memory.c +21 -21
- data/ext/output.c +19 -28
- data/ext/rb_cComputation.c +370 -0
- data/ext/rb_cPoint.c +35 -0
- data/ext/rb_cPriorityQueue.c +120 -0
- data/ext/ruby_vor_c.c +106 -0
- data/ext/ruby_vor_c.h +36 -0
- data/ext/vdefs.h +3 -2
- data/ext/voronoi.c +57 -65
- data/lib/ruby_vor.rb +9 -6
- data/lib/ruby_vor/computation.rb +137 -0
- data/lib/ruby_vor/geo_ruby_extensions.rb +15 -0
- data/lib/ruby_vor/point.rb +24 -11
- data/lib/ruby_vor/priority_queue.rb +84 -0
- data/lib/ruby_vor/version.rb +1 -1
- data/lib/ruby_vor/visualizer.rb +218 -0
- data/rubyvor.gemspec +8 -6
- data/test/test_computation.rb +344 -0
- data/test/test_point.rb +100 -0
- data/test/test_priority_queue.rb +129 -0
- data/test/test_voronoi_interface.rb +12 -9
- metadata +17 -4
- data/ext/voronoi_interface.c +0 -213
- data/lib/ruby_vor/decomposition.rb +0 -22
data/lib/ruby_vor.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
$:.unshift File.dirname(__FILE__)
|
2
2
|
$:.unshift File.join(File.dirname(__FILE__), '..', 'ext')
|
3
|
-
|
3
|
+
|
4
4
|
require 'ruby_vor/version'
|
5
5
|
require 'ruby_vor/point'
|
6
|
-
require 'ruby_vor/
|
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'
|
7
13
|
|
14
|
+
# DOC HERE
|
8
15
|
module RubyVor
|
9
|
-
def self.test_l
|
10
|
-
VDDT::Decomposition.from_points(Point.test_large)
|
11
|
-
end
|
12
|
-
|
13
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
|
data/lib/ruby_vor/point.rb
CHANGED
@@ -1,19 +1,32 @@
|
|
1
1
|
module RubyVor
|
2
2
|
class Point
|
3
|
-
|
3
|
+
attr_reader :x, :y
|
4
4
|
def initialize(x=0.0,y=0.0)
|
5
|
-
|
6
|
-
@
|
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
|
7
8
|
end
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
a = []
|
12
|
-
1000000.times {
|
13
|
-
a << new(100000.0 * rand, 100000.0 * rand)
|
14
|
-
}
|
15
|
-
a
|
16
|
-
end
|
10
|
+
def <=>(p)
|
11
|
+
(@x != p.x) ? @x <=> p.x : @y <=> p.y
|
17
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
|
+
|
18
31
|
end
|
19
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
|
data/lib/ruby_vor/version.rb
CHANGED
@@ -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
|