1/*
2 * Copyright (C) 2012 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 com.android.inputmethod.keyboard.internal;
18
19import android.content.res.TypedArray;
20
21import com.android.inputmethod.latin.R;
22import com.android.inputmethod.latin.utils.ResizableIntArray;
23
24public final class GestureStrokeWithPreviewPoints extends GestureStroke {
25    public static final int PREVIEW_CAPACITY = 256;
26
27    private final ResizableIntArray mPreviewEventTimes = new ResizableIntArray(PREVIEW_CAPACITY);
28    private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
29    private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
30
31    private final GestureStrokePreviewParams mPreviewParams;
32
33    private int mStrokeId;
34    private int mLastPreviewSize;
35    private final HermiteInterpolator mInterpolator = new HermiteInterpolator();
36    private int mLastInterpolatedPreviewIndex;
37
38    private int mLastX;
39    private int mLastY;
40    private double mDistanceFromLastSample;
41
42    public static final class GestureStrokePreviewParams {
43        public final double mMinSamplingDistance; // in pixel
44        public final double mMaxInterpolationAngularThreshold; // in radian
45        public final double mMaxInterpolationDistanceThreshold; // in pixel
46        public final int mMaxInterpolationSegments;
47
48        public static final GestureStrokePreviewParams DEFAULT = new GestureStrokePreviewParams();
49
50        private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree
51
52        private GestureStrokePreviewParams() {
53            mMinSamplingDistance = 0.0d;
54            mMaxInterpolationAngularThreshold =
55                    degreeToRadian(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD);
56            mMaxInterpolationDistanceThreshold = mMinSamplingDistance;
57            mMaxInterpolationSegments = 4;
58        }
59
60        private static double degreeToRadian(final int degree) {
61            return degree / 180.0d * Math.PI;
62        }
63
64        public GestureStrokePreviewParams(final TypedArray mainKeyboardViewAttr) {
65            mMinSamplingDistance = mainKeyboardViewAttr.getDimension(
66                    R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance,
67                    (float)DEFAULT.mMinSamplingDistance);
68            final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable
69                    .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0);
70            mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0)
71                    ? DEFAULT.mMaxInterpolationAngularThreshold
72                    : degreeToRadian(interpolationAngularDegree);
73            mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable
74                    .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold,
75                    (float)DEFAULT.mMaxInterpolationDistanceThreshold);
76            mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger(
77                    R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments,
78                    DEFAULT.mMaxInterpolationSegments);
79        }
80    }
81
82    public GestureStrokeWithPreviewPoints(final int pointerId,
83            final GestureStrokeParams strokeParams,
84            final GestureStrokePreviewParams previewParams) {
85        super(pointerId, strokeParams);
86        mPreviewParams = previewParams;
87    }
88
89    @Override
90    protected void reset() {
91        super.reset();
92        mStrokeId++;
93        mLastPreviewSize = 0;
94        mLastInterpolatedPreviewIndex = 0;
95        mPreviewEventTimes.setLength(0);
96        mPreviewXCoordinates.setLength(0);
97        mPreviewYCoordinates.setLength(0);
98    }
99
100    public int getGestureStrokeId() {
101        return mStrokeId;
102    }
103
104    private boolean needsSampling(final int x, final int y) {
105        mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY);
106        mLastX = x;
107        mLastY = y;
108        final boolean isDownEvent = (mPreviewEventTimes.getLength() == 0);
109        if (mDistanceFromLastSample >= mPreviewParams.mMinSamplingDistance || isDownEvent) {
110            mDistanceFromLastSample = 0.0d;
111            return true;
112        }
113        return false;
114    }
115
116    @Override
117    public boolean addPointOnKeyboard(final int x, final int y, final int time,
118            final boolean isMajorEvent) {
119        if (needsSampling(x, y)) {
120            mPreviewEventTimes.add(time);
121            mPreviewXCoordinates.add(x);
122            mPreviewYCoordinates.add(y);
123        }
124        return super.addPointOnKeyboard(x, y, time, isMajorEvent);
125
126    }
127
128    /**
129     * Append sampled preview points.
130     *
131     * @param eventTimes the event time array of gesture trail to be drawn.
132     * @param xCoords the x-coordinates array of gesture trail to be drawn.
133     * @param yCoords the y-coordinates array of gesture trail to be drawn.
134     * @param types the point types array of gesture trail. This is valid only when
135     * {@link GestureTrail#DEBUG_SHOW_POINTS} is true.
136     */
137    public void appendPreviewStroke(final ResizableIntArray eventTimes,
138            final ResizableIntArray xCoords, final ResizableIntArray yCoords,
139            final ResizableIntArray types) {
140        final int length = mPreviewEventTimes.getLength() - mLastPreviewSize;
141        if (length <= 0) {
142            return;
143        }
144        eventTimes.append(mPreviewEventTimes, mLastPreviewSize, length);
145        xCoords.append(mPreviewXCoordinates, mLastPreviewSize, length);
146        yCoords.append(mPreviewYCoordinates, mLastPreviewSize, length);
147        if (GestureTrail.DEBUG_SHOW_POINTS) {
148            types.fill(GestureTrail.POINT_TYPE_SAMPLED, types.getLength(), length);
149        }
150        mLastPreviewSize = mPreviewEventTimes.getLength();
151    }
152
153    /**
154     * Calculate interpolated points between the last interpolated point and the end of the trail.
155     * And return the start index of the last interpolated segment of input arrays because it
156     * may need to recalculate the interpolated points in the segment if further segments are
157     * added to this stroke.
158     *
159     * @param lastInterpolatedIndex the start index of the last interpolated segment of
160     *        <code>eventTimes</code>, <code>xCoords</code>, and <code>yCoords</code>.
161     * @param eventTimes the event time array of gesture trail to be drawn.
162     * @param xCoords the x-coordinates array of gesture trail to be drawn.
163     * @param yCoords the y-coordinates array of gesture trail to be drawn.
164     * @param types the point types array of gesture trail. This is valid only when
165     * {@link GestureTrail#DEBUG_SHOW_POINTS} is true.
166     * @return the start index of the last interpolated segment of input arrays.
167     */
168    public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex,
169            final ResizableIntArray eventTimes, final ResizableIntArray xCoords,
170            final ResizableIntArray yCoords, final ResizableIntArray types) {
171        final int size = mPreviewEventTimes.getLength();
172        final int[] pt = mPreviewEventTimes.getPrimitiveArray();
173        final int[] px = mPreviewXCoordinates.getPrimitiveArray();
174        final int[] py = mPreviewYCoordinates.getPrimitiveArray();
175        mInterpolator.reset(px, py, 0, size);
176        // The last segment of gesture stroke needs to be interpolated again because the slope of
177        // the tangent at the last point isn't determined.
178        int lastInterpolatedDrawIndex = lastInterpolatedIndex;
179        int d1 = lastInterpolatedIndex;
180        for (int p2 = mLastInterpolatedPreviewIndex + 1; p2 < size; p2++) {
181            final int p1 = p2 - 1;
182            final int p0 = p1 - 1;
183            final int p3 = p2 + 1;
184            mLastInterpolatedPreviewIndex = p1;
185            lastInterpolatedDrawIndex = d1;
186            mInterpolator.setInterval(p0, p1, p2, p3);
187            final double m1 = Math.atan2(mInterpolator.mSlope1Y, mInterpolator.mSlope1X);
188            final double m2 = Math.atan2(mInterpolator.mSlope2Y, mInterpolator.mSlope2X);
189            final double deltaAngle = Math.abs(angularDiff(m2, m1));
190            final int segmentsByAngle = (int)Math.ceil(
191                    deltaAngle / mPreviewParams.mMaxInterpolationAngularThreshold);
192            final double deltaDistance = Math.hypot(mInterpolator.mP1X - mInterpolator.mP2X,
193                    mInterpolator.mP1Y - mInterpolator.mP2Y);
194            final int segmentsByDistance = (int)Math.ceil(deltaDistance
195                    / mPreviewParams.mMaxInterpolationDistanceThreshold);
196            final int segments = Math.min(mPreviewParams.mMaxInterpolationSegments,
197                    Math.max(segmentsByAngle, segmentsByDistance));
198            final int t1 = eventTimes.get(d1);
199            final int dt = pt[p2] - pt[p1];
200            d1++;
201            for (int i = 1; i < segments; i++) {
202                final float t = i / (float)segments;
203                mInterpolator.interpolate(t);
204                eventTimes.add(d1, (int)(dt * t) + t1);
205                xCoords.add(d1, (int)mInterpolator.mInterpolatedX);
206                yCoords.add(d1, (int)mInterpolator.mInterpolatedY);
207                if (GestureTrail.DEBUG_SHOW_POINTS) {
208                    types.add(d1, GestureTrail.POINT_TYPE_INTERPOLATED);
209                }
210                d1++;
211            }
212            eventTimes.add(d1, pt[p2]);
213            xCoords.add(d1, px[p2]);
214            yCoords.add(d1, py[p2]);
215            if (GestureTrail.DEBUG_SHOW_POINTS) {
216                types.add(d1, GestureTrail.POINT_TYPE_SAMPLED);
217            }
218        }
219        return lastInterpolatedDrawIndex;
220    }
221
222    private static final double TWO_PI = Math.PI * 2.0d;
223
224    /**
225     * Calculate the angular of rotation from <code>a0</code> to <code>a1</code>.
226     *
227     * @param a1 the angular to which the rotation ends.
228     * @param a0 the angular from which the rotation starts.
229     * @return the angular rotation value from a0 to a1, normalized to [-PI, +PI].
230     */
231    private static double angularDiff(final double a1, final double a0) {
232        double deltaAngle = a1 - a0;
233        while (deltaAngle > Math.PI) {
234            deltaAngle -= TWO_PI;
235        }
236        while (deltaAngle < -Math.PI) {
237            deltaAngle += TWO_PI;
238        }
239        return deltaAngle;
240    }
241}
242