You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2021/05/04 22:29:54 UTC
[sis] branch GeoTIFF updated: Put the foundation for reading
GeoTIFF sample values. This code anticipates a support of N-dimensional
data cube,
to be explored later. This commit is not yet functional and will be squashed
before merge.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch GeoTIFF
in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/GeoTIFF by this push:
new a296271 Put the foundation for reading GeoTIFF sample values. This code anticipates a support of N-dimensional data cube, to be explored later. This commit is not yet functional and will be squashed before merge.
a296271 is described below
commit a296271e646ad4bacf040f543e0b47bb38db3fea
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue May 4 20:55:49 2021 +0200
Put the foundation for reading GeoTIFF sample values.
This code anticipates a support of N-dimensional data cube, to be explored later.
This commit is not yet functional and will be squashed before merge.
---
.../org/apache/sis/storage/geotiff/CRSBuilder.java | 8 +-
.../org/apache/sis/storage/geotiff/DataCube.java | 235 +++++++++++++++++++++
.../org/apache/sis/storage/geotiff/DataSubset.java | 146 +++++++++++++
.../org/apache/sis/storage/geotiff/GeoTIFF.java | 10 +-
.../apache/sis/storage/geotiff/GeoTiffStore.java | 11 +
.../sis/storage/geotiff/GridGeometryBuilder.java | 7 +-
.../sis/storage/geotiff/ImageFileDirectory.java | 170 +++++++++------
.../apache/sis/storage/geotiff/PixelDecoder.java | 139 ++++++++++++
.../org/apache/sis/storage/geotiff/Reader.java | 16 +-
9 files changed, 663 insertions(+), 79 deletions(-)
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
index 82d1dd6..abd0d11 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/CRSBuilder.java
@@ -244,7 +244,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
*/
private void warning(final short key, final Object... args) {
final LogRecord r = reader.resources().getLogRecord(Level.WARNING, key, args);
- reader.owner.warning(r);
+ reader.store.warning(r);
}
/**
@@ -463,7 +463,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
try {
expected = Integer.parseInt(id.getCode());
} catch (NumberFormatException e) {
- reader.owner.warning(null, e); // Should not happen.
+ reader.store.warning(null, e); // Should not happen.
return;
}
if (code != expected) {
@@ -720,7 +720,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
if (epsg != null) try {
return getCSAuthorityFactory().createCartesianCS(epsg.toString());
} catch (NoSuchAuthorityCodeException e) {
- reader.owner.warning(null, e);
+ reader.store.warning(null, e);
}
return (CartesianCS) CoordinateSystems.replaceLinearUnit(cs, unit);
}
@@ -738,7 +738,7 @@ final class CRSBuilder extends ReferencingFactoryContainer {
if (epsg != null) try {
return getCSAuthorityFactory().createEllipsoidalCS(epsg.toString());
} catch (NoSuchAuthorityCodeException e) {
- reader.owner.warning(null, e);
+ reader.store.warning(null, e);
}
return (EllipsoidalCS) CoordinateSystems.replaceAngularUnit(cs, unit);
}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
new file mode 100644
index 0000000..417686e
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataCube.java
@@ -0,0 +1,235 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff;
+
+import java.util.List;
+import java.util.Arrays;
+import java.nio.file.Path;
+import org.apache.sis.image.DataType;
+import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.coverage.SampleDimension;
+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.coverage.grid.GridDerivation;
+import org.apache.sis.coverage.grid.GridRoundingMode;
+import org.apache.sis.internal.storage.AbstractGridResource;
+import org.apache.sis.internal.storage.ResourceOnFileSystem;
+import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.math.Vector;
+
+
+/**
+ * One or many GeoTIFF images packaged as a single resource.
+ * This is typically a single two-dimensional image represented as a {@link ImageFileDirectory}.
+ * But it can also be a stack of images organized in a <var>n</var>-dimensional data cube,
+ * or a pyramid of images with their overviews used when low resolution images is requested.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+abstract class DataCube extends AbstractGridResource implements ResourceOnFileSystem {
+ /**
+ * The GeoTIFF reader which contain this {@code DataCube}.
+ * Used for fetching information like the input channel and where to report warnings.
+ */
+ final Reader reader;
+
+ /**
+ * Creates a new data cube.
+ *
+ * @param reader information about the input stream to read, the metadata and the character encoding.
+ */
+ DataCube(final Reader reader) {
+ super(reader.store.listeners());
+ this.reader = reader;
+ }
+
+ /**
+ * Shortcut for a frequently requested information.
+ */
+ final String filename() {
+ return reader.input.filename;
+ }
+
+ /**
+ * Information about pixel layout (data type, number of pixels, …) in the TIFF file.
+ */
+ static final class Layout {
+ /** The type to use for storing raster data. */
+ final DataType type;
+
+ /** Number of bits per component. */
+ final int bitsPerSample;
+
+ /** Whether the components are stored in separate “component planes”. */
+ final boolean isPlanar;
+
+ /** Creates new layout information. */
+ Layout(final DataType type, final int bitsPerSample, final boolean isPlanar) {
+ this.type = type;
+ this.bitsPerSample = bitsPerSample;
+ this.isPlanar = isPlanar;
+ }
+ }
+
+ /**
+ * Returns the type to use for storing raster data, together with the number of bits per sample.
+ * The type should be the one used in the GeoTIFF file as much as possible, but may sometime be wider.
+ *
+ * @throws DataStoreContentException if the type is not recognized.
+ */
+ protected abstract Layout getLayout() throws DataStoreContentException;
+
+ /**
+ * Returns the size in pixels of all tiles in this data cube. The array length shall be equal to
+ * {@link GridGeometry#getDimension()}, unless the grid geometry has one extra dimension for the
+ * vertical axis.
+ *
+ * @see #getGridGeometry()
+ */
+ protected abstract int[] getTileSize();
+
+ /**
+ * Gets the stream position or the length in bytes of compressed tile arrays in the GeoTIFF file.
+ * Values in the returned vector are {@code long} primitive type.
+ *
+ * @param length {@code false} for {@code tileOffsets} or {@code true} for {@code tileByteCounts}.
+ * @return stream position (relative to file beginning) or length of compressed tile arrays, in bytes.
+ */
+ abstract Vector getTileArrayInfo(boolean length);
+
+ /**
+ * Loads a subset of the grid coverage represented by this resource.
+ *
+ * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
+ * @param range 0-based index of sample dimensions to read, or an empty sequence for reading all ranges.
+ * @return the grid coverage for the specified domain and range.
+ * @throws DataStoreException if an error occurred while reading the grid coverage data.
+ */
+ @Override
+ public final GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException {
+ final long startTime = System.nanoTime();
+ final GridCoverage coverage;
+ try {
+ synchronized (reader.store) {
+ coverage = subset(domain, range);
+ }
+ } catch (RuntimeException e) {
+ throw new DataStoreException(reader.errors().getString(Errors.Keys.CanNotRead_1, filename()), e);
+ }
+ logReadOperation(reader.store.path, coverage.getGridGeometry(), startTime);
+ return coverage;
+ }
+
+ /**
+ * Creates a {@link GridCoverage} which will load pixel data in the given domain,
+ * but without loading data immediately. This method does not perform I/O operations.
+ *
+ * @throws ArithmeticException if pixel indices exceed 64 bits integer capacity.
+ */
+ private DataSubset subset(GridGeometry domain, final int... range) throws DataStoreException {
+ final List<SampleDimension> bands = getSampleDimensions();
+ final RangeArgument rangeIndices = validateRangeArgument(bands.size(), range);
+ final GridGeometry gridGeometry = getGridGeometry();
+ final GridExtent fullSize = gridGeometry.getExtent();
+ final int[] tileSize = getTileSize();
+ final int dimension = tileSize.length; // May be less than `fullSize.getDimension()`.
+ final int[] subsamplings;
+ GridExtent areaOfInterest;
+ if (domain == null) {
+ domain = gridGeometry;
+ areaOfInterest = fullSize;
+ subsamplings = new int[gridGeometry.getDimension()];
+ Arrays.fill(subsamplings, 1);
+ } else {
+ final GridDerivation target = gridGeometry.derive()
+ .rounding(GridRoundingMode.ENCLOSING).subgrid(domain);
+ areaOfInterest = target.getIntersection();
+ subsamplings = target.getSubsamplings();
+ domain = target.build();
+ }
+ Vector tileOffsets = getTileArrayInfo(false);
+ Vector tileByteCounts = getTileArrayInfo(true);
+ /*
+ * Compute an initial number of tiles from the image size and tile size.
+ * Those numbers will be modified later if a sub-domain has been requested.
+ */
+ final long[] numTiles = new long[dimension];
+ for (int i=0; i<dimension; i++) {
+ numTiles[i] = Numerics.ceilDiv(fullSize.getSize(i), tileSize[i]);
+ }
+ /*
+ * Compute the values by which to multiply each tile coordinate for obtaining
+ * the index in `tileOffsets` and `tileByteCounts` vectors. This computation
+ * must be done before to clip `numTiles` to user-specified domain.
+ */
+ final int[] tileStrides = new int[dimension];
+ tileStrides[0] = 1;
+ for (int i=1; i<dimension; i++) {
+ tileStrides[i] = Math.toIntExact(Math.multiplyExact(numTiles[i], tileStrides[i-1]));
+ }
+ /*
+ * If a sub-domain is requested, modify the tile indices in order to include only
+ * the tiles that intersect the specified domain. Since we will still use (0,0,…)
+ * as the indices of the first tile, we need to have that tile at index 0 in the
+ * `tileOffsets` and `tileByteCounts` vectors.
+ */
+ if (!fullSize.equals(areaOfInterest)) {
+ long firstTileIndex = 0;
+ final long[] lowerDiff = new long[dimension];
+ final long[] upperDiff = new long[dimension];
+ for (int i=0; i<dimension; i++) {
+ final long low = areaOfInterest.getLow(i); // Always ≥ 0.
+ final long high = areaOfInterest.getHigh(i); // Inclusive
+ final int size = tileSize[i];
+ final long tileLower = low / size; // Always ≥ 0.
+ final long tileUpper = Math.incrementExact(high / size); // Exclusive.
+ upperDiff[i] = Math.multiplyExact(tileUpper, size) - high - 1; // Always ≥ 0.
+ lowerDiff[i] = low % size; // low - tileLower*size
+ numTiles[i] = tileUpper - tileLower;
+ firstTileIndex += tileLower * tileStrides[i];
+ }
+ final int start = Math.toIntExact(firstTileIndex);
+ tileOffsets = tileOffsets .subList(start, tileOffsets.size());
+ tileByteCounts = tileByteCounts.subList(start, tileByteCounts.size());
+ areaOfInterest = areaOfInterest.expand(lowerDiff, upperDiff); // TODO: need to clip.
+ /*
+ * TODO: need to recompute `domain`. We should put above calculation directly in `GridDerivation`.
+ * See `GridDerivation.margin`; we may be able to apply tiling in the same code than margins.
+ */
+ }
+ /*
+ * TODO: take selected bands in account.
+ */
+ final PixelDecoder decoder = new PixelDecoder(tileSize, subsamplings, bands.size(), getLayout());
+ return new DataSubset(this, domain, bands, tileOffsets, tileByteCounts, numTiles, tileStrides, decoder);
+ }
+
+ /**
+ * Gets the paths to files used by this resource, or an empty array if unknown.
+ */
+ @Override
+ public Path[] getComponentFiles() {
+ final Path location = reader.store.path;
+ return (location != null) ? new Path[] {location} : new Path[0];
+ }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
new file mode 100644
index 0000000..c7bc03b
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/DataSubset.java
@@ -0,0 +1,146 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff;
+
+import java.util.List;
+import java.io.IOException;
+import java.awt.image.RenderedImage;
+import java.awt.image.WritableRaster;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.coverage.grid.GridExtent;
+import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.math.Vector;
+import org.apache.sis.util.ArgumentChecks;
+
+
+/**
+ * Raster data obtained from a GeoTIFF file in the domain requested by user. The number of dimensions is 2
+ * for standard TIFF files, but this class accepts higher number of dimensions if 3- or 4-dimensional data
+ * are stored in a GeoTIFF file using some convention.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class DataSubset extends GridCoverage {
+ /**
+ * The GeoTIFF reader which contain this {@code DataSubset}.
+ * Used for fetching information like the input channel and where to report warnings.
+ */
+ private final Reader reader;
+
+ /**
+ * For each tile, the byte offset of that tile as compressed and stored on disk.
+ * Tile X index varies fastest, followed by tile Y index, then tile Z index if any.
+ *
+ * @see ImageFileDirectory#tileOffsets
+ * @see #tileStrides
+ */
+ private final Vector tileOffsets;
+
+ /**
+ * For each tile, the number of (compressed) bytes in that tile.
+ * Elements are in the same order than {@link #tileOffsets}.
+ *
+ * @see ImageFileDirectory#tileByteCounts
+ * @see #tileStrides
+ */
+ private final Vector tileByteCounts;
+
+ /**
+ * Number of tiles along each dimension, inside the domain specified by the user.
+ * The array length is 2 for a standard TIFF file, but this class accepts a higher number of
+ * dimensions if 3- or 4-dimensional data are stored in a GeoTIFF file using some convention.
+ * This is the number of tiles in the requested domain, not necessarily in the whole image on file.
+ * The first tile is located at indices (0, 0, …).
+ */
+ private final int[] numTiles;
+
+ /**
+ * Values by which to multiply each tile coordinates for obtaining the index in
+ * {@link #tileOffsets} and {@link #tileByteCounts} vectors. The length of this
+ * array is same as {@link #numTiles}.
+ */
+ private final int[] tileStrides;
+
+ /**
+ * The method for transferring pixel values from GeoTIFF file to this {@code Raster}.
+ * There is different subtypes depending on decompression methods.
+ */
+ private final PixelDecoder decoder;
+
+ /**
+ * Creates a new data subset. All parameters should have been validated
+ * by {@link ImageFileDirectory#validateMandatoryTags()} before this call.
+ *
+ * @param parent the resource which is creating this {@code DataSubset}.
+ * @param domain the sub-region extent, CRS and conversion from cell indices to CRS.
+ * @param ranges sample dimensions for each image band.
+ * @param tileOffsets the byte offset of each tile relative to the beginning of the TIFF file.
+ * @param tileByteCounts number of byte for each tile stored in the TIFF file.
+ * @param numTiles number of tiles along each dimension, inside the domain specified by the user.
+ * @param tileStrides values by which to multiply each tile coordinates for obtaining index in vectors.
+ * @throws ArithmeticException if the number of tiles overflows 32 bits integer arithmetic.
+ */
+ DataSubset(final DataCube parent, final GridGeometry domain, final List<? extends SampleDimension> ranges,
+ final Vector tileOffsets, final Vector tileByteCounts, final long[] numTiles, final int[] tileStrides,
+ final PixelDecoder decoder)
+ {
+ super(domain, ranges);
+ this.reader = parent.reader;
+ this.tileOffsets = tileOffsets;
+ this.tileByteCounts = tileByteCounts;
+ this.tileStrides = tileStrides;
+ this.decoder = decoder;
+ this.numTiles = new int[numTiles.length];
+ for (int i=0; i<numTiles.length; i++) {
+ this.numTiles[i] = Math.toIntExact(numTiles[i]);
+ }
+ }
+
+ /**
+ * Reads the tile at the given tile coordinates.
+ * This method is thread-safe.
+ *
+ * @param tile indices of the tile to read.
+ * @return image decoded from the GeoTIFF file.
+ * @throws IOException if an I/O error occurred.
+ * @throws DataStoreContentException if the sample model is not supported.
+ * @throws RuntimeException if the Java2D image can not be created for another reason
+ * (too many exception types to list them all).
+ */
+ private WritableRaster readTile(final int[] tile) throws IOException, DataStoreContentException {
+ synchronized (reader.store) {
+ int index = 0;
+ for (int i=0; i<tileStrides.length; i++) {
+ final int t = tile[i];
+ ArgumentChecks.ensureBetween("tile", 0, numTiles[i] - 1, t);
+ index = Math.addExact(index, Math.multiplyExact(tileStrides[i], t));
+ }
+ final long offset = Math.addExact(reader.origin, tileOffsets.longValue(index));
+ return decoder.readSlice(reader.input, offset);
+ }
+ }
+
+ @Override
+ public RenderedImage render(final GridExtent sliceExtent) {
+ throw new UnsupportedOperationException("Not supported yet.");
+ }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
index 1ff3a40..3f9d7da 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTIFF.java
@@ -64,7 +64,7 @@ abstract class GeoTIFF implements Closeable {
/**
* The store which created this reader or writer.
*/
- final GeoTiffStore owner;
+ final GeoTiffStore store;
/**
* The object to use for parsing and formatting dates. Created when first needed.
@@ -74,22 +74,22 @@ abstract class GeoTIFF implements Closeable {
/**
* For subclass constructors.
*/
- GeoTIFF(final GeoTiffStore owner) {
- this.owner = owner;
+ GeoTIFF(final GeoTiffStore store) {
+ this.store = store;
}
/**
* Returns the resources to use for formatting error messages.
*/
final Errors errors() {
- return Errors.getResources(owner.getLocale());
+ return Errors.getResources(store.getLocale());
}
/**
* Returns the GeoTIFF-specific resource for error messages and warnings.
*/
final Resources resources() {
- return Resources.forLocale(owner.getLocale());
+ return Resources.forLocale(store.getLocale());
}
/**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
index d8175b6..aba07c6 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -24,6 +24,7 @@ import java.net.URI;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import org.opengis.util.NameSpace;
import org.opengis.util.NameFactory;
@@ -90,6 +91,15 @@ public class GeoTiffStore extends DataStore implements Aggregate {
private final URI location;
/**
+ * Same value than {@link #location} but as a path, or {@code null} if none.
+ * Stored separately because conversion from path to URI back to path is not
+ * looseness (relative paths become absolutes).
+ *
+ * @todo May become an array later if we want to handle TFW and PRJ file heres.
+ */
+ final Path path;
+
+ /**
* The data store identifier created from the filename, or {@code null} if none.
* Defined as a namespace for use as the scope of children resources (the images).
*/
@@ -128,6 +138,7 @@ public class GeoTiffStore extends DataStore implements Aggregate {
connector.getStorage(), connector.getOption(OptionKey.OPEN_OPTIONS));
}
location = connector.getStorageAs(URI.class);
+ path = connector.getStorageAs(Path.class);
connector.closeAllExcept(input);
try {
reader = new Reader(this, input);
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
index 3a6973f..3ca072d 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/GridGeometryBuilder.java
@@ -182,6 +182,7 @@ final class GridGeometryBuilder {
/**
* The grid geometry to be created by {@link #build(long, long)}.
+ * It has 2 or 3 dimensions, depending on whether the CRS declares a vertical axis or not.
*/
public GridGeometry gridGeometry;
@@ -295,7 +296,7 @@ final class GridGeometryBuilder {
if (e instanceof NoSuchAuthorityCodeException) {
key = Resources.Keys.UnknownCRS_1;
}
- reader.owner.warning(reader.resources().getString(key, reader.owner.getDisplayName()), e);
+ reader.store.warning(reader.resources().getString(key, reader.store.getDisplayName()), e);
} catch (IllegalArgumentException | NoSuchElementException | ClassCastException e) {
if (!helper.alreadyReported) {
canNotCreate(e);
@@ -304,7 +305,7 @@ final class GridGeometryBuilder {
}
/*
* If the CRS is non-null, then it is either two- or three-dimensional.
- * The 'affine' matrix may be for a greater number of dimensions, so it
+ * The `affine` matrix may be for a greater number of dimensions, so it
* may need to be reduced.
*/
int n = (crs != null) ? crs.getCoordinateSystem().getDimension() : 2;
@@ -392,6 +393,6 @@ final class GridGeometryBuilder {
* Logs a warning telling that we can not create a grid geometry for the given reason.
*/
private void canNotCreate(final Exception e) {
- reader.owner.warning(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, reader.input.filename), e);
+ reader.store.warning(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, reader.input.filename), e);
}
}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
index 5456d32..7c7f838 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/ImageFileDirectory.java
@@ -33,18 +33,18 @@ import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.apache.sis.internal.geotiff.Resources;
import org.apache.sis.internal.storage.MetadataBuilder;
-import org.apache.sis.internal.storage.AbstractGridResource;
import org.apache.sis.internal.storage.io.ChannelDataInput;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
-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.coverage.SampleDimension;
import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
import org.apache.sis.math.Vector;
import org.apache.sis.measure.Units;
+import org.apache.sis.image.DataType;
/**
@@ -55,14 +55,14 @@ import org.apache.sis.measure.Units;
* @author Johann Sorel (Geomatys)
* @author Thi Phuong Hao Nguyen (VNSC)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
*
* @see <a href="http://www.awaresystems.be/imaging/tiff/tifftags.html">TIFF Tag Reference</a>
*
* @since 0.8
* @module
*/
-final class ImageFileDirectory extends AbstractGridResource {
+final class ImageFileDirectory extends DataCube {
/**
* Possible value for the {@link #tileTagFamily} field. That field tells whether image tiling
* was specified using the {@code Tile*} family of TIFF tags or the {@code Strip*} family.
@@ -77,12 +77,6 @@ final class ImageFileDirectory extends AbstractGridResource {
private static final byte SIGNED = 1, UNSIGNED = 0, FLOAT = 3;
/**
- * The GeoTIFF reader which contain this {@code ImageFileDirectory}.
- * Used for fetching information like the input channel and where to report warnings.
- */
- private final Reader reader;
-
- /**
* The identifier as a sequence number in the namespace of the {@link GeoTiffStore}.
* The first image has the sequence number "1".
*
@@ -140,7 +134,7 @@ final class ImageFileDirectory extends AbstractGridResource {
* The offset is specified with respect to the beginning of the TIFF file.
* Each tile has a location independent of the locations of other tiles
*
- * <p>Offsets are ordered left-to-right and top-to-bottom. if {@link #isPlanar} is {@code true}
+ * <p>Offsets are ordered left-to-right and top-to-bottom. If {@link #isPlanar} is {@code true}
* (i.e. components are stored in separate “component planes”), then the offsets for the first
* component plane are stored first, followed by all the offsets for the second component plane,
* and so on.</p>
@@ -364,9 +358,8 @@ final class ImageFileDirectory extends AbstractGridResource {
* @param index the image index as a sequence number starting with 0 for the first image.
*/
ImageFileDirectory(final Reader reader, final int index) {
- super(reader.owner.listeners());
- this.reader = reader;
- identifier = reader.nameFactory.createLocalName(reader.owner.identifier, String.valueOf(index + 1));
+ super(reader);
+ identifier = reader.nameFactory.createLocalName(reader.store.identifier, String.valueOf(index + 1));
}
/**
@@ -379,15 +372,8 @@ final class ImageFileDirectory extends AbstractGridResource {
/**
* Shortcut for a frequently requested information.
*/
- private String filename() {
- return input().filename;
- }
-
- /**
- * Shortcut for a frequently requested information.
- */
private Charset encoding() {
- return reader.owner.encoding;
+ return reader.store.encoding;
}
/**
@@ -573,9 +559,9 @@ final class ImageFileDirectory extends AbstractGridResource {
case Tags.BitsPerSample: {
final Vector values = type.readVector(input(), count);
/*
- * The current implementation requires that all 'bitsPerSample' elements have the same value.
+ * The current implementation requires that all `bitsPerSample` elements have the same value.
* This restriction may be revisited in future Apache SIS versions.
- * Note: 'count' is never zero when this method is invoked, so we do not need to check bounds.
+ * Note: `count` is never zero when this method is invoked, so we do not need to check bounds.
*/
bitsPerSample = values.shortValue(0);
final int length = values.size();
@@ -706,7 +692,7 @@ final class ImageFileDirectory extends AbstractGridResource {
break;
}
/*
- * Stores all of the 'double' valued GeoKeys, referenced by the GeoKeyDirectory.
+ * Stores all of the `double` valued GeoKeys, referenced by the GeoKeyDirectory.
*/
case Tags.GeoDoubleParams: {
referencing().numericParameters = type.readVector(input(), count);
@@ -777,7 +763,7 @@ final class ImageFileDirectory extends AbstractGridResource {
//// ////
//// Metadata for discovery purposes, conditions of use, etc. ////
//// Those metadata are not "critical" information for reading the image. ////
- //// Should not write anything under 'metadata/contentInfo' node. ////
+ //// Should not write anything under `metadata/contentInfo` node. ////
//// ////
////////////////////////////////////////////////////////////////////////////////////////////////
@@ -973,7 +959,7 @@ final class ImageFileDirectory extends AbstractGridResource {
i = s;
final Vector t = a; a = b; b = t;
}
- while (--i >= 0) { // At this point, 'b' shall be the longest vector.
+ while (--i >= 0) { // At this point, `b` shall be the longest vector.
final double va = a.doubleValue(i);
final double vb = b.doubleValue(i);
if (Double.isNaN(vb) || (max ? va > vb : va < vb)) {
@@ -1091,7 +1077,7 @@ final class ImageFileDirectory extends AbstractGridResource {
/*
* All of tile width, height and length information should be provided. But if only one of them is missing,
* we can compute it provided that the file does not use any compression method. If there is a compression,
- * then we set a bit for preventing the 'switch' block to perform a calculation but we let the code performs
+ * then we set a bit for preventing the `switch` block to perform a calculation but we let the code performs
* the other checks in order to get an exception to be thrown with a good message.
*/
int missing = !isPlanar && compression.equals(Compression.NONE) ? 0 : 0b1000;
@@ -1113,7 +1099,7 @@ final class ImageFileDirectory extends AbstractGridResource {
missingTag(Tags.TileLength, tileHeight, true);
break;
}
- case 0b0100: { // Compute missing tile byte count.
+ case 0b0100: { // Compute missing tile byte count in uncompressed case.
final long tileByteCount = pixelToByteCount(Math.multiplyExact(tileWidth, tileHeight));
final long[] tileByteCountArray = new long[tileOffsets.size()];
Arrays.fill(tileByteCountArray, tileByteCount);
@@ -1242,19 +1228,24 @@ final class ImageFileDirectory extends AbstractGridResource {
/**
* Returns an object containing the image size, the CRS and the conversion from pixel indices to CRS coordinates.
+ * The grid geometry has 2 or 3 dimensions, depending on whether the CRS declares a vertical axis or not.
+ *
+ * @see #getTileSize()
*/
@Override
public GridGeometry getGridGeometry() throws DataStoreContentException {
- if (referencing != null) {
- GridGeometry gridGeometry = referencing.gridGeometry;
- if (gridGeometry == null) try {
- gridGeometry = referencing.build(imageWidth, imageHeight);
- } catch (FactoryException e) {
- throw new DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, filename()), e);
- }
- return gridGeometry;
- } else {
- return new GridGeometry(new GridExtent(imageWidth, imageHeight), null, null);
+ synchronized (reader.store) {
+ if (referencing != null) {
+ GridGeometry gridGeometry = referencing.gridGeometry;
+ if (gridGeometry == null) try {
+ gridGeometry = referencing.build(imageWidth, imageHeight);
+ } catch (FactoryException e) {
+ throw new DataStoreContentException(reader.resources().getString(Resources.Keys.CanNotComputeGridGeometry_1, filename()), e);
+ }
+ return gridGeometry;
+ } else {
+ return new GridGeometry(new GridExtent(imageWidth, imageHeight), null, null);
+ }
}
}
@@ -1264,32 +1255,93 @@ final class ImageFileDirectory extends AbstractGridResource {
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public List<SampleDimension> getSampleDimensions() throws DataStoreContentException {
- if (sampleDimensions == null) {
- final SampleDimension[] dimensions = new SampleDimension[samplesPerPixel];
- final SampleDimension.Builder builder = new SampleDimension.Builder();
- final InternationalString name = Vocabulary.formatInternational(Vocabulary.Keys.Value);
- for (int band = 0; band < samplesPerPixel;) {
- builder.addQualitative(name, minValues.get(Math.min(band, minValues.size()-1)),
- maxValues.get(Math.min(band, maxValues.size()-1)));
- dimensions[band] = builder.setName(++band).build();
- builder.clear();
- }
- sampleDimensions = UnmodifiableArrayList.wrap(dimensions);
+ synchronized (reader.store) {
+ if (sampleDimensions == null) {
+ final SampleDimension[] dimensions = new SampleDimension[samplesPerPixel];
+ final SampleDimension.Builder builder = new SampleDimension.Builder();
+ final InternationalString name = Vocabulary.formatInternational(Vocabulary.Keys.Value);
+ for (int band = 0; band < samplesPerPixel;) {
+ builder.addQualitative(name, minValues.get(Math.min(band, minValues.size()-1)),
+ maxValues.get(Math.min(band, maxValues.size()-1)));
+ dimensions[band] = builder.setName(++band).build();
+ builder.clear();
+ }
+ sampleDimensions = UnmodifiableArrayList.wrap(dimensions);
+ }
+ return sampleDimensions; // Safe because unmodifiable.
}
- return sampleDimensions; // Safe because unmodifiable.
}
/**
- * Loads a subset of the grid coverage represented by this resource.
+ * Returns the type of raster data. The enumeration values are restricted to types compatible with Java2D,
+ * at the cost of using more bits than {@link #bitsPerSample} if there is no exact match.
+ *
+ * @throws DataStoreContentException if the type is not recognized.
+ */
+ private DataType getDataType() throws DataStoreContentException {
+ final String format;
+ switch (sampleFormat) {
+ case SIGNED: {
+ if (bitsPerSample < Byte .SIZE) return DataType.BYTE;
+ if (bitsPerSample <= Short .SIZE) return DataType.SHORT;
+ if (bitsPerSample <= Integer.SIZE) return DataType.INT;
+ format = "int";
+ break;
+ }
+ case UNSIGNED: {
+ if (bitsPerSample <= Byte .SIZE) return DataType.BYTE;
+ if (bitsPerSample <= Short .SIZE) return DataType.USHORT;
+ if (bitsPerSample <= Integer.SIZE) return DataType.INT;
+ format = "unsigned";
+ break;
+ }
+ case FLOAT: {
+ if (bitsPerSample == Float .SIZE) return DataType.FLOAT;
+ if (bitsPerSample == Double .SIZE) return DataType.DOUBLE;
+ format = "float";
+ break;
+ }
+ default: {
+ format = "?";
+ break;
+ }
+ }
+ throw new DataStoreContentException(Errors.format(
+ Errors.Keys.UnsupportedFormat_1, format + ' ' + bitsPerSample + " bits"));
+ }
+
+ /**
+ * Returns the type to use for storing raster data, together with the number of bits per sample.
+ * The type should be the one used in the GeoTIFF file as much as possible, but may sometime be wider.
+ *
+ * @throws DataStoreContentException if the type is not recognized.
+ */
+ @Override
+ protected Layout getLayout() throws DataStoreContentException {
+ return new Layout(getDataType(), bitsPerSample, isPlanar);
+ }
+
+ /**
+ * Returns the size in pixels of all tiles in this data cube.
+ * The array length is exactly 2 for this class.
+ *
+ * @see #getGridGeometry()
+ */
+ @Override
+ protected int[] getTileSize() {
+ return new int[] {tileWidth, tileHeight};
+ }
+
+ /**
+ * Gets the stream position or the length in bytes of compressed tile arrays in the GeoTIFF file.
+ * Values in the returned vector are {@code long} primitive type.
*
- * @param domain desired grid extent and resolution, or {@code null} for reading the whole domain.
- * @param range 0-based index of sample dimensions to read, or an empty sequence for reading all ranges.
- * @return the grid coverage for the specified domain and range.
- * @throws DataStoreException if an error occurred while reading the grid coverage data.
+ * @param length {@code false} for {@code tileOffsets} or {@code true} for {@code tileByteCounts}.
+ * @return stream position (relative to file beginning) or length of compressed tile arrays, in bytes.
*/
@Override
- public GridCoverage read(final GridGeometry domain, final int... range) throws DataStoreException {
- throw new DataStoreException("Not yet implemented."); // TODO
+ Vector getTileArrayInfo(boolean length) {
+ return length ? tileByteCounts : tileOffsets;
}
/**
@@ -1301,7 +1353,7 @@ final class ImageFileDirectory extends AbstractGridResource {
*/
private void warning(final Level level, final short key, final Object... parameters) {
final LogRecord r = reader.resources().getLogRecord(level, key, parameters);
- reader.owner.warning(r);
+ reader.store.warning(r);
}
/**
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
new file mode 100644
index 0000000..9f743dd
--- /dev/null
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/PixelDecoder.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.storage.geotiff;
+
+import java.nio.Buffer;
+import java.io.IOException;
+import java.awt.image.DataBuffer;
+import java.awt.image.SampleModel;
+import java.awt.image.BandedSampleModel;
+import java.awt.image.PixelInterleavedSampleModel;
+import java.awt.image.MultiPixelPackedSampleModel;
+import java.awt.image.SinglePixelPackedSampleModel;
+import java.awt.image.WritableRaster;
+import org.apache.sis.image.DataType;
+import org.apache.sis.internal.coverage.j2d.ImageUtilities;
+import org.apache.sis.internal.coverage.j2d.RasterFactory;
+import org.apache.sis.internal.storage.io.Region;
+import org.apache.sis.internal.storage.io.ChannelDataInput;
+import org.apache.sis.internal.storage.io.HyperRectangleReader;
+import org.apache.sis.storage.DataStoreContentException;
+import org.apache.sis.util.ArraysExt;
+
+
+/**
+ * Method for transferring pixel values from GeoTIFF tile to a {@link Raster}.
+ * The base class transfers uncompressed data. Compressed data are handled by
+ * specialized subclasses.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+class PixelDecoder {
+ /**
+ * Size of all tiles in the GeoTIFF image (without clipping and subsampling).
+ */
+ private final int[] tileSize;
+
+ /**
+ * Subsampling along each dimension. All values shall be greater than zero.
+ */
+ private final int[] subsamplings;
+
+ /**
+ * The sample model for all rasters. The size of this sample model is the values of
+ * the two first elements of {@link #tileSize} divided by subsampling after clipping.
+ */
+ protected final SampleModel model;
+
+ /**
+ * Creates a new pixel reader for the given type.
+ *
+ * @param tileSize size of all tiles before clipping and subsampling.
+ * @param subsamplings subsampling along each dimension. All values shall be greater than zero.
+ * @param numBands the number of bands in the raster to read.
+ * @param layout the Java2D type to use in destination image and other information.
+ * @throws IllegalArgumentException if a {@link SampleModel} can not be created from the given parameters.
+ */
+ PixelDecoder(final int[] tileSize, final int[] subsamplings, final int numBands, final DataCube.Layout layout) {
+ this.subsamplings = subsamplings;
+ this.tileSize = tileSize;
+ final int width = tileSize[0] / subsamplings[0];
+ final int height = tileSize[1] / subsamplings[1];
+ final int type = layout.type.ordinal();
+ if (layout.bitsPerSample < DataBuffer.getDataTypeSize(type)) {
+ /*
+ * Supported types for both sample models are TYPE_BYTE, TYPE_USHORT, and TYPE_INT.
+ * Note that if the TIFF data are signed bytes, we have TYPE_SHORT which will cause
+ * an exception below.
+ */
+ if (numBands == 1) {
+ model = new MultiPixelPackedSampleModel(type, width, height, layout.bitsPerSample);
+ } else if (!layout.isPlanar) {
+ final int[] bitMasks = new int[numBands];
+ model = new SinglePixelPackedSampleModel(type, width, height, bitMasks);
+ } else {
+ // TODO: we can support that with a little bit more work.
+ throw new IllegalArgumentException("Unsupported data format.");
+ }
+ } else if (layout.isPlanar) {
+ tileSize[2] = numBands;
+ model = new BandedSampleModel(type, width, height, numBands);
+ } else {
+ model = new PixelInterleavedSampleModel(type, width, height, numBands,
+ Math.multiplyExact(numBands, width), ArraysExt.range(0, numBands));
+ }
+ }
+
+ /**
+ * Reads a two-dimensional slice of the data cube. Implementation in base class assumes uncompressed data.
+ * Subclasses must override for handling decompression.
+ *
+ * @param input the channel to read.
+ * @param offset position in the channel where tile data begins.
+ * @return image decoded from the GeoTIFF file.
+ * @throws IOException if an I/O error occurred.
+ * @throws DataStoreContentException if the sample model is not supported.
+ * @throws RuntimeException if the Java2D image can not be created for another reason
+ * (too many exception types to list them all).
+ */
+ WritableRaster readSlice(final ChannelDataInput input, final long offset)
+ throws IOException, DataStoreContentException
+ {
+ final HyperRectangleReader reader = new HyperRectangleReader(
+ ImageUtilities.toNumberEnum(model.getDataType()), input, offset);
+ /*
+ * "Banks" (in `java.awt.image.DataBuffer` sense) are synonymous to "bands" for planar image only.
+ * Otherwise there is only one bank not matter the amount of bands. Each bank is read separately.
+ */
+ final Buffer[] banks = new Buffer[model instanceof BandedSampleModel ? model.getNumBands() : 1];
+ final long[] size = new long[] {tileSize[0], tileSize[1], banks.length};
+ final int[] subsample2D = new int[] {subsamplings[0], subsamplings[1], 1};
+ final long[] regionLower = new long[3]; // TODO: allow subregion if only one tile along x or y.
+ final long[] regionUpper = new long[] {regionLower[0] + tileSize[0],
+ regionLower[1] + tileSize[1], 0};
+ for (int b=0; b < banks.length; b++) {
+ regionLower[2] = b;
+ regionUpper[2] = b + 1;
+ banks[b] = reader.readAsBuffer(new Region(size, regionLower, regionUpper, subsample2D));
+ }
+ final DataBuffer buffer = RasterFactory.wrap(DataType.forDataBufferType(model.getDataType()), banks);
+ return WritableRaster.createWritableRaster(model, buffer, null);
+ }
+}
diff --git a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
index 8302217..e6f90be 100644
--- a/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
+++ b/storage/sis-geotiff/src/main/java/org/apache/sis/storage/geotiff/Reader.java
@@ -50,7 +50,7 @@ import org.apache.sis.util.resources.Errors;
* @author Alexis Manin (Geomatys)
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 0.8
* @module
*/
@@ -63,7 +63,7 @@ final class Reader extends GeoTIFF {
/**
* Stream position of the first byte of the GeoTIFF file. This is usually zero.
*/
- private final long origin;
+ final long origin;
/**
* A multiplication factor for the size of pointers, expressed as a power of 2.
@@ -137,8 +137,8 @@ final class Reader extends GeoTIFF {
* @throws IOException if an error occurred while reading first bytes from the stream.
* @throws DataStoreException if the file is not encoded in the TIFF or BigTIFF format.
*/
- Reader(final GeoTiffStore owner, final ChannelDataInput input) throws IOException, DataStoreException {
- super(owner);
+ Reader(final GeoTiffStore store, final ChannelDataInput input) throws IOException, DataStoreException {
+ super(store);
this.input = input;
this.origin = input.getStreamPosition();
this.metadata = new MetadataBuilder();
@@ -186,7 +186,7 @@ final class Reader extends GeoTIFF {
}
}
// Do not invoke this.errors() yet because GeoTiffStore construction may not be finished. Owner.error() is okay.
- throw new DataStoreContentException(owner.errors().getString(Errors.Keys.UnexpectedFileFormat_2, "TIFF", input.filename));
+ throw new DataStoreContentException(store.errors().getString(Errors.Keys.UnexpectedFileFormat_2, "TIFF", input.filename));
}
/**
@@ -215,7 +215,7 @@ final class Reader extends GeoTIFF {
if (pointer >= 0) {
return pointer;
}
- throw new DataStoreContentException(owner.getLocale(), "TIFF", input.filename, null);
+ throw new DataStoreContentException(store.getLocale(), "TIFF", input.filename, null);
}
/**
@@ -232,7 +232,7 @@ final class Reader extends GeoTIFF {
if (entry >= 0) {
return entry;
}
- throw new DataStoreContentException(owner.getLocale(), "TIFF", input.filename, null);
+ throw new DataStoreContentException(store.getLocale(), "TIFF", input.filename, null);
}
/**
@@ -388,7 +388,7 @@ final class Reader extends GeoTIFF {
exception = null;
}
args[0] = Tags.name(tag);
- owner.warning(errors().getString(key, args), exception);
+ store.warning(errors().getString(key, args), exception);
}
/**