sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Simplify a little bit the handling of CRS changes in CoverageCanvas. We abandon the "intermediate update" done during coverage loading (GridCoverage changes shown before the RenderedImage was available) because it was confusing for both the user and the developer. Instead the CoverageCanvas content is updated in a "all or nothing" way.
Date Fri, 15 May 2020 16:59:56 GMT
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 629eaf2  Simplify a little bit the handling of CRS changes in CoverageCanvas. We
abandon the "intermediate update" done during coverage loading (GridCoverage changes shown
before the RenderedImage was available) because it was confusing for both the user and the
developer. Instead the CoverageCanvas content is updated in a "all or nothing" way.
629eaf2 is described below

commit 629eaf218f1f10d68fcbaf4eafc0e2ab1545bed5
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri May 15 18:55:33 2020 +0200

    Simplify a little bit the handling of CRS changes in CoverageCanvas.
    We abandon the "intermediate update" done during coverage loading
    (GridCoverage changes shown before the RenderedImage was available)
    because it was confusing for both the user and the developer.
    Instead the CoverageCanvas content is updated in a "all or nothing" way.
---
 .../apache/sis/gui/coverage/CoverageCanvas.java    |  33 ++--
 .../apache/sis/gui/coverage/CoverageControls.java  |  24 ++-
 .../apache/sis/gui/coverage/CoverageExplorer.java  |   7 +-
 .../java/org/apache/sis/gui/coverage/GridView.java | 115 +++++++++++--
 .../org/apache/sis/gui/coverage/ImageLoader.java   | 190 ---------------------
 .../org/apache/sis/gui/coverage/ImageRequest.java  | 126 ++++++++++++--
 .../org/apache/sis/gui/coverage/RenderingData.java |  18 +-
 .../org/apache/sis/coverage/grid/GridGeometry.java |  28 +--
 .../java/org/apache/sis/portrayal/Observable.java  |  13 +-
 9 files changed, 285 insertions(+), 269 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 b8b6003..b774cf0 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
@@ -27,9 +27,12 @@ import javafx.scene.layout.Background;
 import javafx.scene.layout.BackgroundFill;
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
+import javafx.beans.value.ObservableValue;
+import javafx.beans.value.WritableValue;
 import javafx.concurrent.Task;
 import org.opengis.geometry.Envelope;
 import org.opengis.util.FactoryException;
+import org.opengis.referencing.ReferenceSystem;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -302,8 +305,10 @@ public class CoverageCanvas extends MapCanvasAWT {
             data               = canvas.data.clone();
             objectiveCRS       = canvas.getObjectiveCRS();
             objectiveToDisplay = canvas.getObjectiveToDisplay();
-            resampledImage     = canvas.resampledImages.get(Stretching.NONE);
-            stretchedImage     = canvas.resampledImages.get(data.selectedStretching);
+            if (data.validateCRS(objectiveCRS)) {
+                resampledImage = canvas.resampledImages.get(Stretching.NONE);
+                stretchedImage = canvas.resampledImages.get(data.selectedStretching);
+            }
         }
 
         /**
@@ -374,27 +379,27 @@ public class CoverageCanvas extends MapCanvasAWT {
     }
 
     /**
-     * Sets the Coordinate Reference System in which all data are transformed before displaying.
-     * The new CRS must be compatible with the previous CRS, i.e. a coordinate operation
between
-     * the two CRSs shall exist. If the CRS can not be set to the specified value, then an
error
-     * message is shown in the status bar.
+     * Invoked when the user changed the CRS from a JavaFX control. If the CRS can not be
set to the specified
+     * value, then an error message is shown in the status bar and the property is reset
to its previous value.
      *
      * @param  crs  the new Coordinate Reference System in which to transform all data before
displaying.
+     * @param  property  the property to reset if the operation fails.
      */
-    @Override
-    public void setObjectiveCRS(final CoordinateReferenceSystem crs) {
-        resampledImages.clear();
-        data.clearCRS();
-        try {
-            super.setObjectiveCRS(crs);
+    final void setObjectiveCRS(final CoordinateReferenceSystem crs, final ObservableValue<?
extends ReferenceSystem> property) {
+        final CoordinateReferenceSystem previous = getObjectiveCRS();
+        if (crs != previous) try {
+            setObjectiveCRS(crs);
+            requestRepaint();
         } catch (Exception e) {
+            if (property instanceof WritableValue<?>) {
+                ((WritableValue<ReferenceSystem>) property).setValue(previous);
+            }
             errorOccurred(e);
             final Locale locale = getLocale();
             final Resources i18n = Resources.forLocale(locale);
             ExceptionReporter.show(null, i18n.getString(Resources.Keys.CanNotUseRefSys_1,
-                    IdentifiedObjects.getDisplayName(crs, locale)), e);
+                                   IdentifiedObjects.getDisplayName(crs, locale)), e);
         }
-        requestRepaint();
     }
 
     /**
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
index a131c5d..91b0284 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/CoverageControls.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sis.gui.coverage;
 
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
 import javafx.scene.control.Accordion;
 import javafx.scene.control.ColorPicker;
 import javafx.scene.control.Control;
@@ -44,7 +46,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * @since   1.1
  * @module
  */
-final class CoverageControls extends Controls {
+final class CoverageControls extends Controls implements PropertyChangeListener {
     /**
      * The component for showing sample values.
      */
@@ -71,6 +73,7 @@ final class CoverageControls extends Controls {
      * @param  vocabulary  localized set of words, provided in argument because often known
by the caller.
      * @param  coverage    property containing the coverage to show.
      */
+    @SuppressWarnings("ThisEscapedInObjectConstruction")
     CoverageControls(final Vocabulary vocabulary, final ObjectProperty<GridCoverage>
coverage,
                      final RecentReferenceSystems referenceSystems)
     {
@@ -91,7 +94,7 @@ final class CoverageControls extends Controls {
         {   // Block for making variables locale to this scope.
             final ChoiceBox<ReferenceSystem> systems = referenceSystems.createChoiceBox((p,o,n)
-> {
                 if (n instanceof CoordinateReferenceSystem) {
-                    view.setObjectiveCRS((CoordinateReferenceSystem) n);
+                    view.setObjectiveCRS((CoordinateReferenceSystem) n, p);
                 }
             });
             systems.setMaxWidth(Double.POSITIVE_INFINITY);
@@ -117,6 +120,7 @@ final class CoverageControls extends Controls {
         );
         controls.setExpandedPane(controls.getPanes().get(0));
         view.coverageProperty.bind(coverage);
+        view.addPropertyChangeListener(CoverageCanvas.OBJECTIVE_CRS_PROPERTY, this);
     }
 
     /**
@@ -132,15 +136,23 @@ final class CoverageControls extends Controls {
 
     /**
      * Invoked in JavaFX thread after {@link CoverageExplorer#setCoverage(ImageRequest)}
completed.
-     * This method updates the GUI with new information available, in particular
-     * the coordinate reference system and the list of sample dimensions.
+     * This method updates the GUI with new information available.
      *
      * @param  data  the new coverage, or {@code null} if none.
      */
     @Override
     final void coverageChanged(final GridCoverage data) {
-        if (data != null) {
-            referenceSystem.set(data.getCoordinateReferenceSystem());
+        // TODO
+    }
+
+    /**
+     * Invoked when a canvas property changed.
+     */
+    @Override
+    public void propertyChange(final PropertyChangeEvent event) {
+        final Object value = event.getNewValue();
+        if (value instanceof CoordinateReferenceSystem) {
+            referenceSystem.set((CoordinateReferenceSystem) value);
         }
     }
 
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 8001d44..f197b5f 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
@@ -322,12 +322,9 @@ public class CoverageExplorer extends Widget {
     }
 
     /**
-     * Invoked in JavaFX thread by {@link ImageLoader} when the coverage has been read.
-     * This method does not set the image because it will be set by {@link ImageLoader}.
-     * This method is invoked only as a step during the loading process, which is continuing
-     * after this method invocation.
+     * Invoked in JavaFX thread by {@link GridView} after the coverage has been read.
      *
-     * @param  coverage  the new coverage, or {@code null} if loading failed.
+     * @param  coverage  the new coverage, or {@code null} if loading failed or has been
cancelled.
      */
     final void onCoverageLoaded(final GridCoverage coverage) {
         notifyCoverageChange(coverage);
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
index ec1031d..de8b21a 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
@@ -31,14 +31,18 @@ import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleDoubleProperty;
 import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.StringProperty;
-import javafx.concurrent.WorkerStateEvent;
+import javafx.concurrent.Task;
 import javafx.scene.control.Control;
 import javafx.scene.control.Skin;
 import javafx.scene.paint.Color;
 import javafx.scene.paint.Paint;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.event.StoreListeners;
 import org.apache.sis.internal.gui.BackgroundThreads;
+import org.apache.sis.internal.gui.ExceptionReporter;
 import org.apache.sis.internal.gui.Styles;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.gui.referencing.RecentReferenceSystems;
@@ -80,8 +84,6 @@ public class GridView extends Control {
 
     /**
      * If a loading is in progress, the loading process. Otherwise {@code null}.
-     *
-     * @see #onImageLoaded(WorkerStateEvent)
      */
     private ImageLoader loader;
 
@@ -310,16 +312,105 @@ public class GridView extends Control {
         if (source == null) {
             setImage((RenderedImage) null);
         } else {
-            if (loader != null) {
-                loader.cancel();
+            final ImageLoader previous = loader;
+            loader = null;
+            if (previous != null) {
+                previous.cancel();
             }
             loader = new ImageLoader(source, true);
-            loader.setOnSucceeded(this::onImageLoaded);
             BackgroundThreads.execute(loader);
         }
     }
 
     /**
+     * A task for loading {@link GridCoverage} from a resource in a background thread,
+     * then fetching an image from it.
+     */
+    private final class ImageLoader extends Task<RenderedImage> {
+        /**
+         * The image source together with optional parameters for reading only a subset.
+         */
+        private final ImageRequest request;
+
+        /**
+         * Whether the caller wants a grid coverage that contains real values or sample values.
+         */
+        private final boolean converted;
+
+        /**
+         * Creates a new task for loading an image from the specified coverage resource.
+         *
+         * @param  request    source of the image to load.
+         * @param  converted  {@code true} for a coverage containing converted values,
+         */
+        ImageLoader(final ImageRequest request, final boolean converted) {
+            this.request   = request;
+            this.converted = converted;
+        }
+
+        /**
+         * Loads the image. Current implementation reads the full image. If the coverage
has more than 2 dimensions,
+         * only two of them are taken for the image; for all other dimensions, only the values
at lowest index will
+         * be read.
+         *
+         * @return the image loaded from the source given at construction time.
+         * @throws DataStoreException if an error occurred while loading the grid coverage.
+         */
+        @Override
+        protected RenderedImage call() throws DataStoreException {
+            return request.load(this, converted);
+        }
+
+        /**
+         * Invoked in JavaFX thread after {@link GridView#loader} completed its task successfully.
+         * This method updates the image shown in this {@link GridView} and configures the
status bar.
+         */
+        @Override
+        protected void succeeded() {
+            loader = null;
+            terminated(request.getCoverage().get());    // Should not be empty when the task
is successful.
+            setImage(getValue());                       // Must be after the coverage has
been set.
+            request.configure(statusBar);
+        }
+
+        /**
+         * Invoked in JavaFX thread on failure.
+         * Current implementation popups a dialog box for reporting the error.
+         */
+        @Override
+        protected void failed() {
+            terminated(null);
+            final GridCoverageResource resource = request.resource;
+            if (resource instanceof StoreListeners) {
+                ExceptionReporter.canNotReadFile(((StoreListeners) resource).getSourceName(),
getException());
+            } else {
+                ExceptionReporter.canNotUseResource(getException());
+            }
+        }
+
+        /**
+         * Invoked in JavaFX thread in case of cancellation.
+         */
+        @Override
+        protected void cancelled() {
+            terminated(null);
+        }
+
+        /**
+         * Notifies listener that the given coverage has been read or failed to be read,
+         * then discards the listener. This method shall be invoked in JavaFX thread.
+         * A null argument means that the read operation failed (or has been cancelled.
+         */
+        private void terminated(final GridCoverage result) {
+            final CoverageExplorer snapshot = request.listener;
+            request.listener = null;                // Clear now in case an error happen.
+            if (snapshot != null) {
+                snapshot.onCoverageLoaded(result);
+            }
+        }
+    }
+
+    /**
      * Returns the index of the band shown in this grid view.
      *
      * @return index of the currently visible band number.
@@ -342,18 +433,6 @@ public class GridView extends Control {
     }
 
     /**
-     * Invoked in JavaFX thread after {@link #loader} completed its task successfully.
-     * This method updates the image shown in this {@link GridView} and configures the
-     * status bar.
-     */
-    private void onImageLoaded(final WorkerStateEvent event) {
-        loader = null;
-        final ImageLoader result = (ImageLoader) event.getSource();
-        setImage(result.getValue());
-        result.request.configure(statusBar);
-    }
-
-    /**
      * Invoked (indirectly) when the user sets a new {@link RenderedImage}.
      * See {@link #setImage(RenderedImage)} for more description.
      *
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java
deleted file mode 100644
index 0368228..0000000
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ImageLoader.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * 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.gui.coverage;
-
-import java.awt.image.RenderedImage;
-import javafx.application.Platform;
-import javafx.concurrent.Task;
-import javafx.event.EventHandler;
-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.coverage.grid.GridDerivation;
-import org.apache.sis.storage.DataStoreException;
-import org.apache.sis.storage.GridCoverageResource;
-import org.apache.sis.storage.event.StoreListeners;
-import org.apache.sis.internal.gui.ExceptionReporter;
-
-
-/**
- * A task for loading {@link GridCoverage} from a resource in a background thread, then fetching
an image from it.
- * Callers needs to define a task to execute on success with {@link #setOnSucceeded(EventHandler)}.
- *
- * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
- * @since   1.1
- * @module
- */
-final class ImageLoader extends Task<RenderedImage> {
-    /**
-     * The {@value} value, for identifying code that assume two-dimensional objects.
-     */
-    private static final int BIDIMENSIONAL = 2;
-
-    /**
-     * The image source together with optional parameters for reading only a subset.
-     */
-    final ImageRequest request;
-
-    /**
-     * The coverage explorer to inform when {@link ImageRequest#coverage} become available,
or {@code null} if none.
-     * We do not provide a more generic listeners API for now, but it could be done in the
future if there is a need.
-     */
-    private CoverageExplorer listener;
-
-    /**
-     * Whether the caller wants a grid coverage that contains real values or sample values.
-     */
-    private final boolean converted;
-
-    /**
-     * Creates a new task for loading an image from the specified resource.
-     *
-     * @param  request    source of the image to load.
-     * @param  converted  {@code true} for a coverage containing converted values,
-     *                    or {@code false} for a coverage containing packed values.
-     */
-    ImageLoader(final ImageRequest request, final boolean converted) {
-        this.request     = request;
-        this.converted   = converted;
-        this.listener    = request.listener;
-        request.listener = null;
-    }
-
-    /**
-     * Computes a two dimension slice of the given grid geometry.
-     * This method select the two first dimension having a size greater than 1 cell.
-     *
-     * @param  domain  the grid geometry in which to choose a two-dimensional slice.
-     * @return a builder configured for returning the desired two-dimensional slice.
-     */
-    private static GridDerivation slice(final GridGeometry domain) {
-        final GridExtent extent = domain.getExtent();
-        final int dimension = extent.getDimension();
-        final int[] sliceDimensions = new int[BIDIMENSIONAL];
-        int k = 0;
-        for (int i=0; i<dimension; i++) {
-            if (extent.getLow(i) != extent.getHigh(i)) {
-                sliceDimensions[k] = i;
-                if (++k >= BIDIMENSIONAL) break;
-            }
-        }
-        return domain.derive().sliceByRatio(ImageRequest.SLICE_RATIO, sliceDimensions);
-    }
-
-    /**
-     * Loads the image. Current implementation reads the full image. If the coverage has
more than 2 dimensions,
-     * only two of them are taken for the image; for all other dimensions, only the values
at lowest index will
-     * be read.
-     *
-     * @return the image loaded from the source given at construction time.
-     * @throws DataStoreException if an error occurred while loading the grid coverage.
-     */
-    @Override
-    protected RenderedImage call() throws DataStoreException {
-        GridCoverage cv = request.coverage;
-        if (cv == null) {
-            GridGeometry domain = request.getDomain().orElse(null);
-            final int[]  range  = request.getRange() .orElse(null);
-            {
-                if (domain == null) {
-                    domain = request.resource.getGridGeometry();
-                }
-                if (domain != null && domain.getDimension() > BIDIMENSIONAL) {
-                    domain = slice(domain).build();
-                }
-                /*
-                 * TODO: We restrict loading to a two-dimensional slice for now.
-                 * Future version will need to give user control over slices.
-                 */
-            }
-            cv = request.resource.read(domain, range);                      // May be long
to execute.
-            cv = cv.forConvertedValues(converted);
-            request.coverage = cv;
-            Platform.runLater(this::fireCoverageLoaded);
-        }
-        if (isCancelled()) {
-            return null;
-        }
-        GridExtent sliceExtent = request.getSliceExtent().orElse(null);
-        if (sliceExtent == null) {
-            final GridGeometry domain = cv.getGridGeometry();
-            if (domain != null && domain.getDimension() > BIDIMENSIONAL) {  //
Should never be null but we are paranoiac.
-                sliceExtent = slice(domain).getIntersection();
-            }
-        }
-        return cv.render(sliceExtent);
-    }
-
-    /**
-     * Invoked in JavaFX thread on failure.
-     * Current implementation popups a dialog box for reporting the error.
-     */
-    @Override
-    protected void failed() {
-        fireFinished(null);
-        final GridCoverageResource resource = request.resource;
-        if (resource instanceof StoreListeners) {
-            ExceptionReporter.canNotReadFile(((StoreListeners) resource).getSourceName(),
getException());
-        } else {
-            ExceptionReporter.canNotUseResource(getException());
-        }
-    }
-
-    /**
-     * Invoked in JavaFX thread in case of cancellation.
-     */
-    @Override
-    protected void cancelled() {
-        fireFinished(null);
-    }
-
-    /**
-     * Notifies listener that the given coverage has been read or failed to be read,
-     * then discards the listener. This method shall be invoked in JavaFX thread.
-     *
-     * <p>This method is also invoked with a null argument for notifying listener
-     * that the read operation failed (or has been cancelled).</p>
-     *
-     * @param  result  the result, or {@code null} on failure.
-     */
-    private void fireFinished(final GridCoverage result) {
-        final CoverageExplorer snapshot = listener;
-        if (snapshot != null) {
-            listener = null;                               // Clear now in case an error
happen.
-            snapshot.onCoverageLoaded(result);
-        }
-    }
-
-    /**
-     * Notifies all listeners that the coverage has been read, then discards the listeners.
-     * This method shall be invoked in JavaFX thread.
-     */
-    private void fireCoverageLoaded() {
-        fireFinished(request.coverage);
-    }
-}
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 3ee0826..85b4378 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,7 +17,8 @@
 package org.apache.sis.gui.coverage;
 
 import java.util.Optional;
-import javafx.concurrent.WorkerStateEvent;
+import java.util.concurrent.FutureTask;
+import java.awt.image.RenderedImage;
 import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.coverage.grid.GridDerivation;
 import org.apache.sis.coverage.grid.GridCoverage;
@@ -26,6 +27,7 @@ import org.apache.sis.coverage.grid.GridExtent;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.gui.map.StatusBar;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.storage.DataStoreException;
 
 
 /**
@@ -40,33 +42,45 @@ import org.apache.sis.referencing.operation.transform.MathTransforms;
  */
 public class ImageRequest {
     /**
+     * The {@value} value, for identifying code that assume two-dimensional objects.
+     */
+    private static final int BIDIMENSIONAL = 2;
+
+    /**
      * The source from where to read the image, specified at construction time.
      */
-    GridCoverageResource resource;
+    final GridCoverageResource resource;
 
     /**
      * The source for rendering the image, specified at construction time.
      * After class initialization, only one of {@link #resource} and {@link #coverage} is
non-null.
      * But after task execution, this field will be set to the coverage which has been read.
      */
-    volatile GridCoverage coverage;
+    private GridCoverage coverage;
 
     /**
      * Desired grid extent and resolution, or {@code null} for reading the whole domain.
      * This is used only if the data source is a {@link GridCoverageResource}.
+     *
+     * @see #getDomain()
      */
-    private GridGeometry domain;
+    private final GridGeometry domain;
 
     /**
      * 0-based indices of sample dimensions to read, or {@code null} for reading them all.
      * This is used only if the data source is a {@link GridCoverageResource}.
+     *
+     * @see #getDomain()
      */
-    private int[] range;
+    private final int[] range;
 
     /**
      * A subspace of the grid coverage extent to render, or {@code null} for the whole extent.
      * If the extent has more than two dimensions, then the image will be rendered along
the
      * two first dimensions having a size greater than 1 cell.
+     *
+     * @see #getSliceExtent()
+     * @see #setSliceExtent(GridExtent)
      */
     private GridExtent sliceExtent;
 
@@ -76,12 +90,12 @@ public class ImageRequest {
      *
      * @see GridDerivation#sliceByRatio(double, int[])
      */
-    static final double SLICE_RATIO = 0;
+    private static final double SLICE_RATIO = 0;
 
     /**
-     * For transferring a listener to {@link ImageLoader#listener} before background execution
starts.
-     * We do not provide a more generic listeners API for now, but it could be done in the
future
-     * if there is a need for that.
+     * The coverage explorer to inform after loading completed, or {@code null} if none.
+     * We do not provide a more generic listeners API for now, but we could do that
+     * in the future if there is a need.
      */
     CoverageExplorer listener;
 
@@ -123,11 +137,24 @@ public class ImageRequest {
      */
     public ImageRequest(final GridCoverage source, final GridExtent sliceExtent) {
         ArgumentChecks.ensureNonNull("source", source);
+        this.resource    = null;
+        this.domain      = null;
+        this.range       = null;
         this.coverage    = source;
         this.sliceExtent = sliceExtent;
     }
 
     /**
+     * Returns the coverage, or an empty value is not yet known. This is either the value
specified explicitly
+     * to the constructor, or otherwise the coverage obtained after a read operation.
+     *
+     * @return the coverage.
+     */
+    public final Optional<GridCoverage> getCoverage() {
+        return Optional.ofNullable(coverage);
+    }
+
+    /**
      * Returns the desired grid extent and resolution, or an empty value for reading the
full domain.
      * This is the {@code domain} argument specified to the following constructor:
      *
@@ -144,7 +171,7 @@ public class ImageRequest {
      *
      * @return the desired grid extent and resolution of the coverage.
      */
-    public Optional<GridGeometry> getDomain() {
+    public final Optional<GridGeometry> getDomain() {
         return Optional.ofNullable(domain);
     }
 
@@ -165,7 +192,7 @@ public class ImageRequest {
      *
      * @return the 0-based indices of sample dimensions to read.
      */
-    public Optional<int[]> getRange() {
+    public final Optional<int[]> getRange() {
         /*
          * To be strict we should clone the array, but ImageRequest is just passing the array
to
          * GridCoverageResource, which is the class making real use of it. This is not sensitive
@@ -189,7 +216,7 @@ public class ImageRequest {
      *
      * @return subspace of the grid coverage extent to render.
      */
-    public Optional<GridExtent> getSliceExtent() {
+    public final Optional<GridExtent> getSliceExtent() {
         return Optional.ofNullable(sliceExtent);
     }
 
@@ -202,14 +229,83 @@ public class ImageRequest {
      *
      * @param  sliceExtent  subspace of the grid coverage extent to render.
      */
-    public void setSliceExtent(final GridExtent sliceExtent) {
+    public final void setSliceExtent(final GridExtent sliceExtent) {
         this.sliceExtent = sliceExtent;
     }
 
     /**
+     * Computes a two dimension slice of the given grid geometry.
+     * This method selects the two first dimensions having a size greater than 1 cell.
+     *
+     * @param  domain  the grid geometry in which to choose a two-dimensional slice.
+     * @return a builder configured for returning the desired two-dimensional slice.
+     */
+    private static GridDerivation slice(final GridGeometry domain) {
+        final GridExtent extent = domain.getExtent();
+        final int dimension = extent.getDimension();
+        final int[] sliceDimensions = new int[BIDIMENSIONAL];
+        int k = 0;
+        for (int i=0; i<dimension; i++) {
+            if (extent.getLow(i) != extent.getHigh(i)) {
+                sliceDimensions[k] = i;
+                if (++k >= BIDIMENSIONAL) break;
+            }
+        }
+        return domain.derive().sliceByRatio(ImageRequest.SLICE_RATIO, sliceDimensions);
+    }
+
+    /**
+     * Loads the image. Current implementation reads the full image. If the coverage has
more than
+     * {@value #BIDIMENSIONAL} dimensions, only two of them are taken for the image; for
all other
+     * dimensions, only the values at lowest index will be read.
+     *
+     * <p>If the {@link #coverage} field was null, it will be initialized as a side-effect.
+     * No other fields will be modified.</p>
+     *
+     * <p>This class does not need to be thread-safe since it should be used only once
in a well-defined
+     * life cycle. We nevertheless synchronize as a safety (user could give the same {@code
ImageRequest}
+     * to two different {@link CoverageExplorer} instances).</p>
+     *
+     * @param  task       the task invoking this method (for checking for cancellation).
+     * @param  converted  {@code true} for a coverage containing converted values,
+     *                    or {@code false} for a coverage containing packed values.
+     * @return the image loaded from the source given at construction time,
+     *         or {@code null} if the task has been cancelled.
+     * @throws DataStoreException if an error occurred while loading the grid coverage.
+     */
+    final synchronized RenderedImage load(final FutureTask<?> task, final boolean converted)
throws DataStoreException {
+        if (coverage == null) {
+            GridGeometry domain = this.domain;
+            if (domain == null) {
+                domain = resource.getGridGeometry();
+            }
+            if (domain != null && domain.getDimension() > BIDIMENSIONAL) {
+                domain = slice(domain).build();
+            }
+            /*
+             * TODO: We restrict loading to a two-dimensional slice for now.
+             * Future version will need to give user control over slices.
+             */
+            coverage = resource.read(domain, range);                    // May be long to
execute.
+            coverage = coverage.forConvertedValues(converted);
+        }
+        if (task.isCancelled()) {
+            return null;
+        }
+        GridExtent se = sliceExtent;
+        if (se == null) {
+            final GridGeometry cd = coverage.getGridGeometry();
+            if (cd != null && cd.getDimension() > BIDIMENSIONAL) {      // Should
never be null but we are paranoiac.
+                se = slice(cd).getIntersection();
+            }
+        }
+        return coverage.render(se);
+    }
+
+    /**
      * Configures the given status bar with the geometry of the grid coverage we have just
read.
-     * This method is invoked by {@link GridView#onImageLoaded(WorkerStateEvent)} in JavaFX
thread
-     * after {@link ImageLoader} successfully loaded in background thread a new image.
+     * This method is invoked in JavaFX thread after {@link GridView#setImage(ImageRequest)}
+     * successfully loaded in background thread a new image.
      */
     final void configure(final StatusBar bar) {
         final GridCoverage cv = coverage;
diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
index 76dfc0d..0948e36 100644
--- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
+++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/RenderingData.java
@@ -44,6 +44,7 @@ import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.transform.MathTransforms;
 import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
 import org.apache.sis.referencing.CRS;
+import org.apache.sis.util.Utilities;
 import org.apache.sis.util.logging.Logging;
 
 
@@ -157,9 +158,24 @@ final class RenderingData implements Cloneable {
     }
 
     /**
+     * Verifies if this {@code RenderingData} contains an image for the given objective CRS.
+     * If this is not the case, the cached resampled images will be discarded.
+     *
+     * @param  objectiveCRS  the coordinate reference system to use for rendering.
+     * @return whether the data are valid for the given objective CRS.
+     */
+    final boolean validateCRS(final CoordinateReferenceSystem objectiveCRS) {
+        if (changeOfCRS != null && !Utilities.equalsIgnoreMetadata(objectiveCRS,
changeOfCRS.getTargetCRS())) {
+            clearCRS();
+            return false;
+        }
+        return true;
+    }
+
+    /**
      * Clears the cache of transforms that depend on the CRS.
      */
-    final void clearCRS() {
+    private void clearCRS() {
         changeOfCRS       = null;
         cornerToObjective = null;
         centerToObjective = null;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 2d7f4d7..07a1e30 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1419,8 +1419,8 @@ public class GridGeometry implements LenientComparable, Serializable
{
             }
             /*
              * Example: Geographic extent
-             *  ├─Upper bound:  90°00′00″N  180°00′00″E  2019-05-02T00:00:00Z
-             *  └─Lower bound:  80°00′00″S  180°00′00″W  2019-05-01T21:00:00Z
+             *  ├─Lower bound:  80°00′00″S  180°00′00″W  2019-05-01T21:00:00Z
+             *  └─Upper bound:  90°00′00″N  180°00′00″E  2019-05-02T00:00:00Z
              *
              * The angle and date/time patterns are fixed for now, with a precision equivalent
to about 30 metres.
              * The angles are rounded toward up and down for making sure that the box encloses
fully the coverage.
@@ -1432,29 +1432,29 @@ public class GridGeometry implements LenientComparable, Serializable
{
                 final AngleFormat nf = new AngleFormat("DD°MM′SS″", locale);
                 final GeographicBoundingBox bbox = geographicBBox();
                 final Instant[] times = timeRange();
-                vocabulary.appendLabel(Vocabulary.Keys.UpperBound, table);
+                vocabulary.appendLabel(Vocabulary.Keys.LowerBound, table);
                 table.setCellAlignment(TableAppender.ALIGN_RIGHT);
                 if (bbox != null) {
-                    nf.setRoundingMode(RoundingMode.CEILING);
-                    table.nextColumn(); table.append(nf.format(new  Latitude(bbox.getNorthBoundLatitude())));
-                    table.nextColumn(); table.append(nf.format(new Longitude(bbox.getEastBoundLongitude())));
+                    nf.setRoundingMode(RoundingMode.FLOOR);
+                    table.nextColumn(); table.append(nf.format(new  Latitude(bbox.getSouthBoundLatitude())));
+                    table.nextColumn(); table.append(nf.format(new Longitude(bbox.getWestBoundLongitude())));
                 }
-                if (times.length >= 2) {
+                if (times.length >= 1) {
                     table.nextColumn();
-                    table.append(times[1].toString());
+                    table.append(times[0].toString());
                 }
                 table.nextLine();
                 table.setCellAlignment(TableAppender.ALIGN_LEFT);
-                vocabulary.appendLabel(Vocabulary.Keys.LowerBound, table);
+                vocabulary.appendLabel(Vocabulary.Keys.UpperBound, table);
                 table.setCellAlignment(TableAppender.ALIGN_RIGHT);
                 if (bbox != null) {
-                    nf.setRoundingMode(RoundingMode.FLOOR);
-                    table.nextColumn(); table.append(nf.format(new  Latitude(bbox.getSouthBoundLatitude())));
-                    table.nextColumn(); table.append(nf.format(new Longitude(bbox.getWestBoundLongitude())));
+                    nf.setRoundingMode(RoundingMode.CEILING);
+                    table.nextColumn(); table.append(nf.format(new  Latitude(bbox.getNorthBoundLatitude())));
+                    table.nextColumn(); table.append(nf.format(new Longitude(bbox.getEastBoundLongitude())));
                 }
-                if (times.length >= 1) {
+                if (times.length >= 2) {
                     table.nextColumn();
-                    table.append(times[0].toString());
+                    table.append(times[1].toString());
                 }
                 table.flush();
                 writeNodes();
diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java
index 7372bfa..67ca9c7 100644
--- a/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java
+++ b/core/sis-portrayal/src/main/java/org/apache/sis/portrayal/Observable.java
@@ -74,16 +74,17 @@ abstract class Observable {
         }
         final PropertyChangeListener[] oldList = listeners.get(propertyName);
         final PropertyChangeListener[] newList;
-        final int n;
+        final boolean success;
         if (oldList != null) {
-            n = oldList.length;
+            final int n = oldList.length;
             newList = Arrays.copyOf(oldList, n+1);
+            newList[n] = listener;
+            success = listeners.replace(propertyName, oldList, newList);
         } else {
-            n = 0;
-            newList = new PropertyChangeListener[1];
+            newList = new PropertyChangeListener[] {listener};
+            success = (listeners.putIfAbsent(propertyName, newList) == null);
         }
-        newList[n] = listener;
-        if (!listeners.replace(propertyName, oldList, newList)) {
+        if (!success) {
             // Opportunistic safety against some multi-threading misuse.
             throw new ConcurrentModificationException();
         }


Mime
View raw message