/*****************************************************************************
* Copyright (C) The Apache Software Foundation. All rights reserved. *
* ------------------------------------------------------------------------- *
* This software is published under the terms of the Apache Software License *
* version 1.1, a copy of which has been included with this distribution in *
* the LICENSE file. *
*****************************************************************************/
package org.apache.batik.ext.awt.geom;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.AffineTransform;
import java.awt.geom.FlatteningPathIterator;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.util.Vector;
/**
* PathLength is a utility class for calculating the length
* of a path, the location of a point at a particular length
* along the path, and the angle of the tangent to the path
* at a given length.
* <p>
* It uses a FlatteningPathIterator to create a flattened version
* of the Path. This means the values returned are not always
* exact (in fact, they rarely are), but in most cases they
* are reasonably accurate.
*
* @author <a href="mailto:dean.jackson@cmis.csiro.au">Dean Jackson</a>
* @version $Id: PathLength.java,v 1.1 2001/01/23 17:07:05 tkormann Exp $
*/
public class PathLength {
/**
* Construct a PathLength utility class to operate on the
* particular Shape.
*
* @param path The Path (or Shape) to use.
*/
public PathLength(Shape path) {
setPath(path);
}
private Shape path = null;
/**
* Get the path to use in calculations.
* @return Path used in calculations.
*/
public Shape getPath() {
return path;
}
/**
* Set the path to use in calculations.
* @param v Path to be used in calculations.
*/
public void setPath(Shape v) {
this.path = v;
initialised = false;
}
/**
* The list of flattened path segments.
*/
private Vector segments = null;
/**
* Cached copy of the path length.
*/
private float pathLength = 0f;
/**
* Has this path been flattened?
*/
private boolean initialised = false;
/**
* Returns the length of the path used by this PathLength object.
*
* @return The length of the path.
*/
public float lengthOfPath() {
if (!initialised) {
initialise();
}
return pathLength;
}
protected void initialise() {
pathLength = 0f;
FlatteningPathIterator fpi = new FlatteningPathIterator(path.getPathIterator(new AffineTransform()), 0.01f);
segments = new Vector(20);
float lastMoveX = 0f;
float lastMoveY = 0f;
float currentX = 0f;
float currentY = 0f;
float seg[] = new float[6];
int segType;
segments.add(new PathSegment(PathIterator.SEG_MOVETO, 0f, 0f, 0f));
while (!fpi.isDone()) {
segType = fpi.currentSegment(seg);
switch (segType) {
case PathIterator.SEG_MOVETO:
//System.out.println("== MOVE TO " + seg[0] + " " + seg[1]);
segments.add(new PathSegment(segType, seg[0], seg[1], pathLength));
currentX = seg[0];
currentY = seg[1];
lastMoveX = currentX;
lastMoveY = currentY;
break;
case PathIterator.SEG_LINETO:
//System.out.println("== LINE TO " + seg[0] + " " + seg[1]);
pathLength += Point2D.distance(currentX, currentY, seg[0], seg[1]);
segments.add(new PathSegment(segType, seg[0], seg[1], pathLength));
currentX = seg[0];
currentY = seg[1];
break;
case PathIterator.SEG_CLOSE:
//System.out.println("== CLOSE TO " + lastMoveX + " " + lastMoveY);
pathLength += Point2D.distance(currentX, currentY, lastMoveX, lastMoveY);
segments.add(new PathSegment(PathIterator.SEG_LINETO, lastMoveX, lastMoveY, pathLength));
currentX = lastMoveX;
currentY = lastMoveY;
break;
default:
// ouch, where have these come from
System.out.println("Bad path segment types");
}
fpi.next();
}
initialised = true;
}
/**
* Return the point that is at the given length
* along the path.
*
* @param length The length along the path
* @return The point at the given length
*/
public Point2D pointAtLength(float length) {
int upperIndex = findUpperIndex(length);
if (upperIndex == -1) {
// length is off the end of the path
return null;
}
PathSegment upper = (PathSegment) segments.elementAt(upperIndex);
if (upperIndex == 0) {
// length was probably zero
// return the upper point
return new Point2D.Float(upper.getX(), upper.getY());
}
PathSegment lower = (PathSegment) segments.elementAt(upperIndex - 1);
// now work out where along the line would be the length
float offset = length - lower.getLength();
// slope
double theta = Math.atan2(upper.getY() - lower.getY(), upper.getX() - lower.getX());
float xPoint = (float) (lower.getX() + offset * Math.cos(theta));
float yPoint = (float) (lower.getY() + offset * Math.sin(theta));
return new Point2D.Float(xPoint, yPoint);
}
public float angleAtLength(float length) {
int upperIndex = findUpperIndex(length);
if (upperIndex == -1) {
// length is off the end of the path
// return 0f
return 0f;
}
PathSegment upper = (PathSegment) segments.elementAt(upperIndex);
if (upperIndex == 0) {
// length was probably zero
// return the angle between the first and second segments
//return new Point2D.Float(upper.getX(), upper.getY());
upperIndex = 1;
}
PathSegment lower = (PathSegment) segments.elementAt(upperIndex - 1);
// slope
float theta = (float) Math.atan2(upper.getY() - lower.getY(), upper.getX() - lower.getX());
return theta;
}
private int findUpperIndex(float length) {
if (!initialised) {
initialise();
}
// find the two segments that are each side of the length
int upperIndex = -1;
int numSegments = segments.size();
int currentIndex = 0;
while (upperIndex <= 0 && currentIndex < numSegments) {
PathSegment ps = (PathSegment) segments.elementAt(currentIndex);
if (ps.getLength() >= length && ps.getSegType() != PathIterator.SEG_MOVETO) {
upperIndex = currentIndex;
}
currentIndex++;
}
return upperIndex;
}
public static void main(String args[]) {
GeneralPath path = new GeneralPath();
path.moveTo(100f, 100f);
path.lineTo(200f, 150f);
path.lineTo(300f, 200f);
path.quadTo(450f, 525f, 400f, 250f);
path.closePath();
PathLength pl = new PathLength(path);
System.out.println("New Path Length created");
System.out.println("Path Length = " + pl.lengthOfPath());
System.out.println("Path Length = " + pl.lengthOfPath());
System.out.println("Point at 0 = " + pl.pointAtLength(0f));
System.out.println("Point at 10 = " + pl.pointAtLength(10f));
System.out.println("Point at 20 = " + pl.pointAtLength(20f));
System.out.println("Point at 300 = " + pl.pointAtLength(300f));
System.out.println("Point at 3000 = " + pl.pointAtLength(3000f));
System.out.println("Point at 0 = " + pl.pointAtLength(0f));
System.out.println("Point at 10 = " + pl.pointAtLength(10f));
System.out.println("Point at 20 = " + pl.pointAtLength(20f));
System.out.println("Point at 300 = " + pl.pointAtLength(300f));
System.out.println("Path Length = " + pl.lengthOfPath());
System.out.println("Point at 0 = " + pl.pointAtLength(0f));
System.out.println("Point at 10 = " + pl.pointAtLength(10f));
}
protected class PathSegment {
public PathSegment(int a, float b, float c, float d) {
setSegType(a);
setX(b);
setY(c);
setLength(d);
}
int segType;
/**
* Get the value of segType.
* @return Value of segType.
*/
public int getSegType() {
return segType;
}
/**
* Set the value of segType.
* @param v Value to assign to segType.
*/
public void setSegType(int v) {
this.segType = v;
}
float X;
/**
* Get the value of X.
* @return Value of X.
*/
public float getX() {
return X;
}
/**
* Set the value of X.
* @param v Value to assign to X.
*/
public void setX(float v) {
this.X = v;
}
float Y;
/**
* Get the value of Y.
* @return Value of Y.
*/
public float getY() {
return Y;
}
/**
* Set the value of Y.
* @param v Value to assign to Y.
*/
public void setY(float v) {
this.Y = v;
}
float length;
/**
* Get the value of Length.
* @return Value of Length.
*/
public float getLength() {
return length;
}
/**
* Set the value of Length.
* @param v Value to assign to Length.
*/
public void setLength(float v) {
this.length = v;
}
}
}