最近项目需要解析svg中的path.直线和贝塞尔曲线都好办,唯独arc不太好办.
svg中的arc是由弧上两点,角度确定的,而java中的arc是由弧的外接矩形和角度决定的.
所有中间需要一个转换工作.
好吧,我自己是转化不来的,apache的batik提供了全套的svg解析,它肯定转化过来了.于是去拔它的算法.
中间辛苦不用说,找到了相关的计算类.
仿照PathIterator做一个遍历接口
import java.awt.geom.PathIterator;
/**
* The <code>ExtendedPathIterator</code> class represents a geometric
* path constructed from straight lines, quadratic and cubic (Bezier)
* curves and elliptical arcs. This interface is identical to that of
* PathIterator except it can return SEG_ARCTO from currentSegment,
* also the array of values passed to currentSegment must be of length
* 7 or an error will be thrown.
*
* This does not extend PathIterator as it would break the interface
* contract for that class.
*
* @author <a href="mailto:deweese@apache.org">Thomas DeWeese</a>
* @version $Id: ExtendedPathIterator.java,v 1.1 2011/05/27 03:01:11 zhengll Exp $
*/
public interface ExtendedPathIterator {
/**
* The segment type constant that specifies that the preceding
* subpath should be closed by appending a line segment back to
* the point corresponding to the most recent SEG_MOVETO.
*/
int SEG_CLOSE = PathIterator.SEG_CLOSE;
/**
* The segment type constant for a point that specifies the end
* point of a line to be drawn from the most recently specified
* point. */
int SEG_MOVETO = PathIterator.SEG_MOVETO;
/**
* The segment type constant for a point that specifies the end
* point of a line to be drawn from the most recently specified
* point.
*/
int SEG_LINETO = PathIterator.SEG_LINETO;
/**
* The segment type constant for the pair of points that specify a
* quadratic parametric curve to be drawn from the most recently
* specified point. The curve is interpolated by solving the
* parametric control equation in the range (t=[0..1]) using the
* most recently specified (current) point (CP), the first control
* point (P1), and the final interpolated control point (P2).
*/
int SEG_QUADTO = PathIterator.SEG_QUADTO;
/**
* The segment type constant for the set of 3 points that specify
* a cubic parametric curve to be drawn from the most recently
* specified point. The curve is interpolated by solving the
* parametric control equation in the range (t=[0..1]) using the
* most recently specified (current) point (CP), the first control
* point (P1), the second control point (P2), and the final
* interpolated control point (P3).
*/
int SEG_CUBICTO = PathIterator.SEG_CUBICTO;
/** The segment type constant for an elliptical arc. This consists of
* Seven values [rx, ry, angle, largeArcFlag, sweepFlag, x, y].
* rx, ry are the radious of the ellipse.
* angle is angle of the x axis of the ellipse.
* largeArcFlag is zero if the smaller of the two arcs are to be used.
* sweepFlag is zero if the 'left' branch is taken one otherwise.
* x and y are the destination for the ellipse. */
int SEG_ARCTO = 4321;
/** The winding rule constant for specifying an even-odd rule for
* determining the interior of a path. The even-odd rule specifies
* that a point lies inside the path if a ray drawn in any
* direction from that point to infinity is crossed by path
* segments an odd number of times.
*/
int WIND_EVEN_ODD = PathIterator.WIND_EVEN_ODD;
/**
* The winding rule constant for specifying a non-zero rule for
* determining the interior of a path. The non-zero rule specifies
* that a point lies inside the path if a ray drawn in any
* direction from that point to infinity is crossed by path
* segments a different number of times in the counter-clockwise
* direction than the clockwise direction.
*/
int WIND_NON_ZERO = PathIterator.WIND_NON_ZERO;
int currentSegment();
int currentSegment(double[] coords);
int currentSegment(float[] coords);
int getWindingRule();
boolean isDone();
void next();
}
再做一个保存信息的中间集合图形
import java.awt.Shape;
/**
* The <code>ExtendedShape</code> class represents a geometric
* path constructed from straight lines, quadratic and cubic (Bezier)
* curves and elliptical arcs.
* @author <a href="mailto:deweese@apache.org">Thomas DeWeese</a>
* @version $Id: ExtendedShape.java,v 1.1 2011/05/27 03:01:11 zhengll Exp $
*/
public interface ExtendedShape extends Shape {
/**
* Get an extended Path iterator that may return SEG_ARCTO commands
*/
ExtendedPathIterator getExtendedPathIterator();
}
重头戏来了,计算逻辑
import java.awt.Shape;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.GeneralPath;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
/**
* The <code>ExtendedGeneralPath</code> class represents a geometric
* path constructed from straight lines, quadratic and cubic (Bezier)
* curves and elliptical arc. This class delegates lines and curves to
* an enclosed <code>GeneralPath</code>. Elliptical arc is implemented
* using an <code>Arc2D</code> in float precision.
*
* <p><b>Warning</b> : An elliptical arc may be composed of several
* path segments. For futher details, see the SVG Appendix F.6
*
* @author <a href="mailto:Thierry.Kormann@sophia.inria.fr">Thierry Kormann</a>
* @version $Id: ExtendedGeneralPath.java,v 1.1 2011/05/27 03:01:11 zhengll Exp $
*/
public class ExtendedGeneralPath implements ExtendedShape, Cloneable {
/** The enclosed general path. */
protected GeneralPath path;
int numVals = 0;
int numSeg = 0;
float [] values = null;
int [] types = null;
float mx, my, cx, cy;
/**
* Constructs a new <code>ExtendedGeneralPath</code>.
*/
public ExtendedGeneralPath() {
path = new GeneralPath();
}
/**
* Constructs a new <code>ExtendedGeneralPath</code> with the
* specified winding rule to control operations that require the
* interior of the path to be defined.
*/
public ExtendedGeneralPath(int rule) {
path = new GeneralPath(rule);
}
/**
* Constructs a new <code>ExtendedGeneralPath</code> object with
* the specified winding rule and the specified initial capacity
* to store path coordinates.
*/
public ExtendedGeneralPath(int rule, int initialCapacity) {
path = new GeneralPath(rule, initialCapacity);
}
/**
* Constructs a new <code>ExtendedGeneralPath</code> object from
* an arbitrary <code>Shape</code> object.
*/
public ExtendedGeneralPath(Shape s) {
this();
append(s, false);
}
/**
* Adds an elliptical arc, defined by two radii, an angle from the
* x-axis, a flag to choose the large arc or not, a flag to
* indicate if we increase or decrease the angles and the final
* point of the arc.
*
* @param rx the x radius of the ellipse
* @param ry the y radius of the ellipse
*
* @param angle the angle from the x-axis of the current
* coordinate system to the x-axis of the ellipse in degrees.
*
* @param largeArcFlag the large arc flag. If true the arc
* spanning less than or equal to 180 degrees is chosen, otherwise
* the arc spanning greater than 180 degrees is chosen
*
* @param sweepFlag the sweep flag. If true the line joining
* center to arc sweeps through decreasing angles otherwise it
* sweeps through increasing angles
*
* @param x the absolute x coordinate of the final point of the arc.
* @param y the absolute y coordinate of the final point of the arc.
*/
public synchronized void arcTo(float rx, float ry,
float angle,
boolean largeArcFlag,
boolean sweepFlag,
float x, float y) {
// Ensure radii are valid
if (rx == 0 || ry == 0) {
lineTo(x, y);
return;
}
checkMoveTo(); // check if prev command was moveto
// Get the current (x, y) coordinates of the path
double x0 = cx;
double y0 = cy;
if (x0 == x && y0 == y) {
// If the endpoints (x, y) and (x0, y0) are identical, then this
// is equivalent to omitting the elliptical arc segment entirely.
return;
}
Arc2D arc = computeArc(x0, y0, rx, ry, angle,
largeArcFlag, sweepFlag, x, y);
if (arc == null) return;
AffineTransform t = AffineTransform.getRotateInstance
(Math.toRadians(angle), arc.getCenterX(), arc.getCenterY());
Shape s = t.createTransformedShape(arc);
path.append(s, true);
makeRoom(7);
types [numSeg++] = ExtendedPathIterator.SEG_ARCTO;
values[numVals++] = rx;
values[numVals++] = ry;
values[numVals++] = angle;
values[numVals++] = largeArcFlag?1:0;
values[numVals++] = sweepFlag?1:0;
cx = values[numVals++] = x;
cy = values[numVals++] = y;
}
/**
* This constructs an unrotated Arc2D from the SVG specification of an
* Elliptical arc. To get the final arc you need to apply a rotation
* transform such as:
*
* AffineTransform.getRotateInstance
* (angle, arc.getX()+arc.getWidth()/2, arc.getY()+arc.getHeight()/2);
*/
public static Arc2D computeArc(double x0, double y0,
double rx, double ry,
double angle,
boolean largeArcFlag,
boolean sweepFlag,
double x, double y) {
//
// Elliptical arc implementation based on the SVG specification notes
//
// Compute the half distance between the current and the final point
double dx2 = (x0 - x) / 2.0;
double dy2 = (y0 - y) / 2.0;
// Convert angle from degrees to radians
angle = Math.toRadians(angle % 360.0);
double cosAngle = Math.cos(angle);
double sinAngle = Math.sin(angle);
//
// Step 1 : Compute (x1, y1)
//
double x1 = (cosAngle * dx2 + sinAngle * dy2);
double y1 = (-sinAngle * dx2 + cosAngle * dy2);
// Ensure radii are large enough
rx = Math.abs(rx);
ry = Math.abs(ry);
double Prx = rx * rx;
double Pry = ry * ry;
double Px1 = x1 * x1;
double Py1 = y1 * y1;
// check that radii are large enough
double radiiCheck = Px1/Prx + Py1/Pry;
if (radiiCheck > 1) {
rx = Math.sqrt(radiiCheck) * rx;
ry = Math.sqrt(radiiCheck) * ry;
Prx = rx * rx;
Pry = ry * ry;
}
//
// Step 2 : Compute (cx1, cy1)
//
double sign = (largeArcFlag == sweepFlag) ? -1 : 1;
double sq = ((Prx*Pry)-(Prx*Py1)-(Pry*Px1)) / ((Prx*Py1)+(Pry*Px1));
sq = (sq < 0) ? 0 : sq;
double coef = (sign * Math.sqrt(sq));
double cx1 = coef * ((rx * y1) / ry);
double cy1 = coef * -((ry * x1) / rx);
//
// Step 3 : Compute (cx, cy) from (cx1, cy1)
//
double sx2 = (x0 + x) / 2.0;
double sy2 = (y0 + y) / 2.0;
double cx = sx2 + (cosAngle * cx1 - sinAngle * cy1);
double cy = sy2 + (sinAngle * cx1 + cosAngle * cy1);
//
// Step 4 : Compute the angleStart (angle1) and the angleExtent (dangle)
//
double ux = (x1 - cx1) / rx;
double uy = (y1 - cy1) / ry;
double vx = (-x1 - cx1) / rx;
double vy = (-y1 - cy1) / ry;
double p, n;
// Compute the angle start
n = Math.sqrt((ux * ux) + (uy * uy));
p = ux; // (1 * ux) + (0 * uy)
sign = (uy < 0) ? -1.0 : 1.0;
double angleStart = Math.toDegrees(sign * Math.acos(p / n));
// Compute the angle extent
n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy));
p = ux * vx + uy * vy;
sign = (ux * vy - uy * vx < 0) ? -1.0 : 1.0;
double angleExtent = Math.toDegrees(sign * Math.acos(p / n));
if(!sweepFlag && angleExtent > 0) {
angleExtent -= 360f;
} else if (sweepFlag && angleExtent < 0) {
angleExtent += 360f;
}
angleExtent %= 360f;
angleStart %= 360f;
//
// We can now build the resulting Arc2D in double precision
//
Arc2D.Double arc = new Arc2D.Double();
arc.x = cx - rx;
arc.y = cy - ry;
arc.width = rx * 2.0;
arc.height = ry * 2.0;
arc.start = -angleStart;
arc.extent = -angleExtent;
return arc;
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized void moveTo(float x, float y) {
// Don't add moveto to general path unless there is a reason.
makeRoom(2);
types [numSeg++] = PathIterator.SEG_MOVETO;
cx = mx = values[numVals++] = x;
cy = my = values[numVals++] = y;
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized void lineTo(float x, float y) {
checkMoveTo(); // check if prev command was moveto
path.lineTo(x, y);
makeRoom(2);
types [numSeg++] = PathIterator.SEG_LINETO;
cx = values[numVals++] = x;
cy = values[numVals++] = y;
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized void quadTo(float x1, float y1, float x2, float y2) {
checkMoveTo(); // check if prev command was moveto
path.quadTo(x1, y1, x2, y2);
makeRoom(4);
types [numSeg++] = PathIterator.SEG_QUADTO;
values[numVals++] = x1;
values[numVals++] = y1;
cx = values[numVals++] = x2;
cy = values[numVals++] = y2;
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized void curveTo(float x1, float y1,
float x2, float y2,
float x3, float y3) {
checkMoveTo(); // check if prev command was moveto
path.curveTo(x1, y1, x2, y2, x3, y3);
makeRoom(6);
types [numSeg++] = PathIterator.SEG_CUBICTO;
values[numVals++] = x1;
values[numVals++] = y1;
values[numVals++] = x2;
values[numVals++] = y2;
cx = values[numVals++] = x3;
cy = values[numVals++] = y3;
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized void closePath() {
// Don't double close path.
if ((numSeg != 0) && (types[numSeg-1] == PathIterator.SEG_CLOSE))
return;
// Only close path if the previous command wasn't a moveto
if ((numSeg != 0) && (types[numSeg-1] != PathIterator.SEG_MOVETO))
path.closePath();
makeRoom(0);
types [numSeg++] = PathIterator.SEG_CLOSE;
cx = mx;
cy = my;
}
/**
* Checks if previous command was a moveto command,
* skipping a close command (if present).
*/
protected void checkMoveTo() {
if (numSeg == 0) return;
switch(types[numSeg-1]) {
case PathIterator.SEG_MOVETO:
path.moveTo(values[numVals-2], values[numVals-1]);
break;
case PathIterator.SEG_CLOSE:
if (numSeg == 1) return;
if (types[numSeg-2] == PathIterator.SEG_MOVETO)
path.moveTo(values[numVals-2], values[numVals-1]);
break;
default:
break;
}
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public void append(Shape s, boolean connect) {
append(s.getPathIterator(new AffineTransform()), connect);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public void append(PathIterator pi, boolean connect) {
double [] vals = new double[6];
while (!pi.isDone()) {
Arrays.fill( vals, 0 );
int type = pi.currentSegment(vals);
pi.next();
if (connect && (numVals != 0)) {
if (type == PathIterator.SEG_MOVETO) {
double x = vals[0];
double y = vals[1];
if ((x != cx) ||
(y != cy)) {
// Change MOVETO to LINETO.
type = PathIterator.SEG_LINETO;
} else {
// Redundent segment (move to current loc) drop it...
if (pi.isDone()) break; // Nothing interesting
type = pi.currentSegment(vals);
pi.next();
}
}
connect = false;
}
switch(type) {
case PathIterator.SEG_CLOSE: closePath(); break;
case PathIterator.SEG_MOVETO:
moveTo ((float)vals[0], (float)vals[1]); break;
case PathIterator.SEG_LINETO:
lineTo ((float)vals[0], (float)vals[1]); break;
case PathIterator.SEG_QUADTO:
quadTo ((float)vals[0], (float)vals[1],
(float)vals[2], (float)vals[3]); break;
case PathIterator.SEG_CUBICTO:
curveTo((float)vals[0], (float)vals[1],
(float)vals[2], (float)vals[3],
(float)vals[4], (float)vals[5]); break;
}
}
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public void append(ExtendedPathIterator epi, boolean connect) {
float[] vals = new float[ 7 ];
while (!epi.isDone()) {
Arrays.fill( vals, 0 );
int type = epi.currentSegment(vals);
epi.next();
if (connect && (numVals != 0)) {
if (type == PathIterator.SEG_MOVETO) {
float x = vals[0];
float y = vals[1];
if ((x != cx) ||
(y != cy)) {
// Change MOVETO to LINETO.
type = PathIterator.SEG_LINETO;
} else {
// Redundant segment (move to current loc) drop it...
if (epi.isDone()) break; // Nothing interesting
type = epi.currentSegment(vals);
epi.next();
}
}
connect = false;
}
switch(type) {
case PathIterator.SEG_CLOSE: closePath(); break;
case PathIterator.SEG_MOVETO:
moveTo (vals[0], vals[1]); break;
case PathIterator.SEG_LINETO:
lineTo (vals[0], vals[1]); break;
case PathIterator.SEG_QUADTO:
quadTo (vals[0], vals[1],
vals[2], vals[3]); break;
case PathIterator.SEG_CUBICTO:
curveTo(vals[0], vals[1],
vals[2], vals[3],
vals[4], vals[5]); break;
case ExtendedPathIterator.SEG_ARCTO:
arcTo (vals[0], vals[1], vals[2],
(vals[3]!=0), (vals[4]!=0),
vals[5], vals[6]); break;
}
}
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized int getWindingRule() {
return path.getWindingRule();
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public void setWindingRule(int rule) {
path.setWindingRule(rule);
}
/**
* get the current position or <code>null</code>.
*/
public synchronized Point2D getCurrentPoint() {
if (numVals == 0) return null;
return new Point2D.Double(cx, cy);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized void reset() {
path.reset();
numSeg = 0;
numVals = 0;
values = null;
types = null;
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public void transform(AffineTransform at) {
if (at.getType() != AffineTransform.TYPE_IDENTITY)
throw new IllegalArgumentException
("ExtendedGeneralPaths can not be transformed");
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized Shape createTransformedShape(AffineTransform at) {
return path.createTransformedShape(at);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized Rectangle getBounds() {
return path.getBounds();
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public synchronized Rectangle2D getBounds2D() {
return path.getBounds2D();
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public boolean contains(double x, double y) {
return path.contains(x, y);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public boolean contains(Point2D p) {
return path.contains(p);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public boolean contains(double x, double y, double w, double h) {
return path.contains(x, y, w, h);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public boolean contains(Rectangle2D r) {
return path.contains(r);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public boolean intersects(double x, double y, double w, double h) {
return path.intersects(x, y, w, h);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public boolean intersects(Rectangle2D r) {
return path.intersects(r);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public PathIterator getPathIterator(AffineTransform at) {
return path.getPathIterator(at);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public PathIterator getPathIterator(AffineTransform at, double flatness) {
return path.getPathIterator(at, flatness);
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public ExtendedPathIterator getExtendedPathIterator() {
return new EPI();
}
class EPI implements ExtendedPathIterator {
int segNum = 0;
int valsIdx = 0;
public int currentSegment() {
return types[segNum];
}
public int currentSegment(double[] coords) {
int ret = types[segNum];
switch (ret) {
case SEG_CLOSE: break;
case SEG_MOVETO:
case SEG_LINETO:
coords[0] = values[valsIdx];
coords[1] = values[valsIdx+1];
break;
case SEG_QUADTO:
coords[0] = values[valsIdx];
coords[1] = values[valsIdx+1];
coords[2] = values[valsIdx+2];
coords[3] = values[valsIdx+3];
break;
case SEG_CUBICTO:
coords[0] = values[valsIdx];
coords[1] = values[valsIdx+1];
coords[2] = values[valsIdx+2];
coords[3] = values[valsIdx+3];
coords[4] = values[valsIdx+4];
coords[5] = values[valsIdx+5];
break;
case SEG_ARCTO:
coords[0] = values[valsIdx];
coords[1] = values[valsIdx+1];
coords[2] = values[valsIdx+2];
coords[3] = values[valsIdx+3];
coords[4] = values[valsIdx+4];
coords[5] = values[valsIdx+5];
coords[6] = values[valsIdx+6];
break;
}
// System.out.println("Seg: [" + segNum + "] type: " + ret +
// " vals: [" + coords[0] + ", " + coords[1] +
// "]");
return ret;
}
public int currentSegment(float[] coords) {
int ret = types[segNum];
switch (ret) {
case SEG_CLOSE: break;
case SEG_MOVETO:
case SEG_LINETO:
coords[0] = values[valsIdx];
coords[1] = values[valsIdx+1];
break;
case SEG_QUADTO:
System.arraycopy( values, valsIdx, coords, 0, 4 );
break;
case SEG_CUBICTO:
System.arraycopy( values, valsIdx, coords, 0, 6 );
break;
case SEG_ARCTO:
System.arraycopy( values, valsIdx, coords, 0, 7 );
break;
}
return ret;
}
public int getWindingRule() {
return path.getWindingRule();
}
public boolean isDone() {
return segNum == numSeg;
}
public void next() {
int type = types[segNum++];
switch (type) {
case SEG_CLOSE: break;
case SEG_MOVETO: // fallthrough is intended
case SEG_LINETO: valsIdx+=2; break;
case SEG_QUADTO: valsIdx+=4; break;
case SEG_CUBICTO:valsIdx+=6; break;
case SEG_ARCTO: valsIdx+=7; break;
}
}
}
/**
* Delegates to the enclosed <code>GeneralPath</code>.
*/
public Object clone() {
try {
ExtendedGeneralPath result = (ExtendedGeneralPath) super.clone();
result.path = (GeneralPath) path.clone();
if ( values != null ){
result.values = new float[values.length];
System.arraycopy(values, 0, result.values, 0, values.length);
}
result.numVals = numVals;
if ( types != null ){
result.types = new int[types.length];
System.arraycopy(types, 0, result.types, 0, types.length);
}
result.numSeg = numSeg;
return result;
} catch (CloneNotSupportedException ex) {}
return null;
}
/**
* Make sure, that the requested number of slots in vales[] are available.
* Must be called even for numValues = 0, because it is also
* used for initialization of those arrays.
*
* @param numValues number of requested coordinates
*/
private void makeRoom(int numValues) {
if (values == null) {
values = new float[2*numValues];
types = new int[2];
numVals = 0;
numSeg = 0;
return;
}
int newSize = numVals + numValues;
if ( newSize > values.length) {
int nlen = values.length*2;
if ( nlen < newSize )
nlen = newSize;
float [] nvals = new float[nlen];
System.arraycopy(values, 0, nvals, 0, numVals);
values = nvals;
}
if (numSeg == types.length) {
int [] ntypes = new int[types.length*2];
System.arraycopy(types, 0, ntypes, 0, types.length);
types = ntypes;
}
}
}