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