blob: d0a7357cdfedfa95133b5ec8c1bbfa3bc8755c24 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.draw2d;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.PrecisionPoint;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.draw2d.graph.Path;
import org.eclipse.draw2d.graph.ShortestPathRouter;
/**
* Routes multiple connections around the children of a given container figure.
*
* @author Whitney Sorenson
* @author Randy Hudson
* @since 3.1
*/
public final class ShortestPathConnectionRouter extends AbstractRouter {
private class LayoutTracker extends LayoutListener.Stub {
public void postLayout(IFigure container) {
processLayout();
}
public void remove(IFigure child) {
removeChild(child);
}
public void setConstraint(IFigure child, Object constraint) {
addChild(child);
}
}
private Map constraintMap = new HashMap();
private Map figuresToBounds;
private Map connectionToPaths;
private boolean isDirty;
private ShortestPathRouter algorithm = new ShortestPathRouter();
private IFigure container;
private Set staleConnections = new HashSet();
private LayoutListener listener = new LayoutTracker();
private FigureListener figureListener = new FigureListener() {
public void figureMoved(IFigure source) {
Rectangle newBounds = source.getBounds().getCopy();
if (algorithm.updateObstacle(
(Rectangle) figuresToBounds.get(source), newBounds)) {
queueSomeRouting();
isDirty = true;
}
figuresToBounds.put(source, newBounds);
}
};
private boolean ignoreInvalidate;
/**
* Creates a new shortest path router with the given container. The
* container contains all the figure's which will be treated as obstacles
* for the connections to avoid. Any time a child of the container moves,
* one or more connections will be revalidated to process the new obstacle
* locations. The connections being routed must not be contained within the
* container.
*
* @param container
* the container
*/
public ShortestPathConnectionRouter(IFigure container) {
isDirty = false;
algorithm = new ShortestPathRouter();
this.container = container;
}
void addChild(IFigure child) {
if (connectionToPaths == null)
return;
if (figuresToBounds.containsKey(child))
return;
Rectangle bounds = child.getBounds().getCopy();
algorithm.addObstacle(bounds);
figuresToBounds.put(child, bounds);
child.addFigureListener(figureListener);
isDirty = true;
}
private void hookAll() {
figuresToBounds = new HashMap();
for (int i = 0; i < container.getChildren().size(); i++)
addChild((IFigure) container.getChildren().get(i));
container.addLayoutListener(listener);
}
private void unhookAll() {
container.removeLayoutListener(listener);
if (figuresToBounds != null) {
Iterator figureItr = figuresToBounds.keySet().iterator();
while (figureItr.hasNext()) {
// Must use iterator's remove to avoid concurrent modification
IFigure child = (IFigure) figureItr.next();
figureItr.remove();
removeChild(child);
}
figuresToBounds = null;
}
}
/**
* Gets the constraint for the given {@link Connection}. The constraint is
* the paths list of bend points for this connection.
*
* @param connection
* The connection whose constraint we are retrieving
* @return The constraint
*/
public Object getConstraint(Connection connection) {
return constraintMap.get(connection);
}
/**
* Returns the default spacing maintained on either side of a connection.
* The default value is 4.
*
* @return the connection spacing
* @since 3.2
*/
public int getSpacing() {
return algorithm.getSpacing();
}
/**
* @see ConnectionRouter#invalidate(Connection)
*/
public void invalidate(Connection connection) {
if (ignoreInvalidate)
return;
staleConnections.add(connection);
isDirty = true;
}
private void processLayout() {
if (staleConnections.isEmpty())
return;
((Connection) staleConnections.iterator().next()).revalidate();
}
private void processStaleConnections() {
Iterator iter = staleConnections.iterator();
if (iter.hasNext() && connectionToPaths == null) {
connectionToPaths = new HashMap();
hookAll();
}
while (iter.hasNext()) {
Connection conn = (Connection) iter.next();
Path path = (Path) connectionToPaths.get(conn);
if (path == null) {
path = new Path(conn);
connectionToPaths.put(conn, path);
algorithm.addPath(path);
}
List constraint = (List) getConstraint(conn);
if (constraint == null)
constraint = Collections.EMPTY_LIST;
Point start = conn.getSourceAnchor().getReferencePoint().getCopy();
Point end = conn.getTargetAnchor().getReferencePoint().getCopy();
container.translateToRelative(start);
container.translateToRelative(end);
path.setStartPoint(start);
path.setEndPoint(end);
if (!constraint.isEmpty()) {
PointList bends = new PointList(constraint.size());
for (int i = 0; i < constraint.size(); i++) {
Bendpoint bp = (Bendpoint) constraint.get(i);
bends.addPoint(bp.getLocation());
}
path.setBendPoints(bends);
} else
path.setBendPoints(null);
isDirty |= path.isDirty;
}
staleConnections.clear();
}
void queueSomeRouting() {
if (connectionToPaths == null || connectionToPaths.isEmpty())
return;
try {
ignoreInvalidate = true;
((Connection) connectionToPaths.keySet().iterator().next())
.revalidate();
} finally {
ignoreInvalidate = false;
}
}
/**
* @see ConnectionRouter#remove(Connection)
*/
public void remove(Connection connection) {
staleConnections.remove(connection);
constraintMap.remove(connection);
if (connectionToPaths == null)
return;
Path path = (Path) connectionToPaths.remove(connection);
algorithm.removePath(path);
isDirty = true;
if (connectionToPaths.isEmpty()) {
unhookAll();
connectionToPaths = null;
} else {
// Make sure one of the remaining is revalidated so that we can
// re-route again.
queueSomeRouting();
}
}
void removeChild(IFigure child) {
if (connectionToPaths == null)
return;
Rectangle bounds = child.getBounds().getCopy();
boolean change = algorithm.removeObstacle(bounds);
figuresToBounds.remove(child);
child.removeFigureListener(figureListener);
if (change) {
isDirty = true;
queueSomeRouting();
}
}
/**
* @see ConnectionRouter#route(Connection)
*/
public void route(Connection conn) {
if (isDirty) {
ignoreInvalidate = true;
processStaleConnections();
isDirty = false;
List updated = algorithm.solve();
Connection current;
for (int i = 0; i < updated.size(); i++) {
Path path = (Path) updated.get(i);
current = (Connection) path.data;
current.revalidate();
PointList points = path.getPoints().getCopy();
Point ref1, ref2, start, end;
ref1 = new PrecisionPoint(points.getPoint(1));
ref2 = new PrecisionPoint(points.getPoint(points.size() - 2));
current.translateToAbsolute(ref1);
current.translateToAbsolute(ref2);
start = current.getSourceAnchor().getLocation(ref1).getCopy();
end = current.getTargetAnchor().getLocation(ref2).getCopy();
current.translateToRelative(start);
current.translateToRelative(end);
points.setPoint(start, 0);
points.setPoint(end, points.size() - 1);
current.setPoints(points);
}
ignoreInvalidate = false;
}
}
/**
* @return All connection paths after routing dirty paths. Some of the paths
* that were not dirty may change as well, as a consequence of new
* routings.
* @since 3.5
*/
public List getPathsAfterRouting() {
if (isDirty) {
processStaleConnections();
isDirty = false;
List all = algorithm.solve();
return all;
}
return null;
}
/**
* @see ConnectionRouter#setConstraint(Connection, Object)
*/
public void setConstraint(Connection connection, Object constraint) {
// Connection.setConstraint() already calls revalidate, so we know that
// a
// route() call will follow.
staleConnections.add(connection);
constraintMap.put(connection, constraint);
isDirty = true;
}
/**
* Sets the default space that should be maintained on either side of a
* connection. This causes the connections to be separated from each other
* and from the obstacles. The default value is 4.
*
* @param spacing
* the connection spacing
* @since 3.2
*/
public void setSpacing(int spacing) {
algorithm.setSpacing(spacing);
}
/**
* @return true if there are connections routed by this router, false
* otherwise
* @since 3.5
*/
public boolean hasMoreConnections() {
return connectionToPaths != null && !connectionToPaths.isEmpty();
}
/**
* @return the container which contains connections routed by this router
* @since 3.5
*/
public IFigure getContainer() {
return container;
}
/**
* Sets the value indicating if connection invalidation should be ignored.
*
* @param b
* true if invalidation should be skipped, false otherwise
* @since 3.5
*/
public void setIgnoreInvalidate(boolean b) {
ignoreInvalidate = b;
}
/**
* Returns the value indicating if connection invalidation should be
* ignored.
*
* @return true if invalidation should be skipped, false otherwise
* @since 3.5
*/
public boolean shouldIgnoreInvalidate() {
return ignoreInvalidate;
}
/**
* Returns the value indicating if the router is dirty, i.e. if there are
* any outstanding connections that need to be routed
*
* @return true if there are connections to be routed, false otherwise
* @since 3.5
*/
public boolean isDirty() {
return isDirty;
}
/**
* Returns true if the given connection is routed by this router, false
* otherwise
*
* @param conn
* Connection whose router is questioned
* @return true if this is the router used for conn
* @since 3.5
*/
public boolean containsConnection(Connection conn) {
return connectionToPaths != null && connectionToPaths.containsKey(conn);
}
}