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