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
9QT_BEGIN_NAMESPACE
10namespace QtVirtualKeyboard {
11
12HandwritingGestureRecognizer::HandwritingGestureRecognizer(QObject *parent) :
13 GestureRecognizer(parent),
14 m_dpi(96)
15{
16}
17
18void HandwritingGestureRecognizer::setDpi(int value)
19{
20 m_dpi = value >= 0 ? value : 96;
21}
22
23int HandwritingGestureRecognizer::dpi() const
24{
25 return m_dpi;
26}
27
28QVariantMap 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
180QT_END_NAMESPACE
181

source code of qtvirtualkeyboard/src/virtualkeyboard/handwritinggesturerecognizer.cpp