org.openbp.cockpit.modeler.figures.spline.PolySplineConnection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of openbp-cockpit Show documentation
Show all versions of openbp-cockpit Show documentation
OpenBP Cockpit (graphical process modeler)
The newest version!
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openbp.cockpit.modeler.figures.spline;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.awt.datatransfer.Transferable;
import java.awt.event.MouseEvent;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.Point2D;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import java.util.Vector;
import org.openbp.cockpit.modeler.Modeler;
import org.openbp.cockpit.modeler.ModelerColors;
import org.openbp.cockpit.modeler.ModelerGraphics;
import org.openbp.cockpit.modeler.drawing.DrawingEditorPlugin;
import org.openbp.cockpit.modeler.drawing.ProcessDrawing;
import org.openbp.cockpit.modeler.figures.VisualElement;
import org.openbp.cockpit.modeler.figures.VisualElementEvent;
import org.openbp.cockpit.modeler.figures.generic.GeometryUtil;
import org.openbp.cockpit.modeler.figures.generic.MoveableTitleFigure;
import org.openbp.cockpit.modeler.figures.generic.Orientation;
import org.openbp.cockpit.modeler.figures.generic.UpdatableFigure;
import org.openbp.cockpit.modeler.figures.generic.XArrowTip;
import org.openbp.cockpit.modeler.figures.tag.TagConnector;
import org.openbp.cockpit.modeler.undo.ModelerUndoable;
import org.openbp.cockpit.modeler.util.FigureUtil;
import org.openbp.common.CommonUtil;
import org.openbp.jaspira.decoration.DecorationMgr;
import CH.ifa.draw.figures.LineDecoration;
import CH.ifa.draw.framework.ConnectionFigure;
import CH.ifa.draw.framework.Connector;
import CH.ifa.draw.framework.Drawing;
import CH.ifa.draw.framework.DrawingView;
import CH.ifa.draw.framework.Figure;
import CH.ifa.draw.framework.FigureChangeEvent;
import CH.ifa.draw.framework.Locator;
import CH.ifa.draw.standard.AbstractHandle;
import CH.ifa.draw.standard.AbstractLocator;
import CH.ifa.draw.util.StorableInput;
import CH.ifa.draw.util.StorableOutput;
/**
* A spline connection is a standard implementation of the connection figure interface.
*
* @author Stephan Moritz
*/
public abstract class PolySplineConnection extends PolySplineFigure
implements ConnectionFigure, VisualElement, UpdatableFigure
{
public static final Double NAN = new Double(Double.NaN);
/** Decoration key for the spline animation (used with Double objects) */
public static final String DECO_ANIMATION = "Line.Animation";
/** Decoration for the end of the connection (arrow) */
private static final LineDecoration endDecoration = new XArrowTip(0.4, 15, 10);
/** Decoration for the end of the animation (arrow) */
private static final LineDecoration animationDecoration = new XArrowTip(0.4, 10, -1);
/** Start connector */
private Connector startConnector;
/** End connector */
private Connector endConnector;
/** Position of the label on the spline */
private SplineLocator textLocator;
/** Label of this connection */
protected MoveableTitleFigure label;
/** Process drawing we belong to */
private ProcessDrawing drawing;
/** Factor used for the postion of the controlpoint of the startpoint */
private double startFactor = 0.3d;
/** Factor used for the postion of the controlpoint of the endpoint */
private double endFactor = 0.3d;
/** Visual status as defined by VisualElement */
private int visualStatus = VISUAL_VISIBLE;
/**
* Constructor.
*
* @param drawing Process drawing that owns the figure
*/
public PolySplineConnection(ProcessDrawing drawing)
{
this();
setDrawing(drawing);
}
/**
* Constructor.
*/
public PolySplineConnection()
{
super();
// Set the decorators
// (start: none, end: arrow, animation: point)
setEndDecoration(endDecoration);
setAnimationDecoration(animationDecoration);
textLocator = new SplineLocator();
label = new MoveableTitleFigure();
label.connect(this);
}
//////////////////////////////////////////////////
// @@ ConnectionFigure implementation
//////////////////////////////////////////////////
/**
* Tests whether two figures can be connected.
*
* @param startFigure Proposed start figure
* @param endFigure Proposed end figure
* @param flags App-specific flags that may define constraints for the connection check
*/
public abstract boolean canConnectFigures(Figure startFigure, Figure endFigure, int flags);
/**
* Tests whether two figures can be connected.
*
* @param startFigure Proposed start figure
* @param endFigure Proposed end figure
*/
public boolean canConnect(Figure startFigure, Figure endFigure)
{
return canConnectFigures(startFigure, endFigure, 0);
}
/**
* Calls {@link #handleDisconnect} if a connection can be established.
*/
public void connectStart(Connector startConnector)
{
this.startConnector = startConnector;
// Add ourself as figure change listener to the figure we are connected to
addStartConnectorChangeListener();
// check if this results in a finished connection
if (endConnector != null)
{
handleConnect(startFigure(), endFigure());
}
}
/**
* Calls {@link #handleConnect}.
*/
public void connectEnd(Connector endConnector)
{
this.endConnector = endConnector;
// Add ourself as figure change listener to the figure we are connected to
addEndConnectorChangeListener();
// If necessary, reverse the connection
if (shouldReverse(startFigure(), endFigure()))
{
Connector swap = startConnector;
startConnector = this.endConnector;
this.endConnector = swap;
}
layoutConnection();
handleConnect(startFigure(), endFigure());
}
/**
* Disconnects the start figure.
* Calls {@link #handleDisconnect}.
*/
public void disconnectStart()
{
handleDisconnect(startFigure(), endFigure());
// Remove ourself as figure change listener from the figure we are connected to
removeStartConnectorChangeListener();
startConnector = null;
}
/**
* Disconnects the end figure.
* Calls {@link #handleDisconnect}.
*/
public void disconnectEnd()
{
handleDisconnect(startFigure(), endFigure());
// Remove ourself as figure change listener from the socket we are connected to
removeEndConnectorChangeListener();
endConnector = null;
}
/**
* Tests whether a connection connects the same figures as another connection figure.
* @param other Connection to check on equality with this figure
* @return
* true: If the start and end figures of this connection and the other connection match.
* false: If the start or end figure is different.
*/
public abstract boolean connectsSame(ConnectionFigure other);
public void updateConnection()
{
layoutConnection();
}
/**
* Sets the start connector.
*
* @param startConnector Start connector
*/
protected void setStartConnector(Connector startConnector)
{
this.startConnector = startConnector;
}
/**
* Gets the start connector.
*
* @return The connector or null if not connected
*/
public Connector getStartConnector()
{
return startConnector;
}
/**
* Sets the end connector.
*
* @param endConnector End connector
*/
protected void setEndConnector(Connector endConnector)
{
this.endConnector = endConnector;
}
/**
* Gets end connector.
*
* @return The connector or null if not connected
*/
public Connector getEndConnector()
{
return endConnector;
}
/**
* Sets the start point of the connection.
*/
public void startPoint(int x, int y)
{
if (segments.size() == 0)
{
willChange();
segments.add(new CubicCurve2D.Double(x, y, x, y, x, y, x, y));
changed();
}
else
{
setPointAt(0, new Point(x, y));
}
}
/**
* Gets the start point of the connection.
*/
public Point startPoint()
{
Point2D p = getPointAt(0);
return new Point(CommonUtil.rnd(p.getX()), CommonUtil.rnd(p.getY()));
}
/**
* Sets the end point of the connection.
*/
public void endPoint(int x, int y)
{
setPointAt(segments.size(), new Point(x, y));
}
/**
* Gets the end point of the connection.
*/
public Point endPoint()
{
Point2D p = getPointAt(segments.size());
return new Point(CommonUtil.rnd(p.getX()), CommonUtil.rnd(p.getY()));
}
/**
* Gets the start figure of the connection.
* @return The start figure or null if not connected
*/
public Figure startFigure()
{
if (startConnector != null)
{
return startConnector.owner();
}
return null;
}
/**
* Gets the end figure of the connection.
* @return The end figure or null if not connected
*/
public Figure endFigure()
{
if (endConnector != null)
{
return endConnector.owner();
}
return null;
}
//////////////////////////////////////////////////
// @@ PolySplineFigure overrides
//////////////////////////////////////////////////
protected Point2D constrainCtrlPoint(int index, int side, Point2D ctrl)
{
if ((index == 0 && side == RIGHT_CONTROLPOINT && startFigure() != null) || (index == segments.size() && side == LEFT_CONTROLPOINT && endFigure() != null))
{
// we are constraining the start-controlPoint
Point2D start = getPointAt(index);
Point2D end = getPointAt(index == 0 ? 1 : segments.size() - 1);
double xdiff = start.getX() - end.getX();
xdiff *= xdiff;
double ydiff = start.getY() - end.getY();
ydiff *= ydiff;
double distance = Math.sqrt(xdiff + ydiff);
Orientation orientation;
if (index == 0)
{
distance *= getStartFactor();
orientation = ((TagConnector) getStartConnector()).getOrientation();
}
else
{
distance *= getEndFactor();
orientation = ((TagConnector) getEndConnector()).getOrientation();
}
switch (orientation)
{
case TOP:
distance = -distance;
// Fall through
case BOTTOM:
ctrl.setLocation(start.getX(), start.getY() + distance);
break;
case LEFT:
distance = -distance;
// Fall through
case RIGHT:
ctrl.setLocation(start.getX() + distance, start.getY());
break;
}
}
return ctrl;
}
/**
* Ensures that a connection is updated if the connection was moved.
*/
protected void basicMoveBy(int dx, int dy)
{
super.basicMoveBy(dx, dy);
// Make sure that we are still connected
layoutConnection();
}
public Rectangle displayBox()
{
return super.displayBox().union(label.displayBox());
}
/**
* Draws the label in addition to the spline.
* Performs drawing only if the connection is not minimized or under the cursor.
*/
protected void drawSpline(Graphics2D g2)
{
if (isVisible() && !isMinimized())
{
super.drawSpline(g2);
label.draw(g2);
}
}
/**
* Draws the start, end and animation decorations of the spline.
*/
protected void drawDecorations(Graphics g)
{
if (isVisible() && !isMinimized())
{
// Draw the arrows
super.drawDecorations(g);
if (getAnimationDecoration() != null)
{
// Draw the small circle running down the spline
double pos = ((Double) DecorationMgr.decorate(this, DECO_ANIMATION, NAN)).doubleValue();
if (pos >= 0d && pos <= 1d)
{
Point2D p = getPointOnCurve(pos);
g.fillOval(CommonUtil.rnd(p.getX()) - 4, CommonUtil.rnd(p.getY()) - 4, 8, 8);
}
}
}
}
public boolean containsPoint(int x, int y)
{
if (!isVisible() || isMinimized())
{
// We won't react on user interaction if we are not visible or minimized.
return false;
}
return label.containsPoint(x, y) || super.containsPoint(x, y);
}
public Vector handles()
{
// We have a handle for each waypoint, one each for start and end
// control points and two each for every inner waypoint's control point
Vector handles = new Vector(segments.size() * 3 + 1);
int max = segments.size();
// Handle of first ControlPoint
handles.add(new ConstrainedControlPointHandle(this, 0, 1));
for (int i = 1; i < max; i++)
{
handles.add(new ControlPointHandle(this, i, 0));
handles.add(new ControlPointHandle(this, i, 1));
}
handles.add(new ConstrainedControlPointHandle(this, max, max - 1));
handles.addElement(new ChangeConnectionStartHandle(this));
handles.addElement(new ChangeConnectionEndHandle(this));
for (int i = 1; i < max; i++)
{
handles.add(new WayPointHandle(this, i));
}
handles.add(new MoveLableHandle());
return handles;
}
public void setPointAt(Point p, int i)
{
super.setPointAt(i, p);
layoutConnection();
}
public Locator connectedTextLocator(Figure figure)
{
return textLocator;
}
public void release()
{
super.release();
handleDisconnect(startFigure(), endFigure());
// Remove ourself as figure change listener from the figure we are connected to
removeStartConnectorChangeListener();
removeEndConnectorChangeListener();
}
//////////////////////////////////////////////////
// @@ Class methods
//////////////////////////////////////////////////
/**
* Checks if connection should be reversed.
* @param startFigure Proposed start figure
* @param endFigure Proposed end figure
* @return The default implementation always returns false
*/
protected boolean shouldReverse(Figure startFigure, Figure endFigure)
{
return false;
}
/**
* Handles the connection of a connection.
* Does nothing by default.
* @param startFigure Figure to connnect to
* @param endFigure Figure to connnect to
*/
protected void handleConnect(Figure startFigure, Figure endFigure)
{
}
/**
* Handles the disconnection of a connection.
* Does nothing by default.
* @param startFigure Figure to disconnect from
* @param endFigure Figure to disconnect from
*/
protected void handleDisconnect(Figure startFigure, Figure endFigure)
{
}
/**
* Adds ourself as figure change listener to the start figure we are connected to.
* Does nothing by default.
*/
protected void addStartConnectorChangeListener()
{
}
/**
* Removes ourself as figure change listener from the start figure we are connected to.
* Does nothing by default.
*/
protected void removeStartConnectorChangeListener()
{
}
/**
* Adds ourself as figure change listener to the end figure we are connected to.
* Does nothing by default.
*/
protected void addEndConnectorChangeListener()
{
}
/**
* Removes ourself as figure change listener from the end figure we are connected to.
* Does nothing by default.
*/
protected void removeEndConnectorChangeListener()
{
}
/**
* Gets the factor used to calculate the actual postion of the control point of the spline's start point.
*/
public double getStartFactor()
{
return startFactor;
}
/**
* Sets the factor used to calculate the actual postion of the control point of the spline's start point.
*/
public void setStartFactor(double startFactor)
{
this.startFactor = startFactor;
layoutConnection();
}
/**
* Gets the factor used to calculate the actual postion of the control point of the spline's end point.
*/
public double getEndFactor()
{
return endFactor;
}
/**
* Sets the factor used to calculate the actual postion of the control point of the spline's end point.
*/
public void setEndFactor(double endFactor)
{
this.endFactor = endFactor;
layoutConnection();
}
/**
* Gets the label of this connection.
*/
public MoveableTitleFigure getLabel()
{
return label;
}
/**
* Checks if the connection is minimized.
*/
public boolean isMinimized()
{
return false;
}
/**
* Performs a layout of the connection.
* This is called when the connection itself changes.
* By default the start and end points of the connection are recalculated.
*/
public void layoutConnection()
{
if (startConnector != null)
{
Point start = startConnector.findStart(this);
if (start != null)
{
startPoint(start.x, start.y);
}
}
if (endConnector != null)
{
Point end = endConnector.findEnd(this);
if (end != null)
{
endPoint(end.x, end.y);
}
}
clearShapeCache();
}
/**
* Performs a layout of the connection, adjusting connection start/end point directions.
* The method tries to ensure that the connection does not cross its start/end point
* figures by adjusting the direction the connection will take from its start or end point.
*
* By default, this method just calls the {@link #layoutConnection} method.
*/
public void layoutAndAdjustConnection()
{
layoutConnection();
}
//////////////////////////////////////////////////
// @@ FigureChangeListener overrides
//////////////////////////////////////////////////
public void figureChanged(FigureChangeEvent e)
{
layoutConnection();
}
public void figureRemoved(FigureChangeEvent e)
{
}
public void figureRequestRemove(FigureChangeEvent e)
{
}
public void figureInvalidated(FigureChangeEvent e)
{
}
public void figureRequestUpdate(FigureChangeEvent e)
{
}
//////////////////////////////////////////////////
// @@ Geometry serialization support
//////////////////////////////////////////////////
public void decode(String geometry, String errName)
{
if (geometry == null)
{
return;
}
StringTokenizer tok = new StringTokenizer(geometry, "|");
while (tok.hasMoreTokens())
{
decodeParameter(tok.nextToken(), errName);
}
rebuildShapeCache();
}
protected boolean decodeParameter(String parameter, String errName)
{
StringTokenizer st = new StringTokenizer(parameter, ":");
String name = st.nextToken();
if (name.equals("points"))
{
int x = GeometryUtil.parseInt(st, "points", errName);
// We provide empty segments
segments.clear();
for (int i = 0; i < x; i++)
{
double x1 = GeometryUtil.parseDouble(st, "points", errName);
double y1 = GeometryUtil.parseDouble(st, "points", errName);
double x2 = GeometryUtil.parseDouble(st, "points", errName);
double y2 = GeometryUtil.parseDouble(st, "points", errName);
double x3 = GeometryUtil.parseDouble(st, "points", errName);
double y3 = GeometryUtil.parseDouble(st, "points", errName);
double x4 = GeometryUtil.parseDouble(st, "points", errName);
double y4 = GeometryUtil.parseDouble(st, "points", errName);
segments.add(new CubicCurve2D.Double(x1, y1, x2, y2, x3, y3, x4, y4));
}
return true;
}
else if (name.equals("label"))
{
// Label position
int x = GeometryUtil.parseInt(st, "label", errName);
int y = GeometryUtil.parseInt(st, "label", errName);
label.moveBy(x, y);
return true;
}
else if (name.equals("factors"))
{
startFactor = GeometryUtil.parseDouble(st, "factors", errName);
endFactor = GeometryUtil.parseDouble(st, "factors", errName);
return true;
}
return false;
}
protected String encode()
{
String result = "points:" + segments.size();
for (Iterator it = segments.iterator(); it.hasNext();)
{
CubicCurve2D next = (CubicCurve2D) it.next();
result += (":" + FigureUtil.printInt(next.getX1()) + ":" + FigureUtil.printInt(next.getY1()) + ":" + FigureUtil.printInt(next.getCtrlX1()) + ":" + FigureUtil.printInt(next.getCtrlY1()) + ":" + FigureUtil.printInt(next.getCtrlX2()) + ":" + FigureUtil.printInt(next.getCtrlY2()) + ":" + FigureUtil.printInt(next.getX2()) + ":" + FigureUtil.printInt(next.getY2()));
}
result += "|label:" + (label.center().x - textLocator.locate(this).x) + ":" + (label.center().y - textLocator.locate(this).y);
result += "|factors:" + startFactor + ":" + endFactor;
return result;
}
//////////////////////////////////////////////////
// @@ VisualElement implementation
//////////////////////////////////////////////////
public void setDrawing(ProcessDrawing drawing)
{
this.drawing = drawing;
}
public ProcessDrawing getDrawing()
{
return drawing;
}
public VisualElement getParentElement()
{
return getDrawing();
}
public Figure getPresentationFigure()
{
return this;
}
public void updatePresentationFigure()
{
// No dynamic presentation figure, so do nothing
}
public boolean isVisible()
{
return (visualStatus & VisualElement.VISUAL_VISIBLE) != 0;
}
public void setVisible(boolean visible)
{
willChange();
if (visible)
{
visualStatus |= VisualElement.VISUAL_VISIBLE;
}
else
{
visualStatus &= ~VisualElement.VISUAL_VISIBLE;
}
changed();
}
public boolean handleEvent(VisualElementEvent event)
{
if (event.type == VisualElementEvent.DOUBLE_CLICK)
{
DrawingEditorPlugin editor = getDrawing().getEditor();
editor.startUndo("Remove Link Control Point");
if (!joinSegments(event.x, event.y))
{
splitSegment(event.x, event.y);
ModelerUndoable undoable = (ModelerUndoable) ((Modeler) editor).getCurrentUndoable();
undoable.setDisplayName("Add Link Control Point");
}
editor.view().toggleSelection(this);
editor.view().toggleSelection(this);
editor.endUndo();
layoutConnection();
return true;
}
return false;
}
//////////////////////////////////////////////////
// @@ UpdatableFigure implementation
//////////////////////////////////////////////////
public void updateFigure()
{
label.updateFigure();
}
/////////////////////////////////////////////////////////////////////////
// @@ InteractionClient
/////////////////////////////////////////////////////////////////////////
public void dragActionTriggered(Object regionId, Point p)
{
}
public void dragEnded(Transferable transferable)
{
}
public void dragStarted(Transferable transferable)
{
}
public List getAllDropRegions(List flavors, Transferable data, MouseEvent mouseEvent)
{
return getDropRegions(flavors, data, mouseEvent);
}
public List getDropRegions(List flavors, Transferable data, MouseEvent mouseEvent)
{
return null;
}
public List getImportersAt(Point p)
{
return null;
}
public List getAllImportersAt(Point p)
{
return getImportersAt(p);
}
public List getSubClients()
{
return null;
}
public boolean importData(Object regionId, Transferable data, Point p)
{
return false;
}
//////////////////////////////////////////////////
// @@ Storable implementation
//////////////////////////////////////////////////
public void write(StorableOutput dw)
{
super.write(dw);
dw.writeStorable(startConnector);
dw.writeStorable(endConnector);
}
public void read(StorableInput dr)
throws IOException
{
super.read(dr);
Connector start = (Connector) dr.readStorable();
if (start != null)
{
connectStart(start);
}
Connector end = (Connector) dr.readStorable();
if (end != null)
{
connectEnd(end);
}
if (start != null && end != null)
{
layoutConnection();
}
}
private void readObject(ObjectInputStream s)
throws ClassNotFoundException, IOException
{
s.defaultReadObject();
if (startConnector != null)
{
connectStart(startConnector);
}
if (endConnector != null)
{
connectEnd(endConnector);
}
}
//////////////////////////////////////////////////
// @@ Spline locator class
//////////////////////////////////////////////////
/**
* Locator that returns an arbitary position on the spline. This position defaults
* to center.
*/
class SplineLocator extends AbstractLocator
{
/** The postion ranging from 0d (start) to 1d (end) */
private double position;
/**
* Create a new SplineLocator that defaults to 0\.5d.
*/
public SplineLocator()
{
super();
this.position = 0.5d;
}
/**
* Returns position on the Spline.
*
* @param figure Is ignored
*/
public Point locate(Figure figure)
{
Point2D p = getPointOnCurve(position);
return new Point(CommonUtil.rnd(p.getX()), CommonUtil.rnd(p.getY()));
}
/**
* Returns the position.
* @return double
*/
public double getPosition()
{
return position;
}
/**
* Sets the position.
* @param position The position to set
*/
public void setPosition(double position)
{
this.position = position;
}
}
//////////////////////////////////////////////////
// @@ Handle class
//////////////////////////////////////////////////
/**
* Handle that is used to move the position of the TextLable,
* relative to its anchorpoint on the spline.
*/
class MoveLableHandle extends AbstractHandle
{
int lastX;
int lastY;
/**
* Constructor.
*/
public MoveLableHandle()
{
super(PolySplineConnection.this);
}
public void invokeStart(int x, int y, Drawing drawing)
{
lastX = x;
lastY = y;
}
public void invokeStep(int x, int y, int ax, int ay, DrawingView view)
{
willChange();
label.moveBy(x - lastX, y - lastY);
lastX = x;
lastY = y;
changed();
}
public Point locate()
{
return label.displayBox().getLocation();
}
public void draw(Graphics g)
{
super.draw(g);
Graphics2D g2 = (Graphics2D) g;
Point p1 = textLocator.locate(PolySplineConnection.this);
Point p2 = locate();
Stroke old = g2.getStroke();
g2.setStroke(ModelerGraphics.labelHandleStroke);
Color olc = g.getColor();
g.setColor(ModelerColors.LABEL_HANDLE_LINE);
g.drawLine(p1.x, p1.y, p2.x, p2.y);
g2.setStroke(old);
g.setColor(olc);
}
}
}