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 com.android.systemui.classifier;
18
19import android.view.MotionEvent;
20
21import java.util.ArrayList;
22import java.util.HashMap;
23import java.util.List;
24
25/**
26 * A classifier which calculates the variance of differences between successive angles in a stroke.
27 * For each stroke it keeps its last three points. If some successive points are the same, it
28 * ignores the repetitions. If a new point is added, the classifier calculates the angle between
29 * the last three points. After that, it calculates the difference between this angle and the
30 * previously calculated angle. Then it calculates the variance of the differences from a stroke.
31 * To the differences there is artificially added value 0.0 and the difference between the first
32 * angle and PI (angles are in radians). It helps with strokes which have few points and punishes
33 * more strokes which are not smooth.
34 *
35 * This classifier also tries to split the stroke into two parts in the place in which the biggest
36 * angle is. It calculates the angle variance of the two parts and sums them up. The reason the
37 * classifier is doing this, is because some human swipes at the beginning go for a moment in one
38 * direction and then they rapidly change direction for the rest of the stroke (like a tick). The
39 * final result is the minimum of angle variance of the whole stroke and the sum of angle variances
40 * of the two parts split up. The classifier tries the tick option only if the first part is
41 * shorter than the second part.
42 *
43 * Additionally, the classifier classifies the angles as left angles (those angles which value is
44 * in [0.0, PI - ANGLE_DEVIATION) interval), straight angles
45 * ([PI - ANGLE_DEVIATION, PI + ANGLE_DEVIATION] interval) and right angles
46 * ((PI + ANGLE_DEVIATION, 2 * PI) interval) and then calculates the percentage of angles which are
47 * in the same direction (straight angles can be left angels or right angles)
48 */
49public class AnglesClassifier extends StrokeClassifier {
50    private HashMap<Stroke, Data> mStrokeMap = new HashMap<>();
51
52    public AnglesClassifier(ClassifierData classifierData) {
53        mClassifierData = classifierData;
54    }
55
56    @Override
57    public String getTag() {
58        return "ANG";
59    }
60
61    @Override
62    public void onTouchEvent(MotionEvent event) {
63        int action = event.getActionMasked();
64
65        if (action == MotionEvent.ACTION_DOWN) {
66            mStrokeMap.clear();
67        }
68
69        for (int i = 0; i < event.getPointerCount(); i++) {
70            Stroke stroke = mClassifierData.getStroke(event.getPointerId(i));
71
72            if (mStrokeMap.get(stroke) == null) {
73                mStrokeMap.put(stroke, new Data());
74            }
75            mStrokeMap.get(stroke).addPoint(stroke.getPoints().get(stroke.getPoints().size() - 1));
76        }
77    }
78
79    @Override
80    public float getFalseTouchEvaluation(int type, Stroke stroke) {
81        Data data = mStrokeMap.get(stroke);
82        return AnglesVarianceEvaluator.evaluate(data.getAnglesVariance())
83                + AnglesPercentageEvaluator.evaluate(data.getAnglesPercentage());
84    }
85
86    private static class Data {
87        private final float ANGLE_DEVIATION = (float) Math.PI / 20.0f;
88
89        private List<Point> mLastThreePoints = new ArrayList<>();
90        private float mFirstAngleVariance;
91        private float mPreviousAngle;
92        private float mBiggestAngle;
93        private float mSumSquares;
94        private float mSecondSumSquares;
95        private float mSum;
96        private float mSecondSum;
97        private float mCount;
98        private float mSecondCount;
99        private float mFirstLength;
100        private float mLength;
101        private float mAnglesCount;
102        private float mLeftAngles;
103        private float mRightAngles;
104        private float mStraightAngles;
105
106        public Data() {
107            mFirstAngleVariance = 0.0f;
108            mPreviousAngle = (float) Math.PI;
109            mBiggestAngle = 0.0f;
110            mSumSquares = mSecondSumSquares = 0.0f;
111            mSum = mSecondSum = 0.0f;
112            mCount = mSecondCount = 1.0f;
113            mLength = mFirstLength = 0.0f;
114            mAnglesCount = mLeftAngles = mRightAngles = mStraightAngles = 0.0f;
115        }
116
117        public void addPoint(Point point) {
118            // Checking if the added point is different than the previously added point
119            // Repetitions are being ignored so that proper angles are calculated.
120            if (mLastThreePoints.isEmpty()
121                    || !mLastThreePoints.get(mLastThreePoints.size() - 1).equals(point)) {
122                if (!mLastThreePoints.isEmpty()) {
123                    mLength += mLastThreePoints.get(mLastThreePoints.size() - 1).dist(point);
124                }
125                mLastThreePoints.add(point);
126                if (mLastThreePoints.size() == 4) {
127                    mLastThreePoints.remove(0);
128
129                    float angle = mLastThreePoints.get(1).getAngle(mLastThreePoints.get(0),
130                            mLastThreePoints.get(2));
131
132                    mAnglesCount++;
133                    if (angle < Math.PI - ANGLE_DEVIATION) {
134                        mLeftAngles++;
135                    } else if (angle <= Math.PI + ANGLE_DEVIATION) {
136                        mStraightAngles++;
137                    } else {
138                        mRightAngles++;
139                    }
140
141                    float difference = angle - mPreviousAngle;
142
143                    // If this is the biggest angle of the stroke so then we save the value of
144                    // the angle variance so far and start to count the values for the angle
145                    // variance of the second part.
146                    if (mBiggestAngle < angle) {
147                        mBiggestAngle = angle;
148                        mFirstLength = mLength;
149                        mFirstAngleVariance = getAnglesVariance(mSumSquares, mSum, mCount);
150                        mSecondSumSquares = 0.0f;
151                        mSecondSum = 0.0f;
152                        mSecondCount = 1.0f;
153                    } else {
154                        mSecondSum += difference;
155                        mSecondSumSquares += difference * difference;
156                        mSecondCount += 1.0;
157                    }
158
159                    mSum += difference;
160                    mSumSquares += difference * difference;
161                    mCount += 1.0;
162                    mPreviousAngle = angle;
163                }
164            }
165        }
166
167        public float getAnglesVariance(float sumSquares, float sum, float count) {
168            return sumSquares / count - (sum / count) * (sum / count);
169        }
170
171        public float getAnglesVariance() {
172            float anglesVariance = getAnglesVariance(mSumSquares, mSum, mCount);
173            if (mFirstLength < mLength / 2f) {
174                anglesVariance = Math.min(anglesVariance, mFirstAngleVariance
175                        + getAnglesVariance(mSecondSumSquares, mSecondSum, mSecondCount));
176            }
177            return anglesVariance;
178        }
179
180        public float getAnglesPercentage() {
181            if (mAnglesCount == 0.0f) {
182                return 1.0f;
183            }
184            return (Math.max(mLeftAngles, mRightAngles) + mStraightAngles) / mAnglesCount;
185        }
186    }
187}