1/*
2 * Copyright (C) 2015 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.util;
18
19import com.android.ide.common.rendering.api.LayoutLog;
20import com.android.layoutlib.bridge.Bridge;
21import com.android.layoutlib.bridge.impl.DelegateManager;
22import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
23
24import android.annotation.NonNull;
25import android.graphics.Path_Delegate;
26
27import java.util.ArrayList;
28import java.util.Arrays;
29import java.util.logging.Level;
30import java.util.logging.Logger;
31
32/**
33 * Delegate that provides implementation for native methods in {@link android.util.PathParser}
34 * <p/>
35 * Through the layoutlib_create tool, selected methods of PathParser have been replaced by calls to
36 * methods of the same name in this delegate class.
37 *
38 * Most of the code has been taken from the implementation in
39 * {@code tools/base/sdk-common/src/main/java/com/android/ide/common/vectordrawable/PathParser.java}
40 * revision be6fe89a3b686db5a75e7e692a148699973957f3
41 */
42public class PathParser_Delegate {
43
44    private static final Logger LOGGER = Logger.getLogger("PathParser");
45
46    // ---- Builder delegate manager ----
47    private static final DelegateManager<PathParser_Delegate> sManager =
48            new DelegateManager<PathParser_Delegate>(PathParser_Delegate.class);
49
50    // ---- delegate data ----
51    @NonNull
52    private PathDataNode[] mPathDataNodes;
53
54    public static PathParser_Delegate getDelegate(long nativePtr) {
55        return sManager.getDelegate(nativePtr);
56    }
57
58    private PathParser_Delegate(@NonNull PathDataNode[] nodes) {
59        mPathDataNodes = nodes;
60    }
61
62    public PathDataNode[] getPathDataNodes() {
63        return mPathDataNodes;
64    }
65
66    @LayoutlibDelegate
67    /*package*/ static void nParseStringForPath(long pathPtr, @NonNull String pathString, int
68            stringLength) {
69        Path_Delegate path_delegate = Path_Delegate.getDelegate(pathPtr);
70        if (path_delegate == null) {
71            return;
72        }
73        assert pathString.length() == stringLength;
74        PathDataNode.nodesToPath(createNodesFromPathData(pathString), path_delegate);
75    }
76
77    @LayoutlibDelegate
78    /*package*/ static void nCreatePathFromPathData(long outPathPtr, long pathData) {
79        Path_Delegate path_delegate = Path_Delegate.getDelegate(outPathPtr);
80        PathParser_Delegate source = sManager.getDelegate(outPathPtr);
81        if (source == null || path_delegate == null) {
82            return;
83        }
84        PathDataNode.nodesToPath(source.mPathDataNodes, path_delegate);
85    }
86
87    @LayoutlibDelegate
88    /*package*/ static long nCreateEmptyPathData() {
89        PathParser_Delegate newDelegate = new PathParser_Delegate(new PathDataNode[0]);
90        return sManager.addNewDelegate(newDelegate);
91    }
92
93    @LayoutlibDelegate
94    /*package*/ static long nCreatePathData(long nativePtr) {
95        PathParser_Delegate source = sManager.getDelegate(nativePtr);
96        if (source == null) {
97            return 0;
98        }
99        PathParser_Delegate dest = new PathParser_Delegate(deepCopyNodes(source.mPathDataNodes));
100        return sManager.addNewDelegate(dest);
101    }
102
103    @LayoutlibDelegate
104    /*package*/ static long nCreatePathDataFromString(@NonNull String pathString,
105            int stringLength) {
106        assert pathString.length() == stringLength : "Inconsistent path string length.";
107        PathDataNode[] nodes = createNodesFromPathData(pathString);
108        PathParser_Delegate delegate = new PathParser_Delegate(nodes);
109        return sManager.addNewDelegate(delegate);
110
111    }
112
113    @LayoutlibDelegate
114    /*package*/ static boolean nInterpolatePathData(long outDataPtr, long fromDataPtr,
115            long toDataPtr, float fraction) {
116        PathParser_Delegate out = sManager.getDelegate(outDataPtr);
117        PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
118        PathParser_Delegate to = sManager.getDelegate(toDataPtr);
119        if (out == null || from == null || to == null) {
120            return false;
121        }
122        int length = from.mPathDataNodes.length;
123        if (length != to.mPathDataNodes.length) {
124            Bridge.getLog().error(LayoutLog.TAG_BROKEN,
125                    "Cannot interpolate path data with different lengths (from " + length + " to " +
126                            to.mPathDataNodes.length + ").", null);
127            return false;
128        }
129        if (out.mPathDataNodes.length != length) {
130            out.mPathDataNodes = new PathDataNode[length];
131        }
132        for (int i = 0; i < length; i++) {
133            if (out.mPathDataNodes[i] == null) {
134                out.mPathDataNodes[i] = new PathDataNode(from.mPathDataNodes[i]);
135            }
136            out.mPathDataNodes[i].interpolatePathDataNode(from.mPathDataNodes[i],
137                        to.mPathDataNodes[i], fraction);
138        }
139        return true;
140    }
141
142    @LayoutlibDelegate
143    /*package*/ static void nFinalize(long nativePtr) {
144        sManager.removeJavaReferenceFor(nativePtr);
145    }
146
147    @LayoutlibDelegate
148    /*package*/ static boolean nCanMorph(long fromDataPtr, long toDataPtr) {
149        PathParser_Delegate fromPath = PathParser_Delegate.getDelegate(fromDataPtr);
150        PathParser_Delegate toPath = PathParser_Delegate.getDelegate(toDataPtr);
151        if (fromPath == null || toPath == null || fromPath.getPathDataNodes() == null || toPath
152                .getPathDataNodes() == null) {
153            return true;
154        }
155        return PathParser_Delegate.canMorph(fromPath.getPathDataNodes(), toPath.getPathDataNodes());
156    }
157
158    @LayoutlibDelegate
159    /*package*/ static void nSetPathData(long outDataPtr, long fromDataPtr) {
160        PathParser_Delegate out = sManager.getDelegate(outDataPtr);
161        PathParser_Delegate from = sManager.getDelegate(fromDataPtr);
162        if (from == null || out == null) {
163            return;
164        }
165        out.mPathDataNodes = deepCopyNodes(from.mPathDataNodes);
166    }
167
168    /**
169     * @param pathData The string representing a path, the same as "d" string in svg file.
170     *
171     * @return an array of the PathDataNode.
172     */
173    @NonNull
174    public static PathDataNode[] createNodesFromPathData(@NonNull String pathData) {
175        int start = 0;
176        int end = 1;
177
178        ArrayList<PathDataNode> list = new ArrayList<PathDataNode>();
179        while (end < pathData.length()) {
180            end = nextStart(pathData, end);
181            String s = pathData.substring(start, end).trim();
182            if (s.length() > 0) {
183                float[] val = getFloats(s);
184                addNode(list, s.charAt(0), val);
185            }
186
187            start = end;
188            end++;
189        }
190        if ((end - start) == 1 && start < pathData.length()) {
191            addNode(list, pathData.charAt(start), new float[0]);
192        }
193        return list.toArray(new PathDataNode[list.size()]);
194    }
195
196    /**
197     * @param source The array of PathDataNode to be duplicated.
198     *
199     * @return a deep copy of the <code>source</code>.
200     */
201    @NonNull
202    public static PathDataNode[] deepCopyNodes(@NonNull PathDataNode[] source) {
203        PathDataNode[] copy = new PathDataNode[source.length];
204        for (int i = 0; i < source.length; i++) {
205            copy[i] = new PathDataNode(source[i]);
206        }
207        return copy;
208    }
209
210    /**
211     * @param nodesFrom The source path represented in an array of PathDataNode
212     * @param nodesTo The target path represented in an array of PathDataNode
213     * @return whether the <code>nodesFrom</code> can morph into <code>nodesTo</code>
214     */
215    public static boolean canMorph(PathDataNode[] nodesFrom, PathDataNode[] nodesTo) {
216        if (nodesFrom == null || nodesTo == null) {
217            return false;
218        }
219
220        if (nodesFrom.length != nodesTo.length) {
221            return false;
222        }
223
224        for (int i = 0; i < nodesFrom.length; i ++) {
225            if (nodesFrom[i].mType != nodesTo[i].mType
226                    || nodesFrom[i].mParams.length != nodesTo[i].mParams.length) {
227                return false;
228            }
229        }
230        return true;
231    }
232
233    /**
234     * Update the target's data to match the source.
235     * Before calling this, make sure canMorph(target, source) is true.
236     *
237     * @param target The target path represented in an array of PathDataNode
238     * @param source The source path represented in an array of PathDataNode
239     */
240    public static void updateNodes(PathDataNode[] target, PathDataNode[] source) {
241        for (int i = 0; i < source.length; i ++) {
242            target[i].mType = source[i].mType;
243            for (int j = 0; j < source[i].mParams.length; j ++) {
244                target[i].mParams[j] = source[i].mParams[j];
245            }
246        }
247    }
248
249    private static int nextStart(@NonNull String s, int end) {
250        char c;
251
252        while (end < s.length()) {
253            c = s.charAt(end);
254            // Note that 'e' or 'E' are not valid path commands, but could be
255            // used for floating point numbers' scientific notation.
256            // Therefore, when searching for next command, we should ignore 'e'
257            // and 'E'.
258            if ((((c - 'A') * (c - 'Z') <= 0) || ((c - 'a') * (c - 'z') <= 0))
259                    && c != 'e' && c != 'E') {
260                return end;
261            }
262            end++;
263        }
264        return end;
265    }
266
267    /**
268     * Calculate the position of the next comma or space or negative sign
269     *
270     * @param s the string to search
271     * @param start the position to start searching
272     * @param result the result of the extraction, including the position of the the starting
273     * position of next number, whether it is ending with a '-'.
274     */
275    private static void extract(@NonNull String s, int start, @NonNull ExtractFloatResult result) {
276        // Now looking for ' ', ',', '.' or '-' from the start.
277        int currentIndex = start;
278        boolean foundSeparator = false;
279        result.mEndWithNegOrDot = false;
280        boolean secondDot = false;
281        boolean isExponential = false;
282        for (; currentIndex < s.length(); currentIndex++) {
283            boolean isPrevExponential = isExponential;
284            isExponential = false;
285            char currentChar = s.charAt(currentIndex);
286            switch (currentChar) {
287                case ' ':
288                case ',':
289                    foundSeparator = true;
290                    break;
291                case '-':
292                    // The negative sign following a 'e' or 'E' is not a separator.
293                    if (currentIndex != start && !isPrevExponential) {
294                        foundSeparator = true;
295                        result.mEndWithNegOrDot = true;
296                    }
297                    break;
298                case '.':
299                    if (!secondDot) {
300                        secondDot = true;
301                    } else {
302                        // This is the second dot, and it is considered as a separator.
303                        foundSeparator = true;
304                        result.mEndWithNegOrDot = true;
305                    }
306                    break;
307                case 'e':
308                case 'E':
309                    isExponential = true;
310                    break;
311            }
312            if (foundSeparator) {
313                break;
314            }
315        }
316        // When there is nothing found, then we put the end position to the end
317        // of the string.
318        result.mEndPosition = currentIndex;
319    }
320
321    /**
322     * Parse the floats in the string. This is an optimized version of
323     * parseFloat(s.split(",|\\s"));
324     *
325     * @param s the string containing a command and list of floats
326     *
327     * @return array of floats
328     */
329    @NonNull
330    private static float[] getFloats(@NonNull String s) {
331        if (s.charAt(0) == 'z' || s.charAt(0) == 'Z') {
332            return new float[0];
333        }
334        try {
335            float[] results = new float[s.length()];
336            int count = 0;
337            int startPosition = 1;
338            int endPosition;
339
340            ExtractFloatResult result = new ExtractFloatResult();
341            int totalLength = s.length();
342
343            // The startPosition should always be the first character of the
344            // current number, and endPosition is the character after the current
345            // number.
346            while (startPosition < totalLength) {
347                extract(s, startPosition, result);
348                endPosition = result.mEndPosition;
349
350                if (startPosition < endPosition) {
351                    results[count++] = Float.parseFloat(
352                            s.substring(startPosition, endPosition));
353                }
354
355                if (result.mEndWithNegOrDot) {
356                    // Keep the '-' or '.' sign with next number.
357                    startPosition = endPosition;
358                } else {
359                    startPosition = endPosition + 1;
360                }
361            }
362            return Arrays.copyOf(results, count);
363        } catch (NumberFormatException e) {
364            assert false : "error in parsing \"" + s + "\"" + e;
365            return new float[0];
366        }
367    }
368
369
370    private static void addNode(@NonNull ArrayList<PathDataNode> list, char cmd,
371            @NonNull float[] val) {
372        list.add(new PathDataNode(cmd, val));
373    }
374
375    private static class ExtractFloatResult {
376        // We need to return the position of the next separator and whether the
377        // next float starts with a '-' or a '.'.
378        private int mEndPosition;
379        private boolean mEndWithNegOrDot;
380    }
381
382    /**
383     * Each PathDataNode represents one command in the "d" attribute of the svg file. An array of
384     * PathDataNode can represent the whole "d" attribute.
385     */
386    public static class PathDataNode {
387        private char mType;
388        @NonNull
389        private float[] mParams;
390
391        private PathDataNode(char type, @NonNull float[] params) {
392            mType = type;
393            mParams = params;
394        }
395
396        public char getType() {
397            return mType;
398        }
399
400        @NonNull
401        public float[] getParams() {
402            return mParams;
403        }
404
405        private PathDataNode(@NonNull PathDataNode n) {
406            mType = n.mType;
407            mParams = Arrays.copyOf(n.mParams, n.mParams.length);
408        }
409
410        /**
411         * Convert an array of PathDataNode to Path. Reset the passed path as needed before
412         * calling this method.
413         *
414         * @param node The source array of PathDataNode.
415         * @param path The target Path object.
416         */
417        public static void nodesToPath(@NonNull PathDataNode[] node, @NonNull Path_Delegate path) {
418            float[] current = new float[6];
419            char previousCommand = 'm';
420            //noinspection ForLoopReplaceableByForEach
421            for (int i = 0; i < node.length; i++) {
422                addCommand(path, current, previousCommand, node[i].mType, node[i].mParams);
423                previousCommand = node[i].mType;
424            }
425        }
426
427        /**
428         * The current PathDataNode will be interpolated between the <code>nodeFrom</code> and
429         * <code>nodeTo</code> according to the <code>fraction</code>.
430         *
431         * @param nodeFrom The start value as a PathDataNode.
432         * @param nodeTo The end value as a PathDataNode
433         * @param fraction The fraction to interpolate.
434         */
435        private void interpolatePathDataNode(@NonNull PathDataNode nodeFrom,
436                @NonNull PathDataNode nodeTo, float fraction) {
437            for (int i = 0; i < nodeFrom.mParams.length; i++) {
438                mParams[i] = nodeFrom.mParams[i] * (1 - fraction)
439                        + nodeTo.mParams[i] * fraction;
440            }
441        }
442
443        @SuppressWarnings("PointlessArithmeticExpression")
444        private static void addCommand(@NonNull Path_Delegate path, float[] current,
445                char previousCmd, char cmd, @NonNull float[] val) {
446
447            int incr = 2;
448            float currentX = current[0];
449            float currentY = current[1];
450            float ctrlPointX = current[2];
451            float ctrlPointY = current[3];
452            float currentSegmentStartX = current[4];
453            float currentSegmentStartY = current[5];
454            float reflectiveCtrlPointX;
455            float reflectiveCtrlPointY;
456
457            switch (cmd) {
458                case 'z':
459                case 'Z':
460                    path.close();
461                    // Path is closed here, but we need to move the pen to the
462                    // closed position. So we cache the segment's starting position,
463                    // and restore it here.
464                    currentX = currentSegmentStartX;
465                    currentY = currentSegmentStartY;
466                    ctrlPointX = currentSegmentStartX;
467                    ctrlPointY = currentSegmentStartY;
468                    path.moveTo(currentX, currentY);
469                    break;
470                case 'm':
471                case 'M':
472                case 'l':
473                case 'L':
474                case 't':
475                case 'T':
476                    incr = 2;
477                    break;
478                case 'h':
479                case 'H':
480                case 'v':
481                case 'V':
482                    incr = 1;
483                    break;
484                case 'c':
485                case 'C':
486                    incr = 6;
487                    break;
488                case 's':
489                case 'S':
490                case 'q':
491                case 'Q':
492                    incr = 4;
493                    break;
494                case 'a':
495                case 'A':
496                    incr = 7;
497                    break;
498            }
499
500            for (int k = 0; k < val.length; k += incr) {
501                switch (cmd) {
502                    case 'm': // moveto - Start a new sub-path (relative)
503                        currentX += val[k + 0];
504                        currentY += val[k + 1];
505
506                        if (k > 0) {
507                            // According to the spec, if a moveto is followed by multiple
508                            // pairs of coordinates, the subsequent pairs are treated as
509                            // implicit lineto commands.
510                            path.rLineTo(val[k + 0], val[k + 1]);
511                        } else {
512                            path.rMoveTo(val[k + 0], val[k + 1]);
513                            currentSegmentStartX = currentX;
514                            currentSegmentStartY = currentY;
515                        }
516                        break;
517                    case 'M': // moveto - Start a new sub-path
518                        currentX = val[k + 0];
519                        currentY = val[k + 1];
520
521                        if (k > 0) {
522                            // According to the spec, if a moveto is followed by multiple
523                            // pairs of coordinates, the subsequent pairs are treated as
524                            // implicit lineto commands.
525                            path.lineTo(val[k + 0], val[k + 1]);
526                        } else {
527                            path.moveTo(val[k + 0], val[k + 1]);
528                            currentSegmentStartX = currentX;
529                            currentSegmentStartY = currentY;
530                        }
531                        break;
532                    case 'l': // lineto - Draw a line from the current point (relative)
533                        path.rLineTo(val[k + 0], val[k + 1]);
534                        currentX += val[k + 0];
535                        currentY += val[k + 1];
536                        break;
537                    case 'L': // lineto - Draw a line from the current point
538                        path.lineTo(val[k + 0], val[k + 1]);
539                        currentX = val[k + 0];
540                        currentY = val[k + 1];
541                        break;
542                    case 'h': // horizontal lineto - Draws a horizontal line (relative)
543                        path.rLineTo(val[k + 0], 0);
544                        currentX += val[k + 0];
545                        break;
546                    case 'H': // horizontal lineto - Draws a horizontal line
547                        path.lineTo(val[k + 0], currentY);
548                        currentX = val[k + 0];
549                        break;
550                    case 'v': // vertical lineto - Draws a vertical line from the current point (r)
551                        path.rLineTo(0, val[k + 0]);
552                        currentY += val[k + 0];
553                        break;
554                    case 'V': // vertical lineto - Draws a vertical line from the current point
555                        path.lineTo(currentX, val[k + 0]);
556                        currentY = val[k + 0];
557                        break;
558                    case 'c': // curveto - Draws a cubic Bézier curve (relative)
559                        path.rCubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
560                                val[k + 4], val[k + 5]);
561
562                        ctrlPointX = currentX + val[k + 2];
563                        ctrlPointY = currentY + val[k + 3];
564                        currentX += val[k + 4];
565                        currentY += val[k + 5];
566
567                        break;
568                    case 'C': // curveto - Draws a cubic Bézier curve
569                        path.cubicTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3],
570                                val[k + 4], val[k + 5]);
571                        currentX = val[k + 4];
572                        currentY = val[k + 5];
573                        ctrlPointX = val[k + 2];
574                        ctrlPointY = val[k + 3];
575                        break;
576                    case 's': // smooth curveto - Draws a cubic Bézier curve (reflective cp)
577                        reflectiveCtrlPointX = 0;
578                        reflectiveCtrlPointY = 0;
579                        if (previousCmd == 'c' || previousCmd == 's'
580                                || previousCmd == 'C' || previousCmd == 'S') {
581                            reflectiveCtrlPointX = currentX - ctrlPointX;
582                            reflectiveCtrlPointY = currentY - ctrlPointY;
583                        }
584                        path.rCubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
585                                val[k + 0], val[k + 1],
586                                val[k + 2], val[k + 3]);
587
588                        ctrlPointX = currentX + val[k + 0];
589                        ctrlPointY = currentY + val[k + 1];
590                        currentX += val[k + 2];
591                        currentY += val[k + 3];
592                        break;
593                    case 'S': // shorthand/smooth curveto Draws a cubic Bézier curve(reflective cp)
594                        reflectiveCtrlPointX = currentX;
595                        reflectiveCtrlPointY = currentY;
596                        if (previousCmd == 'c' || previousCmd == 's'
597                                || previousCmd == 'C' || previousCmd == 'S') {
598                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
599                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
600                        }
601                        path.cubicTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
602                                val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
603                        ctrlPointX = val[k + 0];
604                        ctrlPointY = val[k + 1];
605                        currentX = val[k + 2];
606                        currentY = val[k + 3];
607                        break;
608                    case 'q': // Draws a quadratic Bézier (relative)
609                        path.rQuadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
610                        ctrlPointX = currentX + val[k + 0];
611                        ctrlPointY = currentY + val[k + 1];
612                        currentX += val[k + 2];
613                        currentY += val[k + 3];
614                        break;
615                    case 'Q': // Draws a quadratic Bézier
616                        path.quadTo(val[k + 0], val[k + 1], val[k + 2], val[k + 3]);
617                        ctrlPointX = val[k + 0];
618                        ctrlPointY = val[k + 1];
619                        currentX = val[k + 2];
620                        currentY = val[k + 3];
621                        break;
622                    case 't': // Draws a quadratic Bézier curve(reflective control point)(relative)
623                        reflectiveCtrlPointX = 0;
624                        reflectiveCtrlPointY = 0;
625                        if (previousCmd == 'q' || previousCmd == 't'
626                                || previousCmd == 'Q' || previousCmd == 'T') {
627                            reflectiveCtrlPointX = currentX - ctrlPointX;
628                            reflectiveCtrlPointY = currentY - ctrlPointY;
629                        }
630                        path.rQuadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
631                                val[k + 0], val[k + 1]);
632                        ctrlPointX = currentX + reflectiveCtrlPointX;
633                        ctrlPointY = currentY + reflectiveCtrlPointY;
634                        currentX += val[k + 0];
635                        currentY += val[k + 1];
636                        break;
637                    case 'T': // Draws a quadratic Bézier curve (reflective control point)
638                        reflectiveCtrlPointX = currentX;
639                        reflectiveCtrlPointY = currentY;
640                        if (previousCmd == 'q' || previousCmd == 't'
641                                || previousCmd == 'Q' || previousCmd == 'T') {
642                            reflectiveCtrlPointX = 2 * currentX - ctrlPointX;
643                            reflectiveCtrlPointY = 2 * currentY - ctrlPointY;
644                        }
645                        path.quadTo(reflectiveCtrlPointX, reflectiveCtrlPointY,
646                                val[k + 0], val[k + 1]);
647                        ctrlPointX = reflectiveCtrlPointX;
648                        ctrlPointY = reflectiveCtrlPointY;
649                        currentX = val[k + 0];
650                        currentY = val[k + 1];
651                        break;
652                    case 'a': // Draws an elliptical arc
653                        // (rx ry x-axis-rotation large-arc-flag sweep-flag x y)
654                        drawArc(path,
655                                currentX,
656                                currentY,
657                                val[k + 5] + currentX,
658                                val[k + 6] + currentY,
659                                val[k + 0],
660                                val[k + 1],
661                                val[k + 2],
662                                val[k + 3] != 0,
663                                val[k + 4] != 0);
664                        currentX += val[k + 5];
665                        currentY += val[k + 6];
666                        ctrlPointX = currentX;
667                        ctrlPointY = currentY;
668                        break;
669                    case 'A': // Draws an elliptical arc
670                        drawArc(path,
671                                currentX,
672                                currentY,
673                                val[k + 5],
674                                val[k + 6],
675                                val[k + 0],
676                                val[k + 1],
677                                val[k + 2],
678                                val[k + 3] != 0,
679                                val[k + 4] != 0);
680                        currentX = val[k + 5];
681                        currentY = val[k + 6];
682                        ctrlPointX = currentX;
683                        ctrlPointY = currentY;
684                        break;
685                }
686                previousCmd = cmd;
687            }
688            current[0] = currentX;
689            current[1] = currentY;
690            current[2] = ctrlPointX;
691            current[3] = ctrlPointY;
692            current[4] = currentSegmentStartX;
693            current[5] = currentSegmentStartY;
694        }
695
696        private static void drawArc(@NonNull Path_Delegate p, float x0, float y0, float x1,
697                float y1, float a, float b, float theta, boolean isMoreThanHalf,
698                boolean isPositiveArc) {
699
700            LOGGER.log(Level.FINE, "(" + x0 + "," + y0 + ")-(" + x1 + "," + y1
701                    + ") {" + a + " " + b + "}");
702        /* Convert rotation angle from degrees to radians */
703            double thetaD = theta * Math.PI / 180.0f;
704        /* Pre-compute rotation matrix entries */
705            double cosTheta = Math.cos(thetaD);
706            double sinTheta = Math.sin(thetaD);
707        /* Transform (x0, y0) and (x1, y1) into unit space */
708        /* using (inverse) rotation, followed by (inverse) scale */
709            double x0p = (x0 * cosTheta + y0 * sinTheta) / a;
710            double y0p = (-x0 * sinTheta + y0 * cosTheta) / b;
711            double x1p = (x1 * cosTheta + y1 * sinTheta) / a;
712            double y1p = (-x1 * sinTheta + y1 * cosTheta) / b;
713            LOGGER.log(Level.FINE, "unit space (" + x0p + "," + y0p + ")-(" + x1p
714                    + "," + y1p + ")");
715        /* Compute differences and averages */
716            double dx = x0p - x1p;
717            double dy = y0p - y1p;
718            double xm = (x0p + x1p) / 2;
719            double ym = (y0p + y1p) / 2;
720        /* Solve for intersecting unit circles */
721            double dsq = dx * dx + dy * dy;
722            if (dsq == 0.0) {
723                LOGGER.log(Level.FINE, " Points are coincident");
724                return; /* Points are coincident */
725            }
726            double disc = 1.0 / dsq - 1.0 / 4.0;
727            if (disc < 0.0) {
728                LOGGER.log(Level.FINE, "Points are too far apart " + dsq);
729                float adjust = (float) (Math.sqrt(dsq) / 1.99999);
730                drawArc(p, x0, y0, x1, y1, a * adjust, b * adjust, theta,
731                        isMoreThanHalf, isPositiveArc);
732                return; /* Points are too far apart */
733            }
734            double s = Math.sqrt(disc);
735            double sdx = s * dx;
736            double sdy = s * dy;
737            double cx;
738            double cy;
739            if (isMoreThanHalf == isPositiveArc) {
740                cx = xm - sdy;
741                cy = ym + sdx;
742            } else {
743                cx = xm + sdy;
744                cy = ym - sdx;
745            }
746
747            double eta0 = Math.atan2((y0p - cy), (x0p - cx));
748            LOGGER.log(Level.FINE, "eta0 = Math.atan2( " + (y0p - cy) + " , "
749                    + (x0p - cx) + ") = " + Math.toDegrees(eta0));
750
751            double eta1 = Math.atan2((y1p - cy), (x1p - cx));
752            LOGGER.log(Level.FINE, "eta1 = Math.atan2( " + (y1p - cy) + " , "
753                    + (x1p - cx) + ") = " + Math.toDegrees(eta1));
754            double sweep = (eta1 - eta0);
755            if (isPositiveArc != (sweep >= 0)) {
756                if (sweep > 0) {
757                    sweep -= 2 * Math.PI;
758                } else {
759                    sweep += 2 * Math.PI;
760                }
761            }
762
763            cx *= a;
764            cy *= b;
765            double tcx = cx;
766            cx = cx * cosTheta - cy * sinTheta;
767            cy = tcx * sinTheta + cy * cosTheta;
768            LOGGER.log(
769                    Level.FINE,
770                    "cx, cy, a, b, x0, y0, thetaD, eta0, sweep = " + cx + " , "
771                            + cy + " , " + a + " , " + b + " , " + x0 + " , " + y0
772                            + " , " + Math.toDegrees(thetaD) + " , "
773                            + Math.toDegrees(eta0) + " , " + Math.toDegrees(sweep));
774
775            arcToBezier(p, cx, cy, a, b, x0, y0, thetaD, eta0, sweep);
776        }
777
778        /**
779         * Converts an arc to cubic Bezier segments and records them in p.
780         *
781         * @param p The target for the cubic Bezier segments
782         * @param cx The x coordinate center of the ellipse
783         * @param cy The y coordinate center of the ellipse
784         * @param a The radius of the ellipse in the horizontal direction
785         * @param b The radius of the ellipse in the vertical direction
786         * @param e1x E(eta1) x coordinate of the starting point of the arc
787         * @param e1y E(eta2) y coordinate of the starting point of the arc
788         * @param theta The angle that the ellipse bounding rectangle makes with the horizontal
789         * plane
790         * @param start The start angle of the arc on the ellipse
791         * @param sweep The angle (positive or negative) of the sweep of the arc on the ellipse
792         */
793        private static void arcToBezier(@NonNull Path_Delegate p, double cx, double cy, double a,
794                double b, double e1x, double e1y, double theta, double start,
795                double sweep) {
796            // Taken from equations at:
797            // http://spaceroots.org/documents/ellipse/node8.html
798            // and http://www.spaceroots.org/documents/ellipse/node22.html
799            // Maximum of 45 degrees per cubic Bezier segment
800            int numSegments = (int) Math.ceil(Math.abs(sweep * 4 / Math.PI));
801
802
803            double eta1 = start;
804            double cosTheta = Math.cos(theta);
805            double sinTheta = Math.sin(theta);
806            double cosEta1 = Math.cos(eta1);
807            double sinEta1 = Math.sin(eta1);
808            double ep1x = (-a * cosTheta * sinEta1) - (b * sinTheta * cosEta1);
809            double ep1y = (-a * sinTheta * sinEta1) + (b * cosTheta * cosEta1);
810
811            double anglePerSegment = sweep / numSegments;
812            for (int i = 0; i < numSegments; i++) {
813                double eta2 = eta1 + anglePerSegment;
814                double sinEta2 = Math.sin(eta2);
815                double cosEta2 = Math.cos(eta2);
816                double e2x = cx + (a * cosTheta * cosEta2)
817                        - (b * sinTheta * sinEta2);
818                double e2y = cy + (a * sinTheta * cosEta2)
819                        + (b * cosTheta * sinEta2);
820                double ep2x = -a * cosTheta * sinEta2 - b * sinTheta * cosEta2;
821                double ep2y = -a * sinTheta * sinEta2 + b * cosTheta * cosEta2;
822                double tanDiff2 = Math.tan((eta2 - eta1) / 2);
823                double alpha = Math.sin(eta2 - eta1)
824                        * (Math.sqrt(4 + (3 * tanDiff2 * tanDiff2)) - 1) / 3;
825                double q1x = e1x + alpha * ep1x;
826                double q1y = e1y + alpha * ep1y;
827                double q2x = e2x - alpha * ep2x;
828                double q2y = e2y - alpha * ep2y;
829
830                p.cubicTo((float) q1x,
831                        (float) q1y,
832                        (float) q2x,
833                        (float) q2y,
834                        (float) e2x,
835                        (float) e2y);
836                eta1 = eta2;
837                e1x = e2x;
838                e1y = e2y;
839                ep1x = ep2x;
840                ep1y = ep2y;
841            }
842        }
843    }
844}
845