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 c01f85506e80f866b733d1875c1a609c3685f228
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Thu Apr 21 16:48:02 2022 +0200

    Add World File write test.
---
 .../internal/storage/image/WorldFileResource.java  | 110 +++++++++++++++++----
 .../sis/internal/storage/image/WorldFileStore.java |  20 +++-
 .../storage/image/WorldFileStoreProvider.java      |   3 +-
 .../internal/storage/image/WritableResource.java   |   9 +-
 .../sis/internal/storage/image/WritableStore.java  |   4 +-
 .../sis/internal/storage/image/package-info.java   |  13 +++
 .../internal/storage/image/WorldFileStoreTest.java |  94 ++++++++++++++++--
 7 files changed, 212 insertions(+), 41 deletions(-)

diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
index c292bc5089..57dee0fc58 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileResource.java
@@ -19,12 +19,14 @@ package org.apache.sis.internal.storage.image;
 import java.util.List;
 import java.util.Optional;
 import java.io.IOException;
+import java.lang.ref.SoftReference;
 import java.awt.Rectangle;
 import java.awt.image.RenderedImage;
 import java.awt.image.BandedSampleModel;
 import javax.imageio.ImageReader;
 import javax.imageio.ImageReadParam;
 import javax.imageio.ImageTypeSpecifier;
+import org.opengis.util.LocalName;
 import org.opengis.util.GenericName;
 import org.opengis.util.InternationalString;
 import org.apache.sis.image.ImageProcessor;
@@ -73,23 +75,27 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
 
     /**
      * Index of the image to read or write in the image file. This is usually 
0.
+     * May be decremented when other resources are {@linkplain 
WritableStore#remove removed}.
+     *
+     * @see #getImageIndex()
      */
-    int imageIndex;
+    private int imageIndex;
 
     /**
      * The identifier as a sequence number in the namespace of the {@link 
WorldFileStore}.
      * The first image has the sequence number "1". This is computed when 
first needed.
+     * {@link WritableResource} have no identifier because the numbers may 
change.
      *
      * @see #getIdentifier()
      */
-    private GenericName identifier;
+    private LocalName identifier;
 
     /**
      * The grid geometry of this resource. The grid extent is the image size.
      *
      * @see #getGridGeometry()
      */
-    private final GridGeometry gridGeometry;
+    private GridGeometry gridGeometry;
 
     /**
      * The ranges of sample values, computed when first needed. Shall be an 
unmodifiable list.
@@ -98,6 +104,11 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
      */
     private List<SampleDimension> sampleDimensions;
 
+    /**
+     * Cached coverage for the full image, or {@code null} if none.
+     */
+    private SoftReference<GridCoverage> fullCoverage;
+
     /**
      * Creates a new resource. This resource will have its own set of 
listeners,
      * but the listeners of the data store that created this resource will be 
notified as well.
@@ -132,16 +143,43 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
         throw new 
DataStoreException(Resources.format(Resources.Keys.ResourceRemoved));
     }
 
+    /**
+     * Returns the index of the image to read or write in the image file. This 
is usually 0.
+     * Note that contrarily to {@link #getIdentifier()}, this index is not 
guaranteed to be constant.
+     */
+    final int getImageIndex() {
+        return imageIndex;
+    }
+
+    /**
+     * Decrements the image index. This is needed if images before this image 
have been removed.
+     */
+    final void decrementImageIndex() throws DataStoreException {
+        getIdentifier();    // For identifier creation for keeping it constant.
+        imageIndex--;
+    }
+
     /**
      * Returns the resource identifier. The name space is the file name and
      * the local part of the name is the image index number, starting at 1.
+     * This identifier should be constant.
      */
     @Override
     public final Optional<GenericName> getIdentifier() throws 
DataStoreException {
         final WorldFileStore store = store();
         synchronized (store) {
             if (identifier == null) {
-                identifier = Names.createLocalName(store.getDisplayName(), 
null, String.valueOf(imageIndex + 1));
+                // TODO: get `base` from image metadata if available.
+                final String base = String.valueOf(getImageIndex() + 1);
+                String id = base;
+                int n = 0;
+                while (store.identifiers.putIfAbsent(id, Boolean.TRUE) != 
null) {
+                    if (--n >= 0) {
+                        throw new ArithmeticException();    // Paranoiac 
safety for avoiding never-ending loop.
+                    }
+                    id = base + n;
+                }
+                identifier = Names.createLocalName(store.getDisplayName(), 
null, id);
             }
             return Optional.of(identifier);
         }
@@ -155,7 +193,9 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
      */
     @Override
     public final GridGeometry getGridGeometry() throws DataStoreException {
-        return gridGeometry;
+        synchronized (store()) {
+            return gridGeometry;
+        }
     }
 
     /**
@@ -168,7 +208,7 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
         synchronized (store) {
             if (sampleDimensions == null) try {
                 final ImageReader        reader = store.reader();
-                final ImageTypeSpecifier type   = 
reader.getRawImageType(imageIndex);
+                final ImageTypeSpecifier type   = 
reader.getRawImageType(getImageIndex());
                 final SampleDimension[]  bands  = new 
SampleDimension[type.getNumBands()];
                 final SampleDimension.Builder b = new 
SampleDimension.Builder();
                 final short[] names = 
ImageUtilities.bandNames(type.getColorModel(), type.getSampleModel());
@@ -206,11 +246,17 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
      */
     @Override
     public final GridCoverage read(GridGeometry domain, int... range) throws 
DataStoreException {
-        RenderedImage image;
-        List<SampleDimension> bands;
+        final boolean isFullCoverage = (domain == null && range == null);
         final WorldFileStore store = store();
         try {
             synchronized (store) {
+                if (isFullCoverage && fullCoverage != null) {
+                    final GridCoverage coverage = fullCoverage.get();
+                    if (coverage != null) {
+                        return coverage;
+                    }
+                    fullCoverage = null;
+                }
                 final ImageReader reader = store.reader();
                 final ImageReadParam param = reader.getDefaultReadParam();
                 if (domain == null) {
@@ -254,9 +300,9 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
                  * be the easiest cases. More difficult cases will be handled 
after the reading.
                  * Those heuristic rules may be changed in any future version.
                  */
-                bands = getSampleDimensions();
+                List<SampleDimension> bands = getSampleDimensions();
                 if (range != null) {
-                    final ImageTypeSpecifier type = 
reader.getRawImageType(imageIndex);
+                    final ImageTypeSpecifier type = 
reader.getRawImageType(getImageIndex());
                     final RangeArgument args = 
RangeArgument.validate(type.getNumBands(), range, listeners);
                     if (args.isIdentity()) {
                         range = null;
@@ -270,26 +316,48 @@ class WorldFileResource extends 
AbstractGridCoverageResource implements StoreRes
                         }
                     }
                 }
-                image = reader.readAsRenderedImage(imageIndex, param);
+                RenderedImage image = 
reader.readAsRenderedImage(getImageIndex(), param);
+                /*
+                 * If the reader was presumed unable to handle the band 
subsetting, apply it now.
+                 * It waste some memory because unused bands still in memory. 
But we do that as a
+                 * workaround for limitations in some `ImageReader` 
implementations.
+                 */
+                if (range != null) {
+                    image = new ImageProcessor().selectBands(image, range);
+                }
+                final GridCoverage coverage = new GridCoverage2D(domain, 
bands, image);
+                if (isFullCoverage) {
+                    fullCoverage = new SoftReference<>(coverage);
+                }
+                return coverage;
             }
         } catch (IOException | RuntimeException e) {
             throw canNotRead(store.getDisplayName(), domain, e);
         }
-        /*
-         * If the reader was presumed unable to handle the band subsetting, 
apply it now.
-         * It waste some memory because unused bands still in memory. But we 
do that as a
-         * workaround for limitations in some `ImageReader` implementations.
-         */
-        if (range != null) {
-            image = new ImageProcessor().selectBands(image, range);
-        }
-        return new GridCoverage2D(domain, bands, image);
+    }
+
+    /**
+     * Sets the grid coverage to the given value.
+     * This is used during write operations only.
+     */
+    final void setGridCoverage(final GridCoverage coverage) {
+        sampleDimensions = coverage.getSampleDimensions();
+        gridGeometry     = coverage.getGridGeometry();
+        fullCoverage     = new SoftReference<>(coverage);
     }
 
     /**
      * Notifies this resource that it should not be used anymore.
      */
     final void dispose() {
-        store = null;
+        if (identifier != null) {
+            // For information purpose but not really used.
+            store.identifiers.put(identifier.toString(), Boolean.FALSE);
+        }
+        store            = null;
+        identifier       = null;
+        sampleDimensions = null;
+        gridGeometry     = null;
+        fullCoverage     = null;
     }
 }
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
index 2188e244fe..62052b1731 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStore.java
@@ -18,8 +18,9 @@ package org.apache.sis.internal.storage.image;
 
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.LinkedHashMap;
 import java.util.Map;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.logging.Level;
 import java.io.IOException;
 import java.io.EOFException;
@@ -201,6 +202,16 @@ class WorldFileStore extends PRJDataStore implements 
Aggregate {
      */
     private Metadata metadata;
 
+    /**
+     * Identifiers used by a resource. Identifiers must be unique in the data 
store,
+     * so after an identifier has been used it can not be reused anymore even 
if the
+     * resource having that identifier has been removed.
+     * Values associated to identifiers tell whether the resource still exist.
+     *
+     * @see WorldFileResource#getIdentifier()
+     */
+    final Map<String,Boolean> identifiers;
+
     /**
      * Creates a new store from the given file, URL or stream.
      *
@@ -214,6 +225,7 @@ class WorldFileStore extends PRJDataStore implements 
Aggregate {
             throws DataStoreException, IOException
     {
         super(provider, connector);
+        identifiers = new HashMap<>();
         final Object storage = connector.getStorage();
         if (storage instanceof ImageReader) {
             reader = (ImageReader) storage;
@@ -689,7 +701,7 @@ loop:   for (int convention=0;; convention++) {
          * @param  image  the image to add to this list.
          */
         final void added(final WorldFileResource image) {
-            size = image.imageIndex;
+            size = image.getImageIndex();
             if (size >= images.length) {
                 images = Arrays.copyOf(images, size * 2);
             }
@@ -702,14 +714,14 @@ loop:   for (int convention=0;; convention++) {
          *
          * @param  index  index of the image that has been removed.
          */
-        final void removed(int index) {
+        final void removed(int index) throws DataStoreException {
             final int last = images.length - 1;
             System.arraycopy(images, index+1, images, index, last - index);
             images[last] = null;
             size--;
             while (index < last) {
                 final WorldFileResource image = images[index++];
-                if (image != null) image.imageIndex--;
+                if (image != null) image.decrementImageIndex();
             }
         }
 
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
index 3e06c04f40..6012fd8c67 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WorldFileStoreProvider.java
@@ -23,7 +23,6 @@ import java.io.IOException;
 import javax.imageio.ImageReader;
 import javax.imageio.ImageWriter;
 import javax.imageio.spi.ImageReaderSpi;
-import org.apache.sis.storage.DataStore;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.internal.storage.Capability;
@@ -78,7 +77,7 @@ public final class WorldFileStoreProvider extends 
PRJDataStore.Provider {
      * @throws DataStoreException if an error occurred while creating the data 
store instance.
      */
     @Override
-    public DataStore open(final StorageConnector connector) throws 
DataStoreException {
+    public WorldFileStore open(final StorageConnector connector) throws 
DataStoreException {
         final Object storage = connector.getStorage();
         boolean isWritable = (storage instanceof ImageWriter);
         if (!isWritable) {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableResource.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableResource.java
index 5b6405d892..a0c521f6db 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableResource.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableResource.java
@@ -60,13 +60,14 @@ final class WritableResource extends WorldFileResource 
implements WritableGridCo
         final WritableStore store = (WritableStore) store();
         try {
             synchronized (store) {
-                if (imageIndex != WorldFileStore.MAIN_IMAGE || 
(store.isMultiImages() != 0 && !h.replace(null))) {
+                if (getImageIndex() != WorldFileStore.MAIN_IMAGE || 
(store.isMultiImages() != 0 && !h.replace(null))) {
                     // TODO: we should use `ImageWriter.replacePixels(…)` 
methods instead.
                     coverage = h.update(coverage);
                 }
-                final RenderedImage data = coverage.render(null);              
     // Fail if not two-dimensional.
-                store.setGridGeometry(imageIndex, coverage.getGridGeometry()); 
// May use the image reader.
-                final ImageWriter writer = store.writer();                     
     // Should be after `setGridGeometry(…)`.
+                final RenderedImage data = coverage.render(null);              
         // Fail if not two-dimensional.
+                store.setGridGeometry(getImageIndex(), 
coverage.getGridGeometry());     // May use the image reader.
+                setGridCoverage(coverage);
+                final ImageWriter writer = store.writer();                     
 // Should be after `setGridGeometry(…)`.
                 writer.write(data);
             }
         } catch (IOException | RuntimeException e) {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
index c427c2ec93..0e767ffad5 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
@@ -188,7 +188,6 @@ fallback:       if (writer == null) {
      * @see #setGridGeometry(int, GridGeometry)
      */
     final int isMultiImages() throws IOException, DataStoreException {
-        assert Thread.holdsLock(this);
         if (numImages < 0) {
             // This case happens only when we opened an existing file.
             final Components components = components(true, numImages);
@@ -221,6 +220,7 @@ fallback:       if (writer == null) {
      */
     @Override
     String setGridGeometry(final int index, GridGeometry gg) throws 
IOException, DataStoreException {
+        assert Thread.holdsLock(this);
         /*
          * Make sure that the grid geometry starts at (0,0).
          * Must be done before to compare with existing grid.
@@ -348,7 +348,7 @@ writeCoeffs:    for (int i=0;; i++) {
         if (resource instanceof WritableResource) {
             final WritableResource image = (WritableResource) resource;
             if (image.store() == this) try {
-                final int imageIndex = image.imageIndex;
+                final int imageIndex = image.getImageIndex();
                 writer().removeImage(imageIndex);
                 final Components components = components(false, numImages);
                 if (components != null) {
diff --git 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
index 60397f5937..de642ab341 100644
--- 
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
+++ 
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/package-info.java
@@ -23,6 +23,19 @@
  * completed with an additional source of information for georeferencing the 
image.
  * A commonly-used convention is the <cite>World File</cite> format.
  *
+ * <h2>Limitations</h2>
+ * The World File reader and writer currently have the following limitations.
+ * Those limitations may be addressed in a future SIS version.
+ *
+ * <ul>
+ *   <li>Image metadata are ignored. Some metadata that a future version could 
use are:
+ *       title, author, description, creation time, <i>etc.</i></li>
+ *   <li>Deferred reading of tiles (using {@code 
ImageReader.readTileRaster(…)})
+ *       is not yet implemented.</li>
+ *   <li>Resources have no identifier if the data store is writable,
+ *       because image indices are no longer stable identifiers in such 
case.</li>
+ * </ul>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 1.2
  *
diff --git 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
index 46b90eb276..ec967d2d67 100644
--- 
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
+++ 
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/WorldFileStoreTest.java
@@ -17,12 +17,20 @@
 package org.apache.sis.internal.storage.image;
 
 import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Files;
+import java.nio.file.DirectoryStream;
+import java.nio.file.StandardOpenOption;
 import org.opengis.metadata.Metadata;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.metadata.identification.Identification;
+import org.apache.sis.coverage.grid.GridCoverage;
 import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.GridCoverageResource;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.ProbeResult;
+import org.apache.sis.storage.ResourceAlreadyExistsException;
+import org.apache.sis.setup.OptionKey;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
@@ -53,21 +61,22 @@ public final strictfp class WorldFileStoreTest extends 
TestCase {
      */
     @Test
     public void testProbeContent() throws DataStoreException {
-        final WorldFileStoreProvider p = new WorldFileStoreProvider();
-        final ProbeResult r = p.probeContent(testData());
-        assertTrue(r.isSupported());
-        assertEquals("image/png", r.getMimeType());
+        final WorldFileStoreProvider provider = new WorldFileStoreProvider();
+        final ProbeResult result = provider.probeContent(testData());
+        assertTrue(result.isSupported());
+        assertEquals("image/png", result.getMimeType());
     }
 
     /**
      * Tests the metadata of the {@code "gradient.png"} file.
      *
-     * @throws DataStoreException if an error occurred while reading the file.
-     * @throws IOException if an error occurred while creating the image 
reader instance.
+     * @throws DataStoreException if an error occurred during Image I/O or 
data store operations.
      */
     @Test
-    public void testMetadata() throws DataStoreException, IOException {
-        try (WorldFileStore store = new WorldFileStore(null, testData(), 
true)) {
+    public void testMetadata() throws DataStoreException {
+        final WorldFileStoreProvider provider = new WorldFileStoreProvider();
+        try (WorldFileStore store = provider.open(testData())) {
+            assertFalse(store instanceof WritableStore);
             assertEquals("gradient", store.getIdentifier().get().toString());
             final Metadata metadata = store.getMetadata();
             final Identification id = 
getSingleton(metadata.getIdentificationInfo());
@@ -86,4 +95,73 @@ public final strictfp class WorldFileStoreTest extends 
TestCase {
             assertSame(metadata, store.getMetadata());
         }
     }
+
+    /**
+     * Tests reading the coverage and writing it in a new file.
+     *
+     * @throws DataStoreException if an error occurred during Image I/O or 
data store operations.
+     * @throws IOException if an error occurred when creating, reading or 
deleting temporary files.
+     */
+    @Test
+    public void testReadWrite() throws DataStoreException, IOException {
+        final Path directory = Files.createTempDirectory("SIS-");
+        try {
+            final WorldFileStoreProvider provider = new 
WorldFileStoreProvider();
+            try (WorldFileStore source = provider.open(testData())) {
+                assertFalse(source instanceof WritableStore);
+                final GridCoverageResource resource = 
getSingleton(source.components());
+                assertEquals("identifier", "1", 
resource.getIdentifier().get().toString());
+                /*
+                 * Above `resource` is the content of "gradient.png" file.
+                 * Write the resource in a new file using a different format.
+                 */
+                final Path targetPath = directory.resolve("copy.jpg");
+                final StorageConnector connector = new 
StorageConnector(targetPath);
+                connector.setOption(OptionKey.OPEN_OPTIONS, new 
StandardOpenOption[] {
+                    StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE
+                });
+                try (WritableStore target = (WritableStore) 
provider.open(connector)) {
+                    assertEquals(0, target.isMultiImages());
+                    final WritableResource copy = (WritableResource) 
target.add(resource);
+                    assertEquals(1, target.isMultiImages());
+                    assertNotSame(resource, copy);
+                    assertEquals (resource.getGridGeometry(),     
copy.getGridGeometry());
+                    assertEquals (resource.getSampleDimensions(), 
copy.getSampleDimensions());
+                    /*
+                     * Verify that attempt to write again without `REPLACE` 
mode fails.
+                     */
+                    final GridCoverage coverage = resource.read(null, null);
+                    try {
+                        copy.write(coverage);
+                        fail("Should not have replaced existing resource.");
+                    } catch (ResourceAlreadyExistsException e) {
+                        final String message = e.getMessage();
+                        assertTrue(message, message.contains("1"));     // "1" 
is the image identifier.
+                    }
+                    /*
+                     * Try to write again in `REPLACE` mode.
+                     */
+                    copy.write(coverage, 
WritableResource.CommonOption.REPLACE);
+                    assertEquals(1, target.isMultiImages());
+                }
+                /*
+                 * Verify that the 3 files have been written. The JGW file 
content is verified,
+                 * but the PRJ file content is not fully verified because it 
may vary.
+                 */
+                assertTrue(Files.size(targetPath) > 0);
+                assertTrue(Files.readAllLines(directory.resolve("copy.prj"))
+                                .stream().anyMatch((line) -> 
line.contains("WGS 84")));
+                assertArrayEquals(new String[] {
+                    "2.8125", "0.0", "0.0", "-2.8125", "-178.59375", "88.59375"
+                }, 
Files.readAllLines(directory.resolve("copy.jgw")).toArray());
+            }
+        } finally {
+            try (DirectoryStream<Path> entries = 
Files.newDirectoryStream(directory)) {
+                for (Path entry : entries) {
+                    Files.delete(entry);
+                }
+            }
+            Files.delete(directory);
+        }
+    }
 }

Reply via email to