/*
* Copyright (c) 2004 David Flanagan. All rights reserved.
* This code is from the book Java Examples in a Nutshell, 3nd Edition.
* It is provided AS-IS, WITHOUT ANY WARRANTY either expressed or implied.
* You may study, use, and modify it for any non-commercial purpose,
* including teaching and use in open-source projects.
* You may distribute it non-commercially as long as you retain this notice.
* For a commercial use license, or to purchase the book,
* please visit http://www.davidflanagan.com/javaexamples3.
*/
package je3.graphics;
import java.awt.*;
import java.awt.geom.*;
/** This Shape implementation represents a spiral curve */
public class Spiral implements Shape {
double centerX, centerY; // The center of the spiral
double startRadius, startAngle; // The spiral starting point
double endRadius, endAngle; // The spiral ending point
double outerRadius; // the bigger of the two radii
int angleDirection; // 1 if angle increases, -1 otherwise
// Its hard to do contains() and intersects() tests on a spiral, so we
// do them on this circular approximation of the spiral. This is not an
// ideal solution, and is only a good approximation for "tight" spirals.
Shape approximation;
/**
* The constructor. It takes arguments for the center of the shape, the
* start point, and the end point. The start and end points are specified
* in terms of angle and radius. The spiral curve is formed by varying
* the angle and radius smoothly between the two end points.
**/
public Spiral(double centerX, double centerY,
double startRadius, double startAngle,
double endRadius, double endAngle)
{
// Save the parameters that describe the spiral
this.centerX = centerX; this.centerY = centerY;
this.startRadius = startRadius; this.startAngle = startAngle;
this.endRadius = endRadius; this.endAngle = endAngle;
// figure out the maximum radius, and the spiral direction
this.outerRadius = Math.max(startRadius, endRadius);
if (startAngle < endAngle) angleDirection = 1;
else angleDirection = -1;
if ((startRadius < 0) || (endRadius < 0))
throw new IllegalArgumentException("Spiral radii must be >= 0");
// Here's how we approximate the inside of the spiral
approximation = new Ellipse2D.Double(centerX-outerRadius,
centerY-outerRadius,
outerRadius*2, outerRadius*2);
}
/**
* The bounding box of a Spiral is the same as the bounding box of a
* circle with the same center and the maximum radius
**/
public Rectangle getBounds() {
return new Rectangle((int)(centerX-outerRadius),
(int)(centerY-outerRadius),
(int)(outerRadius*2), (int)(outerRadius*2));
}
/** Same as getBounds(), but with floating-point coordinates */
public Rectangle2D getBounds2D() {
return new Rectangle2D.Double(centerX-outerRadius, centerY-outerRadius,
outerRadius*2, outerRadius*2);
}
/**
* These methods use a circle approximation to determine whether a point
* or rectangle is inside the spiral. We could be more clever than this.
**/
public boolean contains(Point2D p) { return approximation.contains(p); }
public boolean contains(Rectangle2D r) { return approximation.contains(r);}
public boolean contains(double x, double y) {
return approximation.contains(x,y);
}
public boolean contains(double x, double y, double w, double h) {
return approximation.contains(x, y, w, h);
}
/**
* These methods determine whether the specified rectangle intersects the
* spiral. We use our circle approximation. The Shape interface explicitly
* allows approximations to be used for these methods.
**/
public boolean intersects(double x, double y, double w, double h) {
return approximation.intersects(x, y, w, h);
}
public boolean intersects(Rectangle2D r) {
return approximation.intersects(r);
}
/**
* This method is the heart of all Shape implementations. It returns a
* PathIterator that describes the shape in terms of the line and curve
* segments that comprise it. Our iterator implementation approximates
* the shape of the spiral using line segments only. We pass in a
* "flatness" argument that tells it how good the approximation must be.
* (smaller numbers mean a better approximation).
*/
public PathIterator getPathIterator(AffineTransform at) {
return new SpiralIterator(at, outerRadius/500.0);
}
/**
* Return a PathIterator that describes the shape in terms of line
* segments only, with an approximation quality specified by flatness.
**/
public PathIterator getPathIterator(AffineTransform at, double flatness) {
return new SpiralIterator(at, flatness);
}
/**
* This inner class is the PathIterator for our Spiral shape. For
* simplicity, it does not describe the spiral path in terms of Bezier
* curve segments, but simply approximates it with line segments. The
* flatness property specifies how far the approximation is allowed to
* deviate from the true curve.
**/
class SpiralIterator implements PathIterator {
AffineTransform transform; // How to transform generated coordinates
double flatness; // How close an approximation
double angle = startAngle; // Current angle
double radius = startRadius; // Current radius
boolean done = false; // Are we done yet?
/** A simple constructor. Just store the parameters into fields */
public SpiralIterator(AffineTransform transform, double flatness) {
this.transform = transform;
this.flatness = flatness;
}
/**
* All PathIterators have a "winding rule" that helps to specify what
* is the inside of a area and what is the outside. If you fill a
* spiral (which you're not supposed to do) the winding rule returned
* here yields better results than the alternative, WIND_EVEN_ODD
**/
public int getWindingRule() { return WIND_NON_ZERO; }
/** Returns true if the entire path has been iterated */
public boolean isDone() { return done; }
/**
* Store the coordinates of the current segment of the path into the
* specified array, and return the type of the segment. Use
* trigonometry to compute the coordinates based on the current angle
* and radius. If this was the first point, return a MOVETO segment,
* otherwise return a LINETO segment. Also, check to see if we're done.
**/
public int currentSegment(float[] coords) {
// given the radius and the angle, compute the point coords
coords[0] = (float)(centerX + radius*Math.cos(angle));
coords[1] = (float)(centerY - radius*Math.sin(angle));
// If a transform was specified, use it on the coordinates
if (transform != null) transform.transform(coords, 0, coords, 0,1);
// If we've reached the end of the spiral remember that fact
if (angle == endAngle) done = true;
// If this is the first point in the spiral then move to it
if (angle == startAngle) return SEG_MOVETO;
// Otherwise draw a line from the previous point to this one
return SEG_LINETO;
}
/** This method is the same as above, except using double values */
public int currentSegment(double[] coords) {
coords[0] = centerX + radius*Math.cos(angle);
coords[1] = centerY - radius*Math.sin(angle);
if (transform != null) transform.transform(coords, 0, coords, 0,1);
if (angle == endAngle) done = true;
if (angle == startAngle) return SEG_MOVETO;
else return SEG_LINETO;
}
/**
* Move on to the next segment of the path. Compute the angle and
* radius values for the next point in the spiral.
**/
public void next() {
if (done) return;
// First, figure out how much to increment the angle. This
// depends on the required flatness, and also upon the current
// radius. When drawing a circle (which we'll use as our
// approximation) of radius r, we can maintain a flatness f by
// using angular increments given by this formula:
// a = acos(2*(f/r)*(f/r) - 4*(f/r) + 1)
// Use this formula to figure out how much we can increment the
// angle for the next segment. Note that the formula does not
// work well for very small radii, so we special case those.
double x = flatness/radius;
if (Double.isNaN(x) || (x > .1))
angle += Math.PI/4*angleDirection;
else {
double y = 2*x*x - 4*x + 1;
angle += Math.acos(y)*angleDirection;
}
// Check whether we've gone past the end of the spiral
if ((angle-endAngle)*angleDirection > 0) angle = endAngle;
// Now that we know the new angle, we can use interpolation to
// figure out what the corresponding radius is.
double fractionComplete = (angle-startAngle)/(endAngle-startAngle);
radius = startRadius + (endRadius-startRadius)*fractionComplete;
}
}
}
|