blob: 12939d91cc2881cc18823fc5b56412da8d3b40a4 [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
66 elif gesture['gestureType'] == 'fling':
67 if gesture['flingState'] == 1:
68 events.append(FlingStopGesture(start_time, end_time))
69 else:
70 events.append(FlingGesture(gesture['vx'], gesture['vy'], start_time,
71 end_time))
Dennis Kempin06a40b72013-07-17 14:10:06 -070072 elif gesture['gestureType'] == 'metrics':
73 # ignore
74 pass
Dennis Kempin1a8a5be2013-06-18 11:00:02 -070075 else:
76 print 'Unknown gesture:', repr(gesture)
77
78 return events
79
80 def _MergeGestures(self, event_list):
81 gestures = []
82 last_event_of_type = {}
83
84 for event in event_list:
85 # merge motion and scroll events into gestures
86 if (event.type == 'Motion' or event.type == 'Scroll' or
87 event.type == 'Swipe' or event.type == 'Pinch'):
88 if event.type not in last_event_of_type:
89 last_event_of_type[event.type] = event
90 gestures.append(event)
91 else:
92 if float(event.start - last_event_of_type[event.type].end) < 0.1:
93 last_event_of_type[event.type].Append(event)
94 else:
95 last_event_of_type[event.type] = event
96 gestures.append(event)
97 else:
98 if event.type == 'ButtonUp' or event.type == 'ButtonDown':
99 last_event_of_type = {}
100 gestures.append(event)
101 return gestures
102
103
104class AxisGesture(object):
105 """ Generic gesture class to describe gestures with x/y or z axis. """
106
107 def __init__(self, dx, dy, dz, start, end):
108 """ Create a new instance describing a single axis event.
109
110 To describe a list of events that form a gesture use the Append method.
111 @param dx: movement in x coords
112 @param dy: movement in y coords
113 @param dz: movement in z coords
114 @param start: start timestamp
115 @param end: end timestamp
116 """
117 self.dx = math.fabs(dx)
118 self.dy = math.fabs(dy)
119 self.dz = math.fabs(dz)
120 self.start = float(start)
121 self.end = float(end)
122 self.segments = []
123
124 self.distance = math.sqrt(self.dx * self.dx + self.dy * self.dy +
125 self.dz * self.dz)
126 self.segments.append(self.distance)
127
128 def Append(self, motion):
129 """ Append an motion event to build a gesture. """
130 self.dx = self.dx + motion.dx
131 self.dy = self.dy + motion.dy
132 self.dz = self.dz + motion.dz
133 self.distance = self.distance + motion.distance
134 self.segments.append(motion.distance)
135 self.end = motion.end
136
137 def Speed(self):
138 """ Average speed of motion in mm/s. """
139 if self.end > self.start:
140 return self.distance / (self.end - self.start)
141 else:
142 return float('+inf')
143
144 def Roughness(self):
145 """ Returns the roughness of this gesture.
146
147 The roughness measures the variability in the movement events. A continuous
148 stream of events with similar movement distances is considered to be smooth.
149 Choppy movement with a high variation in movement distances on the other
150 hand is considered as rough. i.e. a constant series of movement distances is
151 considered perfectly smooth and will result in a roughness of 0.
152 Whenever there are sudden changes, or very irregular movement distances
153 the roughness will increase.
154 """
155 # Each event in the gesture resulted in a movement distance. These distances
156 # are treated as a signal and high pass filtered. This results in a signal
157 # containing only high frequency changes in the distance, i.e. the rough
158 # parts.
159 # The squared average of this signal is used as a measure for the roughness.
160
161 # gaussian filter kernel with sigma=1:
162 # The kernel is calculated using this formula (with s=sigma):
163 # 1/sqrt(2*pi*s^2)*e^(-x^2/(2*s^2))
164 # The array can be recalculated with modified sigma by entering the
165 # following equation into http://www.wolframalpha.com/:
166 # 1/sqrt(2*pi*s^2)*e^(-[-2, -1, 0, 1, 2]^2/(2*s^2))
167 # (Replace s with the desired sigma value)
168 gaussian = [0.053991, 0.241971, 0.398942, 0.241971, 0.053991]
169
170 # normalize gaussian filter kernel
171 gsum = sum(gaussian)
172 gaussian = map(lambda g: g / gsum, gaussian)
173
174 # add padding to the front/end of the distances
175 segments = []
176 segments.append(self.segments[0])
177 segments.append(self.segments[0])
178 segments.extend(self.segments)
179 segments.append(self.segments[-1])
180 segments.append(self.segments[-1])
181
182 # low pass filter the distances
183 segments_lp = []
184 for i in range(2, len(segments) - 2):
185 v = segments[i - 2] * gaussian[0]
186 v = v + segments[i - 1] * gaussian[1]
187 v = v + segments[i ] * gaussian[2]
188 v = v + segments[i + 1] * gaussian[3]
189 v = v + segments[i + 2] * gaussian[4]
190 segments_lp.append(v)
191
192 # H_HP = 1 - H_LP
193 segments_hp = []
194 for i in range(0, len(self.segments)):
195 segments_hp.append(self.segments[i] - segments_lp[i])
196 # square signal and calculate squared average
197 segments_hp_sq = map(lambda v:v * v, segments_hp)
198 return math.sqrt(sum(segments_hp_sq) / len(segments_hp))
199
200 def __str__(self):
201 fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} z={4:.4g} r={5:.4g} s={6:.4g}'
202 return fstr.format(self.__class__.type, self.distance, self.dx,
203 self.dy, self.dz, self.Roughness(), self.Speed())
204
205 def __repr__(self):
206 return str(self)
207
208class MotionGesture(AxisGesture):
209 """ The motion gesture is only using the X and Y axis. """
210 type = 'Motion'
211
212 def __init__(self, dx, dy, start, end):
213 AxisGesture.__init__(self, dx, dy, 0, start, end)
214
215 def __str__(self):
216 fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} r={4:.4g} s={5:.4g}'
217 return fstr.format(self.__class__.type, self.distance, self.dx,
218 self.dy, self.Roughness(), self.Speed())
219
220class ScrollGesture(MotionGesture):
221 """ The scroll gesture is functionally the same as the MotionGesture """
222 type = 'Scroll'
223
224
225class PinchGesture(AxisGesture):
226 """ The pinch gesture is functionally the same as the MotionGesture.
227
228 However only uses the dx variable to represent the zoom factor.
229 """
230 type = 'Pinch'
231
232 def __init__(self, dz, start, end):
233 AxisGesture.__init__(self, 0, 0, dz, start, end)
234
235 def __str__(self):
236 fstr = '{0} dz={1:.4g} r={2:.4g}'
237 return fstr.format(self.__class__.type, self.dz, self.Roughness())
238
239
240class SwipeGesture(MotionGesture):
241 """ The swipe gesture is functionally the same as the MotionGesture """
242 type = 'Swipe'
243
244
245class FlingGesture(MotionGesture):
246 """ The scroll gesture is functionally the same as the MotionGesture """
247 type = 'Fling'
248
249
250class FlingStopGesture(object):
251 """ The FlingStop gesture only contains the start and end timestamp. """
252 type = 'FlingStop'
253
254 def __init__(self, start, end):
255 self.start = start
256 self.end = end
257
258 def __str__(self):
259 return self.__class__.type
260
261 def __repr__(self):
262 return str(self)
263
264
265class SwipeLiftGesture(object):
266 """ The SwipeLift gesture only contains the start and end timestamp. """
267 type = 'SwipeLift'
268
269 def __init__(self, start, end):
270 self.start = start
271 self.end = end
272
273 def __str__(self):
274 return self.__class__.type
275
276 def __repr__(self):
277 return str(self)
278
279
280class AbstractButtonGesture(object):
281 """ Abstract gesture for up and down button gestures.
282
283 As both button down and up gestures are functionally identical it has
284 been extracted to this class. The AbstractButtonGesture stores a button ID
285 next to the start and end time of the gesture.
286 """
287 type = 'Undefined'
288
289 def __init__(self, button, start, end):
290 self.button = button
291 self.start = start
292 self.end = end
293
294 def __str__(self):
295 return self.__class__.type + '(' + str(self.button) + ')'
296
297 def __repr__(self):
298 return str(self)
299
300
301class ButtonDownGesture(AbstractButtonGesture):
302 """ Functionally the same as AbstractButtonGesture """
303 type = 'ButtonDown'
304
305
306class ButtonUpGesture(AbstractButtonGesture):
307 """ Functionally the same as AbstractButtonGesture """
308 type = 'ButtonUp'