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 2019/12/18 19:07:27 UTC
[sis] 02/02: Partial review of
GridCoverage.evaluate(DirectPosition) method. - Add a new
FractionalGridCoordinates class with some of the calculations done by
GridCoverage methods. - Rename toGridCoord as toGridCoordinates and change
the return type to FractionalGridCoordinates. - Remove
toLongExact(DirectPosition), replaced by FractionalGridCoordinates.
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 0431f2e9f297cab118d4c31a6fb7cb24b5fe2e23
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Wed Dec 18 14:38:22 2019 +0100
Partial review of GridCoverage.evaluate(DirectPosition) method.
- Add a new FractionalGridCoordinates class with some of the calculations done by GridCoverage methods.
- Rename toGridCoord as toGridCoordinates and change the return type to FractionalGridCoordinates.
- Remove toLongExact(DirectPosition), replaced by FractionalGridCoordinates.
---
.../coverage/grid/FractionalGridCoordinates.java | 473 +++++++++++++++++++++
.../sis/coverage/grid/GridCoordinatesView.java | 10 +-
.../org/apache/sis/coverage/grid/GridCoverage.java | 191 +++++----
.../org/apache/sis/coverage/grid/GridExtent.java | 71 ++--
.../org/apache/sis/coverage/grid/GridGeometry.java | 4 +-
.../sis/coverage/grid/PointToGridCoordinates.java | 81 ++++
.../sis/internal/coverage/GridCoverage2D.java | 232 ++++++----
.../org/apache/sis/internal/feature/Resources.java | 5 +
.../sis/internal/feature/Resources.properties | 1 +
.../sis/internal/feature/Resources_fr.properties | 1 +
.../grid/FractionalGridCoordinatesTest.java | 101 +++++
.../coverage/BufferedGridCoverageTest.java | 3 +-
.../sis/internal/coverage/GridCoverage2DTest.java | 3 +-
.../apache/sis/test/suite/FeatureTestSuite.java | 1 +
14 files changed, 956 insertions(+), 221 deletions(-)
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
new file mode 100644
index 0000000..d7df49c
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/FractionalGridCoordinates.java
@@ -0,0 +1,473 @@
+/*
+ * 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.opengis.geometry.DirectPosition;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.coverage.grid.GridCoordinates;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
+import org.apache.sis.internal.feature.Resources;
+import org.apache.sis.internal.util.Strings;
+import org.apache.sis.util.StringBuilders;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Grid coordinates which may have fraction digits after the integer part.
+ * Grid coordinates specify the location of a cell within a {@link GridCoverage}.
+ * They are normally integer numbers, but fractional parts may exist for example
+ * after converting a geospatial {@link DirectPosition} to grid coordinates.
+ * Preserving that fractional part is sometime useful, e.g. for interpolations.
+ * This class can store such fractional part and can also compute a {@link GridExtent}
+ * containing the coordinates, which can be used for requesting data for interpolations.
+ *
+ * <p>Current implementation stores coordinate values as {@code double} precision floating-point numbers
+ * and {@linkplain Math#round(double) rounds} them to 64-bits integers on the fly. If a {@code double}
+ * can not be {@linkplain #getCoordinateValue(int) returned} as a {@code long}, or if a {@code long}
+ * can not be {@linkplain #setCoordinateValue(int, long) stored} as a {@code double}, then an
+ * {@link ArithmeticException} is thrown.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @see GridCoverage#toGridCoordinates(DirectPosition)
+ *
+ * @since 1.1
+ * @module
+ */
+public class FractionalGridCoordinates implements GridCoordinates, Serializable {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = 5652265407347129550L;
+
+ /**
+ * The grid coordinates as floating-point numbers.
+ */
+ final double[] coordinates;
+
+ /**
+ * Creates a new grid coordinates with the given number of dimensions.
+ *
+ * <div class="note"><b>Note:</b>
+ * {@code FractionalGridCoordinates} are usually not created directly, but are instead obtained
+ * indirectly for example from the {@linkplain GridCoverage#toGridCoordinates(DirectPosition)
+ * conversion of a geospatial position}.</div>
+ *
+ * @param dimension the number of dimensions.
+ */
+ public FractionalGridCoordinates(final int dimension) {
+ coordinates = new double[dimension];
+ }
+
+ /**
+ * Creates a new grid coordinates initialized to a copy of the given coordinates.
+ *
+ * @param other the coordinates to copy.
+ */
+ public FractionalGridCoordinates(final FractionalGridCoordinates other) {
+ coordinates = other.coordinates.clone();
+ }
+
+ /**
+ * Returns the number of dimension of this grid coordinates.
+ *
+ * @return the number of dimensions.
+ */
+ @Override
+ public int getDimension() {
+ return coordinates.length;
+ }
+
+ /**
+ * Returns one integer value for each dimension of the grid.
+ * The default implementation invokes {@link #getCoordinateValue(int)}
+ * for each element in the returned array.
+ *
+ * @return a copy of the coordinates. Changes in the returned array will
+ * not be reflected back in this {@code GridCoordinates} object.
+ * @throws ArithmeticException if a coordinate value is outside the range
+ * of values representable as a 64-bits integer value.
+ */
+ @Override
+ public long[] getCoordinateValues() {
+ final long[] indices = new long[coordinates.length];
+ for (int i=0; i<indices.length; i++) {
+ indices[i] = getCoordinateValue(i);
+ }
+ return indices;
+ }
+
+ /**
+ * Returns the grid coordinate value at the specified dimension.
+ * Floating-point values are rounded to the nearest 64-bits integer values.
+ * If the coordinate value is NaN or outside the range of {@code long} values,
+ * then an {@link ArithmeticException} is thrown.
+ *
+ * @param dimension the dimension for which to obtain the coordinate value.
+ * @return the coordinate value at the given dimension,
+ * {@linkplain Math#round(double) rounded} to nearest integer.
+ * @throws IndexOutOfBoundsException if the given index is negative or is
+ * equal or greater than the {@linkplain #getDimension grid dimension}.
+ * @throws ArithmeticException if the coordinate value is outside the range
+ * of values representable as a 64-bits integer value.
+ */
+ @Override
+ public long getCoordinateValue(final int dimension) {
+ final double value = coordinates[dimension];
+ /*
+ * 2048 is the smallest value than can be added or removed to Long.MIN/MAX_VALUE,
+ * as given by Math.ulp(Long.MIN_VALUE). We add this tolerance since the contract
+ * is to return the `long` value closest to the `double` value and we consider a
+ * 1 ULP error as close enough.
+ */
+ if (value >= (Long.MIN_VALUE - 2048d) && value <= (Long.MAX_VALUE + 2048d)) {
+ return Math.round(value);
+ }
+ throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "long", value));
+ }
+
+ /**
+ * Returns a grid coordinate value together with its fractional part, if any.
+ *
+ * @param dimension the dimension for which to obtain the coordinate value.
+ * @return the coordinate value at the given dimension.
+ * @throws IndexOutOfBoundsException if the given index is negative or is
+ * equal or greater than the {@linkplain #getDimension grid dimension}.
+ */
+ public double getCoordinateFractional(final int dimension) {
+ return coordinates[dimension];
+ }
+
+ /**
+ * Sets the coordinate value at the specified dimension. The given value can not be
+ * NaN or infinite and shall be convertible to {@code long} without precision lost.
+ *
+ * @param dimension the dimension for which to set the coordinate value.
+ * @param value the new value.
+ * @throws IndexOutOfBoundsException if the given index is negative or is
+ * equal or greater than the {@linkplain #getDimension grid dimension}.
+ * @throws ArithmeticException if this method can not store the given grid coordinate
+ * without precision lost.
+ */
+ @Override
+ public void setCoordinateValue(final int dimension, final long value) {
+ if ((coordinates[dimension] = value) != value) {
+ throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "double", value));
+ }
+ }
+
+ /**
+ * Creates a new grid extent around this grid coordinates. The returned extent will have the same number
+ * of dimensions than this grid coordinates. For each dimension <var>i</var> the following relationships
+ * will hold:
+ *
+ * <ol>
+ * <li>If <code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> ≥ 2 and no shift (see below) then:<ul>
+ * <li><code>extent.{@linkplain GridExtent#getLow(int) getLow}(i)</code> ≤
+ * <code>{@linkplain #getCoordinateFractional(int) getCoordinateFractional}(i)</code></li>
+ * <li><code>extent.{@linkplain GridExtent#getHigh(int) getHigh}(i)</code> ≥
+ * <code>{@linkplain #getCoordinateFractional(int) getCoordinateFractional}(i)</code></li>
+ * </ul></li>
+ * <li>If {@code bounds.getSize(i)} ≥ {@code size[i]} and {@code size[i]} ≠ 0 then:<ul>
+ * <li><code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> = {@code size[i]}</li>
+ * </ul></li>
+ * </ol>
+ *
+ * <p>The {@code size} argument is optional and can be incomplete (i.e. the number of {@code size} values can be
+ * less than the number of dimensions). For each dimension <var>i</var>, if a {@code size[i]} value is provided
+ * and is not zero, then this method tries to expand the extent in that dimension to the specified {@code size[i]}
+ * value as shown in constraint #2 above. Otherwise the default size is the smallest possible extent that met
+ * constraint #1 above, clipped to the {@code bounds}. This implies a size of 1 if the grid coordinate in that
+ * dimension is an integer, or a size of 2 (before clipping to the bounds) if the grid coordinate has a fractional
+ * part.</p>
+ *
+ * <p>The {@code bounds} argument is also optional.
+ * If non-null, then this method enforces the following additional rules:</p>
+ *
+ * <ul>
+ * <li>Coordinates must be inside the given bounds, otherwise an {@link DisjointExtentException} is thrown.</li>
+ * <li>If the computed extent overlaps an area outside the bounds, then the extent will be shifted (if an explicit
+ * size was given) or clipped (if default size is used) in order to be be fully contained inside the bounds.</li>
+ * <li>If a given size is larger than the corresponding bounds {@linkplain GridExtent#getSize(int) size},
+ * then the returned extent will be clipped to the bounds.</li>
+ * </ul>
+ *
+ * <p>In all cases, this method tries to keep the grid coordinates close to the center of the returned extent.
+ * A shift may exist if necessary for keeping the extent inside the {@code bounds} argument, but will never
+ * move the grid coordinates outside the [<var>low</var> … <var>high</var>+1) range of returned extent.</p>
+ *
+ * @param bounds if the coordinates shall be contained inside a grid, that grid. Otherwise {@code null}.
+ * @param size the desired extent sizes as strictly positive numbers, or 0 sentinel values for automatic
+ * sizes (1 or 2 depending on bounds and coordinate values). This array may have any length;
+ * if shorter than the number of dimensions, missing values default to 0.
+ * If longer than the number of dimensions, extra values are ignored.
+ * @throws IllegalArgumentException if a {@code size} value is negative.
+ * @throws ArithmeticException if a coordinate value is outside the range of {@code long} values.
+ * @throws MismatchedDimensionException if {@code bounds} dimension is not equal to grid coordinates dimension.
+ * @throws DisjointExtentException if the returned extent would not intersect the given bounds.
+ * @return a grid extent of the given size (if possible) containing those grid coordinates.
+ */
+ public GridExtent toExtent(final GridExtent bounds, final long... size) {
+ final int dimension = coordinates.length;
+ if (bounds != null) {
+ final int bd = bounds.getDimension();
+ if (bd != dimension) {
+ throw new MismatchedDimensionException(Errors.format(
+ Errors.Keys.MismatchedDimension_3, "bounds", dimension, bd));
+ }
+ }
+ final long[] extent = GridExtent.allocate(dimension);
+ for (int i=0; i<dimension; i++) {
+ final double value = coordinates[i];
+ if (!(value >= Long.MIN_VALUE && value <= Long.MAX_VALUE)) { // Use ! for catching NaN values.
+ throw new ArithmeticException(Resources.format(
+ Resources.Keys.UnconvertibleGridCoordinate_2, "long", value));
+ }
+ long margin = 0;
+ if (i < size.length) {
+ margin = size[i];
+ if (margin < 0) {
+ throw new IllegalArgumentException(Errors.format(
+ Errors.Keys.NegativeArgument_2, Strings.toIndexed("size", i), margin));
+ }
+ }
+ /*
+ * The lower/upper values are given by Math.floor/ceil respectively (may be equal).
+ * However we do an exception to this rule if user asked explicitly for a size of 1.
+ * In such case we can no longer enforce the `lower ≤ value ≤ upper` rule. The best
+ * we can do is to take the nearest neighbor.
+ */
+ long lower, upper;
+ if (margin == 1) {
+ lower = upper = Math.round(value);
+ } else {
+ final double base = Math.floor(value);
+ lower = (long) base; // Inclusive.
+ upper = (long) Math.ceil(value); // Inclusive too (lower == upper if value is an integer).
+ if (margin != 0) {
+ margin -= (upper - lower + 1);
+ assert margin >= 0 : margin; // Because (upper - lower + 1) ≤ 2
+ if ((margin & 1) != 0) {
+ if (value - base >= 0.5) {
+ upper = Math.incrementExact(upper);
+ } else {
+ lower = Math.decrementExact(lower);
+ }
+ }
+ margin /= 2;
+ lower = Math.subtractExact(lower, margin);
+ upper = Math.addExact(upper, margin);
+ margin = 2; // Any value different than 0 for remembering that it was explicitly specified.
+ }
+ }
+ /*
+ * At this point the grid range has been computed (lower to upper).
+ * Shift it if needed for keeping it inside the enclosing extent.
+ */
+ if (bounds != null) {
+ final long validMin = bounds.getLow(i);
+ final long validMax = bounds.getHigh(i);
+ if (lower > validMax || upper < validMin) {
+ throw new DisjointExtentException(bounds.getAxisIdentification(i,i), validMin, validMax, lower, upper);
+ }
+ if (upper > validMax) {
+ if (margin != 0) { // In automatic mode (margin = 0) just clip, don't shift.
+ /*
+ * Because (upper - validMax) is always positive, then (t > lower) would mean
+ * that we have an overflow. In such cases we do not need the result since we
+ * know that we are outside the enclosing extent anyway.
+ */
+ final long t = lower - Math.subtractExact(upper, validMax);
+ lower = (t >= validMin && t <= lower) ? t : validMin;
+ }
+ upper = validMax;
+ }
+ if (lower < validMin) {
+ if (margin != 0) {
+ final long t = upper + Math.subtractExact(validMin, lower);
+ upper = (t <= validMax && t >= upper) ? t : validMax; // Same rational than above.
+ }
+ lower = validMin;
+ }
+ }
+ extent[i] = lower;
+ extent[i+dimension] = upper;
+ }
+ return new GridExtent(bounds, extent);
+ }
+
+ /**
+ * Creates a new grid coordinates computed from the given geospatial position. The given {@code crsToGrid}
+ * argument should be the inverse of {@link GridGeometry#getGridToCRS(PixelInCell)}. This method does not
+ * verify if CRS of the given point and does not verify if the resulting grid coordinates are inside the
+ * grid coverage bounds.
+ *
+ * @param point the geospatial position.
+ * @param crsToGrid conversion from the geospatial CRS to the grid coordinates.
+ * @return the given position converted to grid coordinates (possibly out of grid bounds).
+ * @throws TransformException if the given position can not be converted.
+ *
+ * @see GridCoverage#toGridCoordinates(DirectPosition)
+ */
+ static FractionalGridCoordinates fromPosition(final DirectPosition point, final MathTransform crsToGrid)
+ throws TransformException
+ {
+ final Position gc = new Position(crsToGrid.getTargetDimensions());
+ final DirectPosition result = crsToGrid.transform(point, gc);
+ if (result != gc) {
+ final double[] coordinates = result.getCoordinate();
+ System.arraycopy(coordinates, 0, gc.coordinates, 0, gc.coordinates.length);
+ }
+ return gc;
+ }
+
+ /**
+ * Returns the grid coordinates converted to a geospatial position using the given transform.
+ * The {@code gridToCRS} argument is typically {@link GridGeometry#getGridToCRS(PixelInCell)}
+ * with {@link PixelInCell#CELL_CENTER}.
+ *
+ * @param gridToCRS the transform to apply on grid coordinates.
+ * @return the grid coordinates converted using the given transform.
+ * @throws TransformException if the grid coordinates can not be converted by {@code gridToCRS}.
+ *
+ * @see GridCoverage#toGridCoordinates(DirectPosition)
+ */
+ public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
+ return gridToCRS.transform(new Position(this), null);
+ }
+
+ /**
+ * A grid coordinates viewed as a {@link DirectPosition}. This class is used only for coordinate transformation.
+ * We do not want to make this class public in order to avoid the abuse of {@link DirectPosition} as a storage
+ * of grid coordinates.
+ *
+ * <p>Note this this class does not comply with the contract documented in {@link DirectPosition#equals(Object)}
+ * and {@link DirectPosition#hashCode()} javadoc. This is another reason for not making this class public.</p>
+ */
+ private static final class Position extends FractionalGridCoordinates implements DirectPosition {
+ /**
+ * For cross-version compatibility.
+ */
+ private static final long serialVersionUID = -7804151694395153401L;
+
+ /**
+ * Creates a new position of the given number of dimensions.
+ */
+ Position(final int dimension) {
+ super(dimension);
+ }
+
+ /**
+ * Creates a new position initialized to a copy of the given coordinates.
+ */
+ Position(final FractionalGridCoordinates other) {
+ super(other);
+ }
+
+ /**
+ * Returns the direct position, which is this object itself.
+ */
+ @Override
+ public DirectPosition getDirectPosition() {
+ return this;
+ }
+
+ /**
+ * Grid coordinates have no coordinate reference system.
+ */
+ @Override
+ public CoordinateReferenceSystem getCoordinateReferenceSystem() {
+ return null;
+ }
+
+ /**
+ * Returns all coordinate values.
+ */
+ @Override
+ public double[] getCoordinate() {
+ return coordinates.clone();
+ }
+
+ /**
+ * Returns the coordinate value at the given dimension.
+ */
+ @Override
+ public double getOrdinate(int dimension) {
+ return coordinates[dimension];
+ }
+
+ /**
+ * Sets the coordinate value at the given dimension.
+ */
+ @Override
+ public void setOrdinate(final int dimension, final double value) {
+ coordinates[dimension] = value;
+ }
+
+ /**
+ * Returns the grid coordinates converted to a geospatial position using the given transform.
+ */
+ @Override
+ public DirectPosition toPosition(final MathTransform gridToCRS) throws TransformException {
+ return gridToCRS.transform(this, null);
+ }
+ }
+
+ /**
+ * Returns a string representation of this grid coordinates for debugging purpose.
+ */
+ @Override
+ public String toString() {
+ final StringBuilder buffer = new StringBuilder("GridCoordinates[");
+ for (int i=0; i<coordinates.length; i++) {
+ if (i != 0) buffer.append(' ');
+ StringBuilders.trimFractionalPart(buffer.append(coordinates[i]));
+ }
+ return buffer.append(']').toString();
+ }
+
+ /**
+ * Returns a hash code value for this grid coordinates.
+ */
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(coordinates) ^ (int) serialVersionUID;
+ }
+
+ /**
+ * 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 boolean equals(final Object object) {
+ if (object == this) { // Slight optimization.
+ return true;
+ }
+ if (object != null && object.getClass() == getClass()) {
+ return Arrays.equals(((FractionalGridCoordinates) object).coordinates, coordinates);
+ }
+ return false;
+ }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
index efbb0ab..cc648b3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoordinatesView.java
@@ -27,7 +27,7 @@ import org.apache.sis.util.ArgumentChecks;
* This is not a general-purpose grid coordinates since it assumes a {@link GridExtent} coordinates layout.
*
* @author Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 1.0
* @module
*/
@@ -89,7 +89,13 @@ final class GridCoordinatesView implements GridCoordinates {
*/
@Override
public final String toString() {
- return "GridCoordinates".concat(Arrays.toString(getCoordinateValues()));
+ final StringBuilder buffer = new StringBuilder("GridCoordinates[");
+ final int dimension = getDimension();
+ for (int i=0; i<dimension; i++) {
+ if (i != 0) buffer.append(' ');
+ buffer.append(coordinates[i + offset]);
+ }
+ return buffer.append(']').toString();
}
/**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
index 6da4620..1f182d3 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridCoverage.java
@@ -19,6 +19,7 @@ package org.apache.sis.coverage.grid;
import java.util.List;
import java.util.Collection;
import java.util.Locale;
+import java.util.Objects;
import java.awt.image.RenderedImage;
import org.opengis.coverage.PointOutsideCoverageException;
import org.opengis.geometry.DirectPosition;
@@ -26,13 +27,10 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
-import org.opengis.util.FactoryException;
import org.apache.sis.internal.util.UnmodifiableArrayList;
import org.apache.sis.coverage.SampleDimension;
import org.apache.sis.coverage.SubspaceNotSpecifiedException;
import org.apache.sis.image.PixelIterator;
-import org.apache.sis.referencing.CRS;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.util.collection.DefaultTreeTable;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.TreeTable;
@@ -53,7 +51,7 @@ import org.opengis.coverage.CannotEvaluateException;
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
- * @version 2.0
+ * @version 1.1
* @since 1.0
* @module
*/
@@ -75,18 +73,27 @@ public abstract class GridCoverage {
private final SampleDimension[] sampleDimensions;
/**
+ * The last coordinate operation used by {@link #toGridCoordinates(DirectPosition)}.
+ * This is cached for avoiding the costly process of fetching a coordinate operation
+ * in the common case where the coordinate reference systems did not changed.
+ */
+ private transient volatile PointToGridCoordinates lastConverter;
+
+ /**
* Constructs a grid coverage using the specified grid geometry and sample dimensions.
+ * The grid geometry defines the "domain" (inputs) of the coverage function,
+ * and the sample dimensions define the "range" (output) of that function.
*
- * @param grid the grid extent, CRS and conversion from cell indices to CRS.
- * @param bands sample dimensions for each image band.
+ * @param domain the grid extent, CRS and conversion from cell indices to CRS.
+ * @param range sample dimensions for each image band.
*/
- protected GridCoverage(final GridGeometry grid, final Collection<? extends SampleDimension> bands) {
- ArgumentChecks.ensureNonNull("grid", grid);
- ArgumentChecks.ensureNonNull("bands", bands);
- gridGeometry = grid;
- sampleDimensions = bands.toArray(new SampleDimension[bands.size()]);
+ protected GridCoverage(final GridGeometry domain, final Collection<? extends SampleDimension> range) {
+ ArgumentChecks.ensureNonNull("domain", domain);
+ ArgumentChecks.ensureNonNull("range", range);
+ gridGeometry = domain;
+ sampleDimensions = range.toArray(new SampleDimension[range.size()]);
for (int i=0; i<sampleDimensions.length; i++) {
- ArgumentChecks.ensureNonNullElement("bands", i, sampleDimensions[i]);
+ ArgumentChecks.ensureNonNullElement("range", i, sampleDimensions[i]);
}
}
@@ -159,6 +166,85 @@ public abstract class GridCoverage {
public abstract GridCoverage forConvertedValues(boolean converted);
/**
+ * Returns a sequence of double values for a given point in the coverage.
+ * The CRS of the given point may be any coordinate reference system;
+ * coordinate transformations will be applied as needed.
+ * If the CRS of the point is undefined, then it is assumed to be the same as this coverage.
+ * The returned sequence includes a value for each {@linkplain SampleDimension sample dimension}.
+ *
+ * <p>The default interpolation type used when accessing grid values for points which fall between
+ * grid cells is nearest neighbor. This default interpolation method may change in future version.</p>
+ *
+ * <p>The default implementation invokes {@link #render(GridExtent)} for a small region around the point.
+ * Subclasses should override with more efficient implementation.</p>
+ *
+ * <p>Warning: this method may change. See <a href="https://issues.apache.org/jira/browse/SIS-485">SIS-485</a>.</p>
+ *
+ * @param point the coordinate point where to evaluate.
+ * @param buffer an array in which to store values, or {@code null} to create a new array.
+ * @return the {@code buffer} array, or a newly created array if {@code buffer} was null.
+ * @throws PointOutsideCoverageException if the evaluation failed because the input point
+ * has invalid coordinates.
+ * @throws CannotEvaluateException if the values can not be computed at the specified coordinate
+ * for an other reason. It may be thrown if the coverage data type can not be converted
+ * to {@code double} by an identity or widening conversion. Subclasses may relax this
+ * constraint if appropriate.
+ *
+ * @since 1.1
+ */
+ public double[] evaluate(final DirectPosition point, double[] buffer) throws CannotEvaluateException {
+ /*
+ * TODO: instead of restricting to a single point, keep the automatic size (1 or 2),
+ * invoke render for each plan, then interpolate. We would keep a value of 1 in the
+ * size array if we want to disable interpolation in some particular axis (e.g. time).
+ */
+ final long[] size = new long[gridGeometry.getDimension()];
+ java.util.Arrays.fill(size, 1);
+ try {
+ final GridExtent subExtent = toGridCoordinates(point).toExtent(gridGeometry.extent, size);
+ final RenderedImage image = render(subExtent);
+ final PixelIterator ite = PixelIterator.create(image); // TODO: avoid costly creation of PixelIterator here.
+ ite.moveTo(0, 0);
+ return ite.getPixel(buffer);
+ } catch (ArithmeticException | DisjointExtentException ex) {
+ throw (PointOutsideCoverageException) new PointOutsideCoverageException(ex.getMessage(), point).initCause(ex);
+ } catch (IllegalArgumentException | TransformException ex) {
+ throw new CannotEvaluateException(ex.getMessage(), ex);
+ }
+ }
+
+ /**
+ * Converts the specified geospatial position to grid coordinates. If the given position
+ * is associated to a non-null coordinate reference system (CRS) different than the CRS
+ * of this coverage, then this method automatically transforms that position to the
+ * {@linkplain #getCoordinateReferenceSystem() coverage CRS} before to compute grid coordinates.
+ *
+ * <p>This method does not put any restriction on the grid coordinates result.
+ * The result may be outside the {@linkplain GridGeometry#getExtent() grid extent}
+ * if the {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform allows it.</p>
+ *
+ * @param point geospatial coordinates (in arbitrary CRS) to transform to grid coordinates.
+ * @return the grid coordinates for the given geospatial coordinates.
+ * @throws IncompleteGridGeometryException if the {@linkplain #getGridGeometry() grid geometry}
+ * does not define a "grid to CRS" transform, or if the given point has a non-null CRS
+ * but this coverage does not {@linkplain #getCoordinateReferenceSystem() have a CRS}.
+ * @throws TransformException if the given coordinates can not be transformed.
+ *
+ * @see FractionalGridCoordinates#toPosition(MathTransform)
+ *
+ * @since 1.1
+ */
+ public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException {
+ final CoordinateReferenceSystem sourceCRS = point.getCoordinateReferenceSystem();
+ PointToGridCoordinates converter = lastConverter;
+ if (converter == null || !Objects.equals(converter.sourceCRS, sourceCRS)) {
+ converter = new PointToGridCoordinates(sourceCRS, this, getGridGeometry());
+ lastConverter = converter;
+ }
+ return FractionalGridCoordinates.fromPosition(point, converter.crsToGrid);
+ }
+
+ /**
* Returns a two-dimensional slice of grid data as a rendered image. The given {@code sliceExtent} argument specifies
* the coordinates of the slice in all dimensions that are not in the two-dimensional image. For example if this grid
* coverage has <i>(<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>)</i> dimensions and we want to render an image
@@ -234,38 +320,6 @@ public abstract class GridCoverage {
public abstract RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException;
/**
- * Returns a sequence of double values for a given point in the coverage. A value for each
- * {@linkplain SampleDimension sample dimension} is included in the sequence. The default
- * interpolation type used when accessing grid values for points which fall between grid cells
- * is nearest neighbor.
- * The CRS of the point may be in any coordinate reference system.
- * If the CRS of the point is undefined, it is assumed to be the same as the coverage.
- *
- * @param coord The coordinate point where to evaluate.
- * @param dest An array in which to store values, or {@code null} to create a new array.
- * @return The {@code dest} array, or a newly created array if {@code dest} was null.
- * @throws PointOutsideCoverageException if the evaluation failed because the input point
- * has invalid coordinates.
- * @throws CannotEvaluateException if the values can't be computed at the specified coordinate
- * for an other reason. It may be thrown if the coverage data type can't be converted
- * to {@code double} by an identity or widening conversion. Subclasses may relax this
- * constraint if appropriate.
- */
- public double[] evaluate(DirectPosition coord, double[] dest) throws CannotEvaluateException {
- try {
- coord = toGridCoord(coord);
- final long[] coordl = toLongExact(coord);
- final GridExtent subExtent = new GridExtent(null, coordl, coordl, true);
- final RenderedImage image = render(subExtent);
- final PixelIterator ite = PixelIterator.create(image);
- ite.moveTo(0, 0);
- return ite.getPixel(dest);
- } catch (FactoryException | TransformException ex) {
- throw new CannotEvaluateException(ex.getMessage(), ex);
- }
- }
-
- /**
* Returns a string representation of this grid coverage for debugging purpose.
* The returned string is implementation dependent and may change in any future version.
* Current implementation is equivalent to the following, where {@code <default flags>}
@@ -309,53 +363,4 @@ public abstract class GridCoverage {
branch.newChild().setValue(column, SampleDimension.toString(locale, sampleDimensions));
return tree;
}
-
- /**
- * Converts the specified point to grid coordinate.
- *
- * @param point point to transform to grid coordinate
- * @return point in grid coordinate
- * @throws org.opengis.util.FactoryException if creating transformation fails
- * @throws org.opengis.referencing.operation.TransformException if transformation fails
- */
- protected DirectPosition toGridCoord(final DirectPosition point)
- throws FactoryException, TransformException
- {
- final CoordinateReferenceSystem sourceCRS = point.getCoordinateReferenceSystem();
- MathTransform trs = getGridGeometry().getGridToCRS(PixelInCell.CELL_CENTER).inverse();
- if (sourceCRS != null) {
- MathTransform toCrs = CRS.findOperation(sourceCRS, getCoordinateReferenceSystem(), null).getMathTransform();
- if (!toCrs.isIdentity()) {
- trs = MathTransforms.concatenate(toCrs, trs);
- }
- }
- return trs.transform(point, null);
- }
-
- /**
- * Converts given grid coordinate to long values and ensure coordinate
- * is inside grid geometry extent.
- *
- * @param position in grid coordinate
- * @return position as long type in grid coordinate
- * @throws PointOutsideCoverageException
- */
- protected long[] toLongExact(DirectPosition position) throws PointOutsideCoverageException {
- final long[] coord = new long[position.getDimension()];
- final GridExtent extent = getGridGeometry().getExtent();
- final long[] low = extent.getLow().getCoordinateValues();
- final long[] high = extent.getHigh().getCoordinateValues();
-
- for (int i = 0; i < coord.length; i++) {
- final double dv = position.getOrdinate(i);
- if (!Double.isFinite(dv)) {
- throw new PointOutsideCoverageException("Position outside coverage, axis " + i + " value " + dv);
- }
- coord[i] = Math.round(dv);
- if (coord[i] < low[i] || coord[i] > high[i]) {
- throw new PointOutsideCoverageException("Position outside coverage, axis " + i + " value " + coord[i]);
- }
- }
- return coord;
- }
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index b2fa4ec..51529ef 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -78,7 +78,7 @@ import org.opengis.coverage.PointOutsideCoverageException;
* The same instance can be shared by different {@link GridGeometry} instances.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 1.0
* @module
*/
@@ -140,7 +140,7 @@ public class GridExtent implements GridEnvelope, Serializable {
*
* @throws IllegalArgumentException if the given number of dimensions is excessive.
*/
- private static long[] allocate(final int dimension) throws IllegalArgumentException {
+ static long[] allocate(final int dimension) throws IllegalArgumentException {
if (dimension >= Numerics.MAXIMUM_MATRIX_SIZE) {
// Actually the real limit is Integer.MAX_VALUE / 2, but a value too high is likely to be an error.
throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, dimension));
@@ -323,6 +323,33 @@ public class GridExtent implements GridEnvelope, Serializable {
{
final int dimension = envelope.getDimension();
coordinates = (enclosing != null) ? enclosing.coordinates.clone() : allocate(dimension);
+ /*
+ * Assign the `types` field before we try to compute the grid extent coordinates
+ * because if the coordinate computation fail, `getAxisIdentification(…)` uses
+ * that information for producing a more informative error message if possible.
+ */
+ if (enclosing != null && enclosing.types != null) {
+ types = enclosing.types;
+ } else {
+ DimensionNameType[] axisTypes = null;
+ final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
+ if (crs != null) {
+ final CoordinateSystem cs = crs.getCoordinateSystem();
+ for (int i=0; i<dimension; i++) {
+ final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection()));
+ if (type != null) {
+ if (axisTypes == null) {
+ axisTypes = new DimensionNameType[dimension];
+ }
+ axisTypes[i] = type;
+ }
+ }
+ }
+ types = validateAxisTypes(axisTypes);
+ }
+ /*
+ * Now computes the grid extent coordinates.
+ */
for (int i=0; i<dimension; i++) {
double min = envelope.getLower(i);
double max = envelope.getUpper(i);
@@ -418,37 +445,27 @@ public class GridExtent implements GridEnvelope, Serializable {
if (lower > validMin) coordinates[lo] = lower;
if (upper < validMax) coordinates[hi] = upper;
if (lower > validMax || upper < validMin) {
- throw new DisjointExtentException(enclosing.getAxisIdentification(lo, i), validMin, validMax, lower, upper);
+ throw new DisjointExtentException(getAxisIdentification(lo, i), validMin, validMax, lower, upper);
}
} else {
coordinates[i] = lower;
coordinates[i+m] = upper;
}
}
- /*
- * At this point we finished to compute coordinate values.
- * Now try to infer dimension types from the CRS axes.
- * This is only for information purpose.
- */
- if (enclosing != null) {
- types = enclosing.types;
- } else {
- DimensionNameType[] axisTypes = null;
- final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
- if (crs != null) {
- final CoordinateSystem cs = crs.getCoordinateSystem();
- for (int i=0; i<dimension; i++) {
- final DimensionNameType type = AXIS_DIRECTIONS.get(AxisDirections.absolute(cs.getAxis(i).getDirection()));
- if (type != null) {
- if (axisTypes == null) {
- axisTypes = new DimensionNameType[dimension];
- }
- axisTypes[i] = type;
- }
- }
- }
- types = validateAxisTypes(axisTypes);
- }
+ }
+
+ /**
+ * Creates a new grid extent with the same axes than the given extent, but different coordinates.
+ * This constructor does not invoke {@link #validateCoordinates()}; we presume that the caller's
+ * computation is correct.
+ *
+ * @param enclosing the extent from which to copy axes, or {@code null} if none.
+ * @param coordinates the coordinates. This array is not cloned.
+ */
+ GridExtent(final GridExtent enclosing, final long[] coordinates) {
+ this.coordinates = coordinates;
+ types = (enclosing != null) ? enclosing.types : null;
+ assert (types == null) || types.length == getDimension();
}
/**
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
index 9caedb6..976e9ff 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/GridGeometry.java
@@ -106,7 +106,7 @@ import org.apache.sis.xml.NilReason;
* The same instance can be shared by different {@link GridCoverage} instances.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 1.0
+ * @version 1.1
* @since 1.0
* @module
*/
@@ -843,7 +843,7 @@ public class GridGeometry implements Serializable {
* Returns the {@link #geographicBBox} value or {@code null} if none.
* This method computes the box when first needed.
*/
- private GeographicBoundingBox geographicBBox() {
+ final GeographicBoundingBox geographicBBox() {
GeographicBoundingBox bbox = geographicBBox;
if (bbox == null) {
if (getCoordinateReferenceSystem(envelope) != null && !envelope.isAllNaN()) {
diff --git a/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java
new file mode 100644
index 0000000..85dc4bc
--- /dev/null
+++ b/core/sis-feature/src/main/java/org/apache/sis/coverage/grid/PointToGridCoordinates.java
@@ -0,0 +1,81 @@
+/*
+ * 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.opengis.util.FactoryException;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.referencing.CRS;
+
+
+/**
+ * Holds the object necessary for converting a geospatial coordinates to grid coordinates.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+final class PointToGridCoordinates {
+ /**
+ * The source coordinate reference system of the converter,
+ * or {@code null} if assumed the same than the coverage CRS.
+ */
+ final CoordinateReferenceSystem sourceCRS;
+
+ /**
+ * The transform from {@link #sourceCRS} to grid coordinates.
+ */
+ final MathTransform crsToGrid;
+
+ /**
+ * Creates a new objects holding the objects for converting from the given source CRS.
+ *
+ * <div class="note"><b>Note about coverage argument:</b>
+ * the {@code coverage} argument is for fetching the coverage CRS when needed.
+ * We could get that CRS from {@link GridCoverage#getCoordinateReferenceSystem()},
+ * but we use {@link GridCoverage#getCoordinateReferenceSystem()} instead for giving users a
+ * chance to override. We do not give the coverage CRS in argument because we want to invoke
+ * {@link GridCoverage#getCoordinateReferenceSystem()} only if {@code sourceCRS} is non-null,
+ * because {@code getCoordinateReferenceSystem()} may throw {@link IncompleteGridGeometryException}.
+ * </div>
+ *
+ * @param sourceCRS the source CRS, or {@code null} if assumed the same than the coverage CRS.
+ * @param coverage the coverage for which we are building those information.
+ * @param gridGeometry the coverage grid geometry.
+ * @throws TransformException if the {@link #crsToGrid} transform can not be built.
+ */
+ PointToGridCoordinates(final CoordinateReferenceSystem sourceCRS, final GridCoverage coverage,
+ final GridGeometry gridGeometry) throws TransformException
+ {
+ this.sourceCRS = sourceCRS;
+ MathTransform tr = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER).inverse();
+ if (sourceCRS != null) try {
+ CoordinateOperation op = CRS.findOperation(sourceCRS,
+ coverage.getCoordinateReferenceSystem(), // See comment in above javadoc.
+ gridGeometry.geographicBBox());
+ tr = MathTransforms.concatenate(op.getMathTransform(), tr);
+ } catch (FactoryException e) {
+ throw new TransformException(e.getMessage(), e);
+ }
+ crsToGrid = tr;
+ }
+}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java
index 57e9a6f..f0bcf3b 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/coverage/GridCoverage2D.java
@@ -16,91 +16,145 @@
*/
package org.apache.sis.internal.coverage;
+import java.util.Collection;
import java.awt.image.BufferedImage;
import java.awt.image.RenderedImage;
-import java.util.Collection;
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.datum.PixelInCell;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.coverage.CannotEvaluateException;
+import org.opengis.coverage.PointOutsideCoverageException;
import org.apache.sis.coverage.SampleDimension;
+import org.apache.sis.coverage.grid.FractionalGridCoordinates;
import org.apache.sis.coverage.grid.GridCoverage;
import org.apache.sis.coverage.grid.GridExtent;
import org.apache.sis.coverage.grid.GridGeometry;
+import org.apache.sis.coverage.grid.DisjointExtentException;
+import org.apache.sis.coverage.grid.IllegalGridGeometryException;
+import org.apache.sis.coverage.grid.IncompleteGridGeometryException;
import org.apache.sis.internal.image.TranslatedRenderedImage;
import org.apache.sis.internal.referencing.AxisDirections;
+import org.apache.sis.internal.feature.Resources;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.operation.transform.TransformSeparator;
import org.apache.sis.util.ArgumentChecks;
-import org.opengis.coverage.CannotEvaluateException;
-import org.opengis.geometry.DirectPosition;
-import org.opengis.referencing.crs.CoordinateReferenceSystem;
-import org.opengis.referencing.datum.PixelInCell;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.util.FactoryException;
+
/**
- * A {@link GridCoverage} with data stored in a {@link RenderedImage}.
+ * Basic access to grid data values backed by a two-dimensional {@link RenderedImage}.
+ * Each band in an image is represented as a {@link SampleDimension}.
+ * The rendered image can be a two-dimensional slice in a <var>n</var>-dimensional space
+ * (i.e. the {@linkplain GridGeometry#getEnvelope() grid geometry envelope} may have more
+ * than two dimensions) provided that the {@linkplain GridExtent grid extent} have a
+ * {@linkplain GridExtent#getSize size} equals to 1 in all dimensions except 2.
*
- * @author Martin Desruisseaux (Geomatys)
- * @author Johann Sorel (Geomatys)
- * @version 2.0
- * @since 2.0
+ * <div class="note"><b>Example:</b>
+ * a remote sensing image may be valid only over some time range
+ * (the time of satellite pass over the observed area).
+ * Envelopes for such grid coverage can have three dimensions:
+ * the two usual ones (horizontal extent along <var>x</var> and <var>y</var>),
+ * and a third one for start time and end time (time extent along <var>t</var>).
+ * The "two-dimensional" grid coverage can have any number of columns along <var>x</var> axis
+ * and any number of rows along <var>y</var> axis, but only one plan along <var>t</var> axis.
+ * This single plan can have a lower bound (the start time) and an upper bound (the end time).
+ * </div>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @author Johann Sorel (Geomatys)
+ * @version 1.1
+ * @since 1.1
* @module
*/
public final class GridCoverage2D extends GridCoverage {
/**
- * The sample values, stored as a RenderedImage.
+ * The sample values stored as a {@code RenderedImage}.
*/
- private final RenderedImage image;
- private final int[] imageAxes;
- private final CoordinateReferenceSystem crs2d;
+ private final RenderedImage data;
+
+ /**
+ * Index of extent dimensions corresponding to image <var>x</var> and <var>y</var> coordinates.
+ * Typical values are 0 for {@code xDimension} and 1 for {@code yDimension}, but different values
+ * are allowed.
+ */
+ private final int xDimension, yDimension;
+
+ /**
+ * The two-dimensional component of the coordinate reference system, or {@code null} if unspecified.
+ */
+ private final CoordinateReferenceSystem crs2D;
/**
* Result of the call to {@link #forConvertedValues(boolean)}, created when first needed.
*/
- private GridCoverage converted;
+ private transient GridCoverage converted;
/**
+ * Constructs a grid coverage using the specified domain, range and data.
* The given RenderedImage may not start at 0,0, so does the gridExtent of the grid geometry.
* Image 0/0 coordinate is expected to match grid extent lower corner.
*
- * @param grid the grid extent, CRS and conversion from cell indices to CRS.
- * @param bands sample dimensions for each image band.
- * @param image the sample values as a RenderedImage, potentially multi-banded in packed view.
+ * @param domain the grid extent, CRS and conversion from cell indices to CRS.
+ * @param range sample dimensions for each image band.
+ * @param data the sample values as a RenderedImage, potentially multi-banded in packed view.
*/
- public GridCoverage2D(final GridGeometry grid, final Collection<? extends SampleDimension> bands, final RenderedImage image) throws FactoryException {
- super(grid, bands);
- this.image = image;
- ArgumentChecks.ensureNonNull("image", image);
-
- //extract the 2D Coordinater
- GridExtent extent = grid.getExtent();
- imageAxes = extent.getSubspaceDimensions(2);
- crs2d = CRS.reduce(grid.getCoordinateReferenceSystem(), imageAxes);
-
- //check image is coherent with grid geometry
- if (image.getWidth() != extent.getSize(imageAxes[0])) {
- throw new IllegalArgumentException("Image width " + image.getWidth() + " does not match grid extent width "+ extent.getSize(imageAxes[0]));
+ public GridCoverage2D(final GridGeometry domain, final Collection<? extends SampleDimension> range, final RenderedImage data) {
+ super(domain, range);
+ this.data = data;
+ ArgumentChecks.ensureNonNull("image", data);
+ /*
+ * Extract the 2D components of the coordinate reference system.
+ */
+ final GridExtent extent = domain.getExtent();
+ final int[] imageAxes = extent.getSubspaceDimensions(2);
+ xDimension = imageAxes[0];
+ yDimension = imageAxes[1];
+ if (domain.isDefined(GridGeometry.CRS)) {
+ final CoordinateReferenceSystem crs = domain.getCoordinateReferenceSystem();
+ try {
+ crs2D = CRS.reduce(crs, imageAxes);
+ } catch (IllegalArgumentException | FactoryException e) {
+ throw new IllegalGridGeometryException("Can not create a two-dimensional CRS from " + crs.getName(), e);
+ }
+ } else {
+ crs2D = null;
}
- if (image.getHeight() != extent.getSize(imageAxes[1])) {
- throw new IllegalArgumentException("Image height " + image.getHeight()+ " does not match grid extent height "+ extent.getSize(imageAxes[1]));
+ /*
+ * Check that image is coherent with grid geometry.
+ */
+ int actual;
+ long expected;
+ if ((actual = data.getWidth()) != (expected = extent.getSize(xDimension))) {
+ throw new IllegalArgumentException("Image width " + actual + " does not match grid extent width " + expected);
}
- if (image.getSampleModel().getNumBands() != bands.size()) {
- throw new IllegalArgumentException("Image sample model number of bands " + image.getSampleModel().getNumBands()+ " does not match number of sample dimensions "+ bands.size());
+ if ((actual = data.getHeight()) != (expected = extent.getSize(yDimension))) {
+ throw new IllegalArgumentException("Image height " + actual + " does not match grid extent height " + expected);
+ }
+ int n;
+ if ((actual = data.getSampleModel().getNumBands()) != (n = range.size())) {
+ throw new IllegalArgumentException("Image sample model number of bands " + actual + " does not match number of sample dimensions " + n);
}
}
/**
- * Returns the two-dimensional part of this grid coverage CRS. If the
- * {@linkplain #getCoordinateReferenceSystem complete CRS} is two-dimensional, then this
- * method returns the same CRS. Otherwise it returns a CRS for the two first axis having
- * a {@linkplain GridExtent#getSize span} greater than 1 in the grid envelope. Note that
- * those axis are guaranteed to appears in the same order than in the complete CRS.
+ * Returns the two-dimensional part of this grid coverage CRS.
+ * If the {@linkplain #getCoordinateReferenceSystem complete CRS} is two-dimensional,
+ * then this method returns the same CRS. Otherwise it returns a CRS for the two first axis
+ * having a {@linkplain GridExtent#getSize(int) size} greater than 1 in the grid envelope.
+ * Note that those axis are guaranteed to appear in the same order than in the complete CRS.
*
- * @return The two-dimensional part of the grid coverage CRS.
+ * @return the two-dimensional part of the grid coverage CRS.
+ * @throws IncompleteGridGeometryException if the grid geometry does not contain a CRS.
*
- * @see #getCoordinateReferenceSystem
+ * @see #getCoordinateReferenceSystem()
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem2D() {
- return crs2d;
+ if (crs2D != null) {
+ return crs2D;
+ }
+ throw new IncompleteGridGeometryException(Resources.format(Resources.Keys.UnspecifiedCRS));
}
/**
@@ -111,7 +165,7 @@ public final class GridCoverage2D extends GridCoverage {
*/
public MathTransform getGridToCrs2D() throws FactoryException {
TransformSeparator sep = new TransformSeparator(getGridGeometry().getGridToCRS(PixelInCell.CELL_CENTER));
- int idx = AxisDirections.indexOfColinear(getCoordinateReferenceSystem().getCoordinateSystem(), crs2d.getCoordinateSystem());
+ int idx = AxisDirections.indexOfColinear(getCoordinateReferenceSystem().getCoordinateSystem(), crs2D.getCoordinateSystem());
sep.addSourceDimensionRange(idx, idx+2);
return sep.separate();
}
@@ -147,66 +201,58 @@ public final class GridCoverage2D extends GridCoverage {
* @return the grid slice as a rendered image.
*/
@Override
- public RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException {
+ public RenderedImage render(final GridExtent sliceExtent) throws CannotEvaluateException {
if (sliceExtent == null || sliceExtent.equals(getGridGeometry().getExtent())) {
- return image;
+ return data;
} else {
- final int subX = Math.toIntExact(sliceExtent.getLow(imageAxes[0]));
- final int subY = Math.toIntExact(sliceExtent.getLow(imageAxes[1]));
- final int subWidth = Math.toIntExact(Math.round(sliceExtent.getSize(imageAxes[0])));
- final int subHeight = Math.toIntExact(Math.round(sliceExtent.getSize(imageAxes[1])));
+ final int subX = Math.toIntExact(sliceExtent.getLow(xDimension));
+ final int subY = Math.toIntExact(sliceExtent.getLow(yDimension));
+ final int subWidth = Math.toIntExact(Math.round(sliceExtent.getSize(xDimension)));
+ final int subHeight = Math.toIntExact(Math.round(sliceExtent.getSize(yDimension)));
- if (image instanceof BufferedImage) {
- final BufferedImage bi = (BufferedImage) image;
+ if (data instanceof BufferedImage) {
+ final BufferedImage bi = (BufferedImage) data;
return bi.getSubimage(subX, subY, subWidth, subHeight);
} else {
- return new TranslatedRenderedImage(image, subX, subY);
+ return new TranslatedRenderedImage(data, subX, subY);
}
}
}
/**
- * {@inheritDoc }
+ * Returns a sequence of double values for a given point in the coverage.
+ * The CRS of the given point may be any coordinate reference system,
+ * or {@code null} for the same CRS than this coverage.
+ * The returned sequence contains a value for each {@linkplain SampleDimension sample dimension}.
+ *
+ * @param point the coordinate point where to evaluate.
+ * @param buffer an array in which to store values, or {@code null} to create a new array.
+ * @return the {@code buffer} array, or a newly created array if {@code buffer} was null.
+ * @throws PointOutsideCoverageException if the evaluation failed because the input point
+ * has invalid coordinates.
+ * @throws CannotEvaluateException if the values can not be computed at the specified coordinate
+ * for an other reason.
*/
@Override
- public double[] evaluate(DirectPosition position, double[] buffer) throws CannotEvaluateException {
+ public double[] evaluate(final DirectPosition point, double[] buffer) throws CannotEvaluateException {
try {
- position = toGridCoord(position);
- long[] coord = toLongExact(position);
- int x = Math.toIntExact(Math.round(coord[imageAxes[0]]));
- int y = Math.toIntExact(Math.round(coord[imageAxes[1]]));
- return image.getTile(XToTileX(x), YToTileY(y)).getPixel(x, y, buffer);
- } catch (FactoryException | TransformException ex) {
+ final FractionalGridCoordinates gc = toGridCoordinates(point);
+ final int x = Math.toIntExact(gc.getCoordinateValue(xDimension));
+ final int y = Math.toIntExact(gc.getCoordinateValue(yDimension));
+ final int xmin = data.getMinX();
+ final int ymin = data.getMinY();
+ if (x >= xmin && x < xmin + (long) data.getWidth() &&
+ y >= ymin && y < ymin + (long) data.getHeight())
+ {
+ final int tx = Math.floorDiv(x - data.getTileGridXOffset(), data.getTileWidth());
+ final int ty = Math.floorDiv(y - data.getTileGridYOffset(), data.getTileHeight());
+ return data.getTile(tx, ty).getPixel(x, y, buffer);
+ }
+ } catch (ArithmeticException | DisjointExtentException ex) {
+ throw (PointOutsideCoverageException) new PointOutsideCoverageException(ex.getMessage(), point).initCause(ex);
+ } catch (IllegalArgumentException | TransformException ex) {
throw new CannotEvaluateException(ex.getMessage(), ex);
}
+ throw new PointOutsideCoverageException(null, point);
}
-
- /**
- * Converts a pixel's X coordinate into a horizontal tile index.
- * @param x pixel x coordinate
- * @return tile x coordinate
- */
- private int XToTileX(int x) {
- int tileWidth = image.getTileWidth();
- x -= image.getTileGridXOffset();
- if (x < 0) {
- x += 1 - tileWidth;
- }
- return x/tileWidth;
- }
-
- /**
- * Converts a pixel's Y coordinate into a vertical tile index.
- * @param y pixel x coordinate
- * @return tile y coordinate
- */
- private int YToTileY(int y) {
- int tileHeight = image.getTileHeight();
- y -= image.getTileGridYOffset();
- if (y < 0) {
- y += 1 - tileHeight;
- }
- return y/tileHeight;
- }
-
}
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
index 76ef8c3..3e23ce6 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.java
@@ -299,6 +299,11 @@ public final class Resources extends IndexedResourceBundle {
public static final short UnavailableGeometryLibrary_1 = 21;
/**
+ * Can not convert grid coordinate {1} to type ‘{0}’.
+ */
+ public static final short UnconvertibleGridCoordinate_2 = 59;
+
+ /**
* Expected {0} bands but got {1}.
*/
public static final short UnexpectedNumberOfBands_2 = 49;
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
index 8ae3d8a..f1153af 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources.properties
@@ -65,6 +65,7 @@ PropertyAlreadyExists_2 = Property \u201c{1}\u201d already exists in f
PropertyNotFound_2 = No property named \u201c{1}\u201d has been found in \u201c{0}\u201d feature.
TooManyQualitatives = Too many qualitative categories.
UnavailableGeometryLibrary_1 = The {0} geometry library is not available in current runtime environment.
+UnconvertibleGridCoordinate_2 = Can not convert grid coordinate {1} to type \u2018{0}\u2019.
UnexpectedNumberOfBands_2 = Expected {0} bands but got {1}.
UnexpectedNumberOfComponents_4 = The \u201c{1}\u201d value given to \u201c{0}\u201d property should be separable in {2} components, but we got {3}.
UnexpectedNumberOfCoordinates_4 = The \u201c{0}\u201d feature at {1} has a {3} coordinate values, while we expected a multiple of {2}.
diff --git a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
index 3828cfa..a0f13a6 100644
--- a/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
+++ b/core/sis-feature/src/main/java/org/apache/sis/internal/feature/Resources_fr.properties
@@ -71,6 +71,7 @@ PropertyAlreadyExists_2 = La propri\u00e9t\u00e9 \u00ab\u202f{1}\u202f
PropertyNotFound_2 = Aucune propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb n\u2019a \u00e9t\u00e9 trouv\u00e9e dans l\u2019entit\u00e9 \u00ab\u202f{0}\u202f\u00bb.
TooManyQualitatives = Trop de cat\u00e9gories qualitatives.
UnavailableGeometryLibrary_1 = La biblioth\u00e8que de g\u00e9om\u00e9tries {0} n\u2019est pas disponible dans l\u2019environnement d\u2019ex\u00e9cution actuel.
+UnconvertibleGridCoordinate_2 = Ne peut pas convertir la coordonn\u00e9e de grille {1} vers le type \u2018{0}\u2019.
UnexpectedNumberOfBands_2 = On attendait {0} bandes mais {1} ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es.
UnexpectedNumberOfComponents_4 = La valeur \u00ab\u202f{1}\u202f\u00bb donn\u00e9e \u00e0 la propri\u00e9t\u00e9 \u00ab\u202f{0}\u202f\u00bb devrait \u00eatre s\u00e9parable en {2} composantes, mais on en a obtenus {3}.
UnexpectedNumberOfCoordinates_4 = L\u2019entit\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb \u00e0 {1} contient {3} coordonn\u00e9es, alors qu\u2019on attendait un multiple de {2}.
diff --git a/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
new file mode 100644
index 0000000..bb9e0b5
--- /dev/null
+++ b/core/sis-feature/src/test/java/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests the {@link FractionalGridCoordinates} implementation.
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ * @since 1.1
+ * @module
+ */
+public final strictfp class FractionalGridCoordinatesTest extends TestCase {
+ /**
+ * Creates a test instance with (4 -1.1 7.6) coordinate values.
+ */
+ private static FractionalGridCoordinates instance() {
+ final FractionalGridCoordinates gc = new FractionalGridCoordinates(3);
+ gc.coordinates[0] = 4;
+ gc.coordinates[1] = -1.1;
+ gc.coordinates[2] = 7.6;
+ return gc;
+ }
+
+ /**
+ * Tests {@link FractionalGridCoordinates#getCoordinateValue(int)}.
+ */
+ @Test
+ public void testGetCoordinateValue() {
+ final FractionalGridCoordinates gc = instance();
+ assertEquals( 4, gc.getCoordinateValue(0));
+ assertEquals(-1, gc.getCoordinateValue(1));
+ assertEquals( 8, gc.getCoordinateValue(2));
+ }
+
+ /**
+ * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)}
+ * with default parameter values.
+ */
+ @Test
+ public void testToExtent() {
+ final GridExtent extent = instance().toExtent(null);
+ GridExtentTest.assertExtentEquals(extent, 0, 4, 4);
+ GridExtentTest.assertExtentEquals(extent, 1, -2, -1);
+ GridExtentTest.assertExtentEquals(extent, 2, 7, 8);
+ }
+
+ /**
+ * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a size of 1.
+ */
+ @Test
+ public void testToExtentSize1() {
+ final GridExtent extent = instance().toExtent(null, 1, 1, 1);
+ GridExtentTest.assertExtentEquals(extent, 0, 4, 4);
+ GridExtentTest.assertExtentEquals(extent, 1, -1, -1);
+ GridExtentTest.assertExtentEquals(extent, 2, 8, 8);
+ }
+
+ /**
+ * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a size greater than 2.
+ */
+ @Test
+ public void testToExtentSizeN() {
+ final GridExtent extent = instance().toExtent(null, 3, 5, 4);
+ GridExtentTest.assertExtentEquals(extent, 0, 3, 5);
+ GridExtentTest.assertExtentEquals(extent, 1, -3, 1);
+ GridExtentTest.assertExtentEquals(extent, 2, 6, 9);
+ }
+
+ /**
+ * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a bounds constraint.
+ */
+ @Test
+ public void testToExtentBounded() {
+ final GridExtent bounds = new GridExtent(null, null, new long[] {4, 2, 7}, true);
+ final GridExtent extent = instance().toExtent(bounds, 3, 5, 4);
+ GridExtentTest.assertExtentEquals(extent, 0, 2, 4);
+ GridExtentTest.assertExtentEquals(extent, 1, 0, 2);
+ GridExtentTest.assertExtentEquals(extent, 2, 4, 7);
+ }
+}
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java
index d151ed0..0904f66 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/BufferedGridCoverageTest.java
@@ -102,8 +102,7 @@ public class BufferedGridCoverageTest extends TestCase {
{ -60, -195},
{-216, -380}
});
-
- /**
+ /*
* Test evaluation
*/
Assert.assertArrayEquals(new double[]{ 70.0}, coverage.evaluate(new DirectPosition2D(0, 0), null), STRICT);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java
index 073a100..0bc953b 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/internal/coverage/GridCoverage2DTest.java
@@ -112,8 +112,7 @@ public class GridCoverage2DTest extends TestCase {
{ -60, -195},
{-216, -380}
});
-
- /**
+ /*
* Test evaluation
*/
Assert.assertArrayEquals(new double[]{ 70.0}, coverage.evaluate(new DirectPosition2D(0, 0), null), STRICT);
diff --git a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
index 3058a84..2a59156 100644
--- a/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
+++ b/core/sis-feature/src/test/java/org/apache/sis/test/suite/FeatureTestSuite.java
@@ -80,6 +80,7 @@ import org.junit.runners.Suite;
org.apache.sis.coverage.grid.GridExtentTest.class,
org.apache.sis.coverage.grid.GridGeometryTest.class,
org.apache.sis.coverage.grid.GridDerivationTest.class,
+ org.apache.sis.coverage.grid.FractionalGridCoordinates.class,
org.apache.sis.coverage.CategoryTest.class,
org.apache.sis.coverage.CategoryListTest.class,
org.apache.sis.coverage.SampleDimensionTest.class,