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 287923cbd5 Initial `WritableStore` for ASCII Grid format. A large part
of this commit is about providing some generic support methods, not only for
ASCII Grid format.
287923cbd5 is described below
commit 287923cbd5e311354fdbf37ec97adeae79df79f9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Fri Apr 8 20:05:39 2022 +0200
Initial `WritableStore` for ASCII Grid format.
A large part of this commit is about providing some generic support
methods, not only for ASCII Grid format.
---
.../java/org/apache/sis/image/ImageCombiner.java | 45 ++-
.../java/org/apache/sis/image/PixelIterator.java | 16 ++
.../sis/internal/coverage/CoverageCombiner.java | 307 +++++++++++++++++++++
.../org/apache/sis/internal/util/Numerics.java | 36 +++
.../org/apache/sis/internal/util/NumericsTest.java | 22 ++
.../apache/sis/internal/storage/PRJDataStore.java | 71 ++++-
.../org/apache/sis/internal/storage/Resources.java | 10 +
.../sis/internal/storage/Resources.properties | 2 +
.../sis/internal/storage/Resources_fr.properties | 2 +
.../apache/sis/internal/storage/URIDataStore.java | 15 +-
.../internal/storage/WritableResourceSupport.java | 225 +++++++++++++++
.../apache/sis/internal/storage/ascii/Store.java | 48 +++-
.../sis/internal/storage/ascii/StoreProvider.java | 6 +-
.../sis/internal/storage/ascii/WritableStore.java | 270 ++++++++++++++++++
.../sis/internal/storage/io/ChannelDataInput.java | 23 +-
.../sis/internal/storage/io/ChannelDataOutput.java | 4 +-
.../sis/storage/WritableGridCoverageResource.java | 34 +--
17 files changed, 1104 insertions(+), 32 deletions(-)
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
index 1ee69e78b9..22f4608df7 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/ImageCombiner.java
@@ -52,8 +52,17 @@ import org.apache.sis.measure.Units;
* In current implementation, the last pixel values win even if those pixels
are transparent
* (i.e. {@code ImageCombiner} does not yet handle alpha values).
*
+ * <h2>Limitations</h2>
+ * Current implementation does not try to map source bands to target bands for
the same colors.
+ * For example it does not verify if band order needs to be reversed because
an image is RGB and
+ * the other image is BVR. It is caller responsibility to ensure that bands
are in the same order.
+ *
+ * <p>Current implementation does not expand the destination image for
accommodating
+ * any area of a given image that appear outside the destination image bounds.
+ * Only the intersection of both images is used.</p>
+ *
* @author Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.2
* @since 1.1
* @module
*/
@@ -68,6 +77,13 @@ public class ImageCombiner implements
Consumer<RenderedImage> {
*/
private final WritableRenderedImage destination;
+ /**
+ * The value to use in calls to {@link
ImageProcessor#setImageLayout(ImageLayout)}.
+ * We set this property before use of {@link #processor} because the value
may change
+ * for each slice processed by {@link
org.apache.sis.internal.coverage.CoverageCombiner}.
+ */
+ private final Layout layout;
+
/**
* Creates an image combiner which will write in the given image. That
image is not cleared;
* pixels that are not overwritten by calls to the {@code accept(…)} or
{@code resample(…)}
@@ -76,10 +92,25 @@ public class ImageCombiner implements
Consumer<RenderedImage> {
* @param destination the image where to combine images.
*/
public ImageCombiner(final WritableRenderedImage destination) {
+ this(destination, new ImageProcessor());
+ }
+
+ /**
+ * Creates an image combiner which will use the given processor for
resampling operations.
+ * The given destination image is not cleared; pixels that are not
overwritten by calls to
+ * the {@code accept(…)} or {@code resample(…)} methods will be left
unchanged.
+ *
+ * @param destination the image where to combine images.
+ * @param processor the processor to use for resampling operations.
+ *
+ * @since 1.2
+ */
+ public ImageCombiner(final WritableRenderedImage destination, final
ImageProcessor processor) {
ArgumentChecks.ensureNonNull("destination", destination);
+ ArgumentChecks.ensureNonNull("processor", processor);
this.destination = destination;
- processor = new ImageProcessor();
- processor.setImageLayout(new Layout(destination.getSampleModel()));
+ this.processor = processor;
+ layout = new Layout(destination.getSampleModel());
}
/**
@@ -250,9 +281,10 @@ public class ImageCombiner implements
Consumer<RenderedImage> {
*/
final RenderedImage result;
synchronized (processor) {
- final Point minTile = ((Layout)
processor.getImageLayout()).minTile;
+ final Point minTile = layout.minTile;
minTile.x = minTileX;
minTile.y = minTileY;
+ processor.setImageLayout(layout);
result = processor.resample(source, bounds, toSource);
}
if (result instanceof ComputedImage) {
@@ -268,7 +300,10 @@ public class ImageCombiner implements
Consumer<RenderedImage> {
* This may be the destination image specified at construction time, but
may also be a larger image if the
* destination has been dynamically expanded for accommodating larger
sources.
*
- * <p><b>Note:</b> dynamic expansion is not yet implemented in current
version.</p>
+ * <p><b>Note:</b> dynamic expansion is not yet implemented in current
version.
+ * If a future version implements it, we shall guarantee that the
coordinate of each pixel is unchanged
+ * (i.e. the image {@code minX} and {@code minY} may become negative, but
the pixel identified by
+ * coordinates (0,0) for instance will stay the same pixel.)</p>
*
* @return the combination of destination image with all source images.
*/
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
index 007077928d..d6c4177a4e 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -553,6 +553,20 @@ public class PixelIterator {
return false;
}
+ /**
+ * Returns the type used for storing data in the raster buffer.
+ * The data type identifies the {@link DataBuffer} subclass used for
storage.
+ *
+ * @return the type used for storing data in the raster buffer.
+ *
+ * @see SampleModel#getDataType()
+ *
+ * @since 1.2
+ */
+ public DataType getDataType() {
+ return DataType.forDataBufferType(getSampleModel().getDataType());
+ }
+
/**
* Returns the most efficient type ({@code int}, {@code float} or {@code
double}) for transferring data between the
* underlying rasters and this iterator. The transfer type is not
necessarily the storage type used by the rasters.
@@ -565,6 +579,8 @@ public class PixelIterator {
* {@link TransferType#DOUBLE}, then {@link #getSampleDouble(int)} will be
both more efficient and avoid accuracy lost.</p>
*
* @return the most efficient data type for transferring data.
+ *
+ * @see SampleModel#getTransferType()
*/
public TransferType<?> getTransferType() {
return TransferType.valueOf(getSampleModel().getTransferType());
diff --git
a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CoverageCombiner.java
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CoverageCombiner.java
new file mode 100644
index 0000000000..d191b99a23
--- /dev/null
+++
b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/CoverageCombiner.java
@@ -0,0 +1,307 @@
+/*
+ * 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.coverage;
+
+import java.awt.Dimension;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRenderedImage;
+import org.opengis.geometry.Envelope;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.ImageRenderer;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.image.ImageCombiner;
+import org.apache.sis.image.PlanarImage;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+
+import static java.lang.Math.addExact;
+import static java.lang.Math.subtractExact;
+import static java.lang.Math.multiplyExact;
+import static java.lang.Math.toIntExact;
+import static java.lang.Math.round;
+import static java.lang.Math.min;
+import static java.lang.Math.max;
+import static org.apache.sis.internal.util.Numerics.saturatingAdd;
+import static org.apache.sis.internal.util.Numerics.saturatingSubtract;
+
+
+/**
+ * Combines an arbitrary amount of coverages into a single one.
+ * The combined coverages may use different coordinate systems.
+ * The workflow is as below:
+ *
+ * <ol>
+ * <li>Creates a {@code CoverageCombiner} with the destination coverage
where to write.</li>
+ * <li>Configure with methods such as {@link #setInterpolation
setInterpolation(…)}.</li>
+ * <li>Invoke {@link #accept accept(…)} or {@link #resample resample(…)}
+ * methods for each coverage to combine.</li>
+ * <li>Get the combined coverage with {@link #result()}.</li>
+ * </ol>
+ *
+ * Coverages are combined in the order they are specified.
+ *
+ * <h2>Limitations</h2>
+ * Current implementation does not apply interpolations except in the two
dimensions
+ * specified at construction time. For all other dimensions, data are taken
from the
+ * nearest neighbor two-dimensional slice.
+ *
+ * <p>In addition, current implementation does not verify if sample dimensions
are in the same order,
+ * and does not expand the destination coverage for accommodating data in
given coverages that would
+ * be outside the bounds of destination coverage.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ *
+ * @see ImageCombiner
+ *
+ * @since 1.2
+ * @module
+ */
+public final class CoverageCombiner {
+ /**
+ * The {@value} value for identifying code expecting exactly 2 dimensions.
+ */
+ private static final int BIDIMENSIONAL = 2;
+
+ /**
+ * The image processor for resampling operation.
+ * The same processor is used for all slices.
+ */
+ private final ImageProcessor processor;
+
+ /**
+ * The destination coverage where to write the coverages given to this
{@code CoverageCombiner}.
+ */
+ private final GridCoverage destination;
+
+ /**
+ * The image combiners to use for combining two-dimensional slices.
+ * There is one {@link ImageCombiner} instance per slice, created when
first needed.
+ */
+ private final ImageCombiner[] sliceCombiners;
+
+ /**
+ * Number of dimensions of the destination grid coverage.
+ */
+ private final int dimension;
+
+ /**
+ * The dimension to extract as {@link RenderedImage}s.
+ * This is usually 0 for <var>x</var> and 1 for <var>y</var>.
+ */
+ private final int xdim, ydim;
+
+ /**
+ * The offset to subtract to grid index before to compute the index of a
slice.
+ */
+ private final long[] sliceIndexOffsets;
+
+ /**
+ * The multiplication factor to apply of grid coordinates (after
subtracting the offset)
+ * for computing slice coordinates.
+ */
+ private final int[] sliceIndexSpans;
+
+ /**
+ * Creates a coverage combiner which will write in the given coverage.
+ * The coverage is not cleared; cells that are not overwritten by calls
+ * to the {@code accept(…)} method will be left unchanged.
+ *
+ * @param destination the coverage where to combine coverages.
+ * @param xdim the dimension to extract as {@link RenderedImage}
<var>x</var> axis. This is usually 0.
+ * @param ydim the dimension to extract as {@link RenderedImage}
<var>y</var> axis. This is usually 1.
+ */
+ public CoverageCombiner(final GridCoverage destination, final int xdim,
final int ydim) {
+ this.destination = destination;
+ final GridExtent extent = destination.getGridGeometry().getExtent();
+ dimension = extent.getDimension();
+ ArgumentChecks.ensureBetween("xdim", 0, dimension-1, xdim);
+ ArgumentChecks.ensureBetween("ydim", 0, dimension-1, ydim);
+ if (xdim == ydim) {
+ throw new
IllegalArgumentException(Errors.format(Errors.Keys.DuplicatedNumber_1, xdim));
+ }
+ this.xdim = xdim;
+ this.ydim = ydim;
+ sliceIndexOffsets = new long[dimension - BIDIMENSIONAL];
+ sliceIndexSpans = new int [dimension - BIDIMENSIONAL];
+ int sliceCount = 1;
+ for (int j=0,i=0; i<dimension; i++) {
+ if (i != xdim && i != ydim) {
+ final int span;
+ sliceIndexOffsets[j] = extent.getLow(i);
+ sliceIndexSpans[j++] = span = toIntExact(extent.getSize(i));
+ sliceCount = multiplyExact(sliceCount, span);
+ }
+ }
+ sliceCombiners = new ImageCombiner[sliceCount];
+ processor = new ImageProcessor();
+ }
+
+ /**
+ * Returns information about conversion from pixel coordinates to "real
world" coordinates.
+ * This is taken from {@link PlanarImage#GRID_GEOMETRY_KEY} if available,
or computed otherwise.
+ *
+ * @param image the image from which to get the conversion.
+ * @param coverage the coverage to use as a fallback if the information
is not provided with the image.
+ * @param slice identification of the slice to read in the coverage.
+ * @return information about conversion from pixel to "real world"
coordinates.
+ */
+ private static GridGeometry getGridGeometry(final RenderedImage image,
+ final GridCoverage coverage, final GridExtent slice)
+ {
+ final Object value = image.getProperty(PlanarImage.GRID_GEOMETRY_KEY);
+ if (value instanceof GridGeometry) {
+ return (GridGeometry) value;
+ }
+ return new ImageRenderer(coverage,
slice).getImageGeometry(BIDIMENSIONAL);
+ }
+
+ /**
+ * Writes the given coverage on top of destination coverage.
+ * The given coverage is resampled to the grid geometry of the destination
coverage.
+ *
+ * @param source the coverage to write on top of destination coverage.
+ * @return {@code true} on success, or {@code false} if at least one slice
+ * in the destination coverage is not writable.
+ * @throws TransformException if the coordinates of given coverage can not
be transformed
+ * to the coordinates of destination coverage.
+ */
+ public boolean accept(final GridCoverage source) throws TransformException
{
+ final Dimension margin = processor.getInterpolation().getSupportSize();
+ margin.width = ((margin.width + 1) >> 1) + 1;
+ margin.height = ((margin.height + 1) >> 1) + 1;
+ final long[] minIndices = new long[dimension]; // Will be
expanded by above margin.
+ final long[] maxIndices = new long[dimension]; // Inclusive.
+ /*
+ * Compute the intersection between `source` and `destination`, in
units
+ * of destination cell indices. A margin is added for interpolations.
+ * This block also verifies that the intersection exists.
+ */
+ final double[] centerIndices;
+ final double[] centerSourceIndices;
+ final MathTransform toSourceSliceCorner;
+ final MathTransform toSourceSliceCenter;
+ { // For keeping following variables in a local scope.
+ final GridGeometry targetGG = destination.getGridGeometry();
+ final GridGeometry sourceGG = source.getGridGeometry();
+ final GridExtent sourceEx = sourceGG.getExtent();
+ final GridExtent targetEx = targetGG.getExtent();
+ centerIndices = targetEx.getPointOfInterest();
+ centerSourceIndices = new double[sourceEx.getDimension()];
+ toSourceSliceCorner = targetGG.createTransformTo(sourceGG,
PixelInCell.CELL_CORNER);
+ toSourceSliceCenter = targetGG.createTransformTo(sourceGG,
PixelInCell.CELL_CENTER);
+ final Envelope env =
sourceEx.toEnvelope(toSourceSliceCorner.inverse());
+ for (int i=0; i<dimension; i++) {
+ minIndices[i] = max(targetEx.getLow (i),
round(env.getMinimum(i)));
+ maxIndices[i] = min(targetEx.getHigh(i),
round(env.getMaximum(i) - 1));
+ if (minIndices[i] >= maxIndices[i]) {
+ throw new DisjointExtentException();
+ }
+ }
+ }
+ /*
+ * Now apply `ImageCombiner` for each two-dimensional slice. We will
iterate on all destination slices
+ * in the intersection area, and locate the corresponding source
slices (this is a strategy similar to
+ * the resampling of pixel values in rasters).
+ */
+ final long[] minSliceIndices = minIndices.clone();
+ final long[] maxSliceIndices = maxIndices.clone();
+ final long[] minSourceIndices = new long[centerSourceIndices.length];
+ final long[] maxSourceIndices = new long[centerSourceIndices.length];
+ boolean success = true;
+next: for (;;) {
+ /*
+ * Compute the index in `sliceCombiners` array for the
two-dimensional
+ * slice identified by the current value of the `slice`
coordinates.
+ */
+ int sliceIndex = 0;
+ for (int j=0,i=0; i<dimension; i++) {
+ if (i != xdim && i != ydim) {
+ maxSliceIndices[i] = minSliceIndices[i];
+ int offset = toIntExact(subtractExact(minSliceIndices[i],
sliceIndexOffsets[j]));
+ offset = multiplyExact(offset, sliceIndexSpans[j++]);
+ sliceIndex = addExact(sliceIndex, offset);
+ }
+ }
+ /*
+ * Get the image for the current slice. It may be the result of a
previous combination.
+ * If the image is not writable, we skip that slice and try the
next one.
+ * A flag will report to the user that at least one slice was
non-writable.
+ */
+ final GridExtent targetSliceExtent = new GridExtent(null,
minSliceIndices, maxSliceIndices, true);
+ final RenderedImage targetSlice;
+ ImageCombiner combiner = sliceCombiners[sliceIndex];
+ if (combiner != null) {
+ targetSlice = combiner.result();
+ } else {
+ targetSlice = destination.render(targetSliceExtent);
+ if (targetSlice instanceof WritableRenderedImage) {
+ combiner = new ImageCombiner((WritableRenderedImage)
targetSlice, processor);
+ sliceCombiners[sliceIndex] = combiner;
+ } else {
+ success = false;
+ }
+ }
+ /*
+ * Compute the bounds of the source image to load (with a margin
for rounding and interpolations).
+ * For all dimensions other than the slice dimensions, we take the
center of the slice to read.
+ */
+ if (combiner != null) {
+ toSourceSliceCenter.transform(centerIndices, 0,
centerSourceIndices, 0, 1);
+ final Envelope sourceArea =
targetSliceExtent.toEnvelope(toSourceSliceCorner);
+ for (int i=0; i<minSourceIndices.length; i++) {
+ if (i == xdim || i == ydim) {
+ final int m = (i == xdim) ? margin.width :
margin.height;
+ minSourceIndices[i] =
saturatingSubtract(round(sourceArea.getMinimum(i)), m );
+ maxSourceIndices[i] = saturatingAdd
(round(sourceArea.getMaximum(i)), m-1);
+ } else {
+ minSourceIndices[i] = round(centerSourceIndices[i]);
+ maxSourceIndices[i] = minSourceIndices[i];
+ }
+ }
+ GridExtent sourceSliceExtent = new GridExtent(null,
minSourceIndices, maxSourceIndices, true);
+ /*
+ * Get the source image and combine with the corresponding
slice of destination coverage.
+ */
+ RenderedImage sourceSlice = source.render(sourceSliceExtent);
+ MathTransform toSource =
+ getGridGeometry(targetSlice, destination,
targetSliceExtent).createTransformTo(
+ getGridGeometry(sourceSlice, source,
sourceSliceExtent), PixelInCell.CELL_CENTER);
+ combiner.resample(sourceSlice, null, toSource);
+ }
+ /*
+ * Increment indices to the next slice.
+ */
+ for (int i=0; i<dimension; i++) {
+ if (i != xdim && i != ydim) {
+ if (minSliceIndices[i]++ <= maxIndices[i]) {
+ continue next;
+ }
+ minSliceIndices[i] = minIndices[i];
+ }
+ }
+ break;
+ }
+ return success;
+ }
+}
diff --git
a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
index f9864241da..bb68a2b420 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
@@ -240,6 +240,42 @@ public final class Numerics extends Static {
return Math.multiplyExact(value, multiplier) / divisor;
}
+ /**
+ * Returns {@code x+y} with saturation if the result overflows long
capacity.
+ * This is <cite>saturation arithmetic</cite>.
+ *
+ * @param x the value for which to add something.
+ * @param y the value to add to {@code x}.
+ * @return {@code x+y} computed with saturation arithmetic.
+ */
+ public static long saturatingAdd(final long x, final int y) {
+ final long result = x + y;
+ if (y >= 0) {
+ if (result < x) return Long.MAX_VALUE;
+ } else {
+ if (result > x) return Long.MIN_VALUE;
+ }
+ return result;
+ }
+
+ /**
+ * Returns {@code x-y} with saturation if the result overflows long
capacity.
+ * This is <cite>saturation arithmetic</cite>.
+ *
+ * @param x the value for which to add something.
+ * @param y the value to subtract from {@code x}.
+ * @return {@code x-y} computed with saturation arithmetic.
+ */
+ public static long saturatingSubtract(final long x, final int y) {
+ final long result = x - y;
+ if (y < 0) {
+ if (result < x) return Long.MAX_VALUE;
+ } else {
+ if (result > x) return Long.MIN_VALUE;
+ }
+ return result;
+ }
+
/**
* Returns the given value clamped to the range on 32 bits integer.
*
diff --git
a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
index d6ec734419..de5c9a953a 100644
---
a/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
+++
b/core/sis-utility/src/test/java/org/apache/sis/internal/util/NumericsTest.java
@@ -83,6 +83,28 @@ public final strictfp class NumericsTest extends TestCase {
assertEquals(-2L, ceilDiv( -8L, 3L));
}
+ /**
+ * Tests {@link Numerics#saturatingAdd(long, int)}.
+ */
+ @Test
+ public void testSaturatingAdd() {
+ assertEquals(1234 + 56, Numerics.saturatingAdd(1234, 56));
+ assertEquals(1234 - 56, Numerics.saturatingAdd(1234, -56));
+ assertEquals(Long.MAX_VALUE, Numerics.saturatingAdd(Long.MAX_VALUE -
10, 56));
+ assertEquals(Long.MIN_VALUE, Numerics.saturatingAdd(Long.MIN_VALUE +
10, -56));
+ }
+
+ /**
+ * Tests {@link Numerics#saturatingSubtract(long, int)}.
+ */
+ @Test
+ public void testSaturatingSubtract() {
+ assertEquals(1234 - 56, Numerics.saturatingSubtract(1234, 56));
+ assertEquals(1234 + 56, Numerics.saturatingSubtract(1234, -56));
+ assertEquals(Long.MAX_VALUE,
Numerics.saturatingSubtract(Long.MAX_VALUE - 10, -56));
+ assertEquals(Long.MIN_VALUE,
Numerics.saturatingSubtract(Long.MIN_VALUE + 10, +56));
+ }
+
/**
* Tests the {@link Numerics#cached(Object)} method.
*/
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 82f8f6556e..a55a8209ab 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
@@ -20,7 +20,9 @@ import java.net.URL;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.io.BufferedWriter;
import java.io.FileNotFoundException;
+import java.net.UnknownServiceException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -47,6 +49,7 @@ import org.apache.sis.parameter.Parameters;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.Classes;
/**
@@ -173,7 +176,7 @@ public abstract class PRJDataStore extends URIDataStore {
Path path = getSpecifiedPath();
if (path != null) {
final String base = getBaseFilename(path);
- path = path.resolveSibling(base.concat(PRJ));
+ path = path.resolveSibling(base.concat(extension));
stream = Files.newInputStream(path);
} else {
final URL url = IOUtilities.toAuxiliaryURL(location, extension);
@@ -198,6 +201,72 @@ public abstract class PRJDataStore extends URIDataStore {
}
}
+ /**
+ * Writes the {@code "*.prj"} auxiliary file if {@link #crs} is non-null.
+ * If {@link #crs} is null and the auxiliary file exists, it is deleted.
+ *
+ * @throws DataStoreException if an error occurred while writing the file.
+ */
+ protected final void writePRJ() throws DataStoreException {
+ try {
+ if (crs == null) {
+ deleteAuxiliaryFile(PRJ);
+ } else try (BufferedWriter out = writeAuxiliaryFile(PRJ,
encoding)) {
+ final StoreFormat format = new StoreFormat(locale, timezone,
null, listeners);
+ format.setConvention(Convention.WKT1_COMMON_UNITS);
+ format.format(crs, out);
+ }
+ } catch (IOException e) {
+ Object identifier = getIdentifier().orElse(null);
+ if (identifier == null) identifier =
Classes.getShortClassName(this);
+ throw new
DataStoreException(Resources.format(Resources.Keys.CanNotWriteResource_1,
identifier), e);
+ }
+ }
+
+ /**
+ * Creates a writer for an auxiliary file with the specified extension.
+ * This method uses the same path than {@link #location},
+ * except for the extension which is replaced by the given value.
+ *
+ * @param extension the filename extension of the auxiliary file to
write.
+ * @param encoding the encoding to use for writing the file content, or
{@code null} for default.
+ * @return a stream opened on the specified file.
+ * @throws UnknownServiceException if no {@link Path} or {@link
java.net.URI} is available.
+ * @throws DataStoreException if the auxiliary file can not be created.
+ * @throws IOException if another error occurred while opening the stream.
+ */
+ protected final BufferedWriter writeAuxiliaryFile(final String extension,
Charset encoding)
+ throws IOException, DataStoreException
+ {
+ final Path[] paths = super.getComponentFiles();
+ if (paths.length == 0) {
+ throw new UnknownServiceException();
+ }
+ if (encoding == null) {
+ encoding = Charset.defaultCharset();
+ }
+ Path path = paths[0];
+ final String base = getBaseFilename(path);
+ path = path.resolveSibling(base.concat(extension));
+ return Files.newBufferedWriter(path, encoding);
+ }
+
+ /**
+ * Deletes the auxiliary file with the given extension if it exists.
+ * If the auxiliary file does not exist, then this method does nothing.
+ *
+ * @param extension the filename extension of the auxiliary file to
delete.
+ * @throws DataStoreException if the auxiliary file is not on a supported
file system.
+ * @throws IOException if an error occurred while deleting the file.
+ */
+ protected final void deleteAuxiliaryFile(final String extension) throws
DataStoreException, IOException {
+ for (Path path : super.getComponentFiles()) {
+ final String base = getBaseFilename(path);
+ path = path.resolveSibling(base.concat(extension));
+ Files.deleteIfExists(path);
+ }
+ }
+
/**
* Returns the {@linkplain #location} as a {@code Path} component together
with auxiliary files.
* The default implementation does the same computation as the
super-class, then adds the sibling
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 70e56b5a31..4e65e5a169 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
@@ -134,6 +134,11 @@ public final class Resources extends IndexedResourceBundle
{
*/
public static final short CanNotStoreResourceType_2 = 41;
+ /**
+ * Can not write the “{0}” resource.
+ */
+ public static final short CanNotWriteResource_1 = 69;
+
/**
* This {0} reader is closed.
*/
@@ -336,6 +341,11 @@ public final class Resources extends IndexedResourceBundle
{
*/
public static final short ResourceNotFound_2 = 24;
+ /**
+ * The “{0}” format does not support rotations.
+ */
+ public static final short RotationNotSupported_1 = 70;
+
/**
* The “{1}” element must be declared before “{0}”.
*/
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 821d1ccc51..e0118b5299 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
@@ -34,6 +34,7 @@ CanNotReadPixel_3 = Can not read pixel at
({0}, {1}) indices in
CanNotRemoveResource_2 = Can not remove resource \u201c{1}\u201d
from aggregate \u201c{0}\u201d.
CanNotRenderImage_1 = Can not render an image for the
\u201c{0}\u201d coverage.
CanNotStoreResourceType_2 = Can not save resources of type
\u2018{1}\u2019 in a \u201c{0}\u201d store.
+CanNotWriteResource_1 = Can not write the \u201c{0}\u201d resource.
ClosedStorageConnector = This storage connector is closed.
ClosedReader_1 = This {0} reader is closed.
ClosedWriter_1 = This {0} writer is closed.
@@ -74,6 +75,7 @@ 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.
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.
SharedDirectory_1 = The \u201c{0}\u201d directory is used more
than once because of symbolic links.
StoreIsReadOnly = Write operations are not supported.
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 d04def0316..8595b62a4a 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
@@ -38,6 +38,7 @@ CanNotReadFile_4 = Ne peut pas lire
apr\u00e8s la colonne {3} d
CanNotReadPixel_3 = Ne peut pas lire le pixel aux indices
({0}, {1}) dans le fichier \u00ab\u202f{2}\u202f\u00bb.
CanNotRemoveResource_2 = Ne peut pas supprimer la ressource
\u00ab\u202f{1}\u202f\u00bb de l\u2019agr\u00e9gat \u00ab\u202f{0}\u202f\u00bb.
CanNotRenderImage_1 = Ne peut pas produire une image pour la
couverture de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb.
+CanNotWriteResource_1 = Ne peut pas \u00e9crire la ressource
\u00ab\u202f{0}\u202f\u00bb.
CanNotStoreResourceType_2 = Ne peut pas enregistrer des ressources de
type \u2018{1}\u2019 dans un entrep\u00f4t de donn\u00e9es
\u00ab\u202f{0}\u202f\u00bb.
ClosedStorageConnector = Ce connecteur est ferm\u00e9.
ClosedReader_1 = Ce lecteur {0} est ferm\u00e9.
@@ -79,6 +80,7 @@ ResourceAlreadyExists_1 = Une ressource existe
d\u00e9j\u00e0 \u00e0 l
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.
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.
SharedDirectory_1 = Le r\u00e9pertoire
\u00ab\u202f{0}\u202f\u00bb est utilis\u00e9 plus d\u2019une fois \u00e0 cause
des liens symboliques.
StoreIsReadOnly = Les op\u00e9rations d\u2019\u00e9criture
ne sont pas support\u00e9es.
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
index 8aef150aaa..1242721b7c 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/URIDataStore.java
@@ -21,6 +21,7 @@ import java.net.URI;
import java.util.Optional;
import java.nio.file.Path;
import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
import java.nio.file.FileSystemNotFoundException;
import java.nio.charset.Charset;
import org.opengis.util.GenericName;
@@ -36,6 +37,8 @@ import org.apache.sis.storage.DataStoreProvider;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.IllegalOpenParameterException;
import org.apache.sis.internal.storage.io.IOUtilities;
+import org.apache.sis.setup.OptionKey;
+import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.iso.Names;
import org.apache.sis.util.logging.Logging;
@@ -199,7 +202,7 @@ public abstract class URIDataStore extends DataStore
implements StoreResource, R
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.2
* @since 0.8
* @module
*/
@@ -311,6 +314,16 @@ public abstract class URIDataStore extends DataStore
implements StoreResource, R
throw new
IllegalOpenParameterException(Resources.format(Resources.Keys.UndefinedParameter_2,
provider.getShortName(), LOCATION), cause);
}
+
+ /**
+ * Returns {@code true} if the open options contains {@link
StandardOpenOption#WRITE}.
+ *
+ * @param connector the connector to use for opening a file.
+ * @return whether the specified connector should open a writable data
store.
+ */
+ public static boolean isWritable(final StorageConnector connector) {
+ return
ArraysExt.contains(connector.getOption(OptionKey.OPEN_OPTIONS),
StandardOpenOption.WRITE);
+ }
}
/**
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java
new file mode 100644
index 0000000000..e41d83c960
--- /dev/null
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/WritableResourceSupport.java
@@ -0,0 +1,225 @@
+/*
+ * 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;
+
+import java.util.Locale;
+import java.io.IOException;
+import java.nio.channels.WritableByteChannel;
+import java.awt.geom.AffineTransform;
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.storage.GridCoverageResource;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.storage.ReadOnlyStorageException;
+import org.apache.sis.storage.ResourceAlreadyExistsException;
+import org.apache.sis.storage.IncompatibleResourceException;
+import org.apache.sis.storage.WritableGridCoverageResource;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.storage.io.ChannelDataOutput;
+import org.apache.sis.internal.coverage.CoverageCombiner;
+import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
+import org.apache.sis.referencing.operation.transform.TransformSeparator;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Localized;
+
+// Branch-dependent imports
+import org.opengis.coverage.CannotEvaluateException;
+
+
+/**
+ * Helper classes for the management of {@link
WritableGridCoverageResource.CommonOption}.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since 1.2
+ * @module
+ */
+public final class WritableResourceSupport implements Localized {
+ /**
+ * The resource where to write.
+ */
+ private final GridCoverageResource resource;
+
+ /**
+ * {@code true} if the {@link
WritableGridCoverageResource.CommonOption.REPLACE} option has been specified.
+ * At most one of {@code replace} and {@link #update} can be {@code true}.
+ */
+ private boolean replace;
+
+ /**
+ * {@code true} if the {@link
WritableGridCoverageResource.CommonOption.UPDATE} option has been specified.
+ * At most one of {@link #replace} and {@code update} can be {@code true}.
+ */
+ private boolean update;
+
+ /**
+ * Creates a new helper class for the given options.
+ *
+ * @param resource the resource where to write.
+ * @param options configuration of the write operation.
+ */
+ public WritableResourceSupport(final GridCoverageResource resource, final
WritableGridCoverageResource.Option[] options) {
+ this.resource = resource;
+ ArgumentChecks.ensureNonNull("options", options);
+ for (final WritableGridCoverageResource.Option option : options) {
+ replace |=
WritableGridCoverageResource.CommonOption.REPLACE.equals(option);
+ update |= WritableGridCoverageResource.CommonOption.UPDATE
.equals(option);
+ }
+ if (replace & update) {
+ throw new
IllegalArgumentException(Errors.format(Errors.Keys.MutuallyExclusiveOptions_2,
+ WritableGridCoverageResource.CommonOption.REPLACE,
+ WritableGridCoverageResource.CommonOption.UPDATE));
+ }
+ }
+
+ /**
+ * Returns the locale used by the resource for error messages, or {@code
null} if unknown.
+ *
+ * @return the locale used by the resource for error messages, or {@code
null} if unknown.
+ */
+ @Override
+ public final Locale getLocale() {
+ return (resource instanceof Localized) ? ((Localized)
resource).getLocale() : null;
+ }
+
+ /**
+ * Returns the writable channel positioned at the beginning of the stream.
+ * The returned channel should <em>not</em> be closed
+ * because it is the same channel than the one used by {@code input}.
+ * Caller should invoke {@link ChannelDataOutput#flush()} after usage.
+ *
+ * @param input the input from which to get the writable channel.
+ * @return the writable channel.
+ * @throws IOException if the stream position can not be reset.
+ * @throws DataStoreException if the channel is read-only.
+ */
+ public final ChannelDataOutput channel(final ChannelDataInput input)
throws IOException, DataStoreException {
+ if (input.channel instanceof WritableByteChannel && input.rewind()) {
+ return new ChannelDataOutput(input.filename, (WritableByteChannel)
input.channel, input.buffer);
+ } else {
+ throw new ReadOnlyStorageException(canNotWrite());
+ }
+ }
+
+ /**
+ * Returns {@code true} if the caller should add or replace the resource
+ * or {@code false} if it needs to update an existing resource.
+ * Current heuristic:
+ *
+ * <ul>
+ * <li>If the given channel is empty, then this method always returns
{@code true}.</li>
+ * <li>Otherwise this method returns {@code true} if the {@code REPLACE}
option was specified,
+ * or returns {@code false} if the {@code UPDATE} option was
specified,
+ * or thrown a {@link ResourceAlreadyExistsException} otherwise.</li>
+ * </ul>
+ *
+ * @param input the channel to test for emptiness.
+ * @return whether the caller should replace ({@code true}) or update
({@code false}) the resource.
+ * @throws IOException if an error occurred while checking the channel
length.
+ * @throws ResourceAlreadyExistsException if the resource exists and the
writer
+ * should neither updating or replacing it.
+ * @throws DataStoreException if another kind of error occurred with the
resource.
+ */
+ public final boolean replace(final ChannelDataInput input) throws
IOException, DataStoreException {
+ if (update) {
+ return input.length() == 0;
+ } else if (replace || input.length() == 0) {
+ return true;
+ } else {
+ Object identifier = resource.getIdentifier().orElse(null);
+ if (identifier == null) identifier = input.filename;
+ throw new
ResourceAlreadyExistsException(Resources.forLocale(getLocale())
+ .getString(Resources.Keys.ResourceAlreadyExists_1,
identifier));
+ }
+ }
+
+ /**
+ * Reads the current coverage in the resource and updates its content with
cell values from the given coverage.
+ * This method can be used as a simple implementation of {@link
WritableGridCoverageResource.CommonOption#UPDATE}.
+ * This method returns the updated coverage; it is caller responsibility
to write it.
+ *
+ * <p>This method can be used when updating the coverage requires to read
it fully, then write if fully.
+ * Advanced writers should try to update only the modified parts
(typically some tiles) instead.</p>
+ *
+ * @param coverage the coverage to use for updating the currently
existing coverage.
+ * @return the updated coverage that the caller should write.
+ * @throws DataStoreException if an error occurred while reading or
updating the coverage.
+ */
+ public final GridCoverage update(final GridCoverage coverage) throws
DataStoreException {
+ final GridCoverage existing = resource.read(null, null);
+ final CoverageCombiner combiner = new CoverageCombiner(existing, 0, 1);
+ try {
+ if (!combiner.accept(coverage)) {
+ throw new ReadOnlyStorageException(canNotWrite());
+ }
+ } catch (TransformException e) {
+ throw new DataStoreReferencingException(canNotWrite(), e);
+ }
+ return existing;
+ }
+
+ /**
+ * Returns the "grid to CRS" transform as a two-dimensional affine
transform.
+ * This is a convenience method for writers that support only this kind of
transform.
+ *
+ * @param extent the extent of the grid coverage to write.
+ * @param gridToCRS the "grid to CRS" transform of the coverage to write.
+ * @return the given "grid to CRS" as a two-dimensional affine transform.
+ * @throws DataStoreException if the affine transform can not be extracted
from the given "grid to CRS" transform.
+ */
+ public final AffineTransform getAffineTransform2D(final GridExtent extent,
final MathTransform gridToCRS)
+ throws DataStoreException
+ {
+ final TransformSeparator s = new TransformSeparator(gridToCRS);
+ try {
+ s.addSourceDimensions(extent.getSubspaceDimensions(2));
+ return AffineTransforms2D.castOrCopy(s.separate());
+ } catch (FactoryException | CannotEvaluateException e) {
+ throw new DataStoreReferencingException(canNotWrite(), e);
+ } catch (IllegalArgumentException e) {
+ throw new IncompatibleResourceException(canNotWrite(), e);
+ }
+ }
+
+ /**
+ * Returns the message for an exception saying that we can not write the
resource.
+ *
+ * @return a localized "Can not write resource" message.
+ * @throws DataStoreException if an error occurred while preparing the
error message.
+ */
+ public final String canNotWrite() throws DataStoreException {
+ Object identifier = resource.getIdentifier().orElse(null);
+ if (identifier == null) identifier =
Classes.getShortClassName(resource);
+ return
Resources.forLocale(getLocale()).getString(Resources.Keys.CanNotWriteResource_1,
identifier);
+ }
+
+ /**
+ * Returns the message for an exception saying that rotations are not
supported.
+ *
+ * @param format name of the format that does not support rotations.
+ * @return a localized "rotation not supported" message.
+ */
+ public final String rotationNotSupported(final String format) {
+ return
Resources.forLocale(getLocale()).getString(Resources.Keys.RotationNotSupported_1,
format);
+ }
+}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
index 0c485df201..ed7531792f 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/Store.java
@@ -22,6 +22,7 @@ import java.util.Optional;
import java.util.StringJoiner;
import java.io.IOException;
import java.nio.file.StandardOpenOption;
+import java.awt.image.RenderedImage;
import java.awt.image.DataBufferFloat;
import org.opengis.geometry.Envelope;
import org.opengis.metadata.Metadata;
@@ -46,6 +47,7 @@ import org.apache.sis.internal.storage.PRJDataStore;
import org.apache.sis.internal.storage.RangeArgument;
import org.apache.sis.internal.storage.Resources;
import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.measure.NumberRange;
import org.apache.sis.metadata.iso.DefaultMetadata;
import org.apache.sis.metadata.sql.MetadataStoreException;
import org.apache.sis.referencing.operation.matrix.Matrix3;
@@ -165,6 +167,14 @@ class Store extends PRJDataStore implements
GridCoverageResource {
listeners.useWarningEventsOnly();
}
+ /**
+ * Returns whether this store is read-only. If {@code true}, we can close
the channel
+ * as soon as the coverage has been fully read. Otherwise we need to keep
it open.
+ */
+ boolean isReadOnly() {
+ return true;
+ }
+
/**
* Reads the {@code "*.prj"} file and the header if not already done.
* This method does nothing if the data store is already initialized.
@@ -418,8 +428,10 @@ cellsize: if (value != null) {
* TODO: a future version could try to convert the image to
integer values.
* In this case only we may need to declare the NODATA_VALUE.
*/
- input = null;
- view.input.channel.close();
+ if (isReadOnly()) {
+ input = null;
+ view.input.channel.close();
+ }
double minimum = stats.minimum();
double maximum = stats.maximum();
if (!(minimum <= maximum)) {
@@ -455,11 +467,39 @@ cellsize: if (value != null) {
/**
* Replaces all data by the given coverage.
* This is used for write operations only.
+ *
+ * @param replacement the new coverage.
+ * @param data the image wrapped by the given coverage.
+ * @param band index of the band to write (usually 0).
+ * @return the "no data" value, or {@link Double#NaN} if none.
*/
- final void setCoverage(final GridCoverage replacement) {
- gridGeometry = replacement.getGridGeometry();
+ final Number setCoverage(final GridCoverage replacement, final
RenderedImage data, final int band) {
coverage = replacement;
+ gridGeometry = replacement.getGridGeometry();
+ crs = gridGeometry.isDefined(GridGeometry.CRS) ?
gridGeometry.getCoordinateReferenceSystem() : null;
+ width = data.getWidth();
+ height = data.getHeight();
metadata = null;
+ nodataText = "null";
+ nodataValue = Double.NaN;
+ final SampleDimension sd = replacement.getSampleDimensions().get(band);
+ final NumberRange<?> range = sd.getSampleRange().orElse(null);
+ if (range != null) {
+ try {
+ for (final Number nodata :
sd.forConvertedValues(false).getNoDataValues()) {
+ if (!range.containsAny(nodata)) {
+ nodataValue = nodata.doubleValue();
+ return nodata;
+ }
+ }
+ } catch (IllegalStateException e) {
+ listeners.warning(e);
+ }
+ if (range.containsAny(DEFAULT_NODATA)) {
+ nodataValue = DEFAULT_NODATA;
+ }
+ }
+ return nodataValue;
}
/**
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
index 86bca8e59c..feb70066ca 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/StoreProvider.java
@@ -128,6 +128,10 @@ cellsize: if
(!header.containsKey(Store.CELLSIZE)) {
*/
@Override
public DataStore open(final StorageConnector connector) throws
DataStoreException {
- return new Store(this, connector);
+ if (isWritable(connector)) {
+ return new WritableStore(this, connector);
+ } else {
+ return new Store(this, connector);
+ }
}
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
new file mode 100644
index 0000000000..7dd32f37a0
--- /dev/null
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/ascii/WritableStore.java
@@ -0,0 +1,270 @@
+/*
+ * 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.ascii;
+
+import java.util.Map;
+import java.util.LinkedHashMap;
+import java.io.IOException;
+import java.awt.image.DataBuffer;
+import java.awt.image.RenderedImage;
+import java.awt.geom.AffineTransform;
+import org.opengis.coverage.grid.SequenceType;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.storage.StorageConnector;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreReferencingException;
+import org.apache.sis.storage.WritableGridCoverageResource;
+import org.apache.sis.internal.storage.WritableResourceSupport;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.storage.io.ChannelDataOutput;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.storage.IncompatibleResourceException;
+import org.apache.sis.image.PixelIterator;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.StringBuilders;
+
+
+/**
+ * An ASCII Grid store with writing capabilities.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.2
+ * @since 1.2
+ * @module
+ */
+final class WritableStore extends Store implements
WritableGridCoverageResource {
+ /**
+ * The line separator for writing the ASCII file.
+ */
+ private final String lineSeparator;
+
+ /**
+ * Creates a new ASCII Grid 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.
+ */
+ public WritableStore(final StoreProvider provider, final StorageConnector
connector) throws DataStoreException {
+ super(provider, connector);
+ lineSeparator = System.lineSeparator();
+ }
+
+ /**
+ * Returns whether this store is read-only.
+ */
+ @Override
+ boolean isReadOnly() {
+ return false;
+ }
+
+ /**
+ * Returns an estimation of how close the "CRS to grid" transform is to
integer values.
+ * This is used for choosing whether to map pixel centers or pixel centers.
+ */
+ private static double distanceFromIntegers(final MathTransform gridToCRS)
throws TransformException {
+ final Matrix m = MathTransforms.getMatrix(gridToCRS.inverse());
+ if (m != null && Matrices.isAffine(m)) {
+ final int last = m.getNumCol() - 1;
+ double sum = 0;
+ for (int j=0; j<last; j++) {
+ final double e = m.getElement(j, last);
+ sum += Math.abs(Math.rint(e) - e);
+ }
+ return sum;
+ }
+ return Double.NaN;
+ }
+
+ /**
+ * Gets the coefficients of the affine transform.
+ *
+ * @param header the map where to put the affine transform coefficients.
+ * @param gg the grid geometry from which to get the affine
transform.
+ * @param h set of helper methods.
+ * @return the iteration order (e.g. from left to right, then top to
bottom).
+ * @throws DataStoreException if the header can not be written.
+ */
+ private static SequenceType getAffineCoefficients(
+ final Map<String,Object> header, final GridGeometry gg,
+ final WritableResourceSupport h) throws DataStoreException
+ {
+ String xll = XLLCORNER;
+ String yll = YLLCORNER;
+ MathTransform gridToCRS = gg.getGridToCRS(PixelInCell.CELL_CORNER);
+ try {
+ final MathTransform alternative =
gg.getGridToCRS(PixelInCell.CELL_CENTER);
+ if (distanceFromIntegers(alternative) <
distanceFromIntegers(gridToCRS)) {
+ gridToCRS = alternative;
+ xll = XLLCENTER;
+ yll = YLLCENTER;
+ }
+ } catch (TransformException e) {
+ throw new DataStoreReferencingException(h.canNotWrite(), e);
+ }
+ final AffineTransform at = h.getAffineTransform2D(gg.getExtent(),
gridToCRS);
+ if (at.getShearX() != 0 || at.getShearY() != 0) {
+ throw new
IncompatibleResourceException(h.rotationNotSupported(StoreProvider.NAME));
+ }
+ double scaleX = at.getScaleX();
+ double scaleY = -at.getScaleY();
+ double x = at.getTranslateX();
+ double y = at.getTranslateY();
+ if (scaleX > 0 && scaleY > 0) {
+ y -= scaleY * (Integer) header.get(NROWS);
+ } else {
+ /*
+ * TODO: future version could support other signs, provided that
+ * we implement `PixelIterator` for other `SequenceType` values.
+ */
+ throw new IncompatibleResourceException(h.canNotWrite());
+ }
+
+ header.put(xll, x);
+ header.put(yll, y);
+ if (scaleX == scaleY) {
+ header.put(CELLSIZE, scaleX);
+ } else {
+ header.put(CELLSIZES[0], scaleX);
+ header.put(CELLSIZES[1], scaleY);
+ }
+ return SequenceType.LINEAR;
+ }
+
+ /**
+ * Writes the content of the given map as the header of ASCII Grid file.
+ */
+ private void writeHeader(final Map<String,Object> header, final
ChannelDataOutput out) throws IOException {
+ int maxKeyLength = 0;
+ int maxValLength = 0;
+ for (final Map.Entry<String,Object> entry : header.entrySet()) {
+ final String text = entry.getValue().toString();
+ entry.setValue(text);
+ maxValLength = Math.max(maxValLength, text.length());
+ maxKeyLength = Math.max(maxKeyLength, entry.getKey().length());
+ }
+ for (final Map.Entry<String,Object> entry : header.entrySet()) {
+ String text = entry.getKey();
+ write(text, out);
+ write(CharSequences.spaces(maxKeyLength - text.length() + 1), out);
+ text = (String) entry.getValue();
+ write(CharSequences.spaces(maxValLength - text.length()), out);
+ write(text, out);
+ write(lineSeparator, out);
+ }
+ }
+
+ /**
+ * 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 synchronized void write(GridCoverage coverage, final Option...
options) throws DataStoreException {
+ final WritableResourceSupport h = new WritableResourceSupport(this,
options); // Does argument validation.
+ final ChannelDataInput input = input().input;
+ final int band = 0; // May become
configurable in a future version.
+ try {
+ if (!h.replace(input)) {
+ coverage = h.update(coverage);
+ }
+ final RenderedImage data = coverage.render(null); //
Fail if not two-dimensional.
+ final Map<String,Object> header = new LinkedHashMap<>();
+ header.put(NCOLS, data.getWidth());
+ header.put(NROWS, data.getHeight());
+ final SequenceType order = getAffineCoefficients(header,
coverage.getGridGeometry(), h);
+ /*
+ * Open the destination channel only after the coverage has been
validated by above method calls.
+ * After this point we should not have any validation errors.
Write the nodata value even if it is
+ * "NaN" because the default is -9999, and we need to overwrite
that default if it can not be used.
+ */
+ final ChannelDataOutput out = h.channel(input);
+ final Number nodataValue = setCoverage(coverage, data, band);
+ header.put(NODATA_VALUE, nodataValue);
+ writeHeader(header, out);
+ /*
+ * Writes all sample values.
+ */
+ final float nodataAsFloat = nodataValue.floatValue();
+ final double nodataAsDouble = nodataValue.doubleValue();
+ final StringBuilder buffer = new StringBuilder();
+ final PixelIterator it = new
PixelIterator.Builder().setIteratorOrder(order).create(data);
+ final int dataType = it.getDataType().toDataBufferType();
+ final int width = it.getDomain().width;
+ int remaining = width;
+ while (it.next()) {
+ switch (dataType) {
+ case DataBuffer.TYPE_DOUBLE: {
+ double value = it.getSampleDouble(band);
+ if (Double.isNaN(value)) {
+ value = nodataAsDouble;
+ }
+ buffer.append(value);
+ StringBuilders.trimFractionalPart(buffer);
+ break;
+ }
+ case DataBuffer.TYPE_FLOAT: {
+ float value = it.getSampleFloat(band);
+ if (Float.isNaN(value)) {
+ value = nodataAsFloat;
+ }
+ buffer.append(value);
+ StringBuilders.trimFractionalPart(buffer);
+ break;
+ }
+ default: {
+ buffer.append(it.getSample(band));
+ break;
+ }
+ }
+ write(buffer, out);
+ buffer.setLength(0);
+ if (--remaining != 0) {
+ out.writeByte(' ');
+ } else {
+ write(lineSeparator, out);
+ remaining = width;
+ }
+ }
+ out.flush();
+ writePRJ();
+ } catch (IOException e) {
+ closeOnError(e);
+ throw new DataStoreException(e);
+ }
+ }
+
+ /**
+ * Writes the given text to the output. All characters must be US-ASCII
(this is not verified).
+ */
+ private static void write(final CharSequence text, final ChannelDataOutput
out) throws IOException {
+ final int length = text.length();
+ out.ensureBufferAccepts(length);
+ for (int i=0; i<length; i++) {
+ out.buffer.put((byte) text.charAt(i));
+ }
+ }
+}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
index 19d918af63..335667359b 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataInput.java
@@ -108,13 +108,14 @@ public class ChannelDataInput extends ChannelData {
/**
* Returns the length of the stream (in bytes), or -1 if unknown.
+ * The length is relative to the channel position at {@linkplain
#ChannelDataInput construction time}.
*
- * @return the length of the stream (in bytes), or -1 if unknown.
+ * @return the length of the stream (in bytes) relative to {@link
#channelOffset}, or -1 if unknown.
* @throws IOException if an error occurred while fetching the stream
length.
*/
public final long length() throws IOException { // Method signature
must match ImageInputStream.length().
if (channel instanceof SeekableByteChannel) {
- return ((SeekableByteChannel) channel).size();
+ return Math.subtractExact(((SeekableByteChannel) channel).size(),
channelOffset);
}
return -1;
}
@@ -923,4 +924,22 @@ public class ChannelDataInput extends ChannelData {
}
clearBitOffset();
}
+
+ /**
+ * Empties the buffer and reset the channel position at the beginning of
the stream.
+ * This method is similar to {@code seek(0)} except that the buffer
content is discarded.
+ *
+ * @return {@code true} on success, or {@code false} if it is not possible
to reset the position.
+ * @throws IOException if the stream can not be moved to the original
position.
+ */
+ public final boolean rewind() throws IOException {
+ if (channel instanceof SeekableByteChannel) {
+ ((SeekableByteChannel) channel).position(channelOffset);
+ buffer.clear().limit(0);
+ bufferOffset = 0;
+ clearBitOffset();
+ return true;
+ }
+ return false;
+ }
}
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
index bf31ecc424..84244c68df 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/internal/storage/io/ChannelDataOutput.java
@@ -60,7 +60,7 @@ import static
org.apache.sis.util.ArgumentChecks.ensureBetween;
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.5
+ * @version 1.2
* @since 0.5
* @module
*/
@@ -98,7 +98,7 @@ public class ChannelDataOutput extends ChannelData implements
Flushable {
* @param n the minimal number of additional bytes that the {@linkplain
#buffer buffer} shall accept.
* @throws IOException if an error occurred while writing to the channel.
*/
- private void ensureBufferAccepts(final int n) throws IOException {
+ public final void ensureBufferAccepts(final int n) throws IOException {
final int capacity = buffer.capacity();
assert n >= 0 && n <= capacity : n;
int after = buffer.position() + n;
diff --git
a/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableGridCoverageResource.java
b/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableGridCoverageResource.java
index 254884b6e3..b4a3337067 100644
---
a/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableGridCoverageResource.java
+++
b/storage/sis-storage/src/main/java/org/apache/sis/storage/WritableGridCoverageResource.java
@@ -43,8 +43,8 @@ public interface WritableGridCoverageResource extends
GridCoverageResource {
* Other options may be defined by the {@linkplain DataStoreProvider} of
specific formats.</p>
*
* @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
+ * @version 1.2
+ * @since 1.2
* @module
*/
interface Option {}
@@ -55,21 +55,23 @@ public interface WritableGridCoverageResource extends
GridCoverageResource {
* This {@code CommonOption} enumeration provides options that do not
depend on the data store.
*
* @author Johann Sorel (Geomatys)
- * @version 1.0
- * @since 1.0
+ * @version 1.2
+ * @since 1.2
* @module
*/
enum CommonOption implements Option {
/**
* Instructs the write operation to replace existing coverage if one
exists.
- * By default the {@linkplain #write write operation} does not
overwrite existing data.
+ * By default (when no option is specified) the {@linkplain #write
write operation}
+ * will only add new coverages and never modify existing ones.
* If this option is specified, then there is a choice:
*
- * <ul>
- * <li>If a coverage already exists in the {@link
GridCoverageResource}, then it will be erased.
- * The existing data will be replaced by the new coverage.</li>
+ * <ul class="verbose">
+ * <li>If a coverage already exists in the {@link
GridCoverageResource}, then it will be deleted.
+ * The existing coverage will be replaced by the new coverage.
+ * The old and new coverages may have different grid
geometries.</li>
* <li>If there are no existing coverages in the {@link
GridCoverageResource},
- * then the new coverage will be inserted as if this option was
not provided.</li>
+ * then the new coverage will be added as if this option was not
provided.</li>
* </ul>
*
* This option is mutually exclusive with {@link #UPDATE}.
@@ -77,20 +79,20 @@ public interface WritableGridCoverageResource extends
GridCoverageResource {
REPLACE,
/**
- * Updates or appends existing coverage with new data.
+ * Instructs the write operation to update existing coverage if one
exists.
* If this option is specified, then there is a choice:
*
- * <ul>
+ * <ul class="verbose">
* <li>If a coverage already exists in the {@link
GridCoverageResource}, then:
* <ul>
- * <li>Areas of the provided {@link GridCoverage} that are
within the existing {@link GridGeometry}
- * will overwrite the existing data.</li>
- * <li>Areas outside the existing {@link GridGeometry} will
result in expanding the grid geometry
- * with the new data.</li>
+ * <li>Cells of the provided {@link GridCoverage} that are
within the {@link GridGeometry}
+ * of the existing coverage will overwrite the existing
cells. The provided coverage
+ * may be resampled to the grid geometry of the existing
coverage in this process.</li>
+ * <li>Cells outside the {@link GridGeometry} of the existing
coverage are ignored.</li>
* </ul>
* </li>
* <li>If there are no existing coverages in the {@link
GridCoverageResource},
- * then the new coverage is inserted as if this option was not
provided.</li>
+ * then the new coverage will be added as if this option was not
provided.</li>
* </ul>
*
* This option is mutually exclusive with {@link #REPLACE}.