1/*
2 * Copyright (C) 2009 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.gesture;
18
19import android.graphics.Canvas;
20import android.graphics.Paint;
21import android.graphics.Path;
22import android.graphics.RectF;
23
24import java.io.IOException;
25import java.io.DataOutputStream;
26import java.io.DataInputStream;
27import java.util.ArrayList;
28
29/**
30 * A gesture stroke started on a touch down and ended on a touch up. A stroke
31 * consists of a sequence of timed points. One or multiple strokes form a gesture.
32 */
33public class GestureStroke {
34    static final float TOUCH_TOLERANCE = 3;
35
36    public final RectF boundingBox;
37
38    public final float length;
39    public final float[] points;
40
41    private final long[] timestamps;
42    private Path mCachedPath;
43
44    /**
45     * A constructor that constructs a gesture stroke from a list of gesture points.
46     *
47     * @param points
48     */
49    public GestureStroke(ArrayList<GesturePoint> points) {
50        final int count = points.size();
51        final float[] tmpPoints = new float[count * 2];
52        final long[] times = new long[count];
53
54        RectF bx = null;
55        float len = 0;
56        int index = 0;
57
58        for (int i = 0; i < count; i++) {
59            final GesturePoint p = points.get(i);
60            tmpPoints[i * 2] = p.x;
61            tmpPoints[i * 2 + 1] = p.y;
62            times[index] = p.timestamp;
63
64            if (bx == null) {
65                bx = new RectF();
66                bx.top = p.y;
67                bx.left = p.x;
68                bx.right = p.x;
69                bx.bottom = p.y;
70                len = 0;
71            } else {
72                len += Math.sqrt(Math.pow(p.x - tmpPoints[(i - 1) * 2], 2)
73                        + Math.pow(p.y - tmpPoints[(i -1 ) * 2 + 1], 2));
74                bx.union(p.x, p.y);
75            }
76            index++;
77        }
78
79        timestamps = times;
80        this.points = tmpPoints;
81        boundingBox = bx;
82        length = len;
83    }
84
85    /**
86     * A faster constructor specially for cloning a stroke.
87     */
88    private GestureStroke(RectF bbx, float len, float[] pts, long[] times) {
89        boundingBox = new RectF(bbx.left, bbx.top, bbx.right, bbx.bottom);
90        length = len;
91        points = pts.clone();
92        timestamps = times.clone();
93    }
94
95    @Override
96    public Object clone() {
97        return new GestureStroke(boundingBox, length, points, timestamps);
98    }
99
100    /**
101     * Draws the stroke with a given canvas and paint.
102     *
103     * @param canvas
104     */
105    void draw(Canvas canvas, Paint paint) {
106        if (mCachedPath == null) {
107            makePath();
108        }
109
110        canvas.drawPath(mCachedPath, paint);
111    }
112
113    public Path getPath() {
114        if (mCachedPath == null) {
115            makePath();
116        }
117
118        return mCachedPath;
119    }
120
121    private void makePath() {
122        final float[] localPoints = points;
123        final int count = localPoints.length;
124
125        Path path = null;
126
127        float mX = 0;
128        float mY = 0;
129
130        for (int i = 0; i < count; i += 2) {
131            float x = localPoints[i];
132            float y = localPoints[i + 1];
133            if (path == null) {
134                path = new Path();
135                path.moveTo(x, y);
136                mX = x;
137                mY = y;
138            } else {
139                float dx = Math.abs(x - mX);
140                float dy = Math.abs(y - mY);
141                if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
142                    path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
143                    mX = x;
144                    mY = y;
145                }
146            }
147        }
148
149        mCachedPath = path;
150    }
151
152    /**
153     * Converts the stroke to a Path of a given number of points.
154     *
155     * @param width the width of the bounding box of the target path
156     * @param height the height of the bounding box of the target path
157     * @param numSample the number of points needed
158     *
159     * @return the path
160     */
161    public Path toPath(float width, float height, int numSample) {
162        final float[] pts = GestureUtils.temporalSampling(this, numSample);
163        final RectF rect = boundingBox;
164
165        GestureUtils.translate(pts, -rect.left, -rect.top);
166
167        float sx = width / rect.width();
168        float sy = height / rect.height();
169        float scale = sx > sy ? sy : sx;
170        GestureUtils.scale(pts, scale, scale);
171
172        float mX = 0;
173        float mY = 0;
174
175        Path path = null;
176
177        final int count = pts.length;
178
179        for (int i = 0; i < count; i += 2) {
180            float x = pts[i];
181            float y = pts[i + 1];
182            if (path == null) {
183                path = new Path();
184                path.moveTo(x, y);
185                mX = x;
186                mY = y;
187            } else {
188                float dx = Math.abs(x - mX);
189                float dy = Math.abs(y - mY);
190                if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
191                    path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
192                    mX = x;
193                    mY = y;
194                }
195            }
196        }
197
198        return path;
199    }
200
201    void serialize(DataOutputStream out) throws IOException {
202        final float[] pts = points;
203        final long[] times = timestamps;
204        final int count = points.length;
205
206        // Write number of points
207        out.writeInt(count / 2);
208
209        for (int i = 0; i < count; i += 2) {
210            // Write X
211            out.writeFloat(pts[i]);
212            // Write Y
213            out.writeFloat(pts[i + 1]);
214            // Write timestamp
215            out.writeLong(times[i / 2]);
216        }
217    }
218
219    static GestureStroke deserialize(DataInputStream in) throws IOException {
220        // Number of points
221        final int count = in.readInt();
222
223        final ArrayList<GesturePoint> points = new ArrayList<GesturePoint>(count);
224        for (int i = 0; i < count; i++) {
225            points.add(GesturePoint.deserialize(in));
226        }
227
228        return new GestureStroke(points);
229    }
230
231    /**
232     * Invalidates the cached path that is used to render the stroke.
233     */
234    public void clearPath() {
235        if (mCachedPath != null) mCachedPath.rewind();
236    }
237
238    /**
239     * Computes an oriented bounding box of the stroke.
240     *
241     * @return OrientedBoundingBox
242     */
243    public OrientedBoundingBox computeOrientedBoundingBox() {
244        return GestureUtils.computeOrientedBoundingBox(points);
245    }
246}
247