yohasebe-rubyfca 0.2.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Yoichiro Hasebe
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,45 @@
1
+ = RubyFCA
2
+
3
+ Command line tool for Formal Concept Analysis (FCA) written in Ruby.
4
+
5
+ == Features
6
+
7
+ * Convert a Conexp's CXT file and generate a Graphviz DOT file, or a PNG/JPG/EPS image file.
8
+ * Adopt the Ganter algorithm (through its Perl implementation of Fcastone by Uta Priss).
9
+
10
+ == Installation
11
+
12
+ Install the gem:
13
+
14
+ $sudo gem install rubyfca --source http://gems.github.com
15
+
16
+ == How to Use
17
+
18
+ rubyfca [options] <source file> <output file>
19
+
20
+ where:
21
+ <source file>
22
+ "foo.cxt"
23
+ <output file>
24
+ "bar.dot", "bar.png", "bar.jpg", or "bar.eps"
25
+ [options]:
26
+ --box, -b: Use box shaped concept nodes
27
+ --full, -f: Do not contract concept labels
28
+ --legend, -l: Print the legend of concept nodes (disabled when using circle node shape) (default: true)
29
+ --coloring, -c: Color concept nodes (default: true)
30
+ --straight, -s: Straighten edges (available when output format is either png, jpg, or eps)
31
+ --help, -h: Show this message
32
+
33
+
34
+ == ToDo
35
+
36
+ * Multiple input formats
37
+ * Database connection capability
38
+
39
+ == Links
40
+
41
+ under construction
42
+
43
+ == Copyright
44
+
45
+ Copyright (c) 2009 Yoichiro Hasebe and Kow Kuroda. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "rubyfca"
8
+ gem.summary = %Q{TODO: one-line summary of your gem}
9
+ gem.description = %Q{TODO: longer description of your gem}
10
+ gem.email = "yohasebe@gmail.com"
11
+ gem.homepage = "http://github.com/yohasebe/rubyfca"
12
+ gem.authors = ["Yoichiro Hasebe"]
13
+ gem.add_development_dependency "thoughtbot-shoulda"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/*_test.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ if File.exist?('VERSION')
47
+ version = File.read('VERSION')
48
+ else
49
+ version = ""
50
+ end
51
+
52
+ rdoc.rdoc_dir = 'rdoc'
53
+ rdoc.title = "rubyfca #{version}"
54
+ rdoc.rdoc_files.include('README*')
55
+ rdoc.rdoc_files.include('lib/**/*.rb')
56
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.3
data/bin/rubyfca ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
4
+ require 'trollop'
5
+ require 'rubyfca'
6
+ require 'ruby_graphviz'
7
+
8
+ ################ parse options ##########
9
+
10
+ opts = Trollop::options do
11
+ version = File.read(File.dirname(__FILE__) + "/../VERSION")
12
+ banner <<-EOS
13
+
14
+ RubuFCA converts Conexp CXT data to Graphviz dot format.
15
+
16
+ Usage:
17
+ rubyfca [options] <source file> <output file>
18
+
19
+ where:
20
+ <source file>
21
+ ".cxt"
22
+ <output file>
23
+ ."dot", ".png", ".jpg", or ".eps"
24
+ [options]:
25
+ EOS
26
+
27
+ opt :circle, "Use circle shaped concept nodes", :default=> false
28
+ opt :full, "Do not contract concept labels", :default=> false
29
+ opt :legend, "Print the legend of concept nodes (available only when using circle node shape)", :default => false
30
+ opt :coloring, "Color concept nodes", :default => false
31
+ opt :straight, "Straighten edges (available when output format is either png, jpg, or eps)", :default => false
32
+ end
33
+
34
+ ############### main program ###############
35
+
36
+ if ARGV.size != 2
37
+ showerror("Input and output files are not set properly", 1)
38
+ end
39
+
40
+ filename1 = ARGV[0] #input filename
41
+ filename2 = ARGV[1] #output filename
42
+
43
+ #
44
+ # extract input and output file types
45
+ #
46
+ input_type = filename1.slice(/\.[^\.]+\z/).split(//)[1..-1].join("")
47
+ output_type = filename2.slice(/\.[^\.]+\z/).split(//)[1..-1].join("")
48
+
49
+ if (input_type !~ /\A(cxt|csv)\z/ || output_type !~ /\A(dot|png|jpg|eps)\z/)
50
+ showerror("These file extensions are not (yet) supported.", 1)
51
+ end
52
+
53
+ #
54
+ # input cxt data is kept as plain text
55
+ #
56
+ f = File.open(filename1, "r")
57
+ inputdata = f.read
58
+ f.close
59
+
60
+ #
61
+ # ask for confirmation of overwriting an exisiting file
62
+ #
63
+ if (File.exist?(filename2) && !opts[:sil])
64
+ print "#{filename2} exists and will be overwritten, OK? [y/n]"
65
+ var1 = STDIN.gets;
66
+ if /y/i !~ var1
67
+ exit;
68
+ end
69
+ end
70
+
71
+ #
72
+ # context data is converted to a hash table
73
+ #
74
+ begin
75
+ ctxt = FormalContext.new(inputdata, opts[:full])
76
+ ctxt.calcurate
77
+ # rescue => e
78
+ # puts e
79
+ # showerror("Source data may have problems. Process aborted.", 1)
80
+ end
81
+
82
+ #
83
+ # create the output file
84
+ #
85
+ case output_type
86
+ when "dot"
87
+ File.open(filename2, "w") do |f|
88
+ f.write(ctxt.generate_dot(opts))
89
+ end
90
+ when "png"
91
+ ctxt.generate_img(filename2, "png", opts)
92
+ when "jpg"
93
+ ctxt.generate_img(filename2, "jpg", opts)
94
+ when "eps"
95
+ ctxt.generate_img(filename2, "eps", opts)
96
+ end
@@ -0,0 +1,167 @@
1
+ ## lib/ruby_graphviz.rb -- graphviz dot generator library
2
+ ## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
3
+ ## Copyright:: Copyright 2009 Yoichiro Hasebe
4
+ ## License:: GNU GPL version 3
5
+
6
+ class RubyGraphviz
7
+
8
+ ## Example:
9
+ ##
10
+ ## g = RubyGraphviz.new("newgraph", {:rankdir => "LR", :nodesep => "0.4", :ranksep => "0.2"})
11
+ ##
12
+ def initialize(name, graph_hash = nil)
13
+ @name = name
14
+ @graph_data = graph_hash
15
+ @nodes = []
16
+ @edges = []
17
+ @dot = ""
18
+ create_graph
19
+ end
20
+
21
+ protected
22
+
23
+ def create_graph
24
+ @dot << "graph #{@name} {\n graph"
25
+ index = 0
26
+ if @graph_data
27
+ @dot << " ["
28
+ @graph_data.each do |k, v|
29
+ k = k.to_s
30
+ @dot << "#{k} = \"#{v}\""
31
+ index += 1
32
+ @dot << ", " unless index == @graph_data.size
33
+ end
34
+ @dot << "]"
35
+ end
36
+ @dot << ";\n"
37
+ end
38
+
39
+ def finish_graph
40
+ @dot << "}\n"
41
+ end
42
+
43
+ def create_edge(edgetype, nid1, nid2, edge_hash = nil)
44
+ temp = " #{nid1.to_s} #{edgetype} #{nid2.to_s}"
45
+ index = 0
46
+ if edge_hash
47
+ temp << " ["
48
+ edge_hash.each do |k, v|
49
+ k = k.to_s
50
+ temp << "#{k} = \"#{v}\""
51
+ index += 1
52
+ temp << ", " unless index == edge_hash.size
53
+ end
54
+ temp << "]"
55
+ end
56
+ return temp
57
+ end
58
+
59
+ public
60
+
61
+ ## Add a subgraph to a graph (recursively)
62
+ ##
63
+ ## Example:
64
+ ##
65
+ ## graph1.subgraph(graph2)
66
+ ##
67
+ def subgraph(graph)
68
+ @dot << graph.to_dot.sub(/\Agraph/, "subgraph")
69
+ end
70
+
71
+ ## Set default options for nodes
72
+ ##
73
+ ## Example:
74
+ ##
75
+ ## graph.node_default(:shape => "record", :color => "gray60")
76
+ ##
77
+ def node_default(node_hash = nil)
78
+ @dot << " node["
79
+ index = 0
80
+ node_hash.each do |k, v|
81
+ k = k.to_s
82
+ @dot << "#{k} = \"#{v}\""
83
+ index += 1
84
+ @dot << ", " unless index == node_hash.size
85
+ end
86
+ @dot << "];\n"
87
+ self
88
+ end
89
+
90
+ ## Set default options for edges
91
+ ##
92
+ ## Example:
93
+ ##
94
+ ## graph.edge_default(:color => "gray60")
95
+ ##
96
+ def edge_default(edge_hash = nil)
97
+ @dot << " edge["
98
+ index = 0
99
+ edge_hash.each do |k, v|
100
+ k = k.to_s
101
+ @dot << "#{k} = \"#{v}\""
102
+ index += 1
103
+ @dot << ", " unless index == edge_hash.size
104
+ end
105
+ @dot << "];\n"
106
+ self
107
+ end
108
+
109
+ ## Create a node with its options
110
+ ##
111
+ ## Example:
112
+ ##
113
+ ## graph.node("node-01", :label => "Node 01", :fillcolor => "pink")
114
+ ##
115
+ def node(node_id, node_hash = nil)
116
+ @dot << " #{node_id.to_s}"
117
+ index = 0
118
+ if node_hash
119
+ @dot << " ["
120
+ node_hash.each do |k, v|
121
+ k = k.to_s
122
+ @dot << "#{k} = \"#{v}\""
123
+ index += 1
124
+ @dot << ", " unless index == node_hash.size
125
+ end
126
+ @dot << "]"
127
+ end
128
+ @dot << ";\n"
129
+ self
130
+ end
131
+
132
+ ## Create a non-directional edge (connection line between nodes) with its options
133
+ ##
134
+ ## Example:
135
+ ##
136
+ ## graph.edge("node-01", "node-02", :label => "connecting 1 and 2", :color => "lightblue")
137
+ ##
138
+ def edge(nid1, nid2, edge_hash = nil)
139
+ @dot << create_edge("--", nid1, nid2, edge_hash) + ";\n"
140
+ self
141
+ end
142
+
143
+ ## Create a directional edge (arrow from node to node) with its options
144
+ ##
145
+ ## Example:
146
+ ## graph.arrow_edge("node-01", "node-02", :label => "from 1 to 2", :color => "lightblue")
147
+ ##
148
+ def arrow_edge(nid1, nid2, edge_hash = nil)
149
+ @dot << create_edge("->", nid1, nid2, edge_hash) + ";\n"
150
+ self
151
+ end
152
+
153
+ ## Align nodes on the same rank connecting them with non-directional edges
154
+ ##
155
+ def rank(nid1, nid2, edge_hash = nil)
156
+ @dot << "{rank=same " + create_edge("--", nid1, nid2, edge_hash) + "}\n"
157
+ self
158
+ end
159
+
160
+ ## Convert graph into dot formatted data
161
+ ##
162
+ def to_dot
163
+ finish_graph
164
+ @dot = @dot.gsub(/\"\</m, "<").gsub(/\>\"/m, ">")
165
+ return @dot
166
+ end
167
+ end
data/lib/rubyfca.rb ADDED
@@ -0,0 +1,397 @@
1
+ ## lib/rubyfca.rb -- Formal Concept Analysis tool in Ruby
2
+ ## Author:: Yoichiro Hasebe (mailto: yohasebe@gmail.com)
3
+ ## Kow Kuroda (mailto: kuroda@nict.go.jp)
4
+ ## Copyright:: Copyright 2009 Yoichiro Hasebe and Kow Kuroda
5
+ ## License:: GNU GPL version 3
6
+
7
+ $KCODE ='utf-8'
8
+ require 'ruby_graphviz'
9
+
10
+ private
11
+
12
+ ## Take two arrays each consisting of 0s and 1s and create their logical disjunction
13
+ def create_and_ary(a, b)
14
+ return false if (s = a.size) != b.size
15
+ result = (0..(s-1)).to_a.map{|i| a[i].to_i & b[i].to_i}
16
+ return result
17
+ end
18
+
19
+ ## Take two arrays each consisting of 0s and 1s and create their logical conjunction
20
+ def create_or_ary(a, b)
21
+ return false if (s = a.size) != b.size
22
+ result = (0..(s-1)).to_a.map{|i| a[i].to_i | b[i].to_i}
23
+ return result
24
+ end
25
+
26
+ public
27
+
28
+ ## Take care of errors
29
+ def showerror(sentence, severity)
30
+ if severity == 0
31
+ puts "Warning: #{sentence} The output may not be meaningful."
32
+ elsif severity == 1
33
+ puts "Error: #{sentence} No output generated."
34
+ exit
35
+ end
36
+ end
37
+
38
+ ## Basic structure of the code is the same as Fcastone written in Perl by Uta Priss
39
+ class FormalContext
40
+
41
+ ## Converte cxt data to three basic structures of objects, attributes, and matrix
42
+ def initialize(input, label_contraction = true)
43
+ if input.size == 0
44
+ showerror("File is empty", 1)
45
+ end
46
+ lines = input.split
47
+ t1 = 3
48
+ if (lines[0] !~ /B/i || (2 * lines[1].to_i + lines[2].to_i + t1) != lines.size)
49
+ showerror("Wrong cxt format!", 1)
50
+ end
51
+
52
+ @label_contraction = label_contraction
53
+
54
+ @objects = lines[t1..(lines[1].to_i + t1 - 1)]
55
+ @attributes = lines[(lines[1].to_i + t1) .. (lines[1].to_i + lines[2].to_i + t1 - 1)]
56
+ lines = lines[(lines[1].to_i + lines[2].to_i + t1) .. lines.size]
57
+ @matrix = changecrosssymbol("X", "\\.", lines)
58
+ end
59
+
60
+ ## Apply a formal concept analysis on the matrix
61
+ def calcurate
62
+ @concepts, @extM, @intM = ganter_alg(@matrix)
63
+ @relM, @reltrans, @rank = create_rel(@intM)
64
+ @gammaM, @muM = gammaMu(@extM, @intM, @matrix)
65
+ end
66
+
67
+ ## This is an implementation of an algorithm described by Bernhard Ganter
68
+ ## in "Two basic algorithms in concept analysis." Technische Hochschule
69
+ ## Darmstadt, FB4-Preprint, 831, 1984.
70
+ def ganter_alg(matrix)
71
+
72
+ m = matrix
73
+
74
+ ## all arrays except @idx are arrays of arrays of 0's and 1's
75
+ idx = []
76
+ extension = []
77
+ intension = []
78
+ ext_A = []
79
+ int_B = []
80
+ endkey = []
81
+ temp = []
82
+
83
+ ## the level in the lattice from the top
84
+ lvl = 0
85
+ ## is lower than the index of leftmost attr
86
+ idx[lvl] = -1
87
+ ## number of attr. and objs
88
+ anzM = m.size
89
+ ## only needed for initialization
90
+ anzG = m[0].size
91
+ ## initialize extA[0] = [1,...,1]
92
+ anzG.times do |i|
93
+ if !ext_A[0]
94
+ ext_A[0] = []
95
+ end
96
+ ext_A[0] << 1
97
+ end
98
+ ## initialize extB[0] = [0,...,0]
99
+ anzM.times do |i|
100
+ if !int_B[0]
101
+ int_B[0] = []
102
+ end
103
+ int_B[0] << 0
104
+ end
105
+ anzCpt = 0
106
+ extension[0] = ext_A[0]
107
+ intension[0] = int_B[0]
108
+ anzM.times do |i|
109
+ endkey << 1
110
+ end
111
+
112
+ ## start of algorithm
113
+ while int_B[lvl] != endkey
114
+ (anzM - 1).downto(0) do |i|
115
+ breakkey = false
116
+ if (int_B[lvl][i] != 1)
117
+ while (i < idx[lvl])
118
+ lvl -= 1
119
+ end
120
+ idx[lvl + 1] = i
121
+ ext_A[lvl + 1] = create_and_ary(ext_A[lvl], m[i])
122
+ 0.upto(i - 1) do |j|
123
+ if(!breakkey && int_B[lvl][j] != 1)
124
+ temp = create_and_ary(ext_A[lvl + 1], m[j])
125
+ if temp == ext_A[lvl + 1]
126
+ breakkey = true
127
+ end
128
+ end
129
+ end
130
+ unless breakkey
131
+ int_B[lvl + 1] = int_B[lvl].dup
132
+ int_B[lvl + 1][i] = 1
133
+ (i+1).upto(anzM - 1) do |k|
134
+ if int_B[lvl + 1][k] != 1
135
+ temp = create_and_ary(ext_A[lvl + 1], m[k])
136
+ if temp == ext_A[lvl + 1]
137
+ int_B[lvl + 1][k] = 1
138
+ end
139
+ end
140
+ end
141
+ lvl += 1
142
+ anzCpt += 1
143
+ extension[anzCpt] = ext_A[lvl]
144
+ intension[anzCpt] = int_B[lvl]
145
+ break
146
+ end
147
+ end
148
+ end
149
+ end
150
+
151
+ a1 = extension[0].join("")
152
+ a2 = extension[1].join("")
153
+ if a1 == a2
154
+ shift extension
155
+ shift intension
156
+ anzCpt -= 1
157
+ end
158
+
159
+ c = []
160
+ 0.upto(anzCpt) do |i|
161
+ c[i] = i
162
+ end
163
+
164
+ [c, intension, extension]
165
+ end
166
+
167
+ ## Output arrayconsists of the following:
168
+ ## r (subconcept superconcept relation)
169
+ ## rt (trans. closure of r)
170
+ ## s (ranked concepts)
171
+ def create_rel(intensions)
172
+ anzCpt = intensions.size
173
+ rank = []
174
+ sup_con = []
175
+ r = []
176
+ rt = []
177
+ s = []
178
+
179
+ 0.upto(anzCpt - 1) do |i|
180
+ 0.upto(anzCpt - 1) do |j|
181
+ unless r[i]
182
+ r[i] = []
183
+ end
184
+ r[i][j] = 0;
185
+ unless rt[i]
186
+ rt[i] = []
187
+ end
188
+ rt[i][j] = 0;
189
+ end
190
+ end
191
+
192
+ 1.upto(anzCpt - 1) do |i|
193
+ rank[i] = 1
194
+ (i - 1).downto(0) do |j|
195
+ temp = create_and_ary(intensions[j], intensions[i])
196
+ if temp == intensions[i]
197
+ unless sup_con[i]
198
+ sup_con[i] = []
199
+ end
200
+ sup_con[i] << j
201
+ r[i][j] = 1
202
+ rt[i][j] = 1
203
+ sup_con[i].each do |elem|
204
+ if r[elem][j] == 1
205
+ r[i][j] = 0
206
+ if rank[elem] >= rank [i]
207
+ rank[i] = rank[elem] + 1
208
+ end
209
+ break
210
+ end
211
+ end
212
+ end
213
+ end
214
+ unless s[rank[i]]
215
+ s[rank[i]] = []
216
+ end
217
+ s[rank[i]] << i
218
+ end
219
+ s = s.collect do |i|
220
+ i ? i : [0]
221
+ end
222
+
223
+ [r, rt, s]
224
+ end
225
+
226
+ def gammaMu(extent, intent, cxt)
227
+
228
+ gamma = []
229
+ mu = []
230
+ invcxt = []
231
+ 0.upto(cxt[0].size - 1) do |i|
232
+ 0.upto(cxt.size - 1) do |k|
233
+ invcxt[i] = [] unless invcxt[i]
234
+ invcxt[i][k] = cxt[k][i]
235
+ end
236
+ end
237
+
238
+ 0.upto(intent.size - 1) do |j|
239
+ 0.upto(cxt.size - 1) do |i|
240
+ gamma[i] = [] unless gamma[i]
241
+ if cxt[i] == intent[j]
242
+ gamma[i][j] = 2
243
+ elsif (!@label_contraction && create_or_ary(cxt[i], intent[j]) == cxt[i])
244
+ gamma[i][j] = 1
245
+ else
246
+ gamma[i][j] = 0
247
+ end
248
+ end
249
+
250
+ 0.upto(invcxt.size - 1) do |i|
251
+ # next unless invcxt[i]
252
+ mu[i] = [] unless mu[i]
253
+ if invcxt[i] == extent[j]
254
+ mu[i][j] = 2
255
+ elsif (!@label_contraction && create_or_ary(invcxt[i], extent[j]) == invcxt[i])
256
+ mu[i][j] = 1
257
+ else
258
+ mu[i][j] = 0
259
+ end
260
+ end
261
+ end
262
+
263
+ [gamma, mu]
264
+ end
265
+
266
+ def changecrosssymbol(char1, char2, lns)
267
+ rel = []
268
+ lns.each do |ln|
269
+ ary = []
270
+ elems = ln.split(//)
271
+ elems.each do |elem|
272
+ if /#{char1}/i =~ elem
273
+ ary << 1
274
+ elsif /#{char2}/i =~ elem
275
+ ary << 0
276
+ end
277
+ end
278
+ rel << ary
279
+ end
280
+ rel
281
+ end
282
+
283
+ ## Generate Graphviz dot data (not creating a file)
284
+ ## For options, see 'rubyfca'
285
+ def generate_dot(opts)
286
+ index_max_width = @concepts.size.to_s.split(//).size
287
+
288
+ clattice = RubyGraphviz.new("clattice", :rankdir => "", :nodesep => "0.4", :ranksep => "0.2")
289
+
290
+ if opts[:circle] and opts[:legend]
291
+ legend = RubyGraphviz.new("legend", :rankdir => "TB", :lebelloc => "t", :centered => "false")
292
+ legend.node_default(:shape => "plaintext")
293
+ legend.edge_default(:color => "gray60") if opts[:coloring]
294
+ legends = []
295
+ end
296
+
297
+ if opts[:circle]
298
+ clattice.node_default(:shape => "circle", :style => "filled")
299
+ clattice.edge_default(:dir => "none", :minlen => "2")
300
+ clattice.edge_default(:color => "gray60") if opts[:coloring]
301
+ else
302
+ clattice.node_default(:shape => "record", :margin => "0.2,0.055")
303
+ clattice.edge_default(:dir => "none")
304
+ clattice.edge_default(:color => "gray60") if opts[:coloring]
305
+ end
306
+
307
+ 0.upto(@concepts.size - 1) do |i|
308
+ objfull = []
309
+ attrfull = []
310
+ 0.upto(@gammaM.size - 1) do |j|
311
+ if @gammaM[j][i] == 2
312
+ obj = opts[:full] ? @objects[j] + " " + [0x261C].pack("U") : @objects[j]
313
+ objfull << obj
314
+ elsif @gammaM[j][i] == 1
315
+ objfull << @objects[j]
316
+ end
317
+ end
318
+ 0.upto(@muM.size - 1) do |k|
319
+ if @muM[k][i] == 2
320
+ att = opts[:full] ? @attributes[k] + " " + [0x261C].pack("U") : @attributes[k]
321
+ attrfull << att
322
+ elsif @muM[k][i] == 1
323
+ attrfull << @attributes[k]
324
+ end
325
+ end
326
+
327
+ concept_id = i + 1
328
+
329
+ attr_str = attrfull.join('<br />')
330
+ attr_str = attr_str == "" ? " " : attr_str
331
+ attr_color = (!opts[:coloring] || /\A\s+\z/ =~ attr_str) ? "white" : "lightblue"
332
+ obj_str = objfull.join('<br />')
333
+ obj_str = obj_str == "" ? " " : obj_str
334
+ obj_color = (!opts[:coloring] || /\A\s+\z/ =~ obj_str) ? "white" : "pink"
335
+
336
+ label = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
337
+ "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" +
338
+ "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" +
339
+ "</table>>"
340
+
341
+ if opts[:circle] and opts[:legend]
342
+
343
+ leg = "<<table border=\"0\" cellborder=\"1\" cellspacing=\"0\">" +
344
+ "<tr><td rowspan=\"2\">#{concept_id}</td><td balign=\"left\" align=\"left\" bgcolor=\"#{attr_color}\">#{attr_str}</td></tr>" +
345
+ "<tr><td balign=\"left\" align=\"left\" bgcolor=\"#{obj_color}\">#{obj_str}</td></tr>" +
346
+ "</table>>"
347
+
348
+ if !attrfull.empty? and !objfull.empty?
349
+ legend.node("cl#{concept_id}k", :label => concept_id, :style => "invis")
350
+ legend.node("cl#{concept_id}v", :label => leg, :fillcolor => "white")
351
+ legend.rank("cl#{concept_id}k", "cl#{concept_id}v", :style => "invis", :length => "0.0")
352
+ if legends[-1]
353
+ legend.edge("cl#{legends[-1]}k", "cl#{concept_id}k", :style => "invis", :length => "0.0")
354
+ end
355
+ legends << concept_id
356
+ end
357
+ end
358
+
359
+ if opts[:circle]
360
+ clattice.node("c#{i}", :width => "0.5", :fontsize => "14.0", :label => concept_id)
361
+ else
362
+ clattice.node("c#{i}", :label => label, :shape => "plaintext",
363
+ :height => "0.0", :width => "0.0", :margin => "0.0")
364
+ end
365
+ end
366
+
367
+ 0.upto(@relM.size - 1) do |i|
368
+ 0.upto(@relM.size - 1) do |j|
369
+ if @relM[i][j] == 1
370
+ clattice.edge("c#{i}", "c#{j}")
371
+ end
372
+ end
373
+ end
374
+
375
+ clattice.subgraph(legend) if opts[:circle] and opts[:legend]
376
+ clattice.to_dot
377
+ end
378
+
379
+ ## Generate an actual graphic file (Graphviz dot needs to be installed properly)
380
+ def generate_img(outfile, image_type, opts)
381
+ dot = generate_dot(opts)
382
+ isthere_dot = `dot -V 2>&1`
383
+ if isthere_dot !~ /dot.*version/i
384
+ showerror("Graphviz's dot program cannot be found.", 1)
385
+ else
386
+ if opts[:straight]
387
+ cmd = "dot | neato -n -T#{image_type} -o#{outfile} 2>rubyfca.log"
388
+ else
389
+ cmd = "dot -T#{image_type} -o#{outfile} 2>rubyfca.log"
390
+ end
391
+ IO.popen(cmd, 'r+') do |io|
392
+ io.puts dot
393
+ end
394
+ end
395
+ end
396
+ end
397
+