1 | // Copyright (C) 2017 The Qt Company Ltd. |
---|---|
2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only |
3 | |
4 | #include <QtVirtualKeyboard/private/handwritinggesturerecognizer_p.h> |
5 | |
6 | #include <QtCore/qmath.h> |
7 | #include <QVector2D> |
8 | |
9 | QT_BEGIN_NAMESPACE |
10 | namespace QtVirtualKeyboard { |
11 | |
12 | HandwritingGestureRecognizer::HandwritingGestureRecognizer(QObject *parent) : |
13 | GestureRecognizer(parent), |
14 | m_dpi(96) |
15 | { |
16 | } |
17 | |
18 | void HandwritingGestureRecognizer::setDpi(int value) |
19 | { |
20 | m_dpi = value >= 0 ? value : 96; |
21 | } |
22 | |
23 | int HandwritingGestureRecognizer::dpi() const |
24 | { |
25 | return m_dpi; |
26 | } |
27 | |
28 | QVariantMap HandwritingGestureRecognizer::recognize(const QList<QVirtualKeyboardTrace *> traceList) |
29 | { |
30 | if (traceList.size() > 0 && traceList.size() < 3) { |
31 | |
32 | // Swipe gesture detection |
33 | // ======================= |
34 | // |
35 | // The following algorithm is based on the assumption that a |
36 | // vector composed of two arbitrary selected, but consecutive |
37 | // measuring points, and a vector composed of the first and last |
38 | // of the measuring points, are approximately in the same angle. |
39 | // |
40 | // If the measuring points are located very close to each other, |
41 | // the angle can fluctuate a lot. This has been taken into account |
42 | // by setting a minimum Euclidean distance between the measuring |
43 | // points. |
44 | // |
45 | |
46 | // Minimum euclidean distance of a segment (in millimeters) |
47 | static const int MINIMUM_EUCLIDEAN_DISTANCE = 8; |
48 | |
49 | // Maximum theta variance (in degrees) |
50 | static const qreal THETA_THRESHOLD = 25.0; |
51 | |
52 | // Maximum width variance in multitouch swipe (+- in percent) |
53 | static const int MAXIMUM_WIDTH_VARIANCE = 20; |
54 | |
55 | const qreal minimumEuclideanDistance = MINIMUM_EUCLIDEAN_DISTANCE / 25.4 * m_dpi; |
56 | static const qreal thetaThreshold = qDegreesToRadians(degrees: THETA_THRESHOLD); |
57 | |
58 | QList<QVector2D> swipeVectors; |
59 | |
60 | int traceIndex; |
61 | const int traceCount = traceList.size(); |
62 | for (traceIndex = 0; traceIndex < traceCount; ++traceIndex) { |
63 | |
64 | const QVirtualKeyboardTrace *trace = traceList.at(i: traceIndex); |
65 | const QVariantList &points = trace->points(); |
66 | QVector2D swipeVector; |
67 | const int pointCount = points.size(); |
68 | int pointIndex = 0; |
69 | if (pointCount >= 2) { |
70 | |
71 | QPointF startPosition = points.first().toPointF(); |
72 | swipeVector = QVector2D(points.last().toPointF() - startPosition); |
73 | const qreal swipeLength = swipeVector.length(); |
74 | |
75 | if (swipeLength >= minimumEuclideanDistance) { |
76 | |
77 | QPointF previousPosition = startPosition; |
78 | qreal euclideanDistance = 0; |
79 | for (pointIndex = 1; pointIndex < pointCount; ++pointIndex) { |
80 | |
81 | QPointF currentPosition(points.at(i: pointIndex).toPointF()); |
82 | |
83 | euclideanDistance += QVector2D(currentPosition - previousPosition).length(); |
84 | if (euclideanDistance >= minimumEuclideanDistance) { |
85 | |
86 | // Set the angle (theta) between the sample vector and the swipe vector |
87 | const QVector2D sampleVector(currentPosition - startPosition); |
88 | const qreal theta = qAcos(v: QVector2D::dotProduct(v1: swipeVector, v2: sampleVector) / (swipeLength * sampleVector.length())); |
89 | |
90 | // Rejected when theta above threshold |
91 | if (theta >= thetaThreshold) { |
92 | swipeVector = QVector2D(); |
93 | break; |
94 | } |
95 | |
96 | startPosition = currentPosition; |
97 | euclideanDistance = 0; |
98 | } |
99 | |
100 | previousPosition = currentPosition; |
101 | } |
102 | |
103 | if (pointIndex < pointCount) { |
104 | swipeVector = QVector2D(); |
105 | break; |
106 | } |
107 | |
108 | // Check to see if angle and length matches to existing touch points |
109 | if (!swipeVectors.isEmpty()) { |
110 | bool matchesToExisting = true; |
111 | const qreal minimumSwipeLength = (swipeLength * (100.0 - MAXIMUM_WIDTH_VARIANCE) / 100.0); |
112 | const qreal maximumSwipeLength = (swipeLength * (100.0 + MAXIMUM_WIDTH_VARIANCE) / 100.0); |
113 | for (const QVector2D &otherSwipeVector : std::as_const(t&: swipeVectors)) { |
114 | const qreal otherSwipeLength = otherSwipeVector.length(); |
115 | const qreal theta = qAcos(v: QVector2D::dotProduct(v1: swipeVector, v2: otherSwipeVector) / (swipeLength * otherSwipeLength)); |
116 | |
117 | if (theta >= thetaThreshold) { |
118 | matchesToExisting = false; |
119 | break; |
120 | } |
121 | |
122 | if (otherSwipeLength < minimumSwipeLength || otherSwipeLength > maximumSwipeLength) { |
123 | matchesToExisting = false; |
124 | break; |
125 | } |
126 | } |
127 | |
128 | if (!matchesToExisting) { |
129 | swipeVector = QVector2D(); |
130 | break; |
131 | } |
132 | } |
133 | } else { |
134 | swipeVector = QVector2D(); |
135 | } |
136 | } |
137 | |
138 | if (swipeVector.isNull()) |
139 | break; |
140 | |
141 | swipeVectors.append(t: swipeVector); |
142 | } |
143 | |
144 | if (swipeVectors.size() == traceCount) { |
145 | |
146 | QVariantMap swipeGesture; |
147 | |
148 | // Get swipe angle from the first vector: |
149 | // 0 degrees == right |
150 | // 90 degrees == down |
151 | // 180 degrees == left |
152 | // 270 degrees == up |
153 | QList<QVector2D>::ConstIterator swipeVector = swipeVectors.constBegin(); |
154 | qreal swipeLength = swipeVector->length(); |
155 | qreal swipeAngle = qAcos(v: swipeVector->x() / swipeLength); |
156 | if (swipeVector->y() < 0) |
157 | swipeAngle = 2 * M_PI - swipeAngle; |
158 | |
159 | // Calculate an average length of the vector |
160 | ++swipeVector; |
161 | for (const auto cend = swipeVectors.cend(); swipeVector != cend; ++swipeVector) |
162 | swipeLength += swipeVector->length(); |
163 | swipeLength /= traceCount; |
164 | |
165 | swipeGesture[QLatin1String("type")] = QLatin1String( "swipe"); |
166 | swipeGesture[QLatin1String("angle")] = swipeAngle; |
167 | swipeGesture[QLatin1String("angle_degrees")] = qRadiansToDegrees(radians: swipeAngle); |
168 | swipeGesture[QLatin1String("length")] = swipeLength; |
169 | swipeGesture[QLatin1String("length_mm")] = swipeLength / m_dpi * 25.4; |
170 | swipeGesture[QLatin1String("touch_count")] = traceCount; |
171 | |
172 | return swipeGesture; |
173 | } |
174 | } |
175 | |
176 | return QVariantMap(); |
177 | } |
178 | |
179 | } // namespace QtVirtualKeyboard |
180 | QT_END_NAMESPACE |
181 |