This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
commit c112a871b485aa20f147b327d13c56b43a7051f2 Author: Martin Desruisseaux <[email protected]> AuthorDate: Mon May 30 17:16:12 2022 +0200 Initial version of a `CoverangeCanvas` capable to navigate in dimensions over 2 (using sliders). It required a change in the ways controls are managed, e.g. with `StatusBar` now managed by `ViewAndControls`. --- .../apache/sis/gui/coverage/CoverageCanvas.java | 155 ++++++++++------ .../apache/sis/gui/coverage/CoverageControls.java | 22 ++- .../org/apache/sis/gui/coverage/GridControls.java | 7 +- .../apache/sis/gui/coverage/GridSliceSelector.java | 28 ++- .../java/org/apache/sis/gui/coverage/GridView.java | 87 ++++++--- .../org/apache/sis/gui/coverage/GridViewSkin.java | 4 +- .../org/apache/sis/gui/coverage/ImageRequest.java | 196 +++++---------------- .../gui/coverage/MultiResolutionImageLoader.java | 74 +------- .../apache/sis/gui/coverage/ViewAndControls.java | 44 +++-- .../java/org/apache/sis/gui/map/StatusBar.java | 173 +++++++++++++----- .../apache/sis/internal/gui/BackgroundThreads.java | 24 ++- .../apache/sis/internal/gui/ExceptionReporter.java | 26 ++- .../sis/internal/gui/OptionalDataDownloader.java | 2 +- .../org/apache/sis/internal/gui/package-info.java | 2 +- .../apache/sis/gui/coverage/CoverageCanvasApp.java | 4 +- .../apache/sis/coverage/grid/ImageRenderer.java | 18 +- .../coverage/MultiResolutionCoverageLoader.java | 24 +-- .../sis/internal/map/coverage/RenderingData.java | 117 ++++++++---- .../sis/internal/map/coverage/package-info.java | 2 +- .../java/org/apache/sis/util/ArgumentChecks.java | 29 ++- 20 files changed, 601 insertions(+), 437 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 67bd1a3b66..57eb233337 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 @@ -47,6 +47,7 @@ import javax.measure.Quantity; import javax.measure.quantity.Length; import org.opengis.geometry.Envelope; 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.coverage.SampleDimension; @@ -57,16 +58,15 @@ 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.GeneralEnvelope; 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; -import org.apache.sis.gui.map.StatusBar; import org.apache.sis.portrayal.RenderException; import org.apache.sis.internal.map.coverage.RenderingWorkaround; import org.apache.sis.internal.coverage.j2d.TileErrorHandler; @@ -79,6 +79,7 @@ import org.apache.sis.internal.system.Modules; import org.apache.sis.util.logging.Logging; import org.apache.sis.io.TableAppender; import org.apache.sis.measure.Units; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Debug; import static java.util.logging.Logger.getLogger; @@ -161,6 +162,11 @@ public class CoverageCanvas extends MapCanvasAWT { */ private boolean isCoverageAdjusting; + /** + * Whether at least one of {@link #coverageProperty} or {@link #resourceProperty} has a non-null value. + */ + private boolean hasCoverageOrResource; + /** * A subspace of the grid coverage extent where all dimensions except two have a size of 1 cell. * May be {@code null} if the grid coverage has only two dimensions with a size greater than 1 cell. @@ -181,8 +187,8 @@ public class CoverageCanvas extends MapCanvasAWT { /** * The {@code RenderedImage} to draw together with transform from pixel coordinates to display coordinates. - * Shall never be {@code null} but may be {@link StyledRenderingData#isEmpty() empty}. This instance shall - * be read and modified in JavaFX thread only and cloned if those data are needed by a background thread. + * Shall never be {@code null} but may be empty. This instance shall be read and modified in JavaFX thread + * only and cloned if those data are needed by a background thread. * * @see Worker */ @@ -213,10 +219,12 @@ public class CoverageCanvas extends MapCanvasAWT { private ImagePropertyExplorer propertyExplorer; /** - * The status bar associated to this {@code MapCanvas}. - * This is non-null only if this {@link CoverageCanvas} is used together with {@link CoverageControls}. + * If this canvas is associated with controls, the controls. Otherwise {@code null}. + * This is used only for notifications; a future version may use a more generic listener. + * + * @see CoverageControls#notifyDataChanged(GridCoverageResource, GridCoverage) */ - StatusBar statusBar; + private final CoverageControls controls; /** * If errors occurred during tile computations, details about the error. Otherwise {@code null}. @@ -239,16 +247,18 @@ public class CoverageCanvas extends MapCanvasAWT { * Creates a new two-dimensional canvas for {@link RenderedImage}. */ public CoverageCanvas() { - this(Locale.getDefault()); + this(null, Locale.getDefault()); } /** * Creates a new two-dimensional canvas using the given locale. * - * @param locale the locale to use for labels and some messages, or {@code null} for default. + * @param controls the controls of this canvas, or {@code null} if none. + * @param locale the locale to use for labels and some messages, or {@code null} for default. */ - CoverageCanvas(final Locale locale) { + CoverageCanvas(final CoverageControls controls, final Locale locale) { super(locale); + this.controls = controls; data = new StyledRenderingData((report) -> errorReport = report.getDescription()); derivedImages = new EnumMap<>(Stretching.class); resourceProperty = new SimpleObjectProperty<>(this, "resource"); @@ -367,6 +377,8 @@ public class CoverageCanvas extends MapCanvasAWT { /** * Sets a subspace of the grid coverage extent where all dimensions except two have a size of 1 cell. + * Note that values set on this property may be overwritten at any time by user interactions if this + * {@code CoverageCanvas} is associated with a {@link GridSliceSelector}. * * @param sliceExtent subspace of the grid coverage extent where all dimensions except two have a size of 1 cell. * @@ -485,8 +497,8 @@ public class CoverageCanvas extends MapCanvasAWT { final GridExtent sliceExtent; if (request != null) { resource = request.resource; - coverage = request.getCoverage().orElse(null); - sliceExtent = request.getSliceExtent().orElse(null); + coverage = request.coverage; + sliceExtent = request.slice; } else { resource = null; coverage = null; @@ -519,6 +531,7 @@ public class CoverageCanvas extends MapCanvasAWT { private void onPropertySpecified(final GridCoverageResource resource, final GridCoverage coverage, final ObjectProperty<?> toClear) { + hasCoverageOrResource = (resource != null || coverage != null); if (isCoverageAdjusting) { return; } @@ -531,15 +544,17 @@ public class CoverageCanvas extends MapCanvasAWT { if (resource == null && coverage == null) { runAfterRendering(this::clear); } else { - BackgroundThreads.execute(new Task<Envelope>() { - /** The coverage geometry reduced to two dimensions. */ - private GridGeometry domain; - + BackgroundThreads.execute(new Task<GridGeometry>() { /** Information about all bands. */ private List<SampleDimension> ranges; - /** Fetch coverage domain, range and geospatial envelope. */ - @Override protected Envelope call() throws Exception { + /** + * Fetches coverage domain and range. In some {@link GridCoverageResource} implementations, + * fetching the grid geometry is a costly operation. So we do it in a background thread and + * invoke {@code setNewSource(…)} later with the result. No rendering happen here. + */ + @Override protected GridGeometry call() throws Exception { + GridGeometry domain; final Long id = LogHandler.loadingStart(resource); try { if (coverage != null) { @@ -549,26 +564,33 @@ public class CoverageCanvas extends MapCanvasAWT { domain = resource.getGridGeometry(); ranges = resource.getSampleDimensions(); } - domain = MultiResolutionImageLoader.slice(domain); - if (domain != null) { - if (domain.isDefined(GridGeometry.ENVELOPE)) { - return domain.getEnvelope(); - } - if (domain.isDefined(GridGeometry.EXTENT)) { - final GeneralEnvelope ge = domain.getExtent().toEnvelope(MathTransforms.identity(BIDIMENSIONAL)); - ge.setCoordinateReferenceSystem(CommonCRS.Engineering.DISPLAY.crs()); - return ge; - } + /* + * The domain should never be null and should always be complete (including envelope). + * Nevertheless we try to be safe, since `setNewSource(…)` wants a complete geometry. + * So if the envelope is missing but the extent is present, then the missing part was + * the "grid to CRS" transform. We use an identity transform with a "display CRS". + */ + if (domain != null && !domain.isDefined(GridGeometry.ENVELOPE) && domain.isDefined(GridGeometry.EXTENT)) { + final GridExtent extent = domain.getExtent(); + final int dimension = extent.getDimension(); + domain = new GridGeometry(extent, PixelInCell.CELL_CORNER, MathTransforms.identity(dimension), + (dimension == BIDIMENSIONAL) ? CommonCRS.Engineering.DISPLAY.crs() : null); } - return null; } finally { LogHandler.loadingStop(id); } + return domain; } - /** Invoked in JavaFX thread for setting the grid geometry we just fetched. */ + /** + * Invoked in JavaFX thread for setting the grid geometry we just fetched. + * This method requests a repaint, which will occur in another thread. + */ @Override protected void succeeded() { - runAfterRendering(() -> setNewSource(getValue(), domain, ranges)); + runAfterRendering(() -> { + setNewSource(getValue(), ranges); + requestRepaint(); // Cause `Worker` class to be executed. + }); } /** @@ -587,17 +609,16 @@ public class CoverageCanvas extends MapCanvasAWT { /** * Invoked when a new resource or coverage has been specified. - * The call to this method is followed by a repaint event, - * which will cause the image to be loaded and resampled in a background thread. + * Caller should invoke {@link #requestRepaint()} after this method + * for loading and resampling the image in a background thread. * * <p>All arguments can be {@code null} for clearing the canvas. * This method is invoked in JavaFX thread.</p> * - * @param bounds geospatial bounds, or {@code null} if none or unknown. - * @param domain the two-dimensional grid geometry, or {@code null} if there is no data. + * @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. */ - private void setNewSource(final Envelope bounds, final GridGeometry domain, final List<SampleDimension> ranges) { + private void setNewSource(GridGeometry domain, final List<SampleDimension> ranges) { if (TRACE) { trace("setNewSource(…): the new domain of data is:%n\t%s", domain); } @@ -606,9 +627,39 @@ public class CoverageCanvas extends MapCanvasAWT { resampledImage = null; derivedImages.clear(); data.clear(); - data.setCoverageSpace(domain, ranges); + /* + * Configure the `GridSliceSelector`, which will compute a new slice extent as a side effect. + * It will overwrite the previous value of `sliceExtent` property in this class, which needs + * to be done before to start the `Worker` process in a background thread. + */ + int[] xyDimensions; + if (controls != null) try { + isCoverageAdjusting = true; + setSliceExtent(controls.setGeometry(domain)); + xyDimensions = controls.sliceSelector.getXYDimensions(); + } finally { + isCoverageAdjusting = false; + } else { + xyDimensions = ArraysExt.range(0, BIDIMENSIONAL); + final GridExtent extent = getSliceExtent(); + if (extent != null) try { + xyDimensions = extent.getSubspaceDimensions(BIDIMENSIONAL); + } catch (SubspaceNotSpecifiedException e) { + unexpectedException(e); // We can continue with dimensions {0,1}. + } + } + /* + * Notify the `RenderingData` and `MapCanvas`. All information below must be two-dimensional. + */ + Envelope bounds = null; + if (domain != null) { + domain = domain.reduce(xyDimensions); + if (domain.isDefined(GridGeometry.ENVELOPE)) { + bounds = domain.getEnvelope(); + } + } + data.setImageSpace(domain, ranges, xyDimensions); setObjectiveBounds(bounds); - requestRepaint(); // Cause `Worker` class to be executed. } /** @@ -642,7 +693,7 @@ public class CoverageCanvas extends MapCanvasAWT { */ @Override protected Renderer createRenderer() { - return (data.getSourceImage() != null || getResource() != null) ? new Worker(this) : null; + return hasCoverageOrResource ? new Worker(this) : null; } /** @@ -668,20 +719,22 @@ public class CoverageCanvas extends MapCanvasAWT { /** * The coverage specified by user or the coverage loaded from the {@linkplain #resource}. - * May be {@code null} if coverages are loaded from resource but did not changed since last rendering. + * Should never be {@code null} after successful execution of {@link #render()}. */ private GridCoverage coverage; /** * Whether the value of {@link #coverage} changed since the last rendering. - * It may happen if {@link #resource} is non-null, contains pyramided data - * and the pyramid level used by this rendering is different than the pyramid - * level used in previous rendering. + * It may happen if {@link #resource} is non-null, contains pyramided data and the pyramid level + * used by this rendering is different than the pyramid level used during the previous rendering. + * Note that a {@code false} value does not mean that {@link #sliceExtent} did not changed. */ private boolean coverageChanged; /** - * The two-dimensional slice to display. + * The two-dimensional slice to display. May change for the same coverage when using + * {@link ViewAndControls#sliceSelector} for navigation in dimensions other than the + * {@value #BIDIMENSIONAL} first dimensions. */ private final GridExtent sliceExtent; @@ -827,10 +880,12 @@ public class CoverageCanvas extends MapCanvasAWT { data.setObjectiveCRS(objectiveCRS); if (resource != null) { data.coverageLoader = MultiResolutionImageLoader.getInstance(resource, data.coverageLoader); - coverage = data.ensureCoverageLoaded(objectiveToDisplay, objectivePOI); - coverageChanged = (coverage != null); + final GridCoverage loaded = data.ensureCoverageLoaded(objectiveToDisplay, objectivePOI); + if (coverageChanged = (loaded != null)) { + coverage = loaded; + } } - if (data.ensureImageLoaded(coverage, sliceExtent)) { + if (data.ensureImageLoaded(coverage, sliceExtent, coverageChanged)) { recoloredImage = null; } /* @@ -966,7 +1021,7 @@ public class CoverageCanvas extends MapCanvasAWT { * Adjust the accuracy of coordinates shown in the status bar. * The number of fraction digits depend on the zoom factor. */ - if (statusBar != null) { + if (controls != null) { final Object value = resampledImage.getProperty(PlanarImage.POSITIONAL_ACCURACY_KEY); Quantity<Length> accuracy = null; if (value instanceof Quantity<?>[]) { @@ -979,7 +1034,7 @@ public class CoverageCanvas extends MapCanvasAWT { } } } - statusBar.setLowestAccuracy(accuracy); + controls.status.setLowestAccuracy(accuracy); } /* * If error(s) occurred during calls to `RenderedImage.getTile(tx, ty)`, reports those errors. @@ -1081,7 +1136,7 @@ public class CoverageCanvas extends MapCanvasAWT { if (TRACE) { trace("clear()"); } - setNewSource(null, null, null); + setNewSource(null, null); super.clear(); } 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 76f630a8ca..8952454ddd 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 @@ -31,7 +31,6 @@ import org.apache.sis.coverage.Category; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.gui.map.MapMenu; -import org.apache.sis.gui.map.StatusBar; import org.apache.sis.internal.gui.control.ValueColorMapper; import org.apache.sis.internal.gui.Styles; import org.apache.sis.internal.gui.Resources; @@ -77,12 +76,12 @@ final class CoverageControls extends ViewAndControls { final Resources resources = Resources.forLocale(locale); final Vocabulary vocabulary = Vocabulary.getResources(locale); - view = new CoverageCanvas(locale); + view = new CoverageCanvas(this, locale); view.setBackground(Color.BLACK); - view.statusBar = new StatusBar(owner.referenceSystems, view); + status.track(view); final MapMenu menu = new MapMenu(view); menu.addReferenceSystems(owner.referenceSystems); - menu.addCopyOptions(view.statusBar); + menu.addCopyOptions(status); /* * "Display" section with the following controls: * - Current CRS @@ -149,7 +148,7 @@ final class CoverageControls extends ViewAndControls { view.resourceProperty.addListener((p,o,n) -> notifyDataChanged(n, null)); view.coverageProperty.addListener((p,o,n) -> notifyDataChanged(view.getResourceIfAdjusting(), n)); deferred.expandedProperty().addListener(new PropertyPaneCreator(view, deferred)); - setView(view.getView(), view.statusBar); + setView(view.getView()); } /** @@ -160,8 +159,7 @@ final class CoverageControls extends ViewAndControls { * @param resource the new source of coverage, or {@code null} if none. * @param coverage the new coverage, or {@code null} if none. */ - @Override - final void notifyDataChanged(final GridCoverageResource resource, final GridCoverage coverage) { + private void notifyDataChanged(final GridCoverageResource resource, final GridCoverage coverage) { final ObservableList<Category> items = categoryTable.getItems(); if (coverage == null) { items.clear(); @@ -169,7 +167,15 @@ final class CoverageControls extends ViewAndControls { final int visibleBand = 0; // TODO: provide a selector for the band to show. items.setAll(coverage.getSampleDimensions().get(visibleBand).getCategories()); } - super.notifyDataChanged(resource, coverage); + owner.notifyDataChanged(resource, coverage); + } + + /** + * Returns the grid coverage shown in the view, or {@code null} if none. + */ + @Override + GridCoverage getCoverage() { + return view.getCoverage(); } /** diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java index eac6982f0a..542cb90bcf 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridControls.java @@ -58,7 +58,7 @@ final class GridControls extends ViewAndControls { */ GridControls(final CoverageExplorer owner) { super(owner); - view = new GridView(this, owner.referenceSystems); + view = new GridView(this); final Vocabulary vocabulary = Vocabulary.getResources(owner.getLocale()); sampleDimensions = new BandRangeTable(view.cellFormat).create(vocabulary); BandSelectionListener.bind(view.bandProperty, sampleDimensions.getSelectionModel()); @@ -88,7 +88,7 @@ final class GridControls extends ViewAndControls { new TitledPane(vocabulary.getString(Vocabulary.Keys.Display), displayPane) // TODO: more controls to be added in a future version. }; - setView(view, view.statusBar); + setView(view); } /** @@ -109,7 +109,6 @@ final class GridControls extends ViewAndControls { * @param resource the new source of coverage, or {@code null} if none. * @param coverage the new coverage, or {@code null} if none. */ - @Override final void notifyDataChanged(final GridCoverageResource resource, final GridCoverage coverage) { final ObservableList<SampleDimension> items = sampleDimensions.getItems(); if (coverage != null) { @@ -118,7 +117,7 @@ final class GridControls extends ViewAndControls { } else { items.clear(); } - super.notifyDataChanged(resource, coverage); + owner.notifyDataChanged(resource, coverage); } /** diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java index a215db2ddb..c30fd9d819 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridSliceSelector.java @@ -78,6 +78,8 @@ import org.apache.sis.util.resources.Vocabulary; public class GridSliceSelector extends Widget { /** * Constants used for identifying the code assuming a two-dimensional space. + * + * @see #getXYDimensions() */ private static final int BIDIMENSIONAL = 2; @@ -110,6 +112,14 @@ public class GridSliceSelector extends Widget { */ private final ReadOnlyObjectWrapper<GridExtent> selectedExtent; + /** + * Indices of {@link GridExtent} dimensions which are assigned to <var>x</var> and <var>y</var> coordinates. + * They are usually 0 for <var>x</var> and 1 for <var>y</var>. + * + * @see #BIDIMENSIONAL + */ + private int xDimension, yDimension; + /** * The locale to use for axis labels, or {@code null} for a default locale. */ @@ -172,6 +182,7 @@ public class GridSliceSelector extends Widget { selectedExtent = new ReadOnlyObjectWrapper<>(this, "selectedExtent"); gridGeometry = new SimpleObjectProperty<>(this, "gridGeometry"); gridGeometry.addListener((p,o,n) -> setGridGeometry(n)); + yDimension = 1; } /** @@ -199,7 +210,11 @@ public class GridSliceSelector extends Widget { for (int dim=0; dim < dimension; dim++) { final long min = extent.getLow (dim); final long max = extent.getHigh(dim); - if (min < max && ++row >= 0) { + if (min < max) { + switch (++row) { + case -2: xDimension = dim; continue; + case -1: yDimension = dim; continue; + } /* * A new slider needs to be shown. Recycle existing slider and label if any, * or create new controls if we already used all existing controls. @@ -595,6 +610,17 @@ public class GridSliceSelector extends Widget { return selectedExtent.getReadOnlyProperty(); } + /** + * Returns the grid dimensions of <var>x</var> and <var>y</var> axes rendered in a two-dimensional image or table. + * This value is inferred from the {@linkplain #gridGeometry grid geometry} property value. + * It is almost always {0,1}, i.e. the 2 first dimensions in a coordinate tuple. + * + * @return indices of <var>x</var> and <var>y</var> coordinate values in a grid coordinate tuple. + */ + public final int[] getXYDimensions() { + return new int[] {xDimension, yDimension}; + } + /** * Returns the encapsulated JavaFX component to add in a scene graph for making the selectors visible. * The {@code Region} subclass is implementation dependent and may change in any future SIS version. 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 d0ddb608bd..00ee0eb64a 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 @@ -36,13 +36,16 @@ 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.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.internal.gui.BackgroundThreads; +import org.apache.sis.internal.gui.LogHandler; import org.apache.sis.internal.gui.Styles; -import org.apache.sis.gui.map.StatusBar; -import org.apache.sis.gui.referencing.RecentReferenceSystems; import org.apache.sis.internal.coverage.j2d.ImageUtilities; +import org.apache.sis.internal.gui.ExceptionReporter; /** @@ -56,7 +59,7 @@ import org.apache.sis.internal.coverage.j2d.ImageUtilities; * consider using the standard JavaFX {@link javafx.scene.control.TableView} instead.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * * @see CoverageExplorer * @@ -201,11 +204,6 @@ public class GridView extends Control { */ final CellFormat cellFormat; - /** - * The status bar where to show coordinates of selected cell. - */ - final StatusBar statusBar; - /** * If this grid view is associated with controls, the controls. Otherwise {@code null}. * This is used only for notifications; a future version may use a more generic listener. @@ -220,17 +218,16 @@ public class GridView extends Control { * construction by a call to {@link #setImage(RenderedImage)}. */ public GridView() { - this(null, null); + this(null); } /** * Creates an initially empty grid view. The content can be set after * construction by a call to {@link #setImage(RenderedImage)}. * - * @param controls the controls of this grid view, or {@code null} if none. - * @param referenceSystems the manager of reference systems chosen by the user, or {@code null} if none. + * @param controls the controls of this grid view, or {@code null} if none. */ - GridView(final GridControls controls, final RecentReferenceSystems referenceSystems) { + GridView(final GridControls controls) { this.controls = controls; bandProperty = new BandProperty(); imageProperty = new SimpleObjectProperty<>(this, "image"); @@ -241,7 +238,6 @@ public class GridView extends Control { headerBackground = new SimpleObjectProperty<>(this, "headerBackground", Color.GAINSBORO); headerFormat = NumberFormat.getIntegerInstance(); cellFormat = new CellFormat(this); - statusBar = new StatusBar(referenceSystems); tiles = new GridTileCache(); tileWidth = 1; tileHeight = 1; // For avoiding division by zero. @@ -334,15 +330,15 @@ public class GridView extends Control { /** * Invoked after the image has been loaded or after failure. * - * @param source the coverage or resource to load (never {@code null}). - * @param image the loaded image, or {@code null} on failure. + * @param resource the new source of coverage, or {@code null} if none. + * @param coverage the new coverage, or {@code null} if none. + * @param image the loaded image, or {@code null} on failure. */ - private void setLoadedImage(final ImageRequest request, final RenderedImage image) { + private void setLoadedImage(GridCoverageResource resource, GridCoverage coverage, RenderedImage image) { loader = null; // Must be first for preventing cancellation. setImage(image); - request.configure(statusBar); if (controls != null) { - controls.notifyDataChanged(request.resource, request.getCoverage().orElse(null)); + controls.notifyDataChanged(resource, coverage); } } @@ -357,6 +353,12 @@ public class GridView extends Control { */ private final ImageRequest request; + /** + * The coverage that has been read. + * It may either be specified explicitly in the {@link #request}, or read from the resource. + */ + private GridCoverage coverage; + /** * Creates a new task for loading an image from the specified coverage resource. * @@ -367,15 +369,29 @@ public class GridView extends Control { } /** - * Loads the 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. + * Invoked in a background thread for loading the image. * * @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); + protected RenderedImage call() throws Exception { + final Long id = LogHandler.loadingStart(request.resource); + try { + coverage = request.load().forConvertedValues(true); + if (isCancelled()) { + return null; + } + GridExtent slice = request.slice; + final GridControls c = controls; + if (c != null) { + final GridGeometry gg = coverage.getGridGeometry(); + slice = BackgroundThreads.runAndWait(() -> c.setGeometry(gg)); + } + return coverage.render(slice); + } finally { + LogHandler.loadingStop(id); + } } /** @@ -384,7 +400,15 @@ public class GridView extends Control { */ @Override protected void succeeded() { - setLoadedImage(request, getValue()); + setLoadedImage(request.resource, coverage, getValue()); + } + + /** + * Invoked in JavaFX thread on cancellation. This method clears all controls. + */ + @Override + protected void cancelled() { + setLoadedImage(null, null, null); } /** @@ -393,8 +417,8 @@ public class GridView extends Control { */ @Override protected void failed() { - setLoadedImage(request, null); - request.reportError(GridView.this, getException()); + cancelled(); + ExceptionReporter.canNotReadFile(GridView.this, request.resource, getException()); } } @@ -627,7 +651,18 @@ public class GridView extends Control { * Then the pixel coordinates are converted to "real world" coordinates and formatted. */ final void formatCoordinates(final int x, final int y) { - statusBar.setLocalCoordinates(minX + x, minY + y); + if (controls != null) { + controls.status.setLocalCoordinates(minX + x, minY + y); + } + } + + /** + * Hides coordinates in the status bar. + */ + final void hideCoordinates() { + if (controls != null) { + controls.status.handle(null); + } } /** diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java index 00220a5de4..c33f959b1d 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridViewSkin.java @@ -242,7 +242,7 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme selectedRow .setVisible(visible); selectedColumn.setVisible(visible); if (!visible) { - getSkinnable().statusBar.handle(null); + getSkinnable().hideCoordinates(); } } @@ -315,7 +315,7 @@ final class GridViewSkin extends VirtualContainerBase<GridView, GridRow> impleme selection .setVisible(false); selectedRow .setVisible(false); selectedColumn.setVisible(false); - getSkinnable().statusBar.handle(null); // Hide the coordinates. + getSkinnable().hideCoordinates(); } /** 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 1f09e4c607..d9f1c58e09 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,20 +17,12 @@ package org.apache.sis.gui.coverage; import java.util.Optional; -import java.util.concurrent.FutureTask; -import java.awt.image.RenderedImage; -import javafx.scene.Node; -import org.opengis.referencing.operation.TransformException; 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.gui.map.StatusBar; -import org.apache.sis.internal.gui.LogHandler; -import org.apache.sis.internal.gui.ExceptionReporter; import org.apache.sis.storage.DataStoreException; -import org.apache.sis.storage.event.StoreListeners; /** @@ -39,7 +31,7 @@ import org.apache.sis.storage.event.StoreListeners; * {@linkplain GridCoverage#render(GridExtent) rendering} and image in a background thread. * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * * @see GridView#setImage(ImageRequest) * @see CoverageExplorer#setCoverage(ImageRequest) @@ -49,19 +41,18 @@ import org.apache.sis.storage.event.StoreListeners; */ public class ImageRequest { /** - * The source from where to read the image, specified at construction time. - * May be {@code null} if {@link #coverage} instance was specified at construction time. + * The source from where to read the image. + * One of {@code resource} and {@link #coverage} fields is non-null. */ final GridCoverageResource resource; /** - * The source for rendering the image, specified at construction time. - * After construction, only one of {@link #resource} and {@code coverage} fields is non-null. - * But after {@link Loader} task execution, this field will be set to the coverage which has been read. + * The source for rendering the image. + * One of {@link #resource} and {@code coverage} fields is non-null. * * @see #getCoverage() */ - private volatile GridCoverage coverage; + final GridCoverage coverage; /** * Desired grid extent and resolution, or {@code null} for reading the whole domain. @@ -82,12 +73,10 @@ public class ImageRequest { /** * A subspace of the grid coverage extent to render, or {@code null} for the whole extent. * It can be used for specifying a slice in a <var>n</var>-dimensional data cube. - * If not specified by the user, will be updated to the extent actually rendered. * * @see #getSliceExtent() - * @see #setSliceExtent(GridExtent) */ - private volatile GridExtent sliceExtent; + final GridExtent slice; /** * Creates a new request with both a resource and a coverage. At least one argument shall be non-null. @@ -103,6 +92,7 @@ public class ImageRequest { coverage = data; domain = null; range = null; + slice = null; } /** @@ -127,6 +117,8 @@ public class ImageRequest { * GridCoverageResource, which is the class making real use of it. This is not sensitive * object state here. */ + this.coverage = null; + this.slice = null; } /** @@ -136,25 +128,33 @@ public class ImageRequest { * or for rendering data along other dimensions, a slice extent can be specified as documented in the * {@linkplain GridCoverage#render(GridExtent) render method javadoc}. * - * @param source source of the image to load. - * @param sliceExtent a subspace of the grid coverage extent to render, or {@code null} for the whole extent. + * @param source source of the image to load. + * @param slice a subspace of the grid coverage extent to render, or {@code null} for the whole extent. * * @see GridCoverage#render(GridExtent) */ - public ImageRequest(final GridCoverage source, final GridExtent sliceExtent) { + public ImageRequest(final GridCoverage source, final GridExtent slice) { ArgumentChecks.ensureNonNull("source", source); - this.resource = null; - this.domain = null; - this.range = null; - this.coverage = source; - this.sliceExtent = sliceExtent; + this.resource = null; + this.domain = null; + this.range = null; + this.coverage = source; + this.slice = slice; + } + + /** + * Returns the resource specified at construction time, or an empty value if none. + * + * @return the resource to read. + */ + public final Optional<GridCoverageResource> getResource() { + return Optional.ofNullable(resource); } /** - * Returns the coverage, or an empty value if not yet known. This is either the value specified explicitly - * to the constructor, or otherwise the coverage obtained after a read operation. + * Returns the coverage specified at construction time, or an empty value if none. * - * @return the coverage. + * @return the coverage to render. */ public final Optional<GridCoverage> getCoverage() { return Optional.ofNullable(coverage); @@ -209,141 +209,35 @@ public class ImageRequest { /** * Returns the subspace of the grid coverage extent to render. - * This method returns the first non-empty value in the following choices: + * This is the {@code sliceExtent} argument specified to the following constructor: * - * <ol> - * <li>The last value specified to {@link #setSliceExtent(GridExtent)}.</li> - * <li>The value specified to the {@link #ImageRequest(GridCoverage, GridExtent)} constructor.</li> - * <li>The extent of the default slice selected by this {@code ImageRequest} - * after completion of the reading task.</li> - * </ol> + * <blockquote>{@link #ImageRequest(GridCoverage, GridExtent)}</blockquote> * - * @return subspace of the grid coverage extent to render. + * This argument will be forwarded verbatim to the following method + * (see its javadoc for more explanation): * - * @see GridCoverage#render(GridExtent) - */ - public final Optional<GridExtent> getSliceExtent() { - return Optional.ofNullable(sliceExtent); - } - - /** - * Sets a new subspace of the grid coverage extent to render. - * This method can be used for specifying a two-dimensional slice in a <var>n</var>-dimensional data cube, - * as specified in {@link GridCoverage#render(GridExtent)} documentation. + * <blockquote>{@link GridCoverage#render(GridExtent)}</blockquote> * - * <div class="note"><b>API design note:</b> - * this {@code sliceExtent} argument is not specified - * to the {@link #ImageRequest(GridCoverageResource, GridGeometry, int[])} constructor because when reading data - * from a {@link GridCoverageResource}, a slicing can already be done by the {@link GridGeometry} {@code domain} - * argument. This method is provided for the rare cases where it may be useful to specify both the {@code domain} - * and the {@code sliceExtent}. The difference between the two ways to specify a slice is that the {@code domain} - * argument is used at reading time for reducing the amount of data to load, while this {@link sliceExtent} - * property is typically used after data has been read.</div> + * If non-empty, then all dimensions except two should have a size of 1 cell. * - * @param sliceExtent subspace of the grid coverage extent to render, or {@code null} for the whole extent. - * All dimensions except two shall have a size of 1 cell. + * @return subspace of the grid coverage extent to render. * * @see GridCoverage#render(GridExtent) */ - public final void setSliceExtent(final GridExtent sliceExtent) { - this.sliceExtent = sliceExtent; - } - - /** - * Loads the 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> - * - * <h4>Thread safety</h4> - * This class does not need to be thread-safe because it should be used only once in a well-defined life cycle. - * We nevertheless synchronize as a safety (e.g. user could give the same {@code ImageRequest} to two different - * {@link CoverageExplorer} instances). In such case the {@link GridCoverage} will be loaded only once, - * but no caching is done for the {@link RenderedImage} (because usually not needed). - * - * @param task the task invoking this method (for checking for cancellation). - * @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) throws DataStoreException { - GridCoverage cv = coverage; - final Long id = LogHandler.loadingStart(resource); - try { - if (cv == null) { - cv = MultiResolutionImageLoader.getInstance(resource, null).getOrLoad(domain, range); - } - coverage = cv = cv.forConvertedValues(true); - GridExtent ex = sliceExtent; - if (ex == null) { - final GridGeometry gg = cv.getGridGeometry(); - if (gg.isDefined(GridGeometry.EXTENT)) { - ex = gg.getExtent(); - if (gg.getDimension() > MultiResolutionImageLoader.BIDIMENSIONAL) { - ex = MultiResolutionImageLoader.slice(gg.derive(), ex).getIntersection(); - } - sliceExtent = ex; - } - } - if (task.isCancelled()) { - return null; - } - return cv.render(ex); - } finally { - LogHandler.loadingStop(id); - } - } - - /** - * Configures the given status bar with the geometry of the grid coverage we have just read. - * This method is invoked in JavaFX thread after above {@link #load(FutureTask)} background - * task completed, regardless if successful or not. - * The two method calls are done (indirectly) by {@link GridView#setImage(ImageRequest)}. - */ - final void configure(final StatusBar bar) { - final Long id = LogHandler.loadingStart(resource); - try { - GridExtent ex = sliceExtent; - GridCoverage cv = coverage; - GridGeometry gg = (cv != null) ? cv.getGridGeometry() : null; - if (gg != null && ex != null) { - /* - * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates are relative - * to the requested `GridExtent`. Consequently we need to translate the grid coordinates so that the - * request coordinates start at zero. - */ - final long[] offset = new long[ex.getDimension()]; - for (final int i : bar.getXYDimensions()) { - offset[i] = Math.negateExact(ex.getLow(i)); - } - ex = ex.translate(offset); - gg = gg.translate(offset); // Does not change the "real world" envelope. - try { - gg = gg.relocate(ex); // Changes the "real world" envelope. - } catch (TransformException e) { - bar.setErrorMessage(null, e); - } - } - bar.applyCanvasGeometry(gg); - } finally { - LogHandler.loadingStop(id); - } + public final Optional<GridExtent> getSliceExtent() { + return Optional.ofNullable(slice); } /** - * Reports an exception in a dialog box. This is a convenience method for - * {@link javafx.concurrent.Task#succeeded()} implementations. + * Returns or loads the coverage. This method should be invoked in a background thread. * - * @param owner control in the window which will own the dialog, or {@code null} if unknown. - * @param exception the error that occurred. + * @return the coverage. May be a cached instance. + * @throws DataStoreException if an error occurred during the loading process. */ - final void reportError(final Node owner, final Throwable exception) { - if (resource instanceof StoreListeners) { - ExceptionReporter.canNotReadFile(owner, ((StoreListeners) resource).getSourceName(), exception); - } else { - ExceptionReporter.canNotUseResource(owner, exception); + final GridCoverage load() throws DataStoreException { + if (coverage != null) { + return coverage; } + return MultiResolutionImageLoader.getInstance(resource, null).getOrLoad(domain, range); } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java index 10283fdd33..e42e6da83e 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/MultiResolutionImageLoader.java @@ -20,14 +20,11 @@ import java.util.WeakHashMap; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.coverage.grid.GridCoverage; -import org.apache.sis.coverage.grid.GridDerivation; -import org.apache.sis.coverage.grid.GridExtent; -import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.internal.map.coverage.MultiResolutionCoverageLoader; /** - * A helper class for reading two-dimensional slices of {@link GridCoverage}. + * A helper class for reading {@link GridCoverage} for rendering purposes. * The same instance may be shared by {@link GridView} and {@link CoverageCanvas}. * {@code GridView} uses only level 0, while {@code CoverageCanvas} use any level. * @@ -35,29 +32,12 @@ import org.apache.sis.internal.map.coverage.MultiResolutionCoverageLoader; * Instances of this class are immutable (except for the cache) and safe for use by multiple threads. * The same instance may be shared by many {@link CoverageCanvas} or {@link GridView} objects. * - * <h2>Limitations (TODO)</h2> - * Current implementation reads only the two first dimensions. - * We will need to define an API for specifying which dimensions to use for the slices. - * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.2 * @module */ final class MultiResolutionImageLoader extends MultiResolutionCoverageLoader { - /** - * The {@value} value, for identifying code that assume two-dimensional objects. - */ - static final int BIDIMENSIONAL = 2; - - /** - * The relative position of slice in dimensions other than the 2 visible dimensions, - * as a ratio between 0 and 1. This may become configurable in a future version. - * - * @see GridDerivation#sliceByRatio(double, int[]) - */ - private static final double SLICE_RATIO = 0; - /** * The loaders created for grid coverage resources. */ @@ -103,54 +83,4 @@ final class MultiResolutionImageLoader extends MultiResolutionCoverageLoader { } return cached; } - - /** - * Given a {@code GridGeometry} configured with the resolution to read, returns an amended domain - * for a two-dimensional slice. - * - * @param subgrid a grid geometry with the desired resolution. - * @return the domain to read from the {@linkplain #resource resource}. - */ - @Override - protected GridGeometry getReadDomain(final GridGeometry subgrid) { - return slice(subgrid); - } - - /** - * Returns the given grid geometry with grid indices narrowed to a two dimensional slice. - * If more than two dimensions are eligible, this method selects the 2 first ones. - * - * @param gg the grid geometry to reduce to two dimensions, or {@code null}. - * @return the given grid geometry reduced to 2 dimensions, or {@code null} if the geometry was null. - */ - static GridGeometry slice(GridGeometry gg) { - if (gg != null && gg.getDimension() > BIDIMENSIONAL && gg.isDefined(GridGeometry.EXTENT)) { - gg = slice(gg.derive(), gg.getExtent()).build(); - } - return gg; - } - - /** - * Configures the given {@link GridDerivation} for applying a two-dimensional slice. - * This method selects the two first dimensions having a size greater than 1 cell. - * - * @param subgrid a grid geometry builder pre-configured with the desired resolution. - * @param extent extent of the coverage to read, in units of the finest level. - * @return the builder configured for returning the desired two-dimensional slice. - */ - static GridDerivation slice(final GridDerivation subgrid, final GridExtent extent) { - final int dimension = extent.getDimension(); - if (dimension <= BIDIMENSIONAL) { - return subgrid; - } - 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 subgrid.sliceByRatio(SLICE_RATIO, sliceDimensions); - } } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java index ff6e9bd8a5..89a993c829 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/ViewAndControls.java @@ -35,8 +35,10 @@ import org.apache.sis.internal.gui.Styles; import org.apache.sis.storage.Resource; import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.coverage.grid.GridCoverage; -import org.apache.sis.gui.map.StatusBar; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.GridExtent; import org.apache.sis.util.resources.IndexedResourceBundle; +import org.apache.sis.gui.map.StatusBar; /** @@ -106,12 +108,17 @@ abstract class ViewAndControls { */ protected final GridSliceSelector sliceSelector; + /** + * The status bar where to show cursor coordinates. + */ + protected final StatusBar status; + /** * The widget which contain this view. This is the widget to inform when the coverage changed. * - * @see #notifyDataChanged(GridCoverageResource, GridCoverage) + * @see CoverageExplorer#notifyDataChanged(GridCoverageResource, GridCoverage) */ - private final CoverageExplorer owner; + protected final CoverageExplorer owner; /** * Creates a new view-control pair. @@ -120,10 +127,11 @@ abstract class ViewAndControls { */ protected ViewAndControls(final CoverageExplorer owner) { this.owner = owner; + status = new StatusBar(owner.referenceSystems); sliceSelector = new GridSliceSelector(owner.getLocale()); viewAndNavigation = new VBox(); sliceSelector.selectedExtentProperty().addListener((p,o,n) -> { - final GridCoverage coverage = ViewAndControls.this.owner.getCoverage(); + final GridCoverage coverage = getCoverage(); if (coverage != null) { load(new ImageRequest(coverage, n)); // Show a new slice of data. } @@ -134,7 +142,7 @@ abstract class ViewAndControls { * Invoked by subclass constructors for declaring the main visual component. * The given component will be added to the {@link #viewAndNavigation} node. */ - final void setView(final Region view, final StatusBar status) { + final void setView(final Region view) { final Region bar = status.getView(); final Region nav = sliceSelector.getView(); VBox.setVgrow(view, Priority.ALWAYS); @@ -160,6 +168,13 @@ abstract class ViewAndControls { return controls; } + /** + * Returns the grid coverage shown in the view, or {@code null} if none. + */ + GridCoverage getCoverage() { + return owner.getCoverage(); + } + /** * Sets the view content to the given resource, coverage or image. * This method is invoked when a new source of data (either a resource or a coverage) is specified, @@ -170,14 +185,15 @@ abstract class ViewAndControls { abstract void load(ImageRequest request); /** - * Notifies all controls that a new coverage has been loaded. - * Subclasses shall invoke this method in the JavaFX thread after loading completed. + * Invoked when a new coverage or coverage resource has been specified. + * This method configures the status bar, adjusts the sliders and returns + * the new selected slice. This method shall be invoked in JavaFX thread. * - * @param resource the new source of coverage, or {@code null} if none. - * @param coverage the new coverage, or {@code null} if none. + * @param geometry grid geometry of the coverage or resource, or {@code null} if none. + * @return new slice to take as the currently selected slice. */ - void notifyDataChanged(final GridCoverageResource resource, final GridCoverage coverage) { - sliceSelector.gridGeometry.set(coverage != null ? coverage.getGridGeometry() : null); + final GridExtent setGeometry(final GridGeometry geometry) { + sliceSelector.gridGeometry.set(geometry); final ObservableList<Node> components = viewAndNavigation.getChildren(); final int count = components.size(); if (sliceSelector.isEmpty()) { @@ -189,7 +205,11 @@ abstract class ViewAndControls { components.add(sliceSelector.getView()); } } - owner.notifyDataChanged(resource, coverage); + // The selected slice changed as a result of new grid geometry. + final GridExtent slice = sliceSelector.selectedExtentProperty().getValue(); + final int[] xyDimensions = sliceSelector.getXYDimensions(); + status.applyCanvasGeometry(geometry, slice, xyDimensions[0], xyDimensions[1]); + return slice; } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java index cce34c15a0..f8595e285b 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/gui/map/StatusBar.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.function.Predicate; +import java.awt.image.RenderedImage; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import javax.measure.Unit; @@ -65,6 +66,7 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.geometry.GeneralDirectPosition; import org.apache.sis.geometry.CoordinateFormat; +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.portrayal.RenderException; @@ -162,7 +164,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * <p>Note that if this field is non-null, then the {@link #localToObjectiveCRS} property value may be overwritten * at any time, for example every time that a gesture event such as pan, zoom or rotation happens.</p> */ - private final MapCanvas canvas; + private MapCanvas canvas; /** * The manager of reference systems chosen by user, or {@code null} if none. @@ -212,7 +214,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * <p>This transform shall never be null. It is initially an identity transform and is modified by * {@link #applyCanvasGeometry(GridGeometry)}. The transform is usually (but not necessarily) affine * and should have no {@linkplain CoordinateOperation#getCoordinateOperationAccuracy() inaccuracy} - * (ignoring rounding error). This is normally the inverse of {@linkplain #canvas} + * (ignoring rounding error). This transform is normally the inverse of {@linkplain #canvas} * {@linkplain MapCanvas#getObjectiveToDisplay() objective to display} transform, * but temporary mismatches may exist during gesture events such as pans, zooms and rotations.</p> * @@ -272,6 +274,14 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { */ private Predicate<MapCanvas> fullOperationSearchRequired; + /** + * Indices where to assign the values of the <var>x</var> and <var>y</var> arguments in {@link #sourceCoordinates}. + * They are usually 0 for <var>x</var> and 1 for <var>y</var>. + * + * @see #BIDIMENSIONAL + */ + private int xDimension, yDimension; + /** * The source local indices before conversion to geospatial coordinates (never {@code null}). * The number of dimensions is often {@value #BIDIMENSIONAL}. May be the same array than @@ -384,12 +394,12 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { /** * Creates a new status bar for showing coordinates of mouse cursor position in a canvas. - * If the {@code canvas} argument is non-empty, this {@code StatusBar} will show coordinates + * If {@link #track(Canvas)} is invoked, then this {@code StatusBar} will show coordinates * (usually geographic or projected) of mouse cursor position when the mouse is over that canvas. * Note that in such case, the {@link #localToObjectiveCRS} property value will be overwritten * at any time (for example every time that a gesture event such as pan, zoom or rotation happens). * - * <p>If the {@code choices} argument is non-null, user will be able to select different CRS + * <p>If the {@code systemChooser} argument is non-null, user will be able to select different CRS * using the contextual menu on the status bar.</p> * * <h4>Limitations</h4> @@ -398,21 +408,16 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * to exist as long as the {@code MapCanvas} and {@code RecentReferenceSystems} instances * given to this constructor. * - * <p>Current implementation accepts only zero or one {@code MapCanvas}. A future implementation - * may accept a larger amount of canvas for tracking many views with a single status bar - * (for example images over the same area but at different times).</p> - * * @param systemChooser the manager of reference systems chosen by user, or {@code null} if none. - * @param toTrack the canvas that this status bar is tracking. - * Currently restricted to an array of length 0 or 1. */ - public StatusBar(final RecentReferenceSystems systemChooser, final MapCanvas... toTrack) { + public StatusBar(final RecentReferenceSystems systemChooser) { positionReferenceSystem = new PositionSystem(); localToObjectiveCRS = new LocalToObjective(); localToPositionCRS = localToObjectiveCRS.get(); targetCoordinates = new GeneralDirectPosition(BIDIMENSIONAL); sourceCoordinates = targetCoordinates.coordinates; lastX = lastY = Double.NaN; + yDimension = 1; format = new CoordinateFormat(); message = new Label(); @@ -485,42 +490,69 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { if (n != null) items.add(0, n.valueChoices); setSampleValuesVisible(n != null && !n.isEmpty()); }); + } + + /** + * @deprecated Replaced by {@link #StatusBar(RecentReferenceSystems)} followed by {@link #track(MapCanvas)}. + * + * @param systemChooser the manager of reference systems chosen by user, or {@code null} if none. + * @param toTrack the canvas that this status bar is tracking. + */ + @Deprecated + public StatusBar(final RecentReferenceSystems systemChooser, final MapCanvas... toTrack) { + this(systemChooser); + for (final MapCanvas canvas : toTrack) { + track(canvas); + } + } + + /** + * Registers listeners on the following canvas for track mouse movements. + * After this method call, this {@code StatusBar} will show coordinates (usually geographic or projected) + * of mouse cursor position when the mouse is over that canvas. The {@link #localToObjectiveCRS} property + * value may be overwritten at any time, for example after each gesture event such as pan, zoom or rotation. + * + * <h4>Limitations</h4> + * Current implementation accepts only zero or one {@code MapCanvas}. A future implementation + * may accept a larger amount of canvas for tracking many views with a single status bar + * (for example images over the same area but at different times). + * + * @param canvas the canvas that this status bar is tracking. + * + * @since 1.3 + */ + public void track(final MapCanvas canvas) { + ArgumentChecks.ensureNonNull("canvas", canvas); + if (this.canvas != null) { + throw new IllegalArgumentException(Errors.format( + Errors.Keys.TooManyCollectionElements_3, "canvas", 1, 2)); + } /* * If a canvas is specified, register listeners for mouse position, rendering events, errors, etc. * We do not allow the canvas to be changed after construction because of the added complexity * (e.g. we would have to remember all registered listeners so we can unregister them). */ - if (toTrack != null && toTrack.length != 0) { - if (toTrack.length != 1) { - throw new IllegalArgumentException(Errors.format( - Errors.Keys.TooManyCollectionElements_3, "toTrack", 1, toTrack.length)); - } - canvas = toTrack[0]; + this.canvas = canvas; + sampleValuesProvider.set(ValuesUnderCursor.create(canvas)); + canvas.errorProperty().addListener((p,o,n) -> setRenderingError(n)); + canvas.renderingProperty().addListener((p,o,n) -> {if (!n) applyCanvasGeometry();}); + applyCanvasGeometry(); + if (canvas.getObjectiveCRS() != null) { + registerMouseListeners(); } else { - canvas = null; - } - if (canvas != null) { - sampleValuesProvider.set(ValuesUnderCursor.create(canvas)); - canvas.errorProperty().addListener((p,o,n) -> setRenderingError(n)); - canvas.renderingProperty().addListener((p,o,n) -> {if (!n) applyCanvasGeometry();}); - applyCanvasGeometry(); - if (canvas.getObjectiveCRS() != null) { - registerMouseListeners(); - } else { - /* - * Wait for objective CRS to be known before to register listeners. - * The canvas "objective CRS" is null only for unitialized canvas. - * After the canvas has been initialized, it can not be null anymore. - * We delay listeners registration because if listeners were enabled - * on uninitialized canvas, the status bar would show irrelevant coordinates. - */ - canvas.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, new PropertyChangeListener() { - @Override public void propertyChange(final PropertyChangeEvent event) { - canvas.removePropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, this); - registerMouseListeners(); - } - }); - } + /* + * Wait for objective CRS to be known before to register listeners. + * The canvas "objective CRS" is null only for unitialized canvas. + * After the canvas has been initialized, it can not be null anymore. + * We delay listeners registration because if listeners were enabled + * on uninitialized canvas, the status bar would show irrelevant coordinates. + */ + canvas.addPropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, new PropertyChangeListener() { + @Override public void propertyChange(final PropertyChangeEvent event) { + canvas.removePropertyChangeListener(MapCanvas.OBJECTIVE_CRS_PROPERTY, this); + registerMouseListeners(); + } + }); } } @@ -584,6 +616,50 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { */ public void applyCanvasGeometry(final GridGeometry geometry) { position.setText(null); + xDimension = 0; + yDimension = 1; + apply(geometry); + } + + /** + * Configures this status bar for showing coordinates of a slice of a grid coverage. + * This method is useful for tracking the pixel coordinates of an image obtained by + * a call to {@link GridCoverage#render(GridExtent)}. + * By {@code render(GridExtent)} contract, the {@link RenderedImage} pixel coordinates + * are relative to the requested {@link GridExtent}. Consequently we need to translate + * the grid coordinates so that the request coordinates start at zero. + * This method handles that translation. + * + * @param geometry geometry of the coverage which produced the {@link RenderedImage} to track, or {@code null}. + * @param sliceExtent the extent specified in call to {@link GridCoverage#render(GridExtent)} (can be {@code null}). + * @param xdim the grid dimension where to assign the values of <var>x</var> pixel coordinates. + * @param ydim the grid dimension where to assign the values of <var>y</var> pixel coordinates. + * + * @since 1.3 + */ + public void applyCanvasGeometry(GridGeometry geometry, GridExtent sliceExtent, final int xdim, final int ydim) { + position.setText(null); + if (geometry != null) { + final int dimension = geometry.getDimension(); + ArgumentChecks.ensureDimensionMatches("sliceExtent", dimension, sliceExtent); + ArgumentChecks.ensureBetween("xdim", 0, dimension-1, xdim); + ArgumentChecks.ensureBetween("ydim", xdim+1, dimension-1, ydim); + xDimension = xdim; + yDimension = ydim; // Shall be assigned before call to `getXYDimensions()` below. + if (sliceExtent != null) { + final long[] offset = new long[dimension]; + for (final int i : getXYDimensions()) { + offset[i] = Math.negateExact(sliceExtent.getLow(i)); + } + sliceExtent = sliceExtent.translate(offset); + geometry = geometry.translate(offset); // Does not change the "real world" envelope. + try { + geometry = geometry.relocate(sliceExtent); // Changes the "real world" envelope. + } catch (TransformException e) { + setErrorMessage(null, e); + } + } + } apply(geometry); } @@ -989,15 +1065,16 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * Returns the indices of <var>x</var> and <var>y</var> coordinate values in a grid coordinate tuple. * They are the indices where to assign the values of the <var>x</var> and <var>y</var> arguments in * calls to <code>{@linkplain #setLocalCoordinates(double, double) setLocalCoordinates}(x,y)</code>. - * The default value is {0,1}, i.e. the 2 first dimensions in a coordinate tuple. + * + * <p>The default value is {0,1}, i.e. the 2 first dimensions in a coordinate tuple. + * The value can be changed by call to {@link #applyCanvasGeometry(GridGeometry, GridExtent, int, int)}.</p> * * @return indices of <var>x</var> and <var>y</var> coordinate values in a grid coordinate tuple. * * @since 1.3 */ public final int[] getXYDimensions() { - // Fixed for now, future version may allow configuration. - return ArraysExt.range(0, BIDIMENSIONAL); + return new int[] {xDimension, yDimension}; } /** @@ -1059,8 +1136,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { */ public void setLocalCoordinates(final double x, final double y) { if (x != lastX || y != lastY) { - sourceCoordinates[0] = lastX = x; - sourceCoordinates[1] = lastY = y; + sourceCoordinates[xDimension] = lastX = x; + sourceCoordinates[yDimension] = lastY = y; String text, values = null; try { convertCoordinates(); @@ -1151,8 +1228,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * @param y the <var>y</var> coordinate local to the view. */ final String formatCoordinates(final double x, final double y) throws TransformException { - sourceCoordinates[0] = x; - sourceCoordinates[1] = y; + sourceCoordinates[xDimension] = x; + sourceCoordinates[yDimension] = y; final String separator = format.getSeparator(); try { format.setSeparator("\t"); diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java index c5351dbb46..a685e33ac9 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BackgroundThreads.java @@ -31,6 +31,7 @@ import org.apache.sis.gui.DataViewer; import org.apache.sis.internal.system.Modules; import org.apache.sis.internal.system.Threads; import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.Exceptions; import static java.util.logging.Logger.getLogger; @@ -48,7 +49,7 @@ import static java.util.logging.Logger.getLogger; * Users should not rely on this implementation details.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.1 * @module */ @@ -135,7 +136,7 @@ public final class BackgroundThreads extends AtomicInteger implements ThreadFact * @param task the task to execute in JavaFX thread. * @return the task result, or {@code null} if an error occurred. */ - public static <V> V runAndWait(final Callable<V> task) { + public static <V> V runAndWaitDialog(final Callable<V> task) { final FutureTask<V> f = new FutureTask<>(task); Platform.runLater(f); try { @@ -148,6 +149,25 @@ public final class BackgroundThreads extends AtomicInteger implements ThreadFact return null; } + /** + * Runs the given task in JavaFX thread and wait for completion before to return. + * This method should <em>not</em> be invoked from JavaFX application thread. + * + * @param <V> type of result that will be returned. + * @param task the task to execute in JavaFX thread. + * @return the task result. + * @throws Exception if the task threw an exception. + */ + public static <V> V runAndWait(final Callable<V> task) throws Exception { + final FutureTask<V> f = new FutureTask<>(task); + Platform.runLater(f); + try { + return f.get(); + } catch (ExecutionException e) { + throw Exceptions.unwrap(e); + } + } + /** * Invoked at application shutdown time for stopping the executor threads after they completed their tasks. * This method returns soon but the background threads may continue for some time if they did not finished diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java index 20bb02e8ae..20a6424bf3 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/ExceptionReporter.java @@ -41,6 +41,9 @@ import javafx.scene.layout.Region; import javafx.scene.layout.VBox; import org.apache.sis.gui.Widget; import org.apache.sis.util.Classes; +import org.apache.sis.storage.DataStore; +import org.apache.sis.storage.Resource; +import org.apache.sis.storage.event.StoreListeners; /** @@ -49,7 +52,7 @@ import org.apache.sis.util.Classes; * * @author Smaniotto Enzo (GSoC) * @author Martin Desruisseaux (Geomatys) - * @version 1.1 + * @version 1.3 * @since 1.1 * @module */ @@ -180,6 +183,27 @@ public final class ExceptionReporter extends Widget { } } + /** + * Shows the reporter for a failure to read a file. + * This method does nothing if the exception is null. + * + * @param owner control in the window which will own the dialog, or {@code null} if unknown. + * @param resource the resource that can not be read. + * @param exception the error that occurred. + */ + public static void canNotReadFile(final Node owner, final Resource resource, final Throwable exception) { + final String name; + if (resource instanceof DataStore) { + name = ((DataStore) resource).getDisplayName(); + } else if (resource instanceof StoreListeners) { + name = ((StoreListeners) resource).getSourceName(); + } else { + canNotUseResource(owner, exception); + return; + } + canNotReadFile(owner, name, exception); + } + /** * Shows the reporter for a failure to read a file. * This method does nothing if the exception is null. diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java index d1b4f2c1b0..99195370a7 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/OptionalDataDownloader.java @@ -64,7 +64,7 @@ public final class OptionalDataDownloader extends OptionalInstallations { @Override protected boolean askUserAgreement(final String authority, final String license) { if (!Platform.isFxApplicationThread()) { - return BackgroundThreads.runAndWait(() -> { + return BackgroundThreads.runAndWaitDialog(() -> { return askUserAgreement(authority, license); }); } diff --git a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java index 3534a98333..4434c2ca57 100644 --- a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java +++ b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/package-info.java @@ -25,7 +25,7 @@ * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.1 * @module */ diff --git a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java index ce23aaa092..ecf9709dd5 100644 --- a/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java +++ b/application/sis-javafx/src/test/java/org/apache/sis/gui/coverage/CoverageCanvasApp.java @@ -70,8 +70,8 @@ public class CoverageCanvasApp extends Application { @Override public void start(final Stage window) { final CoverageCanvas canvas = new CoverageCanvas(); - final StatusBar statusBar = new StatusBar(null, canvas); - canvas.statusBar = statusBar; + final StatusBar statusBar = new StatusBar(null); + statusBar.track(canvas); canvas.setCoverage(createImage()); final BorderPane pane = new BorderPane(canvas.getView()); pane.setBottom(statusBar.getView()); diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java index 9bb02bc94a..c0ab6e9148 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/ImageRenderer.java @@ -100,7 +100,7 @@ import static org.apache.sis.image.PlanarImage.GRID_GEOMETRY_KEY; * Support for tiled images will be added in a future version. * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * * @see GridCoverage#render(GridExtent) * @@ -123,6 +123,9 @@ public class ImageRenderer { /** * The dimensions to select in the grid coverage for producing an image. This is an array of length * {@value GridCoverage2D#BIDIMENSIONAL} obtained by {@link GridExtent#getSubspaceDimensions(int)}. + * The array content is almost always {0,1}, but this class should work with other dimensions too. + * + * @see #getXYDimensions() */ private final int[] gridDimensions; @@ -405,6 +408,19 @@ public class ImageRenderer { return new Rectangle(imageX, imageY, width, height); } + /** + * The dimensions to select in the grid coverage for producing an image. This is the array obtained + * by <code>{@link GridExtent#getSubspaceDimensions(int) GridExtent.getSubspaceDimensions(2)}</code>. + * The array content is almost always {0,1}, i.e. the 2 first dimensions in a coordinate tuple. + * + * @return indices of <var>x</var> and <var>y</var> coordinate values in a grid coordinate tuple. + * + * @since 1.3 + */ + public final int[] getXYDimensions() { + return gridDimensions.clone(); + } + /** * Computes the conversion from pixel coordinates to CRS, together with the geospatial envelope of the image. * The {@link GridGeometry} returned by this method is derived from the {@linkplain GridCoverage#getGridGeometry() diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java index 51ee06b053..d7dfa3d54b 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/MultiResolutionCoverageLoader.java @@ -33,7 +33,6 @@ import org.apache.sis.storage.GridCoverageResource; import org.apache.sis.storage.DataStoreException; import org.apache.sis.coverage.grid.GridGeometry; import org.apache.sis.coverage.grid.GridCoverage; -import org.apache.sis.coverage.grid.GridDerivation; import org.apache.sis.coverage.grid.GridRoundingMode; import org.apache.sis.internal.util.CollectionsExt; import org.apache.sis.math.DecimalFunctions; @@ -294,7 +293,7 @@ dimensions: for (int j=0; j<tgtDim; j++) { final MathTransform gridToCRS = MathTransforms.scale(resolutions); domain = new GridGeometry(PixelInCell.CELL_CORNER, gridToCRS, areaOfInterest, GridRoundingMode.ENCLOSING); } - final GridCoverage coverage = resource.read(getReadDomain(domain), readRanges); + final GridCoverage coverage = resource.read(domain, readRanges); /* * Cache and return the coverage. The returned coverage may be a different instance * if another coverage has been cached concurrently for the same level. @@ -313,7 +312,7 @@ dimensions: for (int j=0; j<tgtDim; j++) { /** * If the a grid coverage for the given domain and range is in the cache, returns that coverage. * Otherwise loads the coverage and eventually caches it. The caching happens only if the given - * domain and range and managed by this loader. + * domain and range are managed by this loader. * * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain. * @param range 0-based indices of sample dimensions to read, or {@code null} or an empty sequence for reading them all. @@ -331,24 +330,7 @@ dimensions: for (int j=0; j<tgtDim; j++) { if (domain == null) { domain = resource.getGridGeometry(); } - return resource.read(getReadDomain(domain), readRanges); - } - - /** - * Given a {@code GridGeometry} configured with the resolution to read, returns an amended domain. - * The default implementation returns {@code domain} unchanged. - * Subclasses can override typically for selecting a two-dimensional slice. - * - * <p>This method is invoked by {@link #getOrLoad(int)} default implementation before to read a coverage.</p> - * - * @param domain a grid geometry with the desired resolution. - * @return the domain to read from the {@linkplain #resource}. - * - * @see GridDerivation#slice(DirectPosition) - * @see GridDerivation#sliceByRatio(double, int...) - */ - protected GridGeometry getReadDomain(final GridGeometry domain) { - return domain; + return resource.read(domain, readRanges); } /** diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java index 1ee35693cc..81ffbcdec8 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/RenderingData.java @@ -19,6 +19,7 @@ package org.apache.sis.internal.map.coverage; import java.util.Map; import java.util.List; import java.util.HashMap; +import java.util.Objects; import java.io.IOException; import java.io.UncheckedIOException; import java.awt.Graphics2D; @@ -42,8 +43,8 @@ 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.coverage.grid.ImageRenderer; -import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.PixelTranslation; +import org.apache.sis.coverage.SampleDimension; import org.apache.sis.geometry.AbstractEnvelope; import org.apache.sis.geometry.Envelope2D; import org.apache.sis.geometry.Shapes2D; @@ -65,6 +66,7 @@ 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.Debug; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Utilities; import org.apache.sis.util.logging.Logging; import org.apache.sis.portrayal.PlanarCanvas; // For javadoc. @@ -75,6 +77,8 @@ import static java.util.logging.Logger.getLogger; /** * The {@code RenderedImage} to draw in a {@link PlanarCanvas} together with transforms from pixel coordinates * to display coordinates. This is a helper class for implementations of stateful renderer. + * All grid geometries and transforms managed by this class are two-dimensional. + * If the source data have more dimensions, a two-dimensional slice will be taken. * * <h2>Note on Java2D optimizations</h2> * {@link Graphics2D#drawRenderedImage(RenderedImage, AffineTransform)} implementation @@ -100,13 +104,15 @@ import static java.util.logging.Logger.getLogger; * We wait to see if this class works well in the general case before doing special cases.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.1 * @module */ public class RenderingData implements Cloneable { /** * The {@value} value, for identifying code that assume two-dimensional objects. + * + * @see #xyDimensions */ private static final int BIDIMENSIONAL = 2; @@ -119,8 +125,8 @@ public class RenderingData implements Cloneable { private static final boolean CREATE_INDEX_COLOR_MODEL = true; /** - * Loader for reading and caching coverages at various resolutions. Used if no image has been - * explicitly assigned to {@link #data}, or if the image may vary depending on the resolution. + * Loader for reading and caching coverages at various resolutions. + * Required if no image has been explicitly assigned to {@link #data}. * The same instance may be shared by many {@link RenderingData} objects. */ public MultiResolutionCoverageLoader coverageLoader; @@ -131,7 +137,20 @@ public class RenderingData implements Cloneable { private int currentPyramidLevel; /** - * The data fetched from {@link GridCoverage#render(GridExtent)} for current {@code sliceExtent}. + * The slice extent which has been used for rendering the {@linkplain #data}. + * May be {@code null} if the grid coverage has only two dimensions with a size greater than 1 cell. + */ + private GridExtent currentSlice; + + /** + * The dimensions to select in the grid coverage for producing an image. + * This is an array of length {@value #BIDIMENSIONAL} almost always equal to {0,1}. + * The values are inferred from {@link #currentSlice}. + */ + private int[] xyDimensions; + + /** + * The data fetched from {@link GridCoverage#render(GridExtent)} for {@link #currentSlice}. * This rendered image may be tiled and fetching those tiles may require computations to be performed * in background threads. Pixels in this {@code data} image are mapped to pixels in the display * {@link PlanarCanvas} by the following chain of operations: @@ -146,8 +165,7 @@ public class RenderingData implements Cloneable { * * @see #dataGeometry * @see #dataRanges - * @see #isEmpty() - * @see #loadIfNeeded(LinearTransform, DirectPosition, GridExtent) + * @see #ensureImageLoaded(GridCoverage, GridExtent, boolean) * @see #getSourceImage() */ private RenderedImage data; @@ -161,7 +179,7 @@ public class RenderingData implements Cloneable { * * @see #data * @see #dataRanges - * @see #setCoverageSpace(GridGeometry, List) + * @see #setImageSpace(GridGeometry, List, int[]) */ private GridGeometry dataGeometry; @@ -169,7 +187,7 @@ public class RenderingData implements Cloneable { * Ranges of sample values in each band of {@link #data}. This is used for determining on which sample values * to apply colors when user asked to apply a color ramp. May be {@code null}. * - * @see #setCoverageSpace(GridGeometry, List) + * @see #setImageSpace(GridGeometry, List, int[]) * @see #statistics() */ private List<SampleDimension> dataRanges; @@ -243,6 +261,8 @@ public class RenderingData implements Cloneable { data = null; dataRanges = null; dataGeometry = null; + xyDimensions = null; + currentSlice = null; } /** @@ -270,19 +290,22 @@ public class RenderingData implements Cloneable { } /** - * Sets the input space (domain) and output space (ranges) of the coverage to be rendered. - * It should be followed by a call to {@link #ensureImageLoaded(GridCoverage, GridExtent)}. + * Sets the input space (domain) and output space (ranges) of the image to be rendered. + * Those values can be initially provided by {@link org.apache.sis.storage.GridCoverageResource} + * and replaced later by the actual {@link GridCoverage} values after coverage loading is completed. + * It is caller's responsibility to reduce <var>n</var>-dimensional domain to two dimensions. * * @param domain the two-dimensional grid geometry, or {@code null} if there is no data. * @param ranges descriptions of bands, or {@code null} if there is no data. - * - * @see #isEmpty() + * @param xyDims the dimensions to select in the grid coverage for producing an image. + * This is an array of length {@value #BIDIMENSIONAL} almost always equal to {0,1}. */ @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") - public final void setCoverageSpace(final GridGeometry domain, final List<SampleDimension> ranges) { + public final void setImageSpace(final GridGeometry domain, final List<SampleDimension> ranges, final int[] xyDims) { processor.setFillValues(SampleDimensions.backgrounds(ranges)); dataRanges = ranges; // Not cloned because already an unmodifiable list. dataGeometry = domain; + xyDimensions = xyDims; /* * If the grid geometry does not define a "grid to CRS" transform, set it to an identity transform. * We do that because this class needs a complete `GridGeometry` as much as possible. @@ -305,7 +328,7 @@ public class RenderingData implements Cloneable { * It is caller's responsibility to ensure that {@link #coverageLoader} has a non-null value * and is using the right resource before to invoke this method. * - * <p>Caller should invoke {@link #ensureImageLoaded(GridCoverage, GridExtent)} + * <p>Caller should invoke {@link #ensureImageLoaded(GridCoverage, GridExtent, boolean)} * after this method (this is not done automatically).</p> * * @param objectiveToDisplay transform used for rendering the coverage on screen. @@ -315,37 +338,40 @@ public class RenderingData implements Cloneable { * @throws TransformException if an error occurred while computing resolution from given transforms. * @throws DataStoreException if an error occurred while loading the coverage. * - * @see #setCoverageSpace(GridGeometry, List) + * @see #setImageSpace(GridGeometry, List, int[]) */ public final GridCoverage ensureCoverageLoaded(final LinearTransform objectiveToDisplay, final DirectPosition objectivePOI) throws TransformException, DataStoreException { final MathTransform dataToObjective = (changeOfCRS != null) ? changeOfCRS.getMathTransform() : null; - final int level = coverageLoader.findPyramidLevel(dataToObjective, objectiveToDisplay, objectivePOI); + final MultiResolutionCoverageLoader loader = coverageLoader; + final int level = loader.findPyramidLevel(dataToObjective, objectiveToDisplay, objectivePOI); if (data != null && level == currentPyramidLevel) { return null; } data = null; currentPyramidLevel = level; - return coverageLoader.getOrLoad(level); + return loader.getOrLoad(level); } /** - * Fetches the rendered image if {@linkplain #data} is null. This method needs to be invoked at least - * once after {@link #setCoverageSpace(GridGeometry, List)}. The {@code coverage} given in argument - * may be the value returned by {@link #ensureCoverageLoaded(LinearTransform, DirectPosition)}. + * Fetches the rendered image if {@linkplain #data} is null or is for a different slice. + * This method needs to be invoked at least once after {@link #setImageSpace(GridGeometry, List, int[])}. + * The {@code coverage} given in argument should be the value returned by a previous call to + * {@link #ensureCoverageLoaded(LinearTransform, DirectPosition)}, except that it shall not be null. * - * @param coverage the coverage from which to read data, or {@code null} if the coverage did not changed. + * @param coverage the coverage from which to read data. Shall not be null. * @param sliceExtent a subspace of the grid coverage extent where all dimensions except two have a size of 1 cell. - * May be {@code null} if this grid coverage has only two dimensions with a size greater than 1 cell. + * May be {@code null} if this grid coverage has only two dimensions with a size greater than 1 cell. + * @param force whether to force data loading. Should be {@code true} if {@code coverage} changed since last call. * @return whether the {@linkpalin #data} changed. * @throws FactoryException if the CRS changed but the transform from old to new CRS can not be determined. * @throws TransformException if an error occurred while transforming coordinates from old to new CRS. */ - public final boolean ensureImageLoaded(GridCoverage coverage, final GridExtent sliceExtent) + public final boolean ensureImageLoaded(GridCoverage coverage, final GridExtent sliceExtent, final boolean force) throws FactoryException, TransformException { - if (data != null || coverage == null) { + if (!force && data != null && Objects.equals(currentSlice, sliceExtent)) { return false; } coverage = coverage.forConvertedValues(true); @@ -353,9 +379,19 @@ public class RenderingData implements Cloneable { final List<SampleDimension> ranges = coverage.getSampleDimensions(); final RenderedImage image = coverage.render(sliceExtent); final Object value = image.getProperty(PlanarImage.GRID_GEOMETRY_KEY); - final GridGeometry domain = (value instanceof GridGeometry) ? (GridGeometry) value - : new ImageRenderer(coverage, sliceExtent).getImageGeometry(BIDIMENSIONAL); - setCoverageSpace(domain, ranges); + final GridGeometry domain; + final int[] xyDims; + if (value instanceof GridGeometry) { + domain = (GridGeometry) value; + xyDims = (sliceExtent == null) ? ArraysExt.range(0, BIDIMENSIONAL) + : sliceExtent.getSubspaceDimensions(BIDIMENSIONAL); + } else { + ImageRenderer r = new ImageRenderer(coverage, sliceExtent); + domain = r.getImageGeometry(BIDIMENSIONAL); + xyDims = r.getXYDimensions(); + } + setImageSpace(domain, ranges, xyDims); + currentSlice = sliceExtent; data = image; /* * Update the transforms in a way that preserve the current zoom level, translation, etc. @@ -412,7 +448,7 @@ public class RenderingData implements Cloneable { /** * Returns the image which will be used as the source for rendering operations. * - * @return the image loaded be {@link #ensureImageLoaded(GridCoverage, GridExtent)}. + * @return the image loaded be {@link #ensureImageLoaded(GridCoverage, GridExtent, boolean)}. */ public final RenderedImage getSourceImage() { return data; @@ -453,10 +489,27 @@ public class RenderingData implements Cloneable { protected final Map<String,Object> statistics() throws DataStoreException { if (statistics == null) { RenderedImage image = data; - if (coverageLoader != null) { - final int level = coverageLoader.getLastLevel(); + final MultiResolutionCoverageLoader loader = coverageLoader; + if (loader != null) { + final int level = loader.getLastLevel(); if (level != currentPyramidLevel) { - image = coverageLoader.getOrLoad(level).forConvertedValues(true).render(null); + /* + * If coarser data are available, we will compute statistics on those data instead of on the + * current pyramid level. We need to adjust the slice extent to the coordinates of coarser data. + */ + final GridCoverage coarse = loader.getOrLoad(level).forConvertedValues(true); + GridExtent sliceExtent = currentSlice; + if (sliceExtent != null) { + if (sliceExtent.getDimension() <= BIDIMENSIONAL) { + sliceExtent = null; + } else { + final GridExtent ce = coarse.getGridGeometry().getExtent(); + for (final int i : xyDimensions) { + sliceExtent = sliceExtent.withRange(i, ce.getLow(i), ce.getHigh(i)); + } + } + } + image = coarse.render(sliceExtent); } } statistics = processor.valueOfStatistics(image, null, SampleDimensions.toSampleFilters(processor, dataRanges)); diff --git a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java index ab1e6286be..c854222da7 100644 --- a/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java +++ b/core/sis-portrayal/src/main/java/org/apache/sis/internal/map/coverage/package-info.java @@ -23,7 +23,7 @@ * may change in incompatible ways in any future version without notice. * * @author Martin Desruisseaux (Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.2 * @module */ diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java index 8acf005185..4240e1be35 100644 --- a/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java +++ b/core/sis-utility/src/main/java/org/apache/sis/util/ArgumentChecks.java @@ -28,6 +28,9 @@ import org.opengis.geometry.MismatchedDimensionException; import org.apache.sis.internal.util.Strings; import org.apache.sis.util.resources.Errors; +// Branch-specific dependencies +import org.opengis.coverage.grid.GridEnvelope; + /** * Static methods for performing argument checks. @@ -84,7 +87,7 @@ import org.apache.sis.util.resources.Errors; * * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.1 + * @version 1.3 * @since 0.3 * @module */ @@ -873,6 +876,30 @@ public final class ArgumentChecks extends Static { } } + /** + * Ensures that the given grid envelope, if non-null, has the expected number of dimensions. + * This method does nothing if the given grid envelope is null. + * + * @param name the name of the argument to be checked. Used only if an exception is thrown. + * @param expected the expected number of dimensions. + * @param envelope the grid envelope to check for its dimension, or {@code null}. + * @throws MismatchedDimensionException if the given envelope is non-null and does + * not have the expected number of dimensions. + * + * @since 1.3 + */ + public static void ensureDimensionMatches(final String name, final int expected, final GridEnvelope envelope) + throws MismatchedDimensionException + { + if (envelope != null) { + final int dimension = envelope.getDimension(); + if (dimension != expected) { + throw new MismatchedDimensionException(Errors.format( + Errors.Keys.MismatchedDimension_3, name, expected, dimension)); + } + } + } + /** * Ensures that the given transform, if non-null, has the expected number of source and target dimensions. * This method does nothing if the given transform is null.
