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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new ad92a26248 Initialize new windows to the same zoom level and map 
projection than the original window.
ad92a26248 is described below

commit ad92a262484ad348393f3d79a5157d99b0325567
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Jun 12 15:35:30 2022 +0200

    Initialize new windows to the same zoom level and map projection than the 
original window.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  47 +++---
 .../apache/sis/gui/coverage/CoverageExplorer.java  |  75 ++++------
 .../org/apache/sis/gui/coverage/ImageRequest.java  |  15 +-
 .../org/apache/sis/gui/dataset/WindowHandler.java  |  21 ++-
 .../java/org/apache/sis/gui/map/MapCanvas.java     | 157 ++++++++++++++-------
 .../org/apache/sis/internal/gui/PrivateAccess.java |  14 +-
 6 files changed, 203 insertions(+), 126 deletions(-)

diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
index b21bdc0f40..1b741939d2 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -50,20 +50,20 @@ import org.opengis.geometry.DirectPosition;
 import org.opengis.referencing.datum.PixelInCell;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.LinearTransform;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
+import org.apache.sis.coverage.Category;
 import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.coverage.grid.GridGeometry;
-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.referencing.CommonCRS;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.image.PlanarImage;
 import org.apache.sis.image.Interpolation;
-import org.apache.sis.coverage.Category;
-import org.apache.sis.coverage.SubspaceNotSpecifiedException;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.gui.map.MapCanvas;
 import org.apache.sis.gui.map.MapCanvasAWT;
@@ -158,7 +158,7 @@ public class CoverageCanvas extends MapCanvasAWT {
      * This is used for preventing never-ending loop when a change of resource 
causes a change of coverage
      * or conversely.
      *
-     * @see #onPropertySpecified(ObjectProperty)
+     * @see #onPropertySpecified(GridCoverageResource, GridCoverage, 
ObjectProperty, GridGeometry)
      */
     private boolean isCoverageAdjusting;
 
@@ -265,9 +265,9 @@ public class CoverageCanvas extends MapCanvasAWT {
         coverageProperty      = new SimpleObjectProperty<>(this, "coverage");
         sliceExtentProperty   = new SimpleObjectProperty<>(this, 
"sliceExtent");
         interpolationProperty = new SimpleObjectProperty<>(this, 
"interpolation", data.processor.getInterpolation());
-        resourceProperty     .addListener((p,o,n) -> onPropertySpecified(n, 
null, coverageProperty));
-        coverageProperty     .addListener((p,o,n) -> onPropertySpecified(null, 
n, resourceProperty));
-        sliceExtentProperty  .addListener((p,o,n) -> 
onPropertySpecified(getResource(), getCoverage(), null));
+        resourceProperty     .addListener((p,o,n) -> onPropertySpecified(n, 
null, coverageProperty, null));
+        coverageProperty     .addListener((p,o,n) -> onPropertySpecified(null, 
n, resourceProperty, null));
+        sliceExtentProperty  .addListener((p,o,n) -> 
onPropertySpecified(getResource(), getCoverage(), null, null));
         interpolationProperty.addListener((p,o,n) -> 
onInterpolationSpecified(n));
     }
 
@@ -459,6 +459,7 @@ public class CoverageCanvas extends MapCanvasAWT {
     public void setObjectiveCRS(final CoordinateReferenceSystem newValue, 
DirectPosition anchor) throws RenderException {
         final Long id = LogHandler.loadingStart(getResource());
         try {
+            // With `LogHandler` because this call may cause searches in EPSG 
database.
             super.setObjectiveCRS(newValue, anchor);
         } finally {
             LogHandler.loadingStop(id);
@@ -478,6 +479,7 @@ public class CoverageCanvas extends MapCanvasAWT {
     public void setGridGeometry(final GridGeometry newValue) throws 
RenderException {
         final Long id = LogHandler.loadingStart(getResource());
         try {
+            // With `LogHandler` because this call may cause searches in EPSG 
database.
             super.setGridGeometry(newValue);
         } finally {
             LogHandler.loadingStop(id);
@@ -495,14 +497,17 @@ public class CoverageCanvas extends MapCanvasAWT {
         final GridCoverageResource resource;
         final GridCoverage coverage;
         final GridExtent sliceExtent;
+        final GridGeometry zoom;
         if (request != null) {
             resource    = request.resource;
             coverage    = request.coverage;
             sliceExtent = request.slice;
+            zoom        = request.zoom;
         } else {
             resource    = null;
             coverage    = null;
             sliceExtent = null;
+            zoom        = null;
         }
         if (getResource() != resource || getCoverage() != coverage || 
getSliceExtent() != sliceExtent) {
             final boolean p = isCoverageAdjusting;
@@ -514,7 +519,7 @@ public class CoverageCanvas extends MapCanvasAWT {
             } finally {
                 isCoverageAdjusting = p;
             }
-            onPropertySpecified(resource, coverage, null);
+            onPropertySpecified(resource, coverage, null, zoom);
         }
     }
 
@@ -527,9 +532,10 @@ public class CoverageCanvas extends MapCanvasAWT {
      * @param  resource  the new resource, or {@code null} if none.
      * @param  coverage  the new coverage, or {@code null} if none.
      * @param  toClear   the property which is an alternative to the property 
that has been set.
+     * @param  zoom      initial "objective to display" transform to use, or 
{@code null} for automatic.
      */
     private void onPropertySpecified(final GridCoverageResource resource, 
final GridCoverage coverage,
-                                     final ObjectProperty<?> toClear)
+                                     final ObjectProperty<?> toClear, final 
GridGeometry zoom)
     {
         hasCoverageOrResource = (resource != null || coverage != null);
         if (isCoverageAdjusting) {
@@ -593,7 +599,7 @@ public class CoverageCanvas extends MapCanvasAWT {
                  */
                 @Override protected void succeeded() {
                     runAfterRendering(() -> {
-                        setNewSource(getValue(), ranges);
+                        setNewSource(getValue(), ranges, zoom);
                         requestRepaint();                   // Cause `Worker` 
class to be executed.
                     });
                 }
@@ -637,8 +643,9 @@ public class CoverageCanvas extends MapCanvasAWT {
      *
      * @param  domain  the multi-dimensional grid geometry, or {@code null} if 
there is no data.
      * @param  ranges  descriptions of bands, or {@code null} if there is no 
data.
+     * @param  zoom    initial "objective to display" transform to use, or 
{@code null} for automatic.
      */
-    private void setNewSource(GridGeometry domain, final List<SampleDimension> 
ranges) {
+    private void setNewSource(GridGeometry domain, final List<SampleDimension> 
ranges, final GridGeometry zoom) {
         if (TRACE) {
             trace("setNewSource(…): the new domain of data is:%n\t%s", domain);
         }
@@ -679,6 +686,7 @@ public class CoverageCanvas extends MapCanvasAWT {
             }
         }
         data.setImageSpace(domain, ranges, xyDimensions);
+        initialize(zoom);
         setObjectiveBounds(bounds);
     }
 
@@ -1134,7 +1142,14 @@ public class CoverageCanvas extends MapCanvasAWT {
      * Invoked when an exception occurred while computing a transform but the 
painting process can continue.
      */
     private static void unexpectedException(final Exception e) {
-        Logging.unexpectedException(getLogger(Modules.APPLICATION), 
CoverageCanvas.class, "render", e);
+        unexpectedException("render", e);
+    }
+
+    /**
+     * Invoked when an exception occurred. The declared source method should 
be a public or protected method.
+     */
+    static void unexpectedException(final String method, final Exception e) {
+        Logging.unexpectedException(getLogger(Modules.APPLICATION), 
CoverageCanvas.class, method, e);
     }
 
     /**
@@ -1156,7 +1171,7 @@ public class CoverageCanvas extends MapCanvasAWT {
         if (TRACE) {
             trace("clear()");
         }
-        setNewSource(null, null);
+        setNewSource(null, null, null);
         super.clear();
     }
 
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
index 43a9ff3752..ae6a3ba058 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageExplorer.java
@@ -36,13 +36,13 @@ import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.internal.gui.Resources;
 import org.apache.sis.internal.gui.ToolbarButton;
 import org.apache.sis.internal.gui.NonNullObjectProperty;
+import org.apache.sis.internal.gui.PrivateAccess;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.portrayal.RenderException;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.gui.dataset.WindowHandler;
-import org.apache.sis.gui.dataset.WindowManager;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.gui.Widget;
-import org.apache.sis.internal.gui.PrivateAccess;
 
 
 /**
@@ -214,7 +214,7 @@ public class CoverageExplorer extends Widget {
      * Handler of the window showing this coverage view. This is used for 
creating new windows.
      * Created when first needed for giving to subclasses a chance to complete 
initialization.
      *
-     * @see #window()
+     * @see #getWindowHandler()
      */
     private WindowHandler window;
 
@@ -264,58 +264,35 @@ public class CoverageExplorer extends Widget {
      * @param  source  the source explorer from which to take the initial 
coverage or resource.
      *
      * @since 1.2
-     *
-     * @deprecated Replaced by {@code 
source.getImageRequest().ifPresent(newExplorer::setCoverage);}.
      */
-    @Deprecated
     public CoverageExplorer(final CoverageExplorer source) {
         this(source.getViewType());
+        window = PrivateAccess.newWindowHandler.apply(source.window, this);
         source.getImageRequest().ifPresent(this::setCoverage);
-    }
-
-    /*
-     * Hack for giving access outside this package to a field that we do not 
want to make public.
-     * This is a way to simulate the "friend" keyword in C++.
-     */
-    static {
-        PrivateAccess.initWindowHandler = CoverageExplorer::initWindowHandler;
-    }
-
-    /**
-     * Initializes {@link #window} to the given value. This method should be 
invoked soon after
-     * construction and can be invoked only once.
-     */
-    private void initWindowHandler(final WindowHandler handler) {
-        assert Platform.isFxApplicationThread() && window == null : window;
-        window = handler;
-    }
-
-    /**
-     * Returns the handler of the window showing this coverage view. Created 
when first needed
-     * for giving to subclass constructors a chance to complete their 
initialization before the
-     * {@code this} reference is passed to {@link WindowHandler} constructor.
-     */
-    private WindowHandler window() {
-        assert Platform.isFxApplicationThread();
-        if (window == null) {
-            window = WindowHandler.create(this);
-        }
-        return window;
+        PrivateAccess.finishWindowHandler.accept(window);
     }
 
     /**
-     * Returns a manager of windows showing different view of the coverage.
-     * Those windows are created when the user click on the "New window" 
button.
+     * Returns the handler of the window showing this coverage view.
+     * Those windows are created when the user clicks on the "New window" 
button.
      * Each window provides the area where data are shown and where the user 
interacts.
      * The window can be a JavaFX top-level window ({@link Stage}), but not 
necessarily.
      * It may also be a tile in a mosaic of windows.
      *
-     * @return the manager of windows created by the "New window" button.
+     * @return the handler of the window showing this coverage view.
      *
      * @since 1.3
      */
-    public final WindowManager getWindowManager() {
-        return window().manager;
+    public final WindowHandler getWindowHandler() {
+        assert Platform.isFxApplicationThread();
+        /*
+         * Created when first needed for giving to subclass constructors a 
chance to complete
+         * their initialization before `this` reference is passed to 
`WindowHandler` constructor.
+         */
+        if (window == null) {
+            window = WindowHandler.create(this);
+        }
+        return window;
     }
 
     /**
@@ -342,7 +319,7 @@ public class CoverageExplorer extends Widget {
         if (c == null) {
             switch (type) {
                 case TABLE: c = new GridControls(this); break;
-                case IMAGE: c = new CoverageControls(this, window()); break;
+                case IMAGE: c = new CoverageControls(this, 
getWindowHandler()); break;
                 default: throw new AssertionError(type);
             }
             views.put(type, c);
@@ -579,7 +556,6 @@ public class CoverageExplorer extends Widget {
      *
      * @param  source  the coverage or resource to load, or {@code null} if 
none.
      *
-     * @see #getImageRequest()
      * @see GridView#setImage(ImageRequest)
      */
     public final void setCoverage(final ImageRequest source) {
@@ -655,14 +631,19 @@ public class CoverageExplorer extends Widget {
      * @return the request to give to another explorer for showing the same 
coverage.
      *
      * @see #setCoverage(ImageRequest)
-     *
-     * @since 1.3
      */
-    public final Optional<ImageRequest> getImageRequest() {
+    private Optional<ImageRequest> getImageRequest() {
         final GridCoverageResource resource = getResource();
         final GridCoverage coverage = getCoverage();
         if (resource != null || coverage != null) {
-            return Optional.of(new ImageRequest(resource, coverage));
+            final ImageRequest request = new ImageRequest(resource, coverage);
+            final CoverageControls c = (CoverageControls) 
views.get(View.IMAGE);
+            if (c != null) try {
+                request.zoom = c.view.getGridGeometry();
+            } catch (RenderException e) {
+                CoverageCanvas.unexpectedException("getGridGeometry", e);
+            }
+            return Optional.of(request);
         } else {
             return Optional.empty();
         }
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
index d9f1c58e09..b0b620e6a4 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageRequest.java
@@ -17,12 +17,12 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.Optional;
-import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.util.ArgumentChecks;
 
 
 /**
@@ -78,6 +78,13 @@ public class ImageRequest {
      */
     final GridExtent slice;
 
+    /**
+     * The initial objective CRS and zoom to use in a new {@link 
CoverageCanvas}, or {@code null} if none.
+     * This is used only if we want to create a new canvas initialized to the 
same viewing region and zoom
+     * level than an existing canvas.
+     */
+    GridGeometry zoom;
+
     /**
      * Creates a new request with both a resource and a coverage. At least one 
argument shall be non-null.
      * If both arguments are non-null, then {@code data} must be the result of 
reading the given resource.
@@ -135,11 +142,11 @@ public class ImageRequest {
      */
     public ImageRequest(final GridCoverage source, final GridExtent slice) {
         ArgumentChecks.ensureNonNull("source", source);
+        this.coverage = source;
+        this.slice    = slice;
         this.resource = null;
         this.domain   = null;
         this.range    = null;
-        this.coverage = source;
-        this.slice    = slice;
     }
 
     /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
index 68aa91620f..d3b1a46c66 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/dataset/WindowHandler.java
@@ -336,6 +336,21 @@ public abstract class WindowHandler {
             this.widget = widget;
         }
 
+        /**
+         * Creates a new handler for duplicating an existing window.
+         * For indirect usage by {@link #duplicate()}.
+         *
+         * @param creator  the handler which is duplicated.
+         * @param widget   the widget providing the new view of the resource.
+         */
+        private ForCoverage(final WindowHandler creator, final 
CoverageExplorer widget) {
+            this(creator, null, widget);
+        }
+        static {
+            PrivateAccess.newWindowHandler = ForCoverage::new;
+            PrivateAccess.finishWindowHandler = WindowHandler::finish;
+        }
+
         /**
          * The resource shown in the {@linkplain #window window}, or {@code 
null} if unspecified.
          */
@@ -375,11 +390,7 @@ public abstract class WindowHandler {
          */
         @Override
         public WindowHandler duplicate() {
-            final CoverageExplorer explorer = new 
CoverageExplorer(widget.getViewType());
-            final ForCoverage handler = new ForCoverage(this, null, explorer);
-            PrivateAccess.initWindowHandler.accept(explorer, handler);
-            widget.getImageRequest().ifPresent(explorer::setCoverage);
-            return handler.finish();
+            return new CoverageExplorer(widget).getWindowHandler();
         }
 
         /**
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java 
b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
index a9411566a3..d911fa447a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/MapCanvas.java
@@ -64,9 +64,9 @@ import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.AbstractEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
 import org.apache.sis.coverage.grid.GridGeometry;
 import org.apache.sis.coverage.grid.GridExtent;
-import org.apache.sis.coverage.grid.GridOrientation;
 import org.apache.sis.gui.referencing.PositionableProjection;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
 import org.apache.sis.util.ArraysExt;
@@ -191,13 +191,22 @@ public abstract class MapCanvas extends PlanarCanvas {
      */
     protected final StackPane fixedPane;
 
+    /**
+     * The data bounds to use for computing the initial value of {@link 
#objectiveToDisplay}.
+     * We differ this recomputation until all parameters are known.
+     *
+     * @see #setObjectiveBounds(Envelope)
+     * @see #invalidObjectiveToDisplay
+     */
+    private Envelope objectiveBounds;
+
     /**
      * The data bounds to use for computing the initial value of {@link 
#objectiveToDisplay}.
      * Optionally contains the initial "objective to display" CRS to use if a 
predetermined
      * value is desired instead of an automatically computed one. The grid 
extent is ignored,
      * except for fetching the grid center if a non-linear transform needs to 
be linearized.
      *
-     * @see #setInitialState(GridGeometry)
+     * @see #initialize(GridGeometry)
      * @see #invalidObjectiveToDisplay
      */
     private GridGeometry initialState;
@@ -247,7 +256,8 @@ public abstract class MapCanvas extends PlanarCanvas {
      * Whether {@link #objectiveToDisplay} needs to be recomputed.
      * We differ this recomputation until all parameters are known.
      *
-     * @see #initialState
+     * @see #objectiveBounds
+     * @see #objectiveToDisplay
      */
     private boolean invalidObjectiveToDisplay;
 
@@ -369,6 +379,46 @@ public abstract class MapCanvas extends PlanarCanvas {
         error = new ReadOnlyObjectWrapper<>(this, "error");
     }
 
+    /**
+     * Sets the objective bounds and/or the zoom level and objective CRS to 
use for the initial view of data.
+     * The {@code visibleArea} {@linkplain 
GridGeometry#getCoordinateReferenceSystem() CRS} defines the initial
+     * {@linkplain #setObjectiveCRS(CoordinateReferenceSystem, DirectPosition) 
objective CRS} of this canvas.
+     * The {@code visibleArea} {@linkplain GridGeometry#getEnvelope() 
envelope} defines the (usually constant)
+     * {@linkplain #setObjectiveBounds(Envelope) objective bounds} of this 
canvas.
+     * In addition if {@code visibleArea} contains a {@linkplain 
GridGeometry#getGridToCRS grid to CRS} transform,
+     * its inverse will define the initial {@linkplain #setObjectiveToDisplay 
objective to display} transform
+     * (which in turn defines the initial viewed area and zoom level).
+     *
+     * <p>This method should be invoked only when new data have been loaded, 
or when the caller wants
+     * to discard any zoom or translation and reset the view to the given 
bounds. This method does not
+     * cause new repaint event; {@link #requestRepaint()} must be invoked by 
the caller if desired.</p>
+     *
+     * @param  visibleArea  bounding box, objective CRS and or initial zoom 
level,
+     *         or {@code null} if unknown (in which case an identity transform 
will be set).
+     * @throws MismatchedDimensionException if the given grid geometry is not 
two-dimensional.
+     *
+     * @see #setObjectiveBounds(Envelope)
+     * @see #getGridGeometry()
+     *
+     * @since 1.3
+     */
+    protected void initialize(final GridGeometry visibleArea) {
+        Envelope bounds = null;
+        if (visibleArea != null) {
+            if (visibleArea.isDefined(GridGeometry.ENVELOPE)) {
+                bounds = visibleArea.getEnvelope();
+                ArgumentChecks.ensureDimensionMatches("visibleArea", 
BIDIMENSIONAL, bounds);
+            }
+            if (visibleArea.isDefined(GridGeometry.GRID_TO_CRS)) {
+                ArgumentChecks.ensureDimensionsMatch("visibleArea", 
BIDIMENSIONAL, BIDIMENSIONAL,
+                        visibleArea.getGridToCRS(PixelInCell.CELL_CENTER));
+            }
+        }
+        objectiveBounds = bounds;
+        initialState = visibleArea;
+        invalidObjectiveToDisplay = true;
+    }
+
     /**
      * Returns the bounds of the content in {@link #floatingPane} coordinates, 
or {@code null} if unknown.
      * Some subclasses may compute a larger image than the widget size for 
better visual transition during
@@ -772,10 +822,25 @@ public abstract class MapCanvas extends PlanarCanvas {
         }
     }
 
+    /**
+     * Returns the data bounds to use for computing the initial "objective to 
display" transform.
+     * This is the value specified by the last call to {@link 
#setObjectiveBounds(Envelope)}.
+     * The coordinate reference system of the returned envelope defines also 
the CRS which
+     * is restored when the {@link #reset()} method is invoked.
+     *
+     * @return the data bounds to use for computing the initial "objective to 
display" transform,
+     *         or {@code null} if unspecified.
+     *
+     * @since 1.3
+     */
+    public Envelope getObjectiveBounds() {
+        return objectiveBounds;
+    }
+
     /**
      * Sets the data bounds to use for computing the initial value of {@link 
#objectiveToDisplay}.
-     * Invoking this method also sets the {@link #getObjectiveCRS() objective 
CRS} of this canvas
-     * to the CRS of given envelope.
+     * Invoking this method also sets the initial {@linkplain 
#getObjectiveCRS() objective CRS}
+     * of this canvas to the CRS of given envelope.
      *
      * <p>This method should be invoked only when new data have been loaded, 
or when the caller wants
      * to discard any zoom or translation and reset the view to the given 
bounds. This method does not
@@ -787,39 +852,25 @@ public abstract class MapCanvas extends PlanarCanvas {
      *
      * @see #setObjectiveCRS(CoordinateReferenceSystem, DirectPosition)
      */
-    protected void setObjectiveBounds(final Envelope visibleArea) {
+    public void setObjectiveBounds(final Envelope visibleArea) {
         ArgumentChecks.ensureDimensionMatches("visibleArea", BIDIMENSIONAL, 
visibleArea);
-        if (visibleArea == null) {
-            initialState = null;
-        } else {
-            setInitialState(new GridGeometry(null, visibleArea, 
GridOrientation.HOMOTHETY));
-        }
+        objectiveBounds = ImmutableEnvelope.castOrCopy(visibleArea);
+        invalidObjectiveToDisplay = true;
     }
 
     /**
-     * Sets the data bounds and initial "objective to display" transform to 
use. This method sets the same
-     * information than {@link #setObjectiveBounds(Envelope)} using the 
envelope of the given grid geometry.
-     * But if in addition a "grid to CRS" transform is specified, it will be 
used for defining the initial
-     * value of the "objective to display" transform.
-     *
-     * <p>Note that the objective bounds (defined by grid geometry envelope) 
are usually constant as long
-     * as the data do not change, while the other properties may change under 
user interactions.
-     * This is the reason why the given argument is an <em>initial</em> 
state.</p>
-     *
-     * <p>A typical use case for this method is to give in argument the {@link 
#getGridGeometry()} value
-     * of another canvas for initializing this canvas to the same geographic 
region and zoom level,
-     * assuming that the two canvas and rendering the same data.</p>
-     *
-     * @param  visibleArea  bounding box and initial zoom level to use for the 
canvas.
-     *
-     * @see #setGridGeometry(GridGeometry)
+     * Sets the conversion from objective CRS to display coordinate system.
+     * Invoking this method has the effect of changing the viewed area, the 
zoom level or the rotation of the map.
+     * Caller needs to invoke {@link #requestRepaint()} after this method call 
(this is not done automatically).
      *
-     * @since 1.3
+     * @param  newValue  the new <cite>objective to display</cite> conversion.
+     * @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.
      */
-    protected void setInitialState(final GridGeometry visibleArea) {
-        ArgumentChecks.ensureNonNull("visibleArea", visibleArea);
-        initialState = visibleArea;
-        invalidObjectiveToDisplay = true;
+    @Override
+    public void setObjectiveToDisplay(final LinearTransform newValue) throws 
RenderException {
+        super.setObjectiveToDisplay(newValue);
+        invalidObjectiveToDisplay = false;
     }
 
     /**
@@ -1068,28 +1119,28 @@ public abstract class MapCanvas extends PlanarCanvas {
                         new long[] {Math.round(target.getMinX()), 
Math.round(target.getMinY())},
                         new long[] {Math.round(target.getMaxX()), 
Math.round(target.getMaxY())}, false);
                 /*
-                 * If `setObjectiveBounds(…)` has been invoked (as it should 
be), initialize the affine
-                 * transform to values which will allow this canvas to contain 
fully the objective bounds.
-                 * Otherwise the transform is initialized to an identity 
transform (should not happen often).
-                 * If a CRS is present, it is used for deciding if we need to 
swap or flip axes.
+                 * The main purpose of this block is to find the initial value 
of the `objectiveToDisplay` transform
+                 * (named `crsToDisplay` here). If that value was explicitly 
specified by a call to `initialize(…)`,
+                 * use it as-is. Otherwise we will compute it from the bounds 
of data.
                  */
-                Envelope objectiveBounds = null;
-                CoordinateReferenceSystem objectiveCRS = null;
+                CoordinateReferenceSystem objectiveCRS;
                 LinearTransform crsToDisplay = null;
-                if (initialState != null) {
-                    if (initialState.isDefined(GridGeometry.GRID_TO_CRS)) {
-                        crsToDisplay = 
initialState.getLinearGridToCRS(PixelInCell.CELL_CORNER).inverse();
-                    }
-                    if (initialState.isDefined(GridGeometry.ENVELOPE)) {
-                        objectiveBounds = initialState.getEnvelope();
-                    }
-                    if (initialState.isDefined(GridGeometry.CRS)) {
-                        objectiveCRS = 
initialState.getCoordinateReferenceSystem();
-                    }
-                }
-                if (crsToDisplay == null) {
+                final GridGeometry init = initialState;
+                initialState = null;                                    // For 
using `objectiveBounds` next times.
+                if (init != null && init.isDefined(GridGeometry.GRID_TO_CRS)) {
+                    crsToDisplay = 
init.getLinearGridToCRS(PixelInCell.CELL_CORNER).inverse();
+                    objectiveCRS = null;    // Value will be fetched after the 
`else` block.
+                } else {
+                    /*
+                     * If `setObjectiveBounds(…)` has been invoked (as it 
should be), initialize the affine
+                     * transform to values which will allow this canvas to 
contain fully the objective bounds.
+                     * Otherwise the transform is initialized to an identity 
transform (should not happen often).
+                     * If a CRS is present, it is used for deciding if we need 
to swap or flip axes.
+                     */
+                    final Envelope objectiveBounds = getObjectiveBounds();
                     if (objectiveBounds != null) {
                         final MatrixSIS m;
+                        objectiveCRS = 
objectiveBounds.getCoordinateReferenceSystem();
                         if (objectiveCRS != null) {
                             AxisDirection[] srcAxes = 
CoordinateSystems.getAxisDirections(objectiveCRS.getCoordinateSystem());
                             m = Matrices.createTransform(objectiveBounds, 
srcAxes, target, toDisplayDirections(srcAxes));
@@ -1104,7 +1155,11 @@ public abstract class MapCanvas extends PlanarCanvas {
                     }
                 }
                 if (objectiveCRS == null) {
-                    objectiveCRS = 
extent.toEnvelope(crsToDisplay.inverse()).getCoordinateReferenceSystem();
+                    if (init.isDefined(GridGeometry.CRS)) {
+                        objectiveCRS = init.getCoordinateReferenceSystem();
+                    } else {
+                        objectiveCRS = 
extent.toEnvelope(crsToDisplay.inverse()).getCoordinateReferenceSystem();
+                    }
                     /*
                      * Above code tried to provide a non-null CRS on a "best 
effort" basis. The objective CRS
                      * may still be null, there is no obvious answer against 
that. It is not the display CRS
diff --git 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
index 68a15aaab3..5415c8a807 100644
--- 
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
+++ 
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/PrivateAccess.java
@@ -16,7 +16,8 @@
  */
 package org.apache.sis.internal.gui;
 
-import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.BiFunction;
 import org.apache.sis.gui.coverage.CoverageExplorer;
 import org.apache.sis.gui.dataset.WindowHandler;
 
@@ -40,7 +41,14 @@ public final class PrivateAccess {
     }
 
     /**
-     * A setter method for {@link CoverageExplorer#window}. Shall be invoked 
in JavaFX thread.
+     * Accessor for {@link 
org.apache.sis.gui.dataset.WindowHandler.ForCoverage} constructor.
+     * Used for assigning {@link CoverageExplorer#window} when duplicating an 
existing window.
+     * Shall be invoked in JavaFX thread.
      */
-    public static volatile BiConsumer<CoverageExplorer, WindowHandler> 
initWindowHandler;
+    public static volatile BiFunction<WindowHandler, CoverageExplorer, 
WindowHandler> newWindowHandler;
+
+    /**
+     * Accessor for {@link WindowHandler#finish()} method. Shall be invoked in 
JavaFX thread.
+     */
+    public static volatile Consumer<WindowHandler> finishWindowHandler;
 }

Reply via email to