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 1946981266ff0d547f463e3b4ee3db72d942ce72
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Jun 11 16:08:30 2022 +0200

    First draft of a `CanvasFollower` class for synchronizing the displacements 
between two canvas.
    It requires more details about the reason why `TransformChangeEvent` occur.
---
 .../main/java/org/apache/sis/portrayal/Canvas.java |  27 +-
 .../org/apache/sis/portrayal/CanvasFollower.java   | 282 +++++++++++++++++++++
 .../org/apache/sis/portrayal/PlanarCanvas.java     |  18 +-
 .../apache/sis/portrayal/TransformChangeEvent.java | 108 +++++++-
 4 files changed, 418 insertions(+), 17 deletions(-)

diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Canvas.java 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Canvas.java
index 51c56e80ed..aea6a4107d 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Canvas.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Canvas.java
@@ -501,7 +501,8 @@ public class Canvas extends Observable implements Localized 
{
      * <cite>objective to display</cite> conversion in a way preserving the 
display coordinates of the given anchor,
      * together with the scales and orientations of features in close 
neighborhood of that point.
      * This calculation may cause {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} 
property change event
-     * to be sent to listeners, in addition of above-cited {@value 
#OBJECTIVE_CRS_PROPERTY}
+     * with the {@link TransformChangeEvent.Reason#CRS_CHANGE} reason to be 
sent to listeners.
+     * That event is sent after the above-cited {@value 
#OBJECTIVE_CRS_PROPERTY} event
      * (note that {@value #POINT_OF_INTEREST_PROPERTY} stay unchanged).
      * All those change events are sent only after all property values have 
been updated to their new values.</p>
      *
@@ -586,7 +587,7 @@ public class Canvas extends Observable implements Localized 
{
             objectiveCRS = newValue;                                // Set 
only after everything else succeeded.
             operationContext.setObjectiveToGeographic(newToGeo);
             firePropertyChange(OBJECTIVE_CRS_PROPERTY, oldValue, newValue);
-            fireIfChanged(oldObjectiveToDisplay, newObjectiveToDisplay);
+            fireIfChanged(oldObjectiveToDisplay, newObjectiveToDisplay, 
false);     // Shall be after CRS change event.
         } catch (FactoryException | TransformException e) {
             throw new 
RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1, 
OBJECTIVE_CRS_PROPERTY), e);
         }
@@ -704,6 +705,7 @@ public class Canvas extends Observable implements Localized 
{
      * Sets the conversion from objective CRS to display coordinate system.
      * If the given value is different than the previous value, then a change 
event is sent
      * to all listeners registered for the {@value 
#OBJECTIVE_TO_DISPLAY_PROPERTY} property.
+     * The event reason is {@link TransformChangeEvent.Reason#ASSIGNMENT}.
      *
      * <p>Invoking this method has the effect of changing the viewed area, the 
zoom level or the rotation of the map.
      * It does not update the {@value #POINT_OF_INTEREST_PROPERTY} property 
however. The point of interest may move
@@ -726,7 +728,8 @@ public class Canvas extends Observable implements Localized 
{
                 }
                 if (!oldValue.equals(newValue)) {
                     setObjectiveToDisplayImpl(newValue);
-                    firePropertyChange(new TransformChangeEvent(this, 
oldValue, newValue));
+                    firePropertyChange(new TransformChangeEvent(this, 
oldValue, newValue,
+                                           
TransformChangeEvent.Reason.ASSIGNMENT));
                 }
                 return;
             }
@@ -1019,9 +1022,10 @@ public class Canvas extends Observable implements 
Localized {
      * Sets canvas properties from the given grid geometry. This convenience 
method converts the
      * coordinate reference system, "grid to CRS" transform and extent of the 
given grid geometry
      * to {@code Canvas} properties. If the given value is different than the 
previous value, then
-     * change events are sent to all listeners registered for the {@value 
#OBJECTIVE_CRS_PROPERTY},
-     * {@value #OBJECTIVE_TO_DISPLAY_PROPERTY}, {@value 
#DISPLAY_BOUNDS_PROPERTY} and/or
-     * {@value #POINT_OF_INTEREST_PROPERTY} properties.
+     * change events are sent to all listeners registered for the {@value 
#DISPLAY_BOUNDS_PROPERTY},
+     * {@value #OBJECTIVE_CRS_PROPERTY}, {@value 
#OBJECTIVE_TO_DISPLAY_PROPERTY}
+     * (with {@link TransformChangeEvent.Reason#GRID_GEOMETRY_CHANGE} reason),
+     * and/or {@value #POINT_OF_INTEREST_PROPERTY} properties, in that order.
      *
      * <p>The value given to this method will be returned by {@link 
#getGridGeometry()} as long as
      * none of above cited properties is changed. If one of those properties 
changes (for example
@@ -1107,11 +1111,11 @@ public class Canvas extends Observable implements 
Localized {
             /*
              * Notify listeners only after all properties have been updated. 
If a listener throws an exception,
              * other listeners will not be notified but this Canvas will not 
be corrupted since all the work to
-             * do in this class is already completed.
+             * do in this class is already completed. Order matter, it is 
documented in this method javadoc.
              */
             fireIfChanged(DISPLAY_BOUNDS_PROPERTY,    oldBounds,             
newBounds);
             fireIfChanged(OBJECTIVE_CRS_PROPERTY,     oldObjectiveCRS,       
newObjectiveCRS);
-            fireIfChanged(/* OBJECTIVE_TO_DISPLAY */  oldObjectiveToDisplay, 
newObjectiveToDisplay);
+            fireIfChanged(/* OBJECTIVE_TO_DISPLAY */  oldObjectiveToDisplay, 
newObjectiveToDisplay, true);
             fireIfChanged(POINT_OF_INTEREST_PROPERTY, oldPOI,                
newPOI);
         } catch (IncompleteGridGeometryException | CannotEvaluateException | 
FactoryException | TransformException e) {
             throw new 
RenderException(errors().getString(Errors.Keys.CanNotSetPropertyValue_1, 
GRID_GEOMETRY_PROPERTY), e);
@@ -1136,10 +1140,13 @@ public class Canvas extends Observable implements 
Localized {
      *
      * @param  oldValue  the old "objective to display" transform.
      * @param  newValue  the new transform, or {@code null} for lazy 
computation.
+     * @param  grid      {@code true} if the reason is a grid geometry change, 
or {@code false} if only a CRS change.
      */
-    private void fireIfChanged(final LinearTransform oldValue, final 
LinearTransform newValue) {
+    private void fireIfChanged(final LinearTransform oldValue, final 
LinearTransform newValue, final boolean grid) {
         if (!Objects.equals(oldValue, newValue)) {
-            firePropertyChange(new TransformChangeEvent(this, oldValue, 
newValue));
+            firePropertyChange(new TransformChangeEvent(this, oldValue, 
newValue,
+                    grid ? TransformChangeEvent.Reason.GRID_GEOMETRY_CHANGE
+                         : TransformChangeEvent.Reason.CRS_CHANGE));
         }
     }
 
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/CanvasFollower.java 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/CanvasFollower.java
new file mode 100644
index 0000000000..6ec1374a40
--- /dev/null
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/CanvasFollower.java
@@ -0,0 +1,282 @@
+/*
+ * 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.portrayal;
+
+import java.util.logging.Logger;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.NoninvertibleTransformException;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.metadata.extent.GeographicBoundingBox;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.util.Disposable;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.internal.system.Modules;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+
+
+/**
+ * A listener of displacements in a source canvas which can reproduce the same 
displacement in a target canvas.
+ * For example if a translation of 100 meters is applied in a source canvas, 
the same translation (in meters)
+ * can be applied in the target canvas. This class does automatically the 
necessary conversions for taking in
+ * account the differences in zoom levels and map projections. For example a 
translation of 10 pixels in one
+ * canvas may map to a translation of 20 pixels in the other canvas for 
reproducing the same "real world" translation.
+ *
+ * <h2>Listeners</h2>
+ * This class implements an unidirectional binding: changes in source are 
applied on target, but not the converse.
+ * {@code CanvasFollower} listener needs to be registered explicitly as below. 
This is not done automatically for
+ * allowing users to control when to listen to changes:
+ *
+ * {@preformat java
+ *     
source.addPropertyChangeListener(PlanarCanvas.OBJECTIVE_TO_DISPLAY_PROPERTY, 
this);
+ *     source.addPropertyChangeListener(PlanarCanvas.OBJECTIVE_CRS_PROPERTY, 
this);
+ *     target.addPropertyChangeListener(PlanarCanvas.OBJECTIVE_CRS_PROPERTY, 
this);
+ * }
+ *
+ * The {@link #dispose()} convenience method is provided for unregistering all 
the above.
+ *
+ * <h2>Multi-threading</h2>
+ * This class is <strong>not</strong> thread-safe.
+ * All events should be processed in the same thread.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.3
+ * @since   1.3
+ * @module
+ */
+public class CanvasFollower implements PropertyChangeListener, Disposable {
+    /**
+     * The canvas which is the source of zoom, translation or rotation events.
+     */
+    protected final PlanarCanvas source;
+
+    /**
+     * The canvas on which to apply the change of zoom, translation or 
rotation.
+     */
+    protected final PlanarCanvas target;
+
+    /**
+     * Whether to follow the source canvas in "real world" coordinates.
+     * If {@code false}, displacements will be followed in pixel coordinates 
instead.
+     */
+    private boolean followRealWorld;
+
+    /**
+     * The effective value of {@link #followRealWorld}.
+     * May be temporarily set to {@code false} if {@link #sourceToTarget} can 
not be computed.
+     */
+    private boolean effectiveRealWorld;
+
+    /**
+     * The transform from a change in source canvas to a change in target 
canvas.
+     * Computed when first needed, and recomputed when the objective CRS 
changes.
+     * A {@code null} value means that no change is needed or can not be done.
+     *
+     * @see #findSourceToTarget()
+     */
+    private MathTransform sourceToTarget;
+
+    /**
+     * Whether {@link #sourceToTarget} field is up to date.
+     * Note that the field can be up-to-date and {@code null}.
+     *
+     * @see #findSourceToTarget()
+     */
+    private boolean isTransformUpdated;
+
+    /**
+     * Whether a change is in progress. This is for avoiding never-ending loop
+     * if a bidirectional mapping or a cycle exists (A → B → C → A).
+     */
+    private boolean changing;
+
+    /**
+     * Creates a new listener for synchronizing "objective to display" 
transform changes
+     * between the specified canvas. This is a unidirectional binding: changes 
in source
+     * are applied on target, but not the converse.
+     *
+     * <p>Caller needs to register this listener explicitly as below
+     * (this is not done automatically by this constructor):</p>
+     *
+     * {@preformat java
+     *     
source.addPropertyChangeListener(PlanarCanvas.OBJECTIVE_TO_DISPLAY_PROPERTY, 
this);
+     *     
source.addPropertyChangeListener(PlanarCanvas.OBJECTIVE_CRS_PROPERTY, this);
+     *     
target.addPropertyChangeListener(PlanarCanvas.OBJECTIVE_CRS_PROPERTY, this);
+     * }
+     *
+     * @param  source  the canvas which is the source of zoom, pan or rotation 
events.
+     * @param  target  the canvas on which to apply the changes of zoom, pan 
or rotation.
+     */
+    public CanvasFollower(final PlanarCanvas source, final PlanarCanvas 
target) {
+        ArgumentChecks.ensureNonNull("source", source);
+        ArgumentChecks.ensureNonNull("target", target);
+        this.source = source;
+        this.target = target;
+        effectiveRealWorld = followRealWorld = true;
+    }
+
+    /**
+     * Returns whether this instance is following changes in "real world" 
coordinates.
+     * If {@code true} (the default value), then changes applied on the 
{@linkplain #source} canvas
+     * and converted into changes to apply on the {@link #target} canvas in 
such a way that the two
+     * canvas got the same translations in real world units. It may result in 
a different amount of
+     * pixels is the two canvas have different zoom level, or a different 
direction if a canvas is
+     * rotated relatively to the other canvas.
+     *
+     * @return whether this instance is following changes in "real world" 
coordinates.
+     */
+    public boolean getFollowRealWorld() {
+        return followRealWorld;
+    }
+
+    /**
+     * Sets whether this instance should following changes in "real world" 
coordinates.
+     * The default value is {@code true}. If this is set to {@code false}, 
then the same changes
+     * in pixel coordinates will be applied on canvas regardless the 
difference in rotation or zoom level.
+     *
+     * @param  real  whether this instance should following changes in "real 
world" coordinates.
+     */
+    public void setFollowRealWorld(final boolean real) {
+        if (real != followRealWorld) {
+            effectiveRealWorld = followRealWorld = real;
+            isTransformUpdated = false;
+            sourceToTarget     = null;
+        }
+    }
+
+    /**
+     * Invoked when the objective CRS, zoom, translation or rotation changed 
on a map that we are tracking.
+     * If the event is an instance of {@link TransformChangeEvent}, then this 
method applies the same change
+     * on the {@linkplain #target} canvas.
+     *
+     * @param  event  a change in the canvas that this listener is tracking.
+     */
+    @Override
+    public void propertyChange(final PropertyChangeEvent event) {
+        if (event instanceof TransformChangeEvent) {
+            final TransformChangeEvent te = (TransformChangeEvent) event;
+            if (!changing && te.getReason().isNavigation()) try {
+                changing = true;
+                if (!isTransformUpdated) {
+                    findSourceToTarget();               // May update the 
`effectiveRealWorld` field.
+                }
+                if (effectiveRealWorld) {
+                    AffineTransform before = 
convertObjectiveChange(te.getObjectiveChange2D().orElse(null));
+                    if (before != null) {
+                        target.transformObjectiveCoordinates(before);
+                        return;
+                    }
+                }
+                
te.getDisplayChange2D().ifPresent(target::transformDisplayCoordinates);
+            } finally {
+                changing = false;
+            }
+        } else if 
(PlanarCanvas.OBJECTIVE_CRS_PROPERTY.equals(event.getPropertyName())) {
+            isTransformUpdated = false;
+            sourceToTarget = null;
+        }
+    }
+
+    /**
+     * Finds the transform to use for converting changes from {@linkplain 
#source} canvas to {@linkplain #target} canvas.
+     * This method should be invoked only if {@link #isTransformUpdated} is 
{@code false}. After this method returned,
+     * {@link #sourceToTarget} contains the transform to use, which may be 
{@code null} if none.
+     */
+    private void findSourceToTarget() {
+        sourceToTarget     = null;
+        effectiveRealWorld = false;
+        isTransformUpdated = true;      // If an exception occurs, use above 
setting.
+        if (followRealWorld) {
+            final CoordinateReferenceSystem sourceCRS = 
source.getObjectiveCRS();
+            final CoordinateReferenceSystem targetCRS = 
target.getObjectiveCRS();
+            if (sourceCRS != null && targetCRS != null) try {
+                GeographicBoundingBox aoi;
+                try {
+                    aoi = target.getGeographicArea().orElse(null);
+                } catch (RenderException e) {
+                    canNotCompute(e);
+                    aoi = null;
+                }
+                sourceToTarget = CRS.findOperation(sourceCRS, targetCRS, 
aoi).getMathTransform();
+                if (sourceToTarget.isIdentity()) {
+                    sourceToTarget = null;
+                }
+                effectiveRealWorld = true;
+            } catch (FactoryException e) {
+                canNotCompute(e);
+                // Stay with "changes in display units" mode.
+            }
+        }
+    }
+
+    /**
+     * Converts a change from units of the source CRS to units of the target 
CRS.
+     * If that change can not be computed, the caller will fallback on a change
+     * in display units (typically pixels).
+     *
+     * @param  before  the change in units of the {@linkplain #source} 
objective CRS.
+     * @return  the same change but in units of the {@linkplain #target} 
objective CRS,
+     *          or {@code null} if it can not be computed.
+     */
+    private AffineTransform convertObjectiveChange(final AffineTransform 
before) {
+        if (sourceToTarget == null) {
+            return before;
+        }
+        if (before != null) {
+            final DirectPosition poi = target.getPointOfInterest(true);
+            if (poi != null) try {
+                AffineTransform t = 
AffineTransforms2D.castOrCopy(MathTransforms.linear(sourceToTarget, poi));
+                AffineTransform c = t.createInverse();
+                c.preConcatenate(before);
+                c.preConcatenate(t);
+                return c;
+            } catch (TransformException | NoninvertibleTransformException e) {
+                canNotCompute(e);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Invoked when the {@link #sourceToTarget} transform can not be computed,
+     * or when an optional information required for that transform is missing.
+     * This method assumes that the public caller (possibly indirectly) is
+     * {@link #propertyChange(PropertyChangeEvent)}.
+     */
+    private static void canNotCompute(final Exception e) {
+        Logging.recoverableException(Logger.getLogger(Modules.PORTRAYAL), 
CanvasFollower.class, "propertyChange", e);
+    }
+
+    /**
+     * Removes all listeners documented in {@linkplain CanvasFollower class 
javadoc}.
+     * This method should be invoked when {@code CanvasFollower} is no longer 
needed
+     * for avoiding memory leak.
+     */
+    @Override
+    public void dispose() {
+        
source.removePropertyChangeListener(PlanarCanvas.OBJECTIVE_TO_DISPLAY_PROPERTY, 
this);
+        
source.removePropertyChangeListener(PlanarCanvas.OBJECTIVE_CRS_PROPERTY, this);
+        
target.removePropertyChangeListener(PlanarCanvas.OBJECTIVE_CRS_PROPERTY, this);
+    }
+}
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
index 345b638217..a6eae370a3 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/PlanarCanvas.java
@@ -162,6 +162,12 @@ public abstract class PlanarCanvas extends Canvas {
      * the current transform. For example if the given {@code before} 
transform is a translation, then the translation
      * vector is in units of the {@linkplain #getObjectiveCRS() objective CRS} 
(typically metres on the map).
      *
+     * <p>This method does nothing if the given transform is identity.
+     * Otherwise an {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} property change 
event will be sent with the
+     * {@link TransformChangeEvent.Reason#OBJECTIVE_NAVIGATION} reason after 
the change became effective.
+     * Depending on the implementation, the change may not take effect 
immediately.
+     * For example subclasses may do the rendering in a background thread.</p>
+     *
      * @param  before  coordinate conversion to apply before the current 
<cite>objective to display</cite> transform.
      *
      * @see TransformChangeEvent#getObjectiveChange()
@@ -172,7 +178,8 @@ public abstract class PlanarCanvas extends Canvas {
             objectiveToDisplay.concatenate(before);
             super.setObjectiveToDisplayImpl(null);
             if (old != null) {
-                final TransformChangeEvent event = new 
TransformChangeEvent(this, old, null);
+                final TransformChangeEvent event = new 
TransformChangeEvent(this, old, null,
+                        TransformChangeEvent.Reason.OBJECTIVE_NAVIGATION);
                 event.objectiveChange2D = before;
                 firePropertyChange(event);
             }
@@ -184,6 +191,12 @@ public abstract class PlanarCanvas extends Canvas {
      * the current transform. For example if the given {@code after} transform 
is a translation, then the translation
      * vector is in pixel units.
      *
+     * <p>This method does nothing if the given transform is identity.
+     * Otherwise an {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} property change 
event will be sent with the
+     * {@link TransformChangeEvent.Reason#DISPLAY_NAVIGATION} reason after the 
change became effective.
+     * Depending on the implementation, the change may not take effect 
immediately.
+     * For example subclasses may do the rendering in a background thread.</p>
+     *
      * @param  after  coordinate conversion to apply after the current 
<cite>objective to display</cite> transform.
      *
      * @see TransformChangeEvent#getDisplayChange()
@@ -194,7 +207,8 @@ public abstract class PlanarCanvas extends Canvas {
             objectiveToDisplay.preConcatenate(after);
             super.setObjectiveToDisplayImpl(null);
             if (old != null) {
-                final TransformChangeEvent event = new 
TransformChangeEvent(this, old, null);
+                final TransformChangeEvent event = new 
TransformChangeEvent(this, old, null,
+                        TransformChangeEvent.Reason.DISPLAY_NAVIGATION);
                 event.displayChange2D = after;
                 firePropertyChange(event);
             }
diff --git 
a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/TransformChangeEvent.java
 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/TransformChangeEvent.java
index 115f1a4692..53ac2908f8 100644
--- 
a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/TransformChangeEvent.java
+++ 
b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/TransformChangeEvent.java
@@ -20,12 +20,16 @@ import java.util.Optional;
 import java.util.logging.Logger;
 import java.awt.geom.AffineTransform;
 import java.beans.PropertyChangeEvent;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.NoninvertibleTransformException;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -53,6 +57,68 @@ public class TransformChangeEvent extends 
PropertyChangeEvent {
      */
     private static final long serialVersionUID = 4444752056666264066L;
 
+    /**
+     * The reason why the "objective to display" transform changed.
+     * It may be because of canvas initialization, or an adjustment for a 
change of CRS
+     * without change in the viewing area, or a navigation for viewing a 
different area.
+     *
+     * @see #getReason()
+     */
+    public enum Reason {
+        /**
+         * A new value has been assigned as part of a wider set of changes.
+         * It typically happens when the canvas is initialized.
+         *
+         * @see Canvas#setGridGeometry(GridGeometry)
+         */
+        GRID_GEOMETRY_CHANGE,
+
+        /**
+         * A new value has been automatically computed for preserving the 
viewing area after a change of CRS.
+         * It typically happens when the user changes the map projection 
without moving to a different region.
+         *
+         * @see Canvas#setObjectiveCRS(CoordinateReferenceSystem, 
DirectPosition)
+         */
+        CRS_CHANGE,
+
+        /**
+         * A new value has been assigned, overwriting the previous values. The 
objective CRS has not changed.
+         * It can be considered as a kind of navigation, moving to absolute 
coordinates and zoom levels.
+         *
+         * @see Canvas#setObjectiveToDisplay(LinearTransform)
+         */
+        ASSIGNMENT,
+
+        /**
+         * A relative change has been applied in units of the objective CRS 
(for example in metres).
+         *
+         * @see PlanarCanvas#transformObjectiveCoordinates(AffineTransform)
+         */
+        OBJECTIVE_NAVIGATION,
+
+        /**
+         * A relative change has been applied in units of display device 
(typically pixel units).
+         *
+         * @see PlanarCanvas#transformDisplayCoordinates(AffineTransform)
+         */
+        DISPLAY_NAVIGATION;
+
+        /**
+         * Returns {@code true} if the "objective to display" transform 
changed because of a change
+         * in viewing area, without change in the data themselves or in the 
map projection.
+         */
+        final boolean isNavigation() {
+            return ordinal() >= ASSIGNMENT.ordinal();
+        }
+    }
+
+    /**
+     * The reason why the "objective to display" transform changed.
+     *
+     * @see #getReason()
+     */
+    private final Reason reason;
+
     /**
      * The change from old coordinates to new coordinates, computed when first 
needed.
      *
@@ -80,10 +146,15 @@ public class TransformChangeEvent extends 
PropertyChangeEvent {
      * @param  source    the canvas that fired the event.
      * @param  oldValue  the old "objective to display" transform.
      * @param  newValue  the new transform, or {@code null} for lazy 
computation.
+     * @param  reason    the reason why the "objective to display" transform 
changed..
      * @throws IllegalArgumentException if {@code source} is {@code null}.
      */
-    public TransformChangeEvent(final Canvas source, final LinearTransform 
oldValue, final LinearTransform newValue) {
+    public TransformChangeEvent(final Canvas source, final LinearTransform 
oldValue, final LinearTransform newValue,
+                                final Reason reason)
+    {
         super(source, Canvas.OBJECTIVE_TO_DISPLAY_PROPERTY, oldValue, 
newValue);
+        ArgumentChecks.ensureNonNull("reason", reason);
+        this.reason = reason;
     }
 
     /**
@@ -96,6 +167,17 @@ public class TransformChangeEvent extends 
PropertyChangeEvent {
         return (Canvas) source;
     }
 
+    /**
+     * Returns the reason why the "objective to display" transform changed.
+     * It may be because of canvas initialization, or an adjustment for a 
change of CRS
+     * without change in the viewing area, or a navigation for viewing a 
different area.
+     *
+     * @return the reason why the "objective to display" transform changed.
+     */
+    public Reason getReason() {
+        return reason;
+    }
+
     /**
      * Gets the old "objective to display" transform.
      *
@@ -204,8 +286,16 @@ public class TransformChangeEvent extends 
PropertyChangeEvent {
      */
     public Optional<AffineTransform> getObjectiveChange2D() {
         if (objectiveChange2D == null) try {
-            objectiveChange2D = 
AffineTransforms2D.castOrCopy(getObjectiveChange());
-        } catch (IllegalArgumentException e) {
+            final Object oldValue = super.getOldValue();
+            final Object newValue = super.getNewValue();
+            if (oldValue instanceof AffineTransform && newValue instanceof 
AffineTransform) {
+                // Equivalent to the `else` branch, but more efficient.
+                objectiveChange2D = ((AffineTransform) 
oldValue).createInverse();
+                objectiveChange2D.concatenate((AffineTransform) newValue);
+            } else {
+                objectiveChange2D = 
AffineTransforms2D.castOrCopy(getObjectiveChange());
+            }
+        } catch (java.awt.geom.NoninvertibleTransformException | 
IllegalArgumentException e) {
             canNotCompute("getObjectiveChange2D", e);
         }
         return Optional.ofNullable(objectiveChange2D);
@@ -222,8 +312,16 @@ public class TransformChangeEvent extends 
PropertyChangeEvent {
      */
     public Optional<AffineTransform> getDisplayChange2D() {
         if (displayChange2D == null) try {
-            displayChange2D = 
AffineTransforms2D.castOrCopy(getDisplayChange());
-        } catch (IllegalArgumentException e) {
+            final Object oldValue = super.getOldValue();
+            final Object newValue = super.getNewValue();
+            if (oldValue instanceof AffineTransform && newValue instanceof 
AffineTransform) {
+                // Equivalent to the `else` branch, but more efficient.
+                displayChange2D = ((AffineTransform) oldValue).createInverse();
+                displayChange2D.preConcatenate((AffineTransform) newValue);
+            } else {
+                displayChange2D = 
AffineTransforms2D.castOrCopy(getDisplayChange());
+            }
+        } catch (java.awt.geom.NoninvertibleTransformException | 
IllegalArgumentException e) {
             canNotCompute("getDisplayChange2D", e);
         }
         return Optional.ofNullable(displayChange2D);

Reply via email to