source: extensions/gsdl-video/trunk/installed/cmdline/lib/ruby/1.8/rdoc/diagram.rb@ 18425

Last change on this file since 18425 was 18425, checked in by davidb, 15 years ago

Video extension to Greenstone

File size: 11.9 KB
Line 
1# A wonderful hack by to draw package diagrams using the dot package.
2# Originally written by Jah, team Enticla.
3#
4# You must have the V1.7 or later in your path
5# http://www.research.att.com/sw/tools/graphviz/
6
7require "rdoc/dot/dot"
8require 'rdoc/options'
9
10module RDoc
11
12 # Draw a set of diagrams representing the modules and classes in the
13 # system. We draw one diagram for each file, and one for each toplevel
14 # class or module. This means there will be overlap. However, it also
15 # means that you'll get better context for objects.
16 #
17 # To use, simply
18 #
19 # d = Diagram.new(info) # pass in collection of top level infos
20 # d.draw
21 #
22 # The results will be written to the +dot+ subdirectory. The process
23 # also sets the +diagram+ attribute in each object it graphs to
24 # the name of the file containing the image. This can be used
25 # by output generators to insert images.
26
27 class Diagram
28
29 FONT = "Arial"
30
31 DOT_PATH = "dot"
32
33 # Pass in the set of top level objects. The method also creates
34 # the subdirectory to hold the images
35
36 def initialize(info, options)
37 @info = info
38 @options = options
39 @counter = 0
40 File.makedirs(DOT_PATH)
41 @diagram_cache = {}
42 end
43
44 # Draw the diagrams. We traverse the files, drawing a diagram for
45 # each. We also traverse each top-level class and module in that
46 # file drawing a diagram for these too.
47
48 def draw
49 unless @options.quiet
50 $stderr.print "Diagrams: "
51 $stderr.flush
52 end
53
54 @info.each_with_index do |i, file_count|
55 @done_modules = {}
56 @local_names = find_names(i)
57 @global_names = []
58 @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
59 'fontname' => FONT,
60 'fontsize' => '8',
61 'bgcolor' => 'lightcyan1',
62 'compound' => 'true')
63
64 # it's a little hack %) i'm too lazy to create a separate class
65 # for default node
66 graph << DOT::DOTNode.new('name' => 'node',
67 'fontname' => FONT,
68 'color' => 'black',
69 'fontsize' => 8)
70
71 i.modules.each do |mod|
72 draw_module(mod, graph, true, i.file_relative_name)
73 end
74 add_classes(i, graph, i.file_relative_name)
75
76 i.diagram = convert_to_png("f_#{file_count}", graph)
77
78 # now go through and document each top level class and
79 # module independently
80 i.modules.each_with_index do |mod, count|
81 @done_modules = {}
82 @local_names = find_names(mod)
83 @global_names = []
84
85 @global_graph = graph = DOT::DOTDigraph.new('name' => 'TopLevel',
86 'fontname' => FONT,
87 'fontsize' => '8',
88 'bgcolor' => 'lightcyan1',
89 'compound' => 'true')
90
91 graph << DOT::DOTNode.new('name' => 'node',
92 'fontname' => FONT,
93 'color' => 'black',
94 'fontsize' => 8)
95 draw_module(mod, graph, true)
96 mod.diagram = convert_to_png("m_#{file_count}_#{count}",
97 graph)
98 end
99 end
100 $stderr.puts unless @options.quiet
101 end
102
103 #######
104 private
105 #######
106
107 def find_names(mod)
108 return [mod.full_name] + mod.classes.collect{|cl| cl.full_name} +
109 mod.modules.collect{|m| find_names(m)}.flatten
110 end
111
112 def find_full_name(name, mod)
113 full_name = name.dup
114 return full_name if @local_names.include?(full_name)
115 mod_path = mod.full_name.split('::')[0..-2]
116 unless mod_path.nil?
117 until mod_path.empty?
118 full_name = mod_path.pop + '::' + full_name
119 return full_name if @local_names.include?(full_name)
120 end
121 end
122 return name
123 end
124
125 def draw_module(mod, graph, toplevel = false, file = nil)
126 return if @done_modules[mod.full_name] and not toplevel
127
128 @counter += 1
129 url = mod.http_url("classes")
130 m = DOT::DOTSubgraph.new('name' => "cluster_#{mod.full_name.gsub( /:/,'_' )}",
131 'label' => mod.name,
132 'fontname' => FONT,
133 'color' => 'blue',
134 'style' => 'filled',
135 'URL' => %{"#{url}"},
136 'fillcolor' => toplevel ? 'palegreen1' : 'palegreen3')
137
138 @done_modules[mod.full_name] = m
139 add_classes(mod, m, file)
140 graph << m
141
142 unless mod.includes.empty?
143 mod.includes.each do |m|
144 m_full_name = find_full_name(m.name, mod)
145 if @local_names.include?(m_full_name)
146 @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
147 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
148 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}",
149 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
150 else
151 unless @global_names.include?(m_full_name)
152 path = m_full_name.split("::")
153 url = File.join('classes', *path) + ".html"
154 @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
155 'shape' => 'box',
156 'label' => "#{m_full_name}",
157 'URL' => %{"#{url}"})
158 @global_names << m_full_name
159 end
160 @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
161 'to' => "#{mod.full_name.gsub( /:/,'_' )}",
162 'lhead' => "cluster_#{mod.full_name.gsub( /:/,'_' )}")
163 end
164 end
165 end
166 end
167
168 def add_classes(container, graph, file = nil )
169
170 use_fileboxes = Options.instance.fileboxes
171
172 files = {}
173
174 # create dummy node (needed if empty and for module includes)
175 if container.full_name
176 graph << DOT::DOTNode.new('name' => "#{container.full_name.gsub( /:/,'_' )}",
177 'label' => "",
178 'width' => (container.classes.empty? and
179 container.modules.empty?) ?
180 '0.75' : '0.01',
181 'height' => '0.01',
182 'shape' => 'plaintext')
183 end
184 container.classes.each_with_index do |cl, cl_index|
185 last_file = cl.in_files[-1].file_relative_name
186
187 if use_fileboxes && !files.include?(last_file)
188 @counter += 1
189 files[last_file] =
190 DOT::DOTSubgraph.new('name' => "cluster_#{@counter}",
191 'label' => "#{last_file}",
192 'fontname' => FONT,
193 'color'=>
194 last_file == file ? 'red' : 'black')
195 end
196
197 next if cl.name == 'Object' || cl.name[0,2] == "<<"
198
199 url = cl.http_url("classes")
200
201 label = cl.name.dup
202 if use_fileboxes && cl.in_files.length > 1
203 label << '\n[' +
204 cl.in_files.collect {|i|
205 i.file_relative_name
206 }.sort.join( '\n' ) +
207 ']'
208 end
209
210 attrs = {
211 'name' => "#{cl.full_name.gsub( /:/, '_' )}",
212 'fontcolor' => 'black',
213 'style'=>'filled',
214 'color'=>'palegoldenrod',
215 'label' => label,
216 'shape' => 'ellipse',
217 'URL' => %{"#{url}"}
218 }
219
220 c = DOT::DOTNode.new(attrs)
221
222 if use_fileboxes
223 files[last_file].push c
224 else
225 graph << c
226 end
227 end
228
229 if use_fileboxes
230 files.each_value do |val|
231 graph << val
232 end
233 end
234
235 unless container.classes.empty?
236 container.classes.each_with_index do |cl, cl_index|
237 cl.includes.each do |m|
238 m_full_name = find_full_name(m.name, cl)
239 if @local_names.include?(m_full_name)
240 @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
241 'to' => "#{cl.full_name.gsub( /:/,'_' )}",
242 'ltail' => "cluster_#{m_full_name.gsub( /:/,'_' )}")
243 else
244 unless @global_names.include?(m_full_name)
245 path = m_full_name.split("::")
246 url = File.join('classes', *path) + ".html"
247 @global_graph << DOT::DOTNode.new('name' => "#{m_full_name.gsub( /:/,'_' )}",
248 'shape' => 'box',
249 'label' => "#{m_full_name}",
250 'URL' => %{"#{url}"})
251 @global_names << m_full_name
252 end
253 @global_graph << DOT::DOTEdge.new('from' => "#{m_full_name.gsub( /:/,'_' )}",
254 'to' => "#{cl.full_name.gsub( /:/, '_')}")
255 end
256 end
257
258 sclass = cl.superclass
259 next if sclass.nil? || sclass == 'Object'
260 sclass_full_name = find_full_name(sclass,cl)
261 unless @local_names.include?(sclass_full_name) or @global_names.include?(sclass_full_name)
262 path = sclass_full_name.split("::")
263 url = File.join('classes', *path) + ".html"
264 @global_graph << DOT::DOTNode.new(
265 'name' => "#{sclass_full_name.gsub( /:/, '_' )}",
266 'label' => sclass_full_name,
267 'URL' => %{"#{url}"})
268 @global_names << sclass_full_name
269 end
270 @global_graph << DOT::DOTEdge.new('from' => "#{sclass_full_name.gsub( /:/,'_' )}",
271 'to' => "#{cl.full_name.gsub( /:/, '_')}")
272 end
273 end
274
275 container.modules.each do |submod|
276 draw_module(submod, graph)
277 end
278
279 end
280
281 def convert_to_png(file_base, graph)
282 str = graph.to_s
283 return @diagram_cache[str] if @diagram_cache[str]
284 op_type = Options.instance.image_format
285 dotfile = File.join(DOT_PATH, file_base)
286 src = dotfile + ".dot"
287 dot = dotfile + "." + op_type
288
289 unless @options.quiet
290 $stderr.print "."
291 $stderr.flush
292 end
293
294 File.open(src, 'w+' ) do |f|
295 f << str << "\n"
296 end
297
298 system "dot", "-T#{op_type}", src, "-o", dot
299
300 # Now construct the imagemap wrapper around
301 # that png
302
303 ret = wrap_in_image_map(src, dot)
304 @diagram_cache[str] = ret
305 return ret
306 end
307
308 # Extract the client-side image map from dot, and use it
309 # to generate the imagemap proper. Return the whole
310 # <map>..<img> combination, suitable for inclusion on
311 # the page
312
313 def wrap_in_image_map(src, dot)
314 res = %{<map id="map" name="map">\n}
315 dot_map = `dot -Tismap #{src}`
316 dot_map.each do |area|
317 unless area =~ /^rectangle \((\d+),(\d+)\) \((\d+),(\d+)\) ([\/\w.]+)\s*(.*)/
318 $stderr.puts "Unexpected output from dot:\n#{area}"
319 return nil
320 end
321
322 xs, ys = [$1.to_i, $3.to_i], [$2.to_i, $4.to_i]
323 url, area_name = $5, $6
324
325 res << %{ <area shape="rect" coords="#{xs.min},#{ys.min},#{xs.max},#{ys.max}" }
326 res << %{ href="#{url}" alt="#{area_name}" />\n}
327 end
328 res << "</map>\n"
329# map_file = src.sub(/.dot/, '.map')
330# system("dot -Timap #{src} -o #{map_file}")
331 res << %{<img src="#{dot}" usemap="#map" border="0" alt="#{dot}">}
332 return res
333 end
334 end
335end
Note: See TracBrowser for help on using the repository browser.