1 /*
   2  * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.tk.quantum;
  27 
  28 import com.sun.glass.events.KeyEvent;
  29 import com.sun.glass.events.TouchEvent;
  30 
  31 import java.security.AccessController;
  32 import java.security.PrivilegedAction;
  33 import java.util.HashMap;
  34 import java.util.Map;
  35 import javafx.event.EventType;
  36 import javafx.scene.input.SwipeEvent;
  37 
  38 class SwipeGestureRecognizer implements GestureRecognizer {
  39     private static final boolean VERBOSE = false;
  40 
  41     // Swipes must be longer than that
  42     private static final double DISTANCE_THRESHOLD = 10; // pixel
  43 
  44     // Traveling this distance against the swipe direction at its end cancels it
  45     private static final double BACKWARD_DISTANCE_THRASHOLD = 5; // pixel
  46 
  47     private SwipeRecognitionState state = SwipeRecognitionState.IDLE;
  48     MultiTouchTracker tracker = new MultiTouchTracker();
  49     private ViewScene scene;
  50 
  51     SwipeGestureRecognizer(final ViewScene scene) {
  52         this.scene = scene;
  53     }
  54 
  55     @Override
  56     public void notifyBeginTouchEvent(long time, int modifiers, boolean isDirect,
  57             int touchEventCount) {
  58         tracker.params(modifiers, isDirect);
  59     }
  60 
  61     @Override
  62     public void notifyNextTouchEvent(long time, int type, long touchId,
  63                                      int x, int y, int xAbs, int yAbs) {
  64         switch(type) {
  65             case TouchEvent.TOUCH_PRESSED:
  66                 tracker.pressed(touchId, time, x, y, xAbs, yAbs);
  67                 break;
  68             case TouchEvent.TOUCH_STILL:
  69                 /* NOBREAK */
  70             case TouchEvent.TOUCH_MOVED:
  71                 tracker.progress(touchId, time, xAbs, yAbs);
  72                 break;
  73             case TouchEvent.TOUCH_RELEASED:
  74                 tracker.released(touchId, time, x, y, xAbs, yAbs);
  75                 break;
  76             default:
  77                 throw new RuntimeException("Error in swipe gesture recognition: "
  78                         + "unknown touch state: " + state);
  79         }
  80     }
  81 
  82     @Override
  83     public void notifyEndTouchEvent(long time) {
  84         // nothing to do
  85     }
  86 
  87     private EventType<SwipeEvent> calcSwipeType(TouchPointTracker tracker) {
  88 
  89         final double distanceX = tracker.getDistanceX();
  90         final double distanceY = tracker.getDistanceY();
  91         final double absDistanceX = Math.abs(distanceX);
  92         final double absDistanceY = Math.abs(distanceY);
  93         
  94         final boolean horizontal = absDistanceX > absDistanceY;
  95 
  96         final double primaryDistance = horizontal ? distanceX : distanceY;
  97         final double absPrimaryDistance = horizontal ? absDistanceX : absDistanceY;
  98         final double absSecondaryDistance = horizontal ? absDistanceY : absDistanceX;
  99         final double absPrimaryLength = horizontal ?
 100                 tracker.lengthX : tracker.lengthY;
 101         final double maxSecondaryDeviation = horizontal ?
 102                 tracker.maxDeviationY : tracker.maxDeviationX;
 103         final double lastPrimaryMovement = horizontal ?
 104                 tracker.lastXMovement : tracker.lastYMovement;
 105 
 106         if (absPrimaryDistance <= DISTANCE_THRESHOLD) {
 107             // too small movement
 108             return null;
 109         }
 110 
 111         if (absSecondaryDistance > absPrimaryDistance * 0.839 /* tan(2Pi/9) */) {
 112             // too diagonal - in range of 10 degrees
 113             return null;
 114         }
 115 
 116         if (maxSecondaryDeviation > absPrimaryLength / (tracker.getDuration() / 100.0)) {
 117             // too imprecise for the performed speed (the slower movement, 
 118             // the higher precision requred)
 119             return null;
 120         }
 121 
 122         if (absPrimaryLength > absPrimaryDistance * 1.5) {
 123             // too much back and forth
 124             return null;
 125         }
 126 
 127         if (Math.signum(primaryDistance) != Math.signum(lastPrimaryMovement) &&
 128                 Math.abs(lastPrimaryMovement) > BACKWARD_DISTANCE_THRASHOLD) {
 129             // gesture finished in the oposite direction
 130             return null;
 131         }
 132 
 133         if (horizontal) {
 134             return tracker.getDistanceX() < 0
 135                     ? SwipeEvent.SWIPE_LEFT : SwipeEvent.SWIPE_RIGHT;
 136         } else {
 137             return tracker.getDistanceY() < 0
 138                     ? SwipeEvent.SWIPE_UP : SwipeEvent.SWIPE_DOWN;
 139         }
 140     }
 141 
 142     private void handleSwipeType(final EventType<SwipeEvent> swipeType,
 143             final CenterComputer cc, final int touchCount, final int modifiers, final boolean isDirect)
 144     {
 145         if (swipeType == null) {
 146             return;
 147         }
 148         if (VERBOSE) {
 149             System.err.println("handleSwipeType swipeType=" + swipeType);
 150         }
 151 
 152         AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 153             if (scene.sceneListener != null) {
 154                 scene.sceneListener.swipeEvent(swipeType, touchCount,
 155                     cc.getX(), cc.getY(),
 156                     cc.getAbsX(), cc.getAbsY(),
 157                     (modifiers & KeyEvent.MODIFIER_SHIFT) != 0,
 158                     (modifiers & KeyEvent.MODIFIER_CONTROL) != 0,
 159                     (modifiers & KeyEvent.MODIFIER_ALT) != 0,
 160                     (modifiers & KeyEvent.MODIFIER_WINDOWS) != 0,
 161                     isDirect);
 162             }
 163             return null;
 164         }, scene.getAccessControlContext());
 165     }
 166 
 167     private static class CenterComputer {
 168         double totalAbsX = 0, totalAbsY = 0;
 169         double totalX = 0, totalY = 0;
 170         int count = 0;
 171 
 172         public void add(double x, double y, double xAbs, double yAbs) {
 173             totalAbsX += xAbs;
 174             totalAbsY += yAbs;
 175             totalX += x;
 176             totalY += y;
 177 
 178             count++;
 179         }
 180 
 181         public double getX() {
 182             return count == 0 ? 0 : totalX / count;
 183         }
 184 
 185         public double getY() {
 186             return count == 0 ? 0 : totalY / count;
 187         }
 188 
 189         public double getAbsX() {
 190             return count == 0 ? 0 : totalAbsX / count;
 191         }
 192 
 193         public double getAbsY() {
 194             return count == 0 ? 0 : totalAbsY / count;
 195         }
 196 
 197         public void reset() {
 198             totalX = 0;
 199             totalY = 0;
 200             totalAbsX = 0;
 201             totalAbsY = 0;
 202             count = 0;
 203         }
 204     }
 205 
 206     private class MultiTouchTracker {
 207         SwipeRecognitionState state = SwipeRecognitionState.IDLE;
 208         Map<Long, TouchPointTracker> trackers =
 209                 new HashMap<Long, TouchPointTracker>();
 210         CenterComputer cc = new CenterComputer();
 211         int modifiers;
 212         boolean direct;
 213         private int touchCount;
 214         private int currentTouchCount;
 215         private EventType<SwipeEvent> type;
 216 
 217         public void params(int modifiers, boolean direct) {
 218             this.modifiers = modifiers;
 219             this.direct = direct;
 220         }
 221 
 222         public void pressed(long id, long nanos, int x, int y, int xAbs, int yAbs) {
 223             currentTouchCount++;
 224             switch (state) {
 225                 case IDLE:
 226                     currentTouchCount = 1;
 227                     state = SwipeRecognitionState.ADDING;
 228                     /* NOBREAK */
 229                 case ADDING:
 230                     TouchPointTracker tracker = new TouchPointTracker();
 231                     tracker.start(nanos, x, y, xAbs, yAbs);
 232                     trackers.put(id, tracker);
 233                     break;
 234                 case REMOVING:
 235                     // we don't allow for swipes with varying touch count
 236                     state = SwipeRecognitionState.FAILURE;
 237                     break;
 238                 default:
 239                     break;
 240             }
 241         }
 242 
 243         public void released(long id, long nanos, int x, int y, int xAbs, int yAbs) {
 244             if (state != SwipeRecognitionState.FAILURE) {
 245                 TouchPointTracker tracker = trackers.get(id);
 246 
 247                 if (tracker == null) {
 248                     // we don't know this ID, something went completely wrong
 249                     state = SwipeRecognitionState.FAILURE;
 250                     throw new RuntimeException("Error in swipe gesture "
 251                             + "recognition: released unknown touch point");
 252                 }
 253 
 254                 tracker.end(nanos, x, y, xAbs, yAbs);
 255                 cc.add(tracker.beginX, tracker.beginY,
 256                         tracker.beginAbsX, tracker.beginAbsY);
 257                 cc.add(tracker.endX, tracker.endY,
 258                         tracker.endAbsX, tracker.endAbsY);
 259 
 260                 final EventType<SwipeEvent> swipeType = calcSwipeType(tracker);
 261 
 262                 switch (state) {
 263                     case IDLE:
 264                         reset();
 265                         throw new RuntimeException("Error in swipe gesture "
 266                                 + "recognition: released touch point outside "
 267                                 + "of gesture");
 268                     case ADDING:
 269                         state = SwipeRecognitionState.REMOVING;
 270                         touchCount = currentTouchCount;
 271                         type = swipeType;
 272                         break;
 273                     case REMOVING:
 274                         if (type != swipeType) {
 275                             // each finger does something else
 276                             state = SwipeRecognitionState.FAILURE;
 277                         }
 278                         break;
 279                     default:
 280                         break;
 281                 }
 282                 trackers.remove(id);
 283             }
 284 
 285             currentTouchCount--;
 286 
 287             if (currentTouchCount == 0) {
 288                 if (state == SwipeRecognitionState.REMOVING) {
 289                     handleSwipeType(type, cc, touchCount, modifiers, direct);
 290                 }
 291 
 292                 state = SwipeRecognitionState.IDLE;
 293                 reset();
 294             }
 295         }
 296 
 297         public void progress(long id, long nanos, int x, int y) {
 298 
 299             if (state == SwipeRecognitionState.FAILURE) {
 300                 return;
 301             }
 302 
 303             TouchPointTracker tracker = trackers.get(id);
 304 
 305             if (tracker == null) {
 306                 // we don't know this ID, something went completely wrong
 307                 state = SwipeRecognitionState.FAILURE;
 308                 throw new RuntimeException("Error in swipe gesture "
 309                         + "recognition: reported unknown touch point");
 310             }
 311 
 312             tracker.progress(nanos, x, y);
 313         }
 314 
 315         void reset() {
 316             trackers.clear();
 317             cc.reset();
 318             state = SwipeRecognitionState.IDLE;
 319         }
 320     }
 321 
 322     private static class TouchPointTracker {
 323         long beginTime, endTime;
 324         double beginX, beginY, endX, endY;
 325         double beginAbsX, beginAbsY, endAbsX, endAbsY;
 326         double lengthX, lengthY;
 327         double maxDeviationX, maxDeviationY;
 328         double lastXMovement, lastYMovement;
 329         double lastX, lastY;
 330 
 331         public void start(long nanos, double x, double y, double absX, double absY) {
 332             beginX = x;
 333             beginY = y;
 334             beginAbsX = absX;
 335             beginAbsY = absY;
 336             lastX = absX;
 337             lastY = absY;
 338             beginTime = nanos / 1000000;
 339         }
 340 
 341         public void end(long nanos, double x, double y, double absX, double absY) {
 342             progress(nanos, absX, absY);
 343             endX = x;
 344             endY = y;
 345             endAbsX = absX;
 346             endAbsY = absY;
 347             endTime = nanos / 1000000;
 348         }
 349 
 350         public void progress(long nanos, double x, double y) {
 351                         final double deltaX = x - lastX;
 352                         final double deltaY = y - lastY;
 353 
 354                         lengthX += Math.abs(deltaX);
 355                         lengthY += Math.abs(deltaY);
 356                         lastX = x;
 357                         lastY = y;
 358 
 359                         final double devX = Math.abs(x - beginAbsX);
 360                         if (devX > maxDeviationX) { maxDeviationX = devX; }
 361 
 362                         final double devY = Math.abs(y - beginAbsY);
 363                         if (devY > maxDeviationY) { maxDeviationY = devY; }
 364 
 365                         if (Math.signum(deltaX) == Math.signum(lastXMovement)) {
 366                                 lastXMovement += deltaX;
 367                         } else {
 368                                 lastXMovement = deltaX;
 369                         }
 370 
 371                         if (Math.signum(deltaY) == Math.signum(lastYMovement)) {
 372                                 lastYMovement += deltaY;
 373                         } else {
 374                                 lastYMovement = deltaY;
 375                         }
 376         }
 377 
 378         public double getDistanceX() {
 379             return endX - beginX;
 380         }
 381 
 382         public double getDistanceY() {
 383             return endY - beginY;
 384         }
 385 
 386         public long getDuration() {
 387             return endTime - beginTime;
 388         }
 389     }
 390 
 391     private enum SwipeRecognitionState {
 392         IDLE,
 393         ADDING,
 394         REMOVING,
 395         FAILURE
 396     }
 397 }