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 a160c6f66af80ac0b45274a0e5e092db221b8ca9 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri Jun 10 14:28:39 2022 +0200 Addition of a method for getting changes in objective CRS. Refactoring: methods renaming and documentation updates. --- .../main/java/org/apache/sis/portrayal/Canvas.java | 77 ++++----- .../java/org/apache/sis/portrayal/MapLayer.java | 1 - .../java/org/apache/sis/portrayal/MapLayers.java | 2 - .../org/apache/sis/portrayal/PlanarCanvas.java | 20 ++- .../apache/sis/portrayal/TransformChangeEvent.java | 175 ++++++++++++++++++--- .../operation/matrix/AffineTransforms2D.java | 2 +- 6 files changed, 194 insertions(+), 83 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 ce293db612..51c56e80ed 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 @@ -575,7 +575,7 @@ public class Canvas extends Observable implements Localized { * normally it should just return the `result` as-is. */ newObjectiveToDisplay = MathTransforms.tangent(result, poiInNew); - updateObjectiveToDisplay(newObjectiveToDisplay); + setObjectiveToDisplayImpl(newObjectiveToDisplay); objectivePOI = poiInNew; // Set only after everything else succeeded. multidimToObjective = poiToNew; augmentedObjectiveCRS = null; // Will be recomputed when first needed. @@ -680,22 +680,23 @@ public class Canvas extends Observable implements Localized { */ public LinearTransform getObjectiveToDisplay() { if (objectiveToDisplay == null) { - objectiveToDisplay = updateObjectiveToDisplay(); + objectiveToDisplay = createObjectiveToDisplay(); } return objectiveToDisplay; } /** - * Takes a snapshot of the <cite>objective to display</cite> conversion. This method needs - * to be overridden only by subclasses that use their own specialized class instead of - * {@link #objectiveToDisplay} for managing changes in the zooms or viewed area. + * Returns the current <cite>objective to display</cite> conversion managed by the subclass. + * This method is invoked only if {@link #objectiveToDisplay} is {@code null}, which may + * happen either at initialization time or if the subclass uses its own specialized field + * instead of {@link #objectiveToDisplay} for managing changes in the zooms or viewed area. + * This method needs to be overridden only by subclasses using such specialization. * - * @return snapshot of objective to display conversion, never null. + * @return objective to display conversion created from current value managed by subclass. * - * @see #updateObjectiveToDisplay(LinearTransform) - * @see #invalidateObjectiveToDisplay() + * @see #setObjectiveToDisplayImpl(LinearTransform) */ - LinearTransform updateObjectiveToDisplay() { + LinearTransform createObjectiveToDisplay() { return MathTransforms.identity(getDisplayDimensions()); } @@ -712,7 +713,7 @@ public class Canvas extends Observable implements Localized { * @throws IllegalArgumentException if given the transform does not have the expected number of dimensions or is not affine. * @throws RenderException if the <cite>objective to display</cite> transform can not be set to the given value for another reason. */ - public void setObjectiveToDisplay(LinearTransform newValue) throws RenderException { + public void setObjectiveToDisplay(final LinearTransform newValue) throws RenderException { ArgumentChecks.ensureNonNull(OBJECTIVE_TO_DISPLAY_PROPERTY, newValue); final int expected = getDisplayDimensions(); int actual = newValue.getSourceDimensions(); @@ -721,11 +722,11 @@ public class Canvas extends Observable implements Localized { if (actual == expected) { LinearTransform oldValue = objectiveToDisplay; // Do not invoke user-overridable method. if (oldValue == null) { - oldValue = updateObjectiveToDisplay(); + oldValue = createObjectiveToDisplay(); } if (!oldValue.equals(newValue)) { - updateObjectiveToDisplay(newValue); - fireTransformChange(oldValue, newValue); + setObjectiveToDisplayImpl(newValue); + firePropertyChange(new TransformChangeEvent(this, oldValue, newValue)); } return; } @@ -735,28 +736,23 @@ public class Canvas extends Observable implements Localized { } /** - * Sets the conversion from objective CRS to display coordinate system. + * Actually sets the conversion from objective CRS to display coordinate system. * Contrarily to other setter methods, this method does not notify listeners about that change; - * it is caller responsibility to send a {@value #OBJECTIVE_TO_DISPLAY_PROPERTY} change event. + * it is caller responsibility to fire a {@link TransformChangeEvent} after all fields are updated. * This design choice is because this method is usually invoked as part of a larger set of changes. * - * @see #updateObjectiveToDisplay() - */ - void updateObjectiveToDisplay(final LinearTransform newValue) { - objectiveToDisplay = newValue; - gridGeometry = null; - operationContext.clear(); - } - - /** - * Declares that the {@link #objectiveToDisplay} transform became invalid and will need to be recomputed. - * It is subclasses responsibility to recompute the transform in their {@link #updateObjectiveToDisplay()} - * method and to invoke {@link #fireTransformChange(LinearTransform, LinearTransform)} (or equivalent). + * <p>If the new value is {@code null}, then this method only declares that the {@link #objectiveToDisplay} + * transform became invalid and will need to be recomputed. It is subclasses responsibility to recompute the + * transform in their {@link #createObjectiveToDisplay()}.</p> + * + * @param newValue the new "objective to display" transform, or {@code null} if it will be computed later + * by {@link #createObjectiveToDisplay()}. A null value is okay only when invoked by subclasses that + * overrode {@link #createObjectiveToDisplay()}. * - * @see #updateObjectiveToDisplay() + * @see #createObjectiveToDisplay() */ - final void invalidateObjectiveToDisplay() { - objectiveToDisplay = null; + void setObjectiveToDisplayImpl(final LinearTransform newValue) { + objectiveToDisplay = newValue; gridGeometry = null; operationContext.clear(); } @@ -991,7 +987,7 @@ public class Canvas extends Observable implements Localized { * translation terms of the `gridToCRS` matrix. */ if (objectiveToDisplay == null) { - objectiveToDisplay = updateObjectiveToDisplay(); + objectiveToDisplay = createObjectiveToDisplay(); } LinearTransform gridToCRS = objectiveToDisplay.inverse(); if (supplementalDimensions != 0) { @@ -1096,11 +1092,11 @@ public class Canvas extends Observable implements Localized { final LinearTransform oldObjectiveToDisplay = objectiveToDisplay; final CoordinateReferenceSystem oldObjectiveCRS = objectiveCRS; /* - * Set internal fields only after we successfully computed everything, in order to have a - * "all or nothing" behavior. + * Set internal fields only after we successfully computed everything, + * in order to have a "all or nothing" behavior. */ displayBounds.setEnvelope(newBounds); - updateObjectiveToDisplay(newObjectiveToDisplay); + setObjectiveToDisplayImpl(newObjectiveToDisplay); pointOfInterest = newPOI; objectivePOI = newPOI; objectiveCRS = newObjectiveCRS; @@ -1143,21 +1139,10 @@ public class Canvas extends Observable implements Localized { */ private void fireIfChanged(final LinearTransform oldValue, final LinearTransform newValue) { if (!Objects.equals(oldValue, newValue)) { - fireTransformChange(oldValue, newValue); + firePropertyChange(new TransformChangeEvent(this, oldValue, newValue)); } } - /** - * Fires a {@link TransformChangeEvent}. - * It is caller responsibility to verify that the old and new values are different. - * - * @param oldValue the old "objective to display" transform. - * @param newValue the new transform, or {@code null} for lazy computation. - */ - final void fireTransformChange(final LinearTransform oldValue, final LinearTransform newValue) { - firePropertyChange(new TransformChangeEvent(this, oldValue, newValue)); - } - /** * Returns the geographic bounding box encompassing the area shown on the display device. * If the {@linkplain #getObjectiveCRS() objective CRS} is not convertible to a geographic CRS, diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java index 3c69cf7a7e..b2b97e41e7 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayer.java @@ -260,5 +260,4 @@ public class MapLayer extends MapItem { } return Optional.empty(); } - } diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java index 4908b101c3..0ead548e0b 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/MapLayers.java @@ -177,7 +177,6 @@ public class MapLayers extends MapItem { for (MapItem i : components) { i.getEnvelope().ifPresent(envelopes::add); } - switch (envelopes.size()) { case 0 : return Optional.empty(); case 1 : return Optional.of(envelopes.get(0)); @@ -190,5 +189,4 @@ public class MapLayers extends MapItem { } } } - } 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 f5c20bb139..345b638217 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 @@ -137,7 +137,7 @@ public abstract class PlanarCanvas extends Canvas { * @see Canvas#objectiveToDisplay */ @Override - final LinearTransform updateObjectiveToDisplay() { + final LinearTransform createObjectiveToDisplay() { return new AffineTransform2D(objectiveToDisplay); } @@ -152,9 +152,9 @@ public abstract class PlanarCanvas extends Canvas { * @throws IllegalArgumentException if the given transform is not two-dimensional or is not affine. */ @Override - final void updateObjectiveToDisplay(final LinearTransform newValue) { + final void setObjectiveToDisplayImpl(final LinearTransform newValue) { objectiveToDisplay.setTransform(AffineTransforms2D.castOrCopy(newValue.getMatrix())); - super.updateObjectiveToDisplay(newValue); + super.setObjectiveToDisplayImpl(newValue); } /** @@ -163,14 +163,18 @@ public abstract class PlanarCanvas extends Canvas { * vector is in units of the {@linkplain #getObjectiveCRS() objective CRS} (typically metres on the map). * * @param before coordinate conversion to apply before the current <cite>objective to display</cite> transform. + * + * @see TransformChangeEvent#getObjectiveChange() */ public void transformObjectiveCoordinates(final AffineTransform before) { if (!before.isIdentity()) { final LinearTransform old = hasListener(OBJECTIVE_TO_DISPLAY_PROPERTY) ? getObjectiveToDisplay() : null; objectiveToDisplay.concatenate(before); - invalidateObjectiveToDisplay(); + super.setObjectiveToDisplayImpl(null); if (old != null) { - fireTransformChange(old, null); + final TransformChangeEvent event = new TransformChangeEvent(this, old, null); + event.objectiveChange2D = before; + firePropertyChange(event); } } } @@ -181,15 +185,17 @@ public abstract class PlanarCanvas extends Canvas { * vector is in pixel units. * * @param after coordinate conversion to apply after the current <cite>objective to display</cite> transform. + * + * @see TransformChangeEvent#getDisplayChange() */ public void transformDisplayCoordinates(final AffineTransform after) { if (!after.isIdentity()) { final LinearTransform old = hasListener(OBJECTIVE_TO_DISPLAY_PROPERTY) ? getObjectiveToDisplay() : null; objectiveToDisplay.preConcatenate(after); - invalidateObjectiveToDisplay(); + super.setObjectiveToDisplayImpl(null); if (old != null) { final TransformChangeEvent event = new TransformChangeEvent(this, old, null); - event.change = AffineTransforms2D.toMathTransform(after); + 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 756d7ee7fd..115f1a4692 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 @@ -16,16 +16,28 @@ */ package org.apache.sis.portrayal; +import java.util.Optional; +import java.util.logging.Logger; +import java.awt.geom.AffineTransform; import java.beans.PropertyChangeEvent; 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.internal.system.Modules; +import org.apache.sis.util.logging.Logging; /** - * A change in the zoom, pan or translation applied for viewing a map. All events fired by - * {@link Canvas} for the {@value Canvas#OBJECTIVE_TO_DISPLAY_PROPERTY} property are of this kind. - * This specialization provides a method for computing the difference between the old and new state. + * A change in the "objective to display" transform that {@code Canvas} uses for rendering data. + * That transform is updated frequently following gestures events such as zoom, translation or rotation. + * All events fired by {@link Canvas} for the {@value Canvas#OBJECTIVE_TO_DISPLAY_PROPERTY} property + * are instances of this class. + * This specialization provides methods for computing the difference between the old and new state. + * + * <h2>Multi-threading</h2> + * This class is <strong>not</strong> thread-safe. + * All listeners should process this event in the same thread. * * @author Martin Desruisseaux (Geomatys) * @version 1.3 @@ -39,15 +51,26 @@ public class TransformChangeEvent extends PropertyChangeEvent { /** * For cross-version compatibility. */ - private static final long serialVersionUID = 4065626270969827867L; + private static final long serialVersionUID = 4444752056666264066L; /** - * The change in display coordinates, computed when first needed. - * This field may be precomputed by the code that fired this event. + * The change from old coordinates to new coordinates, computed when first needed. * - * @see #getChangeInDisplayCoordinates() + * @see #getDisplayChange() + * @see #getObjectiveChange() + */ + private transient LinearTransform displayChange, objectiveChange; + + /** + * Value of {@link #displayChange} or {@link #objectiveChange} precomputed by the code that fired this event. + */ + AffineTransform displayChange2D, objectiveChange2D; + + /** + * Non-null if {@link #canNotCompute(String, NoninvertibleTransformException)} already reported an error. + * This is used for avoiding to report many times the same error. */ - LinearTransform change; + private transient Exception error; /** * Creates a new event for a change of the "objective to display" property. @@ -91,32 +114,132 @@ public class TransformChangeEvent extends PropertyChangeEvent { */ @Override public LinearTransform getNewValue() { - LinearTransform transform = (LinearTransform) super.getNewValue(); - if (transform == null) { - transform = getSource().getObjectiveToDisplay(); + LinearTransform value = (LinearTransform) super.getNewValue(); + if (value == null) { + value = getSource().getObjectiveToDisplay(); } - return transform; + return value; } /** - * Returns the change in display coordinates from the old state to the new state. - * If the "objective to display" transform changed because the users did a zoom, - * pan or translation, this is the transform representing that change in display - * coordinates. + * Returns the change from old objective coordinates to new objective coordinates. + * When the "objective to display" transform changed (e.g. because the user did a zoom, translation or rotation), + * this method expresses how the "real world" coordinates (typically in metres) of any point on the screen changed. + * + * <div class="note"><b>Example:</b> + * if the map is shifted 10 metres toward the right side of the canvas, then (assuming no rotation or axis flip) + * the <var>x</var> translation coefficient of the change is +10 (same sign than {@link #getDisplayChange()}). + * Note that it may correspond to any amount of pixels, depending on the zoom factor.</div> + * + * The {@link #getObjectiveChange2D()} method gives the same transform as a Java2D object. + * That change can be replicated on another canvas by giving the transform to + * {@link PlanarCanvas#transformObjectiveCoordinates(AffineTransform)}. + * + * @return the change in objective coordinates. Usually not {@code null}, + * unless one of the canvas is initializing or has a non-invertible transform. + */ + public LinearTransform getObjectiveChange() { + if (objectiveChange == null) { + if (objectiveChange2D != null) { + objectiveChange = AffineTransforms2D.toMathTransform(objectiveChange2D); + } else { + final LinearTransform oldValue = getOldValue(); + if (oldValue != null) { + final LinearTransform newValue = getNewValue(); + if (newValue != null) try { + objectiveChange = (LinearTransform) MathTransforms.concatenate(newValue, oldValue.inverse()); + } catch (NoninvertibleTransformException e) { + canNotCompute("getObjectiveChange", e); + } + } + } + } + return objectiveChange; + } + + /** + * Returns the change from old display coordinates to new display coordinates. + * When the "objective to display" transform changed (e.g. because the user did a zoom, translation or rotation), + * this method expresses how the display coordinates (typically pixels) of any given point on the map changed. + * + * <div class="note"><b>Example:</b> + * if the map is shifted 10 pixels toward the right side of the canvas, then (assuming no rotation or axis flip) + * the <var>x</var> translation coefficient of the change is +10: the points on the map which were located at + * <var>x</var>=0 pixel before the change are now located at <var>x</var>=10 pixels after the change.</div> + * + * The {@link #getDisplayChange2D()} method gives the same transform as a Java2D object. + * That change can be replicated on another canvas by giving the transform to + * {@link PlanarCanvas#transformDisplayCoordinates(AffineTransform)}. * - * @return the change in display coordinates, or {@code null} if the old or new transform is missing. - * @throws NoninvertibleTransformException if a singular matrix prevent the change to be computed. + * @return the change in display coordinates. Usually not {@code null}, + * unless one of the canvas is initializing or has a non-invertible transform. */ - public LinearTransform getChangeInDisplayCoordinates() throws NoninvertibleTransformException { - if (change == null) { - final LinearTransform oldValue = getOldValue(); - if (oldValue != null) { - final LinearTransform newValue = getNewValue(); - if (newValue != null) { - change = (LinearTransform) MathTransforms.concatenate(oldValue.inverse(), newValue); + public LinearTransform getDisplayChange() { + if (displayChange == null) { + if (displayChange2D != null) { + displayChange = AffineTransforms2D.toMathTransform(displayChange2D); + } else { + final LinearTransform oldValue = getOldValue(); + if (oldValue != null) { + final LinearTransform newValue = getNewValue(); + if (newValue != null) try { + displayChange = (LinearTransform) MathTransforms.concatenate(oldValue.inverse(), newValue); + } catch (NoninvertibleTransformException e) { + canNotCompute("getDisplayChange", e); + } } } } - return change; + return displayChange; + } + + /** + * Returns the change in objective coordinates as a Java2D affine transform. + * This method is suitable for two-dimensional canvas only. + * For performance reason, it does not clone the returned transform. + * + * @return the change in objective coordinates. <strong>Do not modify.</strong> + * + * @see #getObjectiveChange() + */ + public Optional<AffineTransform> getObjectiveChange2D() { + if (objectiveChange2D == null) try { + objectiveChange2D = AffineTransforms2D.castOrCopy(getObjectiveChange()); + } catch (IllegalArgumentException e) { + canNotCompute("getObjectiveChange2D", e); + } + return Optional.ofNullable(objectiveChange2D); + } + + /** + * Returns the change in display coordinates as a Java2D affine transform. + * This method is suitable for two-dimensional canvas only. + * For performance reason, it does not clone the returned transform. + * + * @return the change in display coordinates. <strong>Do not modify.</strong> + * + * @see #getDisplayChange() + */ + public Optional<AffineTransform> getDisplayChange2D() { + if (displayChange2D == null) try { + displayChange2D = AffineTransforms2D.castOrCopy(getDisplayChange()); + } catch (IllegalArgumentException e) { + canNotCompute("getDisplayChange2D", e); + } + return Optional.ofNullable(displayChange2D); + } + + /** + * Invoked when a change can not be computed. It should never happen because "objective to display" + * transforms should always be invertible. If this error nevertheless happens, consider the change + * as a missing optional information. + */ + private void canNotCompute(final String method, final Exception e) { + if (error == null) { + error = e; + Logging.recoverableException(Logger.getLogger(Modules.PORTRAYAL), TransformChangeEvent.class, method, e); + } else { + error.addSuppressed(e); + } } } diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java index b0575cbf33..387bab2757 100644 --- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java +++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/AffineTransforms2D.java @@ -59,7 +59,7 @@ public final class AffineTransforms2D extends Static { * * @param transform the transform to convert, or {@code null}. * @return the transform argument if it can be safely casted (including {@code null} argument) or converted. - * @throws IllegalArgumentException if the given transform can not be caster or converted. + * @throws IllegalArgumentException if the given transform can not be casted or converted. * * @see #toMathTransform(AffineTransform) */
