/*******************************************************************************
* Copyright (c) 2007 Spring IDE Developers
* 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:
* Spring IDE Developers - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.webflow.ui.graph.parts;
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.AbstractRouter;
import org.eclipse.draw2d.Bendpoint;
import org.eclipse.draw2d.Connection;
import org.eclipse.draw2d.ConnectionRouter;
import org.eclipse.draw2d.FigureListener;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.LayoutListener;
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;
import org.springframework.ide.eclipse.webflow.ui.graph.figures.InlineFlowStateFigure;
/**
* Routes multiple connections around the children of a given container figure.
* @author Christian Dupuis
* @since 3.1
*/
@SuppressWarnings("unchecked")
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++) {
IFigure figure = (IFigure) container.getChildren().get(i);
if (!(figure instanceof InlineFlowStateFigure)) {
addChild(figure);
}
else {
for (int j = 0; j < ((IFigure) figure.getChildren().get(0))
.getChildren().size(); j++) {
addChild((IFigure) ((IFigure) figure.getChildren().get(0))
.getChildren().get(j));
}
}
}
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;
if (child instanceof InlineFlowStateFigure) {
// TODO recheck if this is not needed
/*InlineFlowStateFigure inlineFigure = (InlineFlowStateFigure) child;
for (int j = 0; j < ((IFigure) inlineFigure.getChildren().get(0))
.getChildren().size(); j++) {
removeChild((IFigure) ((IFigure) inlineFigure.getChildren()
.get(0)).getChildren().get(j));
}*/
}
else {
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;
}
}
/**
* @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);
}
}