package ika.geo;
import java.awt.geom.*;
import java.awt.*;
import java.io.*;
import ika.utils.*;
import java.util.ArrayList;
import java.util.Random;
/**
* GeoPath - a class that models vector data. It can treat straight lines and
* bezier curves.
* @author Bernhard Jenny, Institute of Cartography, ETH Zurich.
*/
public class GeoPath extends GeoObject implements Serializable, Cloneable {
static public GeoPath newRect(Rectangle2D bounds) {
GeoPath geoPath = new GeoPath();
geoPath.rectangle(bounds);
return geoPath;
}
static public GeoPath newRect(double west, double south, double w, double h) {
GeoPath geoPath = new GeoPath();
geoPath.moveTo(west, south);
geoPath.lineTo(west + w, south);
geoPath.lineTo(west + w, south + h);
geoPath.lineTo(west, south + h);
geoPath.closePath();
return geoPath;
}
static public GeoPath newCircle(double cx, double cy, double r) {
GeoPath geoPath = new GeoPath();
geoPath.circle(cx, cy, r);
return geoPath;
}
static public GeoPath newSquare(double cx, double cy, double d) {
GeoPath geoPath = new GeoPath();
geoPath.square(cx, cy, d);
return geoPath;
}
/**
* Build a b�zier GeoPath that approximates an arc of an ellipsoid.
* @param fromAngle counted in clock wise direction from north.
* @param arcAngle counted in clock wise direction from north.
*/
public static GeoPath newArc(double cx, double cy, double rx, double ry,
double fromAngle, double arcAngle) {
// arcAngle must be larger than kMinAngle.
final double kMinAngle = 0.0001;
// a segment of a path may not be larger than pi/4
final double kMaxSegmentAngle = Math.PI / 4;
double phi; // half angle of current segment
double remainingArc; // rest of arc to convert into segments
double arcSign; // -1. or +1. Indicates direction of arcAngle.
GeoPath path = null; // new path.
// arcAngle must have certain size.
if (Math.abs(arcAngle) < kMinAngle) {
return null;
}
// arcAngle may not be larger than 2*PI
if (Math.abs(arcAngle) > Math.PI * 2) {
arcAngle = Math.PI * 2;
}
arcSign = (arcAngle > 0.) ? 1. : -1.;
remainingArc = arcAngle;
path = new GeoPath();
// split the arc and construct each segment
final int nbrOfSegments = (int) Math.ceil(Math.abs(arcAngle / kMaxSegmentAngle));
for (int i = 0; i < nbrOfSegments; i++) {
if (Math.abs(remainingArc) > kMaxSegmentAngle) {
phi = arcSign * kMaxSegmentAngle * 0.5;
remainingArc -= arcSign * kMaxSegmentAngle;
} else {
phi = 0.5 * remainingArc;
}
final double cosPhi = Math.cos(phi);
final double sinPhi = Math.sin(phi);
final double c1x = (4. - cosPhi) / 3.;
final double c1y = (1. - cosPhi) * (cosPhi - 3.) / (3. * sinPhi);
// arc around x axis with radius = 1 at center x = 0 / y = 0.
if (i == 0) {
path.moveTo(cosPhi, sinPhi);
}
path.curveTo(c1x, -c1y, c1x, c1y, cosPhi, -sinPhi);
// rotate arc against the direction of arcAngle to add next segment at end of the path.
if (i < nbrOfSegments - 2) {
path.rotate(arcSign * kMaxSegmentAngle);
} else if (i == nbrOfSegments - 2) {
path.rotate(arcSign * (0.5 * kMaxSegmentAngle + Math.abs(0.5 * remainingArc)));
}
}
// rotate finished arc
double finalRot = -arcSign * (nbrOfSegments - 1) * kMaxSegmentAngle - 0.5 * arcSign * remainingArc - fromAngle + Math.PI * 0.5;
path.rotate(finalRot);
// scale from 1 to desired radius in x and y direction.
path.scale(rx, ry);
// center on cx, cy.
path.move(cx, cy);
return path;
}
private static final long serialVersionUID = 7350986432785586245L;
/**
* The geometry of this GeoPath.
*/
private GeoPathModel path;
/**
* A VectorSymbol that stores the graphic attributes of this GeoPath.
*/
private VectorSymbol symbol;
/** Creates a new instance of GeoPath */
public GeoPath() {
this.path = new GeoPathModel();
this.symbol = new VectorSymbol();
}
protected GeoPath(GeoPath geoPath) {
this.path = geoPath.path;
this.symbol = geoPath.symbol;
}
@Override
public GeoPath clone() {
try {
GeoPath geoPath = (GeoPath) super.clone();
// make deep clone of the VectorSymbol and the path
geoPath.symbol = (VectorSymbol) this.symbol.clone();
geoPath.path = (GeoPathModel) this.path.clone();
return geoPath;
} catch (Exception exc) {
return null;
}
}
/**
* Append a move-to command to the current path. Places the virtual pen at the
* specified location without drawing any line.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param x The location to move to.
* @param y The location to move to.
*/
public void moveTo(double x, double y) {
path.moveTo(x, y);
}
/**
* Append a move-to command to the current path. Places the virtual pen at the
* specified location without drawing any line.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param xy An array containing the x and the y coordinate.
*/
public void moveTo(double[] xy) {
path.moveTo(xy[0], xy[1]);
}
/**
* Append a move-to command to the current path. Places the virtual pen at the
* specified location without drawing any line.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param point A point containing the x and the y coordinate.
*/
public void moveTo(Point2D point) {
path.moveTo(point.getX(), point.getY());
}
/**
* Draws a line from the current location of the pen to the specified location. Before
* calling lineTo, moveTo must be called. Alternatively, use moveOrLineTo that makes
* sure moveTo is called before lineTo (or quadTo, resp. curveTo).
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param x The end point of the new line segment.
* @param y The end point of the new line segment.
*/
public void lineTo(double x, double y) {
path.lineTo(x, y);
}
/**
* Draws a line from the current location of the pen to the specified location. Before
* calling lineTo, moveTo must be called. Alternatively, use moveOrLineTo that makes
* sure moveTo is called before lineTo (or quadTo, resp. curveTo).
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param xy An array containing the x and the y coordinate.
*/
public void lineTo(double[] xy) {
path.lineTo(xy[0], xy[1]);
}
/**
* Draws a line from the current location of the pen to the specified location. Before
* calling lineTo, moveTo must be called. Alternatively, use moveOrLineTo that makes
* sure moveTo is called before lineTo (or quadTo, resp. curveTo).
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param point A point containing the x and the y coordinate.
*/
public void lineTo(Point2D point) {
path.lineTo(point.getX(), point.getY());
}
/**
* Moves the virtual pen to the specified location if this is the first call that
* changes the geometry. If this is not the first geometry changing call, a straight
* line is drawn to the specified location.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param x The end point of the new line segment, or the location to move to.
* @param y The end point of the new line segment, or the location to move to.
*/
public void moveOrLineTo(double x, double y) {
if (hasOneOrMorePoints()) {
path.lineTo(x, y);
} else {
path.moveTo(x, y);
}
}
/**
* Appends a quadratic bezier curve to this GeoPath.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param x1 The location of the control point that is not on the curve.
* @param y1 The location of the control point that is not on the curve.
* @param x2 The location of the end point of the new curve segment.
* @param y2 The location of the control point that is not on the curve.
*/
public void quadTo(double x1, double y1, double x2, double y2) {
path.quadTo(x1, y1, x2, y2);
}
/**
* Appends a cubic bezier curve to this GeoPath.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param x1 The location of the first control point that is not on the curve.
* @param y1 The location of the first control point that is not on the curve.
* @param x2 The location of the second control point that is not on the curve.
* @param y2 The location of the second control point that is not on the curve.
* @param x3 The location of the end point of the new curve segment.
* @param y3 The location of the end point of the new curve segment.
*/
public void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) {
path.curveTo(x1, y1, x2, y2, x3, y3);
}
/**
* Appends a cubic bezier curve to this GeoPath.
* <B>Important: A call to this method does not generate a MapEvent!</B>
* @param ctrl1 The location of the first control point that is not on the curve.
* @param ctrl2 The location of the second control point that is not on the curve.
* @param end The location of the end point of the new curve segment.
*/
public void curveTo(Point2D ctrl1, Point2D ctrl2, Point2D end) {
final double ctrl1x = ctrl1.getX();
final double ctrl1y = ctrl1.getY();
final double ctrl2x = ctrl2.getX();
final double ctrl2y = ctrl2.getY();
final double endx = end.getX();
final double endy = end.getY();
path.curveTo(ctrl1x, ctrl1y, ctrl2x, ctrl2y, endx, endy);
}
/**
* Closes the path by connecting the last point with the first point using a
* straight line.
* <B>Important: A call to this method does not generate a MapEvent!</B>
*/
public void closePath() {
path.closePath();
}
/**
* Returns true if any of the possible sub-paths is closed.
* @return True if the path is closed.
*/
public boolean isClosed() {
return path.isClosed();
}
/**
* Returns true if this GeoPath consists of more than one line or polygon.
* @return True if this is a compound path.
*/
public boolean isCompound() {
return this.path.isCompound();
}
/**
* Returns the number of compound sub-paths.
* @return The number of sub-paths. Returns 0 if this path does not contain
* any instruction.
*/
public int getCompoundCount() {
return this.path.getCompoundCount();
}
/**
* Constructs a path from a series of points that will be connected by straight
* lines.
* @param points The points to connect.
*/
public void straightLines(Point2D[] points) {
MapEventTrigger trigger = new MapEventTrigger(this);
try {
path.reset();
if (points.length >= 1) {
path.moveTo(points[0].getX(), points[0].getY());
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].getX(), points[i].getY());
}
}
} finally {
trigger.inform();
}
}
/**
* Constructs a path from a series of points that will be connected by straight
* lines.
* @param points The points to connect.
* @param firstPoint The id of the first point in the array.
* @nbrPoints The number of point to use.
*/
public void straightLines(double[][] points, int firstPoint, int nbrPoints) {
MapEventTrigger trigger = new MapEventTrigger(this);
try {
path.reset();
if (points.length >= 1) {
final int lastPoint = firstPoint + nbrPoints;
path.moveTo(points[firstPoint][0], points[firstPoint][1]);
for (int i = firstPoint + 1; i < lastPoint; i++) {
path.lineTo(points[i][0], points[i][1]);
}
}
} finally {
trigger.inform();
}
}
/**
* Constructs a path from a series of points that will be connected by straight
* lines.
* @param param points The points to connect.
*/
public void straightLines(double[] points) {
MapEventTrigger trigger = new MapEventTrigger(this);
try {
path.reset();
if (points.length >= 1) {
path.moveTo(points[0], points[1]);
for (int i = 1; i < points.length / 2; i++) {
path.lineTo(points[i * 2], points[i * 2 + 1]);
}
}
} finally {
trigger.inform();
}
}
/**
* Constructs a bezier control point for two straight lines that meet in a point.
* The control point lies in backward direction from point 1 towards point 0.
*/
private void bezierPoint(
double p0x, double p0y,
double p1x, double p1y,
double p2x, double p2y,
double[] controlPoint,
double smoothness) {
final double F = 0.39;
// length of the line connecting the previous point P0 with the current
// point P1.
final double length = GeometryUtils.length(p1x, p1y, p2x, p2y);
// unary vector from P1 to P0.
double dx1 = p0x - p1x;
double dy1 = p0y - p1y;
final double l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
dx1 /= l1;
dy1 /= l1;
if (Double.isNaN(dx1) || Double.isNaN(dy1)
|| Double.isInfinite(dx1) || Double.isInfinite(dy1)) {
controlPoint[0] = p0x;
controlPoint[1] = p1y;
return;
}
// unary vector from P2 to P1.
double dx2 = p1x - p2x;
double dy2 = p1y - p2y;
final double l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
dx2 /= l2;
dy2 /= l2;
// direction of tangent where bezier control point lies on.
double tx = dx1 + dx2;
double ty = dy1 + dy2;
final double l = Math.sqrt(tx * tx + ty * ty);
tx /= l;
ty /= l;
// first control point
controlPoint[0] = (p1x - length * F * smoothness * tx);
controlPoint[1] = (p1y - length * F * smoothness * ty);
}
/**
*
*/
public void smooth(double smoothness, double[][] points,
int firstPoint, int nbrPoints) {
MapEventTrigger trigger = new MapEventTrigger(this);
try {
if (smoothness <= 0. || MathUtils.numbersAreClose(0., smoothness)) {
straightLines(points, firstPoint, nbrPoints);
return;
}
final double F = 0.39;
final int lastPoint = firstPoint + nbrPoints;
if (points[0].length < 2) {
throw new IllegalArgumentException();
}
path.reset();
final boolean closePath = MathUtils.numbersAreClose(
points[firstPoint][0], points[lastPoint - 1][0])
&& MathUtils.numbersAreClose(
points[firstPoint][1], points[lastPoint - 1][1]);
double prevX = points[firstPoint][0];
double prevY = points[firstPoint][1];
double[] ctrlP1 = new double[2];
double[] ctrlP2 = new double[2];
// move to first point
path.moveTo(points[firstPoint][0], points[firstPoint][1]);
for (int i = firstPoint + 1; i < lastPoint - 1; i++) {
// previous point P0
final double x0 = points[i - 1][0];
final double y0 = points[i - 1][1];
// current point P1
final double x1 = points[i][0];
final double y1 = points[i][1];
// next point P2
final double x2 = points[i + 1][0];
final double y2 = points[i + 1][1];
bezierPoint(prevX, prevY, x0, y0, x1, y1, ctrlP1, smoothness);
bezierPoint(x2, y2, x1, y1, x0, y0, ctrlP2, smoothness);
// add a bezier line segment to the path
path.curveTo(ctrlP1[0], ctrlP1[1], ctrlP2[0], ctrlP2[1], x1, y1);
prevX = x0;
prevY = y0;
}
final double x0 = points[lastPoint - 1][0];
final double y0 = points[lastPoint - 1][1];
bezierPoint(x0, y0, x0, y0, prevX, prevY, ctrlP1, smoothness);
path.curveTo(ctrlP1[0], ctrlP1[1], x0, y0, x0, y0);
} finally {
trigger.inform();
}
}
/**
* Creates a circle. Replaces the current geometry.
* @param cx The horizontal coordinate of the center.
* @param cy The vertical coordinate of the center.
* @param r The radius of the circle.
*/
public void circle(double cx, double cy, double r) {
// Build a Bezier path that approximates a full circle.
// Based on an web-article by G. Adam Stanislav:
// "Drawing a circle with B�zier Curves"
if (r <= 0.f) {
return;
} // throw new IllegalArgumentException();
MapEventTrigger trigger = new MapEventTrigger(this);
try {
this.reset();
final double kappa = (Math.sqrt(2.) - 1.) * 4. / 3.;
final double l = r * kappa;
// move to top center
this.moveTo(cx, cy + r);
// I. quadrant
this.curveTo(cx + l, cy + r, cx + r, cy + l, cx + r, cy);
// II. quadrant
this.curveTo(cx + r, cy - l, cx + l, cy - r, cx, cy - r);
// III. quadrant
this.curveTo(cx - l, cy - r, cx - r, cy - l, cx - r, cy);
// IV. quadrant
this.curveTo(cx - r, cy + l, cx - l, cy + r, cx, cy + r);
this.closePath();
} finally {
trigger.inform();
}
}
/**
* Creates a square. Replaces the current geometry.
* @param cx The horizontal coordinate of the center.
* @param cy The vertical coordinate of the center.
* @param r The length of one side of the square.
*/
public void square(double cx, double cy, double d) {
if (d <= 0.f) {
throw new IllegalArgumentException();
}
MapEventTrigger trigger = new MapEventTrigger(this);
try {
this.reset();
double d_2 = d / 2f;
this.moveTo(cx - d_2, cy - d_2);
this.lineTo(cx + d_2, cy - d_2);
this.lineTo(cx + d_2, cy + d_2);
this.lineTo(cx - d_2, cy + d_2);
this.closePath();
} finally {
trigger.inform();
}
}
/**
* Creates a rectangle. Replaces the current geometry.
* @param rect The geometry describing the rectangle.
*/
public void rectangle(Rectangle2D rect) {
if (rect == null) {
throw new IllegalArgumentException();
}
MapEventTrigger trigger = new MapEventTrigger(this);
try {
this.reset();
final double xMin = rect.getMinX();
final double xMax = rect.getMaxX();
final double yMin = rect.getMinY();
final double yMax = rect.getMaxY();
this.moveTo(xMin, yMin);
this.lineTo(xMax, yMin);
this.lineTo(xMax, yMax);
this.lineTo(xMin, yMax);
this.closePath();
} finally {
trigger.inform();
}
}
public void reset() {
path.reset();
MapEventTrigger.inform(this);
}
public void setPathModel(GeoPathModel path) {
this.path = path;
MapEventTrigger.inform(this);
}
/**
* Removes the last point of the path that was added with moveto, lineto, etc.
*/
public void removeLastPoint() {
path.removeLastInstruction();
MapEventTrigger.inform(this);
}
/**
* Appends the geometry contained in a GeoPath to this GeoPath.
* @param geoPath The GeoPath to append.
* @param connect If true, the currently existing geometry is connected with the new geometry.
*/
public void append(GeoPath geoPath, boolean connect) {
if (geoPath != null) {
path.append(geoPath.path, connect);
MapEventTrigger.inform(this);
}
}
/**
* Appends the geometry contained by a Shape object to this GeoPath.
* @param s The Shape to append.
* @param connect If true, the currently existing geometry is connected with the new geometry.
*/
public void append(java.awt.Shape s, boolean connect) {
GeoPathModel pm = new GeoPathModel();
pm.reset(s.getPathIterator(null));
path.append(pm, connect);
MapEventTrigger.inform(this);
}
private class PathSegment {
public double[] coords;
int id;
}
/**
* Inverts the order of points in a line.
* <B>Only for straight open lines!</B>
*/
public void invertDirection() {
MapEventTrigger trigger = new MapEventTrigger(this);
try {
PathIterator pathIterator = toPathIterator(null); // FIXME without PathIterator
if (pathIterator == null) {
return;
}
java.util.Vector segments = new java.util.Vector(); // FIXME
while (!pathIterator.isDone()) {
PathSegment ps = new PathSegment();
ps.coords = new double[6];
ps.id = pathIterator.currentSegment(ps.coords);
segments.add(ps);
pathIterator.next();
}
if (segments.isEmpty()) {
return;
}
this.path.reset();
PathSegment ps = (PathSegment) (segments.get(segments.size() - 1));
this.path.moveTo(ps.coords[0], ps.coords[1]);
for (int i = segments.size() - 2; i > 0; --i) {
ps = (PathSegment) (segments.get(i));
switch (ps.id) {
case PathIterator.SEG_MOVETO:
this.path.moveTo(ps.coords[0], ps.coords[1]);
break;
case PathIterator.SEG_LINETO:
this.path.lineTo(ps.coords[0], ps.coords[1]);
break;
/*
case PathIterator.SEG_QUADTO:
this.path.quadTo(ps.coords[0], ps.coords[1], ps.coords[2], ps.coords[3]);
break;
case PathIterator.SEG_CUBICTO:
this.path.curveTo(ps.coords[0], ps.coords[1], ps.coords[2], ps.coords[3],
ps.coords[4], ps.coords[5]);
break;
case PathIterator.SEG_CLOSE:
this.path.closePath();
break;
*/
}
}
// treat initial moveto
ps = (PathSegment) (segments.get(0));
this.path.lineTo(ps.coords[0], ps.coords[1]);
} finally {
trigger.inform();
}
}
/**
* Converts all bezier lines of a GeneralPath to straight lines, and stores
* the resulting path in a other GeneralPath.
* @param flatness The maximum distance between the smooth bezier curve and
* the new straight lines approximating the bezier curve.
* @param generalPath The GeneralPath that will receive the flattened path. If null
* a new GeneralPath will be created.
* @return Returns the passed generalPath if not null, or a new GeneralPath otherwise.
*/
private GeneralPath flatten(double flatness, GeneralPath generalPath) {
PathIterator pathIterator = this.toPathIterator(null, flatness);
if (generalPath == null) {
generalPath = new GeneralPath();
}
double coords[] = new double[6];
while (!pathIterator.isDone()) {
int id = pathIterator.currentSegment(coords);
switch (id) {
case PathIterator.SEG_CLOSE:
generalPath.closePath();
break;
case PathIterator.SEG_LINETO:
generalPath.lineTo(coords[0], coords[1]);
break;
case PathIterator.SEG_MOVETO:
generalPath.moveTo(coords[0], coords[1]);
break;
/*case PathIterator.SEG_QUADTO:
generalPath.quadTo(coords[0], coords[1],
coords[2], coords[3]);
break;
case PathIterator.SEG_CUBICTO:
generalPath.curveTo(coords[0], coords[1],
coords[2], coords[3],
coords[4], coords[5]);
break;
*/
}
pathIterator.next();
}
return generalPath;
}
/**
* Converts all bezier lines to straight lines. This changes this GeoPath.
* @param flatness The maximum distance between the smooth bezier curve and
* the new straight lines approximating the bezier curve.
*/
public void flatten(RenderParams rp, double flatness) {
this.path = path.toFlattenedPath(rp, flatness);
MapEventTrigger.inform(this);
}
/**
* Converts all bezier lines to straight lines. This does not change this
* GeoPath. A new GeoPath is returned instead.
* @param flatness The maximum distance between the smooth bezier curve and
* the new straight lines approximating the bezier curve.
*/
public GeoPath toFlattenedPath(RenderParams rp, double flatness) {
GeoPath geoPath = (GeoPath) this.clone();
geoPath.setPathModel(path.toFlattenedPath(rp, flatness));
return geoPath;
}
public double[][] getFirstFlattenedPolygon(double flatness) {
GeneralPath generalPath = this.flatten(flatness, (GeneralPath) null);
if (generalPath == null) {
return null;
}
// count number of points in flattened path
int nbrPts = 0;
PathIterator pathIterator = generalPath.getPathIterator(null, flatness);
while (!pathIterator.isDone()) {
pathIterator.next();
nbrPts++;
}
// allocate memory for coordinates
double[][] pts = new double[nbrPts][2];
// clone points
int ptID = 0;
pathIterator = generalPath.getPathIterator(null, flatness);
double coords[] = new double[6];
while (!pathIterator.isDone()) {
int id = pathIterator.currentSegment(coords);
switch (id) {
case PathIterator.SEG_CLOSE:
pts[ptID][0] = pts[0][0];
pts[ptID][1] = pts[0][1];
return pts;
case PathIterator.SEG_LINETO:
pts[ptID][0] = coords[0];
pts[ptID][1] = coords[1];
break;
case PathIterator.SEG_MOVETO:
if (ptID > 0) {
return pts;
}
pts[ptID][0] = coords[0];
pts[ptID][1] = coords[1];
break;
}
pathIterator.next();
ptID++;
}
return pts;
}
/**
* Returns true if any segments in the path is a bezier curve.
* @return
*/
public final boolean hasBezierSegment() {
return path.hasBezierSegment();
}
/**
* Returns true if this GeoPath contains at least one point.
* @return True if number of points > 0, false otherwise.
*/
public boolean hasOneOrMorePoints() {
return path.getDrawingInstructionCount() > 0;
}
public int getPointsCount() {
return path.getPointsCount();
}
/**
* Returns the number of drawing instructions that build this GeoPath.
* @return The number of instructions.
*/
public int getDrawingInstructionCount() {
return path.getDrawingInstructionCount();
}
public byte getLastDrawingInstruction() {
return path.getLastInstruction();
}
public Point2D getStartPoint() {
return path.getStartPoint();
}
public Point2D getEndPoint() {
return path.getEndPoint();
}
/**
* Returns a reference on the vector symbol that stores the graphic attributes
* used to draw this GeoPath.
* @return The VectorSymbol used to draw this GeoPath.
*/
public VectorSymbol getVectorSymbol() {
return symbol;
}
/**
* Set the VectorSymbol that stores the graphic attributes used to draw this
* GeoPath. The VectorSymbol is not copied, but simply a reference to it is retained.
* @param symbol The new VectorSymbol.
*/
public void setVectorSymbol(VectorSymbol symbol) {
this.symbol = symbol;
MapEventTrigger.inform(this);
}
/**
* Returns a PathIterator that can be used to draw this GeoPath or iterate over its
* geometry.
* @param affineTransform An AffineTransform to apply before the PathIterator is returned.
* @return The PathIterator.
*/
public PathIterator toPathIterator(RenderParams rp) {
return path.toPathIterator(rp);
}
/**
* Returns a flattened PathIterator that can be used to draw this GeoPath or iterate over its
* geometry. A flattened PathIterator does not contain any quatratic or cubic bezier
* curve segments, but only straight lines.
* @param affineTransform An AffineTransform to apply before the PathIterator is returned.
* @param flatness The maximum deviation of the flatted geometry from the original bezier geometry.
* @return The PathIterator.
*/
public PathIterator toPathIterator(RenderParams rp, double flatness) {
return path.toPathIterator(rp, flatness);
}
public void drawNormalState(RenderParams rp) {
final Graphics2D g2d = rp.g2d;
final double scale = rp.scale;
final GeneralPath flattenedPath = path.toGeneralPath(rp);
// fill
if (symbol != null && symbol.isFilled()) {
g2d.setColor(symbol.getFillColor());
g2d.fill(flattenedPath);
}
// stroke
if (symbol != null) {
// apply the symbol attached to this path
if (symbol.isStroked()) {
g2d.setStroke(symbol.getStroke(scale));
g2d.setColor(symbol.getStrokeColor());
g2d.draw(flattenedPath); // stroke it
}
} else {
// there is no VectorSymbol present, use a default stroke.
g2d.setStroke(new BasicStroke(0));
g2d.setColor(Color.BLACK);
g2d.draw(flattenedPath); // stroke it
}
}
public void drawSelectedState(RenderParams rp) {
if (!this.isSelected()) {
return;
}
// only stroke, no fill
final GeneralPath flattenedPath = path.toGeneralPath(rp);
rp.g2d.draw(flattenedPath);
}
public boolean isPointOnSymbol(Point2D point, double tolDist, double scale) {
if (point == null) {
return false;
}
/* First test if point is inside the bounding box.
The rectangle has to be enlarged by tolDist, otherwise contains()
returns false for a straight horizontal or vertical line. */
Rectangle2D bounds = this.getBounds2D(scale);
if (bounds == null) {
return false;
}
bounds = (Rectangle2D) bounds.clone();
GeometryUtils.enlargeRectangle(bounds, tolDist);
if (bounds.contains(point) == false) {
return false;
}
// if path is filled, test if point is inside path
if (this.symbol.isFilled() && path.contains(point.getX(), point.getY())) {
return true;
}
// test if distance to line is smaller than tolDist
// create new path with straight lines only
PathIterator pi = this.path.toPathIterator(null, tolDist / 2.);
double x1 = 0;
double y1 = 0;
double lastMoveToX = 0;
double lastMoveToY = 0;
double[] coords = new double[6];
int segmentType;
while (pi.isDone() == false) {
segmentType = pi.currentSegment(coords);
switch (segmentType) {
case PathIterator.SEG_CLOSE:
// SEG_CLOSE does not return any point.
coords[0] = lastMoveToX;
coords[1] = lastMoveToY;
// fall thru, no break here
case PathIterator.SEG_LINETO:
double d = Line2D.ptSegDistSq(x1, y1, coords[0], coords[1],
point.getX(), point.getY());
if (d < tolDist * tolDist) {
return true;
}
x1 = coords[0];
y1 = coords[1];
break;
case PathIterator.SEG_MOVETO:
lastMoveToX = x1 = coords[0];
lastMoveToY = y1 = coords[1];
break;
}
pi.next();
}
return false;
}
public boolean contains(double x, double y) {
return path.contains(x, y);
}
public boolean isIntersectedByRectangle(Rectangle2D rect, double scale) {
// Test if the passed rectangle and the bounding box of this object
// intersect.
// Don't use Rectangle2D.intersects, but use
// GeometryUtils.rectanglesIntersect, which can handle rectangles with
// an heigt or a width of 0.
final Rectangle2D bounds = this.getBounds2D(scale);
if (GeometryUtils.rectanglesIntersect(rect, bounds) == false) {
return false;
}
// transform curved bezier segments to straight line segments.
// tolerance for conversion is 0.5 pixel converted to world coordinates.
final double tolDist = 0.5 / scale;
// loop over all straight line segments of this path
PathIterator pi = path.toPathIterator(null, tolDist);
double lx1 = 0;
double ly1 = 0;
double lx2, ly2;
double lastMoveToX = 0;
double lastMoveToY = 0;
double[] coords = new double[6];
int segmentType;
while (pi.isDone() == false) {
segmentType = pi.currentSegment(coords);
lx2 = coords[0];
ly2 = coords[1];
switch (segmentType) {
case PathIterator.SEG_CLOSE:
lx2 = lastMoveToX;
ly2 = lastMoveToY;
// fall through, no break here
case PathIterator.SEG_LINETO:
// test if rect and the line segment intersect.
if (GeometryUtils.lineIntersectsRectangle(lx1, ly1, lx2, ly2, rect)) {
return true;
}
lx1 = lx2;
ly1 = ly2;
break;
case PathIterator.SEG_MOVETO:
lastMoveToX = lx1 = lx2;
lastMoveToY = ly1 = ly2;
break;
}
pi.next();
}
return false;
}
public Rectangle2D getBounds2D(double scale) {
return (path != null) ? path.getBounds2D() : null;
}
/**
* Scale this path by a factor relative to a passed origin.
* @param scale Scale factor.
* @param cx The x coordinate of the point relativ to which the object is scaled.
* @param cy The y coordinate of the point relativ to which the object is scaled.
*/
@Override
public void scale(double scale, double cx, double cy) {
this.path.scale(scale, cx, cy);
}
public void transform(AffineTransform affineTransform) {
this.path.transform(affineTransform);
MapEventTrigger.inform(this);
}
/**
* Returns an iterator for this path. It is the caller's responsibility
* to make sure that this path is not changed while a GeoPathIterator
* is used.
*/
public GeoPathIterator getIterator() {
return path.getIterator();
}
public Area toArea() {
return new Area(path.toGeneralPath());
}
@Override
public String toString() {
StringBuilder str = new StringBuilder();
PathIterator pi = path.toPathIterator(null);
double[] coord = new double[6];
while (pi.isDone() == false) {
switch (pi.currentSegment(coord)) {
case PathIterator.SEG_MOVETO:
str.append("moveto ");
str.append(coord[0]);
str.append(" ");
str.append(coord[1]);
str.append("\n");
break;
case PathIterator.SEG_LINETO:
str.append("lineto ");
str.append(coord[0]);
str.append(" ");
str.append(coord[1]);
str.append("\n");
break;
case PathIterator.SEG_QUADTO:
str.append("quad ");
str.append(coord[0]);
str.append(" ");
str.append(coord[1]);
str.append("\n\t");
str.append(coord[2]);
str.append(" ");
str.append(coord[3]);
str.append("\n");
break;
case PathIterator.SEG_CUBICTO:
str.append("cubic ");
str.append(coord[0]);
str.append(" ");
str.append(coord[1]);
str.append("\n\t");
str.append(coord[2]);
str.append(" ");
str.append(coord[3]);
str.append("\n\t");
str.append(coord[4]);
str.append(" ");
str.append(coord[5]);
str.append("\n");
break;
case PathIterator.SEG_CLOSE:
str.append("close\n");
break;
}
pi.next();
}
return super.toString() + " \n" + str.toString() + this.symbol.toString();
}
public double getArea() {
return this.path.getArea();
}
public ArrayList<Point2D> toBeads(double d,
double jitterAlongLine, double jitterVertical) {
Random random = new Random(0);
if (d <= 0) {
throw new IllegalArgumentException();
}
ArrayList<Point2D> xy = new ArrayList<Point2D>();
GeoPathIterator iterator = path.getIterator();
double startX = iterator.getX();
double startY = iterator.getY();
// add start point
xy.add(new Point2D.Double(startX, startY));
double lastMoveToX = startX;
double lastMoveToY = startY;
double length = 0;
while (iterator.next()) {
double endX = 0;
double endY = 0;
final int inst = iterator.getInstruction();
switch (inst) {
case GeoPathModel.CLOSE:
endX = lastMoveToX;
endY = lastMoveToY;
break;
case GeoPathModel.MOVETO:
startX = lastMoveToX = iterator.getX();
startY = lastMoveToY = iterator.getY();
continue;
default:
endX = iterator.getX();
endY = iterator.getY();
break;
}
// normalized direction dx and dy
double dx = endX - startX;
double dy = endY - startY;
final double l = Math.hypot(dx, dy);
dx /= l;
dy /= l;
double rest = length;
length += l;
while (length >= d) {
// compute new point
length -= d;
startX += dx * (d - rest);
startY += dy * (d - rest);
rest = 0;
Point2D.Double pt = new Point2D.Double(startX, startY);
this.jitter(pt, dx, dy, jitterAlongLine, jitterVertical, random);
xy.add(pt);
}
startX = endX;
startY = endY;
}
return xy;
}
private void jitter(Point2D.Double pt, double ndx, double ndy,
double maxJitterAlongLine, double maxJitterVertical, Random random) {
final double jitterAlongLine = maxJitterAlongLine * (random.nextDouble() - 0.5);
final double jitterVertical = maxJitterVertical * (random.nextDouble() - 0.5);
final double dx = ndx * jitterAlongLine - ndy * jitterVertical;
final double dy = ndy * jitterAlongLine + ndx * jitterVertical;
pt.x += dx;
pt.y += dy;
}
}