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