//******************************************************************************
//
// File: Line.java
// Package: edu.rit.draw.item
// Unit: Class edu.rit.draw.item.Line
//
// This Java source file is copyright (C) 2008 by Alan Kaminsky. All rights
// reserved. For further information, contact the author, Alan Kaminsky, at
// ark@cs.rit.edu.
//
// This Java source file is part of the Parallel Java Library ("PJ"). PJ is free
// software; you can redistribute it and/or modify it under the terms of the GNU
// General Public License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// PJ is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// A copy of the GNU General Public License is provided in the file gpl.txt. You
// may also obtain a copy of the GNU General Public License on the World Wide
// Web at http://www.gnu.org/licenses/gpl.html.
//
//******************************************************************************
package edu.rit.draw.item;
import edu.rit.draw.Drawing;
import java.awt.BasicStroke;
import java.awt.Graphics2D;
import java.awt.geom.GeneralPath;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.util.Iterator;
import java.util.LinkedList;
/**
* Class Line provides a {@linkplain DrawingItem} that consists of one or more
* straight line segments. The line may have an {@linkplain Arrow} at either or
* both ends. The line may have sharp corners or round corners. The line has an
* outline but no filled interior.
*
*
*
*
* If the line has round corners, the round corner distance gives the distance
* from where the (sharp) corner would normally be to the starting and ending
* points of the round corner arc, as shown in the adjoining diagram. (The round
* corner distance is d.) A round corner distance of 0 results in a sharp
* corner. In a particular line, the same round corner distance is used for
* every corner; that is, either all the corners are sharp or all the corners
* are rounded the same amount. Different lines can have different round corner
* distances.
*
*
* 
*
*
*
* The static defaultStartArrow(), defaultEndArrow(), and
* defaultRound() methods are provided to set the default starting
* arrow, ending arrow, and round corner distance. If the starting arrow, ending
* arrow, or round corner distance is not specified, the current default
* starting arrow, ending arrow, or round corner distance is used.
*
* @author Alan Kaminsky
* @version 23-Jun-2008
*/
public class Line
extends OutlinedItem
implements Externalizable
{
// Exported constants.
/**
* The normal starting arrow: None.
*/
public static final Arrow NORMAL_START_ARROW = Arrow.NONE;
/**
* The normal ending arrow: None.
*/
public static final Arrow NORMAL_END_ARROW = Arrow.NONE;
/**
* The normal round corner distance (0), signifying sharp corners.
*/
public static final double NORMAL_ROUND = 0.0;
// Hidden data members.
private static final long serialVersionUID = -4094248146984537516L;
// Line segment endpoints.
LinkedList myPoints = new LinkedList();
// Attributes.
Arrow myStartArrow = theDefaultStartArrow;
Arrow myEndArrow = theDefaultEndArrow;
double myRound = theDefaultRound;
// The default attributes.
static Arrow theDefaultStartArrow = NORMAL_START_ARROW;
static Arrow theDefaultEndArrow = NORMAL_END_ARROW;
static double theDefaultRound = NORMAL_ROUND;
// For adding line segments.
Point myLastPoint;
// For computing the bounding box.
Point myNw;
Point mySe;
Size mySize;
// Exported constructors.
/**
* Construct a new line. The line has no points initially. The default
* outline, starting arrow, ending arrow, and round corner distance are
* used.
*/
public Line()
{
super();
}
/**
* Construct a new line with the same points, outline, starting arrow,
* ending arrow, and round corner distance as the given line.
*
* @param theLine Line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theLine is null.
*/
public Line
(Line theLine)
{
super (theLine);
myPoints.clear();
myPoints.addAll (theLine.myPoints);
myStartArrow = theLine.myStartArrow;
myEndArrow = theLine.myEndArrow;
myRound = theLine.myRound;
if (! myPoints.isEmpty())
{
myLastPoint = myPoints.get (myPoints.size()-1);
}
}
// Exported operations.
/**
* Returns the default starting arrow.
*
* @return Default starting arrow.
*/
public static Arrow defaultStartArrow()
{
return theDefaultStartArrow;
}
/**
* Set the default starting arrow. Before calling this method the first
* time, the default starting arrow is Arrow.NONE.
*
* @param theArrow Default starting arrow.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theArrow is null.
*/
public static void defaultStartArrow
(Arrow theArrow)
{
if (theArrow == null) throw new NullPointerException();
theDefaultStartArrow = theArrow;
}
/**
* Returns the default ending arrow.
*
* @return Default ending arrow.
*/
public static Arrow defaultEndArrow()
{
return theDefaultEndArrow;
}
/**
* Set the default ending arrow. Before calling this method the first
* time, the default ending arrow is Arrow.NONE.
*
* @param theArrow Default ending arrow.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theArrow is null.
*/
public static void defaultEndArrow
(Arrow theArrow)
{
if (theArrow == null) throw new NullPointerException();
theDefaultEndArrow = theArrow;
}
/**
* Returns the default round corner distance. A value of 0 signifies sharp
* corners.
*
* @return Default round corner distance.
*/
public static double defaultRound()
{
return theDefaultRound;
}
/**
* Set the default round corner distance. A value of 0 signifies sharp
* corners. Before calling this method the first time, the default round
* corner distance is 0.
*
* @param theRound Default round corner distance.
*
* @exception IllegalArgumentException
* (unchecked exception) Thrown if theRound is less than 0.
*/
public static void defaultRound
(double theRound)
{
if (theRound < 0.0) throw new IllegalArgumentException();
theDefaultRound = theRound;
}
/**
* Returns the size of this drawing item's bounding box.
*
* @return Size.
*/
public Size size()
{
computeBoundingBox();
return mySize;
}
/**
* Returns the width of this drawing item's bounding box.
*
* @return Width.
*/
public double width()
{
computeBoundingBox();
return mySize.width;
}
/**
* Returns the height of this drawing item's bounding box.
*
* @return Height.
*/
public double height()
{
computeBoundingBox();
return mySize.height;
}
/**
* Returns the northwest corner point of this drawing item's bounding box.
*
* @return Northwest corner point.
*/
public Point nw()
{
computeBoundingBox();
return myNw;
}
/**
* Set this line's outline.
*
* @param theOutline Outline, or Outline.NONE.
*
* @return This line.
*/
public Line outline
(Outline theOutline)
{
doOutline (theOutline);
return this;
}
/**
* Clear this line. All of this point's line segments are removed.
*
* @return This line.
*/
public Line clear()
{
myPoints.clear();
myLastPoint = null;
myNw = null;
return this;
}
/**
* Add a line segment to this line. The new endpoint is (x,
* y). If this line has no line segments, the to() method
* specifies this line's starting endpoint.
*
* @param x Endpoint's X coordinate.
* @param y Endpoint's Y coordinate.
*
* @return This line.
*/
public Line to
(double x,
double y)
{
return addEndpoint (new Point (x, y));
}
/**
* Add a line segment to this line. The new endpoint is thePoint.
* If this line has no line segments, the to() method specifies
* this line's starting endpoint.
*
* @param thePoint Endpoint.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if thePoint is null.
*/
public Line to
(Point thePoint)
{
if (thePoint == null) throw new NullPointerException();
return addEndpoint (thePoint);
}
/**
* Add a horizontal line segment to this line. The new endpoint is
* (x, previous endpoint's Y coordinate).
*
* @param x Endpoint's X coordinate.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no previous endpoint.
*/
public Line hto
(double x)
{
return addEndpoint (new Point (x, myLastPoint.y));
}
/**
* Add a horizontal line segment to this line. The new endpoint is
* (thePoint's X coordinate, previous endpoint's Y coordinate).
*
* @param thePoint Endpoint.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if thePoint is null. Thrown if
* there is no previous endpoint.
*/
public Line hto
(Point thePoint)
{
return addEndpoint (new Point (thePoint.x, myLastPoint.y));
}
/**
* Add a vertical line segment to this line. The new endpoint is (previous
* endpoint's X coordinate, y).
*
* @param y Endpoint's Y coordinate.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no previous endpoint.
*/
public Line vto
(double y)
{
return addEndpoint (new Point (myLastPoint.x, y));
}
/**
* Add a vertical line segment to this line. The new endpoint is
* (previous endpoint's X coordinate, thePoint's Y coordinate).
*
* @param thePoint Endpoint.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if thePoint is null. Thrown if
* there is no previous endpoint.
*/
public Line vto
(Point thePoint)
{
return addEndpoint (new Point (myLastPoint.x, thePoint.y));
}
/**
* Add a line segment to this line. The new endpoint is (previous endpoint's
* X coordinate + dx, previous endpoint's Y coordinate +
* dy).
*
* @param dx X distance.
* @param dy Y distance.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no previous endpoint.
*/
public Line by
(double dx,
double dy)
{
return addEndpoint (new Point (myLastPoint.x+dx, myLastPoint.y+dy));
}
/**
* Add a line segment to this line. The new endpoint is (previous endpoint's
* X coordinate + theSize.width(), previous endpoint's Y coordinate
* + theSize.height()).
*
* @param theSize Distance.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theSize is null. Thrown if
* there is no previous endpoint.
*/
public Line by
(Size theSize)
{
return addEndpoint
(new Point
(myLastPoint.x+theSize.width, myLastPoint.y+theSize.height));
}
/**
* Add a horizontal line segment to this line. The new endpoint is (previous
* endpoint's X coordinate + dx, previous endpoint's Y coordinate).
*
* @param dx X distance.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no previous endpoint.
*/
public Line hby
(double dx)
{
return addEndpoint (new Point (myLastPoint.x+dx, myLastPoint.y));
}
/**
* Add a horizontal line segment to this line. The new endpoint is (previous
* endpoint's X coordinate + theSize.width(), previous endpoint's Y
* coordinate).
*
* @param theSize Distance.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theSize is null. Thrown if
* there is no previous endpoint.
*/
public Line hby
(Size theSize)
{
return addEndpoint
(new Point (myLastPoint.x+theSize.width, myLastPoint.y));
}
/**
* Add a vertical line segment to this line. The new endpoint is (previous
* endpoint's X coordinate, previous endpoint's Y coordinate + dy).
*
* @param dy Y distance.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no previous endpoint.
*/
public Line vby
(double dy)
{
return addEndpoint (new Point (myLastPoint.x, myLastPoint.y+dy));
}
/**
* Add a vertical line segment to this line. The new endpoint is (previous
* endpoint's X coordinate, previous endpoint's Y coordinate +
* theSize.height()).
*
* @param theSize Distance.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theSize is null. Thrown if
* there is no previous endpoint.
*/
public Line vby
(Size theSize)
{
return addEndpoint
(new Point (myLastPoint.x, myLastPoint.y+theSize.height));
}
/**
* Returns this line's starting arrow.
*
* @return Starting arrow.
*/
public Arrow startArrow()
{
return myStartArrow;
}
/**
* Set this line's starting arrow.
*
* @param theArrow Starting arrow.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theArrow is null.
*/
public Line startArrow
(Arrow theArrow)
{
if (theArrow == null) throw new NullPointerException();
myStartArrow = theArrow;
return this;
}
/**
* Returns this line's ending arrow.
*
* @return Ending arrow.
*/
public Arrow endArrow()
{
return myEndArrow;
}
/**
* Set this line's ending arrow.
*
* @param theArrow Ending arrow.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theArrow is null.
*/
public Line endArrow
(Arrow theArrow)
{
if (theArrow == null) throw new NullPointerException();
myEndArrow = theArrow;
return this;
}
/**
* Returns this line's round corner distance. A value of 0 signifies sharp
* corners.
*
* @return Round corner distance.
*/
public double round()
{
return myRound;
}
/**
* Set this line's round corner distance. A value of 0 signifies sharp
* corners.
*
* @param theRound Round corner distance.
*
* @return This line.
*
* @exception IllegalArgumentException
* (unchecked exception) Thrown if theRound is less than 0.
*/
public Line round
(double theRound)
{
if (theRound < 0.0) throw new IllegalArgumentException();
myRound = theRound;
return this;
}
/**
* Add this line to the end of the default drawing's sequence of drawing
* items.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no default drawing.
*
* @see Drawing#defaultDrawing()
*/
public Line add()
{
doAdd (Drawing.defaultDrawing());
return this;
}
/**
* Add this line to the end of the given drawing's sequence of drawing
* items.
*
* @param theDrawing Drawing.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theDrawing is null.
*/
public Line add
(Drawing theDrawing)
{
doAdd (theDrawing);
return this;
}
/**
* Add this line to the beginning of the default drawing's sequence of
* drawing items.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if there is no default drawing.
*
* @see Drawing#defaultDrawing()
*/
public Line addFirst()
{
doAddFirst (Drawing.defaultDrawing());
return this;
}
/**
* Add this line to the beginning of the given drawing's sequence of drawing
* items.
*
* @param theDrawing Drawing.
*
* @return This line.
*
* @exception NullPointerException
* (unchecked exception) Thrown if theDrawing is null.
*/
public Line addFirst
(Drawing theDrawing)
{
doAddFirst (theDrawing);
return this;
}
/**
* Write this line to the given object output stream.
*
* @param out Object output stream.
*
* @exception IOException
* Thrown if an I/O error occurred.
*/
public void writeExternal
(ObjectOutput out)
throws IOException
{
super.writeExternal (out);
out.writeInt (myPoints.size());
for (Point point : myPoints)
{
out.writeObject (point);
}
out.writeObject (myStartArrow);
out.writeObject (myEndArrow);
out.writeDouble (myRound);
}
/**
* Read this line from the given object input stream.
*
* @param in Object input stream.
*
* @exception IOException
* Thrown if an I/O error occurred.
* @exception ClassNotFoundException
* Thrown if any class needed to deserialize this line cannot be found.
*/
public void readExternal
(ObjectInput in)
throws IOException, ClassNotFoundException
{
super.readExternal (in);
int n = in.readInt();
myPoints.clear();
for (int i = 0; i < n; ++ i)
{
myPoints.add ((Point) in.readObject());
}
myStartArrow = (Arrow) in.readObject();
myEndArrow = (Arrow) in.readObject();
myRound = in.readDouble();
if (! myPoints.isEmpty())
{
myLastPoint = myPoints.get (myPoints.size()-1);
}
myNw = null;
mySe = null;
mySize = null;
}
/**
* Draw this drawing item in the given graphics context. This method is
* allowed to change the graphics context's paint, stroke, and transform,
* and it doesn't have to change them back.
*
* @param g2d 2-D graphics context.
*/
public void draw
(Graphics2D g2d)
{
super.draw (g2d);
// Draw line.
GeneralPath path = computePath();
myOutline.setGraphicsContext (g2d);
g2d.draw (path);
// Return if there are no arrows.
int n = myPoints.size();
if (n < 2) return;
if (myStartArrow == null && myEndArrow == null) return;
// Prepare to draw arrows. Set stroke to a solid stroke the same width
// as the current outline.
float width = myOutline.getStrokeWidth();
g2d.setStroke (new BasicStroke (width));
double x;
double y;
double dx;
double dy;
double phi;
if (myStartArrow != null)
{
Point p0 = myPoints.get (0);
Point p1 = myPoints.get (1);
x = p0.x;
y = p0.y;
dx = x - p1.x;
dy = y - p1.y;
phi =
dx == 0.0 && dy == 0.0 ?
0.0 :
Math.atan2 (dy, dx);
myStartArrow.draw (g2d, width, x, y, phi);
}
if (myEndArrow != null)
{
Point pnm1 = myPoints.get (n-1);
Point pnm2 = myPoints.get (n-2);
x = pnm1.x;
y = pnm1.y;
dx = x - pnm2.x;
dy = y - pnm2.y;
phi =
dx == 0.0 && dy == 0.0 ?
0.0 :
Math.atan2 (dy, dx);
myEndArrow.draw (g2d, width, x, y, phi);
}
}
// Hidden operations.
/**
* Add the given endpoint to this line.
*/
private Line addEndpoint
(Point p)
{
myLastPoint = p;
myPoints.add (p);
myNw = null;
return this;
}
/**
* Compute this line's bounding box from myPoints. The results are stored in
* myNw, mySe, and mySize.
*/
private void computeBoundingBox()
{
if (myNw == null)
{
myNw = new Point
(Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
mySe = new Point
(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY);
for (Point p : myPoints)
{
myNw = myNw.min (p);
mySe = mySe.max (p);
}
mySize = mySe.difference (myNw);
}
}
/**
* Compute this line's path from myPoints.
*/
private GeneralPath computePath()
{
GeneralPath path = new GeneralPath();
int n = myPoints.size();
Iterator iter = myPoints.iterator();
Point point = null;
if (n == 0)
{
// No points. Empty path.
}
else if (n == 1)
{
// One point.
point = iter.next();
path.moveTo ((float) point.x, (float) point.y);
}
else if (n == 2)
{
// Two points.
point = iter.next();
path.moveTo ((float) point.x, (float) point.y);
point = iter.next();
path.lineTo ((float) point.x, (float) point.y);
}
else if (myRound == 0.0)
{
// Three or more points, sharp corners.
point = iter.next();
path.moveTo ((float) point.x, (float) point.y);
while (iter.hasNext())
{
point = iter.next();
path.lineTo ((float) point.x, (float) point.y);
}
}
else
{
// Three or more points, round corners.
double a_x, a_y;
double b_x, b_y;
double c_x, c_y;
double d_x, d_y;
double e_x, e_y;
double f_x, f_y;
double g_x, g_y;
double current_x, current_y;
double delta_x, delta_y, dist, p, q;
double startLimit, endLimit;
point = iter.next();
path.moveTo ((float) point.x, (float) point.y);
a_x = point.x; a_y = point.y;
current_x = a_x; current_y = a_y;
point = iter.next();
b_x = point.x; b_y = point.y;
startLimit = 1.0;
endLimit = 0.5;
while (iter.hasNext())
{
point = iter.next();
c_x = point.x; c_y = point.y;
if (! iter.hasNext()) endLimit = 1.0;
// Points a, b, and c define the two line segments that join at
// a corner.
// Point d is the cubic Bezier curve starting point.
delta_x = b_x - a_x;
delta_y = b_y - a_y;
dist = Math.sqrt (delta_x * delta_x + delta_y * delta_y);
p = Math.min (myRound / dist, startLimit);
q = 1.0 - p;
d_x = p * a_x + q * b_x;
d_y = p * a_y + q * b_y;
// Point g is the cubic Bezier curve ending point.
delta_x = b_x - c_x;
delta_y = b_y - c_y;
dist = Math.sqrt (delta_x * delta_x + delta_y * delta_y);
p = Math.min (myRound / dist, endLimit);
q = 1.0 - p;
g_x = p * c_x + q * b_x;
g_y = p * c_y + q * b_y;
// Point e is the cubic Bezier curve first control point.
p = 0.4477152501692067;
q = 0.5522847498307933;
e_x = p * d_x + q * b_x;
e_y = p * d_y + q * b_y;
// Point f is the cubic Bezier curve first control point.
f_x = p * g_x + q * b_x;
f_y = p * g_y + q * b_y;
// Line from current point to d if necessary.
if (current_x != d_x || current_y != d_y)
{
path.lineTo ((float) d_x, (float) d_y);
}
// Cubic Bezier curve from d to g.
path.curveTo
((float) e_x, (float) e_y,
(float) f_x, (float) f_y,
(float) g_x, (float) g_y);
current_x = g_x; current_y = g_y;
// Advance to the next corner.
a_x = b_x; a_y = b_y;
b_x = c_x; b_y = c_y;
startLimit = 0.5;
}
// Line to final endpoint if necessary.
if (current_x != b_x || current_y != b_y)
{
path.lineTo ((float) b_x, (float) b_y);
}
}
return path;
}
}
Ads help maintain this website.