org.netbeans.api.visual.widget.ConnectionWidget Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.api.visual.widget;
import org.netbeans.api.visual.anchor.Anchor;
import org.netbeans.api.visual.anchor.AnchorShape;
import org.netbeans.api.visual.anchor.PointShape;
import org.netbeans.api.visual.layout.LayoutFactory;
import org.netbeans.api.visual.model.ObjectState;
import org.netbeans.api.visual.router.Router;
import org.netbeans.api.visual.router.RouterFactory;
import org.netbeans.modules.visual.layout.ConnectionWidgetLayout;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* This class represents a connection between two location. The locations are resolved by Anchors.
* The path of the connection is specified by control points which are resolved by Routers (DirectRouter is used by default).
*
* The connection is rendered using the foreground color and a specified stroke. It also renders control points,
* end points (first and last control points) and anchors. The shape of points are defined by assigned AnchorShape and PointShape.
*
* For speed optimalization, the connection widget are usually placed in the a separate layer widget that is rendered after
* the main layer with widgets (that used anchors depends on) is rendered.
*
* Line color is defined by foregroundColor property. Note: If you are changing a state of the ConnectionWidget
* (e.g. using it as a representation of an object in ObjectScene, GraphScene or GraphPinScene classes,
* then the ConnectionWidget.notifyStateChanged method is automatically called.
* The built-in implementation of this method overrides foregroundColor based on a new state of the widget
* (the particular color is resolved by the LookFeel of the scene).
*
* @author David Kaspar
*/
// TODO - control points can be modified by accessing: getControlPoints ().get (0).x or y
public class ConnectionWidget extends Widget {
private static final double HIT_DISTANCE_SQUARE = 16.0;
private static final Stroke STROKE_DEFAULT = new BasicStroke (1.0f);
/**
* This enum represents a policy which is used for re-routing control points of a ConnectionWidget.
* @since 2.9
*/
public enum RoutingPolicy {
/**
* All control points are rerouted when it is necessary. This is the default policy.
* @since 2.9
*/
ALWAYS_ROUTE,
/**
* All control points (except end points) are kept at the same location.
* End points are updated to locations resolved by source and target anchors.
* Note: This is used when an user customizes/adds/removes control points
* and the change has to be kept until it is reset by the user (setting policy to ALWAYS_ROUTE.
* @since 2.9
*/
UPDATE_END_POINTS_ONLY,
/**
* Temporarily disables routing until any end point changes its location. Locations are the first
* and the last point of control points. When an end point location is changed,
* then the policy is automatically changed to ALWAYS_ROUTE
.
* Note: This is used by GraphLayouts which routes the path (control points)
* by themselves and would like to keep the path until user moves with source or target widget/anchor.
* @since 2.9
*/
DISABLE_ROUTING_UNTIL_END_POINT_IS_MOVED,
/**
* Disable routing completely, so control points are kept at their previous location.
* Note: This is not often used unless you have to freeze a ConnectionWidget
* @since 2.9
*/
DISABLE_ROUTING
}
private Anchor sourceAnchor;
private Anchor targetAnchor;
private AnchorShape sourceAnchorShape;
private AnchorShape targetAnchorShape;
private PointShape controlPointShape;
private PointShape endPointShape;
private Router router;
private boolean routingRequired;
private List controlPoints = Collections.emptyList ();
private List controlPointsUm = Collections.unmodifiableList (controlPoints);
private ConnectionWidgetLayout connectionWidgetLayout;
private Stroke stroke;
private boolean paintControlPoints;
private Color lineColor;
private Cursor controlPointsCursor;
private int controlPointCutDistance;
private Anchor.Entry sourceEntry;
private Anchor.Entry targetEntry;
private RoutingPolicy routingPolicy;
/**
* Creates a connection widget.
* @param scene the scene
*/
public ConnectionWidget (Scene scene) {
super (scene);
sourceAnchorShape = AnchorShape.NONE;
targetAnchorShape = AnchorShape.NONE;
controlPointShape = PointShape.NONE;
endPointShape = PointShape.NONE;
router = RouterFactory.createDirectRouter ();
routingRequired = true;
connectionWidgetLayout = new ConnectionWidgetLayout ();
setLayout (connectionWidgetLayout);
stroke = STROKE_DEFAULT;
paintControlPoints = false;
controlPointCutDistance = 0;
sourceEntry = new ConnectionEntry (true);
targetEntry = new ConnectionEntry (false);
routingPolicy = RoutingPolicy.ALWAYS_ROUTE;
}
/**
* Implements the widget-state specific look of the widget.
* @param previousState the previous state
* @param state the new state
*/
public void notifyStateChanged (ObjectState previousState, ObjectState state) {
setForeground (lineColor != null ? lineColor : getScene ().getLookFeel ().getLineColor (state));
setPaintControlPoints (state.isSelected ());
}
/**
* Returns a stroke of the connection widget.
* @return the stroke
*/
public final Stroke getStroke () {
return stroke;
}
/**
* Sets a stroke.
* @param stroke the stroke
*/
public final void setStroke (Stroke stroke) {
assert stroke != null;
this.stroke = stroke;
repaint (); // TODO - check when to revalidate and when to repaint only
}
/**
* Returns line color of the widget.
* @return the line color; null if no line color is specified
*/
public final Color getLineColor () {
return lineColor;
}
/**
* Sets a line color of the widget.
* @param lineColor the line color; if null, then the line color will be resolved from LookFeel of the scene.
*/
public final void setLineColor (Color lineColor) {
this.lineColor = lineColor;
ObjectState state = getState ();
notifyStateChanged (state, state);
}
/**
* Returns whether the control (and end) points are painted
* @return true, if the control points (and end points) are painted
*/
public final boolean isPaintControlPoints () {
return paintControlPoints;
}
/**
* Sets whether the control (and end) points are painted
* @param paintControlPoints if true, then control points are painted
*/
public final void setPaintControlPoints (boolean paintControlPoints) {
this.paintControlPoints = paintControlPoints;
repaint ();
}
/**
* Returns the cursor for control point.
* @return the cursor
* @since 2.3
*/
public final Cursor getControlPointsCursor () {
return controlPointsCursor;
}
/**
* Sets a control points cursor. The cursor is used only when mouse is over a visible control point
* @param controlPointsCursor the control points cursor
* @since 2.3
*/
public final void setControlPointsCursor (Cursor controlPointsCursor) {
this.controlPointsCursor = controlPointsCursor;
}
/**
* Returns the cut distance at control points.
* @return the cut distance
* @since 2.5
*/
public int getControlPointCutDistance () {
return controlPointCutDistance;
}
/**
* Sets the cut distance at control points.
* @param controlPointCutDistance if positive number, then the path is cut to render smooth corners;
* otherwise the path is rendered using control points only
* @since 2.5
*/
public void setControlPointCutDistance (int controlPointCutDistance) {
this.controlPointCutDistance = controlPointCutDistance;
repaint ();
}
/**
* Returns a source anchor of the connection widget.
* @return the source anchor
*/
public final Anchor getSourceAnchor () {
return sourceAnchor;
}
/**
* Sets a source anchor of the connection widget.
* @param sourceAnchor the source anchor
*/
public final void setSourceAnchor (Anchor sourceAnchor) {
if (this.sourceAnchor != null)
this.sourceAnchor.removeEntry (sourceEntry);
this.sourceAnchor = sourceAnchor;
if (this.sourceAnchor != null)
sourceAnchor.addEntry (sourceEntry);
reroute ();
}
/**
* Returns a target anchor of the connection widget.
* @return the target anchor
*/
public final Anchor getTargetAnchor () {
return targetAnchor;
}
/**
* Sets a target anchor of the connection widget.
* @param targetAnchor the target anchor
*/
public final void setTargetAnchor (Anchor targetAnchor) {
if (this.targetAnchor != null)
this.targetAnchor.removeEntry (targetEntry);
this.targetAnchor = targetAnchor;
if (targetAnchor != null)
targetAnchor.addEntry (targetEntry);
reroute ();
}
/**
* Returns an anchor entry representing the source of the connection widget.
* @return the anchor entry representing the source of the connection widget
*/
public Anchor.Entry getSourceAnchorEntry () {
return sourceEntry;
}
/**
* Returns an anchor entry representing the target of the connection widget.
* @return the anchor entry representing the target of the connection widget
*/
public Anchor.Entry getTargetAnchorEntry () {
return targetEntry;
}
/**
* Returns an anchor shape of the source of the connection widget.
* @return the source anchor shape
*/
public AnchorShape getSourceAnchorShape () {
return sourceAnchorShape;
}
/**
* Sets the anchor shape of the source of the connection widget.
* @param sourceAnchorShape the source anchor shape
*/
public void setSourceAnchorShape (AnchorShape sourceAnchorShape) {
assert sourceAnchorShape != null;
boolean repaintOnly = this.sourceAnchorShape.getRadius () == sourceAnchorShape.getRadius ();
this.sourceAnchorShape = sourceAnchorShape;
revalidate (repaintOnly);
}
/**
* Returns an anchor shape of the target of the connection widget.
* @return the target anchor shape
*/
public AnchorShape getTargetAnchorShape () {
return targetAnchorShape;
}
/**
* Sets the anchor shape of the target of the connection widget.
* @param targetAnchorShape the target anchor shape
*/
public void setTargetAnchorShape (AnchorShape targetAnchorShape) {
assert targetAnchorShape != null;
boolean repaintOnly = this.targetAnchorShape.getRadius () == targetAnchorShape.getRadius ();
this.targetAnchorShape = targetAnchorShape;
revalidate (repaintOnly);
}
/**
* Returns a point shape of control points of the connection widget.
* @return the control points shape
*/
public PointShape getControlPointShape () {
return controlPointShape;
}
/**
* Sets a point shape of control points of the connection widget.
* @param controlPointShape the control points shape
*/
public void setControlPointShape (PointShape controlPointShape) {
assert controlPointShape != null;
boolean repaintOnly = this.controlPointShape.getRadius () == controlPointShape.getRadius ();
this.controlPointShape = controlPointShape;
revalidate (repaintOnly);
}
/**
* Returns a point shape of end points of the connection widget.
* @return the end points shape
*/
public PointShape getEndPointShape () {
return endPointShape;
}
/**
* Sets a point shape of end points of the connection widget.
* @param endPointShape the end points shape
*/
public void setEndPointShape (PointShape endPointShape) {
assert endPointShape != null;
boolean repaintOnly = this.endPointShape.getRadius () == endPointShape.getRadius ();
this.endPointShape = endPointShape;
revalidate (repaintOnly);
}
/**
* Returns a routing policy.
* @return the routing policy
* @since 2.9
*/
public final RoutingPolicy getRoutingPolicy () {
return routingPolicy;
}
/**
* Sets a routing policy. It invokes re-routing in case of routing policy change unless its is changed to DISABLE_ROUTING.
* @param routingPolicy the new routing policy
* @since 2.9
*/
public final void setRoutingPolicy (RoutingPolicy routingPolicy) {
assert routingPolicy != null;
if (this.routingPolicy == routingPolicy)
return;
boolean changed = routingPolicy != RoutingPolicy.DISABLE_ROUTING;
this.routingPolicy = routingPolicy;
if (changed)
reroute ();
}
/**
* Returns the control-points-based path router of the connection widget.
* @return the path router
*/
public final Router getRouter () {
return router;
}
/**
* Sets a control-points-based path router of the connection widget.
* @param router the path router
*/
public final void setRouter (Router router) {
assert router != null;
this.router = router;
reroute ();
}
/**
* Returns a list of control points.
* @return the list of control points
*/
public List getControlPoints () {
return controlPointsUm;
}
/**
* Returns a location of control point at the specified index in the list of control points.
*
* @param index index of the control point to return
* @return the point; null if the control point does not exist
*/
public Point getControlPoint (int index) {
if (index < 0 || index >= controlPoints.size ())
return null;
return new Point (controlPoints.get(index));
}
/**
* Sets control points.
* @param controlPoints the list of control points
* @param sceneLocations if true, then controlPoints argyment is taken as a list of scene locations;
* if false, then controlPoints argument is taken as a list of local locations
*/
public void setControlPoints (Collection controlPoints, boolean sceneLocations) {
//quick return if possible
if (controlPoints == null) return ;
if (sceneLocations) {
Point translation = this.convertLocalToScene (new Point ());
ArrayList list = new ArrayList ();
for (Point point : controlPoints)
list.add (new Point (point.x - translation.x, point.y - translation.y));
this.controlPoints = list;
} else
this.controlPoints = new ArrayList (controlPoints);
this.controlPointsUm = Collections.unmodifiableList (this.controlPoints);
routingRequired = false;
revalidate ();
}
/**
* Sets a constraint for a child widget when ConnectionWidgetLayout (by default) is used.
* @param childWidget the child widget for which the constraint is set
* @param alignment the alignment specified relatively to the origin point
* @param placementInPercentage the placement on a path in percentage of the path length
*/
public void setConstraint (Widget childWidget, LayoutFactory.ConnectionWidgetLayoutAlignment alignment, float placementInPercentage) {
connectionWidgetLayout.setConstraint (childWidget, alignment, placementInPercentage);
}
/**
* Sets a constraint for a child widget when ConnectionWidgetLayout (by default) is used.
* @param childWidget the child widget for which the constraint is set
* @param alignment the alignment specified relatively to the origin point
* @param placementAtDistance the placement on a path in pixels as a distance from the source anchor
*/
public void setConstraint (Widget childWidget, LayoutFactory.ConnectionWidgetLayoutAlignment alignment, int placementAtDistance) {
connectionWidgetLayout.setConstraint (childWidget, alignment, placementAtDistance);
}
/**
* Removes a constraint for a child widget.
* @param childWidget the child widget
*/
public void removeConstraint (Widget childWidget) {
connectionWidgetLayout.removeConstraint (childWidget);
}
/**
* Forces path routing.
*/
public final void calculateRouting () {
if (routingRequired) {
switch (routingPolicy) {
case ALWAYS_ROUTE:
setControlPoints (router.routeConnection (this), true);
break;
case UPDATE_END_POINTS_ONLY: {
Point sourcePoint = sourceAnchor != null ? sourceAnchor.compute (sourceEntry).getAnchorSceneLocation () : null;
Point targetPoint = targetAnchor != null ? targetAnchor.compute (targetEntry).getAnchorSceneLocation () : null;
if (sourcePoint == null || targetPoint == null) {
controlPoints.clear ();
break;
}
sourcePoint = convertSceneToLocal (sourcePoint);
targetPoint = convertSceneToLocal (targetPoint);
if (controlPoints.size () < 1)
controlPoints.add (sourcePoint);
else
controlPoints.set (0, sourcePoint);
if (controlPoints.size () < 2)
controlPoints.add (targetPoint);
else
controlPoints.set (controlPoints.size () - 1, targetPoint);
} break;
case DISABLE_ROUTING_UNTIL_END_POINT_IS_MOVED: {
Point sourcePoint = sourceAnchor != null ? sourceAnchor.compute (sourceEntry).getAnchorSceneLocation () : null;
Point firstPoint = getFirstControlPoint ();
if (firstPoint != null)
firstPoint = convertLocalToScene (firstPoint);
if (sourcePoint == null ? firstPoint == null : sourcePoint.equals (firstPoint)) {
Point targetPoint = targetAnchor != null ? targetAnchor.compute (targetEntry).getAnchorSceneLocation () : null;
Point lastPoint = getLastControlPoint ();
if (lastPoint != null)
lastPoint = convertLocalToScene (lastPoint);
if (targetPoint == null ? lastPoint == null : targetPoint.equals (lastPoint))
break;
}
routingPolicy = RoutingPolicy.ALWAYS_ROUTE;
setControlPoints (router.routeConnection (this), true);
} break;
case DISABLE_ROUTING:
break;
default:
throw new IllegalStateException ("Unexpected routing policy: " + routingPolicy); // NOI18N
}
}
}
/**
* Calculates a client area of the connection widget.
* @return the calculated client area
*/
protected Rectangle calculateClientArea () {
calculateRouting ();
int controlPointShapeRadius = controlPointShape.getRadius ();
int controlPointShapeRadius2 = controlPointShapeRadius + controlPointShapeRadius;
int endPointShapeRadius = endPointShape.getRadius ();
Rectangle rect = null;
for (Point point : controlPoints) {
Rectangle addRect = new Rectangle (point.x - controlPointShapeRadius, point.y - controlPointShapeRadius, controlPointShapeRadius2, controlPointShapeRadius2);
if (rect == null)
rect = addRect;
else
rect.add (addRect);
}
Point firstPoint = getFirstControlPoint ();
if (firstPoint != null) {
int radius = Math.max (sourceAnchorShape.getRadius (), endPointShapeRadius);
int radius2 = radius + radius;
if (rect == null)
rect = new Rectangle (firstPoint.x - radius, firstPoint.y - radius, radius2, radius2);
else
rect.add (new Rectangle (firstPoint.x - radius, firstPoint.y - radius, radius2, radius2));
}
Point lastPoint = getLastControlPoint ();
if (lastPoint != null) {
int radius = Math.max (targetAnchorShape.getRadius (), endPointShapeRadius);
int radius2 = radius + radius;
if (rect == null)
rect = new Rectangle (lastPoint.x - radius, lastPoint.y - radius, radius2, radius2);
else
rect.add (new Rectangle (lastPoint.x - radius, lastPoint.y - radius, radius2, radius2));
}
if (rect != null)
rect.grow (2, 2); // TODO - improve line width calculation
return rect != null ? rect : new Rectangle ();
}
/**
* Returns whether the connection widget is validated and routed.
* @return true, if the connection widget is validated and routed
*/
public boolean isValidated () {
return super.isValidated () && isRouted ();
}
/**
* Returns whether the connection widget is routed.
* @return true if the connection widget is routed
*/
public final boolean isRouted () {
return ! routingRequired;
}
/**
* Schedules the connection widget for re-routing its path.
*/
public final void reroute () {
routingRequired = true;
revalidate ();
}
/**
* Returns the first control point.
* @return the first control point; null, if list of control points is empty
*/
public final Point getFirstControlPoint () {
if (controlPoints.size () <= 0)
return null;
return new Point (controlPoints.get (0));
}
/**
* Returns the last control point.
* @return the last control point; null, if list of control points is empty
*/
public final Point getLastControlPoint () {
int size = controlPoints.size ();
if (size <= 0)
return null;
return new Point (controlPoints.get (size - 1));
}
/**
* Returns the rotation of the source anchor shape.
* @return the source anchor shape rotation
*/
private double getSourceAnchorShapeRotation () {
if (controlPoints.size () <= 1)
return 0.0;
Point point1 = controlPoints.get (0);
Point point2 = controlPoints.get (1);
return Math.atan2 (point2.y - point1.y, point2.x - point1.x);
}
/**
* Returns the rotation of the target anchor shape.
* @return the target anchor shape rotation
*/
public double getTargetAnchorShapeRotation () {
int size = controlPoints.size ();
if (size <= 1)
return 0.0;
Point point1 = controlPoints.get (size - 1);
Point point2 = controlPoints.get (size - 2);
return Math.atan2 (point2.y - point1.y, point2.x - point1.x);
}
/**
* Returns whether a specified local location is a part of the connection widget. It checks whether the location is
* close to the control-points-based path (up to 4px from the line),
* close to the anchors (defined by AnchorShape) or
* close to the control points (PointShape).
* @param localLocation the local locaytion
* @return true, if the location is a part of the connection widget
*/
public boolean isHitAt (Point localLocation) {
if (! super.isHitAt (localLocation))
return false;
List controlPoints = getControlPoints ();
for (int i = 0; i < controlPoints.size () - 1; i++) {
Point point1 = controlPoints.get (i);
Point point2 = controlPoints.get (i + 1);
double dist = Line2D.ptSegDistSq (point1.x, point1.y, point2.x, point2.y, localLocation.x, localLocation.y);
if (dist < HIT_DISTANCE_SQUARE)
return true;
}
return getControlPointHitAt (localLocation) >= 0;
}
/**
* Returns whether the local location hits the first control point (also meant to be the source anchor).
* @param localLocation the local location
* @return true if it hits the first control point
*/
public final boolean isFirstControlPointHitAt (Point localLocation) {
int endRadius = endPointShape.getRadius ();
endRadius *= endRadius;
Point firstPoint = getFirstControlPoint ();
if (firstPoint != null)
if (Point2D.distanceSq (firstPoint.x, firstPoint.y, localLocation.x, localLocation.y) <= endRadius)
return true;
return false;
}
/**
* Returns whether the local location hits the last control point (also meant to be the target anchor).
* @param localLocation the local location
* @return true if it hits the last control point
*/
public final boolean isLastControlPointHitAt (Point localLocation) {
int endRadius = endPointShape.getRadius ();
endRadius *= endRadius;
Point lastPoint = getLastControlPoint ();
if (lastPoint != null)
if (Point2D.distanceSq (lastPoint.x, lastPoint.y, localLocation.x, localLocation.y) <= endRadius)
return true;
return false;
}
/**
* Returns an index of a control point that is hit by the local location
* @param localLocation the local location
* @return the index; -1 if no control point was hit
*/
public final int getControlPointHitAt (Point localLocation) {
int controlRadius = controlPointShape.getRadius ();
controlRadius *= controlRadius;
if (isFirstControlPointHitAt (localLocation))
return 0;
if (isLastControlPointHitAt (localLocation))
return controlPoints.size () - 1;
for (int i = 0; i < controlPoints.size (); i ++) {
Point point = controlPoints.get (i);
if (Point2D.distanceSq (point.x, point.y, localLocation.x, localLocation.y) <= controlRadius)
return i;
}
return -1;
}
/**
* Returns a cursor for a specified local location in the widget.
* If paintControlPoints is true and controlPointsCursor is non-null and local location is over a control point, then it return controlPointsCursor.
* Otherwise it return value from super.getCursorAt method.
* @param localLocation the local location
* @return the cursor
* @since 2.3
*/
protected Cursor getCursorAt (Point localLocation) {
if (paintControlPoints) {
Cursor pointsCursor = getControlPointsCursor ();
if (pointsCursor != null && getControlPointHitAt (localLocation) >= 0)
return pointsCursor;
}
return super.getCursorAt (localLocation);
}
/**
* Paints the connection widget (the path, the anchor shapes, the control points, the end points).
*/
protected void paintWidget () {
Graphics2D gr = getGraphics ();
gr.setColor (getForeground ());
GeneralPath path = null;
Point firstControlPoint = getFirstControlPoint ();
Point lastControlPoint = getLastControlPoint ();
//checking to see if we should draw line through the AnchorShape. If the
//AnchorShape is hollow, the cutdistance will be true.
boolean isSourceCutDistance = sourceAnchorShape.getCutDistance () != 0.0;
boolean isTargetCutDistance = targetAnchorShape.getCutDistance () != 0.0;
double firstControlPointRotation =
firstControlPoint != null && (sourceAnchorShape.isLineOriented ()
|| isSourceCutDistance) ?
getSourceAnchorShapeRotation () : 0.0;
double lastControlPointRotation =
lastControlPoint != null
&& (targetAnchorShape.isLineOriented () || isTargetCutDistance) ?
getTargetAnchorShapeRotation () : 0.0;
List points;
if ((isSourceCutDistance || isTargetCutDistance) && controlPoints.size () >= 2) {
points = new ArrayList (controlPoints);
points.set (0, new Point (
firstControlPoint.x + (int) (sourceAnchorShape.getCutDistance () * Math.cos (firstControlPointRotation)),
firstControlPoint.y + (int) (sourceAnchorShape.getCutDistance () * Math.sin (firstControlPointRotation))
));
points.set (controlPoints.size () - 1, new Point (
lastControlPoint.x + (int) (targetAnchorShape.getCutDistance () * Math.cos (lastControlPointRotation)),
lastControlPoint.y + (int) (targetAnchorShape.getCutDistance () * Math.sin (lastControlPointRotation))
));
} else {
points = controlPoints;
}
if (controlPointCutDistance > 0) {
for (int a = 0; a < points.size () - 1; a ++) {
Point p1 = points.get (a);
Point p2 = points.get (a + 1);
double len = p1.distance (p2);
if (a > 0) {
Point p0 = points.get (a - 1);
double ll = p0.distance (p1);
if (len < ll)
ll = len;
ll /= 2;
double cll = controlPointCutDistance;
if (cll > ll)
cll = ll;
double direction = Math.atan2 (p2.y - p1.y, p2.x - p1.x);
if (!Double.isNaN (direction)) {
path = addToPath (path,
p1.x + (int) (cll * Math.cos (direction)),
p1.y + (int) (cll * Math.sin (direction))
);
}
} else {
path = addToPath (path, p1.x, p1.y);
}
if (a < points.size () - 2) {
Point p3 = points.get (a + 2);
double ll = p2.distance (p3);
if (len < ll)
ll = len;
ll /= 2;
double cll = controlPointCutDistance;
if (cll > ll)
cll = ll;
double direction = Math.atan2 (p2.y - p1.y, p2.x - p1.x);
if (!Double.isNaN (direction)) {
path = addToPath (path,
p2.x - (int) (cll * Math.cos (direction)),
p2.y - (int) (cll * Math.sin (direction))
);
}
} else {
path = addToPath (path, p2.x, p2.y);
}
}
} else {
for (Point point : points)
path = addToPath (path, point.x, point.y);
}
if (path != null) {
Stroke previousStroke = gr.getStroke ();
gr.setPaint (getForeground ());
gr.setStroke (getStroke ());
gr.draw (path);
gr.setStroke (previousStroke);
}
AffineTransform previousTransform;
if (firstControlPoint != null) {
previousTransform = gr.getTransform ();
gr.translate (firstControlPoint.x, firstControlPoint.y);
if (sourceAnchorShape.isLineOriented ())
gr.rotate (firstControlPointRotation);
sourceAnchorShape.paint (gr, true);
gr.setTransform (previousTransform);
}
if (lastControlPoint != null) {
previousTransform = gr.getTransform ();
gr.translate (lastControlPoint.x, lastControlPoint.y);
if (targetAnchorShape.isLineOriented ())
gr.rotate (lastControlPointRotation);
targetAnchorShape.paint (gr, false);
gr.setTransform (previousTransform);
}
if (paintControlPoints) {
int last = controlPoints.size () - 1;
for (int index = 0; index <= last; index ++) {
Point point = controlPoints.get (index);
previousTransform = gr.getTransform ();
gr.translate (point.x, point.y);
if (index == 0 || index == last)
endPointShape.paint (gr);
else
controlPointShape.paint (gr);
gr.setTransform (previousTransform);
}
}
}
private GeneralPath addToPath (GeneralPath path, int x, int y) {
if (path == null) {
path = new GeneralPath ();
path.moveTo (x, y);
} else {
path.lineTo (x, y);
}
return path;
}
private class ConnectionEntry implements Anchor.Entry {
private boolean source;
public ConnectionEntry (boolean source) {
this.source = source;
}
public void revalidateEntry () {
ConnectionWidget.this.reroute ();
}
public ConnectionWidget getAttachedConnectionWidget () {
return ConnectionWidget.this;
}
public boolean isAttachedToConnectionSource () {
return source;
}
public Anchor getAttachedAnchor () {
return source ? ConnectionWidget.this.getSourceAnchor () : ConnectionWidget.this.getTargetAnchor ();
}
public Anchor getOppositeAnchor () {
return source ? ConnectionWidget.this.getTargetAnchor () : ConnectionWidget.this.getSourceAnchor ();
}
}
}