blob: fefa33687a24baf8de2c72c17d56033a827b7697 [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
239 However only uses the dx variable to represent the zoom factor.
240 """
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
250
251class SwipeGesture(MotionGesture):
252 """ The swipe gesture is functionally the same as the MotionGesture """
253 type = 'Swipe'
254
255
Amirhossein Simjour1242bd12016-01-15 13:33:43 -0500256class FourFingerSwipeGesture(MotionGesture):
257 """ The FourFingerSwipe gesture is functionally the same as the
258 MotionGesture """
259 type = 'FourFingerSwipe'
260
261
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700262class FlingGesture(MotionGesture):
263 """ The scroll gesture is functionally the same as the MotionGesture """
264 type = 'Fling'
265
266
267class FlingStopGesture(object):
268 """ The FlingStop gesture only contains the start and end timestamp. """
269 type = 'FlingStop'
270
271 def __init__(self, start, end):
272 self.start = start
273 self.end = end
274
275 def __str__(self):
276 return self.__class__.type
277
278 def __repr__(self):
279 return str(self)
280
281
282class SwipeLiftGesture(object):
283 """ The SwipeLift gesture only contains the start and end timestamp. """
284 type = 'SwipeLift'
285
286 def __init__(self, start, end):
287 self.start = start
288 self.end = end
289
290 def __str__(self):
291 return self.__class__.type
292
293 def __repr__(self):
294 return str(self)
295
Amirhossein Simjour1242bd12016-01-15 13:33:43 -0500296class FourFingerSwipeLiftGesture(object):
297 """ The FourFingerSwipeLift gesture only contains the start and
298 end timestamp. """
299 type = 'FourFingerSwipeLift'
300
301 def __init__(self, start, end):
302 self.start = start
303 self.end = end
304
305 def __str__(self):
306 return self.__class__.type
307
308 def __repr__(self):
309 return str(self)
310
Dennis Kempin1a8a5be2013-06-18 11:00:02 -0700311
312class AbstractButtonGesture(object):
313 """ Abstract gesture for up and down button gestures.
314
315 As both button down and up gestures are functionally identical it has
316 been extracted to this class. The AbstractButtonGesture stores a button ID
317 next to the start and end time of the gesture.
318 """
319 type = 'Undefined'
320
321 def __init__(self, button, start, end):
322 self.button = button
323 self.start = start
324 self.end = end
325
326 def __str__(self):
327 return self.__class__.type + '(' + str(self.button) + ')'
328
329 def __repr__(self):
330 return str(self)
331
332
333class ButtonDownGesture(AbstractButtonGesture):
334 """ Functionally the same as AbstractButtonGesture """
335 type = 'ButtonDown'
336
337
338class ButtonUpGesture(AbstractButtonGesture):
339 """ Functionally the same as AbstractButtonGesture """
340 type = 'ButtonUp'