This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new d5ff44456b Fix version of visual indications of which tiles are loaded
in the JavaFX application.
d5ff44456b is described below
commit d5ff44456b0f844f0a2aa68af9cf7105147d35b2
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Mar 13 06:23:56 2026 +0100
Fix version of visual indications of which tiles are loaded in the JavaFX
application.
---
.../org/apache/sis/coverage/grid/GridGeometry.java | 24 +-
.../main/org/apache/sis/image/PlanarImage.java | 2 +-
.../org/apache/sis/storage/geotiff/DataSubset.java | 1 +
.../org/apache/sis/storage/event/StoreEvent.java | 2 +-
.../apache/sis/storage/event/StoreListeners.java | 14 +-
.../org/apache/sis/storage/event/package-info.java | 4 +-
.../apache/sis/storage/tiling/TileReadEvent.java | 234 ++++++++++++++++++++
.../sis/storage/tiling/TiledGridCoverage.java | 140 ++++++++++--
.../storage/tiling/TiledGridCoverageResource.java | 46 +++-
.../apache/sis/storage/geoheif/FromImageIO.java | 9 +-
.../main/org/apache/sis/storage/geoheif/Image.java | 3 +-
.../apache/sis/gui/coverage/CoverageCanvas.java | 244 +++++++++++++++++++--
.../apache/sis/gui/coverage/CoverageControls.java | 9 +-
.../apache/sis/gui/internal/BackgroundThreads.java | 2 +-
.../org/apache/sis/gui/internal/Resources.java | 5 +
.../apache/sis/gui/internal/Resources.properties | 1 +
.../sis/gui/internal/Resources_fr.properties | 1 +
.../apache/sis/gui/internal/ShapeConverter.java | 153 +++++++++++++
.../org/apache/sis/gui/map/GestureFollower.java | 18 +-
.../main/org/apache/sis/gui/map/MapCanvas.java | 26 ++-
.../main/org/apache/sis/gui/map/MapCanvasAWT.java | 14 +-
.../main/org/apache/sis/gui/map/package-info.java | 2 +-
22 files changed, 884 insertions(+), 70 deletions(-)
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
index cbbfcbe626..ad64df989b 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridGeometry.java
@@ -142,7 +142,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
- * @version 1.6
+ * @version 1.7
* @since 1.0
*/
public class GridGeometry implements LenientComparable, Serializable {
@@ -1146,7 +1146,7 @@ public class GridGeometry implements LenientComparable,
Serializable {
* Returns the {@link #geographicBBox} value or {@code null} if none.
* This method computes the box when first needed.
*/
- private final GeographicBoundingBox geographicBBox() {
+ private GeographicBoundingBox geographicBBox() {
GeographicBoundingBox bbox = geographicBBox;
if (bbox == null) {
if (getCoordinateReferenceSystem(envelope) != null &&
!envelope.isAllNaN()) {
@@ -1881,6 +1881,26 @@ public class GridGeometry implements LenientComparable,
Serializable {
}
}
+ /**
+ * Returns a coordinate operation for transforming coordinates from the
<abbr>CRS</abbr> of this grid geometry
+ * to the given <abbr>CRS</abbr>. The {@linkplain #getGeographicExtent()
geographic bounding box} of this grid
+ * geometry is used as the desired domain of validity.
+ *
+ * @param target the target <abbr>CRS</abbr> of the desired operation.
+ * @return coordinate operation from the <abbr>CRS</abbr> of this grid
geometry to the given <abbr>CRS</abbr>.
+ * @throws IncompleteGridGeometryException if this grid geometry has no
<abbr>CRS</abbr>.
+ * @throws TransformException if the coordinate operation cannot be found.
+ *
+ * @since 1.7
+ */
+ public CoordinateOperation createChangeOfCRS(final
CoordinateReferenceSystem target) throws TransformException {
+ try {
+ return findOperation(getCoordinateReferenceSystem(), target,
geographicBBox());
+ } catch (FactoryException e) {
+ throw new TransformException(e);
+ }
+ }
+
/**
* Creates a transform from cell coordinates in this grid to cell
coordinates in the given grid.
* The returned transform handles change of Coordinate Reference System
and wraparound axes
diff --git
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
index a914a29bed..a84309840b 100644
---
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
+++
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PlanarImage.java
@@ -137,7 +137,7 @@ public abstract class PlanarImage implements RenderedImage {
* <li>The {@linkplain GridGeometry#getDimension() number of grid
dimensions} is always 2.</li>
* <li>The number of {@linkplain
GridGeometry#getCoordinateReferenceSystem() CRS} dimensions is always 2.</li>
* <li>The {@linkplain GridGeometry#getExtent() grid extent} is the
{@linkplain #getBounds() image bounds}.</li>
- * <li>The {@linkplain GridGeometry#getGridToCRS grid to CRS} map pixel
coordinates "real world" coordinates
+ * <li>The {@linkplain GridGeometry#getGridToCRS grid to CRS} map pixel
coordinates to "real world" coordinates
* (always two-dimensional).</li>
* </ul>
*
diff --git
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
index 3570da4926..5179c43c9a 100644
---
a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
+++
b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/DataSubset.java
@@ -404,6 +404,7 @@ class DataSubset extends TiledGridCoverage {
for (int i=0; i<numMissings; i++) {
final Tile tile = missings[i];
if (tile.getRegionInsideTile(lower, upper,
subsampling, false)) {
+ tile.fireTileReadStarted();
origin.x = tile.originX;
origin.y = tile.originY;
tile.copyTileInfo(tileOffsets, offsets,
includedBanks, numTiles);
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreEvent.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreEvent.java
index 1d761bf3e3..a68cedc498 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreEvent.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreEvent.java
@@ -66,7 +66,7 @@ public abstract class StoreEvent extends EventObject
implements Localized {
/**
* Returns the resource where the event occurred. It is not necessarily
the {@linkplain Resource#addListener
- * resource in which listeners have been registered}; it may be one of the
resource children.
+ * resource in which listeners have been registered}. It may be one of the
resource children.
*
* @return the resource where the event occurred.
*/
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreListeners.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreListeners.java
index 246165ab57..81ee54b571 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreListeners.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/StoreListeners.java
@@ -81,7 +81,7 @@ import org.apache.sis.storage.base.StoreUtilities;
* from multiple threads.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
* @since 1.0
*/
public class StoreListeners implements Localized {
@@ -823,11 +823,15 @@ public class StoreListeners implements Localized {
// No need to synchronize this method.
ArgumentChecks.ensureNonNull("listener", listener);
ArgumentChecks.ensureNonNull("eventType", eventType);
- for (ForType<?> e = listeners; e != null; e = e.next) {
- if (e.type == eventType && e.hasListener(listener)) {
- return true;
+ StoreListeners m = this;
+ do {
+ for (ForType<?> e = m.listeners; e != null; e = e.next) {
+ if (e.type == eventType && e.hasListener(listener)) {
+ return true;
+ }
}
- }
+ m = m.parent;
+ } while (m != null);
return false;
}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/package-info.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/package-info.java
index 889f77e9a4..f38c736143 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/package-info.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/event/package-info.java
@@ -19,7 +19,7 @@
/**
* Provides interfaces and classes for dealing with different types of events
fired by resources.
* The different types of events are specified by the {@link StoreEvent}
subclasses.
- * For example if a warning occurred while reading data from a file,
+ * For example, if a warning occurred while reading data from a file,
* then the {@link org.apache.sis.storage.DataStore} implementation should
fire a {@link WarningEvent}.
*
* <p>Events may occur in the following situations:</p>
@@ -35,7 +35,7 @@
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @since 1.4
+ * @since 1.7
* @version 1.0
*/
package org.apache.sis.storage.event;
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
new file mode 100644
index 0000000000..4ff4cd8958
--- /dev/null
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileReadEvent.java
@@ -0,0 +1,234 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.tiling;
+
+import java.io.Serializable;
+import java.awt.Shape;
+import java.awt.Rectangle;
+import java.awt.geom.Rectangle2D;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.MathTransform2D;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.event.StoreEvent;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.PixelInCell;
+import org.apache.sis.geometry.Shapes2D;
+import org.apache.sis.util.internal.shared.Strings;
+
+
+/**
+ * Notifies listeners that the process of reading a tile has started.
+ * This event contains the bounding box of the tile in real world coordinates.
+ * Because this event may be sent early in the reading process, the associated
+ * {@link Tile} is generally not known yet.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @since 1.7
+ * @version 1.7
+ */
+public class TileReadEvent extends StoreEvent {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 4912145530425375808L;
+
+ /**
+ * Contextual information shared by all events emitted by the same tile
iterator.
+ * Contains the conversion from pixel coordinates to real world
coordinates.
+ */
+ static final class Context implements Serializable {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 6873392524154427892L;
+
+ /**
+ * Zero-based index of the pyramid level of the tile which is read.
+ * The level with finest resolution is the level 0.
+ *
+ * @see #getPyramidLevel()
+ * @see #getResolution()
+ */
+ final int pyramidLevel;
+
+ /**
+ * The two-dimensional grid geometry of the slice of the resource
which is represented as an image.
+ */
+ final GridGeometry sliceGeometry;
+
+ /**
+ * Lowest coordinates of the region which has been requested by the
user for producing an image.
+ * The pixel coordinates (0,0) correspond to the lowest coordinates of
the requested extent.
+ */
+ private final long offsetX, offsetY;
+
+ /**
+ * Coordinate operation from the <abbr>CRS</abbr> of the coverage to
the <abbr>CRS</abbr>
+ * given in the last call to the {@code imageToObjective(…)} method.
+ *
+ * @see #imageToObjective(CoordinateReferenceSystem)
+ */
+ private transient CoordinateOperation crsToObjective;
+
+ /**
+ * Conversion from pixel coordinates to "real world" coordinates in a
user-specified <abbr>CRS</abbr>.
+ * That user-specified <abbr>CRS</abbr> is called "objective
<abbr>CRS</abbr>" because it is often the
+ * <abbr>CRS</abbr> using for rendering purposes.
+ */
+ @SuppressWarnings("serial") // Most SIS implementations are
serializable.
+ private transient MathTransform2D imageToObjective;
+
+ /**
+ * Creates a new context.
+ *
+ * @param pyramidLevel index of the pyramid level of the tile which
is read, where 0 is the level with finest resolution.
+ * @param domain the grid geometry of the coverage of which a
slice is rendered as an image.
+ * @param aoi the coordinates requested by the user.
+ * @param xDimension dimension of the grid which is mapped to the
<var>x</var> axis in rendered images.
+ * @param yDimension dimension of the grid which is mapped to the
<var>y</var> axis in rendered images.
+ * @throws RuntimeException if a slice cannot be created in the given
dimensions.
+ */
+ Context(final int pyramidLevel, final GridGeometry domain, final
GridExtent aoi, final int xDimension, final int yDimension) {
+ this.pyramidLevel = pyramidLevel;
+ sliceGeometry = domain.selectDimensions(xDimension, yDimension);
+ offsetX = aoi.getLow(xDimension);
+ offsetY = aoi.getLow(yDimension);
+ }
+
+ /**
+ * Returns the transform from pixel coordinates to real world
coordinates in the given <abbr>CRS</abbr>.
+ *
+ * @param crs the two-dimensional <abbr>CRS</abbr> of the desired
bounding box.
+ * @return transform from pixel coordinates to real world coordinates
in the given <abbr>CRS</abbr>.
+ * @throws TransformException if the transform cannot be computed.
+ */
+ final synchronized MathTransform2D imageToObjective(final
CoordinateReferenceSystem crs) throws TransformException {
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ CoordinateOperation crsToObjective = this.crsToObjective;
+ if (crsToObjective == null ||
!CRS.equivalent(crsToObjective.getTargetCRS(), crs)) {
+ crsToObjective = sliceGeometry.createChangeOfCRS(crs);
+ MathTransform tr = MathTransforms.translation(offsetX,
offsetY);
+ tr = MathTransforms.concatenate(tr,
sliceGeometry.getGridToCRS(PixelInCell.CELL_CORNER));
+ tr = MathTransforms.concatenate(tr,
crsToObjective.getMathTransform());
+ imageToObjective = MathTransforms.bidimensional(tr);
+ this.crsToObjective = crsToObjective; // Store only after
the rest was successful.
+ }
+ return imageToObjective;
+ }
+ }
+
+ /**
+ * Contextual information shared by all events emitted by the same tile
iterator.
+ * Contains the conversion from pixel coordinates to real world
coordinates.
+ * Using a shared instance allow to reuse the cached coordinate operation.
+ */
+ private final Context context;
+
+ /**
+ * Bounds of the tile in pixel coordinates.
+ *
+ * Note: there is no public <abbr>API</abbr> yet for fetching this value
+ * because the pixel coordinates are not necessarily the same as the grid
+ * coordinates of the resource, which may confuse users.
+ */
+ private final Rectangle rasterBounds;
+
+ /**
+ * Creates a new event about a tile which will be read or has been read.
+ *
+ * @param source the resource where the event occurred.
+ * @param context contextual information shared by all events
emitted by the same tile iterator.
+ * @param rasterBounds bounds of the tile in pixel coordinates.
+ */
+ TileReadEvent(final Resource source, final Context context, final
Rectangle rasterBounds) {
+ super(source);
+ this.context = context;
+ this.rasterBounds = rasterBounds;
+ }
+
+ /**
+ * Returns the zero-based index of the pyramid level of the tile which is
read.
+ * This is typically the index in the {@linkplain
TiledGridCoverageResource#getResolutions() list
+ * of resource's resolution} where the values returned by {@link
#getResolution()} can be found.
+ * The level with finest resolution is the level 0.
+ *
+ * @return zero-based index of the pyramid level of the tile which is read.
+ *
+ * @see TiledGridCoverageResource.Pyramid#forPyramidLevel(int)
+ */
+ public int getPyramidLevel() {
+ return context.pyramidLevel;
+ }
+
+ /**
+ * Returns the resolution in units of the coverage <abbr>CRS</abbr>.
+ * The length of the returned array should be 2.
+ *
+ * @return the resolution in units of the coverage <abbr>CRS</abbr>.
+ */
+ public double[] getResolution() {
+ return context.sliceGeometry.getResolution(true);
+ }
+
+ /**
+ * Computes the bounds of the tile in the given two-dimensional
<abbr>CRS</abbr>.
+ * If the use of the given <abbr>CRS</abbr> may change straight lines into
curves
+ * (as with some map projections), the returned bounding box contains
fully (on a
+ * best-effort basis) the tile.
+ *
+ * @param crs the two-dimensional <abbr>CRS</abbr> of the desired
bounding box.
+ * @return real world coordinates of the tile expressed in the given
<abbr>CRS</abbr>.
+ * @throws TransformException if the tile bounds cannot be transformed to
the given <abbr>CRS</abbr>.
+ */
+ public Rectangle2D bounds(final CoordinateReferenceSystem crs) throws
TransformException {
+ return Shapes2D.transform(context.imageToObjective(crs), rasterBounds,
null);
+ }
+
+ /**
+ * Computes the outline of the tile in the given two-dimensional
<abbr>CRS</abbr>.
+ * The returned shape may have curved lines if the use of the given
<abbr>CRS</abbr>
+ * implies a map projection.
+ *
+ * @param crs the two-dimensional <abbr>CRS</abbr> of the desired
outline.
+ * @return real world coordinates of the tile expressed in the given
<abbr>CRS</abbr>.
+ * @throws TransformException if the tile bounds cannot be transformed to
the given <abbr>CRS</abbr>.
+ */
+ public Shape outline(final CoordinateReferenceSystem crs) throws
TransformException {
+ return
context.imageToObjective(crs).createTransformedShape(rasterBounds);
+ }
+
+ /**
+ * Returns a string representation of this event for debugging purposes.
+ *
+ * @return a string representation of this event.
+ */
+ @Override
+ public String toString() {
+ return Strings.toString(getClass(),
+ "x", rasterBounds.x,
+ "y", rasterBounds.y,
+ "width", rasterBounds.width,
+ "height", rasterBounds.height,
+ "resolution", getResolution());
+ }
+}
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
index c736b86c9e..e820ce116b 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverage.java
@@ -17,7 +17,6 @@
package org.apache.sis.storage.tiling;
import java.util.Map;
-import java.util.Locale;
import java.util.Optional;
import java.nio.file.Path;
import java.awt.Point;
@@ -43,6 +42,7 @@ import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.DisjointExtentException;
import org.apache.sis.image.internal.shared.DeferredProperty;
import org.apache.sis.image.internal.shared.TiledImage;
+import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.storage.internal.Resources;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.collection.WeakValueHashMap;
@@ -192,6 +192,15 @@ public abstract class TiledGridCoverage extends
GridCoverage {
*/
private final long[] subsampling, subsamplingOffsets;
+ /**
+ * Zero-based index of the pyramid level of this grid coverage.
+ * This is not used directly by this class, but this information is
+ * stored for providing it to {@link TileReadEvent.Context#pyramidLevel}.
+ *
+ * @see TileReadEvent#getPyramidLevel()
+ */
+ private final int pyramidLevel;
+
/**
* Indices of {@link TiledGridCoverageResource} bands which have been
retained
* for inclusion in this {@code TiledGridCoverage}, in strictly increasing
order.
@@ -271,9 +280,16 @@ public abstract class TiledGridCoverage extends
GridCoverage {
final boolean deferredTileReading;
/**
- * The locale for warnings or error messages, or {@code null} for the
default locale.
+ * The listeners to notify for tile read events.
+ * This is the value of {@link TiledGridCoverageResource#listeners} for
the resource at level 0.
*/
- private final Locale locale;
+ private final StoreListeners listeners;
+
+ /**
+ * A flag for avoiding to report the same warning many times when an error
blocks us from notifying
+ * listeners about tile read events.
+ */
+ private volatile boolean cannotNotifyListeners;
/**
* Creates a new tiled grid coverage. This constructor does not load any
tile.
@@ -285,9 +301,10 @@ public abstract class TiledGridCoverage extends
GridCoverage {
*/
protected TiledGridCoverage(final TiledGridCoverageResource.Subset subset)
{
super(subset.domain, subset.ranges);
- locale = subset.getLocale();
+ listeners = subset.listenersOfLevel0;
xDimension = subset.xDimension();
yDimension = subset.yDimension();
+ pyramidLevel = subset.pyramidLevel();
deferredTileReading = subset.deferredTileReading(); // May be
shorter than other arrays or the grid geometry.
readExtent = subset.readExtent;
subsampling = subset.subsampling;
@@ -333,7 +350,7 @@ public abstract class TiledGridCoverage extends
GridCoverage {
* Returns the localized resources for error messages.
*/
private Errors errors() {
- return Errors.forLocale(locale);
+ return Errors.forLocale(listeners.getLocale());
};
/**
@@ -591,8 +608,16 @@ public abstract class TiledGridCoverage extends
GridCoverage {
* Prepare an iterator over all tiles to read, together with the
following properties:
* - Two-dimensional conversion from pixel coordinates to "real
world" coordinates.
*/
- final var iterator = new TileIterator(tileLower, tileUpper,
offsetAOI, dimension, xDimension, yDimension);
- final Map<String,Object> properties =
DeferredProperty.forGridGeometry(gridGeometry, selectedDimensions);
+ TileReadEvent.Context eventContext = null;
+ if (listeners.hasListeners(TileReadEvent.class) &&
!cannotNotifyListeners) try {
+ eventContext = new TileReadEvent.Context(pyramidLevel,
gridGeometry, sliceExtent, xDimension, yDimension);
+ } catch (RuntimeException e) {
+ cannotNotifyListeners = true;
+ listeners.warning(e);
+ // Leave `eventContext` to null: no event will be fired.
+ }
+ final var iterator = new TileIterator(tileLower, tileUpper,
offsetAOI, dimension, xDimension, yDimension, eventContext);
+ final Map<String, Object> properties =
DeferredProperty.forGridGeometry(gridGeometry, selectedDimensions);
if (deferredTileReading) {
image = new TiledDeferredImage(imageSize, tileLower,
properties, iterator);
} else {
@@ -608,7 +633,7 @@ public abstract class TiledGridCoverage extends
GridCoverage {
} catch (DisjointExtentException | CannotEvaluateException e) {
throw e;
} catch (Exception e) { // Too many exception types for listing
them all.
- throw new
CannotEvaluateException(Resources.forLocale(locale).getString(
+ throw new
CannotEvaluateException(Resources.forLocale(listeners.getLocale()).getString(
Resources.Keys.CanNotRenderImage_1,
getIdentifier().toFullyQualifiedName()), e);
}
return image;
@@ -671,14 +696,37 @@ public abstract class TiledGridCoverage extends
GridCoverage {
*/
final int[] uncroppedTileLocation;
+ /**
+ * The context of all {@link TileReadEvent}s, or {@code null} if this
type of event will never be fired.
+ * The context contains information needed for computing the outline
of the tile that has been read.
+ */
+ final TileReadEvent.Context eventContext;
+
+ /**
+ * Whether to fire a {@code TileReadEvent}. This is {@code true} if
{@link #eventContext} is non-null
+ * and no event has been fired yet for the current tile. This is reset
to {@code false} after an event
+ * has been sent for avoiding to sent the event twice for the same
tile.
+ */
+ boolean fireTileReadEvent;
+
/**
* Creates a new area of interest.
+ *
+ * @param xDimension the dimension of the <var>x</var> axis in
rendered images.
+ * @param yDimension the dimension of the <var>y</var> axis in
rendered images.
+ * @param tmcInSubset Tile Matrix Coordinates (TMC) relative to the
enclosing {@link TiledGridCoverage}.
+ * @param uncroppedTileLocation coordinates (relative to the cropped
tile) of the upper-left corner of the uncropped tile.
+ * @param eventContext the context of all {@link TileReadEvent}s, or
{@code null} if this type of event will never be fired.
*/
- AOI(final int xDimension, final int yDimension, final int[]
tmcInSubset, final int[] uncroppedTileLocation) {
+ AOI(final int xDimension, final int yDimension, final int[]
tmcInSubset,
+ final int[] uncroppedTileLocation, final TileReadEvent.Context
eventContext)
+ {
this.xDimension = xDimension;
this.yDimension = yDimension;
this.tmcInSubset = tmcInSubset;
this.uncroppedTileLocation = uncroppedTileLocation;
+ this.eventContext = eventContext;
+ fireTileReadEvent = (eventContext != null);
}
/**
@@ -808,33 +856,46 @@ public abstract class TiledGridCoverage extends
GridCoverage {
* <p>The raster is <em>not</em> filled with {@link #fillValues}.
* Filling, if needed, should be done by the caller.</p>
*
+ * <p>If some {@linkplain TiledGridCoverageResource#listeners
resource's listeners} have
+ * registered an interest for tile read events, and if these listeners
have not yet been notified
+ * about the reading of the specified tile, then a {@link
TileReadEvent} is sent to these listeners.
+ * This policy is based on the fact that this method is typically
invoked before a tile is read.</p>
+ *
* @return a newly created, initially empty raster.
*/
public WritableRaster createRaster() {
final int x = getTileOrigin(xDimension);
final int y = getTileOrigin(yDimension);
- return Raster.createWritableRaster(getCoverage().model, new
Point(x, y));
+ final WritableRaster tile =
Raster.createWritableRaster(getCoverage().model, new Point(x, y));
+ if (fireTileReadEvent) {
+ fireTileReadEvent(tile.getBounds());
+ }
+ return tile;
}
/**
* Returns the given raster relocated at the current <abbr>AOI</abbr>
position.
* This method does not need to be invoked for tiles created by {@link
#createRaster()},
- * but may need to be invoked for tiles created by a method external
to this class.
+ * but may need to be invoked for tiles created by a method external
to this class,
+ * such as {@link javax.imageio.ImageReader#readTileRaster(int, int,
int)}.
* If the given raster is already at the current <abbr>AOI</abbr>
position,
* then this method returns that raster directly.
*
- * @param raster the raster to move at the current <abbr>AOI</abbr>
position.
+ * @param tile the raster to move at the current <abbr>AOI</abbr>
position.
* @return the relocated raster (may be {@code raster} itself).
*
* @see Raster#createTranslatedChild(int, int)
*/
- public Raster moveRaster(final Raster raster) {
+ public Raster moveRaster(Raster tile) {
final int x = getTileOrigin(xDimension);
final int y = getTileOrigin(yDimension);
- if (raster.getMinX() == x && raster.getMinY() == y) {
- return raster;
+ if (tile.getMinX() != x || tile.getMinY() != y) {
+ tile = tile.createTranslatedChild(x, y);
}
- return raster.createTranslatedChild(x, y);
+ if (fireTileReadEvent) {
+ fireTileReadEvent(tile.getBounds());
+ }
+ return tile;
}
/**
@@ -997,6 +1058,19 @@ public abstract class TiledGridCoverage extends
GridCoverage {
}
return true;
}
+
+ /**
+ * Notifies listeners that a tile is about to be read. This method
shall be invoked only if
+ * {@link #fireTileReadEvent} is true, otherwise a {@link
NullPointerException} may occur.
+ *
+ * @param bounds bounds of the raster which is about to be read, in
pixel coordinates.
+ * @throws NullPointerException if {@link #eventContext} is null.
+ */
+ final void fireTileReadEvent(final Rectangle bounds) {
+ fireTileReadEvent = false;
+ final StoreListeners listeners = getCoverage().listeners;
+ listeners.fire(TileReadEvent.class, new
TileReadEvent(listeners.getSource(), eventContext, bounds));
+ }
}
@@ -1053,9 +1127,14 @@ public abstract class TiledGridCoverage extends
GridCoverage {
final int[] offsetAOI,
final int dimension,
final int xDimension,
- final int yDimension)
+ final int yDimension,
+ final TileReadEvent.Context eventContext)
{
- super(xDimension, yDimension, tileLower.clone(),
uncroppedTileLocation(tileLower));
+ super(xDimension,
+ yDimension,
+ tileLower.clone(),
+ uncroppedTileLocation(tileLower),
+ eventContext);
this.tileLower = tileLower;
this.tileUpper = tileUpper;
this.offsetAOI = offsetAOI;
@@ -1114,7 +1193,7 @@ public abstract class TiledGridCoverage extends
GridCoverage {
for (int i = Math.min(endTile.length, upper.length); --i >= 0;) {
upper[i] = Math.max(lower[i], Math.min(upper[i], endTile[i]));
}
- return new TileIterator(lower, upper, offset, offset.length,
xDimension, yDimension);
+ return new TileIterator(lower, upper, offset, offset.length,
xDimension, yDimension, eventContext);
}
/**
@@ -1275,6 +1354,7 @@ public abstract class TiledGridCoverage extends
GridCoverage {
*/
public boolean next() {
if (++indexInResultArray >= tileCountInQuery) {
+ fireTileReadEvent = false;
return false;
}
/*
@@ -1299,6 +1379,7 @@ public abstract class TiledGridCoverage extends
GridCoverage {
tmcInSubset [i] = tileLower[i];
tileOffsetFull[i] = multiplyExact(getSubsampling(i),
offsetAOI[i]);
}
+ fireTileReadEvent = (eventContext != null);
return true;
}
}
@@ -1329,7 +1410,11 @@ public abstract class TiledGridCoverage extends
GridCoverage {
* @param iterator the iterator for which to create a snapshot of its
current position.
*/
public Snapshot(final AOI iterator) {
- super(iterator.xDimension, iterator.yDimension,
iterator.tmcInSubset.clone(), iterator.uncroppedTileLocation);
+ super(iterator.xDimension,
+ iterator.yDimension,
+ iterator.tmcInSubset.clone(),
+ iterator.uncroppedTileLocation,
+ iterator.eventContext);
coverage = iterator.getCoverage();
indexInResultArray = iterator.indexInResultArray;
indexInTileVector = iterator.indexInTileVector;
@@ -1357,6 +1442,21 @@ public abstract class TiledGridCoverage extends
GridCoverage {
if (dimension == yDimension) return originY;
throw new AssertionError(dimension);
}
+
+ /**
+ * Sends to the listeners a notification that the reading of the tile
identified by this snapshot started.
+ * If this event has not already been sent for this snapshot, this
method creates a {@link TileReadEvent}
+ * and gives this event to the {@linkplain
TiledGridCoverageResource#listeners resource's listeners}.
+ *
+ * <p>This event is sent only once per {@code Snapshot} instance.
+ * If this method is invoked more than once, the extra calls are
no-operation.</p>
+ */
+ public void fireTileReadStarted() {
+ if (fireTileReadEvent) {
+ final SampleModel model = coverage.model;
+ fireTileReadEvent(new Rectangle(originX, originY,
model.getWidth(), model.getHeight()));
+ }
+ }
}
/**
diff --git
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
index aa6d7cc585..d8e6142da1 100644
---
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
+++
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledGridCoverageResource.java
@@ -18,7 +18,6 @@ package org.apache.sis.storage.tiling;
import java.util.List;
import java.util.Arrays;
-import java.util.Locale;
import java.util.Objects;
import java.util.Collection;
import java.util.Spliterator;
@@ -52,6 +51,7 @@ import org.apache.sis.storage.Resource;
import org.apache.sis.storage.AbstractGridCoverageResource;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.RasterLoadingStrategy;
+import org.apache.sis.storage.event.StoreListeners;
import org.apache.sis.measure.NumberRange;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.ArgumentChecks;
@@ -161,6 +161,15 @@ public abstract class TiledGridCoverageResource extends
AbstractGridCoverageReso
*/
private Collection<TileMatrixSet> tileMatrixSets;
+ /**
+ * Zero-based index of the pyramid level of this grid coverage resource.
+ * This is not used directly by this class, but this information is stored
+ * for providing it to {@link TileReadEvent.Context#pyramidLevel}.
+ *
+ * @see TileReadEvent#getPyramidLevel()
+ */
+ private int pyramidLevel;
+
/**
* The dimension of the grid which is mapped to the <var>x</var> axis
(column indexes) in rendered images.
* This value is used, directly or indirectly, at {@link Subset} creation
time. The default value is 0.
@@ -601,6 +610,11 @@ check: if (dataType.isInteger()) {
*/
final WeakValueHashMap<CacheKey, Raster> cache;
+ /**
+ * The listeners of the resource at level 0.
+ */
+ StoreListeners listenersOfLevel0;
+
/**
* Creates parameters for the given domain and range.
*
@@ -741,6 +755,7 @@ check: if (dataType.isInteger()) {
* If they read only sub-regions or apply subsampling, then they
will need their own cache.
*/
cache = sharedCache ? rasters : new
WeakValueHashMap<>(CacheKey.class);
+ listenersOfLevel0 = listeners;
}
/**
@@ -817,11 +832,12 @@ check: if (dataType.isInteger()) {
}
/**
- * Returns the locale for warnings and error messages.
- * This is often {@code null}, which means to use the default locale.
+ * Returns the zero-based index of the pyramid level of this grid
coverage resource.
+ *
+ * @see TileReadEvent#getPyramidLevel()
*/
- final Locale getLocale() {
- return listeners.getLocale();
+ final int pyramidLevel() {
+ return pyramidLevel;
}
}
@@ -874,11 +890,11 @@ check: if (dataType.isInteger()) {
*/
final Pyramid pyramid = choosePyramid(domain, ranges);
if (pyramid == null || (bestFit = pyramid.forPyramidLevel(0)) ==
null) {
- return readAtThisPyramidLevel(domain, ranges);
+ return readAtThisPyramidLevel(domain, ranges, null);
}
+ int level = 0;
final double[] request = bestFit.convertResolutionOf(domain);
if (request != null) {
- int level = 0;
TiledGridCoverageResource c;
while ((c = pyramid.forPyramidLevel(level)) != null) {
final double[] resolution =
c.getGridGeometry().getResolution(true);
@@ -889,14 +905,15 @@ check: if (dataType.isInteger()) {
}
}
if (bestFit == this) {
- return readAtThisPyramidLevel(domain, ranges);
+ return readAtThisPyramidLevel(domain, ranges, null);
}
+ bestFit.pyramidLevel = level;
bestFit.xDimension = xDimension;
bestFit.yDimension = yDimension;
bestFit.loadingStrategy = loadingStrategy;
}
// Invoke outside the synchronization lock because the new lock may be
different.
- return bestFit.readAtThisPyramidLevel(domain, ranges);
+ return bestFit.readAtThisPyramidLevel(domain, ranges, listeners);
}
/**
@@ -905,10 +922,13 @@ check: if (dataType.isInteger()) {
*
* @param domain desired grid extent and resolution, or {@code null} for
reading the whole domain.
* @param ranges 0-based indices of sample dimensions to read, or {@code
null} or an empty sequence for reading them all.
+ * @param listenersOfLevel0 listeners of the resource at level 0, can be
{@code null} if that resource is {@code this}.
* @return the grid coverage for the specified domain and ranges.
* @throws DataStoreException if an error occurred while reading the grid
coverage data.
*/
- private GridCoverage readAtThisPyramidLevel(final GridGeometry domain,
final int... ranges) throws DataStoreException {
+ private GridCoverage readAtThisPyramidLevel(final GridGeometry domain,
final int[] ranges, final StoreListeners listenersOfLevel0)
+ throws DataStoreException
+ {
final TiledGridCoverage coverage;
final GridCoverage loaded;
final boolean preload;
@@ -918,7 +938,11 @@ check: if (dataType.isInteger()) {
preload = (loadingStrategy == null || loadingStrategy ==
RasterLoadingStrategy.AT_READ_TIME);
startTime = preload ? System.nanoTime() : 0;
try {
- coverage = read(new Subset(domain, ranges));
+ final var subset = new Subset(domain, ranges);
+ if (listenersOfLevel0 != null) {
+ subset.listenersOfLevel0 = listenersOfLevel0;
+ }
+ coverage = read(subset);
/*
* In theory the following condition is redundant with
`supportImmediateLoading()`.
* We apply it anyway in case the coverage geometry is not
what was announced.
diff --git
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
index 2db0d56ebb..b708a0a402 100644
---
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
+++
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/FromImageIO.java
@@ -149,13 +149,16 @@ final class FromImageIO extends Image {
protected Reader computeByteRanges(final
ImageResource.Coverage.ReadContext context) throws DataStoreException {
locator.resolve(0, -1, context);
return (final ChannelDataInput input) -> {
+ final int tx = Math.toIntExact(context.subTileX);
+ final int ty = Math.toIntExact(context.subTileY);
final ImageReader reader = context.getReader(provider);
setReaderInput(reader, input, context);
final BufferedImage image;
try {
- image = reader.readTile(IMAGE_INDEX,
- Math.toIntExact(context.subTileX),
- Math.toIntExact(context.subTileY));
+ if (reader.canReadRaster()) {
+ return reader.readTileRaster(IMAGE_INDEX, tx, ty);
+ }
+ image = reader.readTile(IMAGE_INDEX, tx, ty);
} finally {
reader.setInput(null);
}
diff --git
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
index b04b221e54..5db1afde60 100644
---
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
+++
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/geoheif/Image.java
@@ -117,7 +117,7 @@ abstract class Image {
* Instances are prepared and returned by {@link #computeByteRanges
computeByteRanges(…)}.
*/
@FunctionalInterface
- protected interface Reader {
+ interface Reader {
/**
* Reads a single tile from a sequence of bytes in the given input.
* The implementation is responsible for setting the stream position
before to start reading bytes.
@@ -127,6 +127,7 @@ abstract class Image {
*
* @param input a view of the byte sequences as if they were stored
in one single large extent.
* @return tile filled with the pixel values read by this method.
+ * @throws Exception any I/O error, arithmetic error or other kinds of
error.
*/
Raster readTile(ChannelDataInput input) throws Exception;
}
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
index bb8983ebfc..a6a90d2f23 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageCanvas.java
@@ -20,7 +20,9 @@ import java.util.Map;
import java.util.EnumMap;
import java.util.List;
import java.util.Locale;
+import java.util.Queue;
import java.util.concurrent.Future;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.LogRecord;
import java.io.IOException;
import java.io.InputStream;
@@ -33,16 +35,24 @@ import java.awt.image.RenderedImage;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Rectangle2D;
+import javafx.scene.Node;
+import javafx.scene.image.Image;
+import javafx.scene.paint.Color;
+import javafx.scene.shape.Shape;
import javafx.scene.layout.Region;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundImage;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
+import javafx.animation.FadeTransition;
import javafx.application.Platform;
+import javafx.collections.ObservableList;
import javafx.concurrent.Task;
+import javafx.event.ActionEvent;
+import javafx.event.EventHandler;
import javafx.geometry.Insets;
-import javafx.scene.image.Image;
+import javafx.util.Duration;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
import org.opengis.geometry.Envelope;
@@ -65,16 +75,19 @@ import org.apache.sis.geometry.Shapes2D;
import org.apache.sis.image.Colorizer;
import org.apache.sis.image.PlanarImage;
import org.apache.sis.image.Interpolation;
+import org.apache.sis.image.processing.isoline.Isolines;
+import org.apache.sis.image.internal.shared.TileErrorHandler;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.event.StoreListener;
+import org.apache.sis.storage.tiling.TileReadEvent;
import org.apache.sis.gui.map.MapCanvas;
import org.apache.sis.gui.map.MapCanvasAWT;
import org.apache.sis.portrayal.RenderException;
import org.apache.sis.map.coverage.RenderingWorkaround;
-import org.apache.sis.image.internal.shared.TileErrorHandler;
-import org.apache.sis.image.processing.isoline.Isolines;
import org.apache.sis.gui.internal.BackgroundThreads;
import org.apache.sis.gui.internal.ExceptionReporter;
+import org.apache.sis.gui.internal.ShapeConverter;
import org.apache.sis.gui.internal.GUIUtilities;
import org.apache.sis.gui.internal.LogHandler;
import org.apache.sis.util.ArraysExt;
@@ -184,7 +197,7 @@ public class CoverageCanvas extends MapCanvasAWT {
* This is used for preventing never-ending loop when a change of resource
causes a change of coverage
* or conversely.
*
- * @see #onPropertySpecified(GridCoverageResource, GridCoverage,
ObjectProperty, GridGeometry)
+ * @see #onPropertySpecified(GridCoverageResource, GridCoverageResource,
GridCoverage, ObjectProperty, GridGeometry)
*/
private boolean isCoverageAdjusting;
@@ -224,7 +237,7 @@ public class CoverageCanvas extends MapCanvasAWT {
* The {@link #data} with different operations applied on them. Currently
the only supported operation is
* color ramp stretching. The coordinate system is the one of the original
image (no resampling applied).
*/
- private final Map<Stretching,RenderedImage> derivedImages;
+ private final Map<Stretching, RenderedImage> derivedImages;
/**
* Image resampled to a CRS which can easily be mapped to the {@linkplain
#getDisplayCRS() display CRS}.
@@ -269,6 +282,12 @@ public class CoverageCanvas extends MapCanvasAWT {
*/
IsolineRenderer isolines;
+ /**
+ * Listener notified when tiles are read, for showing them on top of the
image as translucent tiles.
+ * This is {@code null} if this effect is not shown.
+ */
+ private TileReadListener tileReadListener;
+
/**
* Creates a new two-dimensional canvas for {@link RenderedImage}.
*/
@@ -292,9 +311,9 @@ public class CoverageCanvas extends MapCanvasAWT {
coverageProperty = new SimpleObjectProperty<>(this, "coverage");
sliceExtentProperty = new SimpleObjectProperty<>(this,
"sliceExtent");
interpolationProperty = new SimpleObjectProperty<>(this,
"interpolation", data.processor.getInterpolation());
- resourceProperty .addListener((p,o,n) -> onPropertySpecified(n,
null, coverageProperty, null));
- coverageProperty .addListener((p,o,n) -> onPropertySpecified(null,
n, resourceProperty, null));
- sliceExtentProperty .addListener((p,o,n) ->
onPropertySpecified(getResource(), getCoverage(), null, null));
+ resourceProperty .addListener((p,o,n) -> onPropertySpecified(o, n,
null, coverageProperty, null));
+ coverageProperty .addListener((p,o,n) ->
onPropertySpecified(getResource(), null, n, resourceProperty, null));
+ sliceExtentProperty .addListener((p,o,n) ->
onPropertySpecified(getResource(), getResource(), getCoverage(), null, null));
interpolationProperty.addListener((p,o,n) ->
onInterpolationSpecified(n));
fixedPane.setBackground(BACKGROUND);
}
@@ -479,6 +498,29 @@ public class CoverageCanvas extends MapCanvasAWT {
requestRepaint();
}
+ /**
+ * Sets whether to show for a few seconds a visual indication of which
tiles were read.
+ * If {@code true}, tiles will be shown by a translucent shape and fade
away.
+ *
+ * @param enabled whether to show for a few seconds a visual indication
of which tiles were read.
+ */
+ final void showTileReads(final boolean enabled) {
+ final GridCoverageResource resource = getResource();
+ if (enabled) {
+ if (tileReadListener == null) {
+ tileReadListener = new TileReadListener();
+ if (resource != null) {
+ resource.addListener(TileReadEvent.class,
tileReadListener);
+ }
+ }
+ } else if (tileReadListener != null) {
+ if (resource != null) {
+ resource.removeListener(TileReadEvent.class, tileReadListener);
+ }
+ tileReadListener = null;
+ }
+ }
+
/**
* Sets the Coordinate Reference System in which the coverage is resampled
before displaying.
* The new CRS must be compatible with the previous CRS, i.e. a coordinate
operation between
@@ -546,7 +588,8 @@ public class CoverageCanvas extends MapCanvasAWT {
sliceExtent = null;
zoom = null;
}
- if (getResource() != resource || getCoverage() != coverage ||
getSliceExtent() != sliceExtent) {
+ final GridCoverageResource discard = getResource();
+ if (discard != resource || getCoverage() != coverage ||
getSliceExtent() != sliceExtent) {
final boolean p = isCoverageAdjusting;
try {
isCoverageAdjusting = true;
@@ -556,7 +599,7 @@ public class CoverageCanvas extends MapCanvasAWT {
} finally {
isCoverageAdjusting = p;
}
- onPropertySpecified(resource, coverage, null, zoom);
+ onPropertySpecified(discard, resource, coverage, null, zoom);
}
}
@@ -566,13 +609,18 @@ public class CoverageCanvas extends MapCanvasAWT {
* Those information will be used for initializing "objective CRS" and
"objective to display" to new values.
* Rendering will happen in another background computation.
*
+ * @param discard the old resource, or {@code null} if none.
* @param resource the new resource, or {@code null} if none.
* @param coverage the new coverage, or {@code null} if none.
* @param toClear the property which is an alternative to the property
that has been set.
* @param zoom initial "objective to display" transform to use, or
{@code null} for automatic.
*/
- private void onPropertySpecified(final GridCoverageResource resource,
final GridCoverage coverage,
- final ObjectProperty<?> toClear, final
GridGeometry zoom)
+ private void onPropertySpecified(
+ final GridCoverageResource discard,
+ final GridCoverageResource resource,
+ final GridCoverage coverage,
+ final ObjectProperty<?> toClear,
+ final GridGeometry zoom)
{
hasCoverageOrResource = (resource != null || coverage != null);
if (isCoverageAdjusting) {
@@ -584,6 +632,10 @@ public class CoverageCanvas extends MapCanvasAWT {
} finally {
isCoverageAdjusting = false;
}
+ if (discard != resource && tileReadListener != null) {
+ if (discard != null) discard.removeListener(TileReadEvent.class,
tileReadListener);
+ if (resource != null) resource.addListener (TileReadEvent.class,
tileReadListener);
+ }
if (resource == null && coverage == null) {
runAfterRendering(this::clear);
} else if (controls != null && controls.isAdjustingSlice) {
@@ -960,6 +1012,10 @@ public class CoverageCanvas extends MapCanvasAWT {
displayBounds.y -= top;
displayBounds.height += top + margin.getBottom();
}
+ /*
+ * Help for auxiliary services. They are special cases for now,
+ * but should be refactored as styling services in a future
version.
+ */
if (canvas.isolines != null) try {
isolines = canvas.isolines.prepare();
objectiveAOI =
Shapes2D.transform(MathTransforms.bidimensional(objectiveToDisplay.inverse()),
displayBounds, null);
@@ -1096,7 +1152,16 @@ public class CoverageCanvas extends MapCanvasAWT {
*/
@Override
protected boolean commit(final MapCanvas canvas) {
- ((CoverageCanvas) canvas).cacheRenderingData(this);
+ final var cc = (CoverageCanvas) canvas;
+ cc.cacheRenderingData(this);
+ /*
+ * Help for auxiliary services. They are special cases for now,
+ * but should be refactored as styling services in a future
version.
+ */
+ final TileReadListener tileReadListener = cc.tileReadListener;
+ if (tileReadListener != null) {
+ tileReadListener.takeSnapshotOfObjectiveCRS();
+ }
if (isolines != null) {
for (final IsolineRenderer.Snapshot s : isolines) {
s.commit();
@@ -1217,6 +1282,157 @@ public class CoverageCanvas extends MapCanvasAWT {
return
Shapes2D.transform(MathTransforms.bidimensional(getObjectiveToDisplay().inverse()),
displayBounds, null);
}
+
+
+
+ /**
+ * Object notified when a tile is about to be read. The notifications can
be sent from any thread,
+ * typically a background thread which is reading the data. The tiles are
enqueued for processing
+ * in another background thread for avoiding to slow down the thread that
read the data.
+ */
+ private final class TileReadListener implements
StoreListener<TileReadEvent>, Runnable, EventHandler<ActionEvent> {
+ /**
+ * Colors of the tiles, using different colors for different
resolutions (pyramid levels).
+ */
+ private static final Color[] TILE_COLORS = {
+ Color.VIOLET, Color.RED, Color.YELLOW, Color.CYAN, Color.PALEGREEN
+ };
+
+ /**
+ * Same colors, but with transparency.
+ */
+ private static final Color[] FILL_COLORS = new
Color[TILE_COLORS.length];
+ static {
+ for (int i=0; i<FILL_COLORS.length; i++) {
+ final Color c = TILE_COLORS[i];
+ FILL_COLORS[i] = Color.color(c.getRed(), c.getGreen(),
c.getBlue(), 0.5);
+ }
+ }
+
+ /**
+ * Time that tiles are visible before they fade away.
+ */
+ private static final Duration DURATION = new Duration(4000);
+
+ /**
+ * The tiles to highlight, as a thread-safe queue.
+ */
+ private final Queue<TileReadEvent> tileEvents;
+
+ /**
+ * The JavaFX shapes (usually rectangles) for highlighting the tiles.
+ * Elements of this queue are derived from {@link #tileEvents}.
+ */
+ private final Queue<FadeTransition> tileShapes;
+
+ /**
+ * The objective <abbr>CRS</abbr> of the canvas.
+ * This information is updated in the JavaFX thread after each
rendering.
+ *
+ * @see CoverageCanvas#getObjectiveCRS()
+ */
+ private CoordinateReferenceSystem objectiveCRS;
+
+ /**
+ * The transform from objective <abbr>CRS</abbr> to the display
coordinate system of the canvas.
+ * This information is updated in the JavaFX thread after each
rendering, so that creations of
+ * JavaFX shapes will use the information that reflects the image
shown in the canvas.
+ *
+ * @see CoverageCanvas#objectiveToDisplay
+ */
+ private LinearTransform objectiveToDisplay;
+
+ /**
+ * Creates a new listener of tile read events.
+ */
+ TileReadListener() {
+ tileEvents = new ConcurrentLinkedQueue<>();
+ tileShapes = new ConcurrentLinkedQueue<>();
+ takeSnapshotOfObjectiveCRS();
+ }
+
+ /**
+ * Takes a snapshot of the objective <abbr>CRS</abbr> and transform to
display coordinate system.
+ * This method should be invoked after each rendering, so that
creations of JavaFX shapes will use
+ * the information that reflects the image shown in the canvas.
+ */
+ final synchronized void takeSnapshotOfObjectiveCRS() {
+ objectiveCRS = getObjectiveCRS();
+ objectiveToDisplay = getObjectiveToDisplay();
+ }
+
+ /**
+ * Invoked when a tile has been read. The specified tile is added to
the list of tiles to highlight.
+ * Starts a background thread for deriving the JavaFX shapes if such
thread is not already running.
+ */
+ @Override
+ public void eventOccured(final TileReadEvent event) {
+ tileEvents.add(event);
+ if (TRACE) {
+ trace("TileReadListener.accept(%d)", tileEvents.size());
+ }
+ BackgroundThreads.EXECUTOR.execute(this);
+ }
+
+ /**
+ * Invoked in a background thread for converting the tile events into
JavaFX shapes.
+ * Usually, the calculation done in this method is faster than the
reading of tiles,
+ * therefore each invocation of this method usually has only one tile
to convert.
+ */
+ @Override
+ @SuppressWarnings({"UseSpecificCatch",
"LocalVariableHidesMemberVariable"})
+ public void run() {
+ Exception error = null;
+ TileReadEvent event;
+ while ((event = tileEvents.poll()) != null) {
+ try {
+ final CoordinateReferenceSystem objectiveCRS;
+ final AffineTransform objectiveToDisplay;
+ synchronized (this) {
+ objectiveCRS = this.objectiveCRS;
+ objectiveToDisplay = (AffineTransform)
this.objectiveToDisplay;
+ }
+ final Shape tile =
ShapeConverter.convert(event.outline(objectiveCRS), objectiveToDisplay);
+ final int ic = event.getPyramidLevel() %
TILE_COLORS.length;
+ tile.setStroke(TILE_COLORS[ic]);
+ tile.setFill(FILL_COLORS[ic]);
+ tile.setOpacity(0.5);
+ final var transition = new FadeTransition(DURATION, tile);
+ transition.setFromValue(0.5);
+ transition.setToValue(0);
+ transition.setOnFinished(this);
+ tileShapes.add(transition);
+ } catch (Exception e) {
+ if (error == null) error = e;
+ else error.addSuppressed(e);
+ }
+ }
+ Platform.runLater(() -> {
+ final ObservableList<Node> children =
floatingPane.getChildren();
+ FadeTransition transition;
+ while ((transition = tileShapes.poll()) != null) {
+ children.add(transition.getNode());
+ transition.play();
+ }
+ });
+ if (error != null) {
+ unexpectedException(error);
+ }
+ }
+
+ /**
+ * Invoked when the animation on a tile is finished.
+ * This method removes the JavaFX geometry object that represented the
tile outline.
+ */
+ @Override
+ public void handle(final ActionEvent event) {
+ final var transition = (FadeTransition) event.getSource();
+ if (floatingPane.getChildren().remove(transition.getNode()) &&
TRACE) {
+ trace("TileReadListener.removeChild");
+ }
+ }
+ }
+
/**
* Invoked when an exception occurred while computing a transform but the
painting process can continue.
*/
@@ -1255,7 +1471,7 @@ public class CoverageCanvas extends MapCanvasAWT {
}
/**
- * Prints {@code "CoverageCanvas"} following by the given message if
{@link #TRACE} is {@code true}.
+ * Prints {@code "CoverageCanvas"} followed by the given message if {@link
#TRACE} is {@code true}.
* This is used for debugging purposes only.
*
* @param format the {@code printf} format string.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
index fce775001d..9e39751247 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/coverage/CoverageControls.java
@@ -20,6 +20,7 @@ import java.util.List;
import java.util.Locale;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ChoiceBox;
+import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TableView;
import javafx.scene.control.Tooltip;
@@ -128,13 +129,19 @@ final class CoverageControls extends ViewAndControls {
styling = new CoverageStyling(view);
categoryTable = styling.createCategoryTable(resources, vocabulary);
VBox.setVgrow(categoryTable, Priority.ALWAYS);
+ /*
+ * Whether to show a visual indication of which tiles are read.
+ */
+ final var showTileReads = new
CheckBox(resources.getString(Resources.Keys.ShowTileReadEvents));
+ showTileReads.selectedProperty().addListener((p,o,n) ->
view.showTileReads(n));
/*
* All sections put together.
*/
displayPane = new VBox(
labelOfGroup(vocabulary, Vocabulary.Keys.ReferenceSystem,
crsControl, true), crsControl,
labelOfGroup(vocabulary, Vocabulary.Keys.Values,
valuesControl, false), valuesControl,
- labelOfGroup(vocabulary, Vocabulary.Keys.Categories,
categoryTable, false), categoryTable);
+ labelOfGroup(vocabulary, Vocabulary.Keys.Categories,
categoryTable, false), categoryTable,
+ showTileReads);
}
/*
* "Isolines" section with the following controls:
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/BackgroundThreads.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/BackgroundThreads.java
index 1a2da2649a..32e4a18bb6 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/BackgroundThreads.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/BackgroundThreads.java
@@ -68,7 +68,7 @@ public final class BackgroundThreads extends AtomicInteger
implements ThreadFact
* The executor for background tasks. This is actually an {@link
ExecutorService} instance,
* but only the {@link Executor} method should be used according JavaFX
documentation.
*/
- private static final ExecutorService EXECUTOR =
Executors.newCachedThreadPool(new BackgroundThreads());
+ public static final ExecutorService EXECUTOR =
Executors.newCachedThreadPool(new BackgroundThreads());
/**
* For the singleton {@link #EXECUTOR}.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.java
index 542f26fb10..f0224a4976 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.java
@@ -386,6 +386,11 @@ public class Resources extends IndexedResourceBundle {
*/
public static final short SendTo = 31;
+ /**
+ * Visual indication of tile readings
+ */
+ public static final short ShowTileReadEvents = 77;
+
/**
* Size or position
*/
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.properties
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.properties
index f723beb139..616e05df42 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.properties
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources.properties
@@ -86,6 +86,7 @@ SelectCRS = Select a coordinate reference system
SelectCrsByContextMenu = For changing the projection, use contextual menu on
the map.
SelectParentLogger = Select parent logger
SendTo = Send to
+ShowTileReadEvents = Visual indication of tile readings
SizeOrPosition = Size or position
StandardErrorStream = Standard error stream
SystemMonitor = System monitor
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources_fr.properties
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources_fr.properties
index 853f861402..5d24f447dd 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources_fr.properties
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/Resources_fr.properties
@@ -91,6 +91,7 @@ SelectCRS = Choisir un syst\u00e8me de
r\u00e9f\u00e9rence des coor
SelectCrsByContextMenu = Pour changer la projection, utilisez le menu
contextuel sur la carte.
SelectParentLogger = Choisir le journal parent
SendTo = Envoyer vers
+ShowTileReadEvents = Indication visuelle des lectures de tuiles
SizeOrPosition = Taille ou position
StandardErrorStream = Flux d\u2019erreur standard
SystemMonitor = Moniteur syst\u00e8me
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/ShapeConverter.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/ShapeConverter.java
new file mode 100644
index 0000000000..638731c239
--- /dev/null
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/internal/ShapeConverter.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.internal;
+
+import java.util.ArrayList;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.PathIterator;
+import java.awt.geom.Rectangle2D;
+import javafx.scene.shape.ClosePath;
+import javafx.scene.shape.CubicCurveTo;
+import javafx.scene.shape.FillRule;
+import javafx.scene.shape.HLineTo;
+import javafx.scene.shape.LineTo;
+import javafx.scene.shape.MoveTo;
+import javafx.scene.shape.Path;
+import javafx.scene.shape.PathElement;
+import javafx.scene.shape.QuadCurveTo;
+import javafx.scene.shape.Rectangle;
+import javafx.scene.shape.Shape;
+import javafx.scene.shape.VLineTo;
+
+
+/**
+ * Converts a Java2D shape to a JavaFX shape.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public final class ShapeConverter {
+ /**
+ * Do not allow instantiation of this class.
+ */
+ private ShapeConverter() {
+ }
+
+ /**
+ * Converts the given Java2D shape to a JavaFX shape.
+ * If possible, this method simplifies the shape for example by replacing
it with a rectangle.
+ *
+ * @param shape the shape to convert.
+ * @param tr an optional affine transform to apply to the coordinates,
{@code null} if none.
+ * @return the JavaFX shape.
+ */
+ public static Shape convert(final java.awt.Shape shape, final
AffineTransform tr) {
+ if (tr == null || (tr.getType() &
(AffineTransform.TYPE_GENERAL_ROTATION |
AffineTransform.TYPE_GENERAL_TRANSFORM)) == 0) {
+ if (shape instanceof Rectangle2D) {
+ return convert((Rectangle2D) shape, tr);
+ }
+ }
+ final PathIterator it = shape.getPathIterator(tr);
+ final var elements = new ArrayList<PathElement>();
+ double x = Double.NaN, y = Double.NaN;
+ final double[] coords = new double[6];
+ while (!it.isDone()) {
+ final PathElement e;
+ switch (it.currentSegment(coords)) {
+ /*
+ * MoveTo with the following optimizations:
+ * - Omit "move to" that does not actually move.
+ * - If consecutive "move to", keep only the last one.
+ */
+ case PathIterator.SEG_MOVETO: {
+ if (x == (x = coords[0]) & y == (y = coords[1]))
continue; // Really &, not &&.
+ if (!elements.isEmpty() && elements.getLast() instanceof
MoveTo) {
+ elements.removeLast();
+ }
+ e = new MoveTo(x, y);
+ break;
+ }
+ /*
+ * LineTo with the following optimizations:
+ * - Omit "line to" that does not actually move.
+ * - Horizontal line to if the y coordinate does no change.
+ * - Vertical line to if the x coordinate does no change.
+ */
+ case PathIterator.SEG_LINETO: {
+ int change = 0;
+ if (x == (x = coords[0])) change = 1;
+ if (y == (y = coords[1])) change |= 2;
+ switch (change) {
+ case 1: e = new HLineTo(x); break;
+ case 2: e = new VLineTo(y); break;
+ case 3: e = new LineTo(x, y); break;
+ default: continue;
+ }
+ break;
+ }
+ case PathIterator.SEG_QUADTO: {
+ e = new QuadCurveTo(coords[0], coords[1], x = coords[2], y
= coords[3]);
+ break;
+ }
+ case PathIterator.SEG_CUBICTO: {
+ e = new CubicCurveTo(coords[0], coords[1], coords[2],
coords[3], x = coords[4], y = coords[5]);
+ break;
+ }
+ case PathIterator.SEG_CLOSE: {
+ x = y = Double.NaN;
+ if (!elements.isEmpty() && elements.getLast() instanceof
ClosePath) {
+ continue; // Avoid repeating `ClosePath`.
+ }
+ e = new ClosePath();
+ break;
+ }
+ default: continue;
+ }
+ elements.add(e);
+ it.next();
+ }
+ final var path = new Path(elements);
+ switch (it.getWindingRule()) {
+ case PathIterator.WIND_EVEN_ODD:
path.setFillRule(FillRule.EVEN_ODD); break;
+ case PathIterator.WIND_NON_ZERO:
path.setFillRule(FillRule.NON_ZERO); break;
+ }
+ return path;
+ }
+
+ /**
+ * Converts the given Java2D rectangle to a JavaFX rectangle. The given
affine transform, if non-null,
+ * should not contain a general rotation or general transform, otherwise
the returned rectangle will
+ * not be a good description of the returned shape.
+ *
+ * @param shape the rectangle to convert.
+ * @param tr an optional affine transform to apply to the coordinates,
{@code null} if none.
+ * @return the JavaFX rectangle.
+ */
+ private static Rectangle convert(final Rectangle2D shape, final
AffineTransform tr) {
+ final double[] coords = {
+ shape.getMinX(), shape.getMinY(),
+ shape.getMaxX(), shape.getMaxY()
+ };
+ if (tr != null) {
+ tr.transform(coords, 0, coords, 0, 2);
+ }
+ return new Rectangle(
+ Math.min(coords[0], coords[2]),
+ Math.min(coords[1], coords[3]),
+ Math.abs(coords[2] - coords[0]),
+ Math.abs(coords[3] - coords[1]));
+ }
+}
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
index c26123c944..04323ad9ac 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/GestureFollower.java
@@ -20,6 +20,7 @@ import java.awt.geom.Point2D;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.util.Optional;
+import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Path;
@@ -34,6 +35,7 @@ import javafx.event.EventHandler;
import javafx.scene.input.MouseEvent;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
+import javafx.collections.ObservableList;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.portrayal.TransformChangeEvent;
@@ -57,7 +59,7 @@ import static org.apache.sis.gui.internal.LogHandler.LOGGER;
* All events should be processed in the JavaFX thread.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
* @since 1.3
*/
public class GestureFollower extends CanvasFollower implements
EventHandler<MouseEvent> {
@@ -146,6 +148,8 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
*/
private void followCursor(final boolean enabled) {
final Pane pane = ((MapCanvas) source).floatingPane;
+ @SuppressWarnings("LocalVariableHidesMemberVariable")
+ Path cursor = this.cursor;
if (enabled) {
if (cursor == null) {
cursor = new Path(CURSOR_SHAPE);
@@ -156,8 +160,9 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
cursor.setManaged(false);
cursor.setSmooth(false);
cursor.setCache(true);
+ this.cursor = cursor;
}
- (((MapCanvas) target).floatingPane).getChildren().add(cursor);
+ getChildrenOfTargetPane().add(cursor);
pane.addEventHandler(MouseEvent.MOUSE_ENTERED, this);
pane.addEventHandler(MouseEvent.MOUSE_EXITED, this);
pane.addEventHandler(MouseEvent.MOUSE_MOVED, this);
@@ -170,11 +175,18 @@ public class GestureFollower extends CanvasFollower
implements EventHandler<Mous
pane.removeEventHandler(MouseEvent.MOUSE_MOVED, this);
pane.removeEventHandler(MouseEvent.MOUSE_DRAGGED, this);
if (cursor != null) {
- (((MapCanvas)
target).floatingPane).getChildren().remove(cursor);
+ getChildrenOfTargetPane().remove(cursor);
}
}
}
+ /**
+ * Returns the list of children in the target pane where the cursor is
shown.
+ */
+ private ObservableList<Node> getChildrenOfTargetPane() {
+ return (((MapCanvas) target).floatingPane).getChildren();
+ }
+
/**
* Returns the position for the mouse cursor in the source canvas if that
position is known.
* This information is used when the source and target canvases do not use
the same CRS.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
index 4f3c69678b..b9ee1bade1 100644
--- a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
+++ b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvas.java
@@ -131,7 +131,7 @@ import org.opengis.coordinate.MismatchedDimensionException;
* </ol>
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.7
* @since 1.1
*/
public abstract class MapCanvas extends PlanarCanvas {
@@ -723,6 +723,7 @@ public abstract class MapCanvas extends PlanarCanvas {
* Creates and registers a new handler for showing a contextual menu
in the enclosing canvas.
* It is caller responsibility to ensure that this method is invoked
only once.
*/
+ @SuppressWarnings("this-escape")
MenuHandler(final ContextMenu menu) {
super(getDisplayCRS());
this.menu = menu;
@@ -763,6 +764,7 @@ public abstract class MapCanvas extends PlanarCanvas {
* Invoked when user selected a projection centered on mouse position.
Those CRS are generated on-the-fly
* and are generally not on the list of CRS managed by {@link
RecentReferenceSystems}.
*/
+ @SuppressWarnings("UseSpecificCatch")
final void createProjectedCRS(final PositionableProjection projection)
{
try {
DirectPosition2D center = new DirectPosition2D();
@@ -809,7 +811,7 @@ public abstract class MapCanvas extends PlanarCanvas {
* @param anchor the point to keep at fixed display coordinates, or
{@code null} for default value.
* @param property the property to reset if the operation fails, or
{@code null} if none.
*/
- @SuppressWarnings("unchecked")
+ @SuppressWarnings({"unchecked", "UseSpecificCatch"})
private void setObjectiveCRS(final CoordinateReferenceSystem crs,
DirectPosition anchor,
final ObservableValue<? extends
ReferenceSystem> property)
{
@@ -1016,7 +1018,7 @@ public abstract class MapCanvas extends PlanarCanvas {
/**
* Returns {@link #transform} as a Java2D affine transform. This is the
change to append to
- * {@link #objectiveToDisplay} for getting the transform that user
currently see on screen.
+ * {@link #objectiveToDisplay} for getting the transform that user
currently sees on screen.
* This is a temporary transform, for immediate feedback to user before
the map is re-rendered.
*
* @param modifiable whether the returned transform should be modifiable.
@@ -1370,6 +1372,15 @@ public abstract class MapCanvas extends PlanarCanvas {
final Point2D p = changeInProgress.transform(xPanStart, yPanStart);
xPanStart = p.getX();
yPanStart = p.getY();
+ Affine copyOfChanges = null;
+ for (final Node child : floatingPane.getChildren()) {
+ if (needsPositionUpdateAfterRepaint(child)) {
+ if (copyOfChanges == null) {
+ copyOfChanges = new Affine(changeInProgress);
+ }
+ child.getTransforms().add(0, copyOfChanges);
+ }
+ }
try {
changeInProgress.invert();
transform.append(changeInProgress);
@@ -1408,6 +1419,15 @@ public abstract class MapCanvas extends PlanarCanvas {
}
}
+ /**
+ * Returns whether the given element of the {@link #floatingPane} children
list needs to have its position
+ * updated after a repaint event. If {@code true}, the position is updated
with the addition of an affine
+ * transform which contains the zoom changes applied by the repaint event.
+ */
+ boolean needsPositionUpdateAfterRepaint(final Node child) {
+ return true;
+ }
+
/**
* A pseudo-rendering task which wait for some delay before to perform the
real repaint.
* The intent is to collect some more gesture events (pans, zooms,
<i>etc.</i>) before consuming CPU time.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
index 494a6cca37..c5caa6417e 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/MapCanvasAWT.java
@@ -33,6 +33,7 @@ import javafx.application.Platform;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.geometry.Rectangle2D;
+import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.image.ImageView;
import javafx.scene.image.PixelBuffer;
@@ -53,7 +54,7 @@ import org.apache.sis.system.Configuration;
* controls by the user.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.7
* @since 1.1
*/
public abstract class MapCanvasAWT extends MapCanvas {
@@ -162,6 +163,17 @@ public abstract class MapCanvasAWT extends MapCanvas {
floatingPane.getChildren().add(image);
}
+ /**
+ * Returns whether the given element of the {@link #floatingPane} children
list needs to have its position
+ * updated after a repaint event. If {@code true}, the position is updated
with the addition of an affine
+ * transform which contains the zoom changes applied by the repaint event.
+ */
+ @Override
+ final boolean needsPositionUpdateAfterRepaint(final Node child) {
+ // Exclude the image because it already contains the zoom changes
applied by the repaint event.
+ return child != image;
+ }
+
/**
* Returns the image bounds. This is used for determining if a
* repaint is necessary after {@link MapCanvas} size changed.
diff --git
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/package-info.java
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/package-info.java
index 1f53783039..e76f704188 100644
---
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/package-info.java
+++
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/map/package-info.java
@@ -22,7 +22,7 @@
* {@link org.apache.sis.gui.map.MapCanvasAWT} is a specialization for
painting the map using Java2D.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @version 1.7
* @since 1.1
*/
package org.apache.sis.gui.map;