blob: 5d2a42a3b00e43b1794310ce2aee886a708f072e [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
Andrew de los Reyes84d34372014-07-11 15:28:00 -0700137 def Distance(self):
138 return self.distance
139
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700140 def Speed(self):
141 """ Average speed of motion in mm/s. """
142 if self.end > self.start:
143 return self.distance / (self.end - self.start)
144 else:
145 return float('+inf')
146
147 def Roughness(self):
148 """ Returns the roughness of this gesture.
149
150 The roughness measures the variability in the movement events. A continuous
151 stream of events with similar movement distances is considered to be smooth.
152 Choppy movement with a high variation in movement distances on the other
153 hand is considered as rough. i.e. a constant series of movement distances is
154 considered perfectly smooth and will result in a roughness of 0.
155 Whenever there are sudden changes, or very irregular movement distances
156 the roughness will increase.
157 """
158 # Each event in the gesture resulted in a movement distance. These distances
159 # are treated as a signal and high pass filtered. This results in a signal
160 # containing only high frequency changes in the distance, i.e. the rough
161 # parts.
162 # The squared average of this signal is used as a measure for the roughness.
163
164 # gaussian filter kernel with sigma=1:
165 # The kernel is calculated using this formula (with s=sigma):
166 # 1/sqrt(2*pi*s^2)*e^(-x^2/(2*s^2))
167 # The array can be recalculated with modified sigma by entering the
168 # following equation into http://www.wolframalpha.com/:
169 # 1/sqrt(2*pi*s^2)*e^(-[-2, -1, 0, 1, 2]^2/(2*s^2))
170 # (Replace s with the desired sigma value)
171 gaussian = [0.053991, 0.241971, 0.398942, 0.241971, 0.053991]
172
173 # normalize gaussian filter kernel
174 gsum = sum(gaussian)
175 gaussian = map(lambda g: g / gsum, gaussian)
176
177 # add padding to the front/end of the distances
178 segments = []
179 segments.append(self.segments[0])
180 segments.append(self.segments[0])
181 segments.extend(self.segments)
182 segments.append(self.segments[-1])
183 segments.append(self.segments[-1])
184
185 # low pass filter the distances
186 segments_lp = []
187 for i in range(2, len(segments) - 2):
188 v = segments[i - 2] * gaussian[0]
189 v = v + segments[i - 1] * gaussian[1]
190 v = v + segments[i ] * gaussian[2]
191 v = v + segments[i + 1] * gaussian[3]
192 v = v + segments[i + 2] * gaussian[4]
193 segments_lp.append(v)
194
195 # H_HP = 1 - H_LP
196 segments_hp = []
197 for i in range(0, len(self.segments)):
198 segments_hp.append(self.segments[i] - segments_lp[i])
199 # square signal and calculate squared average
200 segments_hp_sq = map(lambda v:v * v, segments_hp)
201 return math.sqrt(sum(segments_hp_sq) / len(segments_hp))
202
203 def __str__(self):
204 fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} z={4:.4g} r={5:.4g} s={6:.4g}'
205 return fstr.format(self.__class__.type, self.distance, self.dx,
206 self.dy, self.dz, self.Roughness(), self.Speed())
207
208 def __repr__(self):
209 return str(self)
210
211class MotionGesture(AxisGesture):
212 """ The motion gesture is only using the X and Y axis. """
213 type = 'Motion'
214
215 def __init__(self, dx, dy, start, end):
216 AxisGesture.__init__(self, dx, dy, 0, start, end)
217
218 def __str__(self):
219 fstr = '{0} d={1:.4g} x={2:.4g} y={3:.4g} r={4:.4g} s={5:.4g}'
220 return fstr.format(self.__class__.type, self.distance, self.dx,
221 self.dy, self.Roughness(), self.Speed())
222
223class ScrollGesture(MotionGesture):
224 """ The scroll gesture is functionally the same as the MotionGesture """
225 type = 'Scroll'
226
227
228class PinchGesture(AxisGesture):
229 """ The pinch gesture is functionally the same as the MotionGesture.
230
231 However only uses the dx variable to represent the zoom factor.
232 """
233 type = 'Pinch'
234
235 def __init__(self, dz, start, end):
236 AxisGesture.__init__(self, 0, 0, dz, start, end)
237
238 def __str__(self):
239 fstr = '{0} dz={1:.4g} r={2:.4g}'
240 return fstr.format(self.__class__.type, self.dz, self.Roughness())
241
242
243class SwipeGesture(MotionGesture):
244 """ The swipe gesture is functionally the same as the MotionGesture """
245 type = 'Swipe'
246
247
248class FlingGesture(MotionGesture):
249 """ The scroll gesture is functionally the same as the MotionGesture """
250 type = 'Fling'
251
252
253class FlingStopGesture(object):
254 """ The FlingStop gesture only contains the start and end timestamp. """
255 type = 'FlingStop'
256
257 def __init__(self, start, end):
258 self.start = start
259 self.end = end
260
261 def __str__(self):
262 return self.__class__.type
263
264 def __repr__(self):
265 return str(self)
266
267
268class SwipeLiftGesture(object):
269 """ The SwipeLift gesture only contains the start and end timestamp. """
270 type = 'SwipeLift'
271
272 def __init__(self, start, end):
273 self.start = start
274 self.end = end
275
276 def __str__(self):
277 return self.__class__.type
278
279 def __repr__(self):
280 return str(self)
281
282
283class AbstractButtonGesture(object):
284 """ Abstract gesture for up and down button gestures.
285
286 As both button down and up gestures are functionally identical it has
287 been extracted to this class. The AbstractButtonGesture stores a button ID
288 next to the start and end time of the gesture.
289 """
290 type = 'Undefined'
291
292 def __init__(self, button, start, end):
293 self.button = button
294 self.start = start
295 self.end = end
296
297 def __str__(self):
298 return self.__class__.type + '(' + str(self.button) + ')'
299
300 def __repr__(self):
301 return str(self)
302
303
304class ButtonDownGesture(AbstractButtonGesture):
305 """ Functionally the same as AbstractButtonGesture """
306 type = 'ButtonDown'
307
308
309class ButtonUpGesture(AbstractButtonGesture):
310 """ Functionally the same as AbstractButtonGesture """
311 type = 'ButtonUp'