sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/03: Add listeners to keyboard, track pad or touch screen for navigation.
Date Sat, 11 Apr 2020 21:18:24 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit fba5c2b7bdde2098293f0270f7a4f4ade1d960b8
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat Apr 11 16:29:48 2020 +0200

    Add listeners to keyboard, track pad or touch screen for navigation.
---
 .../java/org/apache/sis/gui/map/MapCanvas.java     | 184 +++++++++++++++++----
 1 file changed, 152 insertions(+), 32 deletions(-)

diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index 58b1bfe..ce27237 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -26,6 +26,7 @@ import java.awt.image.BufferedImage;
 import java.awt.image.DataBufferInt;
 import java.awt.image.RenderedImage;
 import java.awt.image.VolatileImage;
+import javafx.geometry.Bounds;
 import javafx.geometry.Point2D;
 import javafx.geometry.Rectangle2D;
 import javafx.scene.image.ImageView;
@@ -36,8 +37,12 @@ import javafx.scene.layout.Pane;
 import javafx.scene.layout.StackPane;
 import javafx.scene.shape.Rectangle;
 import javafx.scene.transform.Affine;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.ZoomEvent;
+import javafx.scene.input.RotateEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.input.ScrollEvent;
+import javafx.scene.input.GestureEvent;
 import javafx.scene.Cursor;
 import javafx.event.EventType;
 import javafx.beans.Observable;
@@ -49,6 +54,7 @@ import org.apache.sis.referencing.operation.matrix.MatrixSIS;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.geometry.Envelope2D;
+import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
@@ -72,12 +78,21 @@ import org.apache.sis.internal.map.RenderException;
  */
 public abstract class MapCanvas extends PlanarCanvas {
     /**
-     * A factor for converting deltas from scroll wheel into zoom factor.
-     * For positive deltas, the zoom in factor will be {@code delta/MOUSE_WHEEL_ZOOM + 1}.
-     * For a typical value {@code delta} = 40, a {@code MOUSE_WHEEL_ZOOM} value of 400
-     * results in a zoom factor of 10%.
+     * Size in pixels of a scroll or translation event. This value should be close to the
+     * {@linkplain ScrollEvent#getDeltaY() delta of a scroll event done with mouse wheel}.
      */
-    private static final double MOUSE_WHEEL_ZOOM = 400;
+    private static final double SCROLL_EVENT_SIZE = 40;
+
+    /**
+     * The zoom factor to apply on scroll event. A value of 0.1 means that a zoom of 10%
+     * is applied.
+     */
+    private static final double ZOOM_FACTOR = 0.1;
+
+    /**
+     * Division factor to apply on translations and zooms when the control key is down.
+     */
+    private static final double CONTROL_KEY_FACTOR = 10;
 
     /**
      * Number of milliseconds to wait before to repaint the {@linkplain #image} during gesture
events
@@ -164,7 +179,6 @@ public abstract class MapCanvas extends PlanarCanvas {
 
     /**
      * The data bounds to use for computing the initial value of {@link #objectiveToDisplay}.
-     * This is reset to {@code null} after the transform has been computed.
      * We differ this recomputation until all parameters are known.
      *
      * @see #setObjectiveBounds(Envelope)
@@ -202,6 +216,8 @@ public abstract class MapCanvas extends PlanarCanvas {
     /**
      * Whether {@link #objectiveToDisplay} needs to be recomputed.
      * We differ this recomputation until all parameters are known.
+     *
+     * @see #objectiveBounds
      */
     private boolean invalidObjectiveToDisplay;
 
@@ -257,10 +273,14 @@ public abstract class MapCanvas extends PlanarCanvas {
             }
         };
         view.getTransforms().add(transform);
+        view.setOnZoom(this::onZoom);
+        view.setOnRotate(this::onRotate);
         view.setOnScroll(this::onScroll);
         view.setOnMousePressed(this::onDrag);
         view.setOnMouseDragged(this::onDrag);
         view.setOnMouseReleased(this::onDrag);
+        view.setFocusTraversable(true);
+        view.addEventFilter(KeyEvent.KEY_PRESSED, this::onKeyTyped);
         /*
          * Do not set a preferred size, otherwise `repaint()` is invoked twice: once with
the preferred size
          * and once with the actual size of the parent window. Actually the `repaint()` method
appears to be
@@ -291,36 +311,48 @@ public abstract class MapCanvas extends PlanarCanvas {
      * This is interpreted as a translation applied in pixel units on the map.
      */
     private void onDrag(final MouseEvent event) {
-        double x = event.getX();
-        double y = event.getY();
+        final double x = event.getX();
+        final double y = event.getY();
         final EventType<? extends MouseEvent> type = event.getEventType();
         if (type == MouseEvent.MOUSE_PRESSED) {
             floatingPane.setCursor(Cursor.CLOSED_HAND);
+            floatingPane.requestFocus();
             xPanStart = x;
             yPanStart = y;
         } else {
             if (type != MouseEvent.MOUSE_DRAGGED) {
                 floatingPane.setCursor(renderingInProgress != null ? Cursor.WAIT : Cursor.CROSSHAIR);
             }
-            final boolean isFinished = (type == MouseEvent.MOUSE_RELEASED);
-            x -= xPanStart;
-            y -= yPanStart;
-            if (x != 0 || y != 0) {
-                transform.appendTranslation(x, y);
-                final Point2D p = changeInProgress.deltaTransform(x, y);
-                transformOnNewImage.appendTranslation(p.getX(), p.getY());
-                if (!isFinished) {
-                    repaintLater();
-                }
-            }
-            if (isFinished) {
-                repaint();
-            }
+            applyTranslation(x - xPanStart, y - yPanStart, type == MouseEvent.MOUSE_RELEASED);
         }
         event.consume();
     }
 
     /**
+     * Translates the map in response to user event (keyboard, mouse, track pad, touch screen).
+     *
+     * @param  tx       horizontal translation in pixel units.
+     * @param  ty       vertical translation in pixel units.
+     * @param  isFinal  {@code false} if more translations are expected soon, or
+     *                  {@code true} if this is the last translation for now.
+     *
+     * @see #applyZoomOrRotate(GestureEvent, double, double)
+     */
+    private void applyTranslation(final double tx, final double ty, final boolean isFinal)
{
+        if (tx != 0 || ty != 0) {
+            transform.appendTranslation(tx, ty);
+            final Point2D p = changeInProgress.deltaTransform(tx, ty);
+            transformOnNewImage.appendTranslation(p.getX(), p.getY());
+            if (!isFinal) {
+                repaintLater();
+            }
+        }
+        if (isFinal && !transform.isIdentity()) {
+            repaint();
+        }
+    }
+
+    /**
      * Invoked when the user rotates the mouse wheel.
      * This method performs a zoom-in or zoom-out event.
      */
@@ -330,22 +362,112 @@ public abstract class MapCanvas extends PlanarCanvas {
             return;
         }
         final double delta = event.getDeltaY();
-        double zoom = Math.abs(delta) / MOUSE_WHEEL_ZOOM + 1;
+        double zoom = Math.abs(delta) / SCROLL_EVENT_SIZE * ZOOM_FACTOR;
+        if (event.isControlDown()) {
+            zoom /= CONTROL_KEY_FACTOR;
+        }
+        zoom++;
         if (delta < 0) {
             zoom = 1/zoom;
         }
-        if (zoom != 1) {
-            final double x = event.getX();
-            final double y = event.getY();
-            transform.appendScale(zoom, zoom, x, y);
+        applyZoomOrRotate(event, zoom, 0);
+    }
+
+    /**
+     * Invoked when the user performs a zoom on track pad or touch screen.
+     */
+    private void onZoom(final ZoomEvent event) {
+        applyZoomOrRotate(event, event.getZoomFactor(), 0);
+    }
+
+    /**
+     * Invoked when the user performs a rotation on track pad or touch screen.
+     */
+    private void onRotate(final RotateEvent event) {
+        applyZoomOrRotate(event, 1, event.getAngle());
+    }
+
+    /**
+     * Zooms or rotates the map in response to user event (keyboard, mouse, track pad, touch
screen).
+     * If the given event is non-null, it will be consumed.
+     *
+     * @param  event  the mouse, track pad or touch screen event, or {@code null} if the
event was a keyboard event.
+     * @param  zoom   the zoom factor to apply, or 1 if none.
+     * @param  angle  the rotation angle in degrees, or 0 if nine.
+     *
+     * @see #applyTranslation(double, double, boolean)
+     */
+    private void applyZoomOrRotate(final GestureEvent event, final double zoom, final double
angle) {
+        if (zoom != 1 || angle != 0) {
+            final double x, y;
+            if (event != null) {
+                x = event.getX();
+                y = event.getY();
+            } else {
+                final Bounds bounds = floatingPane.getLayoutBounds();
+                x = bounds.getCenterX();
+                y = bounds.getCenterY();
+            }
             final Point2D p = changeInProgress.transform(x, y);
-            transformOnNewImage.appendScale(zoom, zoom, p.getX(), p.getY());
+            if (zoom != 1) {
+                transform.appendScale(zoom, zoom, x, y);
+                transformOnNewImage.appendScale(zoom, zoom, p.getX(), p.getY());
+            }
+            if (angle != 0) {
+                transform.appendRotation(angle, x, y);
+                transformOnNewImage.appendRotation(angle, p.getX(), p.getY());
+            }
             repaintLater();
         }
+        if (event != null) {
+            event.consume();
+        }
+    }
+
+    /**
+     * Invoked when the user presses a key. This handler provides navigation in the direction
of arrow keys,
+     * or zoom-in / zoom-out with page-down / page-up keys. If the control key is down, navigation
is finer.
+     */
+    private void onKeyTyped(final KeyEvent event) {
+        double tx = 0, ty = 0, zoom = 1, angle = 0;
+        if (event.isAltDown()) {
+            switch (event.getCode()) {
+                case RIGHT: case KP_RIGHT: angle = +15; break;
+                case LEFT:  case KP_LEFT:  angle = -15; break;
+                default:                   return;
+            }
+        } else {
+            switch (event.getCode()) {
+                case RIGHT: case KP_RIGHT: tx   = -SCROLL_EVENT_SIZE;  break;
+                case LEFT:  case KP_LEFT:  tx   = +SCROLL_EVENT_SIZE;  break;
+                case DOWN:  case KP_DOWN:  ty   = -SCROLL_EVENT_SIZE;  break;
+                case UP:    case KP_UP:    ty   = +SCROLL_EVENT_SIZE;  break;
+                case PAGE_UP:              zoom = 1/(1 + ZOOM_FACTOR); break;
+                case PAGE_DOWN:            zoom =   (1 + ZOOM_FACTOR); break;
+                case HOME:                 reset(); break;
+                default:                   return;
+            }
+        }
+        if (event.isControlDown()) {
+            tx    /= CONTROL_KEY_FACTOR;
+            ty    /= CONTROL_KEY_FACTOR;
+            angle /= CONTROL_KEY_FACTOR;
+            zoom   = (zoom - 1) / CONTROL_KEY_FACTOR + 1;
+        }
+        applyZoomOrRotate(null, zoom, angle);
+        applyTranslation(tx, ty, false);
         event.consume();
     }
 
     /**
+     * Resets the map view to its default zoom level and default position with no rotation.
+     */
+    public void reset() {
+        invalidObjectiveToDisplay = true;
+        requestRepaint();
+    }
+
+    /**
      * Returns {@code true} if content changed since the last {@link #repaint()} execution.
      */
     private boolean contentsChanged() {
@@ -364,7 +486,7 @@ public abstract class MapCanvas extends PlanarCanvas {
      */
     protected void setObjectiveBounds(final Envelope visibleArea) {
         ArgumentChecks.ensureDimensionMatches("bounds", BIDIMENSIONAL, visibleArea);
-        objectiveBounds = visibleArea;
+        objectiveBounds = ImmutableEnvelope.castOrCopy(visibleArea);
         invalidObjectiveToDisplay = true;
     }
 
@@ -544,11 +666,9 @@ public abstract class MapCanvas extends PlanarCanvas {
             if (invalidObjectiveToDisplay) {
                 invalidObjectiveToDisplay = false;
                 LinearTransform tr;
-                final Envelope source = objectiveBounds;
                 if (objectiveBounds != null) {
-                    objectiveBounds = null;
                     final Envelope2D target = getDisplayBounds();
-                    final MatrixSIS m = Matrices.createTransform(source, target);
+                    final MatrixSIS m = Matrices.createTransform(objectiveBounds, target);
                     Matrices.forceUniformScale(m, 0, new double[] {target.width / 2, target.height
/ 2});
                     tr = MathTransforms.linear(m);
                 } else {


Mime
View raw message