/* 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();
}
}