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 <[email protected]> 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,
