package easik.view; //~--- non-JDK imports -------------------------------------------------------- import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import javax.swing.undo.AbstractUndoableEdit; import org.jgraph.graph.AttributeMap; import org.jgraph.graph.GraphConstants; import org.jgraph.graph.GraphLayoutCache; import easik.DocumentInfo; import easik.model.Model; import easik.model.ModelStateManager; import easik.model.constraint.CommutativeDiagram; import easik.model.constraint.ConstraintException; import easik.model.constraint.EqualizerConstraint; import easik.model.constraint.ModelConstraint; import easik.model.constraint.ProductConstraint; import easik.model.constraint.PullbackConstraint; import easik.model.constraint.SumConstraint; import easik.model.edge.ModelEdge.Cascade; import easik.model.path.ModelPath; import easik.model.states.LoadingState; import easik.model.util.graph.ModelViewFactory; import easik.overview.Overview; import easik.overview.vertex.ViewNode; import easik.sketch.Sketch; import easik.sketch.edge.SketchEdge; import easik.sketch.util.graph.SketchGraphModel; import easik.sketch.vertex.EntityNode; import easik.ui.SketchFrame; import easik.ui.ViewFrame; import easik.ui.menu.popup.DefineQueryNodeAction; import easik.ui.tree.ModelInfoTreeUI; import easik.view.edge.InjectiveViewEdge; import easik.view.edge.NormalViewEdge; import easik.view.edge.PartialViewEdge; import easik.view.edge.View_Edge; import easik.view.util.QueryException; import easik.view.util.graph.ViewGraphModel; import easik.view.vertex.QueryNode; //~--- JDK imports ------------------------------------------------------------ /** * * * @version 12/09/12 * @author Christian Fiddick * @author Updated by Federico Mora 5/2014 */ public class View extends Model<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge> { private static final long serialVersionUID = -8903773663603521530L; /** * Records whether the view has been modified since the last save. Initialized * to <b>false</b> */ private boolean _dirty = false; /** The sketch this view is on */ private Sketch _ourSketch; /** * The default constructor sets all the visual settings for the JGraph, as well * as Initializing the view to be empty. It also adds appropriate listeners for * all of the actions we are concerned with. * * @param inFrame The view frame of the sketch * @param inSketch The sketch this view is on * @param inOverview The overview in which this */ public View(ViewFrame inFrame, Sketch inSketch, Overview inOverview) { super(inFrame, inOverview); _ourSketch = inSketch; _stateManager = new ModelStateManager<>(this); // Set up mouse listener to watch for double clicks // - Double clicks edit a query node this.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if ((_ourSketch.getFrame().getMode() == SketchFrame.Mode.EDIT) && (e.getClickCount() == 2)) { Object[] currSelection = getSelectionCells(); if ((currSelection.length == 1) && (currSelection[0] instanceof QueryNode)) { DefineQueryNodeAction.updateNode((QueryNode) currSelection[0]); } } } }); } /** * When we initialize the sketch, we flush out all the data concerning the * sketch itself. Even the modelAdapter is reinitialized. * * This methods serves as a "new sketch" function. */ @Override public void initialiseModel() { clearSelection(); model = new ViewGraphModel(this); final GraphLayoutCache glc = new GraphLayoutCache(model, new ModelViewFactory<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge>()); setModel(model); setGraphLayoutCache(glc); _nodes = new LinkedHashMap<>(); _edges = new LinkedHashMap<>(); if (_Frame.getInfoTreeUI() != null) { _Frame.setInfoTreeUI(new ModelInfoTreeUI<>(_Frame)); // Wipe // Tree _Frame.getInfoTreeUI().refreshTree(); } _docInfo.reset(); model.discardUndo(); } /** * When we initialize a new sketch, we need to clear the selection buffer just * in case something is still selected. Or else it will remain selected because * there will be no events removing it. */ public void newView() { initialiseModel(); } /** * Used to initialize a new view based on provided data (usually from the Sketch * loading methods). * * @param queryNodes A map of all of the query nodes in the view * @param head The header created from the loaded XML file. */ public void initializeFromData(final Map<String, QueryNode> queryNodes, final DocumentInfo head, final Map<String, View_Edge> edges) { initialiseModel(); _nodes = new LinkedHashMap<>(); _docInfo = head; _Frame.setTreeName(_docInfo.getName()); // Add the entities addEntity(queryNodes.values()); // Add the entities addEdge(edges.values()); /** * not initializing ViewCOnstraints from data at the moment for (final * ModelConstraint c : _constraints.values()) { addConstraint(c); } */ refresh(); model.discardUndo(); } /** * Used to mark a view as dirty or not. Since it's only marked as non-dirty when * saving, we mark all the current queryNode positions if setting non-dirty. * * @param inDirty NEw dirtiness. */ public void setDirty(boolean inDirty) { _dirty = inDirty; if (_dirty) { getDocInfo().updateModificationDate(); } if (!_dirty) { for (QueryNode n : _nodes.values()) { n.savePosition(); } } _Frame.setDirty(_dirty); } /** * Checks to see if any of the query nodes have moved and, if so, updates them * and sets the view to dirty. If the view is already dirty, we don't have to do * anything at all. */ public void checkDirty() { if (_dirty) { return; } for (QueryNode n : _nodes.values()) { if (n == null) { // not sure why this would ever be null } else { Rectangle2D newBounds = GraphConstants.getBounds(n.getAttributes()); if ((int) newBounds.getX() != n.getLastKnownX()) { setDirty(true); return; } } } } /** * Removes an entity, and also cascades to remove all the arrows involved with * it. * * @param toRemove The entity about to be removed */ @Override public void removeNode(final QueryNode toRemove) { // So we don't get a ConcurrentModificationException final ArrayList<View_Edge> removeEdges = new ArrayList<>(); for (final View_Edge edge : _edges.values()) { if (edge.getSourceQueryNode().equals(toRemove) || edge.getTargetQueryNode().equals(toRemove)) { // add the edge to a list of edges to remove removeEdges.add(edge); } } model.beginUpdate(); // Remove the edges for (final View_Edge e : removeEdges) { removeEdge(e); } // remove the constraints this queryNode is a part of for (ModelConstraint<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge> c : toRemove.getConstraints()) { if (_constraints.containsKey(c.getID())) { this.removeConstraint(c); } } _nodes.remove(toRemove.toString()); getGraphLayoutCache().remove(new Object[] { toRemove }); // Remove Entity from tree _Frame.getInfoTreeUI().removeNode(toRemove); model.endUpdate(); } public void removeEdge(final View_Edge toRemove) { model.beginUpdate(); for (final ModelConstraint<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge> c : _constraints.values()) { if (c.hasEdge(toRemove)) { removeConstraint(c); } } _edges.remove(toRemove.getName()); getGraphLayoutCache().remove(new Object[] { toRemove }); model.endUpdate(); } /** * Add a new, empty entity at point X, Y * * @param name The name of the new entity being added * @param x X Coordinate of new entity * @param y Y Coordinate of new entity */ @Override public void addNewNode(final String name, final double x, final double y) { final QueryNode newEntity; // this won't throw exception because query is blank try { newEntity = new QueryNode(name, (int) x, (int) y, this, ""); addEntity(newEntity); } catch (QueryException e) { e.printStackTrace(); } } /** * Add one or more entities, or an array of entities, to the graph, dealing with * all of the dependencies. * * @param theEntities one or more QueryNodes (or an array of querynodes) to be * added */ public void addEntity(final QueryNode... theEntities) { addEntity(Arrays.asList(theEntities)); } /** * Adds a collection (set, list, etc.) of QueryNodes to the graph. * * @param theEntities the collection of entities to be added. */ public void addEntity(final Collection<QueryNode> theEntities) { // Push loading state _stateManager.pushState(new LoadingState<>(this)); final GraphLayoutCache glc = getGraphLayoutCache(); model.beginUpdate(); for (final QueryNode node : theEntities) { // Set the on-screen position of our entity to the attributes of the // entity final AttributeMap nAttribs = node.getAttributes(); GraphConstants.setAutoSize(nAttribs, true); GraphConstants.setBounds(nAttribs, new Rectangle2D.Double(node.getX(), node.getY(), 0, 0)); if (_nodes.containsKey(node.getName())) { node.setName(node.getName()); } // Add our entity to the graph glc.insert(node); // Add our entity to our table of entities _nodes.put(node.toString(), node); // Add Entity to tree _Frame.getInfoTreeUI().addNode(node); } model.postEdit(new AbstractUndoableEdit() { /** * */ private static final long serialVersionUID = -74767611415529681L; @Override public void undo() { super.undo(); for (final QueryNode node : theEntities) { _nodes.remove(node.toString()); } } @Override public void redo() { super.redo(); for (final QueryNode node : theEntities) { _nodes.put(node.toString(), node); } } }); model.endUpdate(); autoAddExistingEdges(); // Reload the graph to respect the new changes glc.reload(); // Pop state _stateManager.popState(); } /** * Checks to see if a name is in use, so that we will not have several instances * at once. * * @param inName The desired new name to check against * @return Is it used or not. */ @Override public boolean isNameUsed(String inName) { Set<String> keys = _nodes.keySet(); for (String name : keys) { if (name.equalsIgnoreCase(inName)) { return true; } } for (EntityNode en : _ourSketch.getEntities()) { if (en.getName().equalsIgnoreCase(inName)) { return true; } } return false; } /** * Gets the sketch to which this view is associated. * * @return The sketch to which this view is associated. */ public Sketch getSketch() { return _ourSketch; } /** * Called internally by View_Edge when an edge is renamed, to keep the edge map * consistent. Should not be called directly; instead just call * edge.setName("newname"). * * @see easik.sketch.edge.SketchEdge#setName(String) * @param edge the edge being renamed * @param oldName the old name of the edge * @param newName the candidate new name * @return a string containing the final new edge name, for SketchEdge to use. */ @Override public String edgeRenamed(final View_Edge edge, final String oldName, String newName) { _edges.put(newName, _edges.remove(oldName)); return newName; } /** * * Adds one or more view edges (or an array of edges) to the view. * * @param inEdges one or more ViewEdges (or an array of viewEdges) to be added * * * @author Sarah van der Laan 2013 */ public void addEdge(View_Edge... inEdges) { addEdge(Arrays.asList(inEdges)); } /** * Adds a collection (set, list, etc.) of edges to the view. * * @param inEdges Collection of ViewEdges to add * * @author Sarah van der Laan 2013 */ public void addEdge(Collection<View_Edge> inEdges) { for (View_Edge edge : inEdges) { // Add our edge to the graph getGraphLayoutCache().insert(edge); // System.out.println("SOURCE: " + // edge.getSourceQueryNode().getName()); // System.out.println("TARGET: " + // edge.getTargetQueryNode().getName()); _edges.put(edge.getName(), edge); } addConstraints(); refresh(); _theOverview.refresh(); } /** * Call this method when a new QueryNode or Edge is created to automatically add * whatever existing edges It has in the underlying sketch with other existing * QueryNodes. * * * @author Federico Mora */ public void autoAddExistingEdges() { Collection<SketchEdge> sketchEdges = _ourSketch.getEdges().values(); HashMap<EntityNode, QueryNode> nodeMatches = getEntityNodePairs(); for (SketchEdge se : sketchEdges) { if (nodeMatches.containsKey(se.getTargetEntity()) && nodeMatches.containsKey(se.getSourceEntity()) && !_edges.containsKey(se.getName())) { View_Edge vEdge; // need to move down?? if (se.isPartial()) { vEdge = new PartialViewEdge(nodeMatches.get(se.getSourceEntity()), nodeMatches.get(se.getTargetEntity()), se.getName()); } else if (se.isInjective()) { // System.out.println("Edge is injective"); // **NEED TO FIGURE OUT CASCADING vEdge = new InjectiveViewEdge(nodeMatches.get(se.getSourceEntity()), nodeMatches.get(se.getTargetEntity()), se.getName(), Cascade.RESTRICT); } else { vEdge = new NormalViewEdge(nodeMatches.get(se.getSourceEntity()), nodeMatches.get(se.getTargetEntity()), se.getName()); } this.addEdge(vEdge); } } } /** * Adds constraints to the view frame is all elements of Sketch constraints are * being queried in view * * @author Federico Mora */ @SuppressWarnings("unchecked") private void addConstraints() { ArrayList<ModelPath<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge>> inPaths = new ArrayList<>(); ArrayList<View_Edge> vEdges = new ArrayList<>(); HashMap<EntityNode, QueryNode> nodeMatches = getEntityNodePairs(); for (ModelConstraint<SketchFrame, SketchGraphModel, Sketch, EntityNode, SketchEdge> c : _ourSketch .getConstraints().values()) { ifblock: if (_constraints.containsKey(c.getID())) { // already represented so lets get out } else { // looking to see if we have all edges needed for each // constraint for (ModelPath<SketchFrame, SketchGraphModel, Sketch, EntityNode, SketchEdge> skPath : c.getPaths()) { vEdges.clear(); for (SketchEdge skEdge : skPath.getEdges()) { if (!_edges.containsKey(skEdge.getName())) { break ifblock; } else if (!nodeMatches.get(skEdge.getTargetEntity()).getWhere().isEmpty() || !nodeMatches.get(skEdge.getSourceEntity()).getWhere().isEmpty()) { // if the nodes being queried have where statements // then just exit break ifblock; } else { vEdges.add(_edges.get(skEdge.getName())); } } inPaths.add(new ModelPath<>(vEdges)); } // add Constraint to View // if we survived to here it means elements are present to add // constraint ModelConstraint<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge> newDiagram = null; if (c.getType().equals("commutativediagram")) { newDiagram = new CommutativeDiagram<>(inPaths, this, c.getID()); } else if (c.getType().equals("sumconstraint")) { newDiagram = new SumConstraint<>(inPaths, this, c.getID()); } else if (c.getType().equals("pullbackconstraint")) { try { newDiagram = new PullbackConstraint<>(inPaths, this, c.getID()); } catch (ConstraintException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else if (c.getType().equals("equalizerconstraint")) { try { newDiagram = new EqualizerConstraint<>(inPaths, this, c.getID()); } catch (ConstraintException e) { // TODO Auto-generated catch block e.printStackTrace(); } } else if (c.getType().equals("productconstraint")) { try { newDiagram = new ProductConstraint<>(inPaths, this, c.getID()); } catch (ConstraintException e) { // TODO Auto-generated catch block e.printStackTrace(); } } this.addNewConstraint(newDiagram); this.setDirty(); } } } /** * Returns a hash map of paired QueryNodes and the EntityNodes they represent * * @return HashMap<EntityNode, QueryNode> * @author Federico Mora */ public HashMap<EntityNode, QueryNode> getEntityNodePairs() { HashMap<EntityNode, QueryNode> eNodes = new HashMap<>(); for (QueryNode qn : _nodes.values()) { if (qn != null) { eNodes.put(qn.getQueriedEntity(), qn); } } return eNodes; } /** * Updates the view by taking away constraints if a node has been updated to * include a where statement. Or if it had a where statement and it was updated * to not having a where statement * * if it had a where statement then it must not have one now, if it didn't then * now it must. * * @author Federico Mora */ public void updateConstraints(QueryNode ourNode, boolean hadWhere) { if (hadWhere) { this.addConstraints(); } else { for (ModelConstraint<ViewFrame, ViewGraphModel, View, QueryNode, View_Edge> vc : ourNode.getConstraints()) { this.removeConstraint(vc); } } } @Override public boolean isSynced() { return _ourSketch.isSynced(); } @Override public void setSynced(boolean state) { // is not used in views but need to guarantee that it is there // for generic puproses } @Override public Collection<ViewNode> getViews() { // we are a view so return null return null; } }