03ca3eb7bf0591785d90dd414c33924b19e7f6b7
[nit.git] / src / model / model_viz.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Visualisation of Nit models
16 module model_viz
17
18 import model
19 import ordered_tree
20
21 # A simple specialisation of OrderedTree to display projects, groups and modules
22 # FIXME do not use Object, but a better common interface of MModule and MGroup
23 class MProjectTree
24 super OrderedTree[Object]
25
26 # The model where to look for information
27 var model: Model
28
29 init(model: Model) do self.model = model
30
31 redef fun display(a) do
32 if a isa MGroup then
33 if a.parent == null then return "{a.mproject.name} ({a.filepath.to_s})"
34 return a.name + " (group)"
35 else if a isa MModule then
36 return a.name
37 else
38 abort
39 end
40 end
41
42 private var linex_comparator: nullable LinexComparator = null
43
44 # Sort modules and groups with their names
45 fun sort_with_alpha
46 do
47 sort_with(alpha_comparator)
48 end
49
50 # Sort modules and groups with a loosely adaptation of the linearization of modules
51 fun sort_with_linex
52 do
53 var c = linex_comparator
54 if c == null then
55 c = new LinexComparator(self)
56 linex_comparator = c
57 end
58 sort_with(c)
59 end
60 end
61
62 # Compare modules and groups using the
63 # FIXME do not use Object, but a better common interface of MModule and MGroup
64 private class LinexComparator
65 super Comparator
66
67 redef type COMPARED: Object
68
69 var mins = new HashMap [MGroup, nullable MModule]
70 var maxs = new HashMap [MGroup, nullable MModule]
71 fun mini(o: Object): nullable MModule do
72 if o isa MModule then return o
73 assert o isa MGroup
74 if not mins.has_key(o) then computeminmax(o)
75 return mins[o]
76 end
77 fun maxi(o: Object): nullable MModule do
78 if o isa MModule then return o
79 assert o isa MGroup
80 if not maxs.has_key(o) then computeminmax(o)
81 return maxs[o]
82 end
83 fun computeminmax(o: MGroup) do
84 if not tree.sub.has_key(o) then
85 mins[o] = null
86 maxs[o] = null
87 return
88 end
89 var subs = tree.sub[o]
90 var minres = mini(subs.first)
91 var maxres = maxi(subs.first)
92 var order = minres.model.mmodule_importation_hierarchy
93 for o2 in subs do
94 var c = mini(o2)
95 if c == null then continue
96 if minres == null or order.compare(minres, c) > 0 then minres = c
97 c = maxi(o2)
98 if c == null then continue
99 if maxres == null or order.compare(maxres, c) < 0 then maxres = c
100 end
101 mins[o] = minres
102 maxs[o] = maxres
103 #if minres != maxres then print "* {o} {minres}..{maxres}"
104 end
105 redef fun compare(a,b) do
106 var ma = mini(a)
107 var mb = mini(b)
108 if ma == null then
109 if mb == null then return 0 else return -1
110 else if mb == null then
111 return 1
112 end
113 var order = ma.model.mmodule_importation_hierarchy
114 return order.compare(ma, mb)
115 end
116 var tree: OrderedTree[Object]
117 end
118
119 redef class Model
120 # Generate a MProjectTree based on the projects, groups and modules known in the model
121 fun to_mproject_tree: MProjectTree
122 do
123 var res = new MProjectTree(self)
124 for p in mprojects do
125 for g in p.mgroups do
126 res.add(g.parent, g)
127 for m in g.mmodules do
128 res.add(g, m)
129 end
130 end
131 end
132 return res
133 end
134 end
135
136 # Generate graphiz files based on projects, groups and modules
137 #
138 # Interesting elements must be selected. See `mmodules`, ``
139 # Display configuration can be set. See `cluster_group`, `project_group`
140 class MProjectDot
141 super Streamable
142
143 # The model where to look for information
144 var model: Model
145
146 # Set of projects to expand fully (ie all groups and modules are displayed)
147 # Initially empty, projects can be added
148 var mprojects = new HashSet[MProject]
149
150 # Set of modules to display
151 # Initially empty, modules can be added
152 var mmodules = new HashSet[MModule]
153
154 private fun node_for(mmodule: MModule): String
155 do
156 return "m_{mmodule.object_id}"
157 end
158
159 # Should groups be shown as clusters?
160 var cluster_group = true is writable
161
162 # Should projects be shown as clusters?
163 var project_group = true is writable
164
165 # Recursively generate node and clusters for a mgroup
166 private fun dot_cluster(o: OStream, mgroup: MGroup)
167 do
168 # Open the cluster, if required
169 if mgroup.parent == null then
170 # is is a root group, so display the project
171 if project_group then
172 o.write("subgraph cluster_{mgroup.object_id} \{\nlabel=\"{mgroup.mproject.name}\\n({mgroup.filepath.to_s})\"\ncolor=black\nstyle=dotted\n")
173 end
174 else
175 if cluster_group then
176 o.write("subgraph cluster_{mgroup.object_id} \{\nlabel=\"{mgroup.name}\"\ncolor=blue\nstyle=dotted\n")
177 end
178 end
179
180 # outputs the mmodules to show
181 for mmodule in mgroup.mmodules do
182 if not mmodules.has(mmodule) then continue
183 o.write("\t{node_for(mmodule)} [label=\"{mmodule.name}\",color=green]\n")
184 end
185
186 # recursively progress on sub-clusters
187 for d in mgroup.in_nesting.direct_smallers do
188 dot_cluster(o, d)
189 end
190
191 # close the cluster if required
192 if mgroup.parent == null then
193 if project_group then o.write("\}\n")
194 else
195 if cluster_group then o.write("\}\n")
196 end
197 end
198
199 # Extends the set of `mmodules` by recursively adding the most specific imported modules of foreign projects
200 fun collect_important_importation
201 do
202 var todo = new List[MModule]
203 todo.add_all(mmodules)
204 while not todo.is_empty do
205 var m = todo.pop
206
207 for psm in m.in_importation.greaters do
208 if m.mgroup.mproject != psm.mgroup.mproject then continue
209 for ssm in psm.in_importation.direct_greaters do
210 if psm.mgroup.mproject == ssm.mgroup.mproject then continue
211 mmodules.add(ssm)
212 todo.add(ssm)
213 end
214 end
215 end
216 end
217
218 # Generate the dot content with the current configuration
219 redef fun write_to(stream)
220 do
221 # Collect interesting nodes
222 for m in model.mmodules do
223 # filter out modules outside wanted projects
224 if m.mgroup == null then continue
225 if not mprojects.has(m.mgroup.mproject) then continue
226
227 mmodules.add(m)
228 end
229
230 collect_important_importation
231
232 # Collect interesting edges
233 var sub_hierarchy = new POSet[MModule]
234 for m in mmodules do
235 sub_hierarchy.add_node(m)
236 for sm in m.in_importation.greaters do
237 if sm == m then continue
238 if not mmodules.has(sm) then continue
239 sub_hierarchy.add_edge(m, sm)
240 end
241 end
242
243 stream.write("digraph g \{\n")
244 stream.write("rankdir=BT;node[shape=box];\n")
245 # Generate the nodes
246 for p in model.mprojects do
247 dot_cluster(stream, p.root.as(not null))
248 end
249 # Generate the edges
250 for m in mmodules do
251 for sm in sub_hierarchy[m].direct_greaters do
252 var nm = node_for(m)
253 var nsm = node_for(sm)
254 if m.in_importation.direct_greaters.has(sm) then
255 stream.write("\t{nm} -> {nsm}[style=bold]\n")
256 else
257 stream.write("\t{nm} -> {nsm}[style=solid]\n")
258 end
259 end
260 end
261 stream.write("\}\n")
262 end
263 end