| /* |
| * 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.Objects; |
| import java.util.Optional; |
| 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.util.logging.Logging; |
| |
| |
| /** |
| * 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 |
| * |
| * @see Canvas#OBJECTIVE_TO_DISPLAY_PROPERTY |
| * |
| * @since 1.3 |
| */ |
| public class TransformChangeEvent extends PropertyChangeEvent { |
| /** |
| * For cross-version compatibility. |
| */ |
| 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, |
| |
| /** |
| * A relative interim change has been applied but is not yet reflected in the "objective to display" transform. |
| * This kind of change is not fired by {@link PlanarCanvas} but may be fired by subclasses such as |
| * {@link org.apache.sis.gui.map.MapCanvas}. That class provides immediate feedback to users |
| * with a temporary visual change before to perform more expensive rendering in background. |
| */ |
| INTERIM; |
| |
| /** |
| * 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() && ordinal() < INTERIM.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. |
| * |
| * @see #getDisplayChange() |
| * @see #getObjectiveChange() |
| */ |
| private transient LinearTransform displayChange, objectiveChange; |
| |
| /** |
| * Value of {@link #displayChange} or {@link #objectiveChange} precomputed by the code that fired this event. |
| * If not precomputed, will be computed when first needed. |
| */ |
| private 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. |
| */ |
| private transient Exception error; |
| |
| /** |
| * Creates a new event for a change of the "objective to display" property. |
| * The old and new transforms should not be null, except on initialization or for lazy computation: |
| * a null {@code newValue} means to take the value from {@link Canvas#getObjectiveToDisplay()} when needed. |
| * |
| * @param source the canvas that fired the event. |
| * @param oldValue the old "objective to display" transform, or {@code null} if none. |
| * @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, |
| final Reason reason) |
| { |
| super(source, Canvas.OBJECTIVE_TO_DISPLAY_PROPERTY, oldValue, newValue); |
| this.reason = Objects.requireNonNull(reason); |
| } |
| |
| /** |
| * Creates a new event for an incremental change of the "objective to display" property. |
| * The incremental change can be specified by the {@code objective} and/or the {@code display} argument. |
| * Usually only one of those two arguments is non-null. |
| * |
| * @param source the canvas that fired the event. |
| * @param oldValue the old "objective to display" transform, or {@code null} if none. |
| * @param newValue the new transform, or {@code null} for lazy computation. |
| * @param objective the incremental change in objective coordinates, or {@code null} for lazy computation. |
| * @param display the incremental change in display coordinates, 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, |
| final AffineTransform objective, final AffineTransform display, final Reason reason) |
| { |
| this(source, oldValue, newValue, reason); |
| objectiveChange2D = objective; |
| displayChange2D = display; |
| } |
| |
| /** |
| * Quick and non-overrideable check about whether the specified source is the source of this event. |
| */ |
| final boolean isSameSource(final Canvas source) { |
| return super.getSource() == source; |
| } |
| |
| /** |
| * Returns the canvas on which this event initially occurred. |
| * |
| * @return the canvas on which this event initially occurred. |
| */ |
| @Override |
| public Canvas getSource() { |
| 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. |
| * |
| * @return the old "objective to display" transform, or {@code null} if none. |
| */ |
| @Override |
| public LinearTransform getOldValue() { |
| return (LinearTransform) super.getOldValue(); |
| } |
| |
| /** |
| * Gets the new "objective to display" transform. |
| * It should be the current value of {@link Canvas#getObjectiveToDisplay()}. |
| * |
| * @return the new "objective to display" transform. |
| */ |
| @Override |
| public LinearTransform getNewValue() { |
| LinearTransform value = (LinearTransform) super.getNewValue(); |
| if (value == null) { |
| value = getSource().getObjectiveToDisplay(); |
| } |
| return value; |
| } |
| |
| /** |
| * 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 as {@link #getDisplayChange()}). |
| * Note that it may correspond to any number 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. Usually not {@code null}, |
| * unless one of the canvas is initializing or has a non-invertible transform. |
| */ |
| 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 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 { |
| 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); |
| } |
| |
| /** |
| * 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 { |
| 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); |
| } |
| |
| /** |
| * Invoked when a change cannot 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(Observable.LOGGER, TransformChangeEvent.class, method, e); |
| } else { |
| error.addSuppressed(e); |
| } |
| } |
| } |