src: add model_viz.nit
authorJean Privat <jean@pryen.org>
Wed, 29 Jan 2014 16:36:59 +0000 (11:36 -0500)
committerJean Privat <jean@pryen.org>
Wed, 5 Feb 2014 19:57:31 +0000 (14:57 -0500)
Change-Id: Ieda8600a0da5404c1ee744c7f56b28e8f417a76f
Signed-off-by: Jean Privat <jean@pryen.org>

src/model_viz.nit [new file with mode: 0644]

diff --git a/src/model_viz.nit b/src/model_viz.nit
new file mode 100644 (file)
index 0000000..1471a49
--- /dev/null
@@ -0,0 +1,269 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Visualisation of Nit models
+module model_viz
+
+import model
+import ordered_tree
+
+# A simple specialisation of OrderedTree to display projects, groups and modules
+# FIXME do not use Object, but a better common interface of MModule and MGroup
+class MProjectTree
+       super OrderedTree[Object]
+
+       # The model where to look for information
+       var model: Model
+
+       init(model: Model) do self.model = model
+
+       redef fun display(a) do
+               if a isa MGroup then
+                       if a.parent == null then return "{a.mproject.name} ({a.filepath})"
+                       return a.name + " (group)"
+               else if a isa MModule then
+                       return a.name
+               else
+                       abort
+               end
+       end
+
+       var alpha_comparator = new AlphaComparator
+
+       var linex_comparator: nullable LinexComparator = null
+
+       # Sort modules and groups with their names
+       fun sort_with_alpha
+       do
+               sort_with(alpha_comparator)
+       end
+
+       # Sort modules and groups with a loosly adaptation of the linerarisation of modules
+       fun sort_with_linex
+       do
+               var c = linex_comparator
+               if c == null then
+                       c = new LinexComparator(self)
+                       linex_comparator = c
+               end
+               sort_with(c)
+       end
+end
+
+# Just compare objects by using the `to_s` method
+private class AlphaComparator
+       super AbstractSorter[Object]
+       redef fun compare(a,b) do return a.to_s <=> b.to_s
+end
+
+# Compare modules and groups using the
+# FIXME do not use Object, but a better common interface of MModule and MGroup
+private class LinexComparator
+       super AbstractSorter[Object]
+       var mins = new HashMap [MGroup, nullable MModule]
+       var maxs = new HashMap [MGroup, nullable MModule]
+       fun min(o: Object): nullable MModule do
+               if o isa MModule then return o
+               assert o isa MGroup
+               if not mins.has_key(o) then computeminmax(o)
+               return mins[o]
+       end
+       fun max(o: Object): nullable MModule do
+               if o isa MModule then return o
+               assert o isa MGroup
+               if not maxs.has_key(o) then computeminmax(o)
+               return maxs[o]
+       end
+       fun computeminmax(o: MGroup) do
+               if not tree.sub.has_key(o) then
+                       mins[o] = null
+                       maxs[o] = null
+                       return
+               end
+               var subs = tree.sub[o]
+               var minres = min(subs.first)
+               var maxres = max(subs.first)
+               var order = minres.model.mmodule_importation_hierarchy
+               for o2 in subs do
+                       var c = min(o2)
+                       if c == null then continue
+                       if minres == null or order.compare(minres, c) > 0 then minres = c
+                       c = max(o2)
+                       if c == null then continue
+                       if maxres == null or order.compare(maxres, c) < 0 then maxres = c
+               end
+               mins[o] = minres
+               maxs[o] = maxres
+               #if minres != maxres then print "* {o} {minres}..{maxres}"
+       end
+       redef fun compare(a,b) do
+               var ma = min(a)
+               var mb = min(b)
+               if ma == null then
+                       if mb == null then return 0 else return -1
+               else if mb == null then
+                       return 1
+               end
+               var order = ma.model.mmodule_importation_hierarchy
+               return order.compare(ma, mb)
+       end
+       var tree: OrderedTree[Object]
+end
+
+redef class Model
+       # Generate a MProjectTree based on the projects, groups and modules known in the model
+       fun to_mproject_tree: MProjectTree
+       do
+               var res = new MProjectTree(self)
+               for p in mprojects do
+                       for g in p.mgroups do
+                               res.add(g.parent, g)
+                               for m in g.mmodules do
+                                       res.add(g, m)
+                               end
+                       end
+               end
+               return res
+       end
+end
+
+# Generate graphiz files based on projects, groups and modules
+#
+# Interessing elements must be selected. See `mmodules`, ``
+# Display configuration can be set. See `cluster_group`, `project_group`
+class MProjectDot
+       # The model where to look for information
+       var model: Model
+
+       # Set of projects to expand fully (ie all groups and modules are displayed)
+       # Initially empty, projects can be added
+       var mprojects = new HashSet[MProject]
+
+       # Set of modules to display
+       # Initially empty, modules can be added
+       var mmodules = new HashSet[MModule]
+
+       private fun node_for(mmodule: MModule): nullable String
+       do
+               return "m_{mmodule.object_id}"
+       end
+
+       # Should groups be shown as clusters?
+       var cluster_group writable = true
+
+       # Should projects be shown as clusters?
+       var project_group writable = true
+
+       # Recursively generate noed ans clusters for a mroup
+       private fun dot_cluster(o: OStream, mgroup: MGroup)
+       do
+               # Open the cluster, if required
+               if mgroup.parent == null then
+                       # is is a root group, so display the project
+                       if project_group then
+                               o.write("subgraph cluster_{mgroup.object_id} \{\nlabel=\"{mgroup.mproject.name}\\n({mgroup.filepath})\"\ncolor=black\nstyle=dotted\n")
+                       end
+               else
+                       if cluster_group then
+                               o.write("subgraph cluster_{mgroup.object_id} \{\nlabel=\"{mgroup.name}\"\ncolor=blue\nstyle=dotted\n")
+                       end
+               end
+
+               # outputs the mmodules to show
+               for mmodule in mgroup.mmodules do
+                       if not mmodules.has(mmodule) then continue
+                       o.write("\t{node_for(mmodule)} [label=\"{mmodule.name}\",color=green]\n")
+               end
+
+               # recusively progress on sub-clusters
+               for d in mgroup.in_nesting.direct_smallers do
+                       dot_cluster(o, d)
+               end
+
+               # close the cluster if required
+               if mgroup.parent == null then
+                       if project_group then o.write("\}\n")
+               else
+                       if cluster_group then o.write("\}\n")
+               end
+       end
+
+       # Extends the set of `mmodules` by recurdively adding the most specific imported modules of foreign projects
+       fun collect_important_importation
+       do
+               var todo = new List[MModule]
+               todo.add_all(mmodules)
+               while not todo.is_empty do
+                       var m = todo.pop
+
+                       for psm in m.in_importation.greaters do
+                               if m.mgroup.mproject != psm.mgroup.mproject then continue
+                               for ssm in psm.in_importation.direct_greaters do
+                                       if psm.mgroup.mproject == ssm.mgroup.mproject then continue
+                                       mmodules.add(ssm)
+                                       todo.add(ssm)
+                               end
+                       end
+               end
+       end
+
+       # Generate the dot-file named `filepath` with the current configuration
+       fun render(filepath: String)
+       do
+               # Collect interessing nodes
+               for m in model.mmodules do
+                       # filter out modules outside wanted projects
+                       if not mprojects.has(m.mgroup.mproject) then continue
+
+                       mmodules.add(m)
+               end
+
+               collect_important_importation
+
+               # Collect interessing edges
+               var sub_hierarchy = new POSet[MModule]
+               for m in mmodules do
+                       sub_hierarchy.add_node(m)
+                       for sm in m.in_importation.greaters do
+                               if sm == m then continue
+                               if not mmodules.has(sm) then continue
+                               sub_hierarchy.add_edge(m, sm)
+                       end
+               end
+
+               print "generating {filepath}"
+               var dot = new OFStream.open(filepath)
+               dot.write("digraph g \{\n")
+               dot.write("rankdir=BT;node[shape=box];\n")
+               # Generate the nodes
+               for p in model.mprojects do
+                       dot_cluster(dot, p.root.as(not null))
+               end
+               # Generate the edges
+               for m in mmodules do
+                       for sm in sub_hierarchy[m].direct_greaters do
+                               var nm = node_for(m)
+                               var nsm = node_for(sm)
+                               if m.in_importation.direct_greaters.has(sm) then
+                                       dot.write("\t{nm} -> {nsm}[style=bold]\n")
+                               else
+                                       dot.write("\t{nm} -> {nsm}[style=solid]\n")
+                               end
+                       end
+               end
+               dot.write("\}\n")
+               dot.close
+               # sys.system("xdot -f dot {filepath}")
+       end
+end