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 2026/06/10 18:31:51 UTC
(sis) branch geoapi-4.0 updated (7d72c08606 -> ca60298d0c)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
from 7d72c08606 fix(JTS): compute 3D and 4D envelopes in JTS geometry Wrapper.getEnvelope() method
new c4cb4db1ca Move the code that search for Z and M values in a separated classes. It make easier to manage the ranges, the flags about Z and M presence and the dimensions all together.
new 593a479d5c Javadoc, formatting and warning suppressions.
new ca60298d0c Add support for writing pyramided GeoTIFF. This is an important step toward a COG writer, but not yet fully COG compliant.
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../sis/geometry/wrapper/jts/GeometryWalker.java | 184 ++++++++++++
.../apache/sis/geometry/wrapper/jts/Wrapper.java | 112 ++------
.../main/org/apache/sis/image/ComputedImage.java | 2 +-
.../main/org/apache/sis/image/ImageProcessor.java | 34 ++-
.../main/org/apache/sis/image/OverviewImage.java | 313 +++++++++++++++++++++
.../main/org/apache/sis/image/PixelIterator.java | 6 +-
.../main/org/apache/sis/image/ResampledImage.java | 2 +-
.../org/apache/sis/image/BandedIteratorTest.java | 1 +
.../org/apache/sis/image/ComputedImageTest.java | 1 +
.../test/org/apache/sis/image/DataTypeTest.java | 1 +
.../test/org/apache/sis/image/ImageLayoutTest.java | 1 +
.../org/apache/sis/image/ImageOverlayTest.java | 1 +
.../test/org/apache/sis/image/ImageTestCase.java | 1 +
.../org/apache/sis/image/InterpolationTest.java | 1 +
.../org/apache/sis/image/LinearIteratorTest.java | 1 +
.../test/org/apache/sis/image/MaskedImageTest.java | 1 +
.../org/apache/sis/image/OverviewImageTest.java | 138 +++++++++
.../test/org/apache/sis/image/TiledImageMock.java | 17 ++
.../apache/sis/storage/geotiff/FormatModifier.java | 19 +-
.../apache/sis/storage/geotiff/GeoTiffStore.java | 22 +-
.../org/apache/sis/storage/geotiff/Writer.java | 31 +-
.../sis/storage/geotiff/GeoTiffStoreTest.java | 78 +++++
22 files changed, 862 insertions(+), 105 deletions(-)
create mode 100644 endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
create mode 100644 endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java
create mode 100644 endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java
(sis) 01/03: Move the code that search for Z and M values in a separated classes. It make easier to manage the ranges, the flags about Z and M presence and the dimensions all together.
Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit c4cb4db1cade5191a99942ed8c0fc07be94f9ed0
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jun 10 20:22:34 2026 +0200
Move the code that search for Z and M values in a separated classes.
It make easier to manage the ranges, the flags about Z and M presence
and the dimensions all together.
---
.../sis/geometry/wrapper/jts/GeometryWalker.java | 184 +++++++++++++++++++++
.../apache/sis/geometry/wrapper/jts/Wrapper.java | 112 +++----------
2 files changed, 209 insertions(+), 87 deletions(-)
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
new file mode 100644
index 0000000000..96033f0ad8
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/GeometryWalker.java
@@ -0,0 +1,184 @@
+/*
+ * 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.geometry.wrapper.jts;
+
+import org.locationtech.jts.geom.CoordinateSequence;
+import org.locationtech.jts.geom.Geometry;
+import org.locationtech.jts.geom.GeometryCollection;
+import org.locationtech.jts.geom.LineString;
+import org.locationtech.jts.geom.Point;
+import org.locationtech.jts.geom.Polygon;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Walks through the component of a geometry for finding the number of dimensions
+ * and the range of <abbr>z</abbr> and <abbr>m</abbr> values.
+ *
+ * @author Johann Sorel (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class GeometryWalker {
+ /**
+ * Whether to search for ranges of <var>z</var> and <var>M</var> values.
+ * If {@code false}, this method will check only for the number of dimensions.
+ */
+ private boolean wantZM;
+
+ /**
+ * Whether the coordinate sequence declares to have <var>z</var> or <var>M</var> values.
+ */
+ private boolean hasZ, hasM;
+
+ /**
+ * Range of <var>z</var> values.
+ */
+ private double minZ, maxZ;
+
+ /**
+ * Range of <var>M</var> values.
+ */
+ private double minM, maxM;
+
+ /**
+ * The maximal number of <em>vertex</em> dimensions found. Note that this is different than the
+ * {@linkplain Geometry#getDimension() geometry topological dimension}, which can be 0, 1 or 2.
+ * The <abbr>JTS</abbr> vertex dimension can be 2, 3 or 4.
+ */
+ private int dimension;
+
+ /**
+ * Creates a new walker.
+ * By default, the walker does <em>not</em> search for ranges of <var>z</var> and <var>M</var> values.
+ * If this search is desired, the {@link #wantZM()} method must be invoked after construction.
+ */
+ GeometryWalker() {
+ }
+
+ /**
+ * Requests the search of ranges of <var>z</var> and <var>M</var> values.
+ */
+ final void wantZM() {
+ wantZM = true;
+ minZ = minM = Double.POSITIVE_INFINITY;
+ maxZ = maxM = Double.NEGATIVE_INFINITY;
+ }
+
+ /**
+ * Returns the number of dimensions found by the calls to {@link #scan(Geometry)}.
+ */
+ final int dimension() {
+ if (hasZ & hasM) return Math.max(4, dimension);
+ if (hasZ | hasM) return Math.max(3, dimension);
+ return dimension;
+ }
+
+ /**
+ * Sets the <var>z</var> and <var>M</var> coordinates in the given envelope.
+ *
+ * @param env the envelope where to set the coordinates.
+ */
+ final void setZM(final GeneralEnvelope env) {
+ if (wantZM) {
+ final int limit = env.getDimension();
+ int target = Factory.BIDIMENSIONAL;
+ if (target < limit) {
+ if (minZ <= maxZ) {
+ env.setRange(target, minZ, maxZ);
+ }
+ if (minM <= maxM) {
+ if (hasZ) {
+ // Increment even if the range of Z was undefined.
+ target = Factory.TRIDIMENSIONAL;
+ if (target >= limit) return;
+ }
+ env.setRange(target, minM, maxM);
+ }
+ }
+ }
+ }
+
+ /**
+ * Scans the given geometry for its number of dimensions and range of <var>z</var> and <var>M</var> values.
+ * This method may invoke itself recursively for walking through components of the given geometry.
+ *
+ * @param geometry the geometry to inspect.
+ * @throws IllegalArgumentException if the type of the given geometry is not recognized.
+ */
+ final void scan(final Geometry geometry) {
+ if (geometry instanceof Point) {
+ // Most efficient method (no allocation) in JTS 1.18.
+ addDimensionAndRanges(((Point) geometry).getCoordinateSequence());
+ } else if (geometry instanceof LineString) {
+ // Most efficient method (no allocation) in JTS 1.18.
+ addDimensionAndRanges(((LineString) geometry).getCoordinateSequence());
+ } else if (geometry instanceof Polygon) {
+ final var polygon = (Polygon) geometry;
+ scan(polygon.getExteriorRing());
+ if (wantZM) {
+ final int n = polygon.getNumInteriorRing();
+ for (int i=0; i<n; i++) {
+ scan(polygon.getInteriorRingN(i));
+ }
+ }
+ } else if (geometry instanceof GeometryCollection) {
+ final var gc = (GeometryCollection) geometry;
+ final int n = gc.getNumGeometries();
+ if (n > 0) {
+ for (int i=0; i<n; i++) {
+ scan(gc.getGeometryN(i));
+ }
+ } else if (dimension == 0) {
+ // Undefined coordinates, JTS assumes 3 for empty geometries.
+ dimension = Factory.TRIDIMENSIONAL;
+ }
+ } else {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, geometry.getGeometryType()));
+ }
+ }
+
+ /**
+ * Updates the number of dimensions and the range of <var>z</var> and <var>M</var> values
+ * from the given coordinate sequence.
+ *
+ * @param cs the coordinate sequence from which to get the number of dimensions
+ * and the range of (<var>z</var>, <var>M</var>) values.
+ */
+ private void addDimensionAndRanges(final CoordinateSequence cs) {
+ dimension = Math.max(dimension, cs.getDimension());
+ if (wantZM) {
+ final int n = cs.size();
+ if (cs.hasZ()) {
+ hasZ = true;
+ for (int i=0; i<n; i++) {
+ double z = cs.getZ(i);
+ if (z < minZ) minZ = z;
+ if (z > maxZ) maxZ = z;
+ }
+ }
+ if (cs.hasM()) {
+ hasM = true;
+ for (int i=0; i<n; i++) {
+ double m = cs.getM(i);
+ if (m < minM) minM = m;
+ if (m > maxM) maxM = m;
+ }
+ }
+ }
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
index cd670f0c8f..4f77c2e54d 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/geometry/wrapper/jts/Wrapper.java
@@ -26,7 +26,6 @@ import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.IntFunction;
import org.locationtech.jts.geom.Coordinate;
-import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollection;
@@ -68,7 +67,7 @@ import org.opengis.filter.DistanceOperatorName;
/**
- * The wrapper of Java Topology Suite (JTS) geometries.
+ * The wrapper of Java Topology Suite (<abbr>JTS</abbr>) geometries.
*
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
@@ -147,72 +146,13 @@ final class Wrapper extends GeometryWrapper {
*/
@Override
public int getCoordinateDimension() {
- return getCoordinatesDimension(geometry, null);
- }
-
- /**
- * Gets the number of dimensions of geometry vertex (sequence of coordinate tuples), which can be 2 or 3.
- * Note that this is different than the {@linkplain Geometry#getDimension() geometry topological dimension},
- * which can be 0, 1 or 2.
- *
- * @param geometry the geometry for which to get <em>vertex</em> (not topological) dimension.
- * @param bounds if defined, compute bounds, array must be of size 8 for [minX,minY,minZ,minM,maxX,maxY,maxZ,maxM]
- * @return vertex dimension of the given geometry.
- * @throws IllegalArgumentException if the type of the given geometry is not recognized.
- */
- private static int getCoordinatesDimension(final Geometry geometry, Double[] bounds) {
- int dim = 0;
- if (geometry instanceof Point) {
- // Most efficient method (no allocation) in JTS 1.18.
- dim = getCoordinatesDimension(dim, ((Point) geometry).getCoordinateSequence(), bounds);
- } else if (geometry instanceof LineString) {
- // Most efficient method (no allocation) in JTS 1.18.
- dim = getCoordinatesDimension(dim, ((LineString) geometry).getCoordinateSequence(), bounds);
- } else if (geometry instanceof Polygon) {
- final Polygon polygon = (Polygon) geometry;
- dim = getCoordinatesDimension(dim, ((LineString) polygon.getExteriorRing()).getCoordinateSequence(), bounds);
- for (int i = 0, n = polygon.getNumInteriorRing(); i < n; i++) {
- dim = getCoordinatesDimension(dim, ((LineString) polygon.getInteriorRingN(i)).getCoordinateSequence(), bounds);
- }
-
- } else if (geometry instanceof GeometryCollection) {
- final GeometryCollection gc = (GeometryCollection) geometry;
- final int n = gc.getNumGeometries();
- if (n == 0) {
- dim = Factory.TRIDIMENSIONAL;// Undefined coordinates, JTS assumes 3 for empty geometries.
- }
- for (int i = 0; i < n; i++) {
- dim = Math.max(dim, getCoordinatesDimension(gc.getGeometryN(i), bounds));
- }
- } else {
- throw new IllegalArgumentException(Errors.format(Errors.Keys.UnknownType_1, geometry.getGeometryType()));
+ int dimension = CRS.getDimensionOrZero(crs);
+ if (dimension == 0) {
+ final var walker = new GeometryWalker();
+ walker.scan(geometry);
+ dimension = walker.dimension();
}
- return dim;
- }
-
- /**
- * Get dimension and bounds of coordinate sequence.
- *
- * @param dim previously computed dimension
- * @param cs the coordinate sequence for which to get <em>vertex</em> (not topological) dimension and bounds
- * @param bounds if defined, compute bounds, array must be of size 8 for [minX,minY,minZ,minM,maxX,maxY,maxZ,maxM]
- * @return coordinate sequence dimension or previous dimension, returning the largest one.
- */
- private static int getCoordinatesDimension(int dim, CoordinateSequence cs, Double[] bounds) {
- final int dimension = cs.getDimension();
- if (bounds != null) {
- for (int i = 0, d = 0, n = cs.size(); i < n; d++) {
- if (d == dimension) {d = -1; i++; continue;}
- final double val = cs.getOrdinate(i, d);
- //check min
- if (bounds[d] == null) bounds[d] = val;
- else if (!bounds[d].isNaN()) bounds[d] = Double.min(bounds[d], val);
- //check max
- if (bounds[d+4] == null) bounds[d+4] = val;
- else if (!bounds[d+4].isNaN()) bounds[d+4] = Double.max(bounds[d+4], val);
- }
- }
- return Math.max(dim, dimension);
+ return dimension;
}
/**
@@ -223,29 +163,27 @@ final class Wrapper extends GeometryWrapper {
*/
@Override
public GeneralEnvelope getEnvelope() {
- if (crs != null && crs.getCoordinateSystem().getDimension() == 2) {
- //we can use JTS envelope internal
- final Envelope bounds = geometry.getEnvelopeInternal();
- final var env = new GeneralEnvelope(crs);
- env.setToNaN();
- if (!bounds.isNull()) {
- env.setRange(0, bounds.getMinX(), bounds.getMaxX());
- env.setRange(1, bounds.getMinY(), bounds.getMaxY());
+ final var walker = new GeometryWalker();
+ final GeneralEnvelope env;
+ if (crs != null) {
+ env = new GeneralEnvelope(crs);
+ if (env.getDimension() >= Factory.TRIDIMENSIONAL) {
+ walker.wantZM();
}
- return env;
+ walker.scan(geometry);
} else {
- //geometry has unknown or more then 2 dimensions
- final Double[] bounds = new Double[8];
- final int dim = getCoordinatesDimension(geometry, bounds);
- final var env = (crs != null) ? new GeneralEnvelope(crs) : new GeneralEnvelope(dim);
- env.setToNaN();
- for (int i = 0, n = Math.min(dim, env.getDimension()); i < n; i++) {
- if (bounds[i] != null){
- env.setRange(i, bounds[i], bounds[i+4]);
- }
- }
- return env;
+ walker.wantZM();
+ walker.scan(geometry);
+ env = new GeneralEnvelope(walker.dimension());
+ }
+ env.setToNaN();
+ final Envelope bounds = geometry.getEnvelopeInternal();
+ if (!bounds.isNull()) {
+ env.setRange(0, bounds.getMinX(), bounds.getMaxX());
+ env.setRange(1, bounds.getMinY(), bounds.getMaxY());
}
+ walker.setZM(env);
+ return env;
}
/**
(sis) 03/03: Add support for writing pyramided GeoTIFF. This is an important step toward a COG writer, but not yet fully COG compliant.
Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit ca60298d0caedd58e3382fe49b423edfb8e082be
Author: eidee <es...@geomatys.com>
AuthorDate: Wed Jun 10 17:55:28 2026 +0200
Add support for writing pyramided GeoTIFF.
This is an important step toward a COG writer, but not yet fully COG compliant.
Co-authored-by: Martin Desruisseaux <ma...@geomatys.com>
---
.../main/org/apache/sis/image/ImageProcessor.java | 34 ++-
.../main/org/apache/sis/image/OverviewImage.java | 313 +++++++++++++++++++++
.../org/apache/sis/image/OverviewImageTest.java | 138 +++++++++
.../test/org/apache/sis/image/TiledImageMock.java | 17 ++
.../apache/sis/storage/geotiff/FormatModifier.java | 19 +-
.../apache/sis/storage/geotiff/GeoTiffStore.java | 22 +-
.../org/apache/sis/storage/geotiff/Writer.java | 31 +-
.../sis/storage/geotiff/GeoTiffStoreTest.java | 78 +++++
8 files changed, 639 insertions(+), 13 deletions(-)
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
index 2d4391ebb9..c72874dbfe 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ImageProcessor.java
@@ -136,7 +136,7 @@ import org.apache.sis.measure.Units;
*
* @author Martin Desruisseaux (Geomatys)
* @author Alexis Manin (Geomatys)
- * @version 1.6
+ * @version 1.7
*
* @see org.apache.sis.coverage.grid.GridCoverageProcessor
*
@@ -1234,6 +1234,38 @@ public class ImageProcessor implements Cloneable {
return RecoloredImage.applySameColors(resampled, colored);
}
+ /**
+ * Creates an overview of the given image computed by the average of 4 pixels.
+ * NaN values are omitted from the average. If all values in a block of 4 pixels are NaN,
+ * then the first value (the upper-left corner of the 4 pixels block) is retained.
+ *
+ * <p>This overview is equivalent to the {@link #resample(RenderedImage, Rectangle, MathTransform) resample(…)}
+ * operation with a bilinear interpolation and the following transform, except that an overview can be created
+ * from the value of another overview, thus creating a pyramid. By contrast, the {@code resample(…)} operation
+ * may optimize with {@link MathTransform} concatenations, which is undesirable when creating a pyramid:</p>
+ *
+ * {@snippet lang="text" :
+ * ┌ ┐
+ * │ 2.0 0 0.5 │
+ * │ 0 2.0 0.5 │
+ * │ 0 0 1 │
+ * └ ┘
+ * }
+ *
+ * <h4>Result relationship with source</h4>
+ * Changes in the source image are reflected in the returned images
+ * if the source image notifies {@linkplain java.awt.image.TileObserver tile observers}.
+ *
+ * @param source the image for which to compute an overview.
+ * @return image overview.
+ *
+ * @since 1.7
+ */
+ public RenderedImage overview(final RenderedImage source) {
+ ArgumentChecks.ensureNonNull("source", source);
+ return unique(new OverviewImage(source));
+ }
+
/**
* Computes immediately all tiles in the given region of interest, then returns an image with those tiles ready.
* Computations will use many threads if {@linkplain #getExecutionMode() execution mode} is parallel.
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java
new file mode 100644
index 0000000000..1c381c7e96
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/OverviewImage.java
@@ -0,0 +1,313 @@
+/*
+ * 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.image;
+
+import java.awt.Rectangle;
+import java.awt.image.ColorModel;
+import java.awt.image.Raster;
+import java.awt.image.WritableRaster;
+import java.awt.image.RenderedImage;
+import java.awt.image.ImagingOpException;
+import org.apache.sis.feature.internal.Resources;
+import org.apache.sis.util.Disposable;
+import org.apache.sis.util.internal.shared.Numerics;
+import org.apache.sis.image.internal.shared.ImageUtilities;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.coverage.grid.SequenceType;
+
+
+/**
+ * An image which is the result of averaging 4 pixels of the image at higher resolution.
+ * It can be seen as a special case of {@link ResampledImage} with bilinear interpolation
+ * at the exact center of a block of 4 pixels.
+ *
+ * @todo Add an auxiliary image with contains the rest of the division by 4 (when sample values are integers)
+ * or the number of averaged sample values (when sample values are floating points).
+ * Use that information when computing overview of overviews, for better accuracy.
+ *
+ * @author Estelle Idée (Geomatys)
+ * @author Martin Desruisseaux (Geomatys)
+ */
+final class OverviewImage extends ComputedImage {
+ /**
+ * The image origin.
+ */
+ private final int minX, minY;
+
+ /**
+ * The image size in pixels.
+ */
+ private final int width, height;
+
+ /**
+ * The offset to add after conversion from target to source pixel coordinates.
+ * This is either 0 or 1.
+ */
+ private final byte offsetX, offsetY;
+
+ /**
+ * Creates a new image which will create an overview of the given image.
+ *
+ * @param source the image at higher resolution.
+ */
+ OverviewImage(final RenderedImage source) {
+ this(targetBounds(ImageUtilities.getBounds(source)), source);
+ }
+
+ /** Workaround for RFE #4093999 ("Relax constraint on placement of this()/super() call in constructors"). */
+ private static Rectangle targetBounds(final Rectangle bounds) {
+ bounds.x >>= 1; // Round toward negative infinity.
+ bounds.y >>= 1;
+ bounds.width /= 2; // Round toward 0.
+ bounds.height /= 2;
+ return bounds;
+ }
+
+ /** Workaround for RFE #4093999 ("Relax constraint on placement of this()/super() call in constructors"). */
+ private OverviewImage(final Rectangle bounds, final RenderedImage source) {
+ super(ImageLayout.DEFAULT.createCompatibleSampleModel(source, bounds), source);
+ offsetX = (byte) (source.getMinX() & 1); // TODO: move before `targetBounds(…)` after RFE #4093999.
+ offsetY = (byte) (source.getMinY() & 1);
+ minX = bounds.x;
+ minY = bounds.y;
+ width = bounds.width;
+ height = bounds.height;
+ }
+
+ /**
+ * Returns the color model of this resampled image.
+ * Default implementation assumes that this image has the same color model as the source image.
+ *
+ * @return the color model, or {@code null} if unspecified.
+ */
+ @Override
+ public ColorModel getColorModel() {
+ return getSource().getColorModel();
+ }
+
+ /**
+ * Returns the minimum tile index in the <var>x</var> direction.
+ */
+ @Override
+ public final int getMinTileX() {
+ return getSource().getMinTileX() / 2; // Round toward zero.
+ }
+
+ /**
+ * Returns the minimum tile index in the <var>y</var> direction.
+ */
+ @Override
+ public final int getMinTileY() {
+ return getSource().getMinTileY() / 2;
+ }
+
+ /**
+ * Returns the minimum <var>x</var> coordinate (inclusive) of this image.
+ */
+ @Override
+ public final int getMinX() {
+ return minX;
+ }
+
+ /**
+ * Returns the minimum <var>y</var> coordinate (inclusive) of this image.
+ */
+ @Override
+ public final int getMinY() {
+ return minY;
+ }
+
+ /**
+ * Returns the number of columns in this image.
+ */
+ @Override
+ public final int getWidth() {
+ return width;
+ }
+
+ /**
+ * Returns the number of rows in this image.
+ */
+ @Override
+ public final int getHeight() {
+ return height;
+ }
+
+ /**
+ * Invoked when a tile needs to be computed or updated.
+ *
+ * @param tileX the column index of the tile to compute.
+ * @param tileY the row index of the tile to compute.
+ * @param tile if the tile already exists but needs to be updated, the tile to update. Otherwise {@code null}.
+ * @return computed tile for the given indices.
+ */
+ @Override
+ protected Raster computeTile(final int tileX, final int tileY, WritableRaster tile) {
+ if (tile == null) {
+ tile = createTile(tileX, tileY);
+ }
+ Rectangle bounds = tile.getBounds();
+ bounds.width <<= 1;
+ bounds.height <<= 1;
+ bounds.x <<= 1;
+ bounds.y <<= 1;
+ bounds.x += offsetX;
+ bounds.y += offsetY;
+ final PixelIterator it = new PixelIterator.Builder()
+ .setIteratorOrder(SequenceType.LINEAR)
+ .setRegionOfInterest(bounds)
+ .create(getSource());
+ /*
+ * The iterator may have intersected the given bounds with the source image bounds.
+ * Therefore, we derive the limits from these bounds instead of from tile bounds.
+ * It should cover the whole valid area of the tile.
+ */
+ bounds = it.getDomain();
+ if (((bounds.width | bounds.height) & 1) != 0) {
+ throw new ImagingOpException(Resources.format(Resources.Keys.IncompatibleTile_2, tileX, tileY));
+ }
+ bounds.x >>= 1; // Round toward negative infinity.
+ bounds.y >>= 1;
+ bounds.width >>= 1;
+ bounds.height >>= 1;
+ final int numBands = tile.getNumBands();
+ final var buffer = new double[Math.multiplyExact(bounds.width, numBands)];
+ final var counts = new byte[buffer.length];
+ double[] left = null;
+ double[] right = null;
+ final int ymax = bounds.y + bounds.height;
+ for (int y = bounds.y; y < ymax; y++) {
+ int x = bounds.x;
+ /*
+ * Memorize the sum of two consecutive pixels for all pixels in the current source row.
+ * The `counts` array contains the number of valid values, which will by 2, 3 or 4 on
+ * the assumption that the next row will not contain NaN value (verified in next loop).
+ */
+ for (int i=0; i < buffer.length;) {
+ if (it.next()) {
+ left = it.getPixel(left);
+ if (it.next()) {
+ right = it.getPixel(right);
+ for (int b=0; b<numBands; b++) {
+ byte count = 4;
+ double sum = left[b] + right[b];
+ if (Double.isNaN(sum)) {
+ // Give precedence to the left side if both sides are NaN.
+ count = (Double.isNaN(sum = right[b]) &&
+ Double.isNaN(sum = left[b])) ? (byte) 2 : (byte) 3;
+ }
+ buffer[i] = sum;
+ counts[i++] = count;
+ }
+ continue;
+ }
+ }
+ throw new ImagingOpException(Resources.format(Resources.Keys.OutOfIteratorDomain_2, i/numBands + x, y));
+ }
+ /*
+ * Read the next row and compute the average with the previous row which was memorized by above loop.
+ * If some values are NaN, the number of valid values gien by `counts` is adjusted.
+ */
+ for (int i=0; i < buffer.length; x++) {
+ if (it.next()) {
+ left = it.getPixel(left);
+ if (it.next()) {
+ right = it.getPixel(right);
+ for (int b=0; b<numBands; b++, i++) {
+ int count = counts[i];
+ double add = left[b] + right[b];
+ double sum = add + buffer[i];
+ // Test `isNaN(sum)` first because it will be false in the vast majority of cases.
+ if (Double.isNaN(sum) && Double.isNaN(sum = add)) {
+ sum = buffer[i];
+ if (Double.isNaN(add = right[b]) && Double.isNaN(add = left[b])) {
+ count -= 2; // The two values of the current row are invalid.
+ } else {
+ count--; // Exactly one value of the current row is valid.
+ sum = Double.isNaN(sum) ? add : sum + add;
+ }
+ if (count <= 1) {
+ // Avoid a division by 0 in order to preserve the NaN bits pattern.
+ left[b] = sum;
+ continue;
+ }
+ }
+ left[b] = sum / count;
+ }
+ tile.setPixel(x, y, left);
+ continue;
+ }
+ }
+ throw new ImagingOpException(Resources.format(Resources.Keys.OutOfIteratorDomain_2, x, y));
+ }
+ }
+ return tile;
+ }
+
+ /**
+ * Notifies the source image that tiles will be computed soon in the given region.
+ * If the source image is an instance of {@link ComputedImage}, then this method
+ * forwards the notification to it.
+ */
+ @Override
+ protected Disposable prefetch(final Rectangle tiles) {
+ final RenderedImage source = getSource();
+ if (source instanceof PlanarImage) {
+ final long xmin = 2L * tiles.x + offsetX;
+ final long ymin = 2L * tiles.y + offsetY;
+ final long xmax = 2L * tiles.width + xmin;
+ final long ymax = 2L * tiles.height + ymin;
+ final int x = Numerics.clamp(xmin);
+ final int y = Numerics.clamp(ymin);
+ return ((PlanarImage) source).prefetch(
+ new Rectangle(x, y, Numerics.clamp(xmax - x),
+ Numerics.clamp(ymax - y)));
+ }
+ return super.prefetch(tiles);
+ }
+
+ /**
+ * Compares the given object with this image for equality.
+ *
+ * @param object the object to compare with this image.
+ * @return {@code true} if the given object is an image performing the same overview as this image.
+ */
+ @Override
+ public boolean equals(final Object object) {
+ if (equalsBase(object)) {
+ final var other = (OverviewImage) object;
+ return minX == other.minX &&
+ minY == other.minY &&
+ width == other.width &&
+ height == other.height &&
+ offsetX == other.offsetX &&
+ offsetY == other.offsetY;
+ }
+ return false;
+ }
+
+ /**
+ * Returns a hash code value for this image. The {@link #minX}, {@link #minY}, {@link #width} and {@link #height}
+ * fields are included in the hash computation as a matter of principle, but this is actually not very important
+ * because they are derived information.
+ */
+ @Override
+ public int hashCode() {
+ return hashCodeBase() + (minX + 31*(minY + 31*(width + 31*height)));
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java
new file mode 100644
index 0000000000..e67639cbd0
--- /dev/null
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/OverviewImageTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.image;
+
+import java.awt.Point;
+import java.awt.image.RenderedImage;
+import java.util.Random;
+
+// Specific to the geoapi-3.1 and geoapi-4.0 branches:
+import org.opengis.coverage.grid.SequenceType;
+
+// Test dependencies
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.test.TestUtilities;
+
+
+/**
+ * Tests {@link OverviewImage}.
+ *
+ * @author Estelle Idée (Geomatys)
+ */
+@SuppressWarnings("exports")
+public final class OverviewImageTest extends TestCase {
+ /**
+ * Creates a new test case.
+ */
+ public OverviewImageTest() {
+ }
+
+ /**
+ * Tests on an image filled with integer values.
+ */
+ @Test
+ public void testOnIntegers() {
+ testForType(DataType.INT);
+ }
+
+ /**
+ * Tests on an image filled with floating point values.
+ * Some random values are set to NaN.
+ */
+ @Test
+ public void testOnFloats() {
+ testForType(DataType.DOUBLE);
+ }
+
+ /**
+ * Runs the test on an image of the specified type.
+ *
+ * @param type type of data stored in the image.
+ */
+ private static void testForType(final DataType type) {
+ final Random r = TestUtilities.createRandomNumberGenerator();
+ final var source = new TiledImageMock(
+ type.toDataBufferType(),
+ r.nextInt( 2) + 1, // num bands
+ r.nextInt( 9) - 4, // min X
+ r.nextInt( 9) - 4, // min Y
+ r.nextInt(20) + 10, // width
+ r.nextInt(20) + 10, // height
+ r.nextInt( 5) + 5, // tile width
+ r.nextInt( 5) + 5, // tile height
+ r.nextInt( 9) - 4, // min tile X
+ r.nextInt( 9) - 4, // min tile Y
+ true); // banded
+
+ source.initializeAllTiles();
+ if (!type.isInteger()) {
+ source.setRandomNaN(r);
+ }
+ verify(source, new OverviewImage(source), type.isInteger());
+ }
+
+ /**
+ * Verifies an image which is expected to be the result of an image overview operation.
+ *
+ * @param source the image used for computing the overview.
+ * @param target the result of the image overview operation.
+ * @param isInteger whether the images use an integer type.
+ */
+ public static void verify(final RenderedImage source, final RenderedImage target, final boolean isInteger) {
+ assertEquals(source.getWidth() / 2, target.getWidth());
+ assertEquals(source.getHeight() / 2, target.getHeight());
+ final int offsetX = source.getMinX() & 1;
+ final int offsetY = source.getMinY() & 1;
+
+ double[] p00 = null, p01 = null, p10 = null, p11 = null;
+ double[] actual = null;
+
+ final PixelIterator itSource = new PixelIterator.Builder().setIteratorOrder(SequenceType.LINEAR).create(source);
+ final PixelIterator itTarget = PixelIterator.create(target);
+ int count = 0;
+ while (itTarget.next()) {
+ final Point p = itTarget.getPosition();
+ final int sx = p.x * 2 + offsetX;
+ final int sy = p.y * 2 + offsetY;
+
+ // Read 2×2 block from source.
+ itSource.moveTo(sx, sy); p00 = itSource.getPixel(p00);
+ assertTrue(itSource.next()); p01 = itSource.getPixel(p01);
+ itSource.moveTo(sx, sy + 1); p10 = itSource.getPixel(p10);
+ assertTrue(itSource.next()); p11 = itSource.getPixel(p11);
+
+ actual = itTarget.getPixel(actual);
+ for (int b = 0; b < actual.length; b++) {
+ int n = 0;
+ double sum = 0, v;
+ if (!Double.isNaN(v = p00[b])) {sum += v; n++;}
+ if (!Double.isNaN(v = p01[b])) {sum += v; n++;}
+ if (!Double.isNaN(v = p10[b])) {sum += v; n++;}
+ if (!Double.isNaN(v = p11[b])) {sum += v; n++;}
+ double expected = (n != 0) ? sum / n : Double.NaN;
+ if (isInteger) {
+ expected = (int) expected;
+ }
+ assertEquals(expected, actual[b], 1E-10, () -> "Mismatch at (" + p.x + ", " + p.y + ')');
+ }
+ count++;
+ }
+ assertEquals(target.getWidth() * target.getHeight(), count);
+ }
+}
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
index 1db6fcd075..70989ebf3b 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/TiledImageMock.java
@@ -262,6 +262,23 @@ public final class TiledImageMock extends PlanarImage implements WritableRendere
}
}
+ /**
+ * Sets random values to NaN. This method assumes that the image use floating points.
+ *
+ * @param random random number generator to use.
+ */
+ public synchronized void setRandomNaN(final Random random) {
+ final int numBands = sampleModel.getNumBands();
+ for (int i = random.nextInt(StrictMath.max(StrictMath.multiplyExact(width, height) / 8, 4)) + 5; --i >= 0;) {
+ final int ox = random.nextInt(width);
+ final int oy = random.nextInt(height);
+ final int b = random.nextInt(numBands);
+ tile(StrictMath.floorDiv(ox, tileWidth) + minTileX,
+ StrictMath.floorDiv(oy, tileHeight) + minTileY, true)
+ .setSample(ox + minX, oy + minY, b, Double.NaN);
+ }
+ }
+
/**
* Sets a sample value at the given location in pixel coordinates.
* This is a helper method for testing purpose on small images only,
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/FormatModifier.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/FormatModifier.java
index 23e2dbfe91..612327917b 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/FormatModifier.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/FormatModifier.java
@@ -23,7 +23,8 @@ import org.apache.sis.io.stream.InternalOptionKey;
/**
* Characteristics of the GeoTIFF file to write.
- * The modifiers can control, for example, the maximal size and number of images that can be stored in a TIFF file.
+ * The modifiers can control, for example, the maximal size and number of images
+ * that can be stored in a <abbr>TIFF</abbr> file.
*
* <p>The modifiers can be specified as an option when opening the data store.
* For example for writing a BigTIFF file, the following code can be used:</p>
@@ -39,7 +40,8 @@ import org.apache.sis.io.stream.InternalOptionKey;
* }
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.5
+ * @author Estelle Idée (Geomatys)
+ * @version 1.7
*
* @see GeoTiffStore#getModifiers()
*
@@ -47,7 +49,7 @@ import org.apache.sis.io.stream.InternalOptionKey;
*/
public enum FormatModifier {
/**
- * The Big TIFF extension (non-standard).
+ * The Big <abbr>TIFF</abbr> extension (non-standard).
* When this modifier is absent (which is the default), the standard TIFF format as defined by Adobe is used.
* That standard uses the addressable space of 32-bits integers, which allows a maximal file size of about 4 GB.
* When the {@code BIG_TIFF} modifier is present, the addressable space of 64-bits integers is used.
@@ -55,6 +57,17 @@ public enum FormatModifier {
*/
BIG_TIFF,
+ /**
+ * A pyramided GeoTIFF format in which overviews are generated automatically from the base image.
+ * This is almost the Cloud Optimized GeoTIFF (<abbr>COG</abbr>) format, except that the overviews
+ * are written in unspecified order, not in the order mandated by the <abbr>COG</abbr> conventions.
+ * This flexibility makes possible to write the <abbr>TIFF</abbr> file directly,
+ * with no need to write in a temporary file and reorder the images at the end.
+ *
+ * @since 1.7
+ */
+ PYRAMIDED,
+
// TODO: COG, SPARSE.
/**
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
index e7b4c4dd79..97657c40af 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/GeoTiffStore.java
@@ -82,6 +82,7 @@ import org.apache.sis.util.resources.Errors;
* @author Martin Desruisseaux (Geomatys)
* @author Thi Phuong Hao Nguyen (VNSC)
* @author Alexis Manin (Geomatys)
+ * @author Estelle Idée (Geomatys)
* @version 1.7
* @since 0.8
*/
@@ -690,7 +691,8 @@ public class GeoTiffStore extends DataStore implements Aggregate {
/**
* Encodes the given image in the GeoTIFF file.
* The image is appended after any existing images in the GeoTIFF file.
- * This method does not handle pyramids such as Cloud Optimized GeoTIFF (COG).
+ * If the {@link FormatModifier#PYRAMIDED} was given at construction time,
+ * then overviews are automatically written.
*
* @param image the image to encode.
* @param grid mapping from pixel coordinates to "real world" coordinates, or {@code null} if none.
@@ -703,7 +705,7 @@ public class GeoTiffStore extends DataStore implements Aggregate {
* @since 1.5
*/
@SuppressWarnings("LocalVariableHidesMemberVariable")
- public synchronized GridCoverageResource append(final RenderedImage image, final GridGeometry grid, final Metadata metadata)
+ public synchronized GridCoverageResource append(RenderedImage image, final GridGeometry grid, final Metadata metadata)
throws DataStoreException
{
final int index;
@@ -711,9 +713,18 @@ public class GeoTiffStore extends DataStore implements Aggregate {
final Reader reader = this.reader;
final Writer writer = writer();
writer.synchronize(reader, false);
- final long offsetIFD;
+ long offsetIFD;
try {
- offsetIFD = writer.append(image, grid, metadata);
+ offsetIFD = writer.append(image, grid, metadata, false);
+ if (writer.isPyramided) {
+ while (image.getWidth() > Writer.OVERVIEW_SIZE ||
+ image.getHeight() > Writer.OVERVIEW_SIZE)
+ {
+ image = writer.processor().overview(image);
+ offsetIFD = writer.append(image, null, null, true);
+ // Grid and metadata are null as we don't want to repeat metadata in overviews.
+ }
+ }
} finally {
writer.synchronize(reader, true);
}
@@ -755,7 +766,8 @@ public class GeoTiffStore extends DataStore implements Aggregate {
/**
* Adds a new grid coverage in the GeoTIFF file.
* The coverage is appended after any existing images in the GeoTIFF file.
- * This method does not handle pyramids such as Cloud Optimized GeoTIFF (COG).
+ * If the {@link FormatModifier#PYRAMIDED} was given at construction time,
+ * then overviews are automatically written.
*
* @param coverage the grid coverage to encode.
* @param metadata title, author and other information, or {@code null} if none.
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
index be885bc0f8..9e7022f300 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/main/org/apache/sis/storage/geotiff/Writer.java
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Deque;
import java.util.Queue;
import java.util.Set;
+import java.util.EnumSet;
import java.awt.image.RenderedImage;
import java.awt.image.SampleModel;
import java.awt.image.BandedSampleModel;
@@ -39,6 +40,7 @@ import org.opengis.util.FactoryException;
import org.opengis.metadata.Metadata;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.image.ImageProcessor;
+import org.apache.sis.image.ImageLayout;
import org.apache.sis.image.DataType;
import org.apache.sis.coverage.grid.GridGeometry;
import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
@@ -78,8 +80,15 @@ import org.opengis.coverage.CannotEvaluateException;
*
* @author Erwan Roussel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
+ * @author Estelle Idée (Geomatys)
*/
final class Writer extends IOBase implements Flushable {
+ /**
+ * Maximal size of the highest overview.
+ * Used as a criteria for deciding when to stop creating overviews in a pyramided file.
+ */
+ static final int OVERVIEW_SIZE = ImageLayout.DEFAULT_TILE_SIZE;
+
/**
* BigTIFF code for unsigned 64-bits integer type.
*
@@ -138,6 +147,14 @@ final class Writer extends IOBase implements Flushable {
*/
private final boolean isBigTIFF;
+ /**
+ * Whether to write a pyramid of overviews. This is one requirement of Cloud Optimized GeoTIFF <abbr>COG</abbr>,
+ * but not sufficient for claiming compliance because this option does not force <abbr>COG</abbr> image order.
+ *
+ * @see #getFormat()
+ */
+ final boolean isPyramided;
+
/**
* Whether to disable the <abbr>TIFF</abbr> requirement that tile sizes are multiple of 16 pixels.
*/
@@ -192,6 +209,7 @@ final class Writer extends IOBase implements Flushable {
super(store);
this.output = output;
isBigTIFF = ArraysExt.contains(options, FormatModifier.BIG_TIFF);
+ isPyramided = ArraysExt.contains(options, FormatModifier.PYRAMIDED);
anyTileSize = ArraysExt.contains(options, FormatModifier.ANY_TILE_SIZE);
/*
* Write the TIFF file header before first IFD. Stream position matter and must start at zero.
@@ -223,6 +241,7 @@ final class Writer extends IOBase implements Flushable {
Writer(final Reader reader) throws IOException, DataStoreException {
super(reader.store);
isBigTIFF = (reader.intSizeExpansion != 0);
+ isPyramided = false;
anyTileSize = false;
try {
output = new ChannelDataOutput(reader.input);
@@ -266,14 +285,17 @@ final class Writer extends IOBase implements Flushable {
*/
@Override
public final Set<FormatModifier> getModifiers() {
- return isBigTIFF ? Set.of(FormatModifier.BIG_TIFF) : Set.of();
+ final var modifiers = EnumSet.noneOf(FormatModifier.class);
+ if (isBigTIFF) modifiers.add(FormatModifier.BIG_TIFF);
+ if (isPyramided) modifiers.add(FormatModifier.PYRAMIDED);
+ return modifiers;
}
/**
* Returns the processor to use for reformatting the image before to write it.
* The processor is created only when this method is first invoked.
*/
- private ImageProcessor processor() {
+ final ImageProcessor processor() {
if (processor == null) {
processor = new ImageProcessor();
}
@@ -289,6 +311,7 @@ final class Writer extends IOBase implements Flushable {
* @param image the image to encode.
* @param grid mapping from pixel coordinates to "real world" coordinates, or {@code null} if none.
* @param metadata title, author and other information, or {@code null} if none.
+ * @param overview whether the image is an overview of another image.
* @return offset in {@link #output} where the Image File Directory (IFD) starts.
* @throws RasterFormatException if the raster uses an unsupported sample model.
* @throws ArithmeticException if an integer overflow occurs.
@@ -297,7 +320,7 @@ final class Writer extends IOBase implements Flushable {
* which is not supported by TIFF specification or by this writer.
*/
@SuppressWarnings("UseSpecificCatch")
- public final long append(final RenderedImage image, final GridGeometry grid, final Metadata metadata)
+ public final long append(final RenderedImage image, final GridGeometry grid, final Metadata metadata, final boolean overview)
throws IOException, DataStoreException
{
final var exportable = new ReformattedImage(image, this::processor, anyTileSize);
@@ -321,7 +344,7 @@ final class Writer extends IOBase implements Flushable {
try {
final TileMatrix tiles;
try {
- tiles = writeImageFileDirectory(exportable, grid, metadata, false);
+ tiles = writeImageFileDirectory(exportable, grid, metadata, overview);
} finally {
largeTagData.clear(); // For making sure that there is no memory retention.
}
diff --git a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
index 5d495e9544..2b5804e8a5 100644
--- a/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
+++ b/endorsed/src/org.apache.sis.storage.geotiff/test/org/apache/sis/storage/geotiff/GeoTiffStoreTest.java
@@ -16,23 +16,31 @@
*/
package org.apache.sis.storage.geotiff;
+import java.util.Random;
+import java.util.Iterator;
import java.io.IOException;
import java.io.InputStream;
import java.io.ByteArrayOutputStream;
import java.nio.file.Path;
import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
import java.awt.Dimension;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.image.DataType;
+import org.apache.sis.storage.OptionKey;
import org.apache.sis.storage.DataStore;
import org.apache.sis.storage.DataStores;
import org.apache.sis.storage.DataStoreException;
+import org.apache.sis.storage.StorageConnector;
import org.apache.sis.storage.GridCoverageResource;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
@@ -48,7 +56,9 @@ import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import static org.apache.sis.test.Assertions.assertSingleton;
import static org.apache.sis.feature.Assertions.assertGridToCornerEquals;
+import org.apache.sis.image.OverviewImageTest;
import org.apache.sis.test.TestCase;
+import org.apache.sis.test.TestUtilities;
import org.apache.sis.referencing.crs.HardCodedCRS;
import org.apache.sis.referencing.operation.HardCodedConversions;
@@ -61,6 +71,7 @@ import static org.opengis.test.Assertions.assertAxisDirectionsEqual;
* This class tests indirectly (via {@link GeoTiffStore}) the {@link Reader} and {@link Writer} classes.
*
* @author Martin Desruisseaux (Geomatys)
+ * @author Estelle Idée (Geomatys)
*/
@SuppressWarnings("exports")
public final class GeoTiffStoreTest extends TestCase {
@@ -190,4 +201,71 @@ public final class GeoTiffStoreTest extends TestCase {
assertArrayEquals(expected, actual);
assertEquals(length, actual.length);
}
+
+ /**
+ * Tests writing a pyramided image.
+ *
+ * @throws Exception if an error occurred while preparing or running the test.
+ */
+ @Test
+ public void testPyramided() throws Exception {
+ final Random r = TestUtilities.createRandomNumberGenerator();
+ final int width = r.nextInt(2 * Writer.OVERVIEW_SIZE) + 3 * Writer.OVERVIEW_SIZE;
+ final int height = r.nextInt(2 * Writer.OVERVIEW_SIZE) + 3 * Writer.OVERVIEW_SIZE;
+ final var area = new GeneralEnvelope(HardCodedCRS.WGS84);
+ area.setRange(0, 132, 145); // Range of longitude values.
+ area.setRange(1, 30, 42); // Range of latitude values.
+ final GridCoverage coverage = new GridCoverageBuilder()
+ .setDomain(Envelopes.transform(area, HardCodedCRS.WGS84))
+ .setValues(DataType.BYTE, new Rectangle(width, height), null, (x, y) -> 100 * y + x)
+ .flipGridAxis(1)
+ .build();
+
+ final Path path = Files.createTempFile("pyramided", ".tiff");
+ try {
+ final var connector = new StorageConnector(path);
+ connector.setOption(FormatModifier.OPTION_KEY, new FormatModifier[] {
+ FormatModifier.PYRAMIDED
+ });
+ connector.setOption(OptionKey.OPEN_OPTIONS, new StandardOpenOption[] {
+ StandardOpenOption.CREATE,
+ StandardOpenOption.WRITE
+ });
+ try (var store = new GeoTiffStore(null, connector)) {
+ assertNotNull(store.append(coverage, null));
+ }
+ /*
+ * Try to read the image using the standard TIFF reader, which is used as a reference implementation.
+ * We expect at least one implementation. If there is more implementations, test will all of them.
+ */
+ final Iterator<ImageReader> imageReaders = ImageIO.getImageReadersByFormatName("TIFF");
+ assertTrue(imageReaders.hasNext());
+ do {
+ final ImageReader reader = imageReaders.next();
+ try (ImageInputStream input = ImageIO.createImageInputStream(path.toFile())) {
+ reader.setInput(input);
+ RenderedImage image = reader.read(0);
+ assertEquals(width, image.getWidth());
+ assertEquals(height, image.getHeight());
+ int imageIndex = 1;
+ while (image.getWidth() > Writer.OVERVIEW_SIZE || image.getHeight() > Writer.OVERVIEW_SIZE) {
+ final RenderedImage overview = reader.read(imageIndex);
+ OverviewImageTest.verify(image, overview, true);
+ image = overview;
+ imageIndex++;
+ }
+ try {
+ reader.read(imageIndex);
+ fail("Expected no more images.");
+ } catch (IndexOutOfBoundsException e) {
+ // Ignore expected exception.
+ }
+ assertTrue(imageIndex > 1); // Expect at least one overview.
+ }
+ reader.dispose();
+ } while (imageReaders.hasNext());
+ } finally {
+ Files.delete(path);
+ }
+ }
}
(sis) 02/03: Javadoc, formatting and warning suppressions.
Posted by de...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 593a479d5c30186130a77a023e2ca1c3e0144557
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Jun 10 20:27:15 2026 +0200
Javadoc, formatting and warning suppressions.
---
.../main/org/apache/sis/image/ComputedImage.java | 2 +-
.../main/org/apache/sis/image/PixelIterator.java | 6 +++---
.../main/org/apache/sis/image/ResampledImage.java | 2 +-
.../test/org/apache/sis/image/BandedIteratorTest.java | 1 +
.../test/org/apache/sis/image/ComputedImageTest.java | 1 +
.../test/org/apache/sis/image/DataTypeTest.java | 1 +
.../test/org/apache/sis/image/ImageLayoutTest.java | 1 +
.../test/org/apache/sis/image/ImageOverlayTest.java | 1 +
.../test/org/apache/sis/image/ImageTestCase.java | 1 +
.../test/org/apache/sis/image/InterpolationTest.java | 1 +
.../test/org/apache/sis/image/LinearIteratorTest.java | 1 +
.../test/org/apache/sis/image/MaskedImageTest.java | 1 +
12 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
index 83d65a92a5..7a342f5c28 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ComputedImage.java
@@ -555,7 +555,7 @@ public abstract class ComputedImage extends PlanarImage implements Disposable {
}
/**
- * Invoked when a tile need to be computed or updated. This method is invoked by {@link #getTile(int, int)}
+ * Invoked when a tile needs to be computed or updated. This method is invoked by {@link #getTile(int, int)}
* when the requested tile is not in the cache, or when a writable source notified us that its data changed.
* The returned tile will be automatically cached.
*
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
index 46303499b5..cc325657ad 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/PixelIterator.java
@@ -162,7 +162,7 @@ public class PixelIterator {
/**
* Maximal {@linkplain #x} and {@linkplain #y} coordinates (exclusive) that {@link Window} can use for
- * fetching values in current tile. If some (x,y) coordinates inside the window are equal or greater,
+ * fetching values in current tile. If some (x, y) coordinates inside the window are equal or greater,
* then the window will need to fetch some values on neighbor tiles (i.e. the window is overlapping
* two or more tiles).
*
@@ -702,7 +702,7 @@ public class PixelIterator {
} else if (tileY >= tileUpperY) {
message = Resources.Keys.IterationIsFinished;
} else {
- return new Point(x,y);
+ return new Point(x, y);
}
throw new IllegalStateException(Resources.format(message));
}
@@ -832,7 +832,7 @@ public class PixelIterator {
}
/**
- * Returns whether given position is on the same row and same tile as current (x,y) position.
+ * Returns whether given position is on the same row and same tile as current (x, y) position.
* This method is provided as a complement to {@link #changedRowOrTile()}.
*/
final boolean isSameRowAndTile(final int px, final int py) {
diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
index 0fdd61773f..043df29625 100644
--- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
+++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/image/ResampledImage.java
@@ -672,7 +672,7 @@ public class ResampledImage extends ComputedImage {
}
/**
- * Invoked when a tile need to be computed or updated. This method fills all pixel values of the tile
+ * Invoked when a tile needs to be computed or updated. This method fills all pixel values of the tile
* with values interpolated from the source image. It may be invoked concurrently in different threads.
*
* @param tileX the column index of the tile to compute.
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedIteratorTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedIteratorTest.java
index 21907b60a1..92eae772af 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedIteratorTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/BandedIteratorTest.java
@@ -34,6 +34,7 @@ import org.opengis.coverage.grid.SequenceType;
*
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
public final class BandedIteratorTest extends PixelIteratorTest {
/**
* Creates a new test case.
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ComputedImageTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ComputedImageTest.java
index 17b2dd076e..4ebe04794d 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ComputedImageTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ComputedImageTest.java
@@ -38,6 +38,7 @@ import static org.apache.sis.feature.Assertions.assertValuesEqual;
*
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
public final class ComputedImageTest extends TestCase {
/**
* Size of tiles in this test. The width should be different than the height
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/DataTypeTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/DataTypeTest.java
index f8360b8339..73919a6edb 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/DataTypeTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/DataTypeTest.java
@@ -34,6 +34,7 @@ import org.apache.sis.test.TestCase;
*
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
public final class DataTypeTest extends TestCase {
/**
* Creates a new test case.
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageLayoutTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageLayoutTest.java
index 29ec35377c..43e1cd18a6 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageLayoutTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageLayoutTest.java
@@ -29,6 +29,7 @@ import org.apache.sis.test.TestCase;
*
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
public final class ImageLayoutTest extends TestCase {
/**
* Creates a new test case.
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java
index 489676e91a..311b376c6e 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageOverlayTest.java
@@ -41,6 +41,7 @@ import static org.apache.sis.feature.Assertions.assertValuesEqual;
*
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public final class ImageOverlayTest extends TestCase {
/**
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageTestCase.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageTestCase.java
index 5212eca181..91fd6cd161 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageTestCase.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/ImageTestCase.java
@@ -40,6 +40,7 @@ import org.apache.sis.test.TestCase;
*
* @author Martin Desruisseaux (IRD, Geomatys)
*/
+@SuppressWarnings("exports")
public abstract class ImageTestCase extends TestCase {
/**
* Small value for comparisons of sample values. Since most grid coverage implementations in
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/InterpolationTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/InterpolationTest.java
index c0cb2e31b7..8d4c545d28 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/InterpolationTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/InterpolationTest.java
@@ -37,6 +37,7 @@ import org.apache.sis.test.TestCase;
* @author Rémi Marechal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
public final class InterpolationTest extends TestCase {
/**
* Minimal pixel X and Y coordinates used in this test, inclusive.
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/LinearIteratorTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/LinearIteratorTest.java
index 7f845ccfb5..a3ffe83593 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/LinearIteratorTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/LinearIteratorTest.java
@@ -37,6 +37,7 @@ import org.opengis.coverage.grid.SequenceType;
* @author Johann Sorel (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
+@SuppressWarnings("exports")
public final class LinearIteratorTest extends PixelIteratorTest {
/**
* Creates a new test case.
diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/MaskedImageTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/MaskedImageTest.java
index 4ed3565666..7caa72e7d3 100644
--- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/MaskedImageTest.java
+++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/image/MaskedImageTest.java
@@ -48,6 +48,7 @@ import static org.opengis.test.Assertions.assertSampleValuesEqual;
*
* @author Alexis Manin (Geomatys)
*/
+@SuppressWarnings("exports")
public final class MaskedImageTest extends TestCase {
/**
* The image width and height in pixels.