/* Alloy Analyzer 4 -- Copyright (c) 2006-2009, Felix Chang * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ package edu.mit.csail.sdg.alloy4viz; import java.awt.Color; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import javax.swing.JPanel; import edu.mit.csail.sdg.alloy4.ErrorFatal; import edu.mit.csail.sdg.alloy4.Util; import edu.mit.csail.sdg.alloy4graph.DotColor; import edu.mit.csail.sdg.alloy4graph.DotDirection; import edu.mit.csail.sdg.alloy4graph.DotPalette; import edu.mit.csail.sdg.alloy4graph.DotShape; import edu.mit.csail.sdg.alloy4graph.DotStyle; import edu.mit.csail.sdg.alloy4graph.GraphEdge; import edu.mit.csail.sdg.alloy4graph.Graph; import edu.mit.csail.sdg.alloy4graph.GraphNode; import edu.mit.csail.sdg.alloy4graph.GraphViewer; /** This utility class generates a graph for a particular index of the projection. * * <p><b>Thread Safety:</b> Can be called only by the AWT event thread. */ public final class StaticGraphMaker { /** The theme customization. */ private final VizState view; /** The projected instance for the graph currently being generated. */ private final AlloyInstance instance; /** The projected model for the graph currently being generated. */ private final AlloyModel model; /** The map that contains all edges and what the AlloyTuple that each edge corresponds to. */ private final Map<GraphEdge,AlloyTuple> edges = new LinkedHashMap<GraphEdge,AlloyTuple>(); /** The map that contains all nodes and what the AlloyAtom that each node corresponds to. */ private final Map<GraphNode,AlloyAtom> nodes = new LinkedHashMap<GraphNode,AlloyAtom>(); /** This maps each atom to the node representing it; if an atom doesn't have a node, it won't be in the map. */ private final Map<AlloyAtom,GraphNode> atom2node = new LinkedHashMap<AlloyAtom,GraphNode>(); /** This stores a set of additional labels we want to add to an existing node. */ private final Map<GraphNode,Set<String>> attribs = new LinkedHashMap<GraphNode,Set<String>>(); /** The resulting graph. */ private final Graph graph; /** Produces a single Graph from the given Instance and View and choice of Projection */ public static JPanel produceGraph(AlloyInstance instance, VizState view, AlloyProjection proj) throws ErrorFatal { view = new VizState(view); if (proj == null) proj = new AlloyProjection(); Graph graph = new Graph(view.getFontSize() / 12.0D); new StaticGraphMaker(graph, instance, view, proj); if (graph.nodes.size()==0) new GraphNode(graph, "", "Due to your theme settings, every atom is hidden.", "Please click Theme and adjust your settings."); return new GraphViewer(graph); } /** The list of colors, in order, to assign each legend. */ private static final List<Color> colorsClassic = Util.asList( new Color(228,26,28) ,new Color(166,86,40) ,new Color(255,127,0) ,new Color(77,175,74) ,new Color(55,126,184) ,new Color(152,78,163) ); /** The list of colors, in order, to assign each legend. */ private static final List<Color> colorsStandard = Util.asList( new Color(227,26,28) ,new Color(255,127,0) ,new Color(251*8/10,154*8/10,153*8/10) ,new Color(51,160,44) ,new Color(31,120,180) ); /** The list of colors, in order, to assign each legend. */ private static final List<Color> colorsMartha = Util.asList( new Color(231,138,195) ,new Color(252,141,98) ,new Color(166,216,84) ,new Color(102,194,165) ,new Color(141,160,203) ); /** The list of colors, in order, to assign each legend. */ private static final List<Color> colorsNeon = Util.asList( new Color(231,41,138) ,new Color(217,95,2) ,new Color(166,118,29) ,new Color(102,166,30) ,new Color(27,158,119) ,new Color(117,112,179) ); /** The constructor takes an Instance and a View, then insert the generate graph(s) into a blank cartoon. */ private StaticGraphMaker (Graph graph, AlloyInstance originalInstance, VizState view, AlloyProjection proj) throws ErrorFatal { final boolean hidePrivate = view.hidePrivate(); final boolean hideMeta = view.hideMeta(); final Map<AlloyRelation,Color> magicColor = new TreeMap<AlloyRelation,Color>(); final Map<AlloyRelation,Integer> rels = new TreeMap<AlloyRelation,Integer>(); this.graph = graph; this.view = view; instance = StaticProjector.project(originalInstance, proj); model = instance.model; for (AlloyRelation rel: model.getRelations()) { rels.put(rel, null); } List<Color> colors; if (view.getEdgePalette() == DotPalette.CLASSIC) colors = colorsClassic; else if (view.getEdgePalette() == DotPalette.STANDARD) colors = colorsStandard; else if (view.getEdgePalette() == DotPalette.MARTHA) colors = colorsMartha; else colors = colorsNeon; int ci = 0; for (AlloyRelation rel: model.getRelations()) { DotColor c = view.edgeColor.resolve(rel); Color cc = (c==DotColor.MAGIC) ? colors.get(ci) : c.getColor(view.getEdgePalette()); int count = ((hidePrivate && rel.isPrivate) || !view.edgeVisible.resolve(rel)) ? 0 : edgesAsArcs(hidePrivate, hideMeta, rel, colors.get(ci)); rels.put(rel, count); magicColor.put(rel, cc); if (count>0) ci = (ci+1)%(colors.size()); } for (AlloyAtom atom: instance.getAllAtoms()) { List<AlloySet> sets = instance.atom2sets(atom); if (sets.size()>0) { for (AlloySet s: sets) if (view.nodeVisible.resolve(s) && !view.hideUnconnected.resolve(s)) {createNode(hidePrivate, hideMeta, atom); break;} } else if (view.nodeVisible.resolve(atom.getType()) && !view.hideUnconnected.resolve(atom.getType())) { createNode(hidePrivate, hideMeta, atom); } } for (AlloyRelation rel: model.getRelations()) if (!(hidePrivate && rel.isPrivate)) if (view.attribute.resolve(rel)) edgesAsAttribute(rel); for(Map.Entry<GraphNode,Set<String>> e: attribs.entrySet()) { Set<String> set = e.getValue(); if (set!=null) for(String s: set) if (s.length() > 0) e.getKey().addLabel(s); } for(Map.Entry<AlloyRelation,Integer> e: rels.entrySet()) { Color c = magicColor.get(e.getKey()); if (c==null) c = Color.BLACK; int n = e.getValue(); if (n>0) graph.addLegend(e.getKey(), e.getKey().getName()+": "+n, c); else graph.addLegend(e.getKey(), e.getKey().getName(), null); } } /** Return the node for a specific AlloyAtom (create it if it doesn't exist yet). * @return null if the atom is explicitly marked as "Don't Show". */ private GraphNode createNode(final boolean hidePrivate, final boolean hideMeta, final AlloyAtom atom) { GraphNode node = atom2node.get(atom); if (node!=null) return node; if ( (hidePrivate && atom.getType().isPrivate) || (hideMeta && atom.getType().isMeta) || !view.nodeVisible(atom, instance)) return null; // Make the node DotColor color = view.nodeColor(atom, instance); DotStyle style = view.nodeStyle(atom, instance); DotShape shape = view.shape(atom, instance); String label = atomname(atom, false); node = new GraphNode(graph, atom, label).set(shape).set(color.getColor(view.getNodePalette())).set(style); // Get the label based on the sets and relations String setsLabel=""; boolean showLabelByDefault = view.showAsLabel.get(null); for (AlloySet set: instance.atom2sets(atom)) { String x = view.label.get(set); if (x.length()==0) continue; Boolean showLabel = view.showAsLabel.get(set); if ((showLabel==null && showLabelByDefault) || (showLabel!=null && showLabel.booleanValue())) setsLabel += ((setsLabel.length()>0?", ":"")+x); } if (setsLabel.length()>0) { Set<String> list = attribs.get(node); if (list==null) attribs.put(node, list=new TreeSet<String>()); list.add("("+setsLabel+")"); } nodes.put(node,atom); atom2node.put(atom,node); return node; } /** Create an edge for a given tuple from a relation (if neither start nor end node is explicitly invisible) */ private boolean createEdge(final boolean hidePrivate, final boolean hideMeta, AlloyRelation rel, AlloyTuple tuple, boolean bidirectional, Color magicColor) { // This edge represents a given tuple from a given relation. // // If the tuple's arity==2, then the label is simply the label of the relation. // // If the tuple's arity>2, then we append the node labels for all the intermediate nodes. // eg. Say a given tuple is (A,B,C,D) from the relation R. // An edge will be drawn from A to D, with the label "R [B, C]" if ((hidePrivate && tuple.getStart().getType().isPrivate) ||(hideMeta && tuple.getStart().getType().isMeta) || !view.nodeVisible(tuple.getStart(), instance)) return false; if ((hidePrivate && tuple.getEnd().getType().isPrivate) ||(hideMeta && tuple.getEnd().getType().isMeta) || !view.nodeVisible(tuple.getEnd(), instance)) return false; GraphNode start = createNode(hidePrivate, hideMeta, tuple.getStart()); GraphNode end = createNode(hidePrivate, hideMeta, tuple.getEnd()); if (start==null || end==null) return false; boolean layoutBack = view.layoutBack.resolve(rel); String label = view.label.get(rel); if (tuple.getArity() > 2) { StringBuilder moreLabel = new StringBuilder(); List<AlloyAtom> atoms=tuple.getAtoms(); for (int i=1; i<atoms.size()-1; i++) { if (i>1) moreLabel.append(", "); moreLabel.append(atomname(atoms.get(i),false)); } if (label.length()==0) { /* label=moreLabel.toString(); */ } else { label=label+(" ["+moreLabel+"]"); } } DotDirection dir = bidirectional ? DotDirection.BOTH : (layoutBack ? DotDirection.BACK : DotDirection.FORWARD); DotStyle style = view.edgeStyle.resolve(rel); DotColor color = view.edgeColor.resolve(rel); int weight = view.weight.get(rel); GraphEdge e = new GraphEdge((layoutBack ? end : start), (layoutBack ? start : end), tuple, label, rel); if (color == DotColor.MAGIC && magicColor != null) e.set(magicColor); else e.set(color.getColor(view.getEdgePalette())); e.set(style); e.set(dir!=DotDirection.FORWARD, dir!=DotDirection.BACK); e.set(weight<1 ? 1 : (weight>100 ? 10000 : 100*weight)); edges.put(e, tuple); return true; } /** Create edges for every visible tuple in the given relation. */ private int edgesAsArcs(final boolean hidePrivate, final boolean hideMeta, AlloyRelation rel, Color magicColor) { int count = 0; if (!view.mergeArrows.resolve(rel)) { // If we're not merging bidirectional arrows, simply create an edge for each tuple. for (AlloyTuple tuple: instance.relation2tuples(rel)) if (createEdge(hidePrivate, hideMeta, rel, tuple, false, magicColor)) count++; return count; } // Otherwise, find bidirectional arrows and only create one edge for each pair. Set<AlloyTuple> tuples = instance.relation2tuples(rel); Set<AlloyTuple> ignore = new LinkedHashSet<AlloyTuple>(); for (AlloyTuple tuple: tuples) { if (!ignore.contains(tuple)) { AlloyTuple reverse = tuple.getArity()>2 ? null : tuple.reverse(); // If the reverse tuple is in the same relation, and it is not a self-edge, then draw it as a <-> arrow. if (reverse!=null && tuples.contains(reverse) && !reverse.equals(tuple)) { ignore.add(reverse); if (createEdge(hidePrivate, hideMeta, rel, tuple, true, magicColor)) count = count + 2; } else { if (createEdge(hidePrivate, hideMeta, rel, tuple, false, magicColor)) count = count + 1; } } } return count; } /** Attach tuple values as attributes to existing nodes. */ private void edgesAsAttribute(AlloyRelation rel) { // If this relation wants to be shown as an attribute, // then generate the annotations and attach them to each tuple's starting node. // Eg. // If (A,B) and (A,C) are both in the relation F, // then the A node would have a line that says "F: B, C" // Eg. // If (A,B,C) and (A,D,E) are both in the relation F, // then the A node would have a line that says "F: B->C, D->E" // Eg. // If (A,B,C) and (A,D,E) are both in the relation F, and B belongs to sets SET1 and SET2, // and SET1's "show in relational attribute" is on, // and SET2's "show in relational attribute" is on, // then the A node would have a line that says "F: B (SET1, SET2)->C, D->E" // Map<GraphNode,String> map = new LinkedHashMap<GraphNode,String>(); for (AlloyTuple tuple: instance.relation2tuples(rel)) { GraphNode start=atom2node.get(tuple.getStart()); if (start==null) continue; // null means the node won't be shown, so we can't show any attributes String attr=""; List<AlloyAtom> atoms=tuple.getAtoms(); for (int i=1; i<atoms.size(); i++) { if (i>1) attr+="->"; attr+=atomname(atoms.get(i),true); } if (attr.length()==0) continue; String oldattr=map.get(start); if (oldattr!=null && oldattr.length()>0) attr=oldattr+", "+attr; if (attr.length()>0) map.put(start,attr); } for (Map.Entry<GraphNode,String> e: map.entrySet()) { GraphNode node = e.getKey(); Set<String> list = attribs.get(node); if (list==null) attribs.put(node, list=new TreeSet<String>()); String attr = e.getValue(); if (view.label.get(rel).length()>0) attr = view.label.get(rel) + ": " + attr; list.add(attr); } } /** Return the label for an atom. * @param atom - the atom * @param showSets - whether the label should also show the sets that this atom belongs to * * <p> eg. If atom A is the 3rd atom in type T, and T's label is "Person", * then the return value would be "Person3". * * <p> eg. If atom A is the only atom in type T, and T's label is "Person", * then the return value would be "Person". * * <p> eg. If atom A is the 3rd atom in type T, and T's label is "Person", * and T belongs to the sets Set1, Set2, and Set3. * However, only Set1 and Set2 have "show in relational attribute == on", * then the return value would be "Person (Set1, Set2)". */ private String atomname(AlloyAtom atom, boolean showSets) { String label = atom.getVizName(view, view.number.resolve(atom.getType())); if (!showSets) return label; String attr = ""; boolean showInAttrByDefault = view.showAsAttr.get(null); for (AlloySet set: instance.atom2sets(atom)) { String x = view.label.get(set); if (x.length()==0) continue; Boolean showAsAttr = view.showAsAttr.get(set); if ((showAsAttr==null && showInAttrByDefault) || (showAsAttr!=null && showAsAttr)) attr += ((attr.length()>0?", ":"")+x); } if (label.length()==0) return (attr.length()>0) ? ("("+attr+")") : ""; return (attr.length()>0) ? (label+" ("+attr+")") : label; } static String esc(String name) { if (name.indexOf('\"') < 0) return name; StringBuilder out = new StringBuilder(); for(int i=0; i<name.length(); i++) { char c=name.charAt(i); if (c=='\"') out.append('\\'); out.append(c); } return out.toString(); } }