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 574fd06 Implement loading of tiles in background thread.
574fd06 is described below
commit 574fd06f8c7ecf78836c0d78e2332966e4fe0c99
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Jan 25 17:08:14 2020 +0100
Implement loading of tiles in background thread.
---
.../java/org/apache/sis/gui/coverage/GridRow.java | 10 +-
.../java/org/apache/sis/gui/coverage/GridTile.java | 121 +++++++++++++++++++++
.../java/org/apache/sis/gui/coverage/GridView.java | 79 ++++++++------
.../apache/sis/internal/gui/BoundedHashMap.java | 58 ++++++++++
4 files changed, 227 insertions(+), 41 deletions(-)
diff --git
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
index 361fd4c..0c6baaf 100644
---
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
+++
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridRow.java
@@ -54,11 +54,11 @@ final class GridRow extends IndexedCell<Void> {
private int y;
/**
- * The zero-based <var>y</var> coordinate of the tile. This is not
necessarily the
- * {@code tileY} index in the image, since image tile index may not start
at zero.
+ * The <var>y</var> coordinate of the tile in the {@link RenderedImage}.
+ * Note that those coordinates do not necessarily start at zero; negative
values may be valid.
* This value is computed from {@link #y} value and cached for efficiency.
*/
- private int tileRow;
+ private int tileY;
/**
* Invoked by {@link VirtualFlow} when a new cell is needed.
@@ -81,7 +81,7 @@ final class GridRow extends IndexedCell<Void> {
public void updateIndex(final int row) {
super.updateIndex(row);
y = view.toImageY(row);
- tileRow = view.toTileRow(row);
+ tileY = view.toTileY(row);
final Skin<?> skin = getSkin();
if (skin != null) {
((GridRowSkin) skin).setRowIndex(row);
@@ -97,7 +97,7 @@ final class GridRow extends IndexedCell<Void> {
* @return the sample value in the specified column, or {@code null} if
not yet available.
*/
final String getSampleValue(final int column) {
- return view.getSampleValue(y, tileRow, column);
+ return view.getSampleValue(y, tileY, column);
}
/**
diff --git
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridTile.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridTile.java
new file mode 100644
index 0000000..e9224d6
--- /dev/null
+++
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridTile.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.gui.coverage;
+
+import java.awt.image.Raster;
+import java.awt.image.RenderedImage;
+import javafx.concurrent.Task;
+import org.apache.sis.internal.gui.BackgroundThreads;
+
+
+/**
+ * A {@link Raster} for a {@link RenderedImage} file, potentially to be loaded
in a background thread.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class GridTile {
+ /**
+ * The tile coordinates.
+ */
+ final int tileX, tileY;
+
+ /**
+ * Hash code value computed from tile indices.
+ */
+ private final int hashCode;
+
+ /**
+ * The tile, or {@code null} if not yet loaded.
+ */
+ Raster tile;
+
+ /**
+ * Whether an error occurred while reading the tile.
+ */
+ boolean error;
+
+ /**
+ * Creates a new tile for the given tile coordinates.
+ */
+ GridTile(final int tileX, final int tileY, final int numXTiles) {
+ this.tileX = tileX;
+ this.tileY = tileY;
+ hashCode = tileX + tileY * numXTiles;
+ }
+
+ /**
+ * Loads tile from the given image in a background thread and informs the
specified view
+ * when the tile become available.
+ *
+ * @param view the view for which to load a tile.
+ */
+ final void load(final GridView view) {
+ final RenderedImage image = view.getImage();
+ BackgroundThreads.execute(new Task<Raster>() {
+ /** Invoked in background thread for fetching the tile. */
+ @Override protected Raster call() {
+ return image.getTile(tileX, tileY);
+ }
+
+ /** Invoked in JavaFX thread on success. */
+ @Override protected void succeeded() {
+ super.succeeded();
+ tile = getValue();
+ error = false;
+ view.contentChanged(false);
+ }
+
+ /** Invoked in JavaFX thread on failure. */
+ @Override protected void failed() {
+ super.failed();
+ error = true;
+ }
+ });
+ }
+
+ /**
+ * Returns a hash code value for this tile.
+ */
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ /**
+ * Compares the indices of this tile with the given object for equality.
+ * Only indices are compared; the raster is ignored.
+ */
+ @Override
+ public boolean equals(final Object other) {
+ if (other instanceof GridTile) {
+ final GridTile that = (GridTile) other;
+ return tileX == that.tileX && tileY == that.tileY;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation for debugging purpose only.
+ */
+ @Override
+ public String toString() {
+ return getClass().getCanonicalName() + '[' + tileX + ", " + tileY +
']';
+ }
+}
diff --git
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
index 45b1407..47e7c3b 100644
---
a/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
+++
b/application/sis-javafx/src/main/java/org/apache/sis/gui/coverage/GridView.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.gui.coverage;
+import java.util.Map;
import java.util.Arrays;
import java.text.NumberFormat;
import java.text.FieldPosition;
@@ -37,6 +38,8 @@ import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.gui.BoundedHashMap;
/**
@@ -80,23 +83,29 @@ public class GridView extends Control {
/**
* Information copied from {@link #imageProperty} for performance.
*/
- private int width, height, minX, minY, minTileX, minTileY, tileWidth,
tileHeight, numXTiles;
+ private int width, height, minX, minY, tileWidth, tileHeight, numXTiles;
/**
* Information copied and adjusted from {@link #imageProperty} for
performance. Values are adjusted for using
* zero-based indices as expected by JavaFX tables (by contrast, pixel
indices in a {@link RenderedImage} may
- * start at a non-zero value). The results of those adjustments should be
0, but we nevertheless compute them
- * in case a {@link RenderedImage} defines unusual relationship between
image and tile coordinate system.
+ * start at a non-zero value).
*/
private int tileGridXOffset, tileGridYOffset;
/**
- * The {@link #imageProperty} tiles (fetched when first needed), or {@code
null} if the image is null.
- * All {@code Raster[]} array element and {@code Raster} sub-array
elements are initially {@code null}
- * and initialized when first needed. We store {@link
RenderedImage#getTile(int, int)} results because
- * we do not know if calling that method causes a costly computation or
not (it depends on image class).
+ * A cache of most recently used {@link #imageProperty} tiles. We use a
very simple caching mechanism here,
+ * keeping the most recently used tiles up to 10 Mb of memory. We do not
need more sophisticated mechanism
+ * since "real" caching is done by {@link
org.apache.sis.image.ComputedImage}. The purpose of this cache is
+ * to remember that a tile is immediately available and that we do not
need to start a background thread.
*/
- private Raster[][] tiles;
+ private final Map<GridTile,GridTile> tiles = new BoundedHashMap<>(10 *
(1024 * 1024) /
+ (ImageUtilities.DEFAULT_TILE_SIZE *
ImageUtilities.DEFAULT_TILE_SIZE));
+
+ /**
+ * The most recently used tile. Cached separately because it will be the
desired tile in the vast majority
+ * of cases.
+ */
+ private GridTile lastTile;
/**
* The image band to show in the table.
@@ -279,13 +288,13 @@ public class GridView extends Control {
* @param property the {@link #imageProperty} (ignored).
* @param previous the previous image (ignored).
* @param image the new image to show. May be {@code null}.
- * @throws ArithmeticException if the "tile grid x/y offset" property is
too big. Should never happen
- * since those properties should be zero after the adjustment
mentioned in their javadoc.
+ * @throws ArithmeticException if the "tile grid x/y offset" property is
too large.
*/
private void startImageLoading(final ObservableValue<? extends
RenderedImage> property,
final RenderedImage previous, final
RenderedImage image)
{
- tiles = null; // Let garbage collector dispose the rasters.
+ tiles.clear(); // Let garbage collector dispose the rasters.
+ lastTile = null;
width = 0;
height = 0;
isInteger = false;
@@ -294,14 +303,11 @@ public class GridView extends Control {
height = image.getHeight();
minX = image.getMinX();
minY = image.getMinY();
- minTileX = image.getMinTileX();
- minTileY = image.getMinTileY();
tileWidth = image.getTileWidth();
tileHeight = image.getTileHeight();
- tileGridXOffset = Math.toIntExact(((long)
image.getTileGridXOffset()) - minX + ((long) tileWidth) * minTileX);
- tileGridYOffset = Math.toIntExact(((long)
image.getTileGridYOffset()) - minY + ((long) tileHeight) * minTileY);
+ tileGridXOffset = Math.subtractExact(image.getTileGridXOffset(),
minX);
+ tileGridYOffset = Math.subtractExact(image.getTileGridYOffset(),
minY);
numXTiles = image.getNumXTiles();
- tiles = new Raster[image.getNumYTiles()][];
final SampleModel sm = image.getSampleModel();
if (sm != null) { // Should never be
null, but we are paranoiac.
final int numBands = sm.getNumBands();
@@ -345,7 +351,7 @@ public class GridView extends Control {
* may have changed including the number of rows and columns. If {@code
all} is {@code false}
* then the number of rows and columns is assumed the same.
*/
- private void contentChanged(final boolean all) {
+ final void contentChanged(final boolean all) {
final Skin<?> skin = getSkin(); // May be null if the view
is not yet shown.
if (skin instanceof GridViewSkin) { // Could be a user
instance (not recommended).
((GridViewSkin) skin).contentChanged(all);
@@ -390,12 +396,13 @@ public class GridView extends Control {
}
/**
- * Converts a grid row index to tile index. Note that the returned value
may differ from
- * the {@link RenderedImage} tile <var>y</var> coordinates because the
index returned by
- * this method is zero-based, while image tile index is arbitrary based.
+ * Converts a grid row index to tile index. Note that those {@link
RenderedImage}
+ * tile coordinates do not necessarily start at 0; negative values may be
valid.
+ *
+ * @see GridRow#tileY
*/
- final int toTileRow(final int row) {
- return Math.subtractExact(row, tileGridYOffset) / tileHeight;
+ final int toTileY(final int row) {
+ return Math.floorDiv(Math.subtractExact(row, tileGridYOffset),
tileHeight);
}
/**
@@ -403,11 +410,11 @@ public class GridView extends Control {
* time this method is invoked, then the tile will loaded in a background
thread and the grid view will
* be refreshed when the tile become available.
*
- * <p>The {@code y} parameter is computed by {@link #toImageY(int)} and
the {@code tileRow} parameter
- * is computed by {@link #toTileRow(int)}. Those values are stored in
{@link GridRow}.</p>
+ * <p>The {@code y} parameter is computed by {@link #toImageY(int)} and
the {@code tileY} parameter
+ * is computed by {@link #toTileY(int)}. Those values are stored in {@link
GridRow}.</p>
*
* @param y arbitrary-based <var>y</var> coordinate in the image
(may differ from table {@code row}).
- * @param tileRow zero-based <var>y</var> coordinate of the tile (may
differ from image tile Y).
+ * @param tileY arbitrary-based <var>y</var> coordinate of the tile.
* @param column zero-based <var>x</var> coordinate of sample to get
(may differ from image coordinate X).
* @return the sample value in the specified column, or {@code null} if
unknown (because the loading process
* is still under progress), or the empty string ({@code ""}) if
out of bounds.
@@ -415,20 +422,20 @@ public class GridView extends Control {
*
* @see GridRow#getSampleValue(int)
*/
- final String getSampleValue(final int y, final int tileRow, final int
column) {
+ final String getSampleValue(final int y, final int tileY, final int
column) {
if (y >= 0 && y < height && column >= 0 && column < width) {
- final int tx = Math.subtractExact(column, tileGridXOffset) /
tileWidth;
- Raster[] row = tiles[tileRow];
- if (row == null) {
- tiles[tileRow] = row = new Raster[Math.min(16, numXTiles)];
// Arbitrary limit, expanded if needed.
- } else if (tx >= row.length && tx < numXTiles) {
- tiles[tileRow] = row = Arrays.copyOf(row, Math.min(tx*2,
numXTiles));
+ final int tileX = Math.floorDiv(Math.subtractExact(column,
tileGridXOffset), tileWidth);
+ GridTile cache = lastTile;
+ if (cache == null || cache.tileX != tileX || cache.tileY != tileY)
{
+ final GridTile key = new GridTile(tileX, tileY, numXTiles);
+ cache = tiles.putIfAbsent(key, key);
+ if (cache == null) cache = key;
+ lastTile = cache;
}
- Raster tile = row[tx];
+ Raster tile = cache.tile;
if (tile == null) {
- // TODO: load in background and return null for meaning "not
yet available".
- tile = getImage().getTile(Math.addExact(tx, minTileX),
Math.addExact(tileRow, minTileY));
- row[tx] = tile;
+ cache.load(this);
+ return null;
}
final int x = Math.addExact(column, minX);
final int b = getBand();
diff --git
a/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BoundedHashMap.java
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BoundedHashMap.java
new file mode 100644
index 0000000..e41aae6
--- /dev/null
+++
b/application/sis-javafx/src/main/java/org/apache/sis/internal/gui/BoundedHashMap.java
@@ -0,0 +1,58 @@
+/*
+ * 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.internal.gui;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+
+
+/**
+ * A map with a fixed capacity. When the maximal capacity is exceeded, eldest
entries are removed.
+ * This is a trivial implementation on top of {@link LinkedHashMap} used only
for very simple caching.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+@SuppressWarnings("serial")
+public final class BoundedHashMap<K,V> extends LinkedHashMap<K,V> {
+ /**
+ * The maximal capacity.
+ */
+ private final int capacity;
+
+ /**
+ * Creates a new map with the given maximal capacity.
+ *
+ * @param capacity the maximal capacity.
+ */
+ public BoundedHashMap(final int capacity) {
+ this.capacity = capacity;
+ }
+
+ /**
+ * Removes the given entry if this map has reached its maximal capacity.
+ *
+ * @param entry the eldest entry.
+ * @return whether to remove the entry.
+ */
+ @Override
+ protected boolean removeEldestEntry​(final Map.Entry<K,V> entry) {
+ return size() > capacity;
+ }
+}