package easik.overview; //~--- non-JDK imports -------------------------------------------------------- //~--- JDK imports ------------------------------------------------------------ import java.awt.Point; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Random; import org.jgraph.JGraph; import org.jgraph.event.GraphSelectionEvent; import org.jgraph.event.GraphSelectionListener; import org.jgraph.graph.AttributeMap; import org.jgraph.graph.DefaultCellViewFactory; import org.jgraph.graph.DefaultGraphModel; import org.jgraph.graph.GraphCell; import org.jgraph.graph.GraphConstants; import org.jgraph.graph.GraphLayoutCache; import easik.DocumentInfo; import easik.Easik; import easik.EasikTools; import easik.overview.edge.ViewDefinitionEdge; import easik.overview.util.OverviewFileIO; import easik.overview.util.graph.OverviewGraphModel; import easik.overview.vertex.OverviewVertex; import easik.overview.vertex.SketchNode; import easik.overview.vertex.ViewNode; import easik.sketch.Sketch; import easik.ui.ApplicationFrame; import easik.ui.GraphUI; import easik.ui.SketchFrame; import easik.ui.ViewFrame; import easik.ui.tree.OverviewInfoTreeUI; import easik.view.View; /** * An Overview represents a collection of EA Sketch diagrams used to represent * databases. This Object also extends the JGraph swing component, allowing it * to be added directly to our application's GUI. * * As of now, we have no way of represecting relationships between sketches. */ public class Overview extends JGraph { /** * */ private static final long serialVersionUID = 8958091082779615522L; /** The current file, initialized to <b>null</b> */ private File _currentFile = null; /** * Records whether the sketch has been modified since the last save. Initialized * to <b>false</b> */ private boolean _dirty = false; /** The current ApplicationFrame */ private ApplicationFrame _appFrame; /** The current DocumentInfo */ private DocumentInfo _docInfo; /** A hash map of all sketch nodes, indexed by their name */ private HashMap<String, SketchNode> _sketchNodes; /** A hash map of all view edges, indexed by their label */ private HashMap<String, ViewDefinitionEdge> _viewEdges; /** A hash map of all view nodes, indexed by their name */ private HashMap<String, ViewNode> _viewNodes; /** * The default constructor sets all the visual settings for the JGraph, as well * as initialising the sketch to be empty. It also adds appropriate listeners * for all of the actions we are concerned with. * * @param inFrame The application frame of the sketch */ public Overview(ApplicationFrame inFrame) { super(); setBackground(Easik.getInstance().getSettings().getColor("overview_canvas_background")); _appFrame = inFrame; setAntiAliased(true); setDisconnectable(false); setConnectable(false); setEditable(false); setSizeable(false); getGraphLayoutCache().setAutoSizeOnValueChange(true); addGraphSelectionListener(new GraphSelectionListener() { @Override public void valueChanged(GraphSelectionEvent e) { Overview.this.getGraphLayoutCache().reload(); } }); // Set up mouse listener to watch for double clicks // - Double clicks make a sketch visible this.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { boolean doubleClick = ((e.getClickCount() > 1) && (e.getButton() == MouseEvent.BUTTON1)); if (doubleClick) { Object[] currSelection = Overview.this.getSelectionCells(); // double click on sketch, so make visible if (currSelection.length == 1) { if (currSelection[0] instanceof SketchNode) { SketchFrame f = ((SketchNode) currSelection[0]).getFrame(); if (f.getMModel().isSynced()) { f.enableDataManip(true); } else { f.enableSketchEdit(); } } else if (currSelection[0] instanceof ViewNode) { // enableDataManip so that you can't just open the // view without // verifying connection SketchFrame f = ((ViewNode) currSelection[0]).getMModel().getSketch().getFrame(); if (f.getMModel().isSynced()) { f.enableDataManip(false); } ((ViewNode) currSelection[0]).getFrame().setVisible(true); } } } } @Override public void mousePressed(MouseEvent e) { mouseClicked(e); } }); initializeOverview(); updateUI(); } /** * Refreshes the specified nodes/edges in the overview GUI. If called with no * arguments, all items are refreshed. * * @param cells */ public void refresh(GraphCell... cells) { setBackground(Easik.getInstance().getSettings().getColor("overview_canvas_background")); Object[] toRefresh = (cells.length > 0) ? cells : getRoots(); ((DefaultGraphModel) getModel()).cellsChanged(toRefresh); _appFrame.updateTitle(); if (cells.length == 0) { super.refresh(); } } /** * Overridden refresh method to force parameterless calls to go through * refresh(GraphCell...cells) */ @Override public void refresh() { this.refresh(new GraphCell[0]); } /** * Refreshes the overview GUI as well as the GUIs of all Sketches and Views. */ public void refreshAll() { for (SketchNode node : _sketchNodes.values()) { Sketch s = node.getFrame().getMModel(); s.refresh(); s.updateThumb(); } for (ViewNode node : _viewNodes.values()) { View v = node.getMModel(); v.refresh(); v.updateThumb(); } refresh(); } /** * Gets the outgoing edge from the node representing the given view. * * @param view The view * @return The edge leaving the given view. */ public ViewDefinitionEdge getViewEdge(View view) { ViewNode vn = view.getFrame().getNode(); for (ViewDefinitionEdge currEdge : _viewEdges.values()) { if (currEdge.getSourceNode() == vn) { return currEdge; } } return null; } /** * Gets a view edge with a given label. * * @param inLabel The label of the edge we want. * @return The edge who's label matches inLabel. */ public ViewDefinitionEdge getViewEdge(String inLabel) { return _viewEdges.get(inLabel); } /** * Accessor for the sketches in the overview * * @return Collection of the sketches */ public Collection<SketchNode> getSketches() { return Collections.unmodifiableCollection(_sketchNodes.values()); } /** * Accessor for the views in the overview * * @return Collection of the views */ public Collection<ViewNode> getViews() { return Collections.unmodifiableCollection(_viewNodes.values()); } /** * Accessor for the view edges in the overview * * @return Collection of view edges */ public Collection<ViewDefinitionEdge> getViewEdges() { return Collections.unmodifiableCollection(_viewEdges.values()); } /** * Removes a sketch from both the tree representation, and the graphical. * * @param toRemove The entity about to be removed */ public void removeSketch(SketchNode toRemove) { ArrayList<ViewNode> removeViews = new ArrayList<>(toRemove.getFrame().getMModel().getViews()); for (ViewNode v : removeViews) { removeView(v); } // dispose of the sketch's frame toRemove.getFrame().dispose(); _sketchNodes.remove(toRemove.toString()); getGraphLayoutCache().remove(new Object[] { toRemove }); _appFrame.getInfoTreeUI().removeSketch(toRemove); } /** * Removes a view edge from the overview. This does not automatically delete the * source view from the graph. * * @param toRemove The edge to remove. */ private void removeViewEdge(ViewDefinitionEdge toRemove) { _viewEdges.remove(toRemove.getName()); getGraphLayoutCache().remove(new Object[] { toRemove }); } /** * Remove view and it's view edge from the overview. * * @param remove */ public void removeView(ViewNode remove) { // remove from sketch View toRemove = remove.getFrame().getMModel(); toRemove.getSketch().removeView(toRemove.getName()); for (ViewDefinitionEdge edge : _viewEdges.values()) { if (edge.getSourceNode().getName().equals(toRemove.getName())) { removeViewEdge(edge); break; } } // remove from overview graph _viewNodes.remove(toRemove.getName()); getGraphLayoutCache().remove(new Object[] { remove }); // remove from info tree _appFrame.getInfoTreeUI().removeView(toRemove.getFrame().getNode()); // dispose of the frame remove.getFrame().dispose(); } /** * Get the working file for this sketch. * * @return The file last saved using this sketch */ public File getFile() { return _currentFile; } /** * This assigns a file to the current overview. * * @param inFile File to be assigned. */ public void setFile(File inFile) { _currentFile = inFile; } /** * Since this is a Swing component, this method is overloading a method of * JGraph to adjust the look and feel. The feel we are changing is ignoring all * but left clicks, allowing for right-click functionality not affecting the * selections. */ @Override public void updateUI() { this.setUI(new GraphUI()); this.invalidate(); } /** * Determines whether the sketch has been modified since the last save. * * @return The dirtiness, true means dirty. */ public boolean getDirty() { return _dirty; } /** * Used to mark a sketch as dirty or not. Since it's only marked as non-dirty * when saving, we mark all the current node/view positions if setting * non-dirty. * * @param inDirty NEw dirtiness. */ public void setDirty(boolean inDirty) { _dirty = inDirty; if (_dirty) { getDocInfo().updateModificationDate(); } if (!_dirty) { for (SketchNode n : _sketchNodes.values()) { n.savePosition(); } for (ViewNode v : _viewNodes.values()) { v.savePosition(); } } _appFrame.setDirty(_dirty); } /** * Checks to see if any of the sketch nodes or view nodes have moved and, if so, * updates them and sets the overview to dirty. If the overview is already * dirty, we don't have to do anything at all. */ public void checkDirty() { if (_dirty) { return; } for (SketchNode n : _sketchNodes.values()) { Rectangle2D newBounds = GraphConstants.getBounds(n.getAttributes()); if ((int) newBounds.getX() != n.getLastKnownX()) { setDirty(true); return; } } for (ViewNode v : _viewNodes.values()) { Rectangle2D newBounds = GraphConstants.getBounds(v.getAttributes()); if ((int) newBounds.getX() != v.getLastKnownX()) { setDirty(true); return; } } } /** * Returns the parental application frame to whoever asks for it. * * @return The current application frame */ public ApplicationFrame getFrame() { return _appFrame; } /** * Gets the document information * * @return The document information */ public DocumentInfo getDocInfo() { return _docInfo; } /** * When we initialise the overview, we flush out all the data concerning the * sketch itself. * * This methods serves as a "new overview" function. */ public void initializeOverview() { clearSelection(); if (_sketchNodes != null) { for (SketchNode node : _sketchNodes.values()) { node.getFrame().dispose(); } } if (_viewNodes != null) { for (ViewNode node : _viewNodes.values()) { node.getFrame().dispose(); } } setFile(null); _sketchNodes = new HashMap<>(); _viewNodes = new HashMap<>(); _viewEdges = new HashMap<>(); _docInfo = new DocumentInfo(_appFrame); if (_appFrame.getInfoTreeUI() != null) { _appFrame.setInfoTreeUI(new OverviewInfoTreeUI(_appFrame)); _appFrame.getInfoTreeUI().refreshTree(); } OverviewGraphModel model = new OverviewGraphModel(this); GraphLayoutCache glc = new GraphLayoutCache(model, new DefaultCellViewFactory()); setModel(model); setGraphLayoutCache(glc); } /** * Used to initialise a new sketch based on provided data (usually from the * Sketch loading methods). * * @param sketchNodes A Map of all of the sketches in the overview * @param viewNodes * @param viewDefinitionEdges * @param docInfo The document information to be stored along with * this overview */ public void initializeFromData(Map<String, SketchNode> sketchNodes, Map<String, ViewNode> viewNodes, Map<String, ViewDefinitionEdge> viewDefinitionEdges, DocumentInfo docInfo) { initializeOverview(); _sketchNodes = new HashMap<>(); _viewNodes = new HashMap<>(); _viewEdges = new HashMap<>(); _docInfo = docInfo; for (SketchNode node : sketchNodes.values()) { if (node != null) { addVertex(node); } } for (ViewNode node : viewNodes.values()) { if (node != null) { addVertex(node); } } for (ViewDefinitionEdge edge : viewDefinitionEdges.values()) { if (edge != null) { addViewEdge(edge); } } refresh(); } /** * Called internally by SketchNode when a sketch is renamed, to keep the sketch * node map consistent. Should not be called directly; instead call * overview.getSketch("currentname").setName("newname"). * * @see easik.overview.SketchNode.setName(String) * @param node the SketchNode being renamed * @param oldName the old name of the node * @param newName the candidate new name * @return a string containing the final new node name, for EntityNode to use. */ public String sketchRenamed(SketchNode node, String oldName, String newName) { // If the name already exists, we have to rename it while (_sketchNodes.containsKey(newName)) { newName = EasikTools.incrementName(newName); } if (_sketchNodes.containsKey(oldName)) { _sketchNodes.put(newName, _sketchNodes.remove(oldName)); } return newName; } /** * Called internally by ViewNode when a view is renamed, to keep the view node * map consistent. Should not be called directly; instead call * overview.getView("currentname").setName("newname"). * * @see easik.overview.vertex.ViewNode.setName(String) * @param node the ViewNode being renamed * @param oldName the old name of the node * @param newName the candidate new name * @return a string containing the final new node name, for ViewNode to use. */ public String viewRenamed(ViewNode node, String oldName, String newName) { // If the name already exists, we have to rename it while (_viewNodes.containsKey(newName)) { newName = EasikTools.incrementName(newName); } _viewNodes.put(newName, _viewNodes.remove(oldName)); return newName; } /** * Called internally by ViewDefinitionEdge 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. */ public String viewEdgeRenamed(ViewDefinitionEdge edge, String oldName, String newName) { // If the name already exists, we have to rename it while (_viewEdges.containsKey(newName)) { newName = EasikTools.incrementName(newName); } _viewEdges.put(newName, _viewEdges.remove(oldName)); return newName; } // TODO: For loading and saving, have it return its success or an error // message /** * Saves the overview as an XML file. * * @param outputFile The file to be written to */ public void saveToXML(File outputFile) { OverviewFileIO.overviewToXML(outputFile, this); } /** * Requests that an XML file be loaded into the overview. Note that this only * loads the data, it doesn't do the other operations required for properly * loading an overview XML file. * * @param inputFile The file from which the data will be drawn. * @see openOverview( java.io.File) for complete file loading */ public void loadFromXML(File inputFile) { if (!OverviewFileIO.graphicalOverviewFromXML(inputFile, this)) { System.err.println("Error loading overview from XML..."); } } /** * Loads an XML overview file into the overview, sets up the window, marks the * window clean, and adds the file to the recent files list. * * @param inputFile The file from which the data will be drawn. */ public void openOverview(File inputFile) { loadFromXML(inputFile); setFile(inputFile); setDirty(false); refresh(); _appFrame.addRecentFile(inputFile); } /** * Add a new sketch at point (x,y). Returns the new SketchNode. * * @param name The name of the new sketch being added * @param x X Coordinate of new sketch * @param y Y Coordinate of new sketch * @return the created SketchNode */ public SketchNode addNewSketch(String name, double x, double y) { SketchFrame newFrame = new SketchFrame(this); SketchNode newNode = new SketchNode(name, (int) x, (int) y, newFrame); addVertex(newNode); return newNode; } /** * Add a view to a given sketch node * * @param sketch * @param viewName The name of the new view. Assumes no naming naming conflict * @return The new view node */ public ViewNode addNewView(SketchNode sketch, String viewName) { Point newP = getNewViewPosition(sketch.getX(), sketch.getY(), 10); ViewFrame newFrame = new ViewFrame(this, sketch.getFrame().getMModel()); ViewNode newNode = new ViewNode(viewName, (int) newP.getX(), (int) newP.getY(), newFrame); sketch.getFrame().getMModel().addView(newNode); // Add our ViewNode addVertex(newNode); // Add edge to out connected sketch addViewEdge(new ViewDefinitionEdge(newNode, sketch, getNewEdgeName())); return newNode; } /** * Add one one of our verticies to the overview * * @param theNode The node to be added */ public void addVertex(OverviewVertex theNode) { // The next call will fire a rendering. At this point, the model adapter // does not know // where it should place the node, and picks a default value. This will // cause an update // in our Node's x and y coordinates, making it forget where it was // initialized. // We store it's initializeded position so as not to lose them in the // first rendering. int initX = theNode.getX(); int initY = theNode.getY(); // Make sure the name is unique; increment it if not. if (isNameUsed(theNode.getName())) { theNode.setName(getNewName(theNode.getName())); } // Add our sketch to the graph getGraphLayoutCache().insert(theNode); // Add our vertex to the appropriate map if (theNode instanceof SketchNode) { _sketchNodes.put(theNode.toString(), (SketchNode) theNode); _appFrame.getInfoTreeUI().addSketch((SketchNode) theNode); } else if (theNode instanceof ViewNode) { _viewNodes.put(theNode.toString(), (ViewNode) theNode); _appFrame.getInfoTreeUI().addView((ViewNode) theNode); } // Set the on-screen position of our sketch to the attributes of the // sketch AttributeMap nAttribs = theNode.getAttributes(); GraphConstants.setAutoSize(nAttribs, true); GraphConstants.setBounds(nAttribs, new Rectangle2D.Double(initX, initY, 0, 0)); // Reload the graph to reflect the new changes refresh(theNode); } /** * Adds a view edge to the overview. * * @param edge */ public void addViewEdge(ViewDefinitionEdge edge) { getGraphLayoutCache().insert(edge); _viewEdges.put(edge.getName(), edge); refresh(edge); } /** * Returns the next available 'NewSketch' name, so we don't get duplicates. * * @return The next new sketch name. */ public String getNewSketchName() { return getNewName("NewSketch0"); } /** * Returns the next available 'NewView' name, so we don't get duplicates. * * @return The next new view name. */ public String getNewViewName() { return getNewName("NewView0"); } /** * Returns the next available edge name, so we don't get duplicates. * * @return The next net edge name. */ public String getNewEdgeName() { return getNewEdgeName("ve_0"); } /** * Takes an edge name and makes it unique by append a number if needed. * * @param tryName the first name to try * @return the next available new name */ public String getNewEdgeName(String tryName) { while (isEdgeNameUsed(tryName)) { tryName = EasikTools.incrementName(tryName); } return tryName; } /** * Takes a name, and makes it unique. If it is already used, appends a number to * make it unique. * * @param tryName the first name to try * @return the next available new name */ public String getNewName(String tryName) { while (isNameUsed(tryName)) { tryName = EasikTools.incrementName(tryName); } return tryName; } /** * 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. */ public boolean isNameUsed(String inName) { if (_sketchNodes.keySet().contains(inName) || _viewNodes.keySet().contains(inName)) { return true; } return false; } /** * Checks to see if an edge name is in use so that we will not have duplicates. * * @param inName The desired new name to check against * @return Is it used or not. */ public boolean isEdgeNameUsed(String inName) { if (_viewEdges.keySet().contains(inName)) { return true; } return false; } /** * Tries random positions to find location for a new sketch node to be placed in * an effort to avoid hidden nodes. Looks for any location on the canvas. Gives * up afer a specified number of tries, accepting the random position. * * @param tries The number of random tries to get a new location before giving * up. * @return The point deemed acceptable for placement of a new node. */ public Point getNewSketchPosition(int tries) { Random r = new Random(); int w = getWidth() - 120; int h = getHeight() - 40; Point p = new Point(r.nextInt(w), r.nextInt(h)); for (int i = 0; i < tries; i++) { if (getFirstCellForLocation(p.getX(), p.getY()) == null) { return p; } p = new Point(r.nextInt(w), r.nextInt(h)); } return p; } /** * Tries random positions to find location for a new view node to be placed in * an effort to avoid hidden nodes. Looks for a point on a circe around a * specified point. Gives up afer a specified number of tries, accepting the * random position. * * @param x The X coordinate circle's centre on whose circumference we are * placing the new node. * @param y The Y coordinate circle's centre on whose circumference we are * placing the new node. * @param tries The number of random tries to get a new location before giving * up. * @return The point deemed acceptable for placement of a new node. */ public Point getNewViewPosition(int x, int y, int tries) { Random random = new Random(); // make sure we have tries tries = (tries <= 0) ? 10 : tries; // Radius and angle int r = 145; int theta; // Java compiler thinks it is smart and wants these initialized here. int newX = 0; int newY = 0; for (int i = 0; i < tries; i++) { theta = (int) (random.nextDouble() * 2 * Math.PI); // These can't be negative newX = (int) (x + r * Math.sin(theta)); newY = (int) (y - r * Math.cos(theta)); if (getFirstCellForLocation((newX > 0) ? newX : 0, (newY > 0) ? newY : 0) == null) { return new Point((newX > 0) ? newX : 0, (newY > 0) ? newY : 0); } } return new Point((newX > 0) ? newX : 0, (newY > 0) ? newY : 0); } }