001package com.hfg.svg.path; 002 003import java.awt.*; 004import java.awt.geom.AffineTransform; 005import java.awt.geom.Arc2D; 006import java.awt.geom.Path2D; 007import java.awt.geom.Point2D; 008import java.util.Arrays; 009import java.util.List; 010 011//------------------------------------------------------------------------------ 012/** 013 * Object representation of an SVG (Scalable Vector Graphics) path elliptical arc ('A' or 'a') command. 014 * 015 * @author J. Alex Taylor, hairyfatguy.com 016 */ 017//------------------------------------------------------------------------------ 018// com.hfg XML/HTML Coding Library 019// 020// This library is free software; you can redistribute it and/or 021// modify it under the terms of the GNU Lesser General Public 022// License as published by the Free Software Foundation; either 023// version 2.1 of the License, or (at your option) any later version. 024// 025// This library is distributed in the hope that it will be useful, 026// but WITHOUT ANY WARRANTY; without even the implied warranty of 027// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 028// Lesser General Public License for more details. 029// 030// You should have received a copy of the GNU Lesser General Public 031// License along with this library; if not, write to the Free Software 032// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 033// 034// J. Alex Taylor, President, Founder, CEO, COO, CFO, OOPS hairyfatguy.com 035// jataylor@hairyfatguy.com 036//------------------------------------------------------------------------------ 037 038public class SvgPathEllipticalArcCmd extends SvgPathCmd 039{ 040 041 //--------------------------------------------------------------------------- 042 public SvgPathEllipticalArcCmd() 043 { 044 super('A'); 045 } 046 047 048 //--------------------------------------------------------------------------- 049 @Override 050 public SvgPathEllipticalArcCmd setIsRelative(boolean inValue) 051 { 052 return (SvgPathEllipticalArcCmd) super.setIsRelative(inValue); 053 } 054 055 //--------------------------------------------------------------------------- 056 @Override 057 public SvgPathEllipticalArcCmd setRawNumbers(List<Float> inValue) 058 { 059 if (inValue.size()%7 != 0) 060 { 061 throw new SvgPathDataException("7 numbers must be given to a elliptical arc path command!"); 062 } 063 064 setNumSteps(inValue.size()/7); 065 066 super.setRawNumbers(inValue); 067 return this; 068 } 069 070 //--------------------------------------------------------------------------- 071 public SvgPathEllipticalArcCmd setRawNumbers(Float... inValues) 072 { 073 if (inValues.length%7 != 0) 074 { 075 throw new SvgPathDataException("7 numbers must be given to a elliptical arc path command!"); 076 } 077 078 setNumSteps(inValues.length/7); 079 080 super.setRawNumbers(Arrays.asList(inValues)); 081 return this; 082 } 083 084 085 //-------------------------------------------------------------------------- 086 // From http://www.w3.org/TR/SVG/paths.html 087 // 088 // Draws an elliptical arc from the current point to (x, y). The size and orientation of the ellipse are defined by 089 // two radii (rx, ry) and an x-axis-rotation, which indicates how the ellipse as a whole is rotated relative to the 090 // current coordinate system. The center (cx, cy) of the ellipse is calculated automatically to satisfy the constraints 091 // imposed by the other parameters. large-arc-flag and sweep-flag contribute to the automatic calculations and help 092 // determine how the arc is drawn. 093 // 094 // See http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes 095 public Point2D.Float draw(Path2D.Float inPath) 096 { 097 List<Float> rawNumbers = getRawNumbers(); 098 int numIndex = 0; 099 100 Point2D.Float currentPoint = getStartingPoint(); 101 102 while (numIndex < rawNumbers.size() - 1) 103 { 104 Float rx = rawNumbers.get(numIndex++); 105 Float ry = rawNumbers.get(numIndex++); 106 Float xAxisRotation = rawNumbers.get(numIndex++); 107 int largeArcFlag = rawNumbers.get(numIndex++).intValue(); 108 int sweepFlag = rawNumbers.get(numIndex++).intValue(); 109 Point2D.Float endPoint = new Point2D.Float(rawNumbers.get(numIndex++), rawNumbers.get(numIndex++)); 110 111 if (isRelative()) 112 { 113 endPoint.setLocation(endPoint.getX() + currentPoint.getX(), endPoint.getY() + currentPoint.getY()); 114 } 115 116 // If the endpoints (x1, y1) and (x2, y2) are identical, then this is equivalent to omitting the elliptical arc segment entirely. 117 if (currentPoint.getX() == endPoint.getX() 118 && currentPoint.getY() == endPoint.getY()) 119 { 120 // Do nothing 121 } 122 else if (0 == rx 123 || 0 == ry) 124 { 125 // If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto") joining the endpoints. 126 inPath.lineTo(endPoint.getX(), endPoint.getY()); 127 } 128 else 129 { 130 Arc2D.Float arc = generateArc(rx, ry, xAxisRotation, largeArcFlag, sweepFlag, endPoint); 131 132 // The rotation of the arc has to be done as a transform 133 AffineTransform t = AffineTransform.getRotateInstance(Math.toRadians(xAxisRotation), arc.getCenterX(), arc.getCenterY()); 134 Shape s = t.createTransformedShape(arc); 135 inPath.append(s, true); 136 } 137 138 currentPoint = endPoint; 139 } 140 141 // Return the last point. 142 return currentPoint; 143 } 144 145 //-------------------------------------------------------------------------- 146 private Arc2D.Float generateArc(float inRx, 147 float inRy, 148 float inXAxisRotationInDeg, 149 int inLargeArcFlag, 150 int inSweepFlag, 151 Point2D.Float inEndPoint) 152 { 153 // If rx or ry have negative signs, these are dropped; the absolute value is used instead. 154 float rx = Math.abs(inRx); 155 float ry = Math.abs(inRy); 156 157 // The equations simplify after a translation which places the origin at the midpoint of the line joining 158 // (x1, y1) to (x2, y2), followed by a rotation to line up the coordinate axes with the axes of the ellipse. 159 160 double halfDistanceX = (getStartingPoint().getX() - inEndPoint.getX()) / 2.0f; 161 double halfDistanceY = (getStartingPoint().getY() - inEndPoint.getY()) / 2.0f; 162 163 // Un-rotate to line up the coordinate axes with the axes of the ellipse - convert the angle from degrees to radians 164 double xAxisRotationInRad = Math.toRadians(inXAxisRotationInDeg % 360.0); 165 double cosRotation = Math.cos(xAxisRotationInRad); 166 double sinRotation = Math.sin(xAxisRotationInRad); 167 168 double unrotatedStartX = (cosRotation * halfDistanceX + sinRotation * halfDistanceY); 169 double unrotatedStartY = (-sinRotation * halfDistanceX + cosRotation * halfDistanceY); 170 171 // Ensure radii are large enough 172 double wedge = Math.pow(unrotatedStartX, 2) / Math.pow(rx, 2) + Math.pow(unrotatedStartY, 2) / Math.pow(ry, 2); 173 174 // If the result of the above equation is less than or equal to 1, then no further change need be made to rx and ry. 175 // If the result of the above equation is greater than 1, then make the replacements: 176 if (wedge > 1) 177 { 178 rx = (float) Math.sqrt(wedge) * rx; 179 ry = (float) Math.sqrt(wedge) * ry; 180 } 181 182 // Calculate the un-rotated center point 183 double rx_2 = Math.pow(rx, 2); 184 double ry_2 = Math.pow(ry, 2); 185 double unrotatedStartX_2 = Math.pow(unrotatedStartX, 2); 186 double unrotatedStartY_2 = Math.pow(unrotatedStartY, 2); 187 float sign = (inLargeArcFlag != inSweepFlag) ? 1 : -1; 188 189 double product = ((rx_2 * ry_2) - (rx_2 * unrotatedStartY_2) - (ry_2 * unrotatedStartX_2)) / ((rx_2 * unrotatedStartY_2) + (ry_2 * unrotatedStartX_2)); 190 if (product < 0) 191 { 192 product = 0; 193 } 194 195 double sqrt = sign * Math.sqrt(product); 196 double unrotatedCenterX = sqrt * ((rx * unrotatedStartY) / ry); 197 double unrotatedCenterY = sqrt * -((ry * unrotatedStartX) / rx); 198 199 // Calculate the rotated center point 200 double rotatedCenterX = (cosRotation * unrotatedCenterX - sinRotation * unrotatedCenterY) + ((getStartingPoint().getX() + inEndPoint.getX()) / 2.0); 201 double rotatedCenterY = (sinRotation * unrotatedCenterX + cosRotation * unrotatedCenterY) + ((getStartingPoint().getY() + inEndPoint.getY()) / 2.0); 202 203 // Compute θ1 (angle start) and Δθ (angle extent) 204 double ux = (unrotatedStartX - unrotatedCenterX) / rx; 205 double uy = (unrotatedStartY - unrotatedCenterY) / ry; 206 double vx = (-unrotatedStartX - unrotatedCenterX) / rx; 207 double vy = (-unrotatedStartY - unrotatedCenterY) / ry; 208 sign = (uy < 0) ? -1 : 1; 209 210 float angleStart = (float) Math.toDegrees(sign * Math.acos(ux / Math.sqrt((ux * ux) + (uy * uy)))); 211 212 // Compute the angle extent 213 sign = (ux * vy - uy * vx < 0) ? -1 : 1; 214 float angleExtent = (float) Math.toDegrees(sign * Math.acos((ux * vx + uy * vy) / Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)))); 215 216 if (0 == inSweepFlag 217 && angleExtent > 0) 218 { 219 angleExtent -= 360f; 220 } 221 else if (inSweepFlag > 0 222 && angleExtent < 0) 223 { 224 angleExtent += 360f; 225 } 226 227 angleExtent %= 360f; 228 angleStart %= 360f; 229 230 return new Arc2D.Float((float) rotatedCenterX - rx, 231 (float) rotatedCenterY - ry, 232 rx * 2, 233 ry * 2, 234 -angleStart, 235 -angleExtent, 236 Arc2D.OPEN); 237 } 238}