From: Jean Privat Date: Wed, 29 Jan 2014 16:36:59 +0000 (-0500) Subject: src: add model_viz.nit X-Git-Tag: v0.6.4~41^2~7 X-Git-Url: http://nitlanguage.org src: add model_viz.nit Change-Id: Ieda8600a0da5404c1ee744c7f56b28e8f417a76f Signed-off-by: Jean Privat --- diff --git a/src/model_viz.nit b/src/model_viz.nit new file mode 100644 index 0000000..1471a49 --- /dev/null +++ b/src/model_viz.nit @@ -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