This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
new 81614ecb87 Add a "World File" writer.
81614ecb87 is described below
commit 81614ecb870fec12f2b11260603b51401d456c5a
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Tue Apr 19 19:22:57 2022 +0200
Add a "World File" writer.
https://issues.apache.org/jira/browse/SIS-541
---
.../org/apache/sis/coverage/grid/GridGeometry.java | 4 +-
.../sis/internal/referencing/j2d/AffineMatrix.java | 3 +-
.../sis/internal/util/ListOfUnknownSize.java | 4 +-
.../apache/sis/internal/storage/PRJDataStore.java | 2 +-
.../org/apache/sis/internal/storage/Resources.java | 10 +
.../sis/internal/storage/Resources.properties | 2 +
.../sis/internal/storage/Resources_fr.properties | 2 +
.../sis/internal/storage/image/FormatFilter.java | 8 +-
.../apache/sis/internal/storage/image/Image.java | 141 +++---
.../apache/sis/internal/storage/image/Store.java | 337 ++++++++++++--
.../sis/internal/storage/image/StoreProvider.java | 13 +-
.../internal/storage/image/WarningListener.java | 18 +-
.../sis/internal/storage/image/WritableImage.java | 76 +++
.../sis/internal/storage/image/WritableStore.java | 515 +++++++++++++++++++++
.../storage/image/SelfConsistencyTest.java | 2 +-
.../sis/internal/storage/image/StoreTest.java | 2 +-
16 files changed, 1018 insertions(+), 121 deletions(-)
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 16c57d9f77..7e5ea0705c 100644
---
a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++
b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -1266,8 +1266,8 @@ public class GridGeometry implements LenientComparable,
Serializable {
*
* @param bitmask any combination of {@link #CRS}, {@link #ENVELOPE},
{@link #EXTENT},
* {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
- * @return {@code true} if all specified attributes are defined (i.e.
invoking the
- * corresponding method will not thrown an {@link
IncompleteGridGeometryException}).
+ * @return {@code true} if all specified properties are defined (i.e.
invoking the
+ * corresponding getter methods will not throw {@link
IncompleteGridGeometryException}).
* @throws IllegalArgumentException if the specified bitmask is not a
combination of known masks.
*
* @see #getCoordinateReferenceSystem()
diff --git
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
index 3c2067e819..c934204fff 100644
---
a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
+++
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/j2d/AffineMatrix.java
@@ -124,7 +124,8 @@ final class AffineMatrix implements
ExtendedPrecisionMatrix, Serializable, Clone
}
/**
- * Returns all matrix elements.
+ * Returns all matrix elements in row-major order.
+ * Note that this is not the same order than {@link AffineTransform}
constructor.
*/
@Override
public double[] getExtendedElements() {
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
index fd28e24f2c..6f5c0f33ce 100644
---
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
+++
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/ListOfUnknownSize.java
@@ -49,11 +49,11 @@ public abstract class ListOfUnknownSize<E> extends
AbstractSequentialList<E> {
}
/**
- * Returns {@link #size()} if its value is already known, or -1 if the
size is still unknown.
+ * Returns {@link #size()} if its value is already known, or a negative
value if the size is still unknown.
* The size may become known for example if it has been cached by the
subclass. In such case,
* some {@code ListOfUnknownSize} methods will take a more efficient path.
*
- * @return {@link #size()} if its value is already known, or -1 if it
still costly to compute.
+ * @return {@link #size()} if its value is already known, or any negative
value if it still costly to compute.
*/
protected int sizeIfKnown() {
return -1;
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
index f373297557..da82d2fa69 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/PRJDataStore.java
@@ -308,7 +308,7 @@ public abstract class PRJDataStore extends URIDataStore {
final StoreFormat format = new StoreFormat(locale, timezone,
null, listeners);
format.setConvention(Convention.WKT1_COMMON_UNITS);
format.format(crs, out);
- out.write(System.lineSeparator());
+ out.newLine();
}
} catch (IOException e) {
Object identifier = getIdentifier().orElse(null);
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
index 34e207be31..d27cddc3e1 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.java
@@ -270,6 +270,11 @@ public final class Resources extends IndexedResourceBundle
{
*/
public static final short IllegalOutputTypeForWriter_2 = 9;
+ /**
+ * All coverages must have the same grid geometry.
+ */
+ public static final short IncompatibleGridGeometry = 72;
+
/**
* Components of the “{1}” name are inconsistent with those of the
name previously binded in
* “{0}” data store.
@@ -346,6 +351,11 @@ public final class Resources extends IndexedResourceBundle
{
*/
public static final short ResourceNotFound_2 = 24;
+ /**
+ * This resource has been removed from its data store.
+ */
+ public static final short ResourceRemoved = 73;
+
/**
* The “{0}” format does not support rotations.
*/
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
index f6bbcfd450..cd6a783721 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources.properties
@@ -61,6 +61,7 @@ IllegalEventType_1 = This resource should not
fire events of type
IllegalFeatureType_2 = The {0} data store does not accept
features of type \u201c{1}\u201d.
IllegalInputTypeForReader_2 = The {0} reader does not accept inputs of
type \u2018{1}\u2019.
IllegalOutputTypeForWriter_2 = The {0} writer does not accept outputs of
type \u2018{1}\u2019.
+IncompatibleGridGeometry = All coverages must have the same grid
geometry.
InconsistentNameComponents_2 = Components of the \u201c{1}\u201d name are
inconsistent with those of the name previously binded in \u201c{0}\u201d data
store.
InvalidExpression_2 = Invalid or unsupported \u201c{1}\u201d
expression at index {0}.
InvalidSampleDimensionIndex_2 = Sample dimension index {1} is invalid.
Expected an index from 0 to {0} inclusive.
@@ -75,6 +76,7 @@ ProcessingExecutedOn_1 = Processing executed on
{0}.
ResourceAlreadyExists_1 = A resource already exists at
\u201c{0}\u201d.
ResourceIdentifierCollision_2 = More than one resource have the
\u201c{1}\u201d identifier in the \u201c{0}\u201d data store.
ResourceNotFound_2 = No resource found for the \u201c{1}\u201d
identifier in the \u201c{0}\u201d data store.
+ResourceRemoved = This resource has been removed from its
data store.
RequestOutOfBounds_5 = The request [{3} \u2026 {4}] is outside
the [{1} \u2026 {2}] domain for \u201c{0}\u201d axis.
RotationNotSupported_1 = The \u201c{0}\u201d format does not
support rotations.
ShallBeDeclaredBefore_2 = The \u201c{1}\u201d element must be
declared before \u201c{0}\u201d.
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
index 0dcaf8238a..6f87021ddc 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/Resources_fr.properties
@@ -66,6 +66,7 @@ IllegalEventType_1 = Cette ressource ne
devrait pas lancer des \u
IllegalFeatureType_2 = Le format {0} ne stocke pas de
donn\u00e9es de type \u00ab\u202f{1}\u202f\u00bb.
IllegalInputTypeForReader_2 = Le lecteur {0} n\u2019accepte pas des
entr\u00e9s de type \u2018{1}\u2019.
IllegalOutputTypeForWriter_2 = L\u2019encodeur {0} n\u2019accepte pas des
sorties de type \u2018{1}\u2019.
+IncompatibleGridGeometry = Toutes les couvertures de donn\u00e9es
doivent avoir la m\u00eame g\u00e9om\u00e9trie de grille.
InvalidExpression_2 = Expression \u00ab\u202f{1}\u202f\u00bb
invalide ou non-support\u00e9e \u00e0 l\u2019index {0}.
InvalidSampleDimensionIndex_2 = L\u2019index de dimension
d\u2019\u00e9chantillonnage {1} est invalide. On attendait un index de 0 \u00e0
{0} inclusif.
InconsistentNameComponents_2 = Les \u00e9l\u00e9ments qui composent le
nom \u00ab\u202f{1}\u202f\u00bb ne sont pas coh\u00e9rents avec ceux du nom qui
avait \u00e9t\u00e9 pr\u00e9c\u00e9demment li\u00e9 dans les donn\u00e9es de
\u00ab\u202f{0}\u202f\u00bb.
@@ -80,6 +81,7 @@ ProcessingExecutedOn_1 = Traitement
ex\u00e9cut\u00e9 sur {0}.
ResourceAlreadyExists_1 = Une ressource existe d\u00e9j\u00e0 \u00e0
l\u2019emplacement \u00ab\u202f{0}\u202f\u00bb.
ResourceIdentifierCollision_2 = Plusieurs ressources utilisent
l\u2019identifiant \u00ab\u202f{1}\u202f\u00bb dans les donn\u00e9es de
\u00ab\u202f{0}\u202f\u00bb.
ResourceNotFound_2 = Aucune ressource n\u2019a \u00e9t\u00e9
trouv\u00e9e pour l\u2019identifiant \u00ab\u202f{1}\u202f\u00bb dans les
donn\u00e9es de \u00ab\u202f{0}\u202f\u00bb.
+ResourceRemoved = Cette ressource a \u00e9t\u00e9
supprim\u00e9e de sa source de donn\u00e9es.
RequestOutOfBounds_5 = La demande [{3} \u2026 {4}] est en dehors
du domaine [{1} \u2026 {2}] pour l\u2019axe \u00ab\u202f{0}\u202f\u00bb.
RotationNotSupported_1 = Le format \u00ab\u202f{0}\u202f\u00bb ne
supporte pas les rotations.
ShallBeDeclaredBefore_2 = L\u2019\u00e9l\u00e9ment
\u00ab\u202f{1}\u202f\u00bb doit \u00eatre d\u00e9clar\u00e9 avant
\u00ab\u202f{0}\u202f\u00bb.
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
index 8b66e05ce0..97d8a7e50c 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/FormatFilter.java
@@ -89,7 +89,8 @@ enum FormatFilter {
* if an image reader requests a sub-type, we can probably not provide it
ourselves.
*/
private static final Class<?>[] VALID_OUTPUTS = {
- ImageOutputStream.class, DataOutput.class, OutputStream.class,
File.class, Path.class, URL.class, URI.class
+ // `ImageOutputStream` intentionally excluded because not handled by
`StorageConnector`.
+ DataOutput.class, OutputStream.class, File.class, Path.class,
URL.class, URI.class
};
/**
@@ -231,10 +232,11 @@ enum FormatFilter {
final ImageWriter writer =
provider.createWriterInstance();
writer.setOutput(output);
return writer;
- } else if (type == ImageOutputStream.class) {
- deferred.put(provider, Boolean.TRUE);
}
}
+ if (type == ImageOutputStream.class) {
+ deferred.put(provider, Boolean.TRUE);
+ }
}
}
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
index a212a1fe9e..268f8619d1 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Image.java
@@ -39,6 +39,7 @@ import org.apache.sis.storage.AbstractGridCoverageResource;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.event.StoreListeners;
+import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.storage.StoreResource;
import org.apache.sis.internal.storage.RangeArgument;
import org.apache.sis.internal.coverage.j2d.ImageUtilities;
@@ -63,17 +64,17 @@ class Image extends AbstractGridCoverageResource implements
StoreResource {
* The dimensions of <var>x</var> and <var>y</var> axes.
* Static constants for now, may become configurable fields in the future.
*/
- private static final int X_DIMENSION = 0, Y_DIMENSION = 1;
+ static final int X_DIMENSION = 0, Y_DIMENSION = 1;
/**
- * The parent data store.
+ * The parent data store, or {@code null} if this resource is not valid
anymore.
*/
- private final Store store;
+ private volatile Store store;
/**
- * Index of the image to read.
+ * Index of the image to read or write in the image file. This is usually
0.
*/
- private final int imageIndex;
+ int imageIndex;
/**
* The identifier as a sequence number in the namespace of the {@link
Store}.
@@ -116,12 +117,26 @@ class Image extends AbstractGridCoverageResource
implements StoreResource {
return store;
}
+ /**
+ * Returns the data store.
+ *
+ * @throws DataStoreException if this resource is not valid anymore.
+ */
+ final Store store() throws DataStoreException {
+ final Store store = this.store;
+ if (store != null) {
+ return store;
+ }
+ throw new
DataStoreException(Resources.format(Resources.Keys.ResourceRemoved));
+ }
+
/**
* 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.
*/
@Override
- public Optional<GenericName> getIdentifier() throws DataStoreException {
+ public final Optional<GenericName> getIdentifier() throws
DataStoreException {
+ final Store store = store();
synchronized (store) {
if (identifier == null) {
identifier = Names.createLocalName(store.getDisplayName(),
null, String.valueOf(imageIndex + 1));
@@ -147,6 +162,7 @@ class Image extends AbstractGridCoverageResource implements
StoreResource {
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public final List<SampleDimension> getSampleDimensions() throws
DataStoreException {
+ final Store store = store();
synchronized (store) {
if (sampleDimensions == null) try {
final ImageReader reader = store.reader();
@@ -188,46 +204,47 @@ class Image extends AbstractGridCoverageResource
implements StoreResource {
*/
@Override
public final GridCoverage read(GridGeometry domain, int... range) throws
DataStoreException {
- synchronized (store) {
- final ImageReader reader = store.reader();
- final ImageReadParam param = reader.getDefaultReadParam();
- if (domain == null) {
- domain = gridGeometry;
- } else {
- final GridDerivation gd =
gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
- final GridExtent extent = gd.getIntersection();
- final int[] subsampling = gd.getSubsampling();
- final int[] offsets = gd.getSubsamplingOffsets();
- final int subX = subsampling[X_DIMENSION];
- final int subY = subsampling[Y_DIMENSION];
- final Rectangle region = new Rectangle(
- toIntExact(extent.getLow (X_DIMENSION)),
- toIntExact(extent.getLow (Y_DIMENSION)),
- toIntExact(extent.getSize(X_DIMENSION)),
- toIntExact(extent.getSize(Y_DIMENSION)));
- /*
- * Ths subsampling offset Δx is defined differently in Image
I/O and `GridGeometry`.
- * The conversion from coordinate x in subsampled image to xₒ
in original image is:
- *
- * Image I/O: xₒ = xᵣ + (x⋅s + Δx′)
- * GridGeometry: xₒ = (truncate(xᵣ/s) + x)⋅s + Δx
- *
- * Where xᵣ is the the lower coordinate of `region`, s is the
subsampling and
- * `truncate(xᵣ/s)` is given by the lower coordinate of
subsampled extent.
- * Rearranging equations:
- *
- * Δx′ = truncate(xᵣ/s)⋅s + Δx - xᵣ
- */
- domain = gd.build();
- GridExtent subExtent = domain.getExtent();
- param.setSourceRegion(region);
- param.setSourceSubsampling(subX, subY,
- toIntExact(subExtent.getLow(X_DIMENSION) * subX +
offsets[X_DIMENSION] - region.x),
- toIntExact(subExtent.getLow(Y_DIMENSION) * subY +
offsets[Y_DIMENSION] - region.y));
- }
- RenderedImage image;
- List<SampleDimension> sampleDimensions = getSampleDimensions();
- try {
+ RenderedImage image;
+ List<SampleDimension> bands;
+ final Store store = store();
+ try {
+ synchronized (store) {
+ final ImageReader reader = store.reader();
+ final ImageReadParam param = reader.getDefaultReadParam();
+ if (domain == null) {
+ domain = gridGeometry;
+ } else {
+ final GridDerivation gd =
gridGeometry.derive().rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
+ final GridExtent extent = gd.getIntersection();
+ final int[] subsampling = gd.getSubsampling();
+ final int[] offsets = gd.getSubsamplingOffsets();
+ final int subX = subsampling[X_DIMENSION];
+ final int subY = subsampling[Y_DIMENSION];
+ final Rectangle region = new Rectangle(
+ toIntExact(extent.getLow (X_DIMENSION)),
+ toIntExact(extent.getLow (Y_DIMENSION)),
+ toIntExact(extent.getSize(X_DIMENSION)),
+ toIntExact(extent.getSize(Y_DIMENSION)));
+ /*
+ * Ths subsampling offset Δx is defined differently in
Image I/O and `GridGeometry`.
+ * The conversion from coordinate x in subsampled image to
xₒ in original image is:
+ *
+ * Image I/O: xₒ = xᵣ + (x⋅s + Δx′)
+ * GridGeometry: xₒ = (truncate(xᵣ/s) + x)⋅s + Δx
+ *
+ * Where xᵣ is the the lower coordinate of `region`, s is
the subsampling and
+ * `truncate(xᵣ/s)` is given by the lower coordinate of
subsampled extent.
+ * Rearranging equations:
+ *
+ * Δx′ = truncate(xᵣ/s)⋅s + Δx - xᵣ
+ */
+ domain = gd.build();
+ GridExtent subExtent = domain.getExtent();
+ param.setSourceRegion(region);
+ param.setSourceSubsampling(subX, subY,
+ toIntExact(subExtent.getLow(X_DIMENSION) * subX +
offsets[X_DIMENSION] - region.x),
+ toIntExact(subExtent.getLow(Y_DIMENSION) * subY +
offsets[Y_DIMENSION] - region.y));
+ }
/*
* If a subset of the bands is requested, ideally we should
forward this request to the `ImageReader`.
* But experience suggests that not all `ImageReader`
implementations support band subsetting well.
@@ -235,13 +252,14 @@ class Image extends AbstractGridCoverageResource
implements StoreResource {
* 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();
if (range != null) {
final ImageTypeSpecifier type =
reader.getRawImageType(imageIndex);
final RangeArgument args =
RangeArgument.validate(type.getNumBands(), range, listeners);
if (args.isIdentity()) {
range = null;
} else {
- sampleDimensions =
UnmodifiableArrayList.wrap(args.select(sampleDimensions));
+ bands = UnmodifiableArrayList.wrap(args.select(bands));
if (args.hasAllBands || type.getSampleModel()
instanceof BandedSampleModel) {
range = args.getSelectedBands();
param.setSourceBands(range);
@@ -251,18 +269,25 @@ class Image extends AbstractGridCoverageResource
implements StoreResource {
}
}
image = reader.readAsRenderedImage(imageIndex, param);
- } catch (IOException e) {
- throw new DataStoreException(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, sampleDimensions, image);
+ } 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);
+ }
+
+ /**
+ * Notifies this resource that it should not be used anymore.
+ */
+ final void dispose() {
+ store = null;
}
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
index c3ac161e47..9b4846d6d8 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/Store.java
@@ -19,13 +19,14 @@ package org.apache.sis.internal.storage.image;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.io.IOException;
import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardOpenOption;
import javax.imageio.ImageIO;
@@ -38,6 +39,7 @@ import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.Resource;
import org.apache.sis.storage.Aggregate;
import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.GridCoverageResource;
@@ -45,6 +47,7 @@ import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreClosedException;
import org.apache.sis.storage.DataStoreContentException;
import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.storage.ReadOnlyStorageException;
import org.apache.sis.storage.UnsupportedStorageException;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.storage.PRJDataStore;
@@ -69,7 +72,7 @@ import org.apache.sis.setup.OptionKey;
* @since 1.2
* @module
*/
-final class Store extends PRJDataStore implements Aggregate {
+class Store extends PRJDataStore implements Aggregate {
/**
* Image I/O format names (ignoring case) for which we have an entry in
the {@code SpatialMetadata} database.
*/
@@ -85,7 +88,7 @@ final class Store extends PRJDataStore implements Aggregate {
* @see #width
* @see #height
*/
- private static final int MAIN_IMAGE = 0;
+ static final int MAIN_IMAGE = 0;
/**
* The default World File suffix when it can not be determined from {@link
#location}.
@@ -93,14 +96,31 @@ final class Store extends PRJDataStore implements Aggregate
{
*/
private static final String DEFAULT_SUFFIX = "wld";
+ /**
+ * The "cell center" versus "cell corner" interpretation of translation
coefficients.
+ * The ESRI specification said that the coefficients map to pixel center.
+ */
+ static final PixelInCell CELL_ANCHOR = PixelInCell.CELL_CENTER;
+
/**
* The filename extension (may be an empty string), or {@code null} if
unknown.
* It does not include the leading dot.
*/
- private final String suffix;
+ final String suffix;
+
+ /**
+ * The filename extension for the auxiliary "world file".
+ * For the TIFF format, this is typically {@code "tfw"}.
+ * This is computed as a side-effect of {@link #readWorldFile()}.
+ */
+ private String suffixWLD;
/**
- * The image reader, set by the constructor and cleared when no longer
needed.
+ * The image reader, set by the constructor and cleared when the store is
closed.
+ * May also be null if the store is initially write-only, in which case a
reader
+ * may be created the first time than an image is read.
+ *
+ * @see #reader()
*/
private ImageReader reader;
@@ -130,7 +150,7 @@ final class Store extends PRJDataStore implements Aggregate
{
*
* @see #components()
*/
- private List<Image> components;
+ private Components components;
/**
* The metadata object, or {@code null} if not yet created.
@@ -144,21 +164,30 @@ final class Store extends PRJDataStore implements
Aggregate {
*
* @param provider the factory that created this {@code DataStore}
instance, or {@code null} if unspecified.
* @param connector information about the storage (URL, stream,
<i>etc</i>).
+ * @param readOnly whether to fail if the channel can not be opened at
least in read mode.
* @throws DataStoreException if an error occurred while opening the
stream.
* @throws IOException if an error occurred while creating the image
reader instance.
*/
- public Store(final StoreProvider provider, final StorageConnector
connector)
+ Store(final StoreProvider provider, final StorageConnector connector,
final boolean readOnly)
throws DataStoreException, IOException
{
super(provider, connector);
- final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
final Object storage = connector.getStorage();
suffix = IOUtilities.extension(storage);
+ if (!(readOnly || fileExists(connector))) {
+ /*
+ * If the store is opened in read-write mode, create the image
reader only
+ * if the file exists and is non-empty. Otherwise we let `reader`
to null
+ * and the caller will create an image writer instead.
+ */
+ return;
+ }
/*
* Search for a reader that claim to be able to read the storage input.
* First we try readers associated to the file suffix. If no reader is
* found, we try all other readers.
*/
+ final Map<ImageReaderSpi,Boolean> deferred = new LinkedHashMap<>();
if (suffix != null) {
reader = FormatFilter.SUFFIX.createReader(suffix, connector,
deferred);
}
@@ -174,14 +203,20 @@ fallback: if (reader == null) {
for (final Map.Entry<ImageReaderSpi,Boolean> entry :
deferred.entrySet()) {
if (entry.getValue()) {
if (stream == null) {
- stream = ImageIO.createImageInputStream(storage);
- if (stream == null) break;
+ if (!readOnly) {
+ // ImageOutputStream is both read and write.
+ stream =
ImageIO.createImageOutputStream(storage);
+ }
+ if (stream == null) {
+ stream =
ImageIO.createImageInputStream(storage);
+ if (stream == null) break;
+ }
}
final ImageReaderSpi p = entry.getKey();
if (p.canDecodeInput(stream)) {
connector.closeAllExcept(storage);
reader = p.createReaderInstance();
- reader.setInput(stream, false, true);
+ reader.setInput(stream);
break fallback;
}
}
@@ -190,15 +225,43 @@ fallback: if (reader == null) {
storage,
connector.getOption(OptionKey.OPEN_OPTIONS));
}
}
+ configureReader();
/*
- * Sets the locale to use for warning messages, if supported. If the
reader
- * does not support the locale, the reader's default locale will be
used.
+ * Do not invoke any method that may cause the image reader to start
reading the stream,
+ * because the `WritableStore` subclass will want to save the initial
stream position.
*/
+ }
+
+ /**
+ * Sets the locale to use for warning messages, if supported. If the reader
+ * does not support the locale, the reader's default locale will be used.
+ */
+ private void configureReader() {
try {
reader.setLocale(listeners.getLocale());
} catch (IllegalArgumentException e) {
// Ignore
}
+ reader.addIIOReadWarningListener(new WarningListener(listeners));
+ }
+
+ /**
+ * Returns {@code true} if the image file exists and is non-empty.
+ * This is used for checking if an {@link ImageReader} should be created.
+ * If the file is going to be truncated, then it is considered already
empty.
+ *
+ * @param connector the connector to use for opening the file.
+ * @return whether the image file exists and is non-empty.
+ */
+ private boolean fileExists(final StorageConnector connector) throws
DataStoreException, IOException {
+ if (!ArraysExt.contains(connector.getOption(OptionKey.OPEN_OPTIONS),
StandardOpenOption.TRUNCATE_EXISTING)) {
+ for (Path path : super.getComponentFiles()) {
+ if (Files.isRegularFile(path) && Files.size(path) > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
}
/**
@@ -267,7 +330,7 @@ loop: for (int convention=0;; convention++) {
}
}
if (warning != null) {
-
listeners.warning(Resources.format(Resources.Keys.CanNotReadAuxiliaryFile_1,
preferred), warning);
+
listeners.warning(resources().getString(Resources.Keys.CanNotReadAuxiliaryFile_1,
preferred), warning);
}
return null;
}
@@ -281,11 +344,12 @@ loop: for (int convention=0;; convention++) {
* @throws DataStoreException if the file content can not be parsed.
*/
private AffineTransform2D readWorldFile(final String wld) throws
IOException, DataStoreException {
- final AuxiliaryContent content = readAuxiliaryFile(wld, encoding);
- final CharSequence[] lines =
CharSequences.splitOnEOL(readAuxiliaryFile(wld, encoding));
- int count = 0;
- final int expected = 6; // Expected number of
elements.
- final double[] elements = new double[expected];
+ final AuxiliaryContent content = readAuxiliaryFile(wld, encoding);
+ final String filename = content.getFilename();
+ final CharSequence[] lines =
CharSequences.splitOnEOL(readAuxiliaryFile(wld, encoding));
+ final int expected = 6; // Expected number of
elements.
+ int count = 0; // Actual number of
elements.
+ final double[] elements = new double[expected];
for (int i=0; i<expected; i++) {
final String line = lines[i].toString().trim();
if (!line.isEmpty() && line.charAt(0) != '#') {
@@ -295,16 +359,29 @@ loop: for (int convention=0;; convention++) {
try {
elements[count++] = Double.parseDouble(line);
} catch (NumberFormatException e) {
- throw new
DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2,
content.getFilename(), i), e);
+ throw new
DataStoreContentException(errors().getString(Errors.Keys.ErrorInFileAtLine_2,
filename, i), e);
}
}
}
if (count != expected) {
- throw new
EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1,
content.getFilename()));
+ throw new
EOFException(errors().getString(Errors.Keys.UnexpectedEndOfFile_1, filename));
+ }
+ if (filename != null) {
+ final int s = filename.lastIndexOf('.');
+ if (s >= 0) {
+ suffixWLD = filename.substring(s+1);
+ }
}
return new AffineTransform2D(elements);
}
+ /**
+ * Returns the localized resources for producing warnings or error
messages.
+ */
+ final Resources resources() {
+ return Resources.forLocale(listeners.getLocale());
+ }
+
/**
* Returns the localized resources for producing error messages.
*/
@@ -312,6 +389,22 @@ loop: for (int convention=0;; convention++) {
return Errors.getResources(listeners.getLocale());
}
+ /**
+ * Returns paths to the main file together with auxiliary files.
+ *
+ * @return paths to the main file and auxiliary files, or an empty array
if unknown.
+ * @throws DataStoreException if the URI can not be converted to a {@link
Path}.
+ */
+ @Override
+ public final synchronized Path[] getComponentFiles() throws
DataStoreException {
+ if (suffixWLD == null) try {
+ getGridGeometry(MAIN_IMAGE); // Will compute
`suffixWLD` as a side effect.
+ } catch (IOException e) {
+ throw new DataStoreException(e);
+ }
+ return listComponentFiles(suffixWLD, PRJ); // `suffixWLD` still
null if file was not found.
+ }
+
/**
* Gets the grid geometry for image at the given index.
* This method should be invoked only once per image, and the result
cached.
@@ -322,7 +415,7 @@ loop: for (int convention=0;; convention++) {
* @throws IOException if an I/O error occurred.
* @throws DataStoreException if the {@code *.prj} or {@code *.tfw}
auxiliary file content can not be parsed.
*/
- private GridGeometry getGridGeometry(final int index) throws IOException,
DataStoreException {
+ final GridGeometry getGridGeometry(final int index) throws IOException,
DataStoreException {
assert Thread.holdsLock(this);
final ImageReader reader = reader();
if (gridGeometry == null) {
@@ -331,23 +424,48 @@ loop: for (int convention=0;; convention++) {
height = reader.getHeight(MAIN_IMAGE);
gridToCRS = readWorldFile();
readPRJ();
- gridGeometry = new GridGeometry(new GridExtent(width, height),
PixelInCell.CELL_CENTER, gridToCRS, crs);
+ gridGeometry = new GridGeometry(new GridExtent(width, height),
CELL_ANCHOR, gridToCRS, crs);
}
if (index != MAIN_IMAGE) {
final int w = reader.getWidth (index);
final int h = reader.getHeight(index);
if (w != width || h != height) {
- return new GridGeometry(new GridExtent(w, h),
PixelInCell.CELL_CENTER, null, null);
+ // Can not use `gridToCRS` and `crs` because they may not
apply.
+ return new GridGeometry(new GridExtent(w, h), CELL_ANCHOR,
null, null);
}
}
return gridGeometry;
}
+ /**
+ * Sets the store-wide grid geometry when a new coverage is written. The
{@link WritableStore} implementation
+ * is responsible for making sure that the new grid geometry is compatible
with preexisting grid geometry.
+ *
+ * @param index index of the image for which to set the grid geometry.
+ * @param gg the new grid geometry.
+ * @return suffix of the "world file", or {@code null} if the image can
not be written.
+ */
+ String setGridGeometry(final int index, final GridGeometry gg) throws
IOException, DataStoreException {
+ if (index != MAIN_IMAGE) {
+ return null;
+ }
+ final GridExtent extent = gg.getExtent();
+ final int w = Math.toIntExact(extent.getSize(Image.X_DIMENSION));
+ final int h = Math.toIntExact(extent.getSize(Image.Y_DIMENSION));
+ final String s = (suffixWLD != null) ? suffixWLD :
getWorldFileSuffix();
+ crs = gg.isDefined(GridGeometry.CRS) ?
gg.getCoordinateReferenceSystem() : null;
+ gridGeometry = gg; // Set only after success of all
the above.
+ width = w;
+ height = h;
+ suffixWLD = s;
+ return s;
+ }
+
/**
* Returns information about the data store as a whole.
*/
@Override
- public synchronized Metadata getMetadata() throws DataStoreException {
+ public final synchronized Metadata getMetadata() throws DataStoreException
{
if (metadata == null) try {
final MetadataBuilder builder = new MetadataBuilder();
String format = reader().getFormatName();
@@ -381,40 +499,58 @@ loop: for (int convention=0;; convention++) {
/**
* Returns all images in this store. Note that fetching the size of the
list is a potentially costly operation.
+ *
+ * @return list of images in this store.
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public final synchronized Collection<? extends GridCoverageResource>
components() throws DataStoreException {
if (components == null) try {
- components = new Components();
+ components = new Components(reader().getNumImages(false));
} catch (IOException e) {
throw new DataStoreException(e);
}
return components;
}
+ /**
+ * Returns all images in this store, or {@code null} if none and {@code
create} is false.
+ *
+ * @param create whether to create the component list if it was not
already created.
+ * @param numImages number of images, or any negative value if unknown.
+ */
+ @SuppressWarnings("ReturnOfCollectionOrArrayField")
+ final Components components(final boolean create, final int numImages) {
+ if (components == null && create) {
+ components = new Components(numImages);
+ }
+ return components;
+ }
+
/**
* A list of images where each {@link Image} instance is initialized when
first needed.
* Fetching the list size may be a costly operation and will be done only
if requested.
*/
- private final class Components extends ListOfUnknownSize<Image> {
+ final class Components extends ListOfUnknownSize<Image> {
/**
- * Size of this list, or -1 if unknown.
+ * Size of this list, or any negative value if unknown.
*/
private int size;
/**
- * All elements in this list. Some array element may be {@code null}
if the image
- * as never been requested.
+ * All elements in this list. Some array elements may be {@code null}
if the image
+ * has never been requested.
*/
private Image[] images;
/**
* Creates a new list of images.
+ *
+ * @param numImages number of images, or any negative value if
unknown.
*/
- private Components() throws DataStoreException, IOException {
- size = reader().getNumImages(false);
- images = new Image[size >= 0 ? size : 1];
+ private Components(final int numImages) {
+ size = numImages;
+ images = new Image[Math.max(numImages, 1)];
}
/**
@@ -437,7 +573,7 @@ loop: for (int convention=0;; convention++) {
}
/**
- * Returns the number of images if this information is known, or -1
otherwise.
+ * Returns the number of images if this information is known, or any
negative value otherwise.
* This is used by {@link ListOfUnknownSize} for optimizing some
operations.
*/
@Override
@@ -457,12 +593,20 @@ loop: for (int convention=0;; convention++) {
if (size >= 0) {
return index >= 0 && index < size;
}
- return get(index) != null;
+ try {
+ return get(index) != null;
+ } catch (IndexOutOfBoundsException e) {
+ return false;
+ }
}
}
/**
* Returns the image at the given index. New instances are created
when first requested.
+ *
+ * @param index index of the image for which to get a resource.
+ * @return resource for the image identified by the given index.
+ * @throws IndexOutOfBoundsException if the image index is out of
bounds.
*/
@Override
public Image get(final int index) {
@@ -472,7 +616,7 @@ loop: for (int convention=0;; convention++) {
image = images[index];
}
if (image == null) try {
- image = new Image(Store.this, listeners, index,
getGridGeometry(index));
+ image = createImageResource(index);
if (index >= images.length) {
images = Arrays.copyOf(images, Math.max(images.length
* 2, index + 1));
}
@@ -485,17 +629,113 @@ loop: for (int convention=0;; convention++) {
return image;
}
}
+
+ /**
+ * Invoked <em>after</em> an image has been added to the image file.
+ * This method adds in this list a reference to the newly added file.
+ *
+ * @param image the image to add to this list.
+ */
+ final void added(final Image image) {
+ size = image.imageIndex;
+ if (size >= images.length) {
+ images = Arrays.copyOf(images, size * 2);
+ }
+ images[size++] = image;
+ }
+
+ /**
+ * Invoked <em>after</em> an image has been removed from the image
file.
+ * This method performs no bounds check (it must be done by the
caller).
+ *
+ * @param index index of the image that has been removed.
+ */
+ final void removed(int index) {
+ final int last = images.length - 1;
+ System.arraycopy(images, index+1, images, index, last - index);
+ images[last] = null;
+ size--;
+ while (index < last) {
+ final Image image = images[index++];
+ if (image != null) image.imageIndex--;
+ }
+ }
+
+ /**
+ * Removes the element at the specified position in this list.
+ */
+ @Override
+ public Image remove(final int index) {
+ final Image image = get(index);
+ try {
+ Store.this.remove(image);
+ } catch (DataStoreException e) {
+ throw new UnsupportedOperationException(e);
+ }
+ return image;
+ }
+ }
+
+ /**
+ * Invoked by {@link Components} when the caller want to remove a resource.
+ * The actual implementation is provided by {@link WritableStore}.
+ */
+ void remove(final Resource resource) throws DataStoreException {
+ throw new ReadOnlyStorageException();
+ }
+
+ /**
+ * Creates a {@link GridCoverageResource} for the specified image.
+ * This method is invoked by {@link Components} when first needed
+ * and the result is cached by the caller.
+ *
+ * @param index index of the image for which to create a resource.
+ * @return resource for the image identified by the given index.
+ * @throws IndexOutOfBoundsException if the image index is out of bounds.
+ */
+ Image createImageResource(final int index) throws DataStoreException,
IOException {
+ return new Image(this, listeners, index, getGridGeometry(index));
+ }
+
+ /**
+ * Prepares an image reader compatible with the writer and sets its input.
+ * This method is invoked for switching from write mode to read mode.
+ * Its actual implementation is provided by {@link WritableImage}.
+ *
+ * @param current the current image reader, or {@code null} if none.
+ * @return the image reader to use, or {@code null} if none.
+ * @throws IOException if an error occurred while preparing the reader.
+ */
+ ImageReader prepareReader(ImageReader current) throws IOException {
+ return null;
+ }
+
+ /**
+ * Returns the reader without doing any validation. The reader may be
{@code null} either
+ * because the store is closed or because the store is initially opened in
write-only mode.
+ * The reader may have a {@code null} input.
+ */
+ final ImageReader getCurrentReader() {
+ return reader;
}
/**
* Returns the reader if it has not been closed.
+ *
+ * @throws DataStoreClosedException if this data store is closed.
+ * @throws IOException if an error occurred while preparing the reader.
*/
- final ImageReader reader() throws DataStoreException {
- final ImageReader in = reader;
- if (in == null) {
- throw new DataStoreClosedException(getLocale(),
StoreProvider.NAME, StandardOpenOption.READ);
+ final ImageReader reader() throws DataStoreException, IOException {
+ assert Thread.holdsLock(this);
+ ImageReader current = reader;
+ if (current == null || current.getInput() == null) {
+ reader = current = prepareReader(current);
+ if (current == null) {
+ throw new DataStoreClosedException(getLocale(),
StoreProvider.NAME, StandardOpenOption.READ);
+ }
+ configureReader();
}
- return in;
+ return current;
}
/**
@@ -505,12 +745,15 @@ loop: for (int convention=0;; convention++) {
*/
@Override
public synchronized void close() throws DataStoreException {
- final ImageReader r = reader;
- reader = null;
- if (r != null) try {
- final Object input = r.getInput();
- r.setInput(null);
- r.dispose();
+ final ImageReader codec = reader;
+ reader = null;
+ metadata = null;
+ components = null;
+ gridGeometry = null;
+ if (codec != null) try {
+ final Object input = codec.getInput();
+ codec.setInput(null);
+ codec.dispose();
if (input instanceof AutoCloseable) {
((AutoCloseable) input).close();
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
index 435264a72d..d987c2ca45 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/StoreProvider.java
@@ -27,6 +27,7 @@ import org.apache.sis.internal.storage.Capability;
import org.apache.sis.internal.storage.StoreMetadata;
import org.apache.sis.internal.storage.PRJDataStore;
import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.storage.GridCoverageResource;
import org.apache.sis.storage.ProbeResult;
@@ -38,8 +39,10 @@ import org.apache.sis.storage.ProbeResult;
* @since 1.2
* @module
*/
-@StoreMetadata(formatName = StoreProvider.NAME,
- capabilities = Capability.READ)
+@StoreMetadata(formatName = StoreProvider.NAME,
+ fileSuffixes = {"jpeg", "jpg", "png", "gif", "bmp"}, //
Non-exhaustive list.
+ capabilities = {Capability.READ, Capability.WRITE,
Capability.CREATE},
+ resourceTypes = GridCoverageResource.class)
public final class StoreProvider extends PRJDataStore.Provider {
/**
* The format name.
@@ -72,7 +75,11 @@ public final class StoreProvider extends
PRJDataStore.Provider {
@Override
public DataStore open(final StorageConnector connector) throws
DataStoreException {
try {
- return new Store(this, connector);
+ if (isWritable(connector)) {
+ return new WritableStore(this, connector);
+ } else {
+ return new Store(this, connector, true);
+ }
} catch (IOException e) {
throw new DataStoreException(e);
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
index fd0e389270..c879d00a06 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WarningListener.java
@@ -17,7 +17,9 @@
package org.apache.sis.internal.storage.image;
import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
import javax.imageio.event.IIOReadWarningListener;
+import javax.imageio.event.IIOWriteWarningListener;
import org.apache.sis.storage.event.StoreListeners;
@@ -30,7 +32,7 @@ import org.apache.sis.storage.event.StoreListeners;
* @since 1.2
* @module
*/
-final class WarningListener implements IIOReadWarningListener {
+final class WarningListener implements IIOReadWarningListener,
IIOWriteWarningListener {
/**
* The set of registered {@link StoreListener}s for the data store.
*/
@@ -50,7 +52,19 @@ final class WarningListener implements
IIOReadWarningListener {
* @param message the warning.
*/
@Override
- public void warningOccurred(final ImageReader reader, final String
message) {
+ public void warningOccurred(final ImageReader source, final String
message) {
+ listeners.warning(message);
+ }
+
+ /**
+ * Reports a non-fatal error in encoding.
+ *
+ * @param source the writer calling this method.
+ * @param imageIndex index of the image being written.
+ * @param message the warning.
+ */
+ @Override
+ public void warningOccurred(final ImageWriter source, final int
imageIndex, final String message) {
listeners.warning(message);
}
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableImage.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableImage.java
new file mode 100644
index 0000000000..fcc92c97da
--- /dev/null
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableImage.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.image;
+
+import java.io.IOException;
+import java.awt.image.RenderedImage;
+import javax.imageio.ImageWriter;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.WritableGridCoverageResource;
+import org.apache.sis.internal.storage.WritableResourceSupport;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.storage.event.StoreListeners;
+
+
+/**
+ * An image which can be replaced or updated.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since 1.2
+ * @module
+ */
+final class WritableImage extends Image implements
WritableGridCoverageResource {
+ /**
+ * Creates a new resource.
+ */
+ WritableImage(final WritableStore store, final StoreListeners parent,
final int imageIndex,
+ final GridGeometry gridGeometry) throws DataStoreException
+ {
+ super(store, parent, imageIndex, gridGeometry);
+ }
+
+ /**
+ * Writes a new coverage in the data store for this resource. If a
coverage already exists for this resource,
+ * then it will be overwritten only if the {@code TRUNCATE} or {@code
UPDATE} option is specified.
+ *
+ * @param coverage new data to write in the data store for this resource.
+ * @param options configuration of the write operation.
+ * @throws DataStoreException if an error occurred while writing data in
the underlying data store.
+ */
+ @Override
+ public void write(GridCoverage coverage, final Option... options) throws
DataStoreException {
+ final WritableResourceSupport h = new WritableResourceSupport(this,
options); // Does argument validation.
+ final WritableStore store = (WritableStore) store();
+ try {
+ synchronized (store) {
+ if (imageIndex != Store.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(…)`.
+ writer.write(data);
+ }
+ } catch (IOException | RuntimeException e) {
+ throw new
DataStoreException(store.resources().getString(Resources.Keys.CanNotWriteResource_1,
store.getDisplayName()), 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
new file mode 100644
index 0000000000..986105f849
--- /dev/null
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/image/WritableStore.java
@@ -0,0 +1,515 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.storage.image;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.util.function.BiConsumer;
+import java.io.File;
+import java.io.IOException;
+import java.io.BufferedWriter;
+import java.nio.file.StandardOpenOption;
+import java.awt.geom.AffineTransform;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.ImageWriter;
+import javax.imageio.spi.IIORegistry;
+import javax.imageio.spi.ImageReaderSpi;
+import javax.imageio.spi.ImageWriterSpi;
+import javax.imageio.spi.ImageReaderWriterSpi;
+import javax.imageio.stream.ImageInputStream;
+import javax.imageio.stream.ImageOutputStream;
+import javax.imageio.stream.FileImageOutputStream;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.internal.storage.Resources;
+import org.apache.sis.storage.Resource;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.WritableAggregate;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreClosedException;
+import org.apache.sis.storage.UnsupportedStorageException;
+import org.apache.sis.storage.IncompatibleResourceException;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.setup.OptionKey;
+
+
+/**
+ * A data store with writing capabilities.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since 1.2
+ * @module
+ */
+final class WritableStore extends Store implements WritableAggregate {
+ /**
+ * Position of the input/output stream beginning. This is usually 0.
+ */
+ private final long streamBeginning;
+
+ /**
+ * The image writer, created when first needed and cleared when the store
is closed.
+ * Only one of {@link #reader} and {@link #writer} should have its input
or output set
+ * at a given time.
+ *
+ * @see #writer()
+ */
+ private ImageWriter writer;
+
+ /**
+ * Number of images in this store, or any negative value if unknown. This
information is redundant
+ * with {@link ImageReader#getNumImages(boolean)} but is stored here
because {@link #reader} may be
+ * null and {@link ImageWriter} does not have a {@code getNumImages(…)}
method.
+ *
+ * @see #isMultiImages()
+ */
+ private int numImages;
+
+ /**
+ * Creates a new store from the given file, URL or stream.
+ *
+ * @param provider the factory that created this {@code DataStore}
instance, or {@code null} if unspecified.
+ * @param connector information about the storage (URL, stream,
<i>etc</i>).
+ * @throws DataStoreException if an error occurred while opening the
stream.
+ * @throws IOException if an error occurred while creating the image
reader instance.
+ */
+ WritableStore(final StoreProvider provider, final StorageConnector
connector)
+ throws DataStoreException, IOException
+ {
+ super(provider, connector, false);
+ final ImageReader reader = getCurrentReader();
+ final Object inout;
+ if (reader != null) {
+ inout = reader.getInput();
+ numImages = -1;
+ } else {
+ /*
+ * If it was possible to initialize an image reader, wait to see
if an image writer is needed.
+ * Otherwise (i.e. if the destination file does not exist), create
the image writer immediately.
+ * The code below is a copy of the code in parent class
constructor (for creating `ImageReader`),
+ * but adapted to the case of creating an `ImageWriter`.
+ */
+ final Map<ImageWriterSpi,Boolean> deferred = new LinkedHashMap<>();
+ if (suffix != null) {
+ writer = FormatFilter.SUFFIX.createWriter(suffix, connector,
null, deferred);
+ }
+ if (writer == null) {
+ writer = FormatFilter.SUFFIX.createWriter(null, connector,
null, deferred);
+fallback: if (writer == null) {
+ ImageOutputStream stream = null;
+ final Object storage = connector.getStorage();
+ for (final Map.Entry<ImageWriterSpi,Boolean> entry :
deferred.entrySet()) {
+ if (entry.getValue()) {
+ if (stream == null) {
+ final File file =
connector.getStorageAs(File.class);
+ if (file != null) {
+ stream = new FileImageOutputStream(file);
+ } else {
+ stream =
ImageIO.createImageOutputStream(storage);
+ if (stream == null) break;
+ }
+ }
+ final ImageWriterSpi p = entry.getKey();
+ connector.closeAllExcept(storage);
+ writer = p.createWriterInstance();
+ writer.setOutput(stream);
+ break fallback;
+ }
+ }
+ throw new UnsupportedStorageException(super.getLocale(),
StoreProvider.NAME,
+ storage,
connector.getOption(OptionKey.OPEN_OPTIONS));
+ }
+ }
+ configureWriter();
+ inout = writer.getOutput();
+ // Leave `numImages` to 0 because we know that the stream is empty.
+ }
+ streamBeginning = (inout instanceof ImageInputStream) ?
((ImageInputStream) inout).getStreamPosition() : 0;
+ }
+
+ /**
+ * Sets the locale to use for warning messages, if supported. If the writer
+ * does not support the locale, the writer's default locale will be used.
+ */
+ private void configureWriter() {
+ try {
+ writer.setLocale(listeners.getLocale());
+ } catch (IllegalArgumentException e) {
+ // Ignore
+ }
+ writer.addIIOWriteWarningListener(new WarningListener(listeners));
+ }
+
+ /**
+ * Returns whether this data store contains more than one image.
+ * This is used for deciding if {@link WritableStore} can overwrite a grid
geometry.
+ *
+ * @return 0 if this store is empty, 1 if it contains exactly one image,
+ * or a value greater than 1 if it contains more than one image.
+ * The returned value is not necessarily the number of images.
+ * @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);
+ if (components.isEmpty()) {
+ numImages = 0;
+ } else if (components.exists(1)) {
+ return 2;
+ } else {
+ numImages = 1;
+ }
+ }
+ return numImages;
+ }
+
+ /**
+ * Sets the store-wide grid geometry. Only one grid geometry can be set
for a data store.
+ * If a grid geometry already exists and the specified grid geometry is
incompatible,
+ * then an {@link IncompatibleResourceException} is thrown.
+ *
+ * <p>This method may use the {@link ImageReader} for checking the number
of images,
+ * so it is better to invoke this method before {@link #writer()}.</p>
+ *
+ * @param index index of the image for which to read the grid geometry.
+ * @param gg the new grid geometry.
+ * @return suffix of the "world file", or {@code null} if this method
wrote nothing.
+ * @throws IncompatibleResourceException if the "grid to CRS" is not
affine,
+ * or if a different grid geometry already exists.
+ *
+ * @see #getGridGeometry(int)
+ */
+ @Override
+ String setGridGeometry(final int index, GridGeometry gg) throws
IOException, DataStoreException {
+ /*
+ * Make sure that the grid geometry starts at (0,0).
+ * Must be done before to compare with existing grid.
+ */
+ final GridExtent extent = gg.getExtent();
+ final long[] translation = new long[extent.getDimension()];
+ for (int i=0; i<translation.length; i++) {
+ translation[i] = Math.negateExact(extent.getLow(i));
+ }
+ gg = gg.translate(translation);
+ /*
+ * If the data store already contains a coverage, then the given grid
geometry
+ * must be identical to the existing one, in which case there is
nothing to do.
+ */
+ if (index != MAIN_IMAGE || isMultiImages() > 1) {
+ if (!getGridGeometry(MAIN_IMAGE).equals(gg,
ComparisonMode.IGNORE_METADATA)) {
+ throw new IncompatibleResourceException(
+
resources().getString(Resources.Keys.IncompatibleGridGeometry));
+ }
+ }
+ /*
+ * Get the two-dimensional affine transform (it provides the "World
file" content).
+ * Only after we successfully got all the information, assign the grid
geometry to
+ * this store.
+ */
+ AffineTransform gridToCRS = null;
+ if (gg.isDefined(GridGeometry.GRID_TO_CRS)) try {
+ gridToCRS =
AffineTransforms2D.castOrCopy(gg.getGridToCRS(CELL_ANCHOR));
+ } catch (IllegalArgumentException e) {
+ throw new IncompatibleResourceException(e.getLocalizedMessage(),
e);
+ }
+ final String suffix = super.setGridGeometry(index, gg); // May
throw `ArithmeticException`.
+ /*
+ * If the image is the main one, overwrite (possibly with same
content) the previous auxiliary files.
+ * Otherwise above checks should have ensured that the existing
auxiliary files are applicable.
+ */
+ if (suffix != null) {
+ if (gridToCRS == null) {
+ deleteAuxiliaryFile(suffix);
+ } else try (BufferedWriter out = writeAuxiliaryFile(suffix,
encoding)) {
+writeCoeffs: for (int i=0;; i++) {
+ final double c;
+ switch (i) {
+ case 0: c = gridToCRS.getScaleX(); break;
+ case 1: c = gridToCRS.getShearY(); break;
+ case 2: c = gridToCRS.getShearX(); break;
+ case 3: c = gridToCRS.getScaleY(); break;
+ case 4: c = gridToCRS.getTranslateX(); break;
+ case 5: c = gridToCRS.getTranslateY(); break;
+ default: break writeCoeffs;
+ }
+ out.write(Double.toString(c));
+ out.newLine();
+ }
+ }
+ writePRJ();
+ }
+ return suffix;
+ }
+
+ /**
+ * Creates a {@link GridCoverageResource} for the specified image.
+ * This method is invoked by {@link Components} when first needed
+ * and the result is cached by the caller.
+ *
+ * @param index index of the image for which to create a resource.
+ * @return resource for the image identified by the given index.
+ * @throws IndexOutOfBoundsException if the image index is out of bounds.
+ */
+ @Override
+ Image createImageResource(final int index) throws DataStoreException,
IOException {
+ return new WritableImage(this, listeners, index,
getGridGeometry(index));
+ }
+
+ /**
+ * Adds a new {@code Resource} in this {@code Aggregate}.
+ * The given {@link Resource} will be copied, and the <cite>effectively
added</cite> resource returned.
+ *
+ * @param resource the resource to copy in this {@code Aggregate}.
+ * @return the effectively added resource.
+ * @throws DataStoreException if the given resource can not be stored in
this {@code Aggregate}.
+ */
+ @Override
+ public synchronized Resource add(final Resource resource) throws
DataStoreException {
+ Exception cause = null;
+ if (resource instanceof GridCoverageResource) try {
+ final Components components = components(true, numImages);
+ if (numImages < 0) {
+ numImages = components.size(); // For this method, we
need an accurate count.
+ }
+ /*
+ * If we are adding the first image, the grid geometry of the
coverage will determine
+ * the new grid geometry of the data store. Otherwise (if we are
adding more images)
+ * the coverage grid geometry must be the same as the current data
store grid geometry.
+ */
+ GridGeometry domain = null;
+ if (numImages != 0) {
+ domain = getGridGeometry(MAIN_IMAGE);
+ }
+ final GridCoverage coverage = ((GridCoverageResource)
resource).read(domain, null);
+ if (domain == null) {
+ domain = coverage.getGridGeometry(); // We are adding
the first image.
+ }
+ final WritableImage image = new WritableImage(this, listeners,
numImages, domain);
+ image.write(coverage);
+ components.added(image); // Must be invoked only after
above succeeded.
+ numImages++;
+ return image;
+ } catch (IOException | RuntimeException e) {
+ cause = e;
+ }
+ throw new
DataStoreException(resources().getString(Resources.Keys.CanNotWriteResource_1,
label(resource)), cause);
+ }
+
+ /**
+ * Removes a {@code Resource} from this {@code Aggregate}.
+ * The given resource should be one of the instances returned by {@link
#components()}.
+ *
+ * @param resource child resource to remove from this {@code Aggregate}.
+ * @throws DataStoreException if the given resource could not be removed.
+ */
+ @Override
+ public synchronized void remove(final Resource resource) throws
DataStoreException {
+ Exception cause = null;
+ if (resource instanceof WritableImage) {
+ final WritableImage image = (WritableImage) resource;
+ if (image.store() == this) try {
+ final int imageIndex = image.imageIndex;
+ writer().removeImage(imageIndex);
+ final Components components = components(false, numImages);
+ if (components != null) {
+ components.removed(imageIndex);
+ image.dispose();
+ numImages--; // Okay if negative.
+ }
+ } catch (IOException | RuntimeException e) {
+ cause = e;
+ }
+ }
+ throw new DataStoreException(resources().getString(
+ Resources.Keys.CanNotRemoveResource_2, getDisplayName(),
label(resource)), cause);
+ }
+
+ /**
+ * Returns a label for the given resource in error messages.
+ */
+ private static String label(final Resource resource) throws
DataStoreException {
+ return resource.getIdentifier().map(Object::toString).orElse("?");
+ }
+
+ /**
+ * Prepares an image reader compatible with the writer and sets its input.
+ * This method is invoked for switching from write mode to read mode.
+ *
+ * @param current the current image reader, or {@code null} if none.
+ * @return the image reader to use, or {@code null} if none.
+ */
+ @Override
+ ImageReader prepareReader(ImageReader current) throws IOException {
+ final ImageWriter writer = this.writer;
+ if (writer != null) {
+ final Object output = writer.getOutput();
+ if (output != null) {
+ if (current == null) {
+ final ImageWriterSpi wp = writer.getOriginatingProvider();
+ if (wp != null) {
+ final ImageReaderSpi rp =
getProviderByClass(ImageReaderSpi.class, wp.getImageReaderSpiNames(), wp);
+ if (rp != null) {
+ current = rp.createReaderInstance();
+ }
+ }
+ }
+ if (current != null) {
+ writer.setOutput(null);
+ setStream(current, output, ImageReader::setInput);
+ return current;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the writer if it has not been closed.
+ * If the data store was in read mode, invoking this method switch to
write mode.
+ *
+ * @throws DataStoreClosedException if this data store is closed.
+ * @throws IOException if an error occurred while preparing the writer.
+ */
+ final ImageWriter writer() throws DataStoreException, IOException {
+ assert Thread.holdsLock(this);
+ ImageWriter current = writer;
+ if (current != null && current.getOutput() != null) {
+ return current;
+ }
+ final ImageReader reader = getCurrentReader();
+ if (reader != null) {
+ final Object input = reader.getInput();
+ if (input != null) {
+ if (current == null) {
+ final ImageReaderSpi rp = reader.getOriginatingProvider();
+ if (rp != null) {
+ final ImageWriterSpi wp =
getProviderByClass(ImageWriterSpi.class, rp.getImageWriterSpiNames(), rp);
+ if (wp != null) {
+ current = wp.createWriterInstance();
+ }
+ }
+ }
+ if (current != null) {
+ reader.setInput(null);
+ setStream(current, input, ImageWriter::setOutput);
+ writer = current;
+ configureWriter();
+ return current;
+ }
+ }
+ }
+ throw new DataStoreClosedException(getLocale(), StoreProvider.NAME,
StandardOpenOption.WRITE);
+ }
+
+ /**
+ * Sets the input or output stream on the given image reader or writer.
+ * If the operation fails, the stream is closed.
+ *
+ * @param <T> class of the {@code codec} argument.
+ * @param codec the {@link ImageReader} or {@link ImageWriter} on which
to set the stream.
+ * @param stream the input or output to set on the specified {@code
codec}.
+ * @param setter for calling the {@code setInput(Object)} or {@code
setOutput(Object)} method.
+ */
+ private <T> void setStream(final T codec, final Object stream, final
BiConsumer<T,Object> setter) throws IOException {
+ try {
+ /*
+ * `ImageOutputStream` extends `ImageInputStream`,
+ * so there is no need to check the output stream case.
+ */
+ if (stream instanceof ImageInputStream) {
+ ((ImageInputStream) stream).seek(streamBeginning);
+ }
+ setter.accept(codec, stream);
+ } catch (Throwable exception) {
+ if (stream instanceof AutoCloseable) try {
+ ((AutoCloseable) stream).close();
+ } catch (Throwable s) {
+ exception.addSuppressed(s);
+ }
+ throw exception;
+ }
+ }
+
+ /**
+ * Returns the first service provider that we can get from the given list
of class names.
+ *
+ * @param <T> compile-time value of {@code type} argument.
+ * @param type type of the provider to get.
+ * @param classNames class names of provider implementations, or {@code
null} if none.
+ * @param originating the originating provider, used for fetching the
class loader.
+ * @return first provider found, or {@code null} if none.
+ */
+ private <T extends ImageReaderWriterSpi> T getProviderByClass(final
Class<T> type,
+ final String[] classNames, final ImageReaderWriterSpi
originating)
+ {
+ if (classNames != null) {
+ final IIORegistry registry = IIORegistry.getDefaultInstance();
+ final ClassLoader loader = originating.getClass().getClassLoader();
+ for (final String name : classNames) {
+ final Class<? extends T> impl;
+ try {
+ impl = Class.forName(name, true, loader).asSubclass(type);
+ } catch (ClassNotFoundException | ClassCastException e) {
+ listeners.warning(e);
+ continue;
+ }
+ final T candidate = registry.getServiceProviderByClass(impl);
+ if (candidate != null) {
+ return candidate;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Closes this data store and releases any underlying resources.
+ *
+ * @throws DataStoreException if an error occurred while closing this data
store.
+ */
+ @Override
+ public synchronized void close() throws DataStoreException {
+ try {
+ final ImageWriter codec = writer;
+ writer = null;
+ if (codec != null) try {
+ final Object output = codec.getOutput();
+ codec.setOutput(null);
+ codec.dispose();
+ if (output instanceof AutoCloseable) {
+ ((AutoCloseable) output).close();
+ }
+ } catch (Exception e) {
+ throw new DataStoreException(e);
+ }
+ } catch (Throwable e) {
+ try {
+ super.close();
+ } catch (Throwable s) {
+ e.addSuppressed(s);
+ }
+ throw e;
+ }
+ super.close();
+ }
+}
diff --git
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
index bf7eebff62..ea99ffe540 100644
---
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
+++
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/SelfConsistencyTest.java
@@ -55,7 +55,7 @@ public final strictfp class SelfConsistencyTest extends
CoverageReadConsistency
public static void openFile() throws IOException, DataStoreException {
final URL url = StoreTest.class.getResource("gradient.png");
assumeNotNull(url);
- store = new Store(null, new StorageConnector(url));
+ store = new Store(null, new StorageConnector(url), true);
}
/**
diff --git
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
index 42feef4317..d3f6c02ddb 100644
---
a/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
+++
b/storage/sis-storage/src/test/java/org/apache/sis/internal/storage/image/StoreTest.java
@@ -67,7 +67,7 @@ public final strictfp class StoreTest extends TestCase {
*/
@Test
public void testMetadata() throws DataStoreException, IOException {
- try (Store store = new Store(null, testData())) {
+ try (Store store = new Store(null, testData(), true)) {
assertEquals("gradient", store.getIdentifier().get().toString());
final Metadata metadata = store.getMetadata();
final Identification id =
getSingleton(metadata.getIdentificationInfo());