You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/10/17 19:14:34 UTC

[sis] 02/03: Better identification of case where the image shown in `MapCanvas` is in error (i.e. because an exception has been thrown during the rendering process).

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 17bf8499d87b8008b4892b7a012bd15cba46d6e8
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Oct 17 20:00:40 2021 +0200

    Better identification of case where the image shown in `MapCanvas` is in error
    (i.e. because an exception has been thrown during the rendering process).
---
 .../java/org/apache/sis/gui/map/MapCanvas.java     | 87 +++++++++++++++++-----
 .../java/org/apache/sis/gui/map/MapCanvasAWT.java  | 74 +++++++++++++++++-
 .../java/org/apache/sis/gui/map/package-info.java  |  2 +-
 .../apache/sis/internal/gui/BackgroundThreads.java |  4 +-
 .../org/apache/sis/internal/gui/package-info.java  |  2 +-
 5 files changed, 144 insertions(+), 25 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 cdf0a26..288bfda 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
@@ -122,7 +122,7 @@ import static org.apache.sis.internal.util.StandardDateFormat.NANOS_PER_MILLISEC
  * </ol>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -273,6 +273,13 @@ public abstract class MapCanvas extends PlanarCanvas {
     private boolean isMouseChangeScheduled;
 
     /**
+     * {@code true} if navigation should be disabled.
+     *
+     * @see #setNavigationDisabled(boolean)
+     */
+    private boolean isNavigationDisabled;
+
+    /**
      * Whether a rendering is in progress. This property is set to {@code true} when {@code MapCanvas}
      * is about to start a background thread for performing a rendering, and is reset to {@code false}
      * after the {@code MapCanvas} has been updated with new rendering result.
@@ -290,6 +297,21 @@ public abstract class MapCanvas extends PlanarCanvas {
     private final ReadOnlyObjectWrapper<Throwable> error;
 
     /**
+     * Whether the {@link #error} value should be considered non-null.
+     * This is set to {@code false} when a new painting start.
+     * If the value is still {@code false} after painting finished, we can clear {@link #error}.
+     *
+     * <h4>Rational</h4>
+     * A simpler approach would have been to clear {@link #error} when painting start, but this action
+     * fires an event which may resize the {@link StatusBar} height, which in turn may change the size
+     * of this {@code MapCanvas} and cause a new painting. The new painting may fail again, which causes
+     * an error to be reported, which may cause the status bar to change its height again, <i>etc.</i>
+     * It can cause a loop with hundred of repaints before the system stabilize.
+     * Using this flag avoids above problem.
+     */
+    private boolean hasError;
+
+    /**
      * If a contextual menu is currently visible, that menu. Otherwise {@code null}.
      */
     private ContextMenu menuShown;
@@ -356,11 +378,13 @@ public abstract class MapCanvas extends PlanarCanvas {
             switch (event.getButton()) {
                 case PRIMARY: {
                     hideContextMenu();
-                    floatingPane.setCursor(Cursor.CLOSED_HAND);
-                    floatingPane.requestFocus();
-                    isDragging = true;
-                    xPanStart  = x;
-                    yPanStart  = y;
+                    if (!isNavigationDisabled) {
+                        floatingPane.setCursor(Cursor.CLOSED_HAND);
+                        floatingPane.requestFocus();
+                        isDragging = true;
+                        xPanStart  = x;
+                        yPanStart  = y;
+                    }
                     event.consume();
                     break;
                 }
@@ -418,6 +442,10 @@ public abstract class MapCanvas extends PlanarCanvas {
             // Do not interpret scroll events on touch pad as a zoom.
             return;
         }
+        if (isNavigationDisabled) {
+            event.consume();
+            return;
+        }
         final double delta = event.getDeltaY();
         double zoom = Math.abs(delta) / SCROLL_EVENT_SIZE * ZOOM_FACTOR;
         if (event.isControlDown()) {
@@ -441,7 +469,7 @@ public abstract class MapCanvas extends PlanarCanvas {
      * @see #applyTranslation(double, double, boolean)
      */
     private void applyZoomOrRotate(final GestureEvent event, final double zoom, final double angle) {
-        if (zoom != 1 || angle != 0) {
+        if (!isNavigationDisabled && (zoom != 1 || angle != 0)) {
             double x, y;
             if (event != null) {
                 x = event.getX();
@@ -481,6 +509,10 @@ public abstract class MapCanvas extends PlanarCanvas {
      * 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) {
+        if (isNavigationDisabled) {
+            event.consume();
+            return;
+        }
         double tx = 0, ty = 0, zoom = 1, angle = 0;
         if (event.isAltDown()) {
             switch (event.getCode()) {
@@ -532,6 +564,16 @@ public abstract class MapCanvas extends PlanarCanvas {
     }
 
     /**
+     * Disables or re-enable navigation. Navigation is disabled when an error occurred while
+     * rendering the image, and navigating is likely to cause the error to happen again.
+     */
+    final void setNavigationDisabled(final boolean disabled) {
+        isNavigationDisabled = disabled;
+        if (disabled) isDragging = false;
+        floatingPane.setCursor(disabled ? Cursor.DEFAULT : Cursor.CROSSHAIR);
+    }
+
+    /**
      * If a context menu is currently shown, hide that menu. Otherwise does nothing.
      */
     private void hideContextMenu() {
@@ -884,13 +926,14 @@ public abstract class MapCanvas extends PlanarCanvas {
                 return;
             }
         }
+        hasError = false;
         isRendering.set(true);                      // Avoid that `requestRepaint(…)` trig new paints.
         renderingStartTime = System.nanoTime();
-        /*
-         * If a new canvas size is known, inform the parent `PlanarCanvas` about that.
-         * It may cause a recomputation of the "objective to display" transform.
-         */
         try {
+            /*
+             * If a new canvas size is known, inform the parent `PlanarCanvas` about that.
+             * It may cause a recomputation of the "objective to display" transform.
+             */
             if (sizeChanged) {
                 sizeChanged = false;
                 final Pane view = floatingPane;
@@ -986,7 +1029,9 @@ public abstract class MapCanvas extends PlanarCanvas {
                 isMouseChangeScheduled = true;
             }
         } else {
-            clearError();
+            if (!hasError) {
+                clearError();
+            }
             isRendering.set(false);
             restoreCursorAfterPaint();
         }
@@ -1047,6 +1092,8 @@ public abstract class MapCanvas extends PlanarCanvas {
         final Throwable ex = task.getException();
         if (ex != null) {
             errorOccurred(ex);
+        } else if (!hasError) {
+            clearError();
         }
     }
 
@@ -1161,6 +1208,7 @@ public abstract class MapCanvas extends PlanarCanvas {
      * Clears the error message in status bar.
      */
     protected final void clearError() {
+        hasError = false;
         error.set(null);
     }
 
@@ -1172,12 +1220,15 @@ public abstract class MapCanvas extends PlanarCanvas {
      * @param  ex  the exception that occurred (can not be null).
      */
     protected void errorOccurred(final Throwable ex) {
-        final Throwable current = error.get();
-        if (current != null) {
-            current.addSuppressed(ex);
-        } else {
-            error.set(Objects.requireNonNull(ex));
+        if (hasError) {
+            final Throwable current = error.get();
+            if (current != null) {
+                current.addSuppressed(ex);
+                return;
+            }
         }
+        hasError = true;
+        error.set(Objects.requireNonNull(ex));
     }
 
     /**
@@ -1199,6 +1250,8 @@ public abstract class MapCanvas extends PlanarCanvas {
         invalidObjectiveToDisplay = true;
         objectiveBounds = null;
         clearError();
+        isDragging = false;
+        isNavigationDisabled = false;
         isRendering.set(false);
         requestRepaint();
     }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
index 1553a93..3fd42b7 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvasAWT.java
@@ -19,9 +19,12 @@ package org.apache.sis.gui.map;
 import java.util.Locale;
 import java.nio.IntBuffer;
 import java.awt.AlphaComposite;
+import java.awt.Color;
 import java.awt.Graphics2D;
 import java.awt.GraphicsEnvironment;
 import java.awt.GraphicsConfiguration;
+import java.awt.Font;
+import java.awt.font.GlyphVector;
 import java.awt.image.DataBufferInt;
 import java.awt.image.RenderedImage;
 import java.awt.image.BufferedImage;
@@ -48,7 +51,7 @@ import org.apache.sis.internal.coverage.j2d.ColorModelFactory;
  * controls by the user.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -63,6 +66,16 @@ public abstract class MapCanvasAWT extends MapCanvas {
     private static final boolean NATIVE_ACCELERATION = false;
 
     /**
+     * Size (in pixels) of the warning symbol to show if rendering fail.
+     */
+    private static final int WARNING_SIZE = 200;
+
+    /**
+     * The symbols to show on top of the image as a warning when rendering failed.
+     */
+    private static final String WARNING_TEXT = "\u26A0";
+
+    /**
      * Number of additional pixels to paint on each sides of the image, outside the viewing area.
      * Computing a larger image reduces the black borders that user sees during translations or
      * during zoom out before the new image is repainted. The default value is {@code null}.
@@ -122,6 +135,12 @@ public abstract class MapCanvasAWT extends MapCanvas {
     protected final ImageView image;
 
     /**
+     * Whether {@link #WARNING_TEXT} has been drawn on top of the image.
+     * When warning is shown, navigation should be disabled.
+     */
+    private boolean isWarningShown;
+
+    /**
      * Creates a new canvas for JavaFX application.
      *
      * @param  locale  the locale to use for labels and some messages, or {@code null} for default.
@@ -409,6 +428,7 @@ public abstract class MapCanvasAWT extends MapCanvas {
             bufferWrapper       = wrapper;
             bufferConfiguration = configuration;
             final boolean done  = renderer.commit(MapCanvasAWT.this);
+            clearWarning();
             renderingCompleted(this);
             if (!done || contentsChanged()) {
                 repaint();
@@ -517,7 +537,7 @@ public abstract class MapCanvasAWT extends MapCanvas {
             } finally {
                 gr.dispose();
             }
-            return null;
+            return null;                                    // Indicate that the entire buffer was dirty.
         }
 
         /**
@@ -536,14 +556,60 @@ public abstract class MapCanvasAWT extends MapCanvas {
             }
             renderer.translate(image);
             final boolean done = renderer.commit(MapCanvasAWT.this);
+            clearWarning();
             renderingCompleted(this);
             if (!done || contentsLost || contentsChanged()) {
                 repaint();
             }
         }
 
-        @Override protected void failed()    {renderingCompleted(this);}
-        @Override protected void cancelled() {renderingCompleted(this);}
+        /** Clears the image in the same way than failure. */
+        @Override protected void cancelled() {failed();}
+
+        /**
+         * Invoked in JavaFX thread on failure. No result is available. The JavaFX image is set to an empty image.
+         */
+        @Override
+        protected void failed() {
+            if (!isWarningShown) {
+                final int rw = renderer.getWidth();
+                final int rh = renderer.getHeight();
+                final int x = Math.max((rw - WARNING_SIZE) / 2, 0);
+                final int y = Math.max((rh - WARNING_SIZE) / 2, 0);
+                final int width  = rw - 2*x;
+                final int height = rh - 2*y;
+                final Font font = new Font(Font.SERIF, Font.PLAIN, height);
+                bufferWrapper.updateBuffer((final PixelBuffer<IntBuffer> wrapper) -> {
+                    final Graphics2D gr = buffer.createGraphics();
+                    try {
+                        renderer.translate(gr);
+                        gr.setColor(new Color(0XA0804040, true));
+                        gr.fillRoundRect(x, y, width, height, width/5, height/5);
+                        gr.setColor(Color.RED);
+                        final GlyphVector glyphs = font.createGlyphVector(gr.getFontRenderContext(), WARNING_TEXT);
+                        final java.awt.geom.Rectangle2D vb = glyphs.getVisualBounds();
+                        gr.drawGlyphVector(glyphs, (float) (x + 0.5*width  - vb.getCenterX()),
+                                                   (float) (y + 0.5*height - vb.getCenterY()));
+                    } finally {
+                        gr.dispose();
+                    }
+                    return null;
+                });
+                isWarningShown = true;
+                setNavigationDisabled(true);
+            }
+            renderingCompleted(this);
+        }
+    }
+
+    /**
+     * If warning symbol was drawn on top of the image, clears it and restores navigation.
+     */
+    private void clearWarning() {
+        if (isWarningShown) {
+            isWarningShown = false;
+            setNavigationDisabled(false);
+        }
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java
index 19e49b1..694f2d9 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/package-info.java
@@ -22,7 +22,7 @@
  * {@link org.apache.sis.gui.map.MapCanvasAWT} is a specialization for painting the map using Java2D.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
index 312292f..c2e9316 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java
@@ -46,7 +46,7 @@ import org.apache.sis.util.logging.Logging;
  * Users should not rely on this implementation details.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */
@@ -133,7 +133,7 @@ public final class BackgroundThreads extends AtomicInteger implements ThreadFact
         try {
             return f.get();
         } catch (ExecutionException e) {
-            ExceptionReporter.show(DataViewer.getCurrentStage(), null, null, e.getCause());
+            ExceptionReporter.show(DataViewer.getCurrentStage(), null, null, e);
         } catch (InterruptedException e) {
             interrupted("runAndWait", e);
         }
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
index 3f36f7f..3534a98 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java
@@ -25,7 +25,7 @@
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
  * @since   1.1
  * @module
  */