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 2018/06/18 09:44:18 UTC
[sis] 07/31: First draft of a port of GridGeometry class,
for grid coverage (raster) support.
This is an automated email from the ASF dual-hosted git repository.
desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git
commit 20423a32ff2c50dbd652047791d29c3df88e7e13
Author: Martin Desruisseaux <de...@apache.org>
AuthorDate: Tue May 29 16:58:56 2018 +0000
First draft of a port of GridGeometry class, for grid coverage (raster) support.
git-svn-id: https://svn.apache.org/repos/asf/sis/branches/JDK8@1832460 13f79535-47bb-0310-9956-ffa450edef68
---
core/sis-raster/pom.xml | 2 +-
.../sis/coverage/grid/GridCoordinatesView.java | 142 +++++
.../org/apache/sis/coverage/grid/GridExtent.java | 322 ++++++++++++
.../org/apache/sis/coverage/grid/GridGeometry.java | 582 +++++++++++++++++++++
.../grid/IncompleteGridGeometryException.java | 70 +++
.../apache/sis/coverage/grid/PixelTranslation.java | 358 +++++++++++++
.../raster => coverage/grid}/package-info.java | 15 +-
.../java/org/apache/sis/image/DefaultIterator.java | 4 +-
.../java/org/apache/sis/image/PixelIterator.java | 4 +-
.../java/org/apache/sis/image/TransferType.java | 4 +-
.../apache/sis/image/WritablePixelIterator.java | 4 +-
.../java/org/apache/sis/image/package-info.java | 4 +-
.../org/apache/sis/internal/raster/Resources.java | 30 +-
.../sis/internal/raster/Resources.properties | 1 +
.../sis/internal/raster/Resources_fr.properties | 1 +
.../apache/sis/internal/raster/package-info.java | 4 +-
16 files changed, 1522 insertions(+), 25 deletions(-)
diff --git a/core/sis-raster/pom.xml b/core/sis-raster/pom.xml
index be8cda6..cc6b8fa 100644
--- a/core/sis-raster/pom.xml
+++ b/core/sis-raster/pom.xml
@@ -101,7 +101,7 @@
<dependencies>
<dependency>
<groupId>org.apache.sis.core</groupId>
- <artifactId>sis-utility</artifactId>
+ <artifactId>sis-referencing</artifactId>
<version>${project.version}</version>
</dependency>
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
new file mode 100644
index 0000000..bd338a3
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
@@ -0,0 +1,142 @@
+/*
+ * 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.coverage.grid;
+
+import java.util.Arrays;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+
+// Branch-dependent imports
+import org.opengis.coverage.grid.GridCoordinates;
+
+
+/**
+ * A view over the low or high grid envelope coordinates.
+ * This is not a general-purpose grid coordinates since it assumes a {@link GridExtent} coordinates layout.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+final class GridCoordinatesView implements GridCoordinates {
+ /**
+ * A reference to the coordinate array of the enclosing grid envelope.
+ */
+ private final int[] ordinates;
+
+ /**
+ * Index of the first value in the {@link #ordinates} array.
+ * This is 0 for low values, or {@link #getDimension()} for high values.
+ */
+ private final int offset;
+
+ /**
+ * Creates a new view over the low or high coordinates.
+ */
+ GridCoordinatesView(final int[] ordinates, final int offset) {
+ this.ordinates = ordinates;
+ this.offset = offset;
+ }
+
+ /**
+ * Returns the number of dimension.
+ */
+ @Override
+ public final int getDimension() {
+ return ordinates.length >>> 1;
+ }
+
+ /**
+ * Returns all coordinate values.
+ */
+ @Override
+ public final int[] getCoordinateValues() {
+ return Arrays.copyOfRange(ordinates, offset, offset + getDimension());
+ }
+
+ /**
+ * Returns the coordinate value for the specified dimension.
+ */
+ @Override
+ public final int getCoordinateValue(final int index) {
+ ArgumentChecks.ensureValidIndex(getDimension(), index);
+ return ordinates[offset + index];
+ }
+
+ /**
+ * Do not allow modification of grid coordinates since they are backed by {@link GridExtent}.
+ */
+ @Override
+ public void setCoordinateValue(final int index, int value) {
+ throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, "GridCoordinates"));
+ }
+
+ /**
+ * Returns a string representation of this grid coordinates for debugging purpose.
+ */
+ @Debug
+ @Override
+ public final String toString() {
+ return "GridCoordinates".concat(Arrays.toString(getCoordinateValues()));
+ }
+
+ /**
+ * Returns a hash code value for this object.
+ */
+ @Override
+ public final int hashCode() {
+ int code = -3; // Arbitrary seed for differentiating from Arrays.hashCode(int[]).
+ final int end = offset + getDimension();
+ for (int i=offset; i<end; i++) {
+ code = 31 * code + ordinates[i];
+ }
+ return code;
+ }
+
+ /**
+ * Compares this grid coordinates with the specified object for equality.
+ *
+ * @param object the object to compares with this grid coordinates.
+ * @return {@code true} if the given object is equal to this grid coordinates.
+ */
+ @Override
+ public final boolean equals(final Object object) {
+ if (object == this) { // Slight optimization.
+ return true;
+ }
+ /*
+ * We do not require the exact same class because we want to accept
+ * immutable grid coordinates as equal to mutable grid coordinates.
+ */
+ if (object instanceof GridCoordinatesView) {
+ final GridCoordinatesView that = (GridCoordinatesView) object;
+ final int dimension = getDimension();
+ if (dimension == that.getDimension()) {
+ // TODO: use Arrays.equals(...) with JDK9 instead.
+ for (int i=0; i<dimension; i++) {
+ if (ordinates[offset + i] != that.ordinates[that.offset + i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
new file mode 100644
index 0000000..816a62f
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -0,0 +1,322 @@
+/*
+ * 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.coverage.grid;
+
+import java.util.Arrays;
+import java.io.Serializable;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.internal.raster.Resources;
+
+// Branch-dependent imports
+import org.opengis.coverage.grid.GridEnvelope;
+import org.opengis.coverage.grid.GridCoordinates;
+
+
+/**
+ * A range of grid coverage coordinates, also known as "grid envelope".
+ * {@code GridExtent} instances are unmodifiable,
+ * so they can be shared between different {@link GridGeometry} instances.
+ *
+ * <p><b>CAUTION:</b>
+ * ISO 19123 defines {@linkplain #getHigh high} coordinates as <strong>inclusive</strong>.
+ * We follow this specification for all getters methods, but developer should keep in mind
+ * that this is the opposite of Java2D usage where {@link java.awt.Rectangle} maximal values are exclusive.
+ * When the context is ambiguous, an explicit {@code isHighIncluded} argument is required.</p>
+ *
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+public class GridExtent implements GridEnvelope, Serializable {
+ /**
+ * Serial number for inter-operability with different versions.
+ */
+ private static final long serialVersionUID = -4717353677844056017L;
+
+ /**
+ * Minimum and maximum grid ordinates. The first half contains minimum ordinates (inclusive),
+ * while the last half contains maximum ordinates (<strong>inclusive</strong>). Note that the
+ * later is the opposite of Java2D usage but conform to ISO specification.
+ */
+ private final int[] ordinates;
+
+ /**
+ * Creates a new array of coordinates with the given number of dimensions.
+ *
+ * @throws IllegalArgumentException if the given number of dimensions is excessive.
+ */
+ private static int[] allocate(final int dimension) throws IllegalArgumentException {
+ if (dimension > Integer.MAX_VALUE / 2) {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
+ }
+ return new int[dimension << 1];
+ }
+
+ /**
+ * Checks if ordinate values in the low part are less than or
+ * equal to the corresponding ordinate value in the high part.
+ *
+ * @throws IllegalArgumentException if a coordinate value in the low part is
+ * greater than the corresponding coordinate value in the high part.
+ */
+ private static void checkCoherence(final int[] ordinates) throws IllegalArgumentException {
+ final int dimension = ordinates.length >>> 1;
+ for (int i=0; i<dimension; i++) {
+ final int lower = ordinates[i];
+ final int upper = ordinates[i + dimension];
+ if (lower > upper) {
+ throw new IllegalArgumentException(Resources.format(
+ Resources.Keys.IllegalGridEnvelope_3, i, lower, upper));
+ }
+ }
+ }
+
+ /**
+ * Creates an initially empty grid envelope with the given number of dimensions.
+ * All grid coordinate values are initialized to zero. This constructor is private
+ * since {@code GridExtent} coordinate values can not be modified by public API.
+ *
+ * @param dimension number of dimensions.
+ */
+ private GridExtent(final int dimension) {
+ ordinates = allocate(dimension);
+ }
+
+ /**
+ * Constructs a new grid envelope set to the specified coordinates.
+ * The given arrays contain a minimum (inclusive) and maximum value for each dimension of the grid coverage.
+ * The lowest valid grid coordinates are often zero, but this is not mandatory.
+ * As a convenience for this common case, a null {@code low} array means that all low coordinates are zero.
+ *
+ * @param low the valid minimum grid coordinate (always inclusive), or {@code null} for all zeros.
+ * @param high the valid maximum grid coordinate, inclusive or exclusive depending on the next argument.
+ * @param isHighIncluded {@code true} if the {@code high} values are inclusive (as in ISO 19123 specification),
+ * or {@code false} if they are exclusive (as in Java2D usage).
+ * This argument does not apply to {@code low} values, which are always inclusive.
+ * @throws IllegalArgumentException if a coordinate value in the low part is
+ * greater than the corresponding coordinate value in the high part.
+ *
+ * @see #getLow()
+ * @see #getHigh()
+ */
+ public GridExtent(final int[] low, final int[] high, final boolean isHighIncluded) {
+ ArgumentChecks.ensureNonNull("high", high);
+ final int dimension = high.length;
+ if (low != null && low.length != dimension) {
+ throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedDimension_2, low.length, dimension));
+ }
+ ordinates = allocate(dimension);
+ if (low != null) {
+ System.arraycopy(low, 0, ordinates, 0, dimension);
+ }
+ System.arraycopy(high, 0, ordinates, dimension, dimension);
+ if (!isHighIncluded) {
+ for (int i=dimension; i < ordinates.length; i++) {
+ ordinates[i]--;
+ }
+ }
+ checkCoherence(ordinates);
+ }
+
+ /**
+ * Creates a new grid envelope as a copy of the given one.
+ *
+ * @param extent the grid envelope to copy.
+ * @throws IllegalArgumentException if a coordinate value in the low part is
+ * greater than the corresponding coordinate value in the high part.
+ */
+ protected GridExtent(final GridEnvelope extent) {
+ ArgumentChecks.ensureNonNull("extent", extent);
+ final int dimension = extent.getDimension();
+ ordinates = allocate(dimension);
+ for (int i=0; i<dimension; i++) {
+ ordinates[i] = extent.getLow(i);
+ ordinates[i + dimension] = extent.getHigh(i);
+ }
+ checkCoherence(ordinates);
+ }
+
+ /**
+ * Returns the given grid envelope as a {@code GridExtent} implementation.
+ * If the given extent is already a {@code GridExtent} instance or is null, then it is returned as-is.
+ * Otherwise a new extent is created using the {@linkplain #GridExtent(GridEnvelope) copy constructor}.
+ *
+ * @param extent the grid envelope to cast or copy, or {@code null}.
+ * @return the grid envelope as a {@code GridExtent}, or {@code null} if the given extent was null.
+ */
+ public static GridExtent castOrCopy(final GridEnvelope extent) {
+ if (extent == null || extent instanceof GridExtent) {
+ return (GridExtent) extent;
+ } else {
+ return new GridExtent(extent);
+ }
+ }
+
+ /**
+ * Returns the number of dimensions.
+ *
+ * @return the number of dimensions.
+ */
+ @Override
+ public final int getDimension() {
+ return ordinates.length >>> 1;
+ }
+
+ /**
+ * Returns the valid minimum grid coordinates, inclusive.
+ * The sequence contains a minimum value for each dimension of the grid coverage.
+ *
+ * @return the valid minimum grid coordinates, inclusive.
+ */
+ @Override
+ public GridCoordinates getLow() {
+ return new GridCoordinatesView(ordinates, 0);
+ }
+
+ /**
+ * Returns the valid maximum grid coordinates, <strong>inclusive</strong>.
+ * The sequence contains a maximum value for each dimension of the grid coverage.
+ *
+ * @return the valid maximum grid coordinates, <strong>inclusive</strong>.
+ */
+ @Override
+ public GridCoordinates getHigh() {
+ return new GridCoordinatesView(ordinates, getDimension());
+ }
+
+ /**
+ * Returns the valid minimum inclusive grid coordinate along the specified dimension.
+ *
+ * @param index the dimension for which to obtain the coordinate value.
+ * @return the low coordinate value at the given dimension, inclusive.
+ * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+ * than the {@linkplain #getDimension() grid dimension}.
+ *
+ * @see #getLow()
+ * @see #getHigh(int)
+ */
+ @Override
+ public int getLow(final int index) {
+ ArgumentChecks.ensureValidIndex(getDimension(), index);
+ return ordinates[index];
+ }
+
+ /**
+ * Returns the valid maximum <strong>inclusive</strong> grid coordinate along the specified dimension.
+ *
+ * @param index the dimension for which to obtain the coordinate value.
+ * @return the high coordinate value at the given dimension, <strong>inclusive</strong>.
+ * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+ * than the {@linkplain #getDimension() grid dimension}.
+ *
+ * @see #getHigh()
+ * @see #getLow(int)
+ */
+ @Override
+ public int getHigh(final int index) {
+ final int dimension = getDimension();
+ ArgumentChecks.ensureValidIndex(dimension, index);
+ return ordinates[index + dimension];
+ }
+
+ /**
+ * Returns the number of integer grid coordinates along the specified dimension.
+ * This is equal to {@code getHigh(dimension) - getLow(dimension) + 1}.
+ *
+ * @param index the dimension for which to obtain the span.
+ * @return the span at the given dimension.
+ * @throws IndexOutOfBoundsException if the given index is negative or is equals or greater
+ * than the {@linkplain #getDimension() grid dimension}.
+ *
+ * @see #getLow(int)
+ * @see #getHigh(int)
+ */
+ @Override
+ public int getSpan(final int index) {
+ final int dimension = getDimension();
+ ArgumentChecks.ensureValidIndex(dimension, index);
+ return ordinates[dimension + index] - ordinates[index] + 1;
+ }
+
+ /**
+ * Returns a new grid envelope that encompass only some dimensions of this grid envelope.
+ * This method copies this grid envelope into a new grid envelope, beginning at dimension
+ * {@code lower} and extending to dimension {@code upper-1} inclusive. Thus the dimension
+ * of the sub grid envelope is {@code upper - lower}.
+ *
+ * @param lower the first dimension to copy, inclusive.
+ * @param upper the last dimension to copy, exclusive.
+ * @return the sub grid envelope.
+ * @throws IndexOutOfBoundsException if an index is out of bounds.
+ */
+ public GridExtent subEnvelope(final int lower, final int upper) {
+ final int dimension = getDimension();
+ ArgumentChecks.ensureValidIndexRange(dimension, lower, upper);
+ final int newDim = upper - lower;
+ if (newDim == dimension && getClass() == GridExtent.class) {
+ return this;
+ }
+ final GridExtent sub = new GridExtent(newDim);
+ System.arraycopy(ordinates, lower, sub.ordinates, 0, newDim);
+ System.arraycopy(ordinates, lower+dimension, sub.ordinates, newDim, newDim);
+ return sub;
+ }
+
+ /**
+ * Returns a hash value for this grid envelope. This value need not remain
+ * consistent between different implementations of the same class.
+ *
+ * @return a hash value for this grid envelope.
+ */
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(ordinates) ^ (int) serialVersionUID;
+ }
+
+ /**
+ * Compares the specified object with this grid envelope for equality.
+ *
+ * @param object the object to compare with this grid envelope for equality.
+ * @return {@code true} if the given object is equal to this grid envelope.
+ */
+ @Override
+ public boolean equals(final Object object) {
+ if (object instanceof GridExtent) {
+ return Arrays.equals(ordinates, ((GridExtent) object).ordinates);
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation of this grid envelope. The returned string
+ * is implementation dependent and is provided for debugging purposes only.
+ */
+ @Debug
+ @Override
+ public String toString() {
+ final int dimension = getDimension();
+ final StringBuilder buffer = new StringBuilder("GridEnvelope").append('[');
+ for (int i=0; i<dimension; i++) {
+ if (i != 0) buffer.append(", ");
+ buffer.append(ordinates[i]).append('…').append(ordinates[i + dimension]);
+ }
+ return buffer.append(']').toString();
+ }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
new file mode 100644
index 0000000..79ac7cc
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -0,0 +1,582 @@
+/*
+ * 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.coverage.grid;
+
+import java.util.Objects;
+import java.io.Serializable;
+import java.awt.image.RenderedImage; // For javadoc only.
+import org.opengis.geometry.Envelope;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.math.MathFunctions;
+import org.apache.sis.geometry.Envelopes;
+import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.geometry.ImmutableEnvelope;
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.operation.transform.PassThroughTransform;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.Debug;
+
+// Branch-dependent imports
+import org.opengis.coverage.grid.GridEnvelope;
+
+
+/**
+ * Describes the valid extent of grid coordinates and the transform from those grid coordinates
+ * to real world coordinates. Grid geometries contains:
+ *
+ * <ul>
+ * <li>A <cite>grid envelope</cite> (a.k.a. <cite>"grid extent"</cite>),
+ * often inferred from the {@link RenderedImage} size.</li>
+ * <li>A <cite>grid to CRS</cite> {@link MathTransform},
+ * which can be inferred from the grid envelope and the georeferenced envelope.</li>
+ * <li>A georeferenced {@link Envelope}, which can be inferred from the grid envelope
+ * and the <cite>grid to CRS</cite> transform.</li>
+ * <li>An optional {@link CoordinateReferenceSystem} (CRS) specified as part of the georeferenced envelope.
+ * This CRS is the target of the <cite>grid to CRS</cite> transform.</li>
+ * </ul>
+ *
+ * All above properties except the CRS should be mandatory, but are allowed to be temporarily absent during
+ * grid coverage construction. Temporarily absent properties are allowed because they may be inferred from
+ * a wider context. For example a grid geometry know nothing about {@link RenderedImage},
+ * but {@code GridCoverage2D} does and may use that information for providing a missing grid envelope.
+ * By default, any request for an undefined property will throw an {@link IncompleteGridGeometryException}.
+ * In order to check if a property is defined, use {@link #isDefined(int)}.
+ *
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @author Alessio Fabiani (Geosolutions)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+public class GridGeometry implements Serializable {
+ /**
+ * Serial number for inter-operability with different versions.
+ */
+ private static final long serialVersionUID = -954786616001606624L;
+
+ /**
+ * A bitmask to specify the validity of the Coordinate Reference System property.
+ *
+ * @see #isDefined(int)
+ * @see #getCoordinateReferenceSystem()
+ */
+ public static final int CRS = 1;
+
+ /**
+ * A bitmask to specify the validity of the geodetic envelope property.
+ *
+ * @see #isDefined(int)
+ * @see #getEnvelope()
+ */
+ public static final int ENVELOPE = 2;
+
+ /**
+ * A bitmask to specify the validity of the grid envelope property.
+ *
+ * @see #isDefined(int)
+ * @see #getExtent()
+ */
+ public static final int EXTENT = 4;
+
+ /**
+ * A bitmask to specify the validity of the <cite>"grid to CRS"</cite> transform.
+ *
+ * @see #isDefined(int)
+ * @see #getGridToCRS()
+ */
+ public static final int GRID_TO_CRS = 8;
+
+ /**
+ * The valid domain of a grid coverage, or {@code null} if unknown. The lowest valid grid coordinate is zero
+ * for {@link java.awt.image.BufferedImage}, but may be non-zero for arbitrary {@link RenderedImage}.
+ * A grid with 512 cells can have a minimum coordinate of 0 and maximum of 511.
+ */
+ protected final GridExtent extent;
+
+ /**
+ * The geodetic envelope, or {@code null} if unknown. If non-null, this envelope is usually the grid {@link #extent}
+ * {@linkplain #gridToCRS transformed} to real world coordinates. The Coordinate Reference System} (CRS) of this
+ * envelope defines the "real world" CRS of this grid geometry.
+ */
+ protected final ImmutableEnvelope envelope;
+
+ /**
+ * The math transform from grid indices to "real world" coordinates, or {@code null} if unknown.
+ * This math transform is usually affine. It maps {@linkplain PixelInCell#CELL_CENTER pixel center}
+ * to "real world" coordinate using the following line:
+ *
+ * {@preformat java
+ * DirectPosition aCellIndices = ...:
+ * DirectPosition aPixelCenter = gridToCRS.transform(pixels, aCellIndices);
+ * }
+ */
+ protected final MathTransform gridToCRS;
+
+ /**
+ * Same transform than {@link #gridToCRS} but from {@linkplain PixelInCell#CELL_CORNER pixel corner}
+ * instead than center. This transform is preferable to {@code gridToCRS} for transforming envelopes.
+ *
+ * @serial This field is serialized because it may be a value specified explicitly at construction time,
+ * in which case it can be more accurate than a computed value.
+ */
+ private final MathTransform cornerToCRS;
+
+ /**
+ * The resolution in units of the CRS axes.
+ * Computed only when first needed.
+ */
+ private transient double[] resolution;
+
+ /**
+ * Creates a new grid geometry with the same values than the given grid geometry.
+ * This is a copy constructor for subclasses.
+ *
+ * @param other the other grid geometry to copy.
+ */
+ protected GridGeometry(final GridGeometry other) {
+ extent = other.extent;
+ gridToCRS = other.gridToCRS;
+ cornerToCRS = other.cornerToCRS;
+ envelope = other.envelope;
+ resolution = other.resolution;
+ }
+
+ /**
+ * Creates a new grid geometry from a grid envelope and a math transform mapping pixel <em>center</em>.
+ *
+ * @param extent the valid extent of grid coordinates, or {@code null} if unknown.
+ * @param gridToCRS the math transform which allows for the transformations from grid coordinates
+ * ({@linkplain PixelInCell#CELL_CENTER pixel center}) to real world earth coordinates.
+ * May be {@code null} if unknown.
+ * @param crs the coordinate reference system for the "real world" coordinates, or {@code null} if unknown.
+ * This CRS is given to the {@linkplain #getEnvelope() envelope}.
+ *
+ * @throws MismatchedDimensionException if the math transform and the CRS do not have consistent dimensions.
+ * @throws TransformException if the math transform can not compute the geospatial envelope from the grid envelope.
+ */
+ public GridGeometry(final GridEnvelope extent, final MathTransform gridToCRS, final CoordinateReferenceSystem crs)
+ throws TransformException
+ {
+ this(extent, PixelInCell.CELL_CENTER, gridToCRS, crs);
+ }
+
+ /**
+ * Creates a new grid geometry from a grid envelope and a math transform mapping pixel center or corner.
+ * This is the most general constructor, the one that gives the maximal control over the grid geometry
+ * to be created.
+ *
+ * @param extent the valid extent of grid coordinates, or {@code null} if unknown.
+ * @param anchor {@link PixelInCell#CELL_CENTER} for OGC conventions or
+ * {@link PixelInCell#CELL_CORNER} for Java2D/JAI conventions.
+ * @param gridToCRS the math transform which allows for the transformations from grid coordinates
+ * to real world earth coordinates. May be {@code null} if unknown.
+ * @param crs the coordinate reference system for the "real world" coordinates, or {@code null} if unknown.
+ * This CRS is given to the {@linkplain #getEnvelope() envelope}.
+ * @throws MismatchedDimensionException if the math transform and the CRS do not have consistent dimensions.
+ * @throws TransformException if the math transform can not compute the geospatial envelope from the grid envelope.
+ */
+ public GridGeometry(final GridEnvelope extent, final PixelInCell anchor, final MathTransform gridToCRS,
+ final CoordinateReferenceSystem crs) throws TransformException
+ {
+ if (gridToCRS != null) {
+ if (extent != null) {
+ ensureDimensionMatches("extent", extent.getDimension(), gridToCRS.getSourceDimensions());
+ }
+ if (crs != null) {
+ ensureDimensionMatches("crs", crs.getCoordinateSystem().getDimension(), gridToCRS.getTargetDimensions());
+ }
+ }
+ this.extent = GridExtent.castOrCopy(extent);
+ this.gridToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
+ this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
+ GeneralEnvelope env = null;
+ if (extent != null && gridToCRS != null) {
+ final int dimension = extent.getDimension();
+ env = new GeneralEnvelope(dimension);
+ for (int i=0; i<dimension; i++) {
+ env.setRange(i, extent.getLow(i), extent.getHigh(i) + 1);
+ }
+ env = Envelopes.transform(cornerToCRS, env);
+ if (crs != null) {
+ env.setCoordinateReferenceSystem(crs);
+ }
+ } else if (crs != null) {
+ env = new GeneralEnvelope(crs);
+ env.setToNaN();
+ }
+ envelope = (env != null) ? new ImmutableEnvelope(env) : null;
+ }
+
+ /**
+ * Ensures that the given dimension is equals to the expected value. If not, throws an exception.
+ *
+ * @param argument the name of the argument being tested.
+ * @param dimension the dimension of the argument value.
+ * @param expected the expected dimension.
+ */
+ private static void ensureDimensionMatches(final String argument, final int dimension, final int expected)
+ throws MismatchedDimensionException
+ {
+ if (dimension != expected) {
+ throw new MismatchedDimensionException(Errors.format(
+ Errors.Keys.MismatchedDimension_3, argument, dimension, expected));
+ }
+ }
+
+ /**
+ * Returns the number of dimensions of the <em>grid</em>. This is typically the same
+ * than the number of dimension of the envelope or the CRS, but not necessarily.
+ *
+ * @return the number of grid dimensions.
+ */
+ public int getDimension() {
+ if (gridToCRS != null) {
+ return gridToCRS.getSourceDimensions();
+ }
+ return extent.getDimension();
+ }
+
+ /**
+ * Returns the "real world" coordinate reference system.
+ *
+ * @return the coordinate reference system (never {@code null}).
+ * @throws IncompleteGridGeometryException if this grid geometry has no CRS
+ * (i.e. <code>{@linkplain #isDefined isDefined}({@linkplain #CRS})</code> returned {@code false}).
+ */
+ public CoordinateReferenceSystem getCoordinateReferenceSystem() throws IncompleteGridGeometryException {
+ if (envelope != null) {
+ final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
+ if (crs != null) {
+ assert isDefined(CRS);
+ return crs;
+ }
+ }
+ assert !isDefined(CRS);
+ throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedCrs */);
+ }
+
+ /**
+ * Returns the bounding box of "real world" coordinates for this grid geometry. This envelope is the
+ * {@linkplain #getExtent() grid extent} {@linkplain #getGridToCRS() transformed} to the "real world"
+ * coordinate system.
+ *
+ * @return the bounding box in "real world" coordinates (never {@code null}).
+ * @throws IncompleteGridGeometryException if this grid geometry has no envelope
+ * (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #ENVELOPE})</code> returned {@code false}).
+ */
+ public Envelope getEnvelope() throws IncompleteGridGeometryException {
+ if (envelope != null && !envelope.isAllNaN()) {
+ assert isDefined(ENVELOPE);
+ return envelope;
+ }
+ assert !isDefined(ENVELOPE);
+ throw new IncompleteGridGeometryException(/* gridToCRS == null ?
+ Errors.Keys.UnspecifiedTransform : Errors.Keys.UnspecifiedGridExtent */);
+ }
+
+ /**
+ * Returns the valid coordinate range of a grid coverage. The lowest valid grid coordinate is zero
+ * for {@link java.awt.image.BufferedImage}, but may be non-zero for arbitrary {@link RenderedImage}.
+ * A grid with 512 cells can have a minimum coordinate of 0 and maximum of 511.
+ *
+ * @return the grid envelope (never {@code null}).
+ * @throws IncompleteGridGeometryException if this grid geometry has no extent
+ * (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #EXTENT})</code> returned {@code false}).
+ */
+ public GridEnvelope getExtent() throws IncompleteGridGeometryException {
+ if (extent != null) {
+ assert isDefined(EXTENT);
+ return extent;
+ }
+ assert !isDefined(EXTENT);
+ throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedImageSize */);
+ }
+
+ /**
+ * Returns the transform from grid coordinates to "real world" coordinates in pixel centers.
+ * The transform is often an affine transform.
+ * The coordinate reference system of the real world coordinates is given by
+ * {@link org.opengis.coverage.Coverage#getCoordinateReferenceSystem()}.
+ *
+ * <p><strong>Note:</strong> OGC 01-004 requires that the transform maps <em>pixel centers</em> to real
+ * world coordinates. This is different from some other systems that map pixel's upper left corner.</p>
+ *
+ * @return the transform (never {@code null}).
+ * @throws IncompleteGridGeometryException if this grid geometry has no transform
+ * (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #GRID_TO_CRS})</code> returned {@code false}).
+ */
+ public MathTransform getGridToCRS() throws IncompleteGridGeometryException {
+ if (gridToCRS != null) {
+ assert isDefined(GRID_TO_CRS);
+ return gridToCRS;
+ }
+ assert !isDefined(GRID_TO_CRS);
+ throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedTransform */);
+ }
+
+ /**
+ * Returns the transform from grid coordinates to "real world" coordinates.
+ * This is similar to {@link #getGridToCRS()} except that the transform may map
+ * other pixel parts than {@linkplain PixelInCell#CELL_CENTER pixel center}.
+ *
+ * @param anchor the pixel part to map.
+ * @return the transform (never {@code null}).
+ * @throws IncompleteGridGeometryException if this grid geometry has no transform
+ * (i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #GRID_TO_CRS})</code> returned {@code false}).
+ */
+ public MathTransform getGridToCRS(final PixelInCell anchor) throws IncompleteGridGeometryException {
+ final MathTransform mt;
+ if (PixelInCell.CELL_CENTER.equals(anchor)) {
+ mt = gridToCRS;
+ } else if (PixelInCell.CELL_CORNER.equals(anchor)) {
+ mt = cornerToCRS;
+ } else {
+ mt = PixelTranslation.translate(gridToCRS, PixelInCell.CELL_CENTER, anchor);
+ }
+ if (mt == null) {
+ throw new IncompleteGridGeometryException(/* Errors.Keys.UnspecifiedTransform */);
+ }
+ return mt;
+ }
+
+ /**
+ * Estimates the grid resolution in units of the coordinate reference system.
+ * If non-null, the length of the returned array is the number of CRS dimensions.
+ * If some resolutions are not constant factors (i.e. the {@code gridToCRS} transform for the
+ * corresponding dimension is non-linear), then the resolution is set to one of the following values:
+ *
+ * <ul>
+ * <li>{@link Double#NaN} if {@code allowEstimates} is {@code false}.</li>
+ * <li>an arbitrary resolution otherwise (currently the resolution in the grid center,
+ * but this arbitrary choice may change in any future Apache SIS version).</li>
+ * </ul>
+ *
+ * @param allowEstimates whether to provide some values even for resolutions that are not constant factors.
+ * @return the grid resolution, or {@code null} if unknown.
+ * @throws TransformException if an error occurred while computing the grid resolution.
+ */
+ public double[] resolution(final boolean allowEstimates) throws TransformException {
+ /*
+ * If the gridToCRS transform is linear, we do not even need to check the grid extent;
+ * it can be null. Otherwise (if the transform is non-linear) the extent is mandatory.
+ */
+ Matrix mat = MathTransforms.getMatrix(gridToCRS);
+ if (mat != null) {
+ return resolution(mat, 1);
+ }
+ if (extent == null || gridToCRS == null) {
+ return null;
+ }
+ /*
+ * If we reach this line, the gridToCRS transform has some non-linear parts.
+ * The easiest way to estimate a resolution is to ask for the derivative at
+ * some arbitrary point. For this method, we take the grid center.
+ */
+ final int gridDimension = extent.getDimension();
+ final GeneralDirectPosition gridCenter = new GeneralDirectPosition(gridDimension);
+ for (int i=0; i<gridDimension; i++) {
+ gridCenter.setOrdinate(i, extent.getLow(i) + 0.5*extent.getSpan(i));
+ }
+ final double[] res = resolution(gridToCRS.derivative(gridCenter), 0);
+ if (!allowEstimates) {
+ /*
+ * If we reach this line, we successfully estimated the resolutions but we need to hide non-constant values.
+ * We currently don't have an API for finding the non-linear dimensions. We assume that everthing else than
+ * LinearTransform and pass-through dimensions are non-linear. This is not always true (e.g. in a Mercator
+ * projection, the "longitude → easting" part is linear too), but should be okay for GridGeometry purposes.
+ *
+ * We keep trace of non-linear dimensions in a bitmask, with bits of non-linear dimensions set to 1.
+ * This limit us to 64 dimensions, which is assumed more than enough.
+ */
+ long nonLinearDimensions = 0;
+ for (final MathTransform step : MathTransforms.getSteps(gridToCRS)) {
+ mat = MathTransforms.getMatrix(step);
+ if (mat != null) {
+ /*
+ * For linear transforms there is no bits to set. However if some bits were set by a previous
+ * iteration, we may need to move them (for example the transform may swap axes). We take the
+ * current bitmasks as source dimensions and find what are the target dimensions for them.
+ */
+ long mask = nonLinearDimensions;
+ nonLinearDimensions = 0;
+ while (mask != 0) {
+ final int i = Long.numberOfTrailingZeros(mask); // Source dimension of non-linear part
+ for (int j = mat.getNumRow() - 1; --j >= 0;) { // Possible target dimensions
+ if (mat.getElement(j, i) != 0) {
+ if (j >= Long.SIZE) {
+ throw new ArithmeticException("Excessive number of dimensions.");
+ }
+ nonLinearDimensions |= (1 << j);
+ }
+ }
+ mask &= ~(1 << i);
+ }
+ } else if (step instanceof PassThroughTransform) {
+ /*
+ * Assume that all modified coordinates use non-linear transform. We do not inspect the
+ * sub-transform recursively because if it had a non-linear step, PassThroughTransform
+ * should have moved that step outside the sub-transform for easier concatenation with
+ * the LinearTransforms before of after that PassThroughTransform.
+ */
+ long mask = 0;
+ final int dimIncrease = step.getTargetDimensions() - step.getSourceDimensions();
+ final int maxBits = Long.SIZE - Math.max(dimIncrease, 0);
+ for (final int i : ((PassThroughTransform) step).getModifiedCoordinates()) {
+ if (i >= maxBits) {
+ throw new ArithmeticException("Excessive number of dimensions.");
+ }
+ mask |= (1 << i);
+ }
+ /*
+ * The mask we just computed identifies non-linear source dimensions, but we need target
+ * dimensions. They are usually the same (the pass-through coordinate values do not have
+ * their order changed). However we have a difficulty if the number of dimensions changes.
+ * We know that the change happen in the sub-transform, but we do not know where exactly.
+ * For example if the mask is 001010 and the number of dimensions increases by 1, we know
+ * that we still have "00" at the beginning and "0" at the end of the mask, but we don't
+ * know what happen between the two. Does "101" become "1101" or "1011"? We conservatively
+ * take "1111", i.e. we unconditionally set all bits in the middle to 1.
+ *
+ * Mathematics:
+ * (Long.highestOneBit(mask) << 1) - 1
+ * is a mask identifying all source dimensions before trailing pass-through dimensions.
+ *
+ * maskHigh = (Long.highestOneBit(mask) << (dimIncrease + 1)) - 1
+ * is a mask identifying all target dimensions before trailing pass-through dimensions.
+ *
+ * maskLow = Long.lowestOneBit(mask) - 1
+ * is a mask identifying all leading pass-through dimensions (both source and target).
+ *
+ * maskHigh & ~maskLow
+ * is a mask identifying only target dimensions after leading pass-through and before
+ * trailing pass-through dimensions. In our case, all 1 bits in maskLow are also 1 bits
+ * in maskHigh. So we can rewrite as
+ *
+ * maskHigh - maskLow
+ * and the -1 terms cancel each other.
+ */
+ if (dimIncrease != 0) {
+ mask = (Long.highestOneBit(mask) << (dimIncrease + 1)) - Long.lowestOneBit(mask);
+ }
+ nonLinearDimensions |= mask;
+ } else {
+ /*
+ * Not a know transform. Assume dimension may become non-linear.
+ */
+ return null;
+ }
+ }
+ /*
+ * Set the resolution to NaN for all dimensions that we have determined to be non-linear.
+ */
+ while (nonLinearDimensions != 0) {
+ final int i = Long.numberOfTrailingZeros(nonLinearDimensions);
+ nonLinearDimensions &= ~(1 << i);
+ res[i] = Double.NaN;
+ }
+ }
+ return res;
+ }
+
+ /**
+ * Computes the resolutions from the given matrix. This is the length of each row vector.
+ *
+ * @param numToIgnore number of rows and columns to ignore at the end of the matrix.
+ * This is 0 if the matrix is a derivative (i.e. we ignore nothing), or 1 if the matrix
+ * is an affine transform (i.e. we ignore the translation column and the [0 0 … 1] row).
+ */
+ private static double[] resolution(final Matrix gridToCRS, final int numToIgnore) {
+ final double[] resolution = new double[gridToCRS.getNumRow() - numToIgnore];
+ final double[] buffer = new double[gridToCRS.getNumCol() - numToIgnore];
+ for (int j=0; j<resolution.length; j++) {
+ for (int i=0; i<buffer.length; i++) {
+ buffer[i] = gridToCRS.getElement(j,i);
+ }
+ resolution[j] = MathFunctions.magnitude(buffer);
+ }
+ return resolution;
+ }
+
+ /**
+ * Returns {@code true} if all the parameters specified by the argument are set.
+ *
+ * @param bitmask any combination of {@link #CRS}, {@link #ENVELOPE}, {@link #EXTENT} and {@link #GRID_TO_CRS}.
+ * @return {@code true} if all specified attributes are defined (i.e. invoking the
+ * corresponding method will not thrown an {@link IncompleteGridGeometryException}).
+ * @throws IllegalArgumentException if the specified bitmask is not a combination of known masks.
+ */
+ public boolean isDefined(final int bitmask) throws IllegalArgumentException {
+ if ((bitmask & ~(CRS | ENVELOPE | EXTENT | GRID_TO_CRS)) != 0) {
+ throw new IllegalArgumentException(Errors.format(
+ Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
+ }
+ return ((bitmask & CRS) == 0 || (envelope != null && envelope.getCoordinateReferenceSystem() != null))
+ && ((bitmask & ENVELOPE) == 0 || (envelope != null && !envelope.isAllNaN()))
+ && ((bitmask & EXTENT) == 0 || (extent != null))
+ && ((bitmask & GRID_TO_CRS) == 0 || (gridToCRS != null));
+ }
+
+ /**
+ * Returns a hash value for this grid geometry. This value need not remain
+ * consistent between different implementations of the same class.
+ */
+ @Override
+ public int hashCode() {
+ int code = (int) serialVersionUID;
+ if (gridToCRS != null) {
+ code += gridToCRS.hashCode();
+ }
+ if (extent != null) {
+ code += extent.hashCode();
+ }
+ // We do not check the envelope since it has a determinist relationship with other attributes.
+ return code;
+ }
+
+ /**
+ * Compares the specified object with this grid geometry for equality.
+ *
+ * @param object the object to compare with.
+ * @return {@code true} if the given object is equals to this grid geometry.
+ */
+ @Override
+ public boolean equals(final Object object) {
+ if (object != null && object.getClass() == getClass()) {
+ final GridGeometry that = (GridGeometry) object;
+ return Objects.equals(this.extent, that.extent) &&
+ Objects.equals(this.gridToCRS, that.gridToCRS) &&
+ Objects.equals(this.envelope, that.envelope);
+ }
+ return false;
+ }
+
+ /**
+ * Returns a string representation of this grid geometry. The returned string
+ * is implementation dependent. It is provided for debugging purposes only.
+ */
+ @Debug
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + '[' + extent + ", " + gridToCRS + ']';
+ }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/IncompleteGridGeometryException.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/IncompleteGridGeometryException.java
new file mode 100644
index 0000000..0ea406c
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/IncompleteGridGeometryException.java
@@ -0,0 +1,70 @@
+/*
+ * 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.coverage.grid;
+
+import org.apache.sis.internal.raster.Resources;
+
+
+/**
+ * Thrown by {@link GridGeometry} when a grid geometry can not provide the requested information.
+ * For example this exception is thrown when {@link GridGeometry#getEnvelope()} is invoked while
+ * the grid geometry has been built with a null envelope.
+ *
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+public class IncompleteGridGeometryException extends IllegalStateException {
+ /**
+ * Serial number for inter-operability with different versions.
+ */
+ private static final long serialVersionUID = -7386283388753448743L;
+
+ /**
+ * Constructs an exception with no detail message.
+ */
+ public IncompleteGridGeometryException() {
+ }
+
+ /**
+ * Constructs an exception with a detail message from the specified error code.
+ * Should not be public because the SIS I18N framework is not a committed one.
+ */
+ IncompleteGridGeometryException(final short code) {
+ super(Resources.format(code));
+ }
+
+ /**
+ * Constructs an exception with the specified detail message.
+ *
+ * @param message the detail message.
+ */
+ public IncompleteGridGeometryException(final String message) {
+ super(message);
+ }
+
+ /**
+ * Constructs an exception with the specified detail message and cause.
+ *
+ * @param message the detail message.
+ * @param cause the cause for this exception.
+ */
+ public IncompleteGridGeometryException(final String message, final Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/PixelTranslation.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/PixelTranslation.java
new file mode 100644
index 0000000..ab683b0
--- /dev/null
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/PixelTranslation.java
@@ -0,0 +1,358 @@
+/*
+ * 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.coverage.grid;
+
+import java.util.Map;
+import java.util.HashMap;
+import java.util.Objects;
+import java.io.Serializable;
+
+import org.opengis.referencing.operation.Matrix;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.metadata.spatial.PixelOrientation;
+import static org.opengis.metadata.spatial.PixelOrientation.*;
+
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.Static;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.referencing.operation.matrix.Matrices;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+
+
+/**
+ * The translation to apply for different values of {@link PixelOrientation} or {@link PixelInCell}.
+ * The translation are returned by a call to one of the following static methods:
+ *
+ * <ul>
+ * <li>{@link #getPixelTranslation(PixelOrientation)} for the two-dimensional case.</li>
+ * <li>{@link #getPixelTranslation(PixelInCell)} for the <var>n</var>-dimensional case.</li>
+ * </ul>
+ *
+ * This class provides also a few {@code translate(…)} convenience methods,
+ * which apply the pixel translation on a given {@link MathTransform} instance.
+ *
+ * <div class="note"><b>Example:</b>
+ * if the following code snippet, {@code gridToCRS} is an {@link java.awt.geom.AffineTransform} from
+ * <cite>grid cell</cite> coordinates (typically pixel coordinates) to some arbitrary CRS coordinates.
+ * In this example, the transform maps pixels {@linkplain PixelOrientation#CENTER center},
+ * while the {@linkplain PixelOrientation#UPPER_LEFT upper left} corner is desired.
+ * This code will switch the affine transform from the <cite>pixel center</cite> to
+ * <cite>upper left corner</cite> convention:
+ *
+ * {@preformat java
+ * final AffineTransform gridToCRS = ...;
+ * final PixelOrientation current = PixelOrientation.CENTER;
+ * final PixelOrientation expected = PixelOrientation.UPPER_LEFT;
+ *
+ * // Switch the transform from 'current' to 'expected' convention.
+ * final PixelTranslation source = getPixelTranslation(current);
+ * final PixelTranslation target = getPixelTranslation(expected);
+ * gridToCRS.translate(target.dx - source.dx,
+ * target.dy - source.dy);
+ * }
+ * </div>
+ *
+ * @author Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ *
+ * @see PixelInCell
+ * @see PixelOrientation
+ *
+ * @since 1.0
+ * @module
+ */
+public final class PixelTranslation extends Static implements Serializable {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = -5671620211497720808L;
+
+ /**
+ * Math transforms created by {@link #translate(MathTransform, PixelInCell, PixelInCell)}.
+ * Each element in this array will be created when first needed.
+ * Even indices are translations by -0.5 while odd indices are translations by +0.5.
+ */
+ private static final MathTransform[] translations = new MathTransform[16];
+
+ /**
+ * The pixel orientation for this translation.
+ */
+ public final PixelOrientation orientation;
+
+ /**
+ * The translation among the <var>x</var> axis relative to pixel center.
+ * The value is typically in the [-0.5 .. +0.5] range.
+ */
+ public final double dx;
+
+ /**
+ * The translation among the <var>y</var> axis relative to pixel center.
+ * The value is typically in the [-0.5 .. +0.5] range.
+ */
+ public final double dy;
+
+ /**
+ * The offset for various pixel orientations. Keys must be upper-case names.
+ */
+ private static final Map<PixelOrientation, PixelTranslation> ORIENTATIONS = new HashMap<>(12);
+ static {
+ add(CENTER, 0.0, 0.0);
+ add(UPPER_LEFT, -0.5, -0.5);
+ add(UPPER_RIGHT, 0.5, -0.5);
+ add(LOWER_LEFT, -0.5, 0.5);
+ add(LOWER_RIGHT, 0.5, 0.5);
+ }
+
+ /** For {@link #ORIENTATIONS} construction only. */
+ private static void add(final PixelOrientation orientation, final double dx, final double dy) {
+ if (ORIENTATIONS.put(orientation, new PixelTranslation(orientation, dx, dy)) != null) {
+ throw new AssertionError();
+ }
+ }
+
+ /**
+ * Creates a new pixel translation.
+ */
+ private PixelTranslation(final PixelOrientation orientation, final double dx, final double dy) {
+ this.orientation = orientation;
+ this.dx = dx;
+ this.dy = dy;
+ }
+
+ /**
+ * Returns the pixel orientation for the given {@code PixelInCell} code.
+ *
+ * @param anchor the {@code PixelInCell} code, or {@code null}.
+ * @return the corresponding pixel orientation, or {@code null} if the argument was null.
+ * @throws IllegalArgumentException if the given code is unknown.
+ */
+ public static PixelOrientation getPixelOrientation(final PixelInCell anchor) {
+ if (anchor == null) {
+ return null;
+ } else if (anchor.equals(PixelInCell.CELL_CENTER)) {
+ return CENTER;
+ } else if (anchor.equals(PixelInCell.CELL_CORNER)) {
+ return UPPER_LEFT;
+ } else {
+ throw new IllegalArgumentException(Errors.format(
+ Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
+ }
+ }
+
+ /**
+ * Returns the position relative to the pixel center.
+ * This method returns a value from the following table:
+ *
+ * <table>
+ * <caption>Translations for "pixel in cell" values</caption>
+ * <tr><th>Pixel in cell</th><th>offset</th></tr>
+ * <tr><td>{@link PixelInCell#CELL_CENTER CELL_CENTER}</td><td> 0.0</td></tr>
+ * <tr><td>{@link PixelInCell#CELL_CORNER CELL_CORNER}</td><td>-0.5</td></tr>
+ * </table>
+ *
+ * This method is typically used for <var>n</var>-dimensional grids,
+ * where the number of dimension is unknown.
+ *
+ * @param anchor the "pixel in cell" value.
+ * @return the translation for the given "pixel in cell" value.
+ */
+ public static double getPixelTranslation(final PixelInCell anchor) {
+ if (PixelInCell.CELL_CENTER.equals(anchor)) {
+ return 0;
+ } else if (PixelInCell.CELL_CORNER.equals(anchor)) {
+ return -0.5;
+ } else {
+ throw new IllegalArgumentException(Errors.format(
+ Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
+ }
+ }
+
+ /**
+ * Returns the specified position relative to the pixel center.
+ * This method returns a value from the following table:
+ *
+ * <table>
+ * <caption>Translations for "pixel orientation" values</caption>
+ * <tr><th>Pixel orientation</th> <th> x </th><th> y </th></tr>
+ * <tr><td>{@link PixelOrientation#CENTER CENTER}</td> <td> 0.0</td><td> 0.0</td></tr>
+ * <tr><td>{@link PixelOrientation#UPPER_LEFT UPPER_LEFT}</td> <td>-0.5</td><td>-0.5</td></tr>
+ * <tr><td>{@link PixelOrientation#UPPER_RIGHT UPPER_RIGHT}</td><td>+0.5</td><td>-0.5</td></tr>
+ * <tr><td>{@link PixelOrientation#LOWER_LEFT LOWER_LEFT}</td> <td>-0.5</td><td>+0.5</td></tr>
+ * <tr><td>{@link PixelOrientation#LOWER_RIGHT LOWER_RIGHT}</td><td>+0.5</td><td>+0.5</td></tr>
+ * </table>
+ *
+ * This method can be used for grid restricted to 2 dimensions.
+ *
+ * @param anchor the pixel orientation.
+ * @return the position relative to the pixel center.
+ * @throws IllegalArgumentException if the specified orientation is unknown.
+ */
+ public static PixelTranslation getPixelTranslation(final PixelOrientation anchor) {
+ final PixelTranslation offset = ORIENTATIONS.get(anchor);
+ if (offset == null) {
+ throw new IllegalArgumentException(Errors.format(
+ Errors.Keys.IllegalArgumentValue_2, "anchor", anchor));
+ }
+ return offset;
+ }
+
+ /**
+ * Returns the pixel orientation for the given offset, or {@code null} if none.
+ * This is the reverse of {@link #getPixelTranslation(PixelOrientation)}.
+ *
+ * @param dx the translation along <var>x</var> axis.
+ * @param dy the translation along <var>y</var> axis.
+ * @return the pixel orientation of the given values, or {@code null} if none.
+ */
+ public static PixelOrientation getPixelOrientation(final double dx, final double dy) {
+ for (final PixelTranslation candidate : ORIENTATIONS.values()) {
+ if (candidate.dx == dx && candidate.dy == dy) {
+ return candidate.orientation;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Translates the specified math transform according the specified pixel orientations.
+ *
+ * @param gridToCRS a math transform from <cite>pixel</cite> coordinates to any CRS.
+ * @param current the pixel orientation of the given {@code gridToCRS} transform.
+ * @param expected the pixel orientation of the desired transform.
+ * @return the translation from {@code current} to {@code expected}.
+ */
+ public static MathTransform translate(final MathTransform gridToCRS, final PixelInCell current, final PixelInCell expected) {
+ if (Objects.equals(current, expected)) {
+ return gridToCRS;
+ }
+ if (gridToCRS == null) {
+ return null;
+ }
+ final int dimension = gridToCRS.getSourceDimensions();
+ final double offset = getPixelTranslation(expected) - getPixelTranslation(current);
+ final int index;
+ if (offset == -0.5) {
+ index = 2*dimension;
+ } else if (offset == 0.5) {
+ index = 2*dimension + 1;
+ } else {
+ index = translations.length;
+ }
+ MathTransform mt;
+ if (index >= translations.length) {
+ mt = translate(dimension, offset);
+ } else synchronized (translations) {
+ mt = translations[index];
+ if (mt == null) {
+ mt = translate(dimension, offset);
+ translations[index] = mt;
+ }
+ }
+ return MathTransforms.concatenate(mt, gridToCRS);
+ }
+
+ /**
+ * Translates the specified math transform according the specified pixel orientations.
+ *
+ * @param gridToCRS a math transform from <cite>pixel</cite> coordinates to any CRS.
+ * @param current the pixel orientation of the given {@code gridToCRS} transform.
+ * @param expected the pixel orientation of the desired transform.
+ * @param xDimension the dimension of <var>x</var> coordinates (pixel columns). Often 0.
+ * @param yDimension the dimension of <var>y</var> coordinates (pixel rows). Often 1.
+ * @return the translation from {@code current} to {@code expected}.
+ */
+ public static MathTransform translate(final MathTransform gridToCRS,
+ final PixelOrientation current,
+ final PixelOrientation expected,
+ final int xDimension,
+ final int yDimension)
+ {
+ if (Objects.equals(current, expected)) {
+ return gridToCRS;
+ }
+ if (gridToCRS == null) {
+ return null;
+ }
+ final int dimension = gridToCRS.getSourceDimensions();
+ if (xDimension < 0 || xDimension >= dimension) {
+ throw illegalDimension("xDimension", xDimension);
+ }
+ if (yDimension < 0 || yDimension >= dimension) {
+ throw illegalDimension("yDimension", yDimension);
+ }
+ if (xDimension == yDimension) {
+ throw illegalDimension("xDimension", "yDimension");
+ }
+ final PixelTranslation source = getPixelTranslation(current);
+ final PixelTranslation target = getPixelTranslation(expected);
+ final double dx = target.dx - source.dx;
+ final double dy = target.dy - source.dy;
+ MathTransform mt;
+ if (dimension == 2 && (xDimension | yDimension) == 1 && dx == dy && Math.abs(dx) == 0.5) {
+ final int index = (dx >= 0) ? 5 : 4;
+ synchronized (translations) {
+ mt = translations[index];
+ if (mt == null) {
+ mt = translate(dimension, dx);
+ translations[index] = mt;
+ }
+ }
+ } else {
+ final Matrix matrix = Matrices.createIdentity(dimension + 1);
+ matrix.setElement(xDimension, dimension, dx);
+ matrix.setElement(yDimension, dimension, dy);
+ mt = MathTransforms.linear(matrix);
+ }
+ return MathTransforms.concatenate(mt, gridToCRS);
+ }
+
+ /**
+ * Formats an exception for an illegal dimension.
+ */
+ private static IllegalArgumentException illegalDimension(final String name, final Object dimension) {
+ return new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, name, dimension));
+ }
+
+ /**
+ * Creates an affine transform that apply the same linear conversion for all dimensions.
+ * For each dimension, input values <var>x</var> are converted into output values <var>y</var>
+ * using the following equation:
+ *
+ * <blockquote><var>y</var> = <var>x</var> × {@code scale} + {@code offset}</blockquote>
+ *
+ * @param dimension the input and output dimensions.
+ * @param offset the {@code offset} term in the linear equation.
+ * @return the linear transform for the given scale and offset.
+ */
+ private static MathTransform translate(final int dimension, final double offset) {
+ final Matrix matrix = Matrices.createIdentity(dimension + 1);
+ for (int i=0; i<dimension; i++) {
+ matrix.setElement(i, dimension, offset);
+ }
+ return MathTransforms.linear(matrix);
+ }
+
+ /**
+ * Returns a string representation of this pixel translation.
+ */
+ @Debug
+ @Override
+ public String toString() {
+ return String.valueOf(orientation) + '[' + dx + ", " + dy + ']';
+ }
+}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/package-info.java
similarity index 73%
copy from core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
copy to core/sis-raster/src/main/java/org/apache/sis/coverage/grid/package-info.java
index 8a19ee5..597ff30 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/package-info.java
@@ -15,17 +15,14 @@
* limitations under the License.
*/
+
/**
- * A set of helper classes for the SIS implementation.
- *
- * <p><strong>Do not use!</strong></p>
- *
- * This package is for internal use by SIS only. Classes in this package
- * may change in incompatible ways in any future version without notice.
+ * A coverage backed by a regular grid. In the two-dimensional case, the grid coverage is an image and the cells are pixels.
+ * In the three-dimensional case, the cells are voxels.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*/
-package org.apache.sis.internal.raster;
+package org.apache.sis.coverage.grid;
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java b/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java
index bef1739..cd5f58e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/DefaultIterator.java
@@ -48,8 +48,8 @@ import org.apache.sis.util.ArgumentChecks;
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*
* @todo Change iteration order on tiles for using Hilbert iterator.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java b/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java
index 2b9fd54..c076236 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/PixelIterator.java
@@ -53,8 +53,8 @@ import static java.lang.Math.floorDiv;
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*/
public abstract class PixelIterator {
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java b/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java
index b611308..e70c1ee 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/TransferType.java
@@ -42,11 +42,11 @@ import org.apache.sis.util.resources.Errors;
* <a href="http://openjdk.java.net/jeps/301">JEP 301</a> is implemented.</div>
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.0
*
* @param <T> the type of buffer which can be used for transferring data.
*
- * @since 0.8
+ * @since 1.0
* @module
*
* @see Raster#getTransferType()
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java b/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java
index 5e8a8aa..2e71179 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/WritablePixelIterator.java
@@ -51,8 +51,8 @@ import org.apache.sis.internal.raster.Resources;
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*/
public abstract class WritablePixelIterator extends PixelIterator implements Closeable {
diff --git a/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java b/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java
index fbb7f73..9150e80 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/image/package-info.java
@@ -34,8 +34,8 @@
*
* @author Rémi Maréchal (Geomatys)
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*/
package org.apache.sis.image;
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
index 7287cb1..01256eb 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.java
@@ -30,8 +30,8 @@ import org.apache.sis.util.resources.IndexedResourceBundle;
* all modules in the Apache SIS project, see {@link org.apache.sis.util.resources} package.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*/
public final class Resources extends IndexedResourceBundle {
@@ -42,7 +42,7 @@ public final class Resources extends IndexedResourceBundle {
* pools of compiled classes.
*
* @author Martin Desruisseaux (Geomatys)
- * @since 0.8
+ * @since 1.0
* @module
*/
@Generated("org.apache.sis.util.resources.IndexedResourceCompiler")
@@ -64,6 +64,11 @@ public final class Resources extends IndexedResourceBundle {
public static final short CoordinateOutsideDomain_2 = 1;
/**
+ * Illegal grid envelope [{1} … {2}] for dimension {0}.
+ */
+ public static final short IllegalGridEnvelope_3 = 8;
+
+ /**
* The ({0}, {1}) tile has an unexpected size, number of bands or sample layout.
*/
public static final short IncompatibleTile_2 = 2;
@@ -167,4 +172,23 @@ public final class Resources extends IndexedResourceBundle {
{
return forLocale(null).getString(key, arg0, arg1);
}
+
+ /**
+ * Gets a string for the given key are replace all occurrence of "{0}",
+ * "{1}", with values of {@code arg0}, {@code arg1}, etc.
+ *
+ * @param key the key for the desired string.
+ * @param arg0 value to substitute to "{0}".
+ * @param arg1 value to substitute to "{1}".
+ * @param arg2 value to substitute to "{2}".
+ * @return the formatted string for the given key.
+ * @throws MissingResourceException if no object for the given key can be found.
+ */
+ public static String format(final short key,
+ final Object arg0,
+ final Object arg1,
+ final Object arg2) throws MissingResourceException
+ {
+ return forLocale(null).getString(key, arg0, arg1, arg2);
+ }
}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
index 510912e..7ffc66e 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources.properties
@@ -20,6 +20,7 @@
# For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
#
CoordinateOutsideDomain_2 = The ({0}, {1}) pixel coordinate is outside iterator domain.
+IllegalGridEnvelope_3 = Illegal grid envelope [{1} \u2026 {2}] for dimension {0}.
IncompatibleTile_2 = The ({0}, {1}) tile has an unexpected size, number of bands or sample layout.
IterationIsFinished = Iteration is finished.
IterationNotStarted = Iteration did not started.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
index 9ac7916..418351a 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/Resources_fr.properties
@@ -25,6 +25,7 @@
# U+00A0 NO-BREAK SPACE before :
#
CoordinateOutsideDomain_2 = La coordonn\u00e9e pixel ({0}, {1}) est en dehors du domaine de l\u2019it\u00e9rateur.
+IllegalGridEnvelope_3 = La plage d\u2019index [{1} \u2026 {2}] de la dimension {0} n\u2019est pas valide.
IncompatibleTile_2 = La tuile ({0}, {1}) a une taille, un nombre de bandes ou une disposition des valeurs inattendu.
IterationIsFinished = L\u2019it\u00e9ration est termin\u00e9e.
IterationNotStarted = L\u2019it\u00e9ration n\u2019a pas commenc\u00e9e.
diff --git a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
index 8a19ee5..29a4094 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/internal/raster/package-info.java
@@ -24,8 +24,8 @@
* may change in incompatible ways in any future version without notice.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 0.8
- * @since 0.8
+ * @version 1.0
+ * @since 1.0
* @module
*/
package org.apache.sis.internal.raster;
--
To stop receiving notification emails like this one, please contact
desruisseaux@apache.org.