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 e26c2825bf5cc4a196873bf38c9827f5a9fbc8e0 Author: Martin Desruisseaux <[email protected]> AuthorDate: Fri May 27 01:00:44 2022 +0200 Change of slider position now cause the rendering of corresponding slice of data. It works for `GridView` only at this stage, not yet for `CoverageCanvas`. --- .../org/apache/sis/gui/coverage/ImageRequest.java | 90 +++++++++++++--------- .../apache/sis/gui/coverage/ViewAndControls.java | 6 ++ .../java/org/apache/sis/gui/map/StatusBar.java | 32 +++++++- .../org/apache/sis/coverage/grid/GridGeometry.java | 33 +++++++- .../apache/sis/coverage/grid/GridGeometryTest.java | 31 +++++++- 5 files changed, 151 insertions(+), 41 deletions(-) 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 30b2f589b8..1f09e4c607 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 @@ -20,6 +20,7 @@ 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; @@ -28,7 +29,6 @@ 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.referencing.operation.transform.MathTransforms; import org.apache.sis.storage.DataStoreException; import org.apache.sis.storage.event.StoreListeners; @@ -81,13 +81,13 @@ public class ImageRequest { /** * A subspace of the grid coverage extent to render, or {@code null} for the whole extent. - * If the extent has more than two dimensions, then the image will be rendered along the - * two first dimensions having a size greater than 1 cell. + * 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 GridExtent sliceExtent; + private volatile GridExtent sliceExtent; /** * Creates a new request with both a resource and a coverage. At least one argument shall be non-null. @@ -209,18 +209,18 @@ public class ImageRequest { /** * Returns the subspace of the grid coverage extent to render. - * This is the {@code sliceExtent} argument specified to the following constructor: + * This method returns the first non-empty value in the following choices: * - * <blockquote>{@link #ImageRequest(GridCoverage, GridExtent)}</blockquote> - * - * This argument will be forwarded verbatim to the following method - * (see its javadoc for more explanation): - * - * <blockquote>{@link GridCoverage#render(GridExtent)}</blockquote> - * - * If non-empty, then all dimensions except two should have a size of 1 cell. + * <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> * * @return subspace of the grid coverage extent to render. + * + * @see GridCoverage#render(GridExtent) */ public final Optional<GridExtent> getSliceExtent() { return Optional.ofNullable(sliceExtent); @@ -228,15 +228,22 @@ public class ImageRequest { /** * 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. * * <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}.</div> + * 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> * * @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. + * + * @see GridCoverage#render(GridExtent) */ public final void setSliceExtent(final GridExtent sliceExtent) { this.sliceExtent = sliceExtent; @@ -269,16 +276,20 @@ public class ImageRequest { cv = MultiResolutionImageLoader.getInstance(resource, null).getOrLoad(domain, range); } coverage = cv = cv.forConvertedValues(true); - if (task.isCancelled()) { - return null; - } GridExtent ex = sliceExtent; if (ex == null) { final GridGeometry gg = cv.getGridGeometry(); - if (gg.getDimension() > MultiResolutionImageLoader.BIDIMENSIONAL) { - ex = MultiResolutionImageLoader.slice(gg.derive(), gg.getExtent()).getIntersection(); + 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); @@ -287,30 +298,35 @@ public class ImageRequest { /** * Configures the given status bar with the geometry of the grid coverage we have just read. - * This method is invoked in JavaFX thread after {@link GridView#setImage(ImageRequest)} - * loaded in background thread a new image, successfully or not. + * 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 { - final GridCoverage cv = coverage; - final GridExtent ex = sliceExtent; - bar.applyCanvasGeometry(cv != null ? cv.getGridGeometry() : null); - /* - * By `GridCoverage.render(GridExtent)` contract, the `RenderedImage` pixel coordinates are relative - * to the requested `GridExtent`. Consequently we need to translate the image coordinates so that it - * become the coordinates of the original `GridGeometry` before to apply `gridToCRS`. It is okay to - * modify `StatusBar.localToObjectiveCRS` because we do not associate it to a `MapCanvas`, so it will - * not be overwritten by gesture events (zoom, pan, etc). - */ - if (ex != null) { - final double[] origin = new double[ex.getDimension()]; - for (int i=0; i<origin.length; i++) { - origin[i] = ex.getLow(i); + 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.localToObjectiveCRS.set(MathTransforms.concatenate( - MathTransforms.translation(origin), bar.localToObjectiveCRS.get())); } + bar.applyCanvasGeometry(gg); } finally { LogHandler.loadingStop(id); } 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 3e34ad71d3..ff6e9bd8a5 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 @@ -122,6 +122,12 @@ abstract class ViewAndControls { this.owner = owner; sliceSelector = new GridSliceSelector(owner.getLocale()); viewAndNavigation = new VBox(); + sliceSelector.selectedExtentProperty().addListener((p,o,n) -> { + final GridCoverage coverage = ViewAndControls.this.owner.getCoverage(); + if (coverage != null) { + load(new ImageRequest(coverage, n)); // Show a new slice of data. + } + }); } /** 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 672b10ba7c..cce34c15a0 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 @@ -16,6 +16,7 @@ */ package org.apache.sis.gui.map; +import java.util.Arrays; import java.util.Locale; import java.util.Optional; import java.util.function.Predicate; @@ -71,6 +72,7 @@ import org.apache.sis.internal.util.Strings; import org.apache.sis.measure.Quantities; import org.apache.sis.measure.Units; import org.apache.sis.util.Classes; +import org.apache.sis.util.ArraysExt; import org.apache.sis.util.Utilities; import org.apache.sis.util.Exceptions; import org.apache.sis.util.ArgumentChecks; @@ -117,6 +119,8 @@ import org.apache.sis.referencing.IdentifiedObjects; public class StatusBar extends Widget implements EventHandler<MouseEvent> { /** * The {@value} value, for identifying code that assume two-dimensional objects. + * + * @see #getXYDimensions() */ private static final int BIDIMENSIONAL = 2; @@ -600,6 +604,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { */ MathTransform localToCRS = null; CoordinateReferenceSystem crs = null; + sourceCoordinates = ArraysExt.EMPTY_DOUBLE; double resolution = 1; double[] inflate = null; Unit<?> unit = Units.PIXEL; @@ -640,6 +645,7 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { for (int i=0; i<n; i++) { inflate[i] = (0.5 / extent.getSize(i)) + 1; } + sourceCoordinates = extent.getPointOfInterest(PixelInCell.CELL_CENTER); } } final boolean sameCRS = Utilities.equalsIgnoreMetadata(objectiveCRS, crs); @@ -658,8 +664,8 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * Instead we will wait for the next mouse event to provide new local coordinates. */ ((LocalToObjective) localToObjectiveCRS).setNoCheck(localToCRS); + sourceCoordinates = Arrays.copyOf(sourceCoordinates, srcDim); targetCoordinates = new GeneralDirectPosition(tgtDim); - sourceCoordinates = new double[srcDim]; objectiveCRS = crs; localToPositionCRS = localToCRS; // May be updated again below. inflatePrecisions = inflate; @@ -693,8 +699,9 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * Other properties, in particular {@link #objectiveToPositionCRS}, must be valid. */ private void updateLocalToPositionCRS() { + localToPositionCRS = localToObjectiveCRS.get(); if (objectiveToPositionCRS != null) { - localToPositionCRS = MathTransforms.concatenate(localToObjectiveCRS.get(), objectiveToPositionCRS); + localToPositionCRS = MathTransforms.concatenate(localToPositionCRS, objectiveToPositionCRS); } setTargetCRS(format.getDefaultCRS()); } @@ -978,6 +985,21 @@ 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. + * + * @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); + } + /** * Returns the lowest value appended as "± <var>accuracy</var>" after the coordinate values. * This is the last value specified to {@link #setLowestAccuracy(Quantity)}. @@ -1024,6 +1046,12 @@ public class StatusBar extends Widget implements EventHandler<MouseEvent> { * Converts and formats the given pixel coordinates. Those coordinates will be automatically * converted to geographic or projected coordinates if a "local to CRS" conversion is available. * + * <h4>Supplemental dimensions</h4> + * If local coordinates have more than 2 dimensions, then the given (x,y) values will be assigned + * to the dimensions specified by {@link #getXYDimensions()}. Coordinates in all other dimensions + * will have the values given by {@link GridExtent#getPointOfInterest(PixelInCell)} from the extent + * of the grid geometry given to {@link #applyCanvasGeometry(GridGeometry)}. + * * @param x the <var>x</var> coordinate local to the view. * @param y the <var>y</var> coordinate local to the view. * diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java index d7108e3167..7d7c14fe8c 100644 --- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java +++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java @@ -457,7 +457,7 @@ public class GridGeometry implements LenientComparable, Serializable { * The {@link #extent}, {@link #gridToCRS} and {@link #cornerToCRS} fields must be set before this method is invoked. * * @param specified the transform specified by the user. This is not necessarily {@link #gridToCRS}. - * @param crs the coordinate reference system to declare in the envelope. + * @param crs the coordinate reference system to declare in the envelope. May be {@code null}. * @param limits if non-null, intersect with that envelope. The CRS must be the same than {@code crs}. */ private ImmutableEnvelope computeEnvelope(final MathTransform specified, final CoordinateReferenceSystem crs, @@ -1382,6 +1382,37 @@ public class GridGeometry implements LenientComparable, Serializable { return new GridGeometry(te, t1, t2, envelope, resolution, nonLinears); } + /** + * Returns a grid geometry with the given grid extent, which implies a new "real world" computation. + * The "grid to CRS" transforms and the resolution stay the same than this {@code GridGeometry}. + * The "real world" envelope is recomputed for the new grid extent using the "grid to CRS" transforms. + * + * <p>The given extent is taken verbatim; this method does no clipping. + * The given extent does not need to intersect the extent of this grid geometry.</p> + * + * @param extent extent of the grid geometry to return. + * @return grid geometry with the given extent. May be {@code this} if there is no change. + * @throws TransformException if the geospatial envelope can not be recomputed with the new grid extent. + * + * @since 1.3 + */ + public GridGeometry relocate(final GridExtent extent) throws TransformException { + ArgumentChecks.ensureNonNull("size", extent); + if (extent.equals(this.extent)) { + return this; + } + ensureDimensionMatches(getDimension(), extent); + final ImmutableEnvelope relocated; + if (cornerToCRS != null) { + final GeneralEnvelope env = extent.toCRS(cornerToCRS, gridToCRS, null); + env.setCoordinateReferenceSystem(getCoordinateReferenceSystem(envelope)); + relocated = new ImmutableEnvelope(env); + } else { + relocated = envelope; // Either null or contains only the CRS. + } + return new GridGeometry(extent, gridToCRS, cornerToCRS, relocated, resolution, nonLinears); + } + /** * Returns a grid geometry that encompass only some dimensions of this grid geometry. * The specified dimensions will be copied into a new grid geometry if necessary. diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java index c13bac0d13..be70a646f1 100644 --- a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java +++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/GridGeometryTest.java @@ -43,7 +43,7 @@ import static org.apache.sis.test.ReferencingAssert.*; * Tests the {@link GridGeometry} implementation. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.2 + * @version 1.3 * @since 1.0 * @module */ @@ -488,6 +488,35 @@ public final strictfp class GridGeometryTest extends TestCase { assertEquals(envelope, grid.getEnvelope()); } + /** + * Tests {@link GridGeometry#relocate(GridExtent)}. + * + * @throws TransformException if the relocated envelope can not be computed. + */ + @Test + public void testRelocate() throws TransformException { + final GridGeometry grid = new GridGeometry( + new GridExtent(10, 10), + PixelInCell.CELL_CORNER, + MathTransforms.linear(new Matrix3( + 2, 0, 10, + 0, 3, 20, + 0, 0, 1)), + HardCodedCRS.WGS84); + + assertSame(grid, grid.relocate(new GridExtent(10, 10))); + final GridGeometry relocated = grid.relocate(new GridExtent(20, 20)); + assertSame(grid.gridToCRS, relocated.gridToCRS); + assertSame(grid.cornerToCRS, relocated.cornerToCRS); + assertSame(grid.resolution, relocated.resolution); + assertEnvelopeEquals(new GeneralEnvelope( + new double[] {10, 20}, + new double[] {30, 50}), grid.envelope, STRICT); + assertEnvelopeEquals(new GeneralEnvelope( + new double[] {10, 20}, + new double[] {50, 80}), relocated.envelope, STRICT); + } + /** * Tests {@link GridGeometry#reduce(int...)}. */
