You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2022/05/26 23:02:32 UTC
[sis] 03/03: 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`.
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 <ma...@geomatys.com>
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...)}.
*/