Home | History | Annotate | Download | only in util
      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 
     17 package com.android.layoutlib.bridge.util;
     18 
     19 import android.annotation.NonNull;
     20 
     21 import java.awt.geom.CubicCurve2D;
     22 import java.awt.geom.PathIterator;
     23 import java.awt.geom.Point2D;
     24 import java.awt.geom.QuadCurve2D;
     25 import java.util.ArrayList;
     26 
     27 import com.google.android.collect.Lists;
     28 
     29 /**
     30  * Class that returns iterators for a given path. These iterators are lightweight and can be reused
     31  * multiple times to iterate over the path.
     32  */
     33 public class CachedPathIteratorFactory {
     34     /*
     35      * A few conventions used in the code:
     36      * Coordinates or coords arrays store segment coordinates. They use the same format as
     37      * PathIterator#currentSegment coordinates array.
     38      * float arrays store always points where the first element is X and the second is Y.
     39      */
     40 
     41     // This governs how accurate the approximation of the Path is.
     42     private static final float PRECISION = 0.002f;
     43 
     44     private final int mWindingRule;
     45     private final int[] mTypes;
     46     private final float[][] mCoordinates;
     47     private final float[] mSegmentsLength;
     48     private final float mTotalLength;
     49 
     50     public CachedPathIteratorFactory(@NonNull PathIterator iterator) {
     51         mWindingRule = iterator.getWindingRule();
     52 
     53         ArrayList<Integer> typesArray = Lists.newArrayList();
     54         ArrayList<float[]> pointsArray = Lists.newArrayList();
     55         float[] points = new float[6];
     56         while (!iterator.isDone()) {
     57             int type = iterator.currentSegment(points);
     58             int nPoints = getNumberOfPoints(type) * 2; // 2 coordinates per point
     59 
     60             typesArray.add(type);
     61             float[] itemPoints = new float[nPoints];
     62             System.arraycopy(points, 0, itemPoints, 0, nPoints);
     63             pointsArray.add(itemPoints);
     64             iterator.next();
     65         }
     66 
     67         mTypes = new int[typesArray.size()];
     68         mCoordinates = new float[mTypes.length][];
     69         for (int i = 0; i < typesArray.size(); i++) {
     70             mTypes[i] = typesArray.get(i);
     71             mCoordinates[i] = pointsArray.get(i);
     72         }
     73 
     74         // Do measurement
     75         mSegmentsLength = new float[mTypes.length];
     76 
     77         // Curves that we can reuse to estimate segments length
     78         CubicCurve2D.Float cubicCurve = new CubicCurve2D.Float();
     79         QuadCurve2D.Float quadCurve = new QuadCurve2D.Float();
     80         float lastX = 0;
     81         float lastY = 0;
     82         float totalLength = 0;
     83         for (int i = 0; i < mTypes.length; i++) {
     84             switch (mTypes[i]) {
     85                 case PathIterator.SEG_CUBICTO:
     86                     cubicCurve.setCurve(lastX, lastY,
     87                             mCoordinates[i][0], mCoordinates[i][1], mCoordinates[i][2],
     88                             mCoordinates[i][3], lastX = mCoordinates[i][4],
     89                             lastY = mCoordinates[i][5]);
     90                     mSegmentsLength[i] =
     91                             getFlatPathLength(cubicCurve.getPathIterator(null, PRECISION));
     92                     break;
     93                 case PathIterator.SEG_QUADTO:
     94                     quadCurve.setCurve(lastX, lastY, mCoordinates[i][0], mCoordinates[i][1],
     95                             lastX = mCoordinates[i][2], lastY = mCoordinates[i][3]);
     96                     mSegmentsLength[i] =
     97                             getFlatPathLength(quadCurve.getPathIterator(null, PRECISION));
     98                     break;
     99                 case PathIterator.SEG_CLOSE:
    100                     mSegmentsLength[i] = (float) Point2D.distance(lastX, lastY,
    101                             lastX = mCoordinates[0][0],
    102                             lastY = mCoordinates[0][1]);
    103                     mCoordinates[i] = new float[2];
    104                     // We convert a SEG_CLOSE segment to a SEG_LINETO so we do not have to worry
    105                     // about this special case in the rest of the code.
    106                     mTypes[i] = PathIterator.SEG_LINETO;
    107                     mCoordinates[i][0] = mCoordinates[0][0];
    108                     mCoordinates[i][1] = mCoordinates[0][1];
    109                     break;
    110                 case PathIterator.SEG_MOVETO:
    111                     mSegmentsLength[i] = 0;
    112                     lastX = mCoordinates[i][0];
    113                     lastY = mCoordinates[i][1];
    114                     break;
    115                 case PathIterator.SEG_LINETO:
    116                     mSegmentsLength[i] = (float) Point2D.distance(lastX, lastY, mCoordinates[i][0],
    117                             mCoordinates[i][1]);
    118                     lastX = mCoordinates[i][0];
    119                     lastY = mCoordinates[i][1];
    120                 default:
    121             }
    122             totalLength += mSegmentsLength[i];
    123         }
    124 
    125         mTotalLength = totalLength;
    126     }
    127 
    128     private static void quadCurveSegment(float[] coords, float t0, float t1) {
    129         // Calculate X and Y at 0.5 (We'll use this to reconstruct the control point later)
    130         float mt = t0 + (t1 - t0) / 2;
    131         float mu = 1 - mt;
    132         float mx = mu * mu * coords[0] + 2 * mu * mt * coords[2] + mt * mt * coords[4];
    133         float my = mu * mu * coords[1] + 2 * mu * mt * coords[3] + mt * mt * coords[5];
    134 
    135         float u0 = 1 - t0;
    136         float u1 = 1 - t1;
    137 
    138         // coords at t0
    139         coords[0] = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0;
    140         coords[1] = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0;
    141 
    142         // coords at t1
    143         coords[4] = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1;
    144         coords[5] = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1;
    145 
    146         // estimated control point at t'=0.5
    147         coords[2] = 2 * mx - coords[0] / 2 - coords[4] / 2;
    148         coords[3] = 2 * my - coords[1] / 2 - coords[5] / 2;
    149     }
    150 
    151     private static void cubicCurveSegment(float[] coords, float t0, float t1) {
    152         // http://stackoverflow.com/questions/11703283/cubic-bezier-curve-segment
    153         float u0 = 1 - t0;
    154         float u1 = 1 - t1;
    155 
    156         // Calculate the points at t0 and t1 for the quadratic curves formed by (P0, P1, P2) and
    157         // (P1, P2, P3)
    158         float qxa = coords[0] * u0 * u0 + coords[2] * 2 * t0 * u0 + coords[4] * t0 * t0;
    159         float qxb = coords[0] * u1 * u1 + coords[2] * 2 * t1 * u1 + coords[4] * t1 * t1;
    160         float qxc = coords[2] * u0 * u0 + coords[4] * 2 * t0 * u0 + coords[6] * t0 * t0;
    161         float qxd = coords[2] * u1 * u1 + coords[4] * 2 * t1 * u1 + coords[6] * t1 * t1;
    162 
    163         float qya = coords[1] * u0 * u0 + coords[3] * 2 * t0 * u0 + coords[5] * t0 * t0;
    164         float qyb = coords[1] * u1 * u1 + coords[3] * 2 * t1 * u1 + coords[5] * t1 * t1;
    165         float qyc = coords[3] * u0 * u0 + coords[5] * 2 * t0 * u0 + coords[7] * t0 * t0;
    166         float qyd = coords[3] * u1 * u1 + coords[5] * 2 * t1 * u1 + coords[7] * t1 * t1;
    167 
    168         // Linear interpolation
    169         coords[0] = qxa * u0 + qxc * t0;
    170         coords[1] = qya * u0 + qyc * t0;
    171 
    172         coords[2] = qxa * u1 + qxc * t1;
    173         coords[3] = qya * u1 + qyc * t1;
    174 
    175         coords[4] = qxb * u0 + qxd * t0;
    176         coords[5] = qyb * u0 + qyd * t0;
    177 
    178         coords[6] = qxb * u1 + qxd * t1;
    179         coords[7] = qyb * u1 + qyd * t1;
    180     }
    181 
    182     /**
    183      * Returns the end point of a given segment
    184      *
    185      * @param type the segment type
    186      * @param coords the segment coordinates array
    187      * @param point the return array where the point will be stored
    188      */
    189     private static void getShapeEndPoint(int type, @NonNull float[] coords, @NonNull float[]
    190             point) {
    191         // start index of the end point for the segment type
    192         int pointIndex = (getNumberOfPoints(type) - 1) * 2;
    193         point[0] = coords[pointIndex];
    194         point[1] = coords[pointIndex + 1];
    195     }
    196 
    197     /**
    198      * Returns the number of points stored in a coordinates array for the given segment type.
    199      */
    200     private static int getNumberOfPoints(int segmentType) {
    201         switch (segmentType) {
    202             case PathIterator.SEG_QUADTO:
    203                 return 2;
    204             case PathIterator.SEG_CUBICTO:
    205                 return 3;
    206             case PathIterator.SEG_CLOSE:
    207                 return 0;
    208             default:
    209                 return 1;
    210         }
    211     }
    212 
    213     /**
    214      * Returns the estimated length of a flat path. If the passed path is not flat (i.e. contains a
    215      * segment that is not {@link PathIterator#SEG_CLOSE}, {@link PathIterator#SEG_MOVETO} or {@link
    216      * PathIterator#SEG_LINETO} this method will fail.
    217      */
    218     private static float getFlatPathLength(@NonNull PathIterator iterator) {
    219         float segment[] = new float[6];
    220         float totalLength = 0;
    221         float[] previousPoint = new float[2];
    222         boolean isFirstPoint = true;
    223 
    224         while (!iterator.isDone()) {
    225             int type = iterator.currentSegment(segment);
    226             assert type == PathIterator.SEG_LINETO || type == PathIterator.SEG_CLOSE || type ==
    227                     PathIterator.SEG_MOVETO;
    228 
    229             // MoveTo shouldn't affect the length
    230             if (!isFirstPoint && type != PathIterator.SEG_MOVETO) {
    231                 totalLength += Point2D.distance(previousPoint[0], previousPoint[1], segment[0],
    232                         segment[1]);
    233             } else {
    234                 isFirstPoint = false;
    235             }
    236             previousPoint[0] = segment[0];
    237             previousPoint[1] = segment[1];
    238             iterator.next();
    239         }
    240 
    241         return totalLength;
    242     }
    243 
    244     /**
    245      * Returns the estimated position along a path of the given length.
    246      */
    247     private void getPointAtLength(int type, @NonNull float[] coords, float lastX, float
    248             lastY, float t, @NonNull float[] point) {
    249         if (type == PathIterator.SEG_LINETO) {
    250             point[0] = lastX + (coords[0] - lastX) * t;
    251             point[1] = lastY + (coords[1] - lastY) * t;
    252             // Return here, since we do not need a shape to estimate
    253             return;
    254         }
    255 
    256         float[] curve = new float[8];
    257         int lastPointIndex = (getNumberOfPoints(type) - 1) * 2;
    258 
    259         System.arraycopy(coords, 0, curve, 2, coords.length);
    260         curve[0] = lastX;
    261         curve[1] = lastY;
    262         if (type == PathIterator.SEG_CUBICTO) {
    263             cubicCurveSegment(curve, 0f, t);
    264         } else {
    265             quadCurveSegment(curve, 0f, t);
    266         }
    267 
    268         point[0] = curve[2 + lastPointIndex];
    269         point[1] = curve[2 + lastPointIndex + 1];
    270     }
    271 
    272     public CachedPathIterator iterator() {
    273         return new CachedPathIterator();
    274     }
    275 
    276     /**
    277      * Class that allows us to iterate over a path multiple times
    278      */
    279     public class CachedPathIterator implements PathIterator {
    280         private int mNextIndex;
    281 
    282         /**
    283          * Current segment type.
    284          *
    285          * @see PathIterator
    286          */
    287         private int mCurrentType;
    288 
    289         /**
    290          * Stores the coordinates array of the current segment. The number of points stored depends
    291          * on the segment type.
    292          *
    293          * @see PathIterator
    294          */
    295         private float[] mCurrentCoords = new float[6];
    296         private float mCurrentSegmentLength;
    297 
    298         /**
    299          * Current segment length offset. When asking for the length of the current segment, the
    300          * length will be reduced by this amount. This is useful when we are only using portions of
    301          * the segment.
    302          *
    303          * @see #jumpToSegment(float)
    304          */
    305         private float mOffsetLength;
    306 
    307         /** Point where the current segment started */
    308         private float[] mLastPoint = new float[2];
    309         private boolean isIteratorDone;
    310 
    311         private CachedPathIterator() {
    312             next();
    313         }
    314 
    315         public float getCurrentSegmentLength() {
    316             return mCurrentSegmentLength;
    317         }
    318 
    319         @Override
    320         public int getWindingRule() {
    321             return mWindingRule;
    322         }
    323 
    324         @Override
    325         public boolean isDone() {
    326             return isIteratorDone;
    327         }
    328 
    329         @Override
    330         public void next() {
    331             if (mNextIndex >= mTypes.length) {
    332                 isIteratorDone = true;
    333                 return;
    334             }
    335 
    336             if (mNextIndex >= 1) {
    337                 // We've already called next() once so there is a previous segment in this path.
    338                 // We want to get the coordinates where the path ends.
    339                 getShapeEndPoint(mCurrentType, mCurrentCoords, mLastPoint);
    340             } else {
    341                 // This is the first segment, no previous point so initialize to 0, 0
    342                 mLastPoint[0] = mLastPoint[1] = 0f;
    343             }
    344             mCurrentType = mTypes[mNextIndex];
    345             mCurrentSegmentLength = mSegmentsLength[mNextIndex] - mOffsetLength;
    346 
    347             if (mOffsetLength > 0f && (mCurrentType == SEG_CUBICTO || mCurrentType == SEG_QUADTO)) {
    348                 // We need to skip part of the start of the current segment (because
    349                 // mOffsetLength > 0)
    350                 float[] points = new float[8];
    351 
    352                 if (mNextIndex < 1) {
    353                     points[0] = points[1] = 0f;
    354                 } else {
    355                     getShapeEndPoint(mTypes[mNextIndex - 1], mCoordinates[mNextIndex - 1], points);
    356                 }
    357 
    358                 System.arraycopy(mCoordinates[mNextIndex], 0, points, 2,
    359                         mCoordinates[mNextIndex].length);
    360                 float t0 = (mSegmentsLength[mNextIndex] - mCurrentSegmentLength) /
    361                         mSegmentsLength[mNextIndex];
    362                 if (mCurrentType == SEG_CUBICTO) {
    363                     cubicCurveSegment(points, t0, 1f);
    364                 } else {
    365                     quadCurveSegment(points, t0, 1f);
    366                 }
    367                 System.arraycopy(points, 2, mCurrentCoords, 0, mCoordinates[mNextIndex].length);
    368             } else {
    369                 System.arraycopy(mCoordinates[mNextIndex], 0, mCurrentCoords, 0,
    370                         mCoordinates[mNextIndex].length);
    371             }
    372 
    373             mOffsetLength = 0f;
    374             mNextIndex++;
    375         }
    376 
    377         @Override
    378         public int currentSegment(float[] coords) {
    379             System.arraycopy(mCurrentCoords, 0, coords, 0, getNumberOfPoints(mCurrentType) * 2);
    380             return mCurrentType;
    381         }
    382 
    383         @Override
    384         public int currentSegment(double[] coords) {
    385             throw new UnsupportedOperationException();
    386         }
    387 
    388         /**
    389          * Returns the point where the current segment ends
    390          */
    391         public void getCurrentSegmentEnd(float[] point) {
    392             point[0] = mLastPoint[0];
    393             point[1] = mLastPoint[1];
    394         }
    395 
    396         /**
    397          * Restarts the iterator and jumps all the segments of this path up to the length value.
    398          */
    399         public void jumpToSegment(float length) {
    400             isIteratorDone = false;
    401             if (length <= 0f) {
    402                 mNextIndex = 0;
    403                 return;
    404             }
    405 
    406             float accLength = 0;
    407             float lastPoint[] = new float[2];
    408             for (mNextIndex = 0; mNextIndex < mTypes.length; mNextIndex++) {
    409                 float segmentLength = mSegmentsLength[mNextIndex];
    410                 if (accLength + segmentLength >= length && mTypes[mNextIndex] != SEG_MOVETO) {
    411                     float[] estimatedPoint = new float[2];
    412                     getPointAtLength(mTypes[mNextIndex],
    413                             mCoordinates[mNextIndex], lastPoint[0], lastPoint[1],
    414                             (length - accLength) / segmentLength,
    415                             estimatedPoint);
    416 
    417                     // This segment makes us go further than length so we go back one step,
    418                     // set a moveto and offset the length of the next segment by the length
    419                     // of this segment that we've already used.
    420                     mCurrentType = PathIterator.SEG_MOVETO;
    421                     mCurrentCoords[0] = estimatedPoint[0];
    422                     mCurrentCoords[1] = estimatedPoint[1];
    423                     mCurrentSegmentLength = 0;
    424 
    425                     // We need to offset next path length to account for the segment we've just
    426                     // skipped.
    427                     mOffsetLength = length - accLength;
    428                     return;
    429                 }
    430                 accLength += segmentLength;
    431                 getShapeEndPoint(mTypes[mNextIndex], mCoordinates[mNextIndex], lastPoint);
    432             }
    433         }
    434 
    435         /**
    436          * Returns the current segment up to certain length. If the current segment is shorter than
    437          * length, then the whole segment is returned. The segment coordinates are copied into the
    438          * coords array.
    439          *
    440          * @return the segment type
    441          */
    442         public int currentSegment(@NonNull float[] coords, float length) {
    443             int type = currentSegment(coords);
    444             // If the length is greater than the current segment length, no need to find
    445             // the cut point. Same if this is a SEG_MOVETO.
    446             if (mCurrentSegmentLength <= length || type == SEG_MOVETO) {
    447                 return type;
    448             }
    449 
    450             float t = length / getCurrentSegmentLength();
    451 
    452             // We find at which offset the end point is located within the coords array and set
    453             // a new end point to cut the segment short
    454             switch (type) {
    455                 case SEG_CUBICTO:
    456                 case SEG_QUADTO:
    457                     float[] curve = new float[8];
    458                     curve[0] = mLastPoint[0];
    459                     curve[1] = mLastPoint[1];
    460                     System.arraycopy(coords, 0, curve, 2, coords.length);
    461                     if (type == SEG_CUBICTO) {
    462                         cubicCurveSegment(curve, 0f, t);
    463                     } else {
    464                         quadCurveSegment(curve, 0f, t);
    465                     }
    466                     System.arraycopy(curve, 2, coords, 0, coords.length);
    467                     break;
    468                 default:
    469                     float[] point = new float[2];
    470                     getPointAtLength(type, coords, mLastPoint[0], mLastPoint[1], t, point);
    471                     coords[0] = point[0];
    472                     coords[1] = point[1];
    473             }
    474 
    475             return type;
    476         }
    477 
    478         /**
    479          * Returns the total length of the path
    480          */
    481         public float getTotalLength() {
    482             return mTotalLength;
    483         }
    484     }
    485 }
    486