CompositeFigure.java
/*
* @(#)CompositeFigure.java
*
* Project: JHotdraw - a GUI framework for technical drawings
* http://www.jhotdraw.org
* http://jhotdraw.sourceforge.net
* Copyright: © by the original author(s) and all contributors
* License: Lesser GNU Public License (LGPL)
* http://www.opensource.org/licenses/lgpl-license.html
*/
package CH.ifa.draw.standard;
import CH.ifa.draw.util.*;
import CH.ifa.draw.framework.*;
import java.awt.*;
import java.util.*;
import java.io.*;
import Testing.RunToTest;
/**
* A Figure that is composed of several figures. A CompositeFigure
* doesn't define any layout behavior. It is up to subclassers to
* arrange the contained figures.
* <hr>
* <b>Design Patterns</b><P>
* <img src="images/red-ball-small.gif" width=6 height=6 alt=" o ">
* <b><a href=../pattlets/sld012.htm>Composite</a></b><br>
* CompositeFigure enables to treat a composition of figures like
* a single figure.<br>
*
* @see Figure
*
* @version <$CURRENT_VERSION$>
*/
public abstract class CompositeFigure
extends AbstractFigure
implements FigureChangeListener {
/**
* The figures that this figure is composed of
* @see #add
* @see #remove
*/
protected Vector fFigures;
/*
* Serialization support.
*/
private static final long serialVersionUID = 7408153435700021866L;
private int compositeFigureSerializedDataVersion = 1;
private QuadTree _theQuadTree;
protected int _nLowestZ;
protected int _nHighestZ;
protected CompositeFigure() {
fFigures = new Vector();
_nLowestZ = 0;
_nHighestZ = 0;
}
/**
* Adds a figure to the list of figures. Initializes the
* the figure's container.
*
* @param figure to be added to the drawing
* @return the figure that was inserted (might be different from the figure specified).
*/
public Figure add(Figure figure) {
if (!fFigures.contains(figure)) {
figure.setZValue(++_nHighestZ);
fFigures.addElement(figure);
figure.addToContainer(this);
_addToQuadTree(figure);
}
return figure;
}
/**
* Adds a vector of figures.
*
* @see #add
* @deprecated use addAll(FigureEnumeration) instead
*/
public void addAll(Vector newFigures) {
addAll(new FigureEnumerator(newFigures));
}
/**
* Adds a FigureEnumeration of figures.
*
* @see #add
* @param fe (unused) enumeration containing all figures to be added
*/
public void addAll(FigureEnumeration fe) {
while (fe.hasMoreElements()) {
add(fe.nextFigure());
}
}
/**
* Removes a figure from the composite.
*
* @param figure that is part of the drawing and should be removed
* @return the figure that has been removed (might be different from the figure specified)
* @see #removeAll
*/
public Figure remove(Figure figure) {
return orphan(figure);
}
/**
* Removes a vector of figures.
*
* @see #remove
* @deprecated use removeAll(FigureEnumeration) instead
*/
public void removeAll(Vector figures) {
removeAll(new FigureEnumerator(figures));
}
/**
* Removes a FigureEnumeration of figures.
* @see #remove
*/
public void removeAll(FigureEnumeration fe) {
while (fe.hasMoreElements()) {
remove(fe.nextFigure());
}
}
/**
* Removes all children.
* @see #remove
*/
public void removeAll() {
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
Figure figure = fe.nextFigure();
figure.removeFromContainer(this);
}
fFigures.removeAllElements();
_clearQuadTree();
_nLowestZ = 0;
_nHighestZ = 0;
}
/**
* Removes a figure from the figure list, but
* doesn't release it. Use this method to temporarily
* manipulate a figure outside of the drawing.
*
* @param figure that is part of the drawing and should be added
*/
public synchronized Figure orphan(Figure figure) {
if (fFigures.contains(figure)) {
figure.removeFromContainer(this);
fFigures.removeElement(figure);
_removeFromQuadTree(figure);
}
return figure;
}
/**
* Removes a vector of figures from the figure's list
* without releasing the figures.
*
* @see #orphan
* @deprecated use orphanAll(FigureEnumeration) instead
*/
public void orphanAll(Vector newFigures) {
orphanAll(new FigureEnumerator(newFigures));
}
public void orphanAll(FigureEnumeration fe) {
while (fe.hasMoreElements()) {
orphan(fe.nextFigure());
}
}
/**
* Replaces a figure in the drawing without
* removing it from the drawing.
*
* @param figure figure to be replaced
* @param replacement figure that should replace the specified figure
* @return the figure that has been inserted (might be different from the figure specified)
*/
public synchronized Figure replace(Figure figure, Figure replacement) {
int index = fFigures.indexOf(figure);
if (index != -1) {
replacement.setZValue(figure.getZValue());
replacement.addToContainer(this); // will invalidate figure
figure.removeFromContainer(this);
fFigures.setElementAt(replacement, index);
figure.changed();
replacement.changed();
}
return replacement;
}
/**
* Sends a figure to the back of the drawing.
*
* @param figure that is part of the drawing
*/
public synchronized void sendToBack(Figure figure) {
if (fFigures.contains(figure)) {
fFigures.removeElement(figure);
fFigures.insertElementAt(figure,0);
_nLowestZ--;
figure.setZValue(_nLowestZ);
figure.changed();
}
}
/**
* Brings a figure to the front.
*
* @param figure that is part of the drawing
*/
public synchronized void bringToFront(Figure figure) {
if (fFigures.contains(figure)) {
fFigures.removeElement(figure);
fFigures.addElement(figure);
_nHighestZ++;
figure.setZValue(_nHighestZ);
figure.changed();
}
}
/**
* Sends a figure to a certain layer within a drawing. Each figure
* lays in a unique layer and the layering order decides which
* figure is drawn on top of another figure. Figures with a higher
* layer number have usually been added later and may overlay
* figures in lower layers. Layers are counted from to (the number
* of figures - 1).
* The figure is removed from its current layer (if it has been already
* part of this drawing) and is transferred to the specified layers after
* all figures between the original layer and the new layer are shifted to
* one layer below to fill the layer sequence. It is not possible to skip a
* layer number and if the figure is sent to a layer beyond the latest layer
* it will be added as the last figure to the drawing and its layer number
* will be set to the be the one beyond the latest layer so far.
*
* @param figure figure to be sent to a certain layer
* @param layerNr target layer of the figure
*/
public void sendToLayer(Figure figure, int layerNr) {
if (fFigures.contains(figure)) {
if (layerNr >= fFigures.size()) {
layerNr = fFigures.size() - 1;
}
Figure layerFigure = getFigureFromLayer(layerNr);
int layerFigureZValue = layerFigure.getZValue();
int figureLayer = getLayer(figure);
// move figure forward
if (figureLayer < layerNr) {
assignFiguresToPredecessorZValue(figureLayer + 1, layerNr);
}
else if (figureLayer > layerNr) {
assignFiguresToSuccessorZValue(layerNr, figureLayer - 1);
}
fFigures.removeElement(figure);
fFigures.insertElementAt(figure, layerNr);
figure.setZValue(layerFigureZValue);
figure.changed();
}
}
private void assignFiguresToPredecessorZValue(int lowerBound, int upperBound) {
// cannot shift figures to a lower layer if the lower bound is
// already the first layer.
if (upperBound >= fFigures.size()) {
upperBound = fFigures.size() - 1;
}
for (int i = upperBound; i >= lowerBound; i--) {
Figure currentFigure = (Figure)fFigures.elementAt(i);
Figure predecessorFigure = (Figure)fFigures.elementAt(i - 1);
currentFigure.setZValue(predecessorFigure.getZValue());
}
}
private void assignFiguresToSuccessorZValue(int lowerBound, int upperBound) {
if (upperBound >= fFigures.size()) {
upperBound = fFigures.size() - 1;
}
for (int i = upperBound; i >= lowerBound; i--) {
Figure currentFigure = (Figure)fFigures.elementAt(i);
Figure successorFigure = (Figure)fFigures.elementAt(i + 1);
currentFigure.setZValue(successorFigure.getZValue());
}
}
/**
* Gets the layer for a certain figure (first occurrence). The number
* returned is the number of the layer in which the figure is placed.
*
* @param figure figure to be queried for its layering place
* @return number of the layer in which the figure is placed and -1 if the
* figure could not be found.
* @see #sendToLayer
*/
public int getLayer(Figure figure) {
if (!fFigures.contains(figure)) {
return -1;
}
else {
return fFigures.indexOf(figure);
}
}
/**
* Gets the figure from a certain layer.
*
* @param layerNr number of the layer which figure should be returned
* @return figure from the layer specified, null, if the layer nr was outside
* the number of possible layer (0...(number of figures - 1))
* @see #sendToLayer
*/
public Figure getFigureFromLayer(int layerNr) {
if ((layerNr >= 0) && (layerNr < fFigures.size())) {
return (Figure)fFigures.elementAt(layerNr);
}
else {
return null;
}
}
/**
* Draws all the contained figures
* @see Figure#draw
*/
public void draw(Graphics g) {
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
fe.nextFigure().draw(g);
}
}
/**
* Draws only the given figures
* @see Figure#draw
*/
public void draw(Graphics g, FigureEnumeration fe) {
while (fe.hasMoreElements()) {
fe.nextFigure().draw(g);
}
}
/**
* Gets a figure at the given index.
*/
public Figure figureAt(int i) {
return (Figure)fFigures.elementAt(i);
}
/**
* Returns an Enumeration for accessing the contained figures.
* The figures are returned in the drawing order.
*/
public final FigureEnumeration figures() {
return new FigureEnumerator((Vector)fFigures.clone());
}
/**
* Returns an enumeration to iterate in
* Z-order back to front over the figures
* that lie within the given bounds.
*/
public FigureEnumeration figures(Rectangle viewRectangle) {
if (_theQuadTree != null) {
Vector v =
_theQuadTree.getAllWithin(new Bounds(viewRectangle).asRectangle2D());
Vector v2 = new Vector();
for (Enumeration e=v.elements(); e.hasMoreElements(); ) {
Figure f = (Figure) e.nextElement();
//int z = fFigures.indexOf(f);
v2.addElement(new OrderedFigureElement(f, f.getZValue()));
}
Collections.sort(v2);
Vector v3 = new Vector();
for (Enumeration e=v2.elements(); e.hasMoreElements(); ) {
OrderedFigureElement ofe = (OrderedFigureElement)
e.nextElement();
v3.addElement(ofe.getFigure());
}
return new FigureEnumerator(v3);
}
return figures();
}
/**
* Gets number of child figures.
*/
public int figureCount() {
return fFigures.size();
}
/**
* Returns an Enumeration for accessing the contained figures
* in the reverse drawing order.
*/
public final FigureEnumeration figuresReverse() {
return new ReverseFigureEnumerator((Vector)fFigures.clone());
}
/**
* Finds a top level Figure. Use this call for hit detection that
* should not descend into the figure's children.
*/
public Figure findFigure(int x, int y) {
FigureEnumeration k = figuresReverse();
while (k.hasMoreElements()) {
Figure figure = k.nextFigure();
if (figure.containsPoint(x, y)) {
return figure;
}
}
return null;
}
/**
* Finds a top level Figure that intersects the given rectangle.
*/
public Figure findFigure(Rectangle r) {
FigureEnumeration k = figuresReverse();
while (k.hasMoreElements()) {
Figure figure = k.nextFigure();
Rectangle fr = figure.displayBox();
if (r.intersects(fr)) {
return figure;
}
}
return null;
}
/**
* Finds a top level Figure, but supresses the passed
* in figure. Use this method to ignore a figure
* that is temporarily inserted into the drawing.
* @param x the x coordinate
* @param y the y coordinate
* @param without the figure to be ignored during
* the find.
*/
public Figure findFigureWithout(int x, int y, Figure without) {
if (without == null)
return findFigure(x, y);
FigureEnumeration k = figuresReverse();
while (k.hasMoreElements()) {
Figure figure = k.nextFigure();
if (figure.containsPoint(x, y) && !figure.includes(without)) {
return figure;
}
}
return null;
}
/**
* Finds a top level Figure that intersects the given rectangle.
* It supresses the passed
* in figure. Use this method to ignore a figure
* that is temporarily inserted into the drawing.
*/
public Figure findFigure(Rectangle r, Figure without) {
if (without == null)
return findFigure(r);
FigureEnumeration k = figuresReverse();
while (k.hasMoreElements()) {
Figure figure = k.nextFigure();
Rectangle fr = figure.displayBox();
if (r.intersects(fr) && !figure.includes(without)) {
return figure;
}
}
return null;
}
/**
* Finds a figure but descends into a figure's
* children. Use this method to implement <i>click-through</i>
* hit detection, that is, you want to detect the inner most
* figure containing the given point.
*/
public Figure findFigureInside(int x, int y) {
FigureEnumeration k = figuresReverse();
while (k.hasMoreElements()) {
Figure figure = k.nextFigure().findFigureInside(x, y);
if (figure != null) {
return figure;
}
}
return null;
}
/**
* Finds a figure but descends into a figure's
* children. It supresses the passed
* in figure. Use this method to ignore a figure
* that is temporarily inserted into the drawing.
*/
public Figure findFigureInsideWithout(int x, int y, Figure without) {
FigureEnumeration k = figuresReverse();
while (k.hasMoreElements()) {
Figure figure = k.nextFigure();
if (figure != without) {
Figure found = figure.findFigureInside(x, y);
if (found != null) {
return found;
}
}
}
return null;
}
/**
* Checks if the composite figure has the argument as one of
* its children.
* @return true if the figure is part of this CompositeFigure, else otherwise
*/
public boolean includes(Figure figure) {
if (super.includes(figure)) {
return true;
}
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
Figure f = fe.nextFigure();
if (f.includes(figure)) {
return true;
}
}
return false;
}
/**
* Moves all the given figures by x and y. Doesn't announce
* any changes. Subclassers override
* basicMoveBy. Clients usually call moveBy.
* @see #moveBy
*/
protected void basicMoveBy(int x, int y) {
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
fe.nextFigure().moveBy(x,y);
}
}
/**
* Releases the figure and all its children.
*/
public void release() {
super.release();
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
Figure figure = fe.nextFigure();
figure.release();
}
}
/**
* Propagates the figureInvalidated event to my listener.
* @see FigureChangeListener
*/
public void figureInvalidated(FigureChangeEvent e) {
if (listener() != null) {
listener().figureInvalidated(e);
}
}
/**
* Propagates the removeFromDrawing request up to the container.
* @see FigureChangeListener
*/
public void figureRequestRemove(FigureChangeEvent e) {
if (listener() != null) {
listener().figureRequestRemove(new FigureChangeEvent(this));
}
}
/**
* Propagates the requestUpdate request up to the container.
* @see FigureChangeListener
*/
public void figureRequestUpdate(FigureChangeEvent e) {
if (listener() != null) {
listener().figureRequestUpdate(e);
}
}
public void figureChanged(FigureChangeEvent e) {
_removeFromQuadTree(e.getFigure());
_addToQuadTree(e.getFigure());
}
public void figureRemoved(FigureChangeEvent e) {
}
/**
* Writes the contained figures to the StorableOutput.
*/
public void write(StorableOutput dw) {
super.write(dw);
dw.writeInt(figureCount());
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
dw.writeStorable(fe.nextFigure());
}
}
/**
* Reads the contained figures from StorableInput.
*/
public void read(StorableInput dr) throws IOException {
super.read(dr);
int size = dr.readInt();
fFigures = new Vector(size);
for (int i=0; i<size; i++) {
add((Figure)dr.readStorable());
}
}
private void readObject(ObjectInputStream s)
throws ClassNotFoundException, IOException {
s.defaultReadObject();
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
Figure figure = fe.nextFigure();
figure.addToContainer(this);
}
}
/**
* Used to optimize rendering. Rendering of many objects may
* be slow until this method is called. The view rectangle
* should at least approximately enclose the CompositeFigure.
* If the view rectangle is too small or too large, performance
* may suffer.
*
* Don't forget to call this after loading or creating a
* new CompositeFigure. If you forget, drawing performance may
* suffer.
*/
public void init(Rectangle viewRectangle) {
_theQuadTree = new QuadTree(new Bounds(viewRectangle).asRectangle2D());
FigureEnumeration fe = figures();
while (fe.hasMoreElements()) {
_addToQuadTree(fe.nextFigure());
}
}
private void _addToQuadTree(Figure f) {
if (_theQuadTree != null) {
_theQuadTree.add(f, new Bounds(f.displayBox()).asRectangle2D());
}
}
private void _removeFromQuadTree(Figure f) {
if (_theQuadTree != null) {
_theQuadTree.remove(f);
}
}
private void _clearQuadTree() {
if (_theQuadTree != null) {
_theQuadTree.clear();
}
}
}