|
|
@@ -0,0 +1,220 @@
|
|
|
+#!/usr/bin/env python
|
|
|
+from events import TapEvent, FlickEvent
|
|
|
+import time
|
|
|
+from math import atan2
|
|
|
+from threading import Thread
|
|
|
+from OSC import OSCServer
|
|
|
+
|
|
|
+
|
|
|
+def distance(a, b):
|
|
|
+ """
|
|
|
+ Calculate the distance between points a and b.
|
|
|
+ """
|
|
|
+ xa, ya = a
|
|
|
+ xb, yb = b
|
|
|
+
|
|
|
+ return ((xb - xa) ** 2 + (yb - ya) ** 2) ** .5
|
|
|
+
|
|
|
+
|
|
|
+def add(a, b):
|
|
|
+ return a + b
|
|
|
+
|
|
|
+
|
|
|
+class TouchPoint(object):
|
|
|
+ def __init__(self, sid, x, y):
|
|
|
+ self.sid = sid
|
|
|
+ self.x = x
|
|
|
+ self.y = y
|
|
|
+
|
|
|
+ def update(self, x, y):
|
|
|
+ self.px = self.x
|
|
|
+ self.py = self.y
|
|
|
+ self.x = x
|
|
|
+ self.y = y
|
|
|
+
|
|
|
+ def init_gesture_data(self, cx, cy):
|
|
|
+ self.pinch = self.old_pinch = distance(self.x, self.y, cx, cy)
|
|
|
+ self.angle = self.old_angle = atan2(self.y - cy, self.x - cx)
|
|
|
+
|
|
|
+ def set_angle(self, angle):
|
|
|
+ self.old_angle = self.angle
|
|
|
+ self.angle = angle
|
|
|
+
|
|
|
+ def set_pinch(self, pinch):
|
|
|
+ self.old_pinch = self.pinch
|
|
|
+ self.pinch = pinch
|
|
|
+
|
|
|
+ def dx(self):
|
|
|
+ return int(self.x - self.px)
|
|
|
+
|
|
|
+ def dy(self):
|
|
|
+ return int(self.y - self.py)
|
|
|
+
|
|
|
+
|
|
|
+SUPPORTED_GESTURES = ('tap', 'pan', 'flick', 'rotate', 'pinch')
|
|
|
+TUIO_ADDRESS = ('localhost', 3333)
|
|
|
+DOUBLE_TAP_DISTANCE = 30
|
|
|
+TAP_INTERVAL = .200
|
|
|
+TAP_TIMEOUT = .200
|
|
|
+
|
|
|
+
|
|
|
+class MultiTouchListener(object):
|
|
|
+ def __init__(self, verbose=False, update_rate=60):
|
|
|
+ self.verbose = verbose
|
|
|
+ self.last_tap = 0
|
|
|
+ self.update_rate = update_rate
|
|
|
+ self.points_changed = False
|
|
|
+ self.handlers = {}
|
|
|
+
|
|
|
+ # Session id's pointing to point coordinates
|
|
|
+ self.points = {}
|
|
|
+
|
|
|
+ self.taps_down = []
|
|
|
+ self.taps = []
|
|
|
+
|
|
|
+ def update_centroid(self):
|
|
|
+ self.old_centroid = self.centroid
|
|
|
+ cx, cy = zip(*[(p.x, p.y) for p in self.points])
|
|
|
+ l = len(self.points)
|
|
|
+ self.centroid = (reduce(add, cx, 0) / l, reduce(add, cy, 0) / l)
|
|
|
+
|
|
|
+ def analyze(self):
|
|
|
+ self.detect_taps()
|
|
|
+
|
|
|
+ if self.points_changed:
|
|
|
+ self.update_centroid()
|
|
|
+
|
|
|
+ # Do not try to rotate or pinch while dragging
|
|
|
+ # This gets rid of a lot of jittery events
|
|
|
+ if not self.detect_drag():
|
|
|
+ self.detect_rotation()
|
|
|
+ self.detect_pinch()
|
|
|
+
|
|
|
+ self.points_changed = False
|
|
|
+
|
|
|
+ def detect_taps(self):
|
|
|
+ if len(self.taps) == 2:
|
|
|
+ if distance(*self.taps) > DOUBLE_TAP_DISTANCE:
|
|
|
+ # Taps are too far away too be a double tap, add 2 separate
|
|
|
+ # events
|
|
|
+ self.trigger(TapEvent(*self.taps[0]))
|
|
|
+ self.trigger(TapEvent(*self.taps[1]))
|
|
|
+ else:
|
|
|
+ # Distance is within treshold, trigger a 'double tap' event
|
|
|
+ self.trigger(TapEvent(*self.taps[0], double=True))
|
|
|
+
|
|
|
+ self.taps = []
|
|
|
+ elif len(self.taps) == 1:
|
|
|
+ # FIXME: Ignore successive single- and double taps?
|
|
|
+ if time.time() - self.last_tap > TAP_TIMEOUT:
|
|
|
+ self.trigger(TapEvent(*self.taps[0]))
|
|
|
+ self.taps = []
|
|
|
+
|
|
|
+ def detect_rotation(self):
|
|
|
+ if 'rotate' not in self.handlers:
|
|
|
+ return
|
|
|
+
|
|
|
+ def detect_pinch(self):
|
|
|
+ if 'pinch' not in self.handlers:
|
|
|
+ return
|
|
|
+
|
|
|
+ def point_down(self, sid, x, y):
|
|
|
+ if sid in self.points:
|
|
|
+ raise KeyError('Point with session id "%s" already exists.' % sid)
|
|
|
+
|
|
|
+ self.points[sid] = TouchPoint(sid, x, y)
|
|
|
+ self.update_centroid()
|
|
|
+
|
|
|
+ # Detect multi-point gestures
|
|
|
+ if len(self.points) > 1:
|
|
|
+ p.init_gesture_data(*self.centroid)
|
|
|
+
|
|
|
+ if len(self.points) == 2:
|
|
|
+ first_p = self.points[0]
|
|
|
+ first_p.init_gesture_data(*self.centroid)
|
|
|
+
|
|
|
+ self.taps_down.append(p)
|
|
|
+ self.last_tap = time.time()
|
|
|
+ self.points_changed = True
|
|
|
+
|
|
|
+ def point_up(self, sid):
|
|
|
+ if sid not in self.points:
|
|
|
+ raise KeyError('No point with session id "%s".' % sid)
|
|
|
+
|
|
|
+ p = self.points[sid]
|
|
|
+ del self.points[sid]
|
|
|
+
|
|
|
+ # Tap/flick detection
|
|
|
+ if p in self.taps_down:
|
|
|
+ # Detect if Flick based on movement
|
|
|
+ if distance(p.x, p.y, p.px, p.py) > FLICK_VELOCITY_TRESHOLD:
|
|
|
+ self.trigger(FlickEvent(p.px, p.py, (p.dx(), p.dy())))
|
|
|
+ else:
|
|
|
+ if time.time() - self.last_tap < TAP_INTERVAL:
|
|
|
+ # Move from taps_down to taps for us in tap detection
|
|
|
+ self.taps_down.remove(p)
|
|
|
+ self.taps.append((p.x, p.y))
|
|
|
+
|
|
|
+ self.points_changed = True
|
|
|
+
|
|
|
+ def point_move(self, sid, x, y):
|
|
|
+ self.points[sid].update(x, y)
|
|
|
+ self.points_changed = True
|
|
|
+
|
|
|
+ def _tuio_handler(self, addr, tags, data, source):
|
|
|
+ # TODO: Call self.point_{down,up,move}
|
|
|
+ pass
|
|
|
+
|
|
|
+ def _tuio_server(self):
|
|
|
+ server = OSCServer(TUIO_ADDRESS)
|
|
|
+ server.addDefaultHandlers()
|
|
|
+ server.addMsgHandler('/tuio', self._tuio_handler)
|
|
|
+ server.serve_forever()
|
|
|
+
|
|
|
+ def start(self):
|
|
|
+ """
|
|
|
+ Start event loop.
|
|
|
+ """
|
|
|
+ self.log('Starting event loop')
|
|
|
+
|
|
|
+ try:
|
|
|
+ # Run TUIO message listener in a different thread
|
|
|
+ #thread = Thread(target=self.__class__._tuio_server, args=(self))
|
|
|
+ thread = Thread(target=self._tuio_server)
|
|
|
+ thread.daemon = True
|
|
|
+ thread.start()
|
|
|
+
|
|
|
+ interval = 1. / self.update_rate
|
|
|
+
|
|
|
+ while True:
|
|
|
+ self.analyze()
|
|
|
+ time.sleep(interval)
|
|
|
+ except KeyboardInterrupt:
|
|
|
+ self.log('Stopping event loop')
|
|
|
+
|
|
|
+ def log(self, msg):
|
|
|
+ if self.verbose:
|
|
|
+ print '| LOG | %s' % msg
|
|
|
+
|
|
|
+ def bind(self, gesture, handler):
|
|
|
+ if gesture not in SUPPORTED_GESTURES:
|
|
|
+ raise ValueError('Unsopported gesture "%s".' % gesture)
|
|
|
+
|
|
|
+ if gesture not in self.handlers:
|
|
|
+ self.handlers[gesture] = []
|
|
|
+
|
|
|
+ self.handlers[gesture].append(handler)
|
|
|
+
|
|
|
+ def trigger(self, event):
|
|
|
+ if event.gesture in self.handlers:
|
|
|
+ for handler in self.handlers[event.gesture]:
|
|
|
+ handler(event)
|
|
|
+
|
|
|
+
|
|
|
+if __name__ == '__main__':
|
|
|
+ def tap(event):
|
|
|
+ print 'tap:', event
|
|
|
+
|
|
|
+ loop = MultiTouchListener(verbose=True)
|
|
|
+ loop.bind('tap', tap)
|
|
|
+ loop.start()
|