1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 * in compliance with the License. You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the License
10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 * or implied. See the License for the specific language governing permissions and limitations under
12 * the License.
13 */
14
15package android.support.graphics.drawable;
16
17import android.graphics.Path;
18import android.util.Log;
19
20import java.util.ArrayList;
21
22// This class is a duplicate from the PathParser.java of frameworks/base, with slight
23// update on incompatible API like copyOfRange().
24class PathParser {
25    private static final String LOGTAG = "PathParser";
26
27    // Copy from Arrays.copyOfRange() which is only available from API level 9.
28
29    /**
30     * Copies elements from {@code original} into a new array, from indexes start (inclusive) to
31     * end (exclusive). The original order of elements is preserved.
32     * If {@code end} is greater than {@code original.length}, the result is padded
33     * with the value {@code 0.0f}.
34     *
35     * @param original the original array
36     * @param start    the start index, inclusive
37     * @param end      the end index, exclusive
38     * @return the new array
39     * @throws ArrayIndexOutOfBoundsException if {@code start < 0 || start > original.length}
40     * @throws IllegalArgumentException       if {@code start > end}
41     * @throws NullPointerException           if {@code original == null}
42     */
43    private static float[] copyOfRange(float[] original, int start, int end) {
44        if (start > end) {
45            throw new IllegalArgumentException();
46        }
47        int originalLength = original.length;
48        if (start < 0 || start > originalLength) {
49            throw new ArrayIndexOutOfBoundsException();
50        }
51        int resultLength = end - start;
52        int copyLength = Math.min(resultLength, originalLength - start);
53        float[] result = new float[resultLength];
54        System.arraycopy(original, start, result, 0, copyLength);
55        return result;
56    }
57
58    /**
59     * @param pathData The string representing a path, the same as "d" string in svg file.
60     * @return the generated Path object.
61     */
62    public static Path createPathFromPathData(String pathData) {
63        Path path = new Path();
64        PathDataNode[] nodes = createNodesFromPathData(pathData);
65        if (nodes != null) {
66            try {
67                PathDataNode.nodesToPath(nodes, path);
68            } catch (RuntimeException e) {
69                throw new RuntimeException("Error in parsing " + pathData, e);
70            }
71            return path;
72        }
73        return null;
74    }
75
76    /**
77     * @param pathData The string representing a path, the same as "d" string in svg file.
78     * @return an array of the PathDataNode.
79     */
80    public static PathDataNode[] createNodesFromPathData(String pathData) {
81        if (pathData == null) {
82            return null;
83        }
84        int start = 0;
85        int end = 1;
86
87        ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
88        while (end < pathData.length()) {
89            end = nextStart(pathData, end);
90            String s = pathData.substring(start, end).trim();
91            if (s.length() > 0) {
92                float[] val = getFloats(s);
93                addNode(list, s.charAt(0), val);
94            }
95
96            start = end;
97            end++;
98        }
99        if ((end - start) == 1 && start < pathData.length()) {
100            addNode(list, pathData.charAt(start), new float[0]);
101        }
102        return list.toArray(new PathDataNode[list.size()]);
103    }
104
105    /**
106     * @param source The array of PathDataNode to be duplicated.
107     * @return a deep copy of the <code>source</code>.
108     */
109    public static PathDataNode[] deepCopyNodes(PathDataNode[] source) {
110        if (source == null) {
111            return null;
112        }
113        PathDataNode[] copy = new PathParser.PathDataNode[source.length];
114        for (int i = 0; i < source.length; i++) {
115            copy[i] = new PathDataNode(source[i]);
116        }
117        return copy;
118    }
119
120    /**
121     * @param nodesFrom The source path represented in an array of PathDataNode
122     * @param nodesTo   The target path represented in an array of PathDataNode
123     * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
124     */
125    public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
126        if (nodesFrom == null || nodesTo == null) {
127            return false;
128        }
129
130        if (nodesFrom.length != nodesTo.length) {
131            return false;
132        }
133
134        for (int i = 0; i < nodesFrom.length; i++) {
135            if (nodesFrom[i].type != nodesTo[i].type
136                    || nodesFrom[i].params.length != nodesTo[i].params.length) {
137                return false;
138            }
139        }
140        return true;
141    }
142
143    /**
144     * Update the target's data to match the source.
145     * Before calling this, make sure canMorph(target, source) is true.
146     *
147     * @param target The target path represented in an array of PathDataNode
148     * @param source The source path represented in an array of PathDataNode
149     */
150    public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
151        for (int i = 0; i < source.length; i++) {
152            target[i].type = source[i].type;
153            for (int j = 0; j < source[i].params.length; j++) {
154                target[i].params[j] = source[i].params[j];
155            }
156        }
157    }
158
159    private static int nextStart(String s, int end) {
160        char c;
161
162        while (end < s.length()) {
163            c = s.charAt(end);
164            // Note that 'e' or 'E' are not valid path commands, but could be
165            // used for floating point numbers' scientific notation.
166            // Therefore, when searching for next command, we should ignore 'e'
167            // and 'E'.
168            if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
169                    && c != 'e' && c != 'E') {
170                return end;
171            }
172            end++;
173        }
174        return end;
175    }
176
177    private static void addNode(ArrayList<PathDataNode> list, char cmd, float[] val) {
178        list.add(new PathDataNode(cmd, val));
179    }
180
181    private static class ExtractFloatResult {
182        // We need to return the position of the next separator and whether the
183        // next float starts with a '-' or a '.'.
184        int mEndPosition;
185        boolean mEndWithNegOrDot;
186    }
187
188    /**
189     * Parse the floats in the string.
190     * This is an optimized version of parseFloat(s.split(",|\\s"));
191     *
192     * @param s the string containing a command and list of floats
193     * @return array of floats
194     */
195    private static float[] getFloats(String s) {
196        if (s.charAt(0) == 'z' | s.charAt(0) == 'Z') {
197            return new float[0];
198        }
199        try {
200            float[] results = new float[s.length()];
201            int count = 0;
202            int startPosition = 1;
203            int endPosition = 0;
204
205            ExtractFloatResult result = new ExtractFloatResult();
206            int totalLength = s.length();
207
208            // The startPosition should always be the first character of the
209            // current number, and endPosition is the character after the current
210            // number.
211            while (startPosition < totalLength) {
212                extract(s, startPosition, result);
213                endPosition = result.mEndPosition;
214
215                if (startPosition < endPosition) {
216                    results[count++] = Float.parseFloat(
217                            s.substring(startPosition, endPosition));
218                }
219
220                if (result.mEndWithNegOrDot) {
221                    // Keep the '-' or '.' sign with next number.
222                    startPosition = endPosition;
223                } else {
224                    startPosition = endPosition + 1;
225                }
226            }
227            return copyOfRange(results, 0, count);
228        } catch (NumberFormatException e) {
229            throw new RuntimeException("error in parsing \"" + s + "\"", e);
230        }
231    }
232
233    /**
234     * Calculate the position of the next comma or space or negative sign
235     *
236     * @param s      the string to search
237     * @param start  the position to start searching
238     * @param result the result of the extraction, including the position of the
239     *               the starting position of next number, whether it is ending with a '-'.
240     */
241    private static void extract(String s, int start, ExtractFloatResult result) {
242        // Now looking for ' ', ',', '.' or '-' from the start.
243        int currentIndex = start;
244        boolean foundSeparator = false;
245        result.mEndWithNegOrDot = false;
246        boolean secondDot = false;
247        boolean isExponential = false;
248        for (; currentIndex < s.length(); currentIndex++) {
249            boolean isPrevExponential = isExponential;
250            isExponential = false;
251            char currentChar = s.charAt(currentIndex);
252            switch (currentChar) {
253                case ' ':
254                case ',':
255                    foundSeparator = true;
256                    break;
257                case '-':
258                    // The negative sign following a 'e' or 'E' is not a separator.
259                    if (currentIndex != start && !isPrevExponential) {
260                        foundSeparator = true;
261                        result.mEndWithNegOrDot = true;
262                    }
263                    break;
264                case '.':
265                    if (!secondDot) {
266                        secondDot = true;
267                    } else {
268                        // This is the second dot, and it is considered as a separator.
269                        foundSeparator = true;
270                        result.mEndWithNegOrDot = true;
271                    }
272                    break;
273                case 'e':
274                case 'E':
275                    isExponential = true;
276                    break;
277            }
278            if (foundSeparator) {
279                break;
280            }
281        }
282        // When there is nothing found, then we put the end position to the end
283        // of the string.
284        result.mEndPosition = currentIndex;
285    }
286
287    /**
288     * Each PathDataNode represents one command in the "d" attribute of the svg
289     * file.
290     * An array of PathDataNode can represent the whole "d" attribute.
291     */
292    public static class PathDataNode {
293        /*package*/
294        char type;
295        float[] params;
296
297        private PathDataNode(char type, float[] params) {
298            this.type = type;
299            this.params = params;
300        }
301
302        private PathDataNode(PathDataNode n) {
303            type = n.type;
304            params = copyOfRange(n.params, 0, n.params.length);
305        }
306
307        /**
308         * Convert an array of PathDataNode to Path.
309         *
310         * @param node The source array of PathDataNode.
311         * @param path The target Path object.
312         */
313        public static void nodesToPath(PathDataNode[] node, Path path) {
314            float[] current = new float[6];
315            char previousCommand = 'm';
316            for (int i = 0; i < node.length; i++) {
317                addCommand(path, current, previousCommand, node[i].type, node[i].params);
318                previousCommand = node[i].type;
319            }
320        }
321
322        /**
323         * The current PathDataNode will be interpolated between the
324         * <code>nodeFrom</code> and <code>nodeTo</code> according to the
325         * <code>fraction</code>.
326         *
327         * @param nodeFrom The start value as a PathDataNode.
328         * @param nodeTo   The end value as a PathDataNode
329         * @param fraction The fraction to interpolate.
330         */
331        public void interpolatePathDataNode(PathDataNode nodeFrom,
332                                            PathDataNode nodeTo, float fraction) {
333            for (int i = 0; i < nodeFrom.params.length; i++) {
334                params[i] = nodeFrom.params[i] * (1 - fraction)
335                        + nodeTo.params[i] * fraction;
336            }
337        }
338
339        private static void addCommand(Path path, float[] current,
340                                       char previousCmd, char cmd, float[] val) {
341
342            int incr = 2;
343            float currentX = current[0];
344            float currentY = current[1];
345            float ctrlPointX = current[2];
346            float ctrlPointY = current[3];
347            float currentSegmentStartX = current[4];
348            float currentSegmentStartY = current[5];
349            float reflectiveCtrlPointX;
350            float reflectiveCtrlPointY;
351
352            switch (cmd) {
353                case 'z':
354                case 'Z':
355                    path.close();
356                    // Path is closed here, but we need to move the pen to the
357                    // closed position. So we cache the segment's starting position,
358                    // and restore it here.
359                    currentX = currentSegmentStartX;
360                    currentY = currentSegmentStartY;
361                    ctrlPointX = currentSegmentStartX;
362                    ctrlPointY = currentSegmentStartY;
363                    path.moveTo(currentX, currentY);
364                    break;
365                case 'm':
366                case 'M':
367                case 'l':
368                case 'L':
369                case 't':
370                case 'T':
371                    incr = 2;
372                    break;
373                case 'h':
374                case 'H':
375                case 'v':
376                case 'V':
377                    incr = 1;
378                    break;
379                case 'c':
380                case 'C':
381                    incr = 6;
382                    break;
383                case 's':
384                case 'S':
385                case 'q':
386                case 'Q':
387                    incr = 4;
388                    break;
389                case 'a':
390                case 'A':
391                    incr = 7;
392                    break;
393            }
394
395            for (int k = 0; k < val.length; k += incr) {
396                switch (cmd) {
397                    case 'm': // moveto - Start a new sub-path (relative)
398                        currentX += val[k + 0];
399                        currentY += val[k + 1];
400                        if (k > 0) {
401                            // According to the spec, if a moveto is followed by multiple
402                            // pairs of coordinates, the subsequent pairs are treated as
403                            // implicit lineto commands.
404                            path.rLineTo(val[k + 0], val[k + 1]);
405                        } else {
406                            path.rMoveTo(val[k + 0], val[k + 1]);
407                            currentSegmentStartX = currentX;
408                            currentSegmentStartY = currentY;
409                        }
410                        break;
411                    case 'M': // moveto - Start a new sub-path
412                        currentX = val[k + 0];
413                        currentY = val[k + 1];
414                        if (k > 0) {
415                            // According to the spec, if a moveto is followed by multiple
416                            // pairs of coordinates, the subsequent pairs are treated as
417                            // implicit lineto commands.
418                            path.lineTo(val[k + 0], val[k + 1]);
419                        } else {
420                            path.moveTo(val[k + 0], val[k + 1]);
421                            currentSegmentStartX = currentX;
422                            currentSegmentStartY = currentY;
423                        }
424                        break;
425                    case 'l': // lineto - Draw a line from the current point (relative)
426                        path.rLineTo(val[k + 0], val[k + 1]);
427                        currentX += val[k + 0];
428                        currentY += val[k + 1];
429                        break;
430                    case 'L': // lineto - Draw a line from the current point
431                        path.lineTo(val[k + 0], val[k + 1]);
432                        currentX = val[k + 0];
433                        currentY = val[k + 1];
434                        break;
435                    case 'h': // horizontal lineto - Draws a horizontal line (relative)
436                        path.rLineTo(val[k + 0], 0);
437                        currentX += val[k + 0];
438                        break;
439                    case 'H': // horizontal lineto - Draws a horizontal line
440                        path.lineTo(val[k + 0], currentY);
441                        currentX = val[k + 0];
442                        break;
443                    case 'v': // vertical lineto - Draws a vertical line from the current point (r)
444                        path.rLineTo(0, val[k + 0]);
445                        currentY += val[k + 0];
446                        break;
447                    case 'V': // vertical lineto - Draws a vertical line from the current point
448                        path.lineTo(currentX, val[k + 0]);
449                        currentY = val[k + 0];
450                        break;
451                    case 'c': // curveto - Draws a cubic Bézier curve (relative)
452                        path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
453                                val[k + 4], val[k + 5]);
454
455                        ctrlPointX = currentX + val[k + 2];
456                        ctrlPointY = currentY + val[k + 3];
457                        currentX += val[k + 4];
458                        currentY += val[k + 5];
459
460                        break;
461                    case 'C': // curveto - Draws a cubic Bézier curve
462                        path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
463                                val[k + 4], val[k + 5]);
464                        currentX = val[k + 4];
465                        currentY = val[k + 5];
466                        ctrlPointX = val[k + 2];
467                        ctrlPointY = val[k + 3];
468                        break;
469                    case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
470                        reflectiveCtrlPointX = 0;
471                        reflectiveCtrlPointY = 0;
472                        if (previousCmd == 'c' || previousCmd == 's'
473                                || previousCmd == 'C' || previousCmd == 'S') {
474                            reflectiveCtrlPointX = currentX - ctrlPointX;
475                            reflectiveCtrlPointY = currentY - ctrlPointY;
476                        }
477                        path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
478                                val[k + 0], val[k + 1],
479                                val[k + 2], val[k + 3]);
480
481                        ctrlPointX = currentX + val[k + 0];
482                        ctrlPointY = currentY + val[k + 1];
483                        currentX += val[k + 2];
484                        currentY += val[k + 3];
485                        break;
486                    case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
487                        reflectiveCtrlPointX = currentX;
488                        reflectiveCtrlPointY = currentY;
489                        if (previousCmd == 'c' || previousCmd == 's'
490                                || previousCmd == 'C' || previousCmd == 'S') {
491                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
492                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
493                        }
494                        path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
495                                val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
496                        ctrlPointX = val[k + 0];
497                        ctrlPointY = val[k + 1];
498                        currentX = val[k + 2];
499                        currentY = val[k + 3];
500                        break;
501                    case 'q': // Draws a quadratic Bézier (relative)
502                        path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
503                        ctrlPointX = currentX + val[k + 0];
504                        ctrlPointY = currentY + val[k + 1];
505                        currentX += val[k + 2];
506                        currentY += val[k + 3];
507                        break;
508                    case 'Q': // Draws a quadratic Bézier
509                        path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
510                        ctrlPointX = val[k + 0];
511                        ctrlPointY = val[k + 1];
512                        currentX = val[k + 2];
513                        currentY = val[k + 3];
514                        break;
515                    case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
516                        reflectiveCtrlPointX = 0;
517                        reflectiveCtrlPointY = 0;
518                        if (previousCmd == 'q' || previousCmd == 't'
519                                || previousCmd == 'Q' || previousCmd == 'T') {
520                            reflectiveCtrlPointX = currentX - ctrlPointX;
521                            reflectiveCtrlPointY = currentY - ctrlPointY;
522                        }
523                        path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
524                                val[k + 0], val[k + 1]);
525                        ctrlPointX = currentX + reflectiveCtrlPointX;
526                        ctrlPointY = currentY + reflectiveCtrlPointY;
527                        currentX += val[k + 0];
528                        currentY += val[k + 1];
529                        break;
530                    case 'T': // Draws a quadratic Bézier curve (reflective control point)
531                        reflectiveCtrlPointX = currentX;
532                        reflectiveCtrlPointY = currentY;
533                        if (previousCmd == 'q' || previousCmd == 't'
534                                || previousCmd == 'Q' || previousCmd == 'T') {
535                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
536                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
537                        }
538                        path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
539                                val[k + 0], val[k + 1]);
540                        ctrlPointX = reflectiveCtrlPointX;
541                        ctrlPointY = reflectiveCtrlPointY;
542                        currentX = val[k + 0];
543                        currentY = val[k + 1];
544                        break;
545                    case 'a': // Draws an elliptical arc
546                        // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
547                        drawArc(path,
548                                currentX,
549                                currentY,
550                                val[k + 5] + currentX,
551                                val[k + 6] + currentY,
552                                val[k + 0],
553                                val[k + 1],
554                                val[k + 2],
555                                val[k + 3] != 0,
556                                val[k + 4] != 0);
557                        currentX += val[k + 5];
558                        currentY += val[k + 6];
559                        ctrlPointX = currentX;
560                        ctrlPointY = currentY;
561                        break;
562                    case 'A': // Draws an elliptical arc
563                        drawArc(path,
564                                currentX,
565                                currentY,
566                                val[k + 5],
567                                val[k + 6],
568                                val[k + 0],
569                                val[k + 1],
570                                val[k + 2],
571                                val[k + 3] != 0,
572                                val[k + 4] != 0);
573                        currentX = val[k + 5];
574                        currentY = val[k + 6];
575                        ctrlPointX = currentX;
576                        ctrlPointY = currentY;
577                        break;
578                }
579                previousCmd = cmd;
580            }
581            current[0] = currentX;
582            current[1] = currentY;
583            current[2] = ctrlPointX;
584            current[3] = ctrlPointY;
585            current[4] = currentSegmentStartX;
586            current[5] = currentSegmentStartY;
587        }
588
589        private static void drawArc(Path p,
590                                    float x0,
591                                    float y0,
592                                    float x1,
593                                    float y1,
594                                    float a,
595                                    float b,
596                                    float theta,
597                                    boolean isMoreThanHalf,
598                                    boolean isPositiveArc) {
599
600            /* Convert rotation angle from degrees to radians */
601            double thetaD = Math.toRadians(theta);
602            /* Pre-compute rotation matrix entries */
603            double cosTheta = Math.cos(thetaD);
604            double sinTheta = Math.sin(thetaD);
605            /* Transform (x0, y0) and (x1, y1) into unit space */
606            /* using (inverse) rotation, followed by (inverse) scale */
607            double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
608            double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
609            double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
610            double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
611
612            /* Compute differences and averages */
613            double dx = x0p - x1p;
614            double dy = y0p - y1p;
615            double xm = (x0p + x1p) / 2;
616            double ym = (y0p + y1p) / 2;
617            /* Solve for intersecting unit circles */
618            double dsq = dx * dx + dy * dy;
619            if (dsq == 0.0) {
620                Log.w(LOGTAG, " Points are coincident");
621                return; /* Points are coincident */
622            }
623            double disc = 1.0 / dsq - 1.0 / 4.0;
624            if (disc < 0.0) {
625                Log.w(LOGTAG, "Points are too far apart " + dsq);
626                float adjust = (float) (Math.sqrt(dsq) / 1.99999);
627                drawArc(p, x0, y0, x1, y1, a * adjust,
628                        b * adjust, theta, isMoreThanHalf, isPositiveArc);
629                return; /* Points are too far apart */
630            }
631            double s = Math.sqrt(disc);
632            double sdx = s * dx;
633            double sdy = s * dy;
634            double cx;
635            double cy;
636            if (isMoreThanHalf == isPositiveArc) {
637                cx = xm - sdy;
638                cy = ym + sdx;
639            } else {
640                cx = xm + sdy;
641                cy = ym - sdx;
642            }
643
644            double eta0 = Math.atan2((y0p - cy), (x0p - cx));
645
646            double eta1 = Math.atan2((y1p - cy), (x1p - cx));
647
648            double sweep = (eta1 - eta0);
649            if (isPositiveArc != (sweep >= 0)) {
650                if (sweep > 0) {
651                    sweep -= 2 * Math.PI;
652                } else {
653                    sweep += 2 * Math.PI;
654                }
655            }
656
657            cx *= a;
658            cy *= b;
659            double tcx = cx;
660            cx = cx * cosTheta - cy * sinTheta;
661            cy = tcx * sinTheta + cy * cosTheta;
662
663            arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
664        }
665
666        /**
667         * Converts an arc to cubic Bezier segments and records them in p.
668         *
669         * @param p     The target for the cubic Bezier segments
670         * @param cx    The x coordinate center of the ellipse
671         * @param cy    The y coordinate center of the ellipse
672         * @param a     The radius of the ellipse in the horizontal direction
673         * @param b     The radius of the ellipse in the vertical direction
674         * @param e1x   E(eta1) x coordinate of the starting point of the arc
675         * @param e1y   E(eta2) y coordinate of the starting point of the arc
676         * @param theta The angle that the ellipse bounding rectangle makes with horizontal plane
677         * @param start The start angle of the arc on the ellipse
678         * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
679         */
680        private static void arcToBezier(Path p,
681                                        double cx,
682                                        double cy,
683                                        double a,
684                                        double b,
685                                        double e1x,
686                                        double e1y,
687                                        double theta,
688                                        double start,
689                                        double sweep) {
690            // Taken from equations at: http://spaceroots.org/documents/ellipse/node8.html
691            // and http://www.spaceroots.org/documents/ellipse/node22.html
692
693            // Maximum of 45 degrees per cubic Bezier segment
694            int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
695
696            double eta1 = start;
697            double cosTheta = Math.cos(theta);
698            double sinTheta = Math.sin(theta);
699            double cosEta1 = Math.cos(eta1);
700            double sinEta1 = Math.sin(eta1);
701            double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
702            double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
703
704            double anglePerSegment = sweep / numSegments;
705            for (int i = 0; i < numSegments; i++) {
706                double eta2 = eta1 + anglePerSegment;
707                double sinEta2 = Math.sin(eta2);
708                double cosEta2 = Math.cos(eta2);
709                double e2x = cx + (a * cosTheta * cosEta2) - (b * sinTheta * sinEta2);
710                double e2y = cy + (a * sinTheta * cosEta2) + (b * cosTheta * sinEta2);
711                double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
712                double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
713                double tanDiff2 = Math.tan((eta2 - eta1) / 2);
714                double alpha =
715                        Math.sin(eta2 - eta1) * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
716                double q1x = e1x + alpha * ep1x;
717                double q1y = e1y + alpha * ep1y;
718                double q2x = e2x - alpha * ep2x;
719                double q2y = e2y - alpha * ep2y;
720
721                p.cubicTo((float) q1x,
722                        (float) q1y,
723                        (float) q2x,
724                        (float) q2y,
725                        (float) e2x,
726                        (float) e2y);
727                eta1 = eta2;
728                e1x = e2x;
729                e1y = e2y;
730                ep1x = ep2x;
731                ep1y = ep2y;
732            }
733        }
734    }
735}
736