This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new d56a95f9fa Status bar should rewrite coordinates with new accuracy 
after zoom event.
d56a95f9fa is described below

commit d56a95f9fa6a71d76e95a419afa2c3c53d7231d0
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon Apr 20 13:21:58 2026 +0200

    Status bar should rewrite coordinates with new accuracy after zoom event.
---
 .../main/org/apache/sis/gui/map/MapCanvas.java     | 45 ++++++++---
 .../main/org/apache/sis/gui/map/MapMenu.java       |  1 +
 .../sis/gui/map/RenderingCompletedEvent.java       | 86 ++++++++++++++++++++++
 .../main/org/apache/sis/gui/map/RenderingTask.java | 16 +++-
 .../main/org/apache/sis/gui/map/StatusBar.java     | 57 ++++++++------
 .../org/apache/sis/gui/map/ValuesUnderCursor.java  |  1 +
 6 files changed, 169 insertions(+), 37 deletions(-)

diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
index 0129f3d0e2..177e7ab95d 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
@@ -540,8 +540,8 @@ public abstract class MapCanvas extends PlanarCanvas {
         protected StaticGraphics unique() {
             final ObservableList<Node> siblings = floatingPane.getChildren();
             for (int i = siblings.size(); --i >= 0;) {
-                if (siblings.get(i) instanceof EvanescentPane pane) {
-                    final StaticGraphics other = pane.owner;
+                if (siblings.get(i) instanceof EvanescentPane graphics) {
+                    final StaticGraphics other = graphics.owner;
                     if (other.getClass() == getClass()
                             && Objects.equals(objectiveCRS, other.objectiveCRS)
                             && 
objectiveToDisplay.equals(other.objectiveToDisplay))
@@ -864,11 +864,17 @@ public abstract class MapCanvas extends PlanarCanvas {
         /**
          * Creates and registers a new handler for showing a contextual menu 
in the enclosing canvas.
          * It is caller responsibility to ensure that this method is invoked 
only once.
+         * Callers shall invoke {@link #registerListeners()} after this 
constructor.
          */
-        @SuppressWarnings("this-escape")
         MenuHandler(final ContextMenu menu) {
             super(getDisplayCRS());
             this.menu = menu;
+        }
+
+        /**
+         * Registers the listeners after construction.
+         */
+        final void registerListeners() {
             fixedPane.setOnMousePressed (this);
             fixedPane.setOnMouseReleased(this);     // As recommended by 
MouseEvent.isPopupTrigger().
         }
@@ -1426,7 +1432,7 @@ public abstract class MapCanvas extends PlanarCanvas {
             }
         } catch (TransformException | RenderException ex) {
             restoreCursorAfterPaint();
-            isRendering.set(false);
+            fireRenderingCompletedEvent(null);
             errorOccurred(ex);
             return;
         }
@@ -1460,10 +1466,10 @@ public abstract class MapCanvas extends PlanarCanvas {
                 isCursorChangeScheduled = true;
             }
         } else {
-            if (!hasError) {
+            if (!hasError) {    // Confirmation that the error message is not 
actual anymore.
                 clearError();
             }
-            isRendering.set(false);
+            fireRenderingCompletedEvent(null);
             restoreCursorAfterPaint();
         }
     }
@@ -1566,7 +1572,7 @@ public abstract class MapCanvas extends PlanarCanvas {
         /*
          * At this point the rendering is completed. If some error occurred, 
report them.
          */
-        isRendering.set(false);
+        fireRenderingCompletedEvent(changeInProgress);
         final Throwable ex = task.getException();
         if (ex != null) {
             errorOccurred(ex);
@@ -1581,9 +1587,9 @@ public abstract class MapCanvas extends PlanarCanvas {
         if (t != null) try {
             afterRendering = null;
             t.run();
-        } catch (Exception e) {
+        } catch (RuntimeException e) {
             // `runAfterRendering(…)` is the documented method providing this 
feature.
-            unexpectedException("runAfterRendering", e);
+            Logging.recoverableException(LOGGER, MapCanvas.class, 
"runAfterRendering", e);
         }
     }
 
@@ -1680,6 +1686,18 @@ public abstract class MapCanvas extends PlanarCanvas {
         }
     }
 
+    /**
+     * Sets {@link #isRendering} to {@code false} and notifies the listeners 
that the rendering is completed.
+     * The {@code change} argument is the value of {@link 
RenderingTask#changeInProgress}, or {@code null} if
+     * the rendering failed.
+     *
+     * @param  change  the change in the transform between previous rendering 
and new rendering, or {@code null}.
+     */
+    private void fireRenderingCompletedEvent(final Transform change) {
+        isRendering.set(false);
+        firePropertyChange(new RenderingCompletedEvent(this, change));
+    }
+
     /**
      * Returns a property telling whether a rendering is in progress. This 
property become {@code true}
      * when this {@code MapCanvas} is about to start a background thread for 
performing a rendering, and
@@ -1800,8 +1818,13 @@ public abstract class MapCanvas extends PlanarCanvas {
             afterRendering = () -> {
                 try {
                     before.run();
-                } catch (Exception e) {
-                    unexpectedException("runAfterRendering", e);
+                } catch (RuntimeException e) {
+                    try {
+                        task.run();
+                    } catch (RuntimeException s) {
+                        e.addSuppressed(s);
+                    }
+                    throw e;
                 }
                 task.run();
             };
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapMenu.java 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapMenu.java
index 2c7010f7e7..9357ece73a 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapMenu.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapMenu.java
@@ -120,6 +120,7 @@ public class MapMenu extends ContextMenu {
         defined |= mask;
         if (menuHandler == null) {
             menuHandler = canvas.new MenuHandler(this);
+            menuHandler.registerListeners();
         }
         return menuHandler;
     }
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingCompletedEvent.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingCompletedEvent.java
new file mode 100644
index 0000000000..4d074232c7
--- /dev/null
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingCompletedEvent.java
@@ -0,0 +1,86 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.map;
+
+import java.beans.PropertyChangeEvent;
+import javafx.scene.transform.Transform;
+import javafx.geometry.Point2D;
+
+
+/**
+ * Event sent when a rendering finished, either successfully or with errors.
+ *
+ * <h2><abbr>API</abbr> design note</h2>
+ * This event is redundant with {@link MapCanvas#renderingProperty()}, but 
nevertheless added because we need
+ * to provide the {@linkplain #change} information, which is not easily 
representable as an old a new value.
+ * Because of this redundancy, and because the class is not serializable, we 
do not provide this event in the
+ * public <abbr>API</abbr> yet. We should try harder to retrofit in existing 
<abbr>API</abbr>.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see MapCanvas#renderingProperty()
+ * @see MapCanvas#runAfterRendering(Runnable)
+ */
+@SuppressWarnings("serial")     // Not intended to be serialized.
+final class RenderingCompletedEvent extends PropertyChangeEvent {
+    /**
+     * Name of this property event.
+     */
+    static final String NAME = "isRendering";
+
+    /**
+     * The change in the "objective to display" transform between the previous 
rendering and the new rendering.
+     * This is a snapshot of {@link MapCanvas#transform} at the time when 
{@link RenderingTask} started its work.
+     * This field is {@code null} if the rendering was interrupted by an error 
before completion.
+     *
+     * <p>If this field is non-null, then this change has already been 
appended to the {@link MapCanvas}
+     * "objective to display" transform at the time when this {@code 
RenderingCompletedEvent} is fired.
+     * Note however that the "objective to display" transform may also contain 
additional changes
+     * if the user continued to navigate on the map (zooms and pans) after the 
rendering.</p>
+     *
+     * @see RenderingTask#changeInProgress
+     */
+    private final Transform change;
+
+    /**
+     * Creates a new event for a change in the "objective to display" 
transform between two renderings.
+     *
+     * @param  source  the canvas that fired the event.
+     * @param  change  the change in the transform between previous rendering 
and new rendering, or {@code null}.
+     * @throws IllegalArgumentException if {@code source} is {@code null}.
+     */
+    public RenderingCompletedEvent(final MapCanvas source, final Transform 
change) {
+        super(source, NAME, Boolean.TRUE, Boolean.FALSE);
+        this.change = change;
+    }
+
+    /**
+     * Transforms a display coordinate from the old rendering space to the new 
rendering space.
+     * If the change specified at construction time was {@code null}, then 
this method returns {@code null}.
+     *
+     * @param  x  the <var>x</var> coordinate in display units of the old 
rendering.
+     * @param  y  the <var>y</var> coordinate in display units of the old 
rendering.
+     * @return the (<var>x</var>, <var>y</var>) coordinates in display units 
of the new rendering, or {@code null}.
+     */
+    public final Point2D updateDisplayCoordinates(final double x, final double 
y) {
+        if (change != null) {
+            return change.transform(x, y);
+        } else {
+            return null;
+        }
+    }
+}
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
index 6cb9a6a1f2..8e7d67c903 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/RenderingTask.java
@@ -19,6 +19,7 @@ package org.apache.sis.gui.map;
 import javafx.concurrent.Task;
 import javafx.scene.transform.Affine;
 import javafx.scene.transform.Transform;
+import javafx.scene.transform.Translate;
 
 
 /**
@@ -45,13 +46,22 @@ abstract class RenderingTask<V> extends Task<V> {
     }
 
     /**
-     * Takes a copy of the given transform.
+     * Takes a copy of the given transform. This method may use an 
implementation simpler than {@link Affine},
+     * such as {@link Translate}, because the transform stored by this method 
may become part of the transform
+     * chain of graphics such as {@link MapCanvas.StaticGraphics}.
      *
      * @param  transform  value of {@link MapCanvas#transform}.
      */
     final void setChangeInProgress(final Affine transform) {
-        // TODO: select a simpler transform implementation when possible.
-        changeInProgress = new Affine(transform);
+        if (transform.getMxx() == 1 && transform.getMxy() == 0 && 
transform.getMxz() == 0 &&
+            transform.getMyx() == 0 && transform.getMyy() == 1 && 
transform.getMyz() == 0 &&
+            transform.getMzx() == 0 && transform.getMzy() == 0 && 
transform.getMzz() == 1)
+        {
+            // Pans are a very frequent operations.
+            changeInProgress = new Translate(transform.getTx(), 
transform.getTy(), transform.getTz());
+        } else {
+            changeInProgress = new Affine(transform);
+        }
     }
 
     /**
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/StatusBar.java 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/StatusBar.java
index 1336fc9505..61a9603443 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/StatusBar.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/StatusBar.java
@@ -632,10 +632,10 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
         lowestAccuracy.bind(canvas.positionalAccuracyProperty());
         sampleValuesProvider.set(ValuesUnderCursor.create(canvas));
         canvas.errorProperty().addListener((p,o,n) -> setRenderingError(n));
-        canvas.renderingProperty().addListener((p,o,n) -> {
-            if (!n) applyCanvasGeometry();      // Apply only after completion 
of the background rendering task.
+        canvas.addPropertyChangeListener(RenderingCompletedEvent.NAME, (event) 
-> {
+            displaySpaceChanged((RenderingCompletedEvent) event);
         });
-        applyCanvasGeometry();
+        displaySpaceChanged(null);
         if (canvas.getObjectiveCRS() != null) {
             registerMouseListeners();
         } else {
@@ -676,22 +676,31 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
     }
 
     /**
-     * Invoked when {@link MapCanvas} completed its rendering. This method sets
-     * {@link StatusBar#localToObjectiveCRS} to the inverse of {@link 
MapCanvas#objectiveToDisplay}.
-     * It assumes that even if the JavaFX local coordinates and {@link 
#localToPositionCRS} transform
-     * changed, the "real world" coordinates under the mouse cursor is still 
the same. This assumption
-     * should be true if this listener is notified as a result of zoom, 
translation or rotation events.
+     * Invoked when {@link MapCanvas} completed its rendering, either 
successfully or with an error.
+     * This method sets {@link #localToObjectiveCRS} to the inverse of {@link 
MapCanvas#objectiveToDisplay}.
+     *
+     * The geospatial coordinates under the mouse cursor are usually unchanged 
if this listener is notified as a
+     * result of zoom or pan. But the number of fraction digits used for 
formatting the same coordinates may have
+     * changed, because the pixels on the screen may have a different 
resolution after a zoom (or even, sometime,
+     * after a pan). Reformatting the geospatial coordinates requires the 
JavaFX local coordinates of the mouse,
+     * which may have changed because of the change in the {@link 
#localToPositionCRS} transform. We don't have
+     * an <abbr>API</abbr> for asking again the mouse coordinates, so the best 
we can do is to try to reuse
+     * {@link #lastX} and {@link #lastY}.
+     *
+     * @param  event  contains the change in the display space of the {@link 
MapCanvas}.
      */
-    private void applyCanvasGeometry() {
+    private void displaySpaceChanged(final RenderingCompletedEvent event) {
+        final Point2D point = (event != null) ? 
event.updateDisplayCoordinates(lastX, lastY) : null;
         try {
             apply(canvas.getGridGeometry());
-            /*
-             * Do not hide `position` since we assume that "real world" 
coordinates are still valid.
-             * Do not try to rewrite position neither since `lastX` and 
`lastY` are not valid anymore.
-             */
         } catch (RenderException e) {
             setRenderingError(e);
         }
+        if (point != null) {
+            lastX = point.getX();
+            lastY = point.getY();
+        }
+        rewritePosition(null);
     }
 
     /**
@@ -765,11 +774,10 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
 
     /**
      * Implementation of {@link #applyCanvasGeometry(GridGeometry)} without 
changing {@link #position} visibility state.
-     * Invoking this method usually invalidates the coordinates shown in this 
status bar. The new coordinates cannot be
-     * easily recomputed because the {@link #lastX} and {@link #lastY} values 
may not be valid anymore, as a result of
-     * possible changes in JavaFX local coordinate system. Consequently, the 
coordinates should be temporarily hidden
-     * until a new {@link MouseEvent} gives us the new local coordinates, 
unless this method is invoked in a context
-     * where we know that the "real world" coordinates should be the same even 
if local coordinates changed.
+     * This method sets the {@link #lastX} and {@link #lastY} fields to NaN 
because these coordinates become invalid as
+     * a consequence of the change in the {@link #localToPositionCRS} 
transform. However, the {@linkplain #position} is
+     * not hidden because this method may be invoked in a context where we 
know that the "real world" coordinates should
+     * be the same even if local coordinates changed.
      *
      * @param  geometry  geometry of the coverage shown in {@link MapCanvas}, 
or {@code null}.
      */
@@ -1023,8 +1031,8 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
     }
 
     /**
-     * Invoked after a new reference system has been set. This method rewrites 
the coordinates
-     * on the assumption that {@link #lastX} and {@link #lastY} are still 
valid.
+     * Rewrites the coordinates on the assumption that {@link #lastX} and 
{@link #lastY} are still valid.
+     * This method is invoked after a new reference system has been set or 
after a navigation event.
      *
      * @param  current  the local coordinates used for current text, or {@code 
null} if not valid.
      */
@@ -1326,10 +1334,13 @@ public class StatusBar extends Widget implements 
EventHandler<MouseEvent> {
     }
 
     /**
-     * Returns the coordinates given to the last call to {@link 
#setLocalCoordinates(double, double)},
-     * or an empty value if those coordinates are not visible.
+     * Returns the pixel coordinates from which the geospatial coordinates 
have been computed. The returned
+     * point contains the coordinates given in the last call to {@link 
#setLocalCoordinates(double, double)}.
+     * Note that the latter call may have been caused by a user an action such 
as a mouse displacement,
+     * not necessarily an explicit call from the application code.
+     * If the coordinates are not currently shown in the status bar, then this 
method returns an empty value.
      *
-     * @return the local coordinates currently shown in the status bar.
+     * @return the local coordinates which are the sources of the coordinates 
currently shown in the status bar.
      */
     public Optional<Point2D> getLocalCoordinates() {
         if (isPositionVisible() && !Double.isNaN(lastX) && 
!Double.isNaN(lastY)) {
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/ValuesUnderCursor.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/ValuesUnderCursor.java
index ace282bc0f..5c03b0b831 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/ValuesUnderCursor.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/ValuesUnderCursor.java
@@ -344,6 +344,7 @@ public abstract class ValuesUnderCursor {
      * Invoked when an exception occurred while computing values.
      */
     final void setError(final Throwable e) {
+        @SuppressWarnings("LocalVariableHidesMemberVariable")
         final StatusBar owner = this.owner;
         if (owner != null) {
             owner.setSampleValues(owner.cause(e));

Reply via email to