blob: 081b2e5b5e87d096e38ce4d26080b5a3cb804f90 [file] [log] [blame]
Dennis Kempin1a8a5be2013-06-18 11:00:02 -07001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
Harry Cutts0edf1572020-01-21 15:42:10 -08004
5from __future__ import print_function
6
Dennis Kempin1a8a5be2013-06-18 11:00:02 -07007import decimal
8import json
9import math
10
11class GestureLog(object):
12 """ Represents the gestures in an activity log.
13
14 The gesture log is a representation of an activity log as it is generated
15 by the replay tool or 'tpcontrol log'.
16 It converts all gestures into a list of events using the classes below.
17 To allow easier processing all events that belong together are merged into
18 gestures. For example all scroll events from one scroll motion on the touchpad
19 are merged to create a single scroll gesture.
20 - self.events will contain the list of events
21 - self.gestures the list of gestures.
22 """
23 def __init__(self, log):
24 decimal.setcontext(decimal.Context(prec=8))
25 self.raw = json.loads(log, parse_float=decimal.Decimal)
26 raw_events = filter(lambda e: e['type'] == 'gesture', self.raw['entries'])
27 self.events = self._ParseRawEvents(raw_events)
28 self.gestures = self._MergeGestures(self.events)
29
30 self.properties = self.raw['properties']
31 self.hwstates = filter(lambda e: e['type'] == 'hardwareState',
32 self.raw['entries'])
33 self.hwproperties = self.raw['hardwareProperties']
34
35 def _ParseRawEvents(self, gesture_list):
36 events = []
37
38 for gesture in gesture_list:
39 start_time = gesture['startTime']
40 end_time = gesture['endTime']
41
42 if gesture['gestureType'] == 'move':
43 events.append(MotionGesture(gesture['dx'], gesture['dy'], start_time,
44 end_time))
45
46 elif gesture['gestureType'] == 'buttonsChange':
47 if gesture['down'] != 0:
48 button = gesture['down']
49 events.append(ButtonDownGesture(button, start_time, end_time))
50 if gesture['up'] != 0:
51 button = gesture['up']
52 events.append(ButtonUpGesture(button, start_time, end_time))
53
54 elif gesture['gestureType'] == 'scroll':
55 events.append(ScrollGesture(gesture['dx'], gesture['dy'], start_time,
56 end_time))
57
58 elif gesture['gestureType'] == 'pinch':
59 events.append(PinchGesture(gesture['dz'], start_time, end_time))
60
61 elif gesture['gestureType'] == 'swipe':
62 events.append(SwipeGesture(gesture['dx'], gesture['dy'], start_time,
63 end_time))
64
65 elif gesture['gestureType'] == 'swipeLift':
66 events.append(SwipeLiftGesture(start_time, end_time))
67
Amirhossein Simjour1242bd12016-01-15 13:33:43 -050068 elif gesture['gestureType'] == 'fourFingerSwipe':
69 events.append(FourFingerSwipeGesture(gesture['dx'], gesture['dy'],
70 start_time, end_time))
71
72 elif gesture['gestureType'] == 'fourFingerSwipeLift':
73 events.append(FourFingerSwipeLiftGesture(start_time, end_time))
74
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070075 elif gesture['gestureType'] == 'fling':
76 if gesture['flingState'] == 1:
77 events.append(FlingStopGesture(start_time, end_time))
78 else:
79 events.append(FlingGesture(gesture['vx'], gesture['vy'], start_time,
80 end_time))
Dennis Kempin06a40b72013-07-17 14:10:06 -070081 elif gesture['gestureType'] == 'metrics':
82 # ignore
83 pass
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070084 else:
Harry Cutts0edf1572020-01-21 15:42:10 -080085 print('Unknown gesture:', repr(gesture))
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070086
87 return events
88
89 def _MergeGestures(self, event_list):
90 gestures = []
91 last_event_of_type = {}
92
93 for event in event_list:
94 # merge motion and scroll events into gestures
95 if (event.type == 'Motion' or event.type == 'Scroll' or
Amirhossein Simjour1242bd12016-01-15 13:33:43 -050096 event.type == 'Swipe' or event.type == 'Pinch' or
97 event.type == 'FourFingerSwipe'):
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070098 if event.type not in last_event_of_type:
99 last_event_of_type[event.type] = event
100 gestures.append(event)
101 else:
102 if float(event.start - last_event_of_type[event.type].end) < 0.1:
103 last_event_of_type[event.type].Append(event)
104 else:
105 last_event_of_type[event.type] = event
106 gestures.append(event)
107 else:
108 if event.type == 'ButtonUp' or event.type == 'ButtonDown':
109 last_event_of_type = {}
110 gestures.append(event)
111 return gestures
112
113
114class AxisGesture(object):
115 """ Generic gesture class to describe gestures with x/y or z axis. """
116
117 def __init__(self, dx, dy, dz, start, end):
118 """ Create a new instance describing a single axis event.
119
120 To describe a list of events that form a gesture use the Append method.
121 @param dx: movement in x coords
122 @param dy: movement in y coords
123 @param dz: movement in z coords
124 @param start: start timestamp
125 @param end: end timestamp
126 """
127 self.dx = math.fabs(dx)
128 self.dy = math.fabs(dy)
129 self.dz = math.fabs(dz)
130 self.start = float(start)
131 self.end = float(end)
132 self.segments = []
133
134 self.distance = math.sqrt(self.dx * self.dx + self.dy * self.dy +
135 self.dz * self.dz)
136 self.segments.append(self.distance)
137
138 def Append(self, motion):
139 """ Append an motion event to build a gesture. """
140 self.dx = self.dx + motion.dx
141 self.dy = self.dy + motion.dy
142 self.dz = self.dz + motion.dz
143 self.distance = self.distance + motion.distance
144 self.segments.append(motion.distance)
145 self.end = motion.end
146
Andrew de los Reyes84d34372014-07-11 15:28:00 -0700147 def Distance(self):
148 return self.distance
149
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700150 def Speed(self):
151 """ Average speed of motion in mm/s. """
152 if self.end > self.start:
153 return self.distance / (self.end - self.start)
154 else:
155 return float('+inf')
156
157 def Roughness(self):
158 """ Returns the roughness of this gesture.
159
160 The roughness measures the variability in the movement events. A continuous
161 stream of events with similar movement distances is considered to be smooth.
162 Choppy movement with a high variation in movement distances on the other
163 hand is considered as rough. i.e. a constant series of movement distances is
164 considered perfectly smooth and will result in a roughness of 0.
165 Whenever there are sudden changes, or very irregular movement distances
166 the roughness will increase.
167 """
168 # Each event in the gesture resulted in a movement distance. These distances
169 # are treated as a signal and high pass filtered. This results in a signal
170 # containing only high frequency changes in the distance, i.e. the rough
171 # parts.
172 # The squared average of this signal is used as a measure for the roughness.
173
174 # gaussian filter kernel with sigma=1:
175 # The kernel is calculated using this formula (with s=sigma):
176 # 1/sqrt(2*pi*s^2)*e^(-x^2/(2*s^2))
177 # The array can be recalculated with modified sigma by entering the
178 # following equation into http://www.wolframalpha.com/:
179 # 1/sqrt(2*pi*s^2)*e^(-[-2, -1, 0, 1, 2]^2/(2*s^2))
180 # (Replace s with the desired sigma value)
181 gaussian = [0.053991, 0.241971, 0.398942, 0.241971, 0.053991]
182
183 # normalize gaussian filter kernel
184 gsum = sum(gaussian)
185 gaussian = map(lambda g: g / gsum, gaussian)
186
187 # add padding to the front/end of the distances
188 segments = []
189 segments.append(self.segments[0])
190 segments.append(self.segments[0])
191 segments.extend(self.segments)
192 segments.append(self.segments[-1])
193 segments.append(self.segments[-1])
194
195 # low pass filter the distances
196 segments_lp = []
197 for i in range(2, len(segments) - 2):
198 v = segments[i - 2] * gaussian[0]
199 v = v + segments[i - 1] * gaussian[1]
200 v = v + segments[i ] * gaussian[2]
201 v = v + segments[i + 1] * gaussian[3]
202 v = v + segments[i + 2] * gaussian[4]
203 segments_lp.append(v)
204
205 # H_HP = 1 - H_LP
206 segments_hp = []
207 for i in range(0, len(self.segments)):
208 segments_hp.append(self.segments[i] - segments_lp[i])
209 # square signal and calculate squared average
210 segments_hp_sq = map(lambda v:v * v, segments_hp)
211 return math.sqrt(sum(segments_hp_sq) / len(segments_hp))
212
213 def __str__(self):
214 fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} z={4:.4g} r={5:.4g} s={6:.4g}'
215 return fstr.format(self.__class__.type, self.distance, self.dx,
216 self.dy, self.dz, self.Roughness(), self.Speed())
217
218 def __repr__(self):
219 return str(self)
220
221class MotionGesture(AxisGesture):
222 """ The motion gesture is only using the X and Y axis. """
223 type = 'Motion'
224
225 def __init__(self, dx, dy, start, end):
226 AxisGesture.__init__(self, dx, dy, 0, start, end)
227
228 def __str__(self):
229 fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} r={4:.4g} s={5:.4g}'
230 return fstr.format(self.__class__.type, self.distance, self.dx,
231 self.dy, self.Roughness(), self.Speed())
232
233class ScrollGesture(MotionGesture):
234 """ The scroll gesture is functionally the same as the MotionGesture """
235 type = 'Scroll'
236
237
238class PinchGesture(AxisGesture):
239 """ The pinch gesture is functionally the same as the MotionGesture.
240
Amirhossein Simjour9efd0e12016-04-21 13:16:00 -0400241 However only uses the dz variable to represent the zoom factor.
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700242 """
243 type = 'Pinch'
244
245 def __init__(self, dz, start, end):
246 AxisGesture.__init__(self, 0, 0, dz, start, end)
247
248 def __str__(self):
249 fstr = '{0} dz={1:.4g} r={2:.4g}'
250 return fstr.format(self.__class__.type, self.dz, self.Roughness())
251
Amirhossein Simjour9efd0e12016-04-21 13:16:00 -0400252 def Append(self, motion):
253 self.dx = self.dx + motion.dx
254 self.dy = self.dy + motion.dy
255 self.dz = self.dz * max(motion.dz, 1 / motion.dz)
256 self.distance = self.dz
257 self.segments.append(motion.distance)
258 self.end = motion.end
259
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700260
261class SwipeGesture(MotionGesture):
262 """ The swipe gesture is functionally the same as the MotionGesture """
263 type = 'Swipe'
264
265
Amirhossein Simjour1242bd12016-01-15 13:33:43 -0500266class FourFingerSwipeGesture(MotionGesture):
267 """ The FourFingerSwipe gesture is functionally the same as the
268 MotionGesture """
269 type = 'FourFingerSwipe'
270
271
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700272class FlingGesture(MotionGesture):
273 """ The scroll gesture is functionally the same as the MotionGesture """
274 type = 'Fling'
275
276
277class FlingStopGesture(object):
278 """ The FlingStop gesture only contains the start and end timestamp. """
279 type = 'FlingStop'
280
281 def __init__(self, start, end):
282 self.start = start
283 self.end = end
284
285 def __str__(self):
286 return self.__class__.type
287
288 def __repr__(self):
289 return str(self)
290
291
292class SwipeLiftGesture(object):
293 """ The SwipeLift gesture only contains the start and end timestamp. """
294 type = 'SwipeLift'
295
296 def __init__(self, start, end):
297 self.start = start
298 self.end = end
299
300 def __str__(self):
301 return self.__class__.type
302
303 def __repr__(self):
304 return str(self)
305
Amirhossein Simjour1242bd12016-01-15 13:33:43 -0500306class FourFingerSwipeLiftGesture(object):
307 """ The FourFingerSwipeLift gesture only contains the start and
308 end timestamp. """
309 type = 'FourFingerSwipeLift'
310
311 def __init__(self, start, end):
312 self.start = start
313 self.end = end
314
315 def __str__(self):
316 return self.__class__.type
317
318 def __repr__(self):
319 return str(self)
320
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700321
322class AbstractButtonGesture(object):
323 """ Abstract gesture for up and down button gestures.
324
325 As both button down and up gestures are functionally identical it has
326 been extracted to this class. The AbstractButtonGesture stores a button ID
327 next to the start and end time of the gesture.
328 """
329 type = 'Undefined'
330
331 def __init__(self, button, start, end):
332 self.button = button
333 self.start = start
334 self.end = end
335
336 def __str__(self):
337 return self.__class__.type + '(' + str(self.button) + ')'
338
339 def __repr__(self):
340 return str(self)
341
342
343class ButtonDownGesture(AbstractButtonGesture):
344 """ Functionally the same as AbstractButtonGesture """
345 type = 'ButtonDown'
346
347
348class ButtonUpGesture(AbstractButtonGesture):
349 """ Functionally the same as AbstractButtonGesture """
350 type = 'ButtonUp'