/* * PathModel.java * * Created on March 30, 2007, 11:32 AM * */ package ika.geo; import ika.utils.GeometryUtils; import java.awt.geom.AffineTransform; import java.awt.geom.GeneralPath; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; /** * GeoPathModel holds the geometry model of a GeoPath. It does not handle * symbolization. * @author Bernhard Jenny, Institute of Cartography, ETH Zurich. */ public final class GeoPathModel implements Serializable, Cloneable { private static final long serialVersionUID = 74565327855645505L; public static final byte NONE = 0; // no instruction defined. // NONE is not stored in this.instructions[] public static final byte MOVETO = 1; public static final byte LINETO = 2; public static final byte CURVETO = 3; // cubic bezier curve public static final byte QUADCURVETO = 4; // quadratic bezier curve public static final byte CLOSE = 5; /** * An array containing all points: x1, y1, x2, y2, etc. */ protected double[] points = new double[0]; /** * An array containing MOVETO, LINETO, CURVETO, QUADCURVETO or CLOSE. */ protected byte[] instructions = new byte[0]; /** * The bounding box of this path. * Rectangle2D is not serializable! */ private transient Rectangle2D bounds = null; /** Creates a new instance of PathModel */ public GeoPathModel() { } /** * Deserialize this object. */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { // read the serializable part of this GeoPath. stream.defaultReadObject(); // compute bounds of path. this.bounds is not serialized. updateBounds(); } /** * Create and return a copy of this path. */ @Override public Object clone() { GeoPathModel copy = new GeoPathModel(); final int instructionsCount = this.instructions.length; final byte[] instructionsCopy = new byte[instructionsCount]; System.arraycopy(this.instructions, 0, instructionsCopy, 0, instructionsCount); copy.instructions = instructionsCopy; final int coordinatesCount = this.points.length; final double[] pointsCopy = new double[coordinatesCount]; System.arraycopy(this.points, 0, pointsCopy, 0, coordinatesCount); copy.points = pointsCopy; if (this.bounds != null) { copy.bounds = (Rectangle2D) this.bounds.clone(); } return copy; } /** * Convert a cubic bezier spline to an array of straight lines. * @param path Destination for straight line segments. * @param tol The maximum tolerable deviation from the bezier spline. */ public static void bezierToStraightLines(GeoPathModel path, double x1, double y1, double x2, double y2, double x3, double y3, double x4, double y4, double tol) { final double t = 0.5; if (GeometryUtils.isStraightLine(x1, y1, x2, y2, x3, y3, x4, y4, tol)) { // don't add new point if start and end point coincide. if (x1 != x4 || y1 != y4) { path.lineTo(x4, y4); } } else { // divide curve by factor of two final double hx = t * (x2 + x3); final double hy = t * (y2 + y3); final double l2x = t * (x1 + x2); final double l2y = t * (y1 + y2); final double l3x = t * (l2x + hx); final double l3y = t * (l2y + hy); final double r3x = t * (x3 + x4); final double r3y = t * (y3 + y4); final double r2x = t * (hx + r3x); final double r2y = t * (hy + r3y); final double lrx = t * (l3x + r2x); final double lry = t * (l3y + r2y); // left part of curve bezierToStraightLines(path, x1, y1, l2x, l2y, l3x, l3y, lrx, lry, tol); // right part of curve bezierToStraightLines(path, lrx, lry, r2x, r2y, r3x, r3y, x4, y4, tol); } } /** * Adds the passed drawing instruction to this.instructions. This method does * not alter any other variable of this object. * IMPORTANT: It is not guaranteed that the passed drawing instruction is * added to this.instructions. * @param instruction The drawing instruction to add. */ private void pushDrawingInstruction(byte instruction) { final int instructionsCount = instructions.length; final byte[] newInstructions = new byte[instructionsCount + 1]; System.arraycopy(instructions, 0, newInstructions, 0, instructionsCount); newInstructions[instructionsCount] = instruction; instructions = newInstructions; } /** * Removes the last drawing instruction from instructions. This method * does not alter any other variable of this object. */ private void popDrawingInstruction() { final int instructionsCount = instructions.length; if (instructionsCount == 0) { return; } final byte[] newInstructions = new byte[instructionsCount - 1]; System.arraycopy(instructions, 0, newInstructions, 0, instructionsCount - 1); instructions = newInstructions; } /** * Add a drawing instruction that uses a single point as argument (lineto or * moveto). * @param x The horizontal coordinate of the point. * @param y The horizontal coordinate of the point. * @param drawingInstruction The instruction to add. */ private void add(double x, double y, byte drawingInstruction) { // add point final int coordinatesCount = points.length; final double[] newPoints = new double[coordinatesCount + 2]; System.arraycopy(points, 0, newPoints, 0, coordinatesCount); newPoints[coordinatesCount] = x; newPoints[coordinatesCount + 1] = y; points = newPoints; // update bounding box includeLastPointInBoundingBox(); // add drawing instruction pushDrawingInstruction(drawingInstruction); } /** * Removes count x-y point pairs from the points array. */ private void removePoints(int count) { final int coordinatesCount = points.length; final double[] newPoints = new double[coordinatesCount - count * 2]; System.arraycopy(points, 0, newPoints, 0, newPoints.length); points = newPoints; updateBounds(); } /** * Makes sure the last point added to this path is included in the bounding * box. If it is not, the bounding box is altered accordingly. */ private void includeLastPointInBoundingBox() { final int coordinatesCount = points.length; final double x = points[coordinatesCount - 2]; final double y = points[coordinatesCount - 1]; if (bounds == null) { bounds = new Rectangle2D.Double(x, y, 0, 0); return; } // enlarge the bounds rectangle if the new point lays ouside its // current extension. bounds.add(x, y); } /** * Compute the bounding box of this path and store it in this.bounds. */ protected void updateBounds() { final int coordinatesCount = points.length; if (coordinatesCount == 0) { bounds = null; return; } double minX = points[0]; double maxX = points[0]; double minY = points[1]; double maxY = points[1]; for (int i = 2; i < coordinatesCount; i += 2) { final double x = points[i]; final double y = points[i + 1]; if (x < minX) { minX = x; } else if (x > maxX) { maxX = x; } if (y < minY) { minY = y; } else if (y > maxY) { maxY = y; } } bounds = new Rectangle2D.Double(minX, minY, maxX - minX, maxY - minY); } /** * Removes the last drawing instruction of the path that was added with * moveto, lineto, etc. */ public void removeLastInstruction() { final int instructionsCount = instructions.length; if (instructionsCount == 0) { return; } switch (instructions[instructionsCount - 1]) { case CLOSE: break; case MOVETO: case LINETO: removePoints(1); break; case QUADCURVETO: removePoints(2); break; case CURVETO: removePoints(3); break; } popDrawingInstruction(); } /** * Removes all currently stored drawing instructions and all points. */ public void reset() { points = new double[0]; instructions = new byte[0]; bounds = null; } /** * Removes all currently stored drawing instructions and points, and * replaces them with the drawing stored in the passed PathIterator. * @param pathIterator Contains the new geometry of the path. */ public void reset(PathIterator pathIterator) { reset(); append(pathIterator); } /** * Append a move-to command to the current path. Places the virtual pen at * the specified location without drawing any line. * @param x The location to move to. * @param y The location to move to. */ public void moveTo(double x, double y) { final int instructionsCount = instructions.length; if (instructionsCount > 0 && instructions[instructionsCount - 1] == MOVETO) { removeLastInstruction(); } add(x, y, MOVETO); } /** * Append a move-to command to the current path. Places the virtual pen at the * specified location without drawing any line. * @param xy An array containing the x and the y coordinate. */ public void moveTo(double[] xy) { moveTo(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). * @param x The end point of the new line. * @param y The end point of the new line. */ public void lineTo(double x, double y) { add(x, y, LINETO); } /** * 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). * @param xy An array containing the x and the y coordinate. */ public void lineTo(double[] xy) { add(xy[0], xy[1], LINETO); } /** * 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. * @param x The end point of the new line, or the location to move to. * @param y The end point of the new line, or the location to move to. */ public void moveOrLineTo(double x, double y) { if (points.length == 0) { moveTo(x, y); } else { add(x, y, LINETO); } } /** * Appends a quadratic bezier curve. * @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. * @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) { // add two points int coordinatesCount = points.length; final double[] expandedPoints = new double[coordinatesCount + 4]; System.arraycopy(points, 0, expandedPoints, 0, coordinatesCount); expandedPoints[coordinatesCount] = x1; expandedPoints[++coordinatesCount] = y1; expandedPoints[++coordinatesCount] = x2; expandedPoints[++coordinatesCount] = y2; points = expandedPoints; // update bounding box includeLastPointInBoundingBox(); includeLastPointInBoundingBox(); // add drawing instruction pushDrawingInstruction(QUADCURVETO); } /** * Appends a cubic bezier curve. * @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. * @param y3 The location of the end point of the new curve. */ public void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) { // add three points int coordinatesCount = points.length; final double[] expandedPoints = new double[coordinatesCount + 6]; System.arraycopy(points, 0, expandedPoints, 0, coordinatesCount); expandedPoints[coordinatesCount] = x1; expandedPoints[++coordinatesCount] = y1; expandedPoints[++coordinatesCount] = x2; expandedPoints[++coordinatesCount] = y2; expandedPoints[++coordinatesCount] = x3; expandedPoints[++coordinatesCount] = y3; points = expandedPoints; // update bounding box includeLastPointInBoundingBox(); includeLastPointInBoundingBox(); includeLastPointInBoundingBox(); // add drawing instruction pushDrawingInstruction(CURVETO); } /** * Closes the path by connecting the last point with the first point using a * straight line. */ public void closePath() { if (instructions.length > 0) { pushDrawingInstruction(CLOSE); } } /** * Returns true if any of the sub-paths is closed. * @return True if the path is closed. */ public boolean isClosed() { final int instructionsCount = instructions.length; for (int i = 0; i < instructionsCount; i++) { if (instructions[i] == CLOSE) { return true; } } return false; } public void append(GeoPathModel pathModel, boolean connect) { if (pathModel.getDrawingInstructionCount() == 0) { return; } final int instructionsCount = pathModel.instructions.length; int pt = 0; for (int i = 0; i < instructionsCount; i++) { switch (pathModel.instructions[i]) { case MOVETO: if (i == 0 && connect) { lineTo(pathModel.points[pt++], pathModel.points[pt++]); } else { moveTo(pathModel.points[pt++], pathModel.points[pt++]); } break; case LINETO: lineTo(pathModel.points[pt++], pathModel.points[pt++]); break; case CLOSE: closePath(); break; case QUADCURVETO: quadTo(pathModel.points[pt++], pathModel.points[pt++], pathModel.points[pt++], pathModel.points[pt++]); break; case CURVETO: curveTo(pathModel.points[pt++], pathModel.points[pt++], pathModel.points[pt++], pathModel.points[pt++], pathModel.points[pt++], pathModel.points[pt++]); break; } } } public void append(PathIterator pathIterator) { double coords[] = new double[6]; while (!pathIterator.isDone()) { switch (pathIterator.currentSegment(coords)) { case PathIterator.SEG_CLOSE: closePath(); break; case PathIterator.SEG_LINETO: lineTo(coords[0], coords[1]); break; case PathIterator.SEG_MOVETO: moveTo(coords[0], coords[1]); break; case PathIterator.SEG_QUADTO: quadTo(coords[0], coords[1], coords[2], coords[3]); break; case PathIterator.SEG_CUBICTO: curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]); break; } pathIterator.next(); } updateBounds(); } /** * Returns true if this GeoPath consists of more than one line or polygon. * @return True if this is a compound path. */ public boolean isCompound() { final int instructionsCount = instructions.length; if (instructionsCount == 0) { return false; } for (int i = 1; i < instructionsCount; i++) { if (instructions[i] == MOVETO) { return true; } } return false; } /** * 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() { final int instructionsCount = instructions.length; if (instructionsCount == 0) { return 0; } int compoundCount = 0; for (int i = 0; i < instructionsCount; i++) { if (instructions[i] == MOVETO) { ++compoundCount; } } return compoundCount; } /** * Returns true if the passed point is inside this path. * @param x * @param y * @return */ public boolean contains(double x, double y) { if (bounds.contains(x, y) == false) { return false; } return false; // FIXME toGeneralPath().contains(x, y); } /** * Returns the bounding box of this path. * @return The bounding box around all points of this path. May be null if * the path does not contain any drawing instructions. The width and / or * height may be 0. */ public Rectangle2D getBounds2D() { return bounds; } /** * Returns the number of drawing instructions. * @return The number of instructions. */ public int getDrawingInstructionCount() { return instructions.length; } /** * Returns the last drawing instruction. * @return The drawing command. */ public byte getLastInstruction() { if (instructions.length == 0) { return GeoPathModel.NONE; } return instructions[instructions.length - 1]; } public Point2D getLastMoveTo() { if (getPointsCount() < 1) { return null; } GeoPathIterator iterator = getIterator(); Point2D pt = new Point2D.Double(); while (iterator.next()) { if (iterator.getInstruction() == GeoPathModel.MOVETO) { pt.setLocation(iterator.getX(), iterator.getY()); } } return pt; } public Point2D getStartPoint() { if (points.length == 0) { return null; } final double x = points[0]; final double y = points[1]; return new Point2D.Double(x, y); } public Point2D getEndPoint() { if (points.length == 0) { return null; } final double x = points[points.length - 2]; final double y = points[points.length - 1]; return new Point2D.Double(x, y); } public int getPointsCount() { return points.length / 2; } /** * 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. */ public void scale(double scale, double cx, double cy) { final int pointsCount = points.length / 2; if (pointsCount == 0) { bounds = null; return; } for (int i = 0; i < pointsCount; i++) { final double x = points[2 * i]; final double y = points[2 * i + 1]; points[2 * i] = (x - cx) * scale + cx; points[2 * i + 1] = (y - cy) * scale + cy; } updateBounds(); } /** * Apply an affine transformation on this path. * @param affineTransform The transformation to apply. */ public void transform(AffineTransform affineTransform) { affineTransform.transform(points, 0, points, 0, points.length / 2); updateBounds(); } /** * Returns the signed area enclosed by a GeoPath. The last point and the first * point are connected. * Does not work with bezier curves and polygons with islands and holes !!! ??? */ public double getSignedArea() { double area = 0.; // convert this Path if it's (partially) built of bezier curves // GeoPath path = onlyStraightLines() ? this : convertToStraightLines(); // this.bezierToStraightLines() final int nbrPoints = getPointsCount(); if (nbrPoints < 3) { return 0; } GeoPathIterator pIter = getIterator(); double x0 = pIter.getX(); double y0 = pIter.getY(); double x1 = x0; double y1 = y0; while (pIter.next() && pIter.getInstruction() == LINETO) { final double x2 = pIter.getX(); final double y2 = pIter.getY(); area += x1 * y2 - x2 * y1; x1 = x2; y1 = y2; } area += x1 * y0 - x0 * y1; return area * 0.5; } /** * Returns the area enclosed by a GeoPath. The last point and the first * point are connected. * Does not work with bezier curves !!! ??? */ public double getArea() { return Math.abs(getSignedArea()); } /** * Converts all bezier lines to straight lines and returns the result in a * new PathModel. Does not change this PathModel. * @param flatness The maximum distance between the smooth bezier curve and * the new straight lines approximating the bezier curve. */ public GeoPathModel toFlattenedPath(RenderParams rp, double flatness) { PathIterator pi = toGeneralPath(rp).getPathIterator(null, flatness); GeoPathModel pm = new GeoPathModel(); pm.reset(pi); return pm; } /** * Returns true if any segments in the path is a bezier curve. * @return */ public boolean hasBezierSegment() { final int instructionsCount = instructions.length; if (instructionsCount == 0) { return false; } for (int i = 1; i < instructionsCount; i++) { final byte b = instructions[i]; if (b == QUADCURVETO || b == CURVETO) { return true; } } return false; } public GeneralPath toGeneralPath(RenderParams rp) { if (rp == null) { return toGeneralPath(); } GeneralPath path = new GeneralPath(); final int instructionsCount = instructions.length; int ptID = 0; for (int i = 0; i < instructionsCount; i++) { switch (instructions[i]) { case MOVETO: path.moveTo(rp.tX(points[ptID++]), rp.tY(points[ptID++])); break; case LINETO: path.lineTo(rp.tX(points[ptID++]), rp.tY(points[ptID++])); break; case CLOSE: path.closePath(); break; case QUADCURVETO: path.quadTo(rp.tX(points[ptID++]), rp.tY(points[ptID++]), rp.tX(points[ptID++]), rp.tY(points[ptID++])); break; case CURVETO: path.curveTo(rp.tX(points[ptID++]), rp.tY(points[ptID++]), rp.tX(points[ptID++]), rp.tY(points[ptID++]), rp.tX(points[ptID++]), rp.tY(points[ptID++])); break; } } return path; } public GeneralPath toGeneralPath() { // FIXME remove > loss of precision when converting to float GeneralPath path = new GeneralPath(); final int instructionsCount = instructions.length; int ptID = 0; for (int i = 0; i < instructionsCount; i++) { switch (instructions[i]) { case MOVETO: path.moveTo(points[ptID++],points[ptID++]); break; case LINETO: path.lineTo(points[ptID++],points[ptID++]); break; case CLOSE: path.closePath(); break; case QUADCURVETO: path.quadTo(points[ptID++],points[ptID++], points[ptID++],points[ptID++]); break; case CURVETO: path.curveTo(points[ptID++],points[ptID++], points[ptID++],points[ptID++], points[ptID++],points[ptID++]); break; } } return path; } PathIterator toPathIterator(RenderParams rp) { return toGeneralPath(rp).getPathIterator(null); } PathIterator toPathIterator(RenderParams rp, double flatness) { return toGeneralPath(rp).getPathIterator(null, flatness); } @Override public String toString() { StringBuilder str = new StringBuilder(); final int instructionsCount = instructions.length; int ptID = 0; for (int i = 0; i < instructionsCount; i++) { switch (instructions[i]) { case MOVETO: str.append("moveto "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append("\n"); break; case LINETO: str.append("lineto "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append("\n"); break; case CLOSE: str.append("close\n"); break; case QUADCURVETO: str.append("quad "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append("\n"); break; case CURVETO: str.append("cubic "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append(" "); str.append(points[ptID++]); str.append("\n"); break; } } return str.toString(); } /** * Returns an iterator for this path. It is the caller's responsibility * to make sure that the GeoPathModel is not changed while a GeoPathIterator * is used. */ public GeoPathIterator getIterator() { return new GeoPathIterator(this); } }