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);
