/* Alloy Analyzer 4 -- Copyright (c) 2007-2008, Derek Rayside
*
* 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.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import edu.mit.csail.sdg.alloy4.ConstList;
import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4graph.DotColor;
/** This class implements the automatic visualization inference.
*
* <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
*/
final class MagicLayout {
/** The VizState object that we're going to configure. */
private final VizState vizState;
private Set<AlloyType> enumerationTypes = new LinkedHashSet<AlloyType>();
private Set<AlloyType> singletonTypes = new LinkedHashSet<AlloyType>();
private AlloyType projectionType = null;
private Set<AlloyRelation> spineRelations = Collections.emptySet();
/** Constructor. */
private MagicLayout(final VizState vizState) { this.vizState = vizState; }
/** Main method to infer settings. */
public static void magic(final VizState vizState) {
vizState.resetTheme();
final MagicLayout st = new MagicLayout(vizState);
st.identifyEnumerationTypes();
st.projection();
st.nodeVisibility();
st.spine();
st.attributes();
st.edgeLabels();
}
/** SYNTACTIC: An enumeration follows the pattern "abstract sig Colour; one sig Red; one sig Blue".
*/
private void identifyEnumerationTypes() {
final AlloyModel model = vizState.getCurrentModel();
final Set<AlloyType> types = model.getTypes();
for (final AlloyType t : types) {
if (enumerationTypes.contains(t)) continue; // we've already checked this one, don't muck with it now
if (t.isOne) singletonTypes.add(t);
if (!t.isBuiltin && t.isAbstract) {
List<AlloyType> subTypes = model.getSubTypes(t);
int numberOfSingletonSubtypes = 0;
for (AlloyType st: subTypes) {
if (st.isOne) { numberOfSingletonSubtypes++; singletonTypes.add(st); }
}
if (subTypes.size() == numberOfSingletonSubtypes) { // we have a winner!
enumerationTypes.add(t);
enumerationTypes.addAll(subTypes);
for (final AlloyType st: subTypes) {
// all of the subtypes in the enumeration should have visibility inherited
// so that the user only needs to make the abstract supertype visible if we made a mistake hiding these things
vizState.nodeVisible.put(st, null);
}
// hide unless these are the source of some relation
boolean visible = false;
for (AlloyRelation r : model.getRelations()) {
AlloyType sourceType = r.getTypes().get(0);
if (t.equals(sourceType) || subTypes.contains(sourceType)) { visible = true; break; }
}
vizState.nodeVisible.put(t, visible); // log("VizInference: visible status of enumeration type " + t + " " + visible);
}
}
}
}
/** SEMANTIC/LAYOUT: Determine at most one relation to project over.
*
* When do we project over a sig? Do we ever project over more than one?
* <ul>
* <li> pick 0 or 1 things to project over
* <li> match names: Time, State, Tick, TimeStep
* <li> if ord is opened over the sig
* <li> if present in several ternary relations (i.e. if it will help viz
* high arity relations)
* <li> position in relation (always end or always beginning)
* <li> should we try to catch projections such as the one over birthday
* books?
* <li> pattern match (b,b') to catch transition relations
* <li> add combo box in GUI (?)
* </ul>
*/
private void projection() {
// only fiddle with this if it hasn't already been set somewhere else
if (projectionType == null && vizState.getProjectedTypes().isEmpty()) {
AlloyModel model = vizState.getCurrentModel();
//final Set<AlloyType> candidateTypes = new HashSet<AlloyType>();
Map<AlloyType,Integer> scores = new LinkedHashMap<AlloyType,Integer>();
for (AlloyType t : model.getTypes()) {
scores.put(t, 0);
// does it have a name like State, Time, etc
if (hasLikelyProjectionTypeName(t.getName())) {
scores.put(t, scores.get(t)+1 );
}
// is it in some ternary relation?
for (AlloyRelation r : model.getRelations()) {
if (r.getArity() > 2 && r.getTypes().contains(t)) scores.put(t, scores.get(t)+1 );
}
}
// now we have the scores, see who the winners are:
int max = 0;
final Set<AlloyType> winners = new LinkedHashSet<AlloyType>();
for (final Map.Entry<AlloyType,Integer> e : scores.entrySet()) {
if (e.getValue() == max) winners.add(e.getKey());
if (e.getValue() > max) { max = e.getValue(); winners.clear(); winners.add(e.getKey()); }
}
if (max < 2) {
// no winner, don't project
// log("VizInference: no candidate type to project on.");
} else {
if (winners.size() > 1) {
// we have a tie ... what to do?
// log("VizInference: projection tie. " + winners);
}
final AlloyType winner = winners.iterator().next();
// pick one arbitrarily for now ...
// log("VizInference: projecting on " + max + " " + winner);
projectionType = winner;
vizState.project(projectionType);
}
}
}
private final static ConstList<String> LIKELY_PROJECTION_TYPE_NAMES = Util.asList("State", "TrainState", "Time", "Tick", "TimeStep");
private final boolean hasLikelyProjectionTypeName(final String n) {
for(String s: LIKELY_PROJECTION_TYPE_NAMES) if (n.startsWith(s) || n.endsWith(s)) return true;
return false;
}
/** SEMANTIC/LAYOUT: Determine some relations to be the spine (ie, influence
* the layout).
*
* Which relations should be used to layout? all? none? clever?
* <ul>
* <li> interesting example: 2d game grid
* <li> ex: toplogical sort -- layout tree and list, not cnxn between them
* <li> look for homogenius binary relation (a -> a)
* <li> may be several relations defining the spine
* </ul>
*
*/
private void spine() {
AlloyModel model = vizState.getCurrentModel();
Set<AlloyRelation> relations = model.getRelations();
if (!relations.isEmpty()) {
// only mess with the relations if there are some
// only binary relations are candidates
Set<AlloyRelation> spines = new LinkedHashSet<AlloyRelation>();
for (AlloyRelation r : relations) {
if (r.getArity() == 2) {
List<AlloyType> rtypes = r.getTypes();
AlloyType targetType = rtypes.get(1);
// only a spine if the target is not an enumeration type
if (!enumerationTypes.contains(targetType)) { spines.add(r); }
// however, binary relations named parent should be layed out backwards
if (r.getName().equals("parent")) { vizState.layoutBack.put(r, true); }
}
}
// do we have any spines? if so, use them, if not use all relations
spineRelations = spines.isEmpty() ? relations : spines;
}
// set everything to not influence layout
for (AlloyRelation r : relations) {
vizState.constraint.put(r, false);
vizState.edgeColor.put(r, DotColor.GRAY);
}
// set spines to influence layout
for (AlloyRelation s : spineRelations) {
vizState.constraint.put(s, null);
// inherit the default color, which should be black
vizState.edgeColor.put(s, null);
}
}
/** SEMANTIC/LAYOUT: Determine whether non-projection, non-spine relations
* should be shown as attributes or edges.
*
* <ul>
* <li> binary vs. higher arity -- only make binary attributes
* <li> use attributes on-demand to reduce clutter, not blindly
* <li> functional relations should be attributes (what about a tree?)
* <li> never make something an edge and an attribute
*
* </ul>
*
*/
private void attributes() {
AlloyModel model = vizState.getCurrentModel();
for (AlloyRelation r : model.getRelations()) {
List<AlloyType> rTypes = r.getTypes();
if (r.getArity()==2 && !rTypes.contains(projectionType) && !spineRelations.contains(r)) {
// it's binary, non-projection and non-spine
AlloyType targetType = rTypes.get(1);
if (enumerationTypes.contains(targetType)) {
// target is an enumeration: we have an attribute
vizState.attribute.put(r, true);
vizState.edgeVisible.put(r, false);
}
}
}
}
/** PRESENTATIONAL: Labels for edges. */
private void edgeLabels() {
AlloyModel model = vizState.getCurrentModel();
int relationsAsEdges = 0;
AlloyRelation visibleRelation = null;
for (AlloyRelation r : model.getRelations()) {
Boolean v = vizState.edgeVisible.get(r);
if (v == null || v.booleanValue()) {
// it's visible
relationsAsEdges++;
visibleRelation = r;
// remove text before last slash
MagicUtil.trimLabelBeforeLastSlash(vizState, r);
}
}
// If there's only one relation visible as an edge, and it's binary, then no need to label it.
if (1 == relationsAsEdges && visibleRelation.getArity()==2) {
vizState.label.put(visibleRelation, "");
}
}
/** SYNTACTIC/VISUAL: Hide some things. */
private void nodeVisibility() {
AlloyModel model = vizState.getCurrentModel();
Set<AlloyType> types = model.getTypes();
for (AlloyType t: types) if (!t.isBuiltin && MagicUtil.isActuallyVisible(vizState, t) && t.getName().endsWith("/Ord")) {
vizState.nodeVisible.put(t, false);
}
for (AlloySet s: model.getSets()) if (MagicUtil.isActuallyVisible(vizState, s) && s.getName().endsWith("/Ord")) {
vizState.nodeVisible.put(s, false);
}
}
}