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