/* Violet - A program for editing UML diagrams. Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package com.horstmann.violet.framework; import java.awt.Graphics2D; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.beans.DefaultPersistenceDelegate; import java.beans.Encoder; import java.beans.Statement; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; /** A graph consisting of selectable nodes and edges. */ public abstract class Graph implements Serializable { /** Constructs a graph with no nodes or edges. */ public Graph() { nodes = new ArrayList(); edges = new ArrayList(); nodesToBeRemoved = new ArrayList(); edgesToBeRemoved = new ArrayList(); needsLayout = true; } /** Adds an edge to the graph that joins the nodes containing the given points. If the points aren't both inside nodes, then no edge is added. @param e the edge to add @param p1 a point in the starting node @param p2 a point in the ending node */ public boolean connect(Edge e, Point2D p1, Point2D p2) { Node n1 = findNode(p1); Node n2 = findNode(p2); if (n1 != null) { e.connect(n1, n2); if (n1.addEdge(e, p1, p2)) { edges.add(e); if (!nodes.contains(e.getEnd())) nodes.add(e.getEnd()); needsLayout = true; return true; } } return false; } /** Adds a node to the graph so that the top left corner of the bounding rectangle is at the given point. @param n the node to add @param p the desired location */ public boolean add(Node n, Point2D p) { Rectangle2D bounds = n.getBounds(); n.translate(p.getX() - bounds.getX(), p.getY() - bounds.getY()); boolean accepted = false; boolean insideANode = false; for (int i = nodes.size() - 1; i >= 0 && !accepted; i--) { Node parentNode = (Node)nodes.get(i); if (parentNode.contains(p)) { insideANode = true; if (parentNode.addNode(n, p)) accepted = true; } } if (insideANode && !accepted) return false; nodes.add(n); needsLayout = true; return true; } /** Finds a node containing the given point. @param p a point @return a node containing p or null if no nodes contain p */ public Node findNode(Point2D p) { for (int i = nodes.size() - 1; i >= 0; i--) { Node n = (Node)nodes.get(i); if (n.contains(p)) return n; } return null; } /** Finds an edge containing the given point. @param p a point @return an edge containing p or null if no edges contain p */ public Edge findEdge(Point2D p) { for (int i = edges.size() - 1; i >= 0; i--) { Edge e = (Edge)edges.get(i); if (e.contains(p)) return e; } return null; } /** Draws the graph @param g2 the graphics context */ public void draw(Graphics2D g2, Grid g) { layout(g2, g); for (int i = 0; i < nodes.size(); i++) { Node n = (Node)nodes.get(i); n.draw(g2); } for (int i = 0; i < edges.size(); i++) { Edge e = (Edge)edges.get(i); e.draw(g2); } } /** Removes a node and all edges that start or end with that node @param n the node to remove */ public void removeNode(Node n) { if (nodesToBeRemoved.contains(n)) return; nodesToBeRemoved.add(n); // notify nodes of removals for (int i = 0; i < nodes.size(); i++) { Node n2 = (Node)nodes.get(i); n2.removeNode(this, n); } for (int i = 0; i < edges.size(); i++) { Edge e = (Edge)edges.get(i); if (e.getStart() == n || e.getEnd() == n) removeEdge(e); } needsLayout = true; } /** Removes an edge from the graph. @param e the edge to remove */ public void removeEdge(Edge e) { if (edgesToBeRemoved.contains(e)) return; edgesToBeRemoved.add(e); for (int i = nodes.size() - 1; i >= 0; i--) { Node n = (Node)nodes.get(i); n.removeEdge(this, e); } needsLayout = true; } /** Causes the layout of the graph to be recomputed. */ public void layout() { needsLayout = true; } /** Computes the layout of the graph. If you override this method, you must first call <code>super.layout</code>. @param g2 the graphics context @param g the grid to snap to */ protected void layout(Graphics2D g2, Grid g) { if (!needsLayout) return; nodes.removeAll(nodesToBeRemoved); edges.removeAll(edgesToBeRemoved); nodesToBeRemoved.clear(); edgesToBeRemoved.clear(); for (int i = 0; i < nodes.size(); i++) { Node n = (Node) nodes.get(i); n.layout(this, g2, g); } needsLayout = false; } /** Gets the smallest rectangle enclosing the graph @param g2 the graphics context @return the bounding rectangle */ public Rectangle2D getBounds(Graphics2D g2) { Rectangle2D r = minBounds; for (int i = 0; i < nodes.size(); i++) { Node n = (Node)nodes.get(i); Rectangle2D b = n.getBounds(); if (r == null) r = b; else r.add(b); } for (int i = 0; i < edges.size(); i++) { Edge e = (Edge)edges.get(i); r.add(e.getBounds(g2)); } return r == null ? new Rectangle2D.Double() : new Rectangle2D.Double(r.getX(), r.getY(), r.getWidth() + AbstractNode.SHADOW_GAP, r.getHeight() + AbstractNode.SHADOW_GAP); } public Rectangle2D getMinBounds() { return minBounds; } public void setMinBounds(Rectangle2D newValue) { minBounds = newValue; } /** Gets the node types of a particular graph type. @return an array of node prototypes */ public abstract Node[] getNodePrototypes(); /** Gets the edge types of a particular graph type. @return an array of edge prototypes */ public abstract Edge[] getEdgePrototypes(); /** Adds a persistence delegate to a given encoder that encodes the child nodes of this node. @param encoder the encoder to which to add the delegate */ public static void setPersistenceDelegate(Encoder encoder) { encoder.setPersistenceDelegate(Graph.class, new DefaultPersistenceDelegate() { protected void initialize(Class type, Object oldInstance, Object newInstance, Encoder out) { super.initialize(type, oldInstance, newInstance, out); Graph g = (Graph)oldInstance; for (int i = 0; i < g.nodes.size(); i++) { Node n = (Node)g.nodes.get(i); Rectangle2D bounds = n.getBounds(); Point2D p = new Point2D.Double(bounds.getX(), bounds.getY()); out.writeStatement( new Statement(oldInstance, "addNode", new Object[]{ n, p }) ); } for (int i = 0; i < g.edges.size(); i++) { Edge e = (Edge)g.edges.get(i); out.writeStatement( new Statement(oldInstance, "connect", new Object[]{ e, e.getStart(), e.getEnd() }) ); } } }); } /** Gets the nodes of this graph. @return an unmodifiable collection of the nodes */ public Collection getNodes() { return nodes; } /** Gets the edges of this graph. @return an unmodifiable collection of the edges */ public Collection getEdges() { return edges; } /** Adds a node to this graph. This method should only be called by a decoder when reading a data file. @param n the node to add @param p the desired location */ public void addNode(Node n, Point2D p) { Rectangle2D bounds = n.getBounds(); n.translate(p.getX() - bounds.getX(), p.getY() - bounds.getY()); nodes.add(n); } /** Adds an edge to this graph. This method should only be called by a decoder when reading a data file. @param e the edge to add @param start the start node of the edge @param end the end node of the edge */ public void connect(Edge e, Node start, Node end) { e.connect(start, end); edges.add(e); } private ArrayList nodes; private ArrayList edges; private transient ArrayList nodesToBeRemoved; private transient ArrayList edgesToBeRemoved; private transient boolean needsLayout; private transient Rectangle2D minBounds; }