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}