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