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 4d1a8092ea9198c46ab80d3c91a9e6dfdb550517 Author: Martin Desruisseaux <[email protected]> AuthorDate: Sat Feb 14 23:48:14 2026 +0100 Initial implementation of the `TileMatrixSet` interface. Tests are still superficial for now. --- .../sis/coverage/grid/GridCoverageBuilder.java | 7 +- .../sis/image/internal/shared/ReshapedImage.java | 25 ++ .../apache/sis/storage/geotiff/GeoTiffStore.java | 2 +- .../sis/storage/geotiff/ImageFileDirectory.java | 5 +- .../org/apache/sis/storage/geotiff/Reader.java | 4 +- .../apache/sis/storage/geotiff/package-info.java | 2 +- .../org/apache/sis/storage/geotiff/ReaderTest.java | 125 ++++++ .../org/apache/sis/storage/geotiff/WriterTest.java | 1 + .../org/apache/sis/storage/geotiff/untiled.tiff | Bin 0 -> 2284 bytes .../sis/storage/netcdf/base/RasterResource.java | 2 + .../apache/sis/storage/GridCoverageResource.java | 10 +- .../apache/sis/storage/image/SingleImageStore.java | 2 +- .../org/apache/sis/storage/internal/Resources.java | 5 + .../sis/storage/internal/Resources.properties | 1 + .../sis/storage/internal/Resources_fr.properties | 1 + .../apache/sis/storage/tiling/ImagePyramid.java | 437 ++++++++++++++++++ .../apache/sis/storage/tiling/ImageTileMatrix.java | 492 +++++++++++++++++++++ .../apache/sis/storage/tiling/IterationDomain.java | 220 +++++++++ .../apache/sis/storage/tiling/TileMatrixSet.java | 12 +- .../sis/storage/tiling/TiledGridCoverage.java | 8 +- .../storage/tiling/TiledGridCoverageResource.java | 177 +++++++- .../apache/sis/storage/tiling/TiledResource.java | 4 +- .../main/org/apache/sis/io/TableAppender.java | 16 +- .../main/org/apache/sis/io/package-info.java | 2 +- .../main/org/apache/sis/math/DecimalFunctions.java | 27 +- .../sis/util/internal/shared/AbstractMap.java | 2 +- .../org/apache/sis/math/DecimalFunctionsTest.java | 32 +- .../org/apache/sis/storage/gdal/GDALStoreTest.java | 3 +- 28 files changed, 1578 insertions(+), 46 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java index b99bb4ce9d..be3de3d5bd 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverageBuilder.java @@ -76,9 +76,12 @@ import org.apache.sis.util.resources.Errors; * } * } * var builder = new GridCoverageBuilder(); - * builder.setValues(data).flixAxis(1); + * builder.setValues(data).flipGridAxis(1); * - * Envelope domain = ...; // Specify here the "real world" coordinates. + * // Real world coordinates, around Tokyo in this example. + * var domain = new GeneralEnvelope(CommonCRS.WGS84.normalizedGeographic()); + * domain.setRange(1, 32, 40); // Range of latitude values. + * domain.setRange(0, 137, 140); // Range of longitude values. * return builder.setDomain(domain).build(); * } * } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java index ee40136e8f..7db4d35660 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/internal/shared/ReshapedImage.java @@ -28,6 +28,7 @@ import static java.lang.Math.min; import static java.lang.Math.max; import static java.lang.Math.addExact; import static java.lang.Math.subtractExact; +import static java.lang.Math.multiplyFull; import static java.lang.Math.floorDiv; import static java.lang.Math.toIntExact; import org.apache.sis.image.PlanarImage; @@ -107,6 +108,30 @@ public final class ReshapedImage extends PlanarImage { this.minTileY = minTileY; } + /** + * Returns an image for a single tile. + * The indices of the unique tile are set to (0,0). + * The coordinate of the image upper-left corner is set to (0,0). + * + * @param source the image for which to wrap a single tile. + * @param tileX column index of the tile. + * @param tileY row index of the tile. + * @return the single tile. May be the given source or one of its sources. + * @throws ArithmeticException if image indices overflow 32-bits integer capacity. + */ + public static RenderedImage singleTile(final RenderedImage source, final int tileX, final int tileY) { + final int tileWidth = source.getTileWidth(); + final int tileHeight = source.getTileHeight(); + final var image = new ReshapedImage(source, + -(multiplyFull(tileX, tileWidth) + source.getTileGridXOffset()), // This negate cannot overflow. + -(multiplyFull(tileY, tileHeight) + source.getTileGridYOffset()), + 0, 0, + tileWidth, + tileHeight, + 0, 0); + return image.isIdentity() ? image.source : image; + } + /** * Returns an image with the data of the given image translated by the given amount. * The number of tiles and their indexes are unchanged. diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java index f806703e7d..dffd88becf 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java @@ -82,7 +82,7 @@ import org.apache.sis.util.resources.Errors; * @author Martin Desruisseaux (Geomatys) * @author Thi Phuong Hao Nguyen (VNSC) * @author Alexis Manin (Geomatys) - * @version 1.5 + * @version 1.7 * @since 0.8 */ public class GeoTiffStore extends DataStore implements Aggregate { diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java index 25570a7b9c..990a2d49be 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/ImageFileDirectory.java @@ -1540,7 +1540,10 @@ final class ImageFileDirectory extends DataCube { domain = new GridGeometry(new GridExtent(imageWidth, imageHeight), null, null); } final CoverageModifier.Source source = source(); - gridGeometry = (source != null) ? reader.store.customizer.customize(source, domain) : domain; + if (source != null) { + domain = reader.store.customizer.customize(source, domain); + } + gridGeometry = domain; } return domain; } diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java index 164cfb6c56..0eaeffe620 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Reader.java @@ -419,7 +419,7 @@ final class Reader extends IOBase { lastIFD = null; // Clear now in case of error. imageIndex++; // In case next image is full-resolution. ImageFileDirectory image; - final List<ImageFileDirectory> overviews = new ArrayList<>(); + final var overviews = new ArrayList<ImageFileDirectory>(); while ((image = readNextIFD(imageIndex)) != null) { if (image.isReducedResolution()) { overviews.add(image); @@ -430,7 +430,7 @@ final class Reader extends IOBase { } /* * All pyramid levels have been read. If there is only one level, - * use the image directly. Otherwise create the pyramid. + * use the image directly. Otherwise, create the pyramid. */ if (overviews.isEmpty()) { images.add(fullResolution); diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java index 603b212edc..76accbbda4 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/package-info.java @@ -33,7 +33,7 @@ * @author Thi Phuong Hao Nguyen (VNSC) * @author Minh Chinh Vu (VNSC) * @author Martin Desruisseaux (Geomatys) - * @version 1.6 + * @version 1.7 * @since 0.8 */ package org.apache.sis.storage.geotiff; diff --git a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java new file mode 100644 index 0000000000..360d928fa8 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/ReaderTest.java @@ -0,0 +1,125 @@ +/* + * 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.geotiff; + +import java.util.SortedMap; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import org.opengis.util.GenericName; +import org.opengis.referencing.crs.ProjectedCRS; +import org.apache.sis.storage.StorageConnector; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.GridCoverageResource; +import org.apache.sis.storage.NoSuchDataException; +import org.apache.sis.storage.tiling.Tile; +import org.apache.sis.storage.tiling.TileMatrix; +import org.apache.sis.storage.tiling.TileMatrixSet; +import org.apache.sis.storage.tiling.TileStatus; +import org.apache.sis.storage.tiling.TiledResource; + +// Test dependencies +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import static org.apache.sis.test.Assertions.assertSingleton; +import static org.apache.sis.test.Assertions.assertMessageContains; +import org.apache.sis.test.TestCase; + + +/** + * Tests a few read operations. + * + * @author Martin Desruisseaux (Geomatys) + * + * @todo We should rewrite {@code "untiled.tiff"} as a tiled image. + */ +@SuppressWarnings("exports") +public class ReaderTest extends TestCase { + /** + * Name of the test file. + */ + private static final String FILENAME = "untiled.tiff"; + + /** + * Creates a new test case. + */ + public ReaderTest() { + } + + /** + * Creates a new data store for the test file. + * + * @return the data store to test. + * @throws DataStoreException if an error occurred while creating the data store. + */ + private static GeoTiffStore createStore() throws DataStoreException { + return new GeoTiffStore(null, new StorageConnector(ReaderTest.class.getResource(FILENAME))); + } + + /** + * Returns the single raster of the rendered image of the given tile. + */ + private static Raster raster(final Tile tile) throws DataStoreException { + RenderedImage image = assertInstanceOf(GridCoverageResource.class, tile.getResource()).read(null, null).render(null); + assertEquals(1, image.getNumXTiles()); + assertEquals(1, image.getNumYTiles()); + return image.getTile(image.getMinTileX(), image.getMinTileY()); + } + + /** + * Tests the tile matrix set. + * + * @todo Need to be updated if we rewrite {@code "untiled.tiff"} as a more interesting image. + * + * @throws DataStoreException if an error occurred while creating the data store. + */ + @Test + public void testTileMatrixSet() throws DataStoreException { + try (GeoTiffStore ds = createStore()) { + final GridCoverageResource resource = assertInstanceOf(GridCoverageResource.class, assertSingleton(ds.components())); + assertInstanceOf(ProjectedCRS.class, resource.getGridGeometry().getCoordinateReferenceSystem()); + + final TileMatrixSet pyramid = assertSingleton(assertInstanceOf(TiledResource.class, resource).getTileMatrixSets()); + assertSame(resource.getGridGeometry().getCoordinateReferenceSystem(), pyramid.getCoordinateReferenceSystem()); + assertEquals("untiled:1:TMS", pyramid.getIdentifier().toString()); + assertFalse(pyramid.getEnvelope().isEmpty()); + + final SortedMap<GenericName, ? extends TileMatrix> matrices = pyramid.getTileMatrices(); + assertFalse(matrices.isEmpty()); + assertEquals(1, matrices.size()); + assertFalse(matrices.isEmpty()); // Test again because code path changed. + assertTrue(matrices.subMap(matrices.firstKey(), matrices.lastKey()).isEmpty()); + + final TileMatrix matrix = assertSingleton(matrices.values()); + assertEquals("untiled:1:TMS:L0", matrix.getIdentifier().toString()); + assertEquals(assertSingleton(matrices.keySet()), matrix.getIdentifier()); + assertArrayEquals(resource.getGridGeometry().getResolution(false), matrix.getResolution()); + assertSame(TileStatus.OUTSIDE_EXTENT, matrix.getTileStatus(1, 0)); + assertSame(TileStatus.OUTSIDE_EXTENT, matrix.getTileStatus(0, 1)); + assertSame(TileStatus.UNKNOWN, matrix.getTileStatus(0, 0)); // Because the tile has not yet been loaded. + + assertMessageContains(assertThrows(NoSuchDataException.class, () -> matrix.getTile(1, 0))); + final Tile tile = matrix.getTile(0, 0).orElseThrow(); + assertArrayEquals(new long[] {0, 0}, tile.getIndices()); + assertEquals(TileStatus.EXISTS, tile.getStatus()); + assertTrue(tile.getContentPath().isEmpty()); + + final Raster raster = raster(tile); + assertArrayEquals(tile.getIndices(), assertSingleton(matrix.getTiles(null, false).toList()).getIndices()); + assertSame(raster, raster(assertSingleton(matrix.getTiles(null, false).toList()))); + } + } +} diff --git a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java index edb7c694d0..71f318e57c 100644 --- a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java +++ b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/WriterTest.java @@ -61,6 +61,7 @@ import org.apache.sis.referencing.operation.HardCodedConversions; * @author Erwan Roussel (Geomatys) * @author Martin Desruisseaux (Geomatys) */ +@SuppressWarnings("exports") public final class WriterTest extends TestCase { /** * Arbitrary size (in pixels) of tiles in the image to test. The TIFF specification restricts those sizes diff --git a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff new file mode 100644 index 0000000000..96cf0ae509 Binary files /dev/null and b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/untiled.tiff differ diff --git a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java index 3e02b53b18..ee90326291 100644 --- a/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java +++ b/endorsed/src/org.apache.sis.storage.netcdf/main/org/apache/sis/storage/netcdf/base/RasterResource.java @@ -775,6 +775,8 @@ public final class RasterResource extends AbstractGridCoverageResource implement /** * Returns a string representation of this resource for debugging purposes. + * + * @return a string representation of this resource for debugging purposes. */ @Override public String toString() { diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/GridCoverageResource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/GridCoverageResource.java index 6c10e81d73..eec1a421cb 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/GridCoverageResource.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/GridCoverageResource.java @@ -131,13 +131,13 @@ public interface GridCoverageResource extends DataSet { /** * Returns the preferred resolutions (in units of CRS axes) for read operations in this data store. * If the storage supports pyramid, then the list should contain the resolution at each pyramid level - * ordered from finest (smallest numbers) to coarsest (largest numbers) resolution. - * Otherwise the list contains a single element which is the {@linkplain #getGridGeometry() grid geometry} + * ordered from finest (smallest numerical values) to coarsest (largest numerical values) resolution. + * Otherwise, the list contains a single element which is the {@linkplain #getGridGeometry() grid geometry} * resolution, or an empty list if no resolution is applicable to the coverage (e.g. because non-constant). * - * <p>Each element shall be an array with a length equals to the number of CRS dimensions. - * In each array, value at index <var>i</var> is the cell size along CRS dimension <var>i</var> - * in units of the CRS axis <var>i</var>.</p> + * <p>Each element shall be an array with a length equals to the number of <abbr>CRS</abbr> dimensions. + * In each array, value at index <var>i</var> is the cell size along <abbr>CRS</abbr> dimension <var>i</var> + * in units of the <abbr>CRS</abbr> axis <var>i</var>.</p> * * <p>Note that arguments given to {@link #subset(CoverageQuery) subset(…)} or {@link #read read(…)} methods * are <em>not</em> constrained to the resolutions returned by this method. Those resolutions are only hints diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/SingleImageStore.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/SingleImageStore.java index b27693f864..db400d1b36 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/SingleImageStore.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/image/SingleImageStore.java @@ -34,7 +34,7 @@ import org.apache.sis.util.collection.BackingStoreException; /** * A world file store which is expected to contain exactly one image. * This class is used for image formats that are restricted to one image per file. - * Examples: PNG and BMP image formats. + * Examples: <abbr>PNG</abbr> and <abbr>BMP</abbr> image formats. * * <p>See {@link WritableSingleImageStore} for the writable variant of this class.</p> * diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java index 15b79f539f..4f451dffab 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java @@ -453,6 +453,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short SubsetQuery_1 = 77; + /** + * Tile indexes out of bounds. + */ + public static final short TileIndexesOutOfBounds = 87; + /** * Cannot open {0} data store without “{1}” parameter. */ diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties index 9c688036fb..8d78743ee0 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties @@ -98,6 +98,7 @@ StreamIsNotWritable_1 = Stream \u201c{0}\u201d is not writable. StreamIsReadOnce_1 = The \u201c{0}\u201d data store can be read only once. StreamIsWriteOnce_1 = Cannot modify previously written data in \u201c{0}\u201d. SubsetQuery_1 = Query a subset of \u201c{0}\u201d. +TileIndexesOutOfBounds = Tile indexes out of bounds. UndefinedParameter_2 = Cannot open {0} data store without \u201c{1}\u201d parameter. UnexpectedNumberOfCoordinates_4 = The \u201c{0}\u201d feature at {1} has a {3} coordinate values, while we expected a multiple of {2}. UnexpectedSliceResolution_3 = Expected a resolution of {0} but got {1} for the slice or tile \u201c{2}\u201d. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties index 8d63ff1001..187097a201 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties @@ -103,6 +103,7 @@ StreamIsNotWritable_1 = Le flux de donn\u00e9es \u00ab\u202f{0}\u202 StreamIsReadOnce_1 = Les donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb ne peuvent \u00eatre lues qu\u2019une seule fois. StreamIsWriteOnce_1 = Ne peut pas revenir sur les donn\u00e9es d\u00e9j\u00e0 \u00e9crites dans \u00ab\u202f{0}\u202f\u00bb. SubsetQuery_1 = Requ\u00eate d\u2019un sous-ensemble de \u00ab\u202f{0}\u202f\u00bb. +TileIndexesOutOfBounds = Les indexes de la tuile sont en dehors des limites permises. UndefinedParameter_2 = Ne peut pas ouvrir une source de donn\u00e9es {0} sans le param\u00e8tre \u00ab\u202f{1}\u202f\u00bb. UnexpectedNumberOfCoordinates_4 = L\u2019entit\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb \u00e0 {1} contient {3} coordonn\u00e9es, alors qu\u2019on attendait un multiple de {2}. UnexpectedSliceResolution_3 = Une r\u00e9solution de {0} \u00e9tait attendue mais une r\u00e9solution de {1} a \u00e9t\u00e9 trouv\u00e9e dans la tranche ou tuile \u00ab\u202f{2}\u202f\u00bb. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java new file mode 100644 index 0000000000..cd1ca756c2 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImagePyramid.java @@ -0,0 +1,437 @@ +/* + * 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.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.SortedMap; +import org.opengis.util.LocalName; +import org.opengis.util.GenericName; +import org.opengis.geometry.Envelope; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.opengis.referencing.operation.TransformException; +import org.apache.sis.coverage.grid.GridCoverageProcessor; +import org.apache.sis.coverage.grid.IncompleteGridGeometryException; +import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.geometry.ImmutableEnvelope; +import org.apache.sis.measure.NumberRange; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.base.StoreUtilities; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.util.iso.Names; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.internal.shared.AbstractMap; +import org.apache.sis.util.internal.shared.Strings; + + +/** + * Default implementation of {@code TileMatrixSet} as a wrapper for {@code GridCoverage} instances. + * This is a pyramid of two-dimensional slices represented as {@link java.awt.image.RenderedImage}s. + * + * @author Martin Desruisseaux (Geomatys) + */ +final class ImagePyramid extends AbstractMap<GenericName, ImageTileMatrix> + implements TileMatrixSet, SortedMap<GenericName, ImageTileMatrix> +{ + /** + * An alphanumeric identifier unique in the {@code TiledResource} that contains this {@code TileMatrixSet}. + * A tiled resource may contain more than one tile matrix set if the resource provides different set of tiles + * for different <abbr>CRS</abbr>. + * + * @see #getIdentifier() + */ + private final GenericName identifier; + + /** + * The provider of pyramid levels. + */ + private final TiledGridCoverageResource.Pyramid provider; + + /** + * The tile matrices at each level, ordered from coarser resolution to most detailed resolution. + * This list is initially empty. Pyramid levels are created when first requested. + */ + private final List<ImageTileMatrix> matrices; + + /** + * First valid index value (inclusive) in {@link #matrices}. + */ + private final int lowerMatrixIndex; + + /** + * First index value which is known to be invalid. + * This is the lowest value for which {@link #getOrCreateLevel(int)} returned {@code null}. + * This value may be higher than the true number of levels if we didn't tested all values. + */ + private int upperMatrixIndex; + + /** + * The union of the envelopes of all tile matrix sets, or {@code null} if not yet computed. + * + * @see #getEnvelope() + */ + private Envelope envelope; + + /** + * The grid coverage processor to use when tiles use a subset of the bands. + * + * @see #createResourceView(long[], RenderedImage) + */ + private final GridCoverageProcessor processor; + + /** + * Creates a new tile matrix set. + * + * @param parent identifier of the {@code TiledResource} that contains this {@code TileMatrixSet}. + * @param provider information about the tile matrices to create, and provider of pyramid levels. + * @param processor the grid coverage processor to use when tiles use a subset of the bands. + */ + @SuppressWarnings("LocalVariableHidesMemberVariable") + ImagePyramid(final GenericName parent, + final TiledGridCoverageResource.Pyramid provider, + final GridCoverageProcessor processor) + { + this.provider = provider; + this.processor = processor; + identifier = Names.createScopedName(parent, null, provider.identifier()); + matrices = new ArrayList<>(); + lowerMatrixIndex = 0; + upperMatrixIndex = Integer.MAX_VALUE; + } + + /** + * Creates a new tile matrix set as a subset of the given one. + */ + private ImagePyramid(final ImagePyramid parent, final int lowerMatrixIndex, final int upperMatrixIndex) { + this.identifier = parent.identifier; + this.provider = parent.provider; + this.processor = parent.processor; + this.matrices = parent.matrices; + this.lowerMatrixIndex = lowerMatrixIndex; + this.upperMatrixIndex = upperMatrixIndex; + } + + /** + * Returns the tile matrix at the given index. + * If that tile matrix was not already created, it is created now. + * This method shall be invoked with a synchronization lock on {@link #matrices}. + * + * @param level the level for which to get the tile matrix. + * @return tile matrix at the given level, or {@code null} if the given level is too high. + * @throws BackingStoreException if an error occurred while fetching information + */ + private ImageTileMatrix getOrCreateLevel(final int level) { + if (level < 0 || level >= upperMatrixIndex) { + return null; + } + if (level < matrices.size()) { + final ImageTileMatrix tm = matrices.get(level); + if (tm != null) { + return tm; + } + } + final ImageTileMatrix tm; + try { + final TiledGridCoverageResource resource = provider.forPyramidLevel(level); + if (resource == null) { + upperMatrixIndex = level; + return null; + } + GenericName id = Names.createScopedName(identifier, null, provider.identifierOfLevel(level)); + tm = new ImageTileMatrix(id, resource, processor); + } catch (DataStoreException | TransformException e) { + throw new BackingStoreException(e); + } + while (matrices.size() <= level) { + matrices.add(null); + } + matrices.set(level, tm); + return tm; + } + + /** + * Returns the index of the pyramid name for the given identifier, or -1 if none. + * This method shall be invoked with a synchronization lock on {@link #matrices}. + * The returned value is an index in the {@link #matrices} list. + * + * @param name identifier of the desired level. + * @param required whether to thrown an exception if the identifier is not recognized. + * @return index of the desired level, or -1 if none and {@code required} is {@code false}. + * @throws IllegalArgumentException if the given name is not recognized and {@code required} is {@code true}. + */ + private int indexOf(final GenericName name, final boolean required) { + final LocalName tip = name.tip(); + if (tip.scope().name().equals(identifier)) { + final int level; + try { + level = provider.levelOfIdentifier(tip.toString()); + } catch (IllegalArgumentException e) { + if (required) throw e; + Logging.ignorableException(StoreUtilities.LOGGER, ImagePyramid.class, "indexOf", e); + return -1; + } + if (level >= lowerMatrixIndex && level < upperMatrixIndex) { + if (!required || getOrCreateLevel(level) != null) { + return level; + } + } + } + if (required) { + throw new IllegalArgumentException(Errors.format(Errors.Keys.NoSuchValue_1, name)); + } + return -1; + } + + /** + * Compares two generic names for order based on the resolution of the associated tile matrix. + * If a call to {@code compare(o1, o2)}, the comparator returns a positive number of {@code o1} + * is the identifier of a tile matrix having a finer resolution than {@code o2}. + */ + @Override + public Comparator<GenericName> comparator() { + return (GenericName o1, GenericName o2) -> indexOf(o1, false) - indexOf(o2, false); + } + + /** + * Returns whether this Tile Matrix Set has no elements. + * Empty tile matrix sets should not be returned to users. + */ + @Override + public boolean isEmpty() { + synchronized (matrices) { + if (upperMatrixIndex <= lowerMatrixIndex) { + return true; + } + if (!matrices.isEmpty()) { + return false; + } + return getOrCreateLevel(lowerMatrixIndex) == null; + } + } + + /** + * Returns the number of elements in this tile matrix set. + * + * <b>Note:</b> if this implementation is modified, revisit {@link #lastKey()}. + */ + @Override + public int size() { + synchronized (matrices) { + int level; + while ((level = matrices.size()) < upperMatrixIndex) { + getOrCreateLevel(level); // Force fetching. + } + return upperMatrixIndex - lowerMatrixIndex; + } + } + + /** + * Returns a unique (within {@link TiledResource}) identifier. + */ + @Override + public GenericName getIdentifier() { + return identifier; + } + + /** + * Returns the coordinate reference system of all {@code TileMatrix} instances in this set. + * + * @throws IncompleteGridGeometryException if the tile matrices have no <abbr>CRS</abbr>. + */ + @Override + public CoordinateReferenceSystem getCoordinateReferenceSystem() { + final ImageTileMatrix tm; + synchronized (matrices) { + tm = getOrCreateLevel(0); // Really 0, not `lowerMatrixIndex`. + } + return tm.getTilingScheme().getCoordinateReferenceSystem(); + } + + /** + * Returns an envelope that encompasses all {@code TileMatrix} instances in this set. + * + * @throws IncompleteGridGeometryException if a tiling scheme has no envelope. While not strictly mandatory, + * for now we consider that missing extent or missing "grid to CRS" transform is probably an error. + */ + @Override + public Optional<Envelope> getEnvelope() { + synchronized (matrices) { + if (envelope == null) { + int i = lowerMatrixIndex; + Envelope mayReuse = getOrCreateLevel(i).getTilingScheme().getEnvelope(); + final var union = new GeneralEnvelope(mayReuse); + ImageTileMatrix tm; + while ((tm = getOrCreateLevel(++i)) != null) { + final Envelope e = tm.getTilingScheme().getEnvelope(); + union.add(e); + if (union.equals(e, 0, false)) { + mayReuse = e; + } + } + envelope = (union == null || union.equals(mayReuse, 0, false)) ? mayReuse : new ImmutableEnvelope(union); + } + return Optional.of(envelope); + } + } + + /** + * Returns all {@code TileMatrix} instances in this set, together with their identifiers. + * Entries are sorted from coarser resolution to most detailed resolution. + */ + @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") + public SortedMap<GenericName, ? extends TileMatrix> getTileMatrices() { + return this; + } + + /** + * Returns an iterator over the entries in this map. + */ + @Override + protected EntryIterator<GenericName, ImageTileMatrix> entryIterator() { + return new EntryIterator<GenericName, ImageTileMatrix>() { + /** Index of the next element to return. */ + private int index; + + /** Next element to return. */ + private ImageTileMatrix next; + + @Override protected GenericName getKey() {return next.getIdentifier();} + @Override protected ImageTileMatrix getValue() {return next;} + @Override protected boolean next() { + synchronized (matrices) { + return (next = getOrCreateLevel(index++)) != null; + } + } + }; + } + + /** + * Returns the tile matrix for the given identifier. + * + * @param key tile matrix identifier. + * @return tile matrix for the given identifier, or {@code null} if none. + */ + @Override + public ImageTileMatrix get(final Object key) { + if (key instanceof GenericName) { + synchronized (matrices) { + return getOrCreateLevel(indexOf((GenericName) key, false)); + } + } + return null; + } + + /** + * Returns the first key of the map. + */ + @Override + public GenericName firstKey() { + final ImageTileMatrix tm; + synchronized (matrices) { + tm = getOrCreateLevel(lowerMatrixIndex); + } + if (tm != null) { + return tm.getIdentifier(); + } + throw new NoSuchElementException(); + } + + /** + * Returns the last key of the map. + */ + @Override + public GenericName lastKey() { + final ImageTileMatrix tm; + synchronized (matrices) { + final int size = size(); // Implementation of `size()` fills the list. + tm = (size != 0) ? matrices.get(size - 1) : null; + } + if (tm != null) { + return tm.getIdentifier(); + } + throw new NoSuchElementException(); + } + + /** + * Returns a view of this map whose keys are strictly less than {@code toKey}. + * + * @param toKey high endpoint (exclusive) of the keys in the returned map. + */ + @Override + public SortedMap<GenericName, ImageTileMatrix> headMap(GenericName toKey) { + return subMap(lowerMatrixIndex, indexOf(toKey, true)); + } + + /** + * Returns a view of this map whose keys are greater than or equal to {@code fromKey}. + * + * @param fromKey low endpoint (inclusive) of the keys in the returned map. + */ + @Override + public SortedMap<GenericName, ImageTileMatrix> tailMap(GenericName fromKey) { + return subMap(indexOf(fromKey, true), upperMatrixIndex); + } + + /** + * Returns a view this map whose keys range from {@code fromKey}, inclusive, to {@code toKey}, exclusive. + * + * @param fromKey low endpoint (inclusive) of the keys in the returned map. + * @param toKey high endpoint (exclusive) of the keys in the returned map. + */ + @Override + public SortedMap<GenericName, ImageTileMatrix> subMap(GenericName fromKey, GenericName toKey) { + return subMap(indexOf(fromKey, true), indexOf(toKey, true)); + } + + /** + * Returns a view this map whose keys range from {@code fromKey}, inclusive, to {@code toKey}, exclusive. + * + * @param fromKey low endpoint (inclusive) of the keys in the returned map. + * @param toKey high endpoint (exclusive) of the keys in the returned map. + */ + @SuppressWarnings("ReturnOfCollectionOrArrayField") + private SortedMap<GenericName, ImageTileMatrix> subMap(final int fromKey, final int toKey) { + if (fromKey >= toKey) { + return Collections.emptySortedMap(); + } + if (fromKey == lowerMatrixIndex && toKey == upperMatrixIndex) { + return this; + } + return new ImagePyramid(this, fromKey, toKey); + } + + /** + * Returns a string representation for debugging purposes. + * + * @return a string representation for debugging purposes. + */ + @Override + public String toString() { + final Object upper; + synchronized (matrices) { + final Integer max = (upperMatrixIndex == Integer.MAX_VALUE) ? null : upperMatrixIndex; + upper = (matrices.size() >= upperMatrixIndex) ? max : new NumberRange<>(Integer.class, lowerMatrixIndex, true, max, true); + } + return Strings.toString(getClass(), null, identifier.toString(), "lowerMatrixIndex", lowerMatrixIndex, "upperMatrixIndex", upper); + } +} diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java new file mode 100644 index 0000000000..51feda835d --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/ImageTileMatrix.java @@ -0,0 +1,492 @@ +/* + * 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.util.Arrays; +import java.util.Optional; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import java.awt.image.RenderedImage; +import java.nio.file.Path; +import org.opengis.util.GenericName; +import org.opengis.referencing.operation.TransformException; +import org.apache.sis.referencing.operation.matrix.Matrices; +import org.apache.sis.referencing.operation.matrix.MatrixSIS; +import org.apache.sis.referencing.operation.transform.MathTransforms; +import org.apache.sis.storage.MemoryGridCoverageResource; +import org.apache.sis.storage.DataStoreException; +import org.apache.sis.storage.NoSuchDataException; +import org.apache.sis.storage.UnsupportedQueryException; +import org.apache.sis.storage.InternalDataStoreException; +import org.apache.sis.storage.Resource; +import org.apache.sis.storage.base.StoreUtilities; +import org.apache.sis.coverage.grid.GridExtent; +import org.apache.sis.coverage.grid.GridGeometry; +import org.apache.sis.coverage.grid.GridCoverage2D; +import org.apache.sis.coverage.grid.GridCoverageProcessor; +import org.apache.sis.image.internal.shared.ReshapedImage; +import org.apache.sis.pending.jdk.JDK18; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.iso.Names; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.resources.Errors; +import org.apache.sis.util.collection.BackingStoreException; +import org.apache.sis.math.DecimalFunctions; +import org.apache.sis.storage.internal.Resources; + + +/** + * Default implementation of {@code TileMatrix} as a wrapper for a {@code GridCoverage}. + * The tile size must be specified at construction time and must be equal to the size of + * the tiles of the rendered image. + * + * <p>This class is needed only when the application needs details about the tiling scheme, + * for example in order to implement a Web Map Tile Service (<abbr>WMTS</abbr>).</p> + * + * @author Martin Desruisseaux (Geomatys) + */ +final class ImageTileMatrix implements TileMatrix { + /** + * An alphanumeric identifier which is unique in the {@code TileMatrixSet} that contains this {@code TileMatrix}. + * The identifier contains the zoom level as a number encoded in <abbr>ASCII</abbr>. + */ + private final GenericName identifier; + + /** + * The resource for reading the tiles of this tile matrix. + */ + private final TiledGridCoverageResource resource; + + /** + * The coverage from which to get the rendered image which contains the tiles. + * This coverage uses deferred reading of tiles. Created when first requested. + * + * @see #coverage() + */ + private TiledGridCoverage coverage; + + /** + * Pattern for formatting tile indices using {@link java.util.Formatter}. + * The number of digits and the presence of a sign depend on the {@linkplain #tilingScheme tiling scheme}. + * + * @see #getTileIdentifier(long[]) + */ + private final String tileIndicesPattern; + + /** + * Extent of valid tile indices and their relationship with "real world" coordinates. + * The (0, 0) tile indices should map to the tile in the upper-left corner. The last + * row and last column of tiles may contain partial tiles if the coverage size is not + * a divisor of the tile size. + */ + private final GridGeometry tilingScheme; + + /** + * Size of tiles, in number of {@linkplain #coverage} cells. + */ + private final int[] tileSize; + + /** + * Values to add to tile coordinates, after multiplication by {@link #tileSize}, for getting cell coordinates. + * + * @see #tileToCell(long, int) + */ + private final long[] tileToCell; + + /** + * Values to add to tile coordinates in {@link #image} for getting a tile coordinates in {@link #tilingScheme}. + * Those values are updated when a new image is rendered. + */ + private long imageToTileX, imageToTileY; + + /** + * The image containing the tiles of the tile matrix. Computed when first needed, and may be + * recomputed multiple times with different offsets if the tile indices are larger than the + * capacity of 32-bits integers. + */ + private RenderedImage image; + + /** + * The grid coverage processor to use when tiles use a subset of the bands. + * + * @see #createResourceView(long[], RenderedImage) + */ + private final GridCoverageProcessor processor; + + /** + * Creates a new tile matrix for the given coverage. + * + * @param identifier identifier unique in the {@code TileMatrixSet} that contains this {@code TileMatrix}. + * @param resource the resource for reading the tiles of this tile matrix. + * @param processor the grid coverage processor to use when tiles use a subset of the bands. + * @throws TransformException if the "tile indices to CRS" transform cannot be computed. + */ + ImageTileMatrix(final GenericName identifier, + final TiledGridCoverageResource resource, + final GridCoverageProcessor processor) + throws DataStoreException, TransformException + { + this.identifier = identifier; + this.processor = processor; + this.resource = resource; + this.tileSize = resource.getTileSize(); + final GridGeometry cellGrid = resource.getGridGeometry(); + final GridExtent extent = cellGrid.getExtent(); + final int dimension = extent.getDimension(); + final long[] tileCount = new long[dimension]; + final MatrixSIS toCells = Matrices.createIdentity(dimension + 1); + this.tileToCell = new long[dimension]; + final var pattern = new StringBuilder(6 * dimension); + for (int i=0; i<dimension; i++) { + final long offset = extent.getLow(i); + final int scale = tileSize[i]; + toCells.setNumber(i, i, scale); + toCells.setNumber(i, dimension, offset); + tileToCell[i] = offset; + tileCount [i] = JDK18.ceilDiv(extent.getSize(i), scale); + /* + * Prepare a pattern for formatting the tile indices. + * Indices are formatted with fixed number of digits, + * using the minimum number needed for the largest index. + */ + if (i != 0) pattern.append(','); + pattern.append("%0").append(DecimalFunctions.floorLog10(Math.max(tileCount[i] - 1, 1)) + 1).append('d'); + } + tilingScheme = new GridGeometry(cellGrid, extent.reshape(null, tileCount, false), MathTransforms.linear(toCells)); + tileIndicesPattern = pattern.toString(); + } + + /** + * Returns an identifier which is unique in the {@code TileMatrixSet} that contains this {@code TileMatrix}. + * The identifier contains the zoom level as a number encoded in <abbr>ASCII</abbr>. + */ + @Override + public GenericName getIdentifier() { + return identifier; + } + + /** + * Returns the resolution (in units of CRS axes) at which tiles in this matrix should be used. + * The array length is the number of <abbr>CRS</abbr> dimensions, and value at index <var>i</var> + * is the resolution along CRS dimension <var>i</var> in units of the CRS axis <var>i</var>. + */ + @Override + public double[] getResolution() { + try { + return resource.getGridGeometry().getResolution(false); + } catch (DataStoreException e) { + throw new BackingStoreException(e); + } + } + + /** + * Returns a description about how space is partitioned into individual tiled units. + * The description contains the extent of valid tile indices, the spatial reference system, + * and the conversion from tile indices to the spatial reference system coordinates. + * + * @return extent of valid tile indices and their relationship with "real world" coordinates. + */ + @Override + public GridGeometry getTilingScheme() { + return tilingScheme; + } + + /** + * Returns the coverage, which is read when first needed. + * This coverage uses deferred reading of tiles. + * + * @return the coverage from which to get the rendered image which contains the tiles. + * @throws DataStoreException if an error occurred during the construction of the coverage. + */ + private synchronized TiledGridCoverage coverage() throws DataStoreException { + if (coverage == null) { + coverage = resource.readAtGetTileTime(); + } + return coverage; + } + + /** + * Fetches information about whether a tile exists, is missing or failed to load. + * + * @param indices indices of the requested tile (may be outside the tile matrix extent). + * @return information about the availability of the specified tile. + * @throws DataStoreException if fetching the tile status failed. + */ + @Override + @SuppressWarnings("LocalVariableHidesMemberVariable") + public TileStatus getTileStatus(final long... indices) throws DataStoreException { + if (tilingScheme.getExtent().contains(indices)) try { + final TiledGridCoverage coverage; + final RenderedImage image; + final long imageToTileX; + final long imageToTileY; + synchronized (this) { + coverage = this.coverage; // Never null if `image` is non-null. + image = this.image; + imageToTileX = this.imageToTileX; + imageToTileY = this.imageToTileY; + } + if (image != null) { + final long tileX = Math.subtractExact(indices[coverage.xDimension], imageToTileX); + final long x0 = image.getMinTileX(); + if (tileX >= x0 && tileX < x0 + image.getNumXTiles()) { + final long tileY = Math.subtractExact(indices[coverage.yDimension], imageToTileY); + final long y0 = image.getMinTileY(); + if (tileY >= y0 && tileY < y0 + image.getNumYTiles()) { + return getTileStatus(image, Math.toIntExact(tileX), Math.toIntExact(tileY)); + } + } + } + return TileStatus.UNKNOWN; + } catch (ArithmeticException e) { + Logging.ignorableException(StoreUtilities.LOGGER, ImageTileMatrix.class, "getTileStatus", e); + } + return TileStatus.OUTSIDE_EXTENT; + } + + /** + * Returns the status of the tile at the given index. + * + * @param image image from which to get a tile status. + * @param tileX row index of the tile for which to get the status. + * @param tileY column index of the tile for which to get the status. + * @return status of the tile at the specified indexes. + * + * @todo We should check if the image is an instance of {@code ComputedImage}, + * then check if the tile is in the cache. But it would require that we merge + * the feature and storage modules if we want to reuse {@link TileStatus} enumeration. + */ + private static TileStatus getTileStatus(final RenderedImage image, final int tileX, final int tileY) { + return TileStatus.EXISTS; + } + + /** + * Gets a tile at the given indices if not missing. + * + * @param indices indices of the tile to fetch, as coordinates inside the matrix extent. + * @return the tile if it exists, or an empty value if the tile is missing. + * @throws NoSuchDataException if the given indices are outside the matrix extent. + * @throws DataStoreException if fetching the tile failed for another reason. + */ + @Override + public Optional<Tile> getTile(final long... indices) throws DataStoreException { + final GridExtent extent = tilingScheme.getExtent(); + if (extent.contains(indices)) try { + final Tile tile = iterator(extent.reshape(indices, indices, true)).createFirstTile(); + return (tile.getStatus() == TileStatus.MISSING) ? Optional.empty() : Optional.of(tile); + } catch (ArithmeticException e) { + throw new UnsupportedQueryException(e); + } + throw new NoSuchDataException(Resources.format(Resources.Keys.TileIndexesOutOfBounds)); + } + + /** + * Retrieves a stream of existing tiles in the specified region. + * The stream contains the existing tiles that are inside the given region and excludes missing tiles. + * If a tile is {@linkplain TileStatus#IN_ERROR in error}, then the stream nevertheless return a tile + * but its {@link Tile#getResource()} method should throw the exception. + * + * <h4>Limitations</h4> + * The current implementation limits the size of the given extent to {@link Integer#MAX_VALUE} + * in each dimension. Note that this is a maximum size in tile indices, not in pixel coordinates. + * + * @param indiceRanges ranges of tile indices in all dimensions, or {@code null} for all tiles. + * @param parallel {@code true} for a parallel stream (if supported), or {@code false} for a sequential stream. + * @return stream of tiles, excluding missing tiles. + * @throws DataStoreException if the tiles can not be fetched in the given ranges of tile indexes. + */ + @Override + public Stream<Tile> getTiles(GridExtent indiceRanges, final boolean parallel) throws DataStoreException { + ArgumentChecks.ensureDimensionMatches("indiceRanges", tilingScheme.getDimension(), indiceRanges); + if (indiceRanges == null) { + indiceRanges = tilingScheme.getExtent(); + } + try { + return StreamSupport.stream(iterator(indiceRanges).iterator(), parallel); + } catch (ArithmeticException e) { + throw new UnsupportedQueryException(e); + } + } + + /** + * Creates an object which can be used for retrieving a single tile or a stream tiles. + * + * @param indiceRanges ranges of tile indices in all dimensions, or {@code null} for all tiles. + * @return a request which can be used for getting a tile or a stream of tiles in the given region. + * @throws DataStoreException if the tiles can not be fetched in the given ranges of tile indexes. + * @throws ArithmeticException if coordinate computation exceeds the capacity of 64-bits integers. + */ + private synchronized IterationDomain<Tile> iterator(final GridExtent indiceRanges) throws DataStoreException { + @SuppressWarnings("LocalVariableHidesMemberVariable") + final TiledGridCoverage coverage = coverage(); + boolean retry = false; + do { // This loop will be executed only 1 or 2 times. + if (image != null) { + final long xmin, ymin, xmax, ymax; + xmin = Math.subtractExact(indiceRanges.getLow (coverage.xDimension), imageToTileX); + xmax = Math.subtractExact(indiceRanges.getHigh(coverage.xDimension), imageToTileX); + final long x0 = image.getMinTileX(); + if (xmin >= x0 && xmax < x0 + image.getNumXTiles()) { + ymin = Math.subtractExact(indiceRanges.getLow (coverage.yDimension), imageToTileY); + ymax = Math.subtractExact(indiceRanges.getHigh(coverage.yDimension), imageToTileY); + final long y0 = image.getMinTileY(); + if (ymin >= y0 && ymax < y0 + image.getNumYTiles()) { + return new Iterator(Math.toIntExact(xmin), + Math.toIntExact(ymin), + Math.toIntExact(xmax), + Math.toIntExact(ymax)); + } + } + } + /* + * Gets the bounds of the image to read. If deferred reading is supported, + * we can expand to the bounds of the whole coverage in order to perform a + * read operation (deferred) only once. + */ + final GridExtent extent = coverage.getGridGeometry().getExtent(); + final int dimension = extent.getDimension(); + final var low = new long[dimension]; + final var high = new long[dimension]; + for (int i=0; i<dimension; i++) { + final long limit = Math.incrementExact(extent.getHigh(i)); + high[i] = Math.min(limit, tileToCell(Math.incrementExact(indiceRanges.getHigh(i)), i)); + low [i] = Math.max(extent.getLow(i), tileToCell(indiceRanges.getLow(i), i)); + final long span = high[i] - low[i]; + if (span < 0 || span > Integer.MAX_VALUE) { + throw new ArithmeticException(Errors.format(Errors.Keys.IntegerOverflow_1, Integer.SIZE)); + } + if (coverage.deferredTileReading) { + final long remain = Math.min(extent.getSize(i), Integer.MAX_VALUE) - span; + final long after = Math.min(remain >> 1, limit - high[i]); + final long before = Math.min(remain - after, low[i] - extent.getLow(i)); + low [i] -= before; + high[i] += after; + } + } + image = coverage.render(extent.reshape(low, high, false)); + imageToTileX = low[coverage.xDimension]; + imageToTileY = low[coverage.yDimension]; + } while ((retry = !retry) == true); + throw new InternalDataStoreException(); // Should never happen. + } + + /** + * Converts the give tile coordinate in the given dimension to cell coordinates. + * + * @param coordinate the tile coordinate to convert. + * @param dimension the dimension of the coordinate to convert. + * @return the cell coordinate. + * @throws ArithmeticException if the result overflows the capacity of 64-bits integers. + */ + private long tileToCell(final long coordinate, final int dimension) { + return Math.addExact(tileToCell[dimension], Math.multiplyExact(coordinate, tileSize[dimension])); + } + + /** + * Factory for an iterator over tiles in ranges of user-specified tile indices. + */ + private final class Iterator extends IterationDomain<Tile> { + /** + * Snapshot of {@link ImageTileMatrix#image}. + */ + private final RenderedImage tiles; + + /** + * Snapshot of {@link ImageTileMatrix#imageToTileX} and {@link ImageTileMatrix#imageToTileY}. + */ + private final long offsetX, offsetY; + + /** + * Creates a new request for tile iterators. + * + * @param xmin first column index of tiles, inclusive. + * @param xmin first row index of tiles, inclusive. + * @param xmax last column index of tiles, inclusive. + * @param ymax last row index of tiles, inclusive. + */ + Iterator(final int xmin, final int ymin, final int xmax, final int ymax) { + super(xmin, ymin, xmax, ymax); + tiles = image; + offsetX = imageToTileX; + offsetY = imageToTileY; + } + + /** + * Creates the tile at the given indexes. + */ + @Override + protected Tile createTile(final int tileX, final int tileY) { + return new Tile() { + /** This tile viewed as a resource, created when first requested. */ + private Resource resourceView; + + /** Returns the path to content of the tile if known. */ + @Override public Optional<Path> getContentPath() throws DataStoreException { + return Optional.ofNullable(coverage().getContentPath(getIndices())); + } + + /** Returns the indices of this tile in the {@code TileMatrix}. */ + @Override public long[] getIndices() { + return new long[] {offsetX + tileX, offsetY + tileY}; + } + + /** Returns information about whether the tile failed to load. */ + @Override public TileStatus getStatus() { + return getTileStatus(tiles, tileX, tileY); + } + + /** Returns the tile content as a resource. */ + @Override public synchronized Resource getResource() throws DataStoreException { + if (resourceView == null) { + resourceView = createResourceView(getIndices(), ReshapedImage.singleTile(tiles, tileX, tileY)); + } + return resourceView; + } + }; + } + } + + /** + * Creates a resource for the tile at the given indices. + * The resource wraps a grid coverage, which is itself wrapping the given image. + * The given image should contains only the desired tile. The caller currently sets + * the tile indexes and image coordinates to (0,0), but this is not mandatory. + * + * @param indices indices of the tile, as coordinates inside the matrix extent. + * @param tile a rendered image which contains only the tile. + * @return resource for the specified tile. + */ + private Resource createResourceView(final long[] indices, final RenderedImage tile) throws DataStoreException { + final Object[] args = new Object[indices.length]; + Arrays.setAll(args, (i) -> indices[i]); + final GenericName id = Names.createScopedName(identifier, null, String.format(tileIndicesPattern, args)); + final long[] low = new long[indices.length]; + final long[] high = new long[indices.length]; + for (int i=0; i<indices.length; i++) { + final long size = tileSize[i]; + low [i] = Math.addExact(tileToCell[i], Math.multiplyExact(indices[i], size)); + high[i] = Math.addExact(low[i], size - 1); + } + @SuppressWarnings("LocalVariableHidesMemberVariable") + final TiledGridCoverage coverage = coverage(); + GridGeometry cellGrid = coverage.getGridGeometry(); + final GridExtent extent = cellGrid.getExtent().reshape(low, high, true); + cellGrid = cellGrid.derive().subgrid(extent, null).build(); + final var subset = new GridCoverage2D(cellGrid, coverage.getSampleDimensions(), tile); + return new MemoryGridCoverageResource(resource, id, subset, processor); + } +} diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/IterationDomain.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/IterationDomain.java new file mode 100644 index 0000000000..c4a0aa39e4 --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/IterationDomain.java @@ -0,0 +1,220 @@ +/* + * 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.util.Spliterator; +import java.util.function.Consumer; +import java.awt.image.RenderedImage; + + +/** + * A request for tiles from a rendered image. + * Contrarily to {@link TiledGridCoverage.AOI}, this request works with arbitrary {@link RenderedImage} and does + * not manage a tile cache. Tile caching is assumed to be managed by the {@code RenderedImage} implementation. + * This class is designed for use with {@link Spliterator} with parallelism. + * + * <h2>Iteration order</h2> + * Iteration order is unspecified. Current implementation iterates from left to right, then top to bottom. + * But future implementations may support Hilbert order. If the iteration is split, the iteration order is + * interleaved: for example with two threads, the first tile is given to the first thread, the second tile + * is given to the second thread, the third tile is given to the first thread, <i>etc.</i> The intend is to + * stay relatively close to a reading of tiles in sequential order when all threads are executed in parallel. + * + * @author Martin Desruisseaux (Geomatys) + * + * @param <T> type of tiles on which the {@link Spliterator} will iterate. + */ +abstract class IterationDomain<T> { + /** + * Minimal column index of the region on which to iterate. + */ + private final int xmin; + + /** + * Minimal row index of the region on which to iterate. + */ + private final int ymin; + + /** + * Number of columns on which to iterate. + * This is {@code (xmax + 1) - xmin} where {@code xmax} is inclusive. + */ + private final long width; + + /** + * Maximum value (exclusive) of the indexes of the tiles on which to iterate. + * Index is computed using the row-major convention. + */ + private final long limit; + + /** + * Creates a new request for tile iterators. + * + * @param xmin first column index of tiles, inclusive. + * @param xmin first row index of tiles, inclusive. + * @param xmax last column index of tiles, inclusive. + * @param ymax last row index of tiles, inclusive. + */ + protected IterationDomain(final int xmin, final int ymin, final int xmax, final int ymax) { + this.xmin = xmin; + this.ymin = ymin; + this.width = (xmax + 1L) - xmin; + this.limit = Math.multiplyExact(width, (ymax + 1L) - ymin); + } + + /** + * Creates a new item for the tile at the given indexes. If this method returns {@code null}, + * then the tile is assumed missing and this iterator searches for the next tile. + * + * @param tileX column index of the tile. + * @param tileY row index of the tile. + * @return item for the tile at the given indexes, or {@code null} if missing. + */ + protected abstract T createTile(int tileX, int tileY); + + /** + * Creates the first tile, or returns {@code null} if the tile is missing. + */ + final T createFirstTile() { + return createTile(xmin, ymin); + } + + /** + * Returns a new iterator over the tiles. + * + * @return a new iterator. + */ + public final Spliterator<T> iterator() { + return new Iterator(); + } + + /** + * Iterator over the tiles. + */ + private final class Iterator implements Spliterator<T> { + /** + * Row-major index of the next tile to return. + */ + private long index; + + /** + * Increment to apply on {@link #index} between each iteration. + */ + private int increment; + + /** + * Creates a new iterator which will initially traverse all tiles. + */ + Iterator() { + index = Math.multiplyExact(width, ymin); + increment = 1; + } + + /** + * Creates a new iterator which will iterate over half of the tiles covered by the given iterator. + * This constructor modifies the supplied iterator for covering the other half. + * + * @param parent the iterator to split. It will be modified by this method call. + */ + private Iterator(final Iterator parent) { + index = parent.index; + parent.index += parent.increment; + increment = parent.increment *= 2; + } + + /** + * Returns, if possible, an iterator covering half of the tiles covered by this iterator. + * On return, this iterator will cover the other half. + * + * @return an iterator covering half of the tiles, or {@code null} if this iterator cannot be partitioned. + */ + @Override + public Spliterator<T> trySplit() { + return (limit - index) > increment ? new Iterator(this) : null; + } + + /** + * Returns the number of remaining tiles in the iteration. + */ + @Override + public long estimateSize() { + return (limit - index) / increment; + } + + /** + * In the context of this iterator, this is synonymous of {@link #estimateSize()}. + */ + @Override + public long getExactSizeIfKnown() { + return estimateSize(); + } + + /** + * Returns the characteristics of the iteration. The number of tiles is known in advance ({@link #SIZED}), + * null values are not allowed ({@link #NONNULL}), there will be no tiles at same indexes ({@link #DISTINCT}), + * and the tile matrix will not change ({@link #IMMUTABLE}). This iterator makes no guaranteed about iteration + * order (i.e., not {@link #ORDERED}). + * + * <p>Note that {@link #IMMUTABLE} is not a promise that the pixel values will not change, + * as the image may be writable. But this iterator is concerned only about the set of tiles, + * not the content of those tiles.</p> + */ + @Override + public int characteristics() { + return SIZED | SUBSIZED | NONNULL | DISTINCT | IMMUTABLE; + } + + /** + * If a remaining tile exists, performs the given action on it. + * + * @param action the action to execute on the next tile. + * @return whether the action has been performed. + */ + @Override + public boolean tryAdvance(final Consumer<? super T> action) { + while (index < limit) { + final int tileY = Math.toIntExact(index / width); + final int tileX = Math.toIntExact(index % width + xmin); + final T tile = createTile(tileX, tileY); + index += increment; + if (tile != null) { + action.accept(tile); + return true; + } + } + return false; + } + + /** + * Performs the given action on all remaining tiles. + * + * @param action the action to execute on all remaining tiles. + */ + @Override + public void forEachRemaining(final Consumer<? super T> action) { + while (index < limit) { + final int tileY = Math.toIntExact(index / width); + final int tileX = Math.toIntExact(index % width + xmin); + final T tile = createTile(tileX, tileY); + index += increment; + if (tile != null) { + action.accept(tile); + } + } + } + } +} diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSet.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSet.java index 4799f8c0f0..7699b7b470 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSet.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TileMatrixSet.java @@ -26,13 +26,13 @@ import org.apache.sis.storage.base.MetadataBuilder; /** - * A collection of {@code TileMatrix} in the same CRS but at different scale levels. + * A collection of {@code TileMatrix} in the same <abbr>CRS</abbr> but at different scale levels. * Each {@code TileMatrix} is optimized for a particular scale and is identified by a tile matrix identifier. * Tile matrices usually have 2 dimensions (width and height), but this API allows any number of dimensions. * However, the number of dimensions must be the same for all tile matrices. * - * <p>The {@code TileMatrixSet} concept is derived from OGC standards. The same concept is called - * <i>image pyramid</i> or <i>resolution levels</i> in some other standards. + * <p>The {@code TileMatrixSet} concept is derived from <abbr>OGC</abbr> standards. + * The same concept is called <i>image pyramid</i> or <i>resolution levels</i> in some other standards. * Some standards require that all scales must be related by a power of 2, * but {@code TileMatrixSet} does not have this restriction.</p> * @@ -110,7 +110,7 @@ public interface TileMatrixSet { * Returns the coordinate reference system of all {@code TileMatrix} instances in this set. * This is the value returned by {@code TileMatrix.getTilingScheme().getCoordinateReferenceSystem()}. * - * @return the CRS used by all {@code TileMatrix} instances in this set. + * @return the <abbr>CRS</abbr> used by all {@code TileMatrix} instances in this set. * * @see TileMatrix#getTilingScheme() */ @@ -122,12 +122,12 @@ public interface TileMatrixSet { * of all values returned by {@code TileMatrix.getTilingScheme().getEnvelope()}. * May be empty if too costly to compute. * - * @return the bounding box for all tile matrices in CRS coordinates, if available. + * @return the bounding box for all tile matrices in <abbr>CRS</abbr> coordinates, if available. */ Optional<Envelope> getEnvelope(); /** - * Returns all {@link TileMatrix} instances in this set, together with their identifiers. + * Returns all {@code TileMatrix} instances in this set, together with their identifiers. * For each value in the map, the associated key is {@link TileMatrix#getIdentifier()}. * Entries are sorted from coarser resolution (highest scale denominator) to most detailed * resolution (lowest scale denominator). 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 68a989962a..151beac5fe 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 @@ -208,7 +208,8 @@ public abstract class TiledGridCoverage extends GridCoverage { /** * The dimension of the grid which is mapped to the <var>x</var> axis (column indexes) in rendered images. * This is the value of the {@code xDimension} argument in the last call to the - * {@link TiledGridCoverageResource#setRasterDimension(int, int)} method before this coverage has been constructed. + * {@link TiledGridCoverageResource#setRasterSubspaceDimensions(int, int)} method + * before this coverage has been constructed. * This value is usually 0. * * @see #readTiles(TileIterator) @@ -218,7 +219,8 @@ public abstract class TiledGridCoverage extends GridCoverage { /** * The dimension of the grid which is mapped to the <var>y</var> axis (row indexes) in rendered images. * This is the value of the {@code yDimension} argument in the last call to the - * {@link TiledGridCoverageResource#setRasterDimension(int, int)} method before this coverage has been constructed. + * {@link TiledGridCoverageResource#setRasterSubspaceDimensions(int, int)} method + * before this coverage has been constructed. * This value is usually 1. * * @see #readTiles(TileIterator) @@ -266,7 +268,7 @@ public abstract class TiledGridCoverage extends GridCoverage { * This is true if the user explicitly {@linkplain TiledGridCoverageResource#setLoadingStrategy requested * such deferred loading strategy} and the resource considers that it is worth to do so. */ - private final boolean deferredTileReading; + final boolean deferredTileReading; /** * Creates a new tiled grid coverage. This constructor does not load any tile. 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 3507103dac..9ace524095 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 @@ -19,6 +19,7 @@ package org.apache.sis.storage.tiling; import java.util.List; import java.util.Arrays; import java.util.Objects; +import java.util.Collection; import java.lang.reflect.Array; import java.awt.image.DataBuffer; import java.awt.image.ColorModel; @@ -29,6 +30,8 @@ import java.awt.image.IndexColorModel; import java.awt.image.Raster; import java.awt.image.RenderedImage; import java.awt.image.RasterFormatException; +import org.opengis.util.GenericName; +import org.opengis.util.NameFactory; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.grid.GridCoverage; import org.apache.sis.coverage.grid.GridCoverage2D; @@ -52,6 +55,7 @@ import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.internal.shared.Numerics; import org.apache.sis.util.collection.WeakValueHashMap; +import org.apache.sis.util.iso.DefaultNameFactory; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.coverage.CannotEvaluateException; @@ -76,7 +80,7 @@ import org.opengis.coverage.CannotEvaluateException; * @version 1.7 * @since 1.7 */ -public abstract class TiledGridCoverageResource extends AbstractGridCoverageResource { +public abstract class TiledGridCoverageResource extends AbstractGridCoverageResource implements TiledResource { /** * Number of dimensions in a rendered image. * Used for identifying codes where a two-dimensional slice is assumed. @@ -147,6 +151,13 @@ public abstract class TiledGridCoverageResource extends AbstractGridCoverageReso */ private RasterLoadingStrategy loadingStrategy; + /** + * The tile matrix sets, created when first requested. + * + * @see #getTileMatrixSets() + */ + private Collection<TileMatrixSet> tileMatrixSets; + /** * The dimension of the grid which is mapped to the <var>x</var> axis (column indexes) in rendered images. * The default value is 0. @@ -159,11 +170,6 @@ public abstract class TiledGridCoverageResource extends AbstractGridCoverageReso */ private int yDimension; - /** - * The grid coverage processor to use when tiles use a subset of the bands. - */ - final GridCoverageProcessor processor; - /** * Creates a new resource. * @@ -172,7 +178,6 @@ public abstract class TiledGridCoverageResource extends AbstractGridCoverageReso protected TiledGridCoverageResource(final Resource parent) { super(parent); yDimension = 1; - processor = new GridCoverageProcessor(); } /** @@ -191,8 +196,9 @@ public abstract class TiledGridCoverageResource extends AbstractGridCoverageReso * * @see TiledGridCoverage#xDimension * @see TiledGridCoverage#yDimension + * @see GridExtent#getSubspaceDimensions(int) */ - protected void setRasterDimension(final int xDimension, final int yDimension) throws DataStoreException { + protected void setRasterSubspaceDimensions(final int xDimension, final int yDimension) throws DataStoreException { final int max = getGridGeometry().getDimension() - 1; ArgumentChecks.ensureBetween("xDimension", 0, max, xDimension); ArgumentChecks.ensureBetween("yDimension", 0, max, yDimension); @@ -827,6 +833,26 @@ check: if (dataType.isInteger()) { return loaded; } + /** + * Returns a coverage which will read the tiles as late as possible. + * + * @return the coverage. + * @throws DataStoreException if an error occurred while reading the grid coverage data. + */ + final TiledGridCoverage readAtGetTileTime() throws DataStoreException { + synchronized (getSynchronizationLock()) { + final RasterLoadingStrategy old = loadingStrategy; + try { + loadingStrategy = RasterLoadingStrategy.AT_GET_TILE_TIME; + return read(new Subset(null, null)); + } catch (RuntimeException e) { + throw canNotRead(listeners.getSourceName(), null, e); + } finally { + loadingStrategy = old; + } + } + } + /** * Whether this resource supports immediate loading of raster data. * Current implementation does not support immediate loading if the data cube has more than 2 dimensions. @@ -882,4 +908,139 @@ check: if (dataType.isInteger()) { loadingStrategy = loadAtReadTime ? RasterLoadingStrategy.AT_READ_TIME : RasterLoadingStrategy.AT_RENDER_TIME; } + + /** + * Returns the collection of all available tile matrix sets in this resource. + * The returned collection typically contains exactly one instance. + * + * <p>The default implementation uses the information provided by {@link #getPyramids()} + * for creating default {@link TileMatrixSet} instances. + * It is generally easier for subclasses to override {@link #getPyramids()} instead of this method.</p> + * + * @return all available {@link TileMatrixSet} instances, or an empty collection if none. + * @throws DataStoreException if an error occurred while fetching the tile matrix sets. + */ + @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") // The collection is unmodifiable. + public Collection<? extends TileMatrixSet> getTileMatrixSets() throws DataStoreException { + synchronized (getSynchronizationLock()) { + if (tileMatrixSets == null) { + final List<Pyramid> pyramids = getPyramids(); + final var sets = new TileMatrixSet[pyramids.size()]; + final GenericName scope = getIdentifier().orElseGet( + () -> pyramids.get(0).nameFactory().createLocalName(null, listeners.getSourceName())); + final var processor = new GridCoverageProcessor(); + for (int i=0; i<sets.length; i++) { + sets[i] = new ImagePyramid(scope, pyramids.get(i), processor); + } + tileMatrixSets = List.of(sets); + } + return tileMatrixSets; + } + } + + /** + * Returns information about the {@code TileMatrixSet} instances to create. + * This method is invoked by the default implementation of {@link #getTileMatrixSets()} when first needed. + * By default, this method returns a list of only one element, which itself describes a pyramid of only one level. + * This single level describes a {@link TileMatrix} at the resolution of this {@code TiledGridCoverageResource}. + * + * @return information about the tile matrix sets to create. + */ + protected List<Pyramid> getPyramids() { + return List.of((level) -> (level == 0) ? this : null); + } + + /** + * Description of a {@code TileMatrixSet} implemented as an image pyramid. + * This interface is used by the default implementation of {@link #getTileMatrixSets()}. + * There is usually only one pyramid per {@link TiledGridCoverageResource} instance, + * but many pyramids may exist, for example, if data are offered in different + * Coordinate Reference System (<abbr>CRS</abbr>). + * + * <p>Each pyramid can have an arbitrary number of levels. + * It is recommended to have one pyramid level for each {@linkplain #getResolutions() preferred resolutions}. + * The pyramid levels must be sorted from finest resolution (at level 0) to coarsest resolution.</p> + * + * <p>The number of levels is unspecified because some data stores cannot provide this information in advance. + * Instead, the {@link #forPyramidLevel(int)} method will be invoked with different argument values when each + * level is first requested, until that method returns {@code null} for a level too high.</p> + * + * @author Martin Desruisseaux (Geomatys) + * @version 1.7 + * @since 1.7 + */ + protected static interface Pyramid { + /** + * Returns an identifier for this pyramid. The default implementation returns <abbr>TMS</abbr> + * as the abbreviation of "Tile Matrix Set". This is often sufficient in the common case where + * there is only one Tile Matrix Set per Grid Coverage Resource. + * + * <p>This value is used for building the value of {@link TileMatrixSet#getIdentifier()}.</p> + * + * @return an identifier for this pyramid. Default is {@code "TMS"}. + * + * @see TileMatrixSet#getIdentifier() + */ + default String identifier() { + return "TMS"; + } + + /** + * Returns an identifier for the given level of this pyramid. The returned identifier + * will be local in the namespace of the pyramid {@linkplain #identifier() identifier}. + * + * @param level the pyramid level where 0 is the level with the finest resolution. + * @return a local identifier for the specified level. + */ + default String identifierOfLevel(int level) { + return "L" + level; + } + + /** + * Returns the level in this pyramid for the given local identifier. + * This method is the converse of {@link #identifierOfLevel(int)}. + * + * @param identifier the identifier for which to get the pyramid level. + * @return pyramid level associated to the given identifier. + * @throws IllegalArgumentException if the given identifier is not recognized by this pyramid. + */ + default int levelOfIdentifier(final String identifier) { + if (identifier.isEmpty() || identifier.charAt(0) != 'L') { + throw new IllegalArgumentException(identifier); + } + // Note: `NumberFormatException` is a subtype of `IllegalArgumentException`. + return Integer.parseInt(identifier.substring(1)); + } + + /** + * Returns a resource for the same data as this resource but at a different resolution level. + * The resource at index 0 shall be the resource with the finest resolution, and resources at + * increasing index values shall be resources with increasingly coarser resolutions. + * If the specified level is equal or greater than the number of levels in this pyramid, + * then this method shall return {@code null}. + * + * <p>If this method returns a non-null instance <var>r</var>, then the following condition should hold: + * {@code r.getGridGeometry().getResolution(false)} should be equal, ignoring NaN values and rounding + * errors, to {@code getResolutions().get(level)}.</p> + * + * @param level the pyramid level where 0 is the level with the finest resolution. + * @return a resource for data at the specified pyramid level, or {@code null} if the given level is too high. + * @throws DataStoreException if an error occurred while creating the resource. + * + * @see #getResolutions() + */ + TiledGridCoverageResource forPyramidLevel(int level) throws DataStoreException; + + /** + * Returns the name factory to use for creating identifiers of tiles and tile matrices. + * The default implementation returns {@link DefaultNameFactory#provider()}. + * Subclasses can override for more control on the identifiers to create. + * + * @return the name factory to use for creating identifiers of tiles and tile matrices. + */ + default NameFactory nameFactory() { + return DefaultNameFactory.provider(); + } + } } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java index bc7c7f72ff..4c9d6e338a 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/tiling/TiledResource.java @@ -30,9 +30,7 @@ import org.apache.sis.storage.Resource; * <p>A {@code TiledResource} may contain multiple {@link TileMatrixSet} instances, * each one for a different {@link org.opengis.referencing.crs.CoordinateReferenceSystem}. * Most format specifications only support a single {@link TileMatrixSet}, - * but a few ones like WMTS may have several.</p> - * - * <p>All methods in this interface return non-null values.</p> + * but a few ones like <abbr>WMTS</abbr> may have several.</p> * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TableAppender.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TableAppender.java index e11a99bde9..fff3fe23a0 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TableAppender.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/TableAppender.java @@ -68,7 +68,7 @@ import static org.apache.sis.util.Characters.isLineOrParagraphSeparator; * ╚═════════╧═════════╧════════╝</pre> * * @author Martin Desruisseaux (MPO, IRD, Geomatys) - * @version 1.0 + * @version 1.7 * * @see org.apache.sis.util.collection.TreeTableFormat * @@ -227,6 +227,20 @@ public class TableAppender extends Appender implements Flushable { ownOut = true; } + /** + * Creates a new table formatter writing in an internal buffer with the specified column separator and border. + * + * @param leftBorder string to write on the left side of the table. + * @param separator string to write between columns. + * @param rightBorder string to write on the right side of the table. + * + * @since 1.7 + */ + public TableAppender(final String leftBorder, final String separator, final String rightBorder) { + this(new StringBuilder(256), leftBorder, separator, rightBorder); + ownOut = true; + } + /** * Creates a new table formatter writing in the given output with a default column separator. * The default is a vertical double line for the left and right table borders, and a single diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java index 571953421a..7e54a9c545 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java @@ -41,7 +41,7 @@ * Unicode supplementary characters}. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.6 + * @version 1.7 * @since 0.3 */ package org.apache.sis.io; diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/DecimalFunctions.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/DecimalFunctions.java index 70bc3e7271..3a5bfac210 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/DecimalFunctions.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/math/DecimalFunctions.java @@ -45,7 +45,7 @@ import static org.apache.sis.pending.jdk.JDK19.DOUBLE_PRECISION; * since base 10 is not more "real" than base 2 for natural phenomenon. * * @author Martin Desruisseaux (Geomatys) - * @version 1.0 + * @version 1.7 * * @see MathFunctions#pow10(int) * @see Math#log10(double) @@ -497,6 +497,31 @@ public final class DecimalFunctions { throw new ArithmeticException(String.valueOf(x)); } + /** + * Computes {@code (int) floor(log10(x))} from an integer value. For values greater than one, + * this is the number of digits - 1 in the decimal representation of the given number. + * Values smaller than 1 are not allowed. + * + * @param x the value for which to compute the logarithm. Must be greater than zero. + * @return logarithm of the given value, rounded toward zero. + * @throws ArithmeticException if the given value is zero or negative. + * + * @since 1.7 + */ + public static int floorLog10(final long x) { + if (x <= 0) { + throw new ArithmeticException(String.valueOf(x)); + } + // It would be possible to do something clever, but this method is not invoked very often. + int p = 0; + long limit = 10; + while (x >= limit) { + limit *= 10; + p++; + } + return p; + } + /** * Returns {@code true} if the given numbers or equal or differ only by {@code accurate} * having more non-zero trailing decimal fraction digits than {@code approximate}. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/AbstractMap.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/AbstractMap.java index 2da8ee66e8..0104706f87 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/AbstractMap.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/AbstractMap.java @@ -708,7 +708,7 @@ public abstract class AbstractMap<K,V> implements Map<K,V> { */ @Override public String toString() { - final var buffer = new TableAppender(" = "); + final var buffer = new TableAppender("", " = ", ""); buffer.setMultiLinesCells(true); final EntryIterator<K,V> it = entryIterator(); if (it != null) while (it.next()) { diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/math/DecimalFunctionsTest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/math/DecimalFunctionsTest.java index a5c71ba2ad..6c092803ed 100644 --- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/math/DecimalFunctionsTest.java +++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/math/DecimalFunctionsTest.java @@ -33,6 +33,7 @@ import org.apache.sis.test.TestUtilities; * * @author Martin Desruisseaux (Geomatys) */ +@SuppressWarnings("exports") public final class DecimalFunctionsTest extends TestCase { /** * Creates a new test case. @@ -269,14 +270,14 @@ public final class DecimalFunctionsTest extends TestCase { */ @Test public void testFloorLog10() { - assertEquals( 0, floorLog10( 1)); - assertEquals( 0, floorLog10( 9)); - assertEquals( 1, floorLog10( 10)); - assertEquals( 1, floorLog10( 11)); - assertEquals( 1, floorLog10( 99)); - assertEquals( 2, floorLog10( 100)); - assertEquals( 2, floorLog10( 999)); - assertEquals( 3, floorLog10(1000)); + assertEquals( 0, floorLog10( 1d)); + assertEquals( 0, floorLog10( 9d)); + assertEquals( 1, floorLog10( 10d)); + assertEquals( 1, floorLog10( 11d)); + assertEquals( 1, floorLog10( 99d)); + assertEquals( 2, floorLog10( 100d)); + assertEquals( 2, floorLog10( 999d)); + assertEquals( 3, floorLog10(1000d)); assertEquals( -1, floorLog10(0.100)); assertEquals( -2, floorLog10(0.099)); assertEquals( -2, floorLog10(0.010)); @@ -291,6 +292,21 @@ public final class DecimalFunctionsTest extends TestCase { try {floorLog10(POSITIVE_INFINITY); fail("Expected ArithmeticException.");} catch (ArithmeticException e) {} } + /** + * Tests {@link DecimalFunctions#floorLog10(long)} method. + */ + @Test + public void testFloorLog10OnFromInteger() { + assertEquals(0, floorLog10( 1L)); + assertEquals(0, floorLog10( 9L)); + assertEquals(1, floorLog10( 10L)); + assertEquals(1, floorLog10( 11L)); + assertEquals(1, floorLog10( 99L)); + assertEquals(2, floorLog10( 100L)); + assertEquals(2, floorLog10( 999L)); + assertEquals(3, floorLog10(1000L)); + } + /** * Tests {@link DecimalFunctions#equalsIgnoreMissingFractionDigits(double, double)}. * This test uses the conversion factor from degrees to radians as a use case. diff --git a/optional/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java b/optional/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java index 2bd9814ce1..60eddd1638 100644 --- a/optional/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java +++ b/optional/src/org.apache.sis.storage.gdal/test/org/apache/sis/storage/gdal/GDALStoreTest.java @@ -50,8 +50,9 @@ import static org.junit.jupiter.api.Assertions.*; * Tests {@link GDALStore}. * * @author Johann Sorel (Geomatys) - * @author Quentin BIALOTA (Geomatys) + * @author Quentin Bialota (Geomatys) */ +@SuppressWarnings("exports") public final class GDALStoreTest { /** * Name of the test file.
