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 c2f143b493c5b269220ad38d4f9458103373c886 Author: Martin Desruisseaux <[email protected]> AuthorDate: Thu Dec 11 11:15:07 2025 +0100 Add a `stream(Collection<DirectPosition>)` method in the `GridCoverage.Evaluator` interface. Refactor `DefaultEvaluator` implementation for creating a `Spliterator` for each subsequence of points on the same slice and same tile. --- .../org/apache/sis/coverage/BandedCoverage.java | 60 +- .../coverage/grid/BandAggregateGridCoverage.java | 14 +- .../sis/coverage/grid/BufferedGridCoverage.java | 38 +- .../sis/coverage/grid/ConvertedGridCoverage.java | 25 +- .../apache/sis/coverage/grid/DefaultEvaluator.java | 458 +++++++----- .../apache/sis/coverage/grid/EvaluatorWrapper.java | 19 +- .../coverage/grid/FractionalGridCoordinates.java | 157 +--- .../org/apache/sis/coverage/grid/GridCoverage.java | 38 +- .../apache/sis/coverage/grid/GridCoverage2D.java | 46 +- .../sis/coverage/grid/ValuesAtPointIterator.java | 809 +++++++++++++++++++++ .../org/apache/sis/feature/internal/Resources.java | 4 +- .../sis/feature/internal/Resources.properties | 2 +- .../sis/feature/internal/Resources_fr.properties | 2 +- .../sis/coverage/grid/DefaultEvaluatorTest.java | 294 ++++++++ .../grid/FractionalGridCoordinatesTest.java | 11 +- .../sis/coverage/grid/GridCoverage2DTest.java | 4 +- .../main/org/apache/sis/io/CompoundFormat.java | 39 +- .../main/org/apache/sis/io/package-info.java | 2 +- 18 files changed, 1642 insertions(+), 380 deletions(-) diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/BandedCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/BandedCoverage.java index e735e1b0d8..895f9899c0 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/BandedCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/BandedCoverage.java @@ -18,6 +18,8 @@ package org.apache.sis.coverage; import java.util.List; import java.util.Optional; +import java.util.Collection; +import java.util.stream.Stream; import java.util.function.Function; import org.opengis.geometry.Envelope; import org.opengis.geometry.DirectPosition; @@ -131,7 +133,7 @@ public abstract class BandedCoverage { * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.3 + * @version 1.6 * * @see BandedCoverage#evaluator() * @@ -199,16 +201,19 @@ public abstract class BandedCoverage { /** * 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 conversions will be applied as needed. - * If the CRS of the point is undefined, then it is assumed to be the {@linkplain #getCoverage() coverage} CRS. - * The returned sequence includes a value for each {@linkplain SampleDimension sample dimension}. + * If the <abbr>CRS</abbr> of the point is undefined (i.e., {@code null}), + * then it is assumed to be the <abbr>CRS</abbr> of the {@linkplain #getCoverage() coverage}. + * If the <abbr>CRS</abbr> of the point is defined but different than the coverage <abbr>CRS</abbr>, + * then coordinate conversions or transformations will be applied automatically by this method. * - * @param point the position where to evaluate. + * <p>The returned sequence includes a value for each {@linkplain SampleDimension sample dimension}. + * For performance reason, this method may return the same array on every method call by overwriting + * previous values. Therefore, callers can assume that the array content is stable only until the next call + * to an {@code Evaluator} method or traversal of more elements in the {@linkplain #stream stream}.</p> + * + * @param point the position where to evaluate. * @return the sample values at the specified point, or {@code null} if the point is outside the coverage. - * For performance reason, this method may return the same array - * on every method call by overwriting previous values. - * Callers should not assume that the array content stay valid for a long time. + * This is not guaranteed to be a new array on each method call. * @throws PointOutsideCoverageException if the evaluation failed because the input point * has invalid coordinates and the {@link #isNullIfOutside()} flag is {@code false}. * @throws CannotEvaluateException if the values cannot be computed at the specified coordinates @@ -217,5 +222,42 @@ public abstract class BandedCoverage { */ @Override double[] apply(DirectPosition point) throws CannotEvaluateException; + + /** + * Returns a stream of sample values for each point of the given collection. + * The values in the returned stream are traversed in the iteration order of the given collection. + * The returned stream behave as if {@link #apply(DirectPosition)} was invoked for each point. + * + * <p>This method is equivalent to {@code points.stream().map(this::apply)}, but potentially more efficient. + * Therefore, the notes documented in {@link #apply(DirectPosition)} apply also to each elements traversed by + * the stream: the <abbr>CRS</abbr> of each point is handled as documented in {@link #apply(DirectPosition)}, + * consumers should not assume that the content of the {@code double[]} arrays are stable after execution of + * the consumer body, and some elements provided by the stream may be {@code null} if a point is outside the + * coverage and the {@link #isNullIfOutside()} flag is {@code true}.</p> + * + * <h4>Parallel streams</h4> + * While {@code Evaluator} is not thread-safe, parallel streams may be supported provided that the state of + * this {@code Evaluator} is not modified during stream execution. A parallel stream can be requested by + * invoking this method with the {@code parallel} argument set to {@code true}. + * The {@link Stream#parallel()} method should <strong>not</strong> by invoked. + * + * <p>Implementations may ignore the {@code parallel} argument if they do not support parallel streams. + * It is more difficult for implementations to ignore a call to {@link Stream#parallel()}, which is why + * {@code parallel()} should not be invoked on streams returned by this method.</p> + * + * <h4>Exceptions</h4> + * {@link CannotEvaluateException} may be thrown either when this method is invoked, + * or later during the traversal, at implementation choice. + * + * @param points the positions where to evaluate. + * @param parallel {@code true} for a parallel stream, or {@code false} for a sequential stream. + * @return a sequential or parallel stream of sample values at the specified positions. + * + * @since 1.6 + */ + default Stream<double[]> stream(Collection<? extends DirectPosition> points, boolean parallel) { + // Ignore `parallel` because we don't know if `apply(DirectPosition)` is thread-safe. + return points.stream().map(this::apply); + } } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java index b3cb457620..7d8c259534 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BandAggregateGridCoverage.java @@ -190,7 +190,12 @@ final class BandAggregateGridCoverage extends GridCoverage { * The union of slice coordinates from all sources, or {@code null} if not yet computed. * All coverages should have the same values, but we nevertheless compute union in case. */ - private Map<Integer,Long> slices; + private Map<Integer, Long> slices; + + /** + * Result of combining the bands, recycled on each invocation of this method. + */ + private final double[] aggregate; /** * Creates a new evaluator which will delegate to the evaluators of all given sources. @@ -200,6 +205,7 @@ final class BandAggregateGridCoverage extends GridCoverage { for (int i=0; i < coverages.length; i++) { sources[i] = coverages[i].evaluator(); } + aggregate = new double[numBands]; } /** @@ -215,9 +221,9 @@ final class BandAggregateGridCoverage extends GridCoverage { */ @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") - public Map<Integer,Long> getDefaultSlice() { + public Map<Integer, Long> getDefaultSlice() { if (slices == null) { - final var c = new TreeMap<Integer,Long>(); + final var c = new TreeMap<Integer, Long>(); for (final Evaluator source : sources) { c.putAll(source.getDefaultSlice()); } @@ -288,8 +294,8 @@ final class BandAggregateGridCoverage extends GridCoverage { * This method delegates to all source evaluators and merge the results. */ @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") public double[] apply(final DirectPosition point) { - final double[] aggregate = new double[numBands]; int offset = 0; for (int i=0; i < sources.length; i++) { final double[] values = sources[i].apply(point); diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java index e4105667ea..741fb9a20f 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/BufferedGridCoverage.java @@ -77,7 +77,7 @@ import org.opengis.coverage.PointOutsideCoverageException; * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.4 + * @version 1.6 * @since 1.1 */ public class BufferedGridCoverage extends GridCoverage { @@ -234,7 +234,7 @@ public class BufferedGridCoverage extends GridCoverage { */ @Override public Evaluator evaluator() { - return new CellAccessor(this); + return new CellAccessor(); } /** @@ -285,13 +285,9 @@ public class BufferedGridCoverage extends GridCoverage { /** * Implementation of evaluator returned by {@link #evaluator()}. + * This evaluator gets the values directly from the {@link DataBuffer}. */ - private static final class CellAccessor extends DefaultEvaluator { - /** - * A copy of {@link BufferedGridCoverage#data} reference. - */ - private final DataBuffer data; - + private final class CellAccessor extends DefaultEvaluator { /** * The grid lower values. Those values need to be subtracted to each * grid coordinate before to compute index in {@link #data} buffer. @@ -311,13 +307,11 @@ public class BufferedGridCoverage extends GridCoverage { private final boolean banded; /** - * Creates a new evaluator for the specified coverage. + * Creates a new evaluator for the enclosing coverage. */ - CellAccessor(final BufferedGridCoverage coverage) { - super(coverage); - final GridExtent extent = coverage.getGridGeometry().getExtent(); - final int numBands = coverage.getSampleDimensions().size(); - data = coverage.data; + CellAccessor() { + final GridExtent extent = getGridGeometry().getExtent(); + final int numBands = getSampleDimensions().size(); banded = data.getNumBanks() > 1; values = new double[numBands]; lower = new long[extent.getDimension()]; @@ -329,29 +323,37 @@ public class BufferedGridCoverage extends GridCoverage { } } + /** + * Returns the coverage from which this evaluator is fetching sample values. + */ + @Override + public final GridCoverage getCoverage() { + return BufferedGridCoverage.this; + } + /** * 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 as the coverage. */ @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") public double[] apply(final DirectPosition point) throws CannotEvaluateException { final int pos; try { - final FractionalGridCoordinates gc = toGridPosition(point); + final double[] gridCoords = toGridPosition(point); int i = lower.length; long s = sizes[i]; long index = 0; while (--i >= 0) { final long low = lower[i]; - long p = gc.getCoordinateValue(i); + long p = Math.round(gridCoords[i]); // (p - low) may overflow, so we must test (p < low) before. if (p < low || (p -= low) >= s) { if (isNullIfOutside()) { return null; } - throw new PointOutsideCoverageException( - gc.pointOutsideCoverage(getCoverage().gridGeometry.extent), point); + throw new PointOutsideCoverageException(pointOutsideCoverage(point), point); } /* * Following should never overflow, otherwise BufferedGridCoverage diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ConvertedGridCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ConvertedGridCoverage.java index 72b563138e..78c017524c 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ConvertedGridCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ConvertedGridCoverage.java @@ -20,6 +20,8 @@ import java.util.List; import java.util.Arrays; import java.util.ArrayList; import java.util.Optional; +import java.util.Collection; +import java.util.stream.Stream; import java.awt.image.RenderedImage; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.operation.MathTransform1D; @@ -268,7 +270,28 @@ final class ConvertedGridCoverage extends DerivedGridCoverage { */ @Override public double[] apply(final DirectPosition point) throws CannotEvaluateException { - final double[] values = super.apply(point); + return convert(super.apply(point)); + } + + /** + * Returns a stream of double values for given points in the coverage. + * This method delegates to the source coverage, then converts the values. + * + * @param point the position where to evaluate. + * @param parallel {@code true} for a parallel stream, or {@code false} for a sequential stream. + */ + @Override + public Stream<double[]> stream(Collection<? extends DirectPosition> points, boolean parallel) { + return super.stream(points, parallel).map(this::convert); + } + + /** + * Converts the given sample values. + * + * @param values the sample values to convert. May be null. + * @return the converted sample values, or {@code null} if the given values were null. + */ + private double[] convert(final double[] values) { if (values != null) try { final MathTransform1D[] converters = ConvertedGridCoverage.this.converters; for (int i=0; i<converters.length; i++) { diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java index 4711de9781..bc59bf82be 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/DefaultEvaluator.java @@ -17,10 +17,14 @@ package org.apache.sis.coverage.grid; import java.util.Map; +import java.util.List; import java.util.Arrays; import java.util.TreeMap; import java.util.Objects; -import java.awt.image.RenderedImage; +import java.util.Collection; +import java.util.function.IntFunction; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.opengis.util.FactoryException; import org.opengis.geometry.DirectPosition; import org.opengis.metadata.extent.GeographicBoundingBox; @@ -30,12 +34,6 @@ import org.opengis.referencing.operation.TransformException; import org.opengis.referencing.operation.CoordinateOperation; import org.opengis.referencing.operation.NoninvertibleTransformException; import org.opengis.referencing.crs.CoordinateReferenceSystem; -import org.apache.sis.coverage.SampleDimension; -import org.apache.sis.feature.internal.Resources; -import org.apache.sis.util.ArraysExt; -import org.apache.sis.util.ArgumentChecks; -import org.apache.sis.util.internal.shared.CollectionsExt; -import org.apache.sis.image.internal.shared.ImageUtilities; import org.apache.sis.referencing.CRS; import org.apache.sis.referencing.internal.shared.DirectPositionView; import org.apache.sis.referencing.internal.shared.WraparoundAxesFinder; @@ -43,11 +41,18 @@ import org.apache.sis.referencing.operation.matrix.Matrices; import org.apache.sis.referencing.operation.matrix.MatrixSIS; import org.apache.sis.referencing.operation.transform.MathTransforms; import org.apache.sis.referencing.operation.transform.TransformSeparator; +import org.apache.sis.geometry.CoordinateFormat; +import org.apache.sis.feature.internal.Resources; +import org.apache.sis.util.ArraysExt; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.StringBuilders; import org.apache.sis.util.logging.Logging; +import org.apache.sis.util.internal.shared.CollectionsExt; // Specific to the geoapi-3.1 and geoapi-4.0 branches: import org.opengis.coverage.CannotEvaluateException; import org.opengis.coverage.PointOutsideCoverageException; +import org.opengis.coordinate.MismatchedDimensionException; /** @@ -59,24 +64,21 @@ import org.opengis.coverage.PointOutsideCoverageException; * for each thread that need to compute sample values. * * <h2>Limitations</h2> - * Current implementation performs nearest-neighbor sampling only. A future version will provide interpolations. + * Current implementation performs nearest-neighbor sampling only. + * A future version may provide interpolations. * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * * @see GridCoverage#evaluator() + * @see <a href="https://issues.apache.org/jira/browse/SIS-576">SIS-576</a> */ -class DefaultEvaluator implements GridCoverage.Evaluator { - /** - * The coverage in which to evaluate sample values. - */ - private final GridCoverage coverage; - +abstract class DefaultEvaluator implements GridCoverage.Evaluator { /** * The coordinate reference system of input points given to this converter, - * or {@code null} if assumed the same as the coverage CRS. - * This is used by {@link #toGridPosition(DirectPosition)} for checking if {@link #inputToGrid} needs - * to be recomputed. As long at the evaluated points have the same CRS, the same transform is reused. + * or {@code null} if assumed the same as the coverage <abbr>CRS</abbr>. + * Used by {@link #toGridPosition(DirectPosition)} for checking if {@link #inputToGrid} needs to be recomputed. + * As long at the evaluated points have the same <abbr>CRS</abbr>, the same transform is reused. */ private CoordinateReferenceSystem inputCRS; @@ -88,15 +90,16 @@ class DefaultEvaluator implements GridCoverage.Evaluator { private MathTransform inputToGrid; /** - * Grid coordinates after {@link #inputToGrid} conversion. - * - * @see #toGridPosition(DirectPosition) + * Grid coordinates, created when first needed and then recycled. + * This is the result of applying the {@link #inputToGrid} transform. */ - private FractionalGridCoordinates.Position position; + private DirectPosition gridCoordinates; /** * Array where to store sample values computed by {@link #apply(DirectPosition)}. * For performance reasons, the same array may be recycled on every method call. + * This array shall <em>not</em> used by {@link #stream(Collection, boolean)}, + * in order to allow parallel execution. */ double[] values; @@ -146,30 +149,20 @@ class DefaultEvaluator implements GridCoverage.Evaluator { * @see #getDefaultSlice() * @see #setDefaultSlice(Map) */ - private Map<Integer,Long> slice; + private Map<Integer, Long> slice; /** - * Creates a new evaluator for the given coverage. This constructor is protected for allowing - * {@link GridCoverage} subclasses to provide their own {@code DefaultEvaluator} implementations. - * For using an evaluator, invoke {@link GridCoverage#evaluator()} instead. - * - * @param coverage the coverage for which to create an evaluator. - * - * @see GridCoverage#evaluator() + * The format to use for writing error messages for point outside grid domain. + * Created only when first needed. */ - protected DefaultEvaluator(final GridCoverage coverage) { - this.coverage = Objects.requireNonNull(coverage); - } + private CoordinateFormat coordinateFormat; /** - * Returns the coverage from which this evaluator is fetching sample values. - * This is the coverage on which the {@link GridCoverage#evaluator()} method has been invoked. + * Creates a new evaluator. * - * @return the source of sample values for this evaluator. + * @see GridCoverage#evaluator() */ - @Override - public final GridCoverage getCoverage() { - return coverage; + protected DefaultEvaluator() { } /** @@ -185,8 +178,9 @@ class DefaultEvaluator implements GridCoverage.Evaluator { */ @Override @SuppressWarnings("ReturnOfCollectionOrArrayField") // Because the map is unmodifiable. - public final Map<Integer,Long> getDefaultSlice() { + public final Map<Integer, Long> getDefaultSlice() { if (slice == null) { + final GridCoverage coverage = getCoverage(); final GridExtent extent = coverage.getGridGeometry().getExtent(); slice = CollectionsExt.unmodifiableOrCopy(extent.getSliceCoordinates()); } @@ -205,13 +199,14 @@ class DefaultEvaluator implements GridCoverage.Evaluator { */ @Override @SuppressWarnings("AssignmentToCollectionOrArrayFieldFromParameter") - public void setDefaultSlice(Map<Integer,Long> slice) { + public void setDefaultSlice(Map<Integer, Long> slice) { if (!Objects.equals(this.slice, slice)) { if (slice != null) { slice = CollectionsExt.unmodifiableOrCopy(new TreeMap<>(slice)); + final GridCoverage coverage = getCoverage(); final GridExtent extent = coverage.getGridGeometry().getExtent(); final int max = extent.getDimension() - 1; - for (final Map.Entry<Integer,Long> entry : slice.entrySet()) { + for (final Map.Entry<Integer, Long> entry : slice.entrySet()) { final int dim = entry.getKey(); ArgumentChecks.ensureBetween("slice.key", 0, max, dim); ArgumentChecks.ensureBetween(extent.getAxisIdentification(dim, dim).toString(), @@ -249,7 +244,8 @@ class DefaultEvaluator implements GridCoverage.Evaluator { public void setWraparoundEnabled(final boolean allow) { wraparoundAxes = 0; if (allow) try { - final WraparoundAxesFinder f = new WraparoundAxesFinder(coverage.getCoordinateReferenceSystem()); + final GridCoverage coverage = getCoverage(); + final var f = new WraparoundAxesFinder(coverage.getCoordinateReferenceSystem()); if ((periods = f.periods()) != null) { final GridGeometry gridGeometry = coverage.getGridGeometry(); final GridExtent extent = gridGeometry.getExtent(); @@ -324,53 +320,27 @@ class DefaultEvaluator implements GridCoverage.Evaluator { /** * 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 conversions will be applied as needed. - * If the CRS of the point is undefined, then it is assumed to be the {@linkplain #getCoverage() coverage} CRS. - * 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> + * This method checks the <abbr>CRS</abbr> and performs coordinate operations if needed. * - * <p>The default implementation invokes {@link GridCoverage#render(GridExtent)} for a small region - * around the point. Subclasses should override with more efficient implementation.</p> - * - * @param point the position where to evaluate. + * @param point the position where to evaluate. * @return the sample values at the specified point, or {@code null} if the point is outside the coverage. - * For performance reason, this method may return the same array - * on every method call by overwriting previous values. - * Callers should not assume that the array content stay valid for a long time. * @throws PointOutsideCoverageException if the evaluation failed because the input point * has invalid coordinates and the {@link #isNullIfOutside()} flag is {@code false}. - * @throws CannotEvaluateException if the values cannot be computed at the specified coordinates - * for another reason. This exception may be thrown if the coverage data type cannot be - * converted to {@code double} by an identity or widening conversion. - * Subclasses may relax this constraint if appropriate. + * @throws CannotEvaluateException if the values cannot be computed for another reason. */ @Override + @SuppressWarnings("ReturnOfCollectionOrArrayField") public double[] apply(final DirectPosition point) throws CannotEvaluateException { - /* - * TODO: instead of restricting to a single point, keep the automatic size (1 or 2), - * invoke render for each slice, 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). - * - * See https://issues.apache.org/jira/browse/SIS-576 - */ - final GridGeometry gridGeometry = coverage.gridGeometry; - final long[] size = new long[gridGeometry.getDimension()]; - Arrays.fill(size, 1); try { - final FractionalGridCoordinates gc = toGridPosition(point); - try { - final GridExtent subExtent = gc.toExtent(gridGeometry.extent, size, nullIfOutside); - if (subExtent != null) { - return evaluate(coverage.render(subExtent), 0, 0); - } - } catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) { - if (!nullIfOutside) { - throw (PointOutsideCoverageException) new PointOutsideCoverageException( - gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex); - } + final double[] gridCoords = toGridPosition(point); + final IntFunction<String> ifOutside; + if (nullIfOutside) { + ifOutside = null; + } else { + ifOutside = (index) -> pointOutsideCoverage(point); + } + if (ValuesAtPointIterator.create(getCoverage(), gridCoords, 1, ifOutside).tryAdvance((t) -> values = t)) { + return values; } } catch (PointOutsideCoverageException ex) { ex.setOffendingLocation(point); @@ -382,21 +352,95 @@ class DefaultEvaluator implements GridCoverage.Evaluator { } /** - * Gets sample values from the given image at the given index. This method does not verify - * explicitly if the coordinates are out of bounds; we rely on the checks performed by the - * image and sample model implementations. + * Returns a stream of sample values for each point of the given collection. + * The values in the returned stream are traversed in the iteration order of the given collection. + * The returned stream behave as if {@link #apply(DirectPosition)} was invoked for each point. + * {@link CannotEvaluateException} may be thrown when this method is invoked or during the traversal. * - * @param data the data from which to get the sample values. - * @param x column index of the value to get. - * @param y row index of the value to get. - * @return the sample values. The same array may be recycled on every method call. - * @throws ArithmeticException if an integer overflow occurred while computing indices. - * @throws IndexOutOfBoundsException if a coordinate is out of bounds. + * @param points the positions where to evaluate. + * @param parallel {@code true} for a parallel stream, or {@code false} for a sequential stream. + * @return the sample values at the specified positions. */ - final double[] evaluate(final RenderedImage data, final int x, final int y) { - final int tx = ImageUtilities.pixelToTileX(data, x); - final int ty = ImageUtilities.pixelToTileY(data, y); - return values = data.getTile(tx, ty).getPixel(x, y, values); + @Override + public Stream<double[]> stream(final Collection<? extends DirectPosition> points, final boolean parallel) { + final GridCoverage coverage = getCoverage(); + final int dimension = coverage.gridGeometry.getDimension(); + double[] coordinates = ArraysExt.EMPTY_DOUBLE; + MathTransform toGrid = inputToGrid; + int srcDim = (toGrid == null) ? 0 : toGrid.getSourceDimensions(); + int inputCoordinateOffset = 0; + int firstCoordToTransform = 0; + int numPointsToTransform = 0; + try { + /* + * Convert the "real world " coordinates to grid coordinates. + * The CRS of each point is verified. Consecutive points with + * the same CRS will be transformed in a single operation. + */ + for (final DirectPosition point : points) { + if (setInputCRS(point.getCoordinateReferenceSystem())) { + if (numPointsToTransform > 0) { // Because `toGrid` may be null. + assert toGrid.getTargetDimensions() == dimension; + toGrid.transform(coordinates, firstCoordToTransform, + coordinates, firstCoordToTransform, + numPointsToTransform); + } + wraparound(coordinates, firstCoordToTransform, numPointsToTransform); + firstCoordToTransform += numPointsToTransform * dimension; + numPointsToTransform = 0; + toGrid = inputToGrid; + srcDim = toGrid.getSourceDimensions(); + } + int offset = inputCoordinateOffset; + if ((inputCoordinateOffset += srcDim) > coordinates.length) { + int n = firstCoordToTransform / dimension; // Number of points already transformed. + n = points.size() - n + numPointsToTransform; // Number of points left to transform. + coordinates = new double[Math.multiplyExact(n, Math.max(srcDim, dimension))]; + } + for (int i=0; i<srcDim; i++) { + coordinates[offset++] = point.getCoordinate(i); + } + numPointsToTransform++; + } + /* + * Transforms the remaining sequence of points. + * Actually, in most cases, all points are transformed here. + */ + if (numPointsToTransform > 0) { // Because `toGrid` may be null. + assert toGrid.getTargetDimensions() == dimension; + toGrid.transform(coordinates, firstCoordToTransform, + coordinates, firstCoordToTransform, + numPointsToTransform); + } + wraparound(coordinates, firstCoordToTransform, numPointsToTransform); + final int numPoints = firstCoordToTransform / dimension + numPointsToTransform; + /* + * Create the iterator. The `ValuesAtPointIterator.create(…)` method will identify the slices in + * n-dimensional coverage, get the rendered images for the regions of interest and get the tiles. + */ + final IntFunction<String> ifOutside; + if (nullIfOutside) { + ifOutside = null; + } else { + final var listOfPoints = (points instanceof List<?>) ? (List<? extends DirectPosition>) points : null; + ifOutside = (index) -> { + if (listOfPoints != null) try { + DirectPosition point = listOfPoints.get(index); + if (point != null) { + return pointOutsideCoverage(point); + } + } catch (IndexOutOfBoundsException e) { + recoverableException("pointOutsideCoverage", e); + } + return Resources.format(Resources.Keys.PointOutsideCoverageDomain_1, "#" + index); + }; + } + return StreamSupport.stream(ValuesAtPointIterator.create(coverage, coordinates, numPoints, ifOutside), parallel); + } catch (CannotEvaluateException ex) { + throw ex; + } catch (RuntimeException | FactoryException | TransformException ex) { + throw new CannotEvaluateException(ex.getMessage(), ex); + } } /** @@ -421,119 +465,122 @@ class DefaultEvaluator implements GridCoverage.Evaluator { @Override public FractionalGridCoordinates toGridCoordinates(final DirectPosition point) throws TransformException { try { - return new FractionalGridCoordinates(toGridPosition(Objects.requireNonNull(point))); + final double[] gridCoords = toGridPosition(point); + final int dimension = inputToGrid.getTargetDimensions(); + return new FractionalGridCoordinates(ArraysExt.resize(gridCoords, dimension)); } catch (FactoryException e) { throw new TransformException(e.getMessage(), e); } } /** - * Updates the grid {@linkplain #position} with the given geospatial position. - * This is the implementation of {@link #toGridCoordinates(DirectPosition)} except - * that it avoid creating a new {@link FractionalGridCoordinates} on each method call. + * Returns the grid coordinates of the given "real world" coordinates. + * This method may return an array longer than the grid dimension. + * Extra dimensions should be ignored. * * @param point the geospatial position. * @return the given position converted to grid coordinates (possibly out of grid bounds). * @throws FactoryException if no operation is found form given point CRS to coverage CRS. * @throws TransformException if the given position cannot be converted. */ - final FractionalGridCoordinates.Position toGridPosition(final DirectPosition point) - throws FactoryException, TransformException - { + final double[] toGridPosition(final DirectPosition point) throws FactoryException, TransformException { /* * If the `inputToGrid` transform has not yet been computed or is outdated, compute now. * The result will be cached and reused as long as the `inputCRS` is the same. */ - final CoordinateReferenceSystem crs = point.getCoordinateReferenceSystem(); - if (crs != inputCRS || inputToGrid == null) { - setInputCRS(crs); - } - /* - * Transform geospatial coordinates to grid coordinates. Result is unconditionally stored - * in the `position` object, which will be copied by the caller if needed for public API. - */ - final DirectPosition result = inputToGrid.transform(point, position); - if (result != position) { - // Should not happen, but be paranoiac. - final double[] coordinates = position.coordinates; - System.arraycopy(result.getCoordinates(), 0, coordinates, 0, coordinates.length); + setInputCRS(point.getCoordinateReferenceSystem()); + gridCoordinates = inputToGrid.transform(point, gridCoordinates); + final int dimension = inputToGrid.getTargetDimensions(); + final double[] coordinates = point.getCoordinates(); + final double[] gridCoords = (dimension <= coordinates.length) ? coordinates : new double[dimension]; + inputToGrid.transform(coordinates, 0, gridCoords, 0, 1); + wraparound(gridCoords, 0, 1); + return gridCoords; + } + + /** + * If a coordinate is outside the coverage extent, check if a wraparound on some axes + * would bring the coordinates inside the extent. Coordinates are adjusted in-place. + * + * @param gridCoords the grid coordinates. + * @param offset index of the first grid coordinate value. + * @param numPoints number of points in the array. + */ + private void wraparound(final double[] gridCoords, int offset, int numPoints) throws TransformException { + if (wraparoundAxes == 0) { + return; } - /* - * In most cases, the work of this method ends here. The remaining code in this method - * is for handling wraparound axes. If a coordinate is outside the coverage extent, - * check if a wraparound on some axes would bring the coordinates inside the extent. - */ - long axes = wraparoundAxes; - if (axes != 0) { - double[] coordinates = position.coordinates; + double[] twoPoints = null; // Created only if needed. + final int dimension = inputToGrid.getTargetDimensions(); + final int dimOfWrap = gridToWraparound.getTargetDimensions(); +next: while (--numPoints >= 0) { + long axesToCheck = wraparoundAxes; long outsideAxes = 0; - int j = 0; + int limitIndex = 0; // Even indices are lower values, odd indices are upper values. do { - final int i = Long.numberOfTrailingZeros(axes); - final double c = coordinates[i]; - double border; - if (c < (border = wraparoundExtent[j++]) || c > (border = wraparoundExtent[j])) { + final int axis = Long.numberOfTrailingZeros(axesToCheck); + final long mask = 1L << axis; + final double c = gridCoords[offset + axis]; + double limit; + if (c < (limit = wraparoundExtent[limitIndex]) || c > (limit = wraparoundExtent[limitIndex + 1])) { /* * Detected that the point is outside the grid extent along an axis where wraparound is possible. - * The first time that we find such axis, expand the coordinates array for storing two points. - * The two points will have the same coordinates, except on all axes where the point is outside. - * On those axes, the coordinate of the first point is set to the closest border of the grid. + * We will compute 2 points with the same coordinates except on axes where the point is outside. + * On those axes, the coordinate of the copied point is set to the closest limit of the grid. */ if (outsideAxes == 0) { - final int n = coordinates.length; - coordinates = Arrays.copyOf(coordinates, 2*Math.max(n, gridToWraparound.getTargetDimensions())); - System.arraycopy(coordinates, 0, coordinates, n, n); + if (twoPoints == null) { + twoPoints = new double[Math.max(dimension, dimOfWrap * 2)]; + } + System.arraycopy(gridCoords, offset, twoPoints, 0, dimension); } - coordinates[i] = border; - outsideAxes |= (1L << i); + twoPoints[axis] = limit; + outsideAxes |= mask; } - j++; // Outside above `if (…)` statement because increment needs to be unconditional. - axes &= ~(1L << i); - } while (axes != 0); - assert wraparoundExtent.length == j : j; + axesToCheck &= ~mask; + limitIndex += 2; + } while (axesToCheck != 0); + assert wraparoundExtent.length == limitIndex : limitIndex; /* * If a coordinate was found outside the grid, transform to a CRS where we can apply shift. - * It may be the same CRS as the coverage CRS or the source CRS, but not necessarily. + * It may be the same CRS as the coverage CRS or as the source CRS, but not necessarily. * For example if the CRS is projected, then we need to use a geographic intermediate CRS. - * In the common case where the source CRS is already geographic, the second point in the - * `coordinates` array after `transform(…)` will contain the same coordinates as `point`, - * but potentially with more dimensions. */ if (outsideAxes != 0) { - gridToWraparound.transform(coordinates, 0, coordinates, 0, 2); - final int s = gridToWraparound.getTargetDimensions(); - for (int i = periods.length; --i >= 0;) { - final double period = periods[i]; + gridToWraparound.transform(twoPoints, 0, twoPoints, dimOfWrap, 1); // Clipped coordinates. + gridToWraparound.transform(gridCoords, offset, twoPoints, 0, 1); // Original coordinates. + for (int axis = periods.length; --axis >= 0;) { + final double period = periods[axis]; if (period > 0) { /* * Compute the shift that was necessary for moving the point inside the grid, * then round that shift to an integer number of periods. Modify the original * coordinate by applying that modified translation. */ - final int oi = i + s; // Index of original coordinates. - double shift = coordinates[i] - coordinates[oi]; + double shift = twoPoints[axis + dimOfWrap] - twoPoints[axis]; shift = Math.copySign(Math.ceil(Math.abs(shift) / period), shift) * period; - coordinates[oi] += shift; + twoPoints[axis] += shift; } } /* * Convert back the shifted point to grid coordinates, then check again if the new point - * is inside the grid extent. If this is not the case, we will return the old position + * is inside the grid extent. If this is not the case, we will keep the old position * on the assumption that it will be less confusing to the user. */ - gridToWraparound.inverse().transform(coordinates, s, coordinates, 0, 1); - j = 0; + gridToWraparound.inverse().transform(twoPoints, 0, twoPoints, 0, 1); + limitIndex = 0; do { - final int i = Long.numberOfTrailingZeros(outsideAxes); - final double c = coordinates[i]; - if (c < wraparoundExtent[j++] || c > wraparoundExtent[j++]) { - return position; + final int axis = Long.numberOfTrailingZeros(outsideAxes); + final double c = twoPoints[axis]; + if (c < wraparoundExtent[limitIndex++] || c > wraparoundExtent[limitIndex++]) { + offset += dimension; + continue next; } - outsideAxes &= ~(1L << i); + outsideAxes &= ~(1L << axis); } while (outsideAxes != 0); /* - * Copy shifted coordinate values to the final `FractionalGridCoordinates`, except the NaN values. - * NaN values may exist if the given `point` has less dimensions than the grid geometry, in which + * Copy shifted coordinate values, except the NaN values. + * NaN values may exist if the given points have less dimensions than the grid geometry, in which * case missing values have been replaced by `slice` values in the `target` array but not in the * `coordinates` array. We want to keep the `slice` values in the `target` array. * @@ -541,27 +588,32 @@ class DefaultEvaluator implements GridCoverage.Evaluator { * could happen that a transform resulted in NaN values in other dimensions. But that check would * be costly, so we avoid it for now. */ - final double[] target = position.coordinates; - for (int i = target.length; --i >= 0;) { - final double value = coordinates[i]; + for (int i=0; i<dimension; i++) { + final double value = twoPoints[i]; if (!Double.isNaN(value)) { - target[i] = value; + gridCoords[offset] = value; } + offset++; } } } - return position; } /** - * Recomputes the {@link #inputToGrid} field. This method should be invoked when the transform - * has not yet been computed or became outdated because {@link #inputCRS} needs to be changed. + * Recomputes the {@link #inputToGrid} field if the <abbr>CRS</abbr> changed. + * This method should be invoked when the transform has not yet been computed + * or may became outdated because {@link #inputCRS} needs to be changed. * - * @param crs the new value to assign to {@link #inputCRS}. + * @param crs the new value to assign to {@link #inputCRS}. Can be {@code null}. + * @return whether the given <abbr>CRS</abbr> is different than the previous one. */ - private void setInputCRS(final CoordinateReferenceSystem crs) + private boolean setInputCRS(final CoordinateReferenceSystem crs) throws FactoryException, NoninvertibleTransformException { + if (crs == inputCRS && inputToGrid != null) { + return false; + } + final GridCoverage coverage = getCoverage(); final GridGeometry gridGeometry = coverage.getGridGeometry(); MathTransform gridToCRS = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER); MathTransform crsToGrid = gridToCRS.inverse(); @@ -580,7 +632,7 @@ class DefaultEvaluator implements GridCoverage.Evaluator { * dimensions. We try to fill missing dimensions with the help of the `slice` map. */ @SuppressWarnings("LocalVariableHidesMemberVariable") - final Map<Integer,Long> slice = getDefaultSlice(); + final Map<Integer, Long> slice = getDefaultSlice(); try { CoordinateOperation op = CRS.findOperation(stepCRS, crs, areaOfInterest); gridToCRS = MathTransforms.concatenate(gridToCRS, op.getMathTransform()); @@ -626,9 +678,71 @@ class DefaultEvaluator implements GridCoverage.Evaluator { } } // Modify fields only after everything else succeeded. - position = new FractionalGridCoordinates.Position(crsToGrid.getTargetDimensions()); inputCRS = crs; inputToGrid = crsToGrid; + return true; + } + + /** + * Creates an error message for a grid coordinates out of bounds. + * This method tries to detect the dimension of the out-of-bounds + * coordinate by searching for the dimension with largest error. + * + * <h4>Thread safety</h4> + * This method may be invoked during parallel execution of the return value of {@link #stream(Collection, boolean)}. + * Therefore, it needs to be thread-safe even if {@link GridCoverage.Evaluator} is documented as not thread safe. + * + * @param point the point which is outside the grid. + * @return message to provide to {@link PointOutsideCoverageException}. + */ + final synchronized String pointOutsideCoverage(final DirectPosition point) { + String details = null; + final var buffer = new StringBuilder(); + final GridCoverage coverage = getCoverage(); + final GridExtent extent = coverage.gridGeometry.extent; + final MathTransform gridToCRS = coverage.gridGeometry.gridToCRS; + if (extent != null && gridToCRS != null) try { + int axis = 0; + long validMin = 0; + long validMax = 0; + double distance = 0; + gridCoordinates = gridToCRS.inverse().transform(point, gridCoordinates); + final int dimension = Math.min(gridCoordinates.getDimension(), extent.getDimension()); + for (int i=0; i<dimension; i++) { + final long low = extent.getLow(i); + final long high = extent.getHigh(i); + final double c = gridCoordinates.getCoordinate(i); + double d; // Reminder: value may be NaN. + if ((d = low - c) > distance || (d = c - high) > distance) { + axis = i; + validMin = low; + validMax = high; + distance = d; + } + } + for (int i=0; i<dimension; i++) { + if (i != 0) buffer.append(' '); + StringBuilders.trimFractionalPart(buffer.append(gridCoordinates.getCoordinate(i))); + } + details = Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4, + extent.getAxisIdentification(axis, axis), validMin, validMax, buffer); + } catch (MismatchedDimensionException | TransformException e) { + recoverableException("pointOutsideCoverage", e); + } + /* + * If non-null, `message` provides details about the problem using grid coordinates. + * Also format a simpler message using the "real world" coordinates. + */ + buffer.setLength(0); + if (coordinateFormat == null) { + coordinateFormat = coverage.createCoordinateFormat(); + } + coordinateFormat.format(point, buffer); + String message = Resources.format(Resources.Keys.PointOutsideCoverageDomain_1, buffer); + if (details != null) { + message = message + System.lineSeparator() + details; + } + return message; } /** @@ -638,7 +752,7 @@ class DefaultEvaluator implements GridCoverage.Evaluator { * @param caller the method where exception occurred. * @param exception the exception that occurred. */ - private static void recoverableException(final String caller, final TransformException exception) { + private static void recoverableException(final String caller, final Exception exception) { Logging.recoverableException(GridExtent.LOGGER, DefaultEvaluator.class, caller, exception); } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/EvaluatorWrapper.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/EvaluatorWrapper.java index 12e7e658b5..6f2e5db8df 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/EvaluatorWrapper.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/EvaluatorWrapper.java @@ -17,6 +17,8 @@ package org.apache.sis.coverage.grid; import java.util.Map; +import java.util.Collection; +import java.util.stream.Stream; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.operation.TransformException; @@ -84,7 +86,7 @@ abstract class EvaluatorWrapper implements GridCoverage.Evaluator { * with a different grid geometry than the coverage of the wrapped evaluator. */ @Override - public Map<Integer,Long> getDefaultSlice() { + public Map<Integer, Long> getDefaultSlice() { return source.getDefaultSlice(); } @@ -94,7 +96,7 @@ abstract class EvaluatorWrapper implements GridCoverage.Evaluator { * with a different grid geometry than the coverage of the wrapped evaluator. */ @Override - public void setDefaultSlice(Map<Integer,Long> slice) { + public void setDefaultSlice(Map<Integer, Long> slice) { source.setDefaultSlice(slice); } @@ -120,4 +122,17 @@ abstract class EvaluatorWrapper implements GridCoverage.Evaluator { public double[] apply(final DirectPosition point) throws CannotEvaluateException { return source.apply(point); } + + /** + * Returns a stream of double values for given points in the coverage. + * This method should be overridden if this evaluator is for a coverage + * doing some on-the-fly conversion of sample values. + * + * @param points positions where to evaluate the sample values. + * @param parallel {@code true} for a parallel stream, or {@code false} for a sequential stream. + */ + @Override + public Stream<double[]> stream(Collection<? extends DirectPosition> points, boolean parallel) { + return source.stream(points, parallel); + } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java index d8d23d9c0e..5eded56547 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/FractionalGridCoordinates.java @@ -21,6 +21,7 @@ import java.io.Serializable; import org.opengis.geometry.DirectPosition; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.TransformException; +import org.apache.sis.geometry.GeneralDirectPosition; import org.apache.sis.feature.internal.Resources; import org.apache.sis.util.StringBuilders; import org.apache.sis.util.internal.shared.Strings; @@ -48,7 +49,7 @@ import org.opengis.coverage.grid.GridCoordinates; * {@link ArithmeticException} is thrown.</p> * * @author Martin Desruisseaux (Geomatys) - * @version 1.5 + * @version 1.6 * * @see GridCoverage.Evaluator#toGridCoordinates(DirectPosition) * @@ -63,7 +64,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable /** * The grid coordinates as floating-point numbers. */ - final double[] coordinates; + private final double[] coordinates; /** * Creates a new grid coordinates with the given number of dimensions. @@ -79,6 +80,16 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable coordinates = new double[dimension]; } + /** + * Creates a new grid coordinates with the given coordinates. + * The array length is the number of dimensions. + * + * @param coordinates the grid coordinates. + */ + FractionalGridCoordinates(final double[] coordinates) { + this.coordinates = coordinates; + } + /** * Creates a new grid coordinates initialized to a copy of the given coordinates. * @@ -134,13 +145,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable @Override public long getCoordinateValue(final int dimension) { final double value = coordinates[dimension]; - /* - * 2048 is the smallest value that 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)) { + if (value >= ValuesAtPointIterator.DOMAIN_MINIMUM && value <= ValuesAtPointIterator.DOMAIN_MAXIMUM) { return Math.round(value); } throw new ArithmeticException(Resources.format(Resources.Keys.UnconvertibleGridCoordinate_2, "long", value)); @@ -179,7 +184,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable /** * 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: + * will hold (where {@code extent} is the return value): * * <ol> * <li>If <code>extent.{@linkplain GridExtent#getSize(int) getSize}(i)</code> ≥ 2 and no shift (see below) then:<ul> @@ -228,21 +233,11 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable * @throws PointOutsideCoverageException if the grid coordinates (rounded to nearest integers) are outside 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) { - return toExtent(bounds, size, false); - } - - /** - * Implementation of {@link #toExtent(GridExtent, long...)} with the option to replace - * {@link PointOutsideCoverageException} by null return value. * - * @param bounds if the coordinates shall be contained inside a grid, that grid extent. Otherwise {@code null}. - * @param size the desired extent sizes as strictly positive numbers, or 0 for automatic sizes (1 or 2). - * @param nullIfOutside whether to return {@code null} instead of throwing an exception if given point is outside coverage bounds. - * @return a grid extent containing grid coordinates, or {@code null} if outside and {@code nullIfOutside} is {@code true}. + * @deprecated Not used anymore because this method leads to a multiplication of very small read operations. */ - final GridExtent toExtent(final GridExtent bounds, final long[] size, final boolean nullIfOutside) { + @Deprecated(since = "1.6", forRemoval = true) + public GridExtent toExtent(final GridExtent bounds, final long... size) { final int dimension = coordinates.length; if (bounds != null) { final int bd = bounds.getDimension(); @@ -303,10 +298,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable final long validMin = bounds.getLow(i); final long validMax = bounds.getHigh(i); if (nearest > validMax || nearest < validMin) { - if (nullIfOutside) { - return null; - } - final StringBuilder b = new StringBuilder(); + final var b = new StringBuilder(); writeCoordinates(b); throw new PointOutsideCoverageException( Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4, @@ -340,6 +332,7 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable /** * Returns the grid coordinates converted to a geospatial position using the given transform. + * This is the reverse of {@link GridCoverage.Evaluator#toGridCoordinates(DirectPosition)}. * The {@code gridToCRS} argument is typically {@link GridGeometry#getGridToCRS(PixelInCell)} * with {@link PixelInCell#CELL_CENTER}. * @@ -350,115 +343,19 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable * @see GridCoverage.Evaluator#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> - */ - 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 all coordinate values. - */ - @Override - public double[] getCoordinates() { - return coordinates.clone(); - } - - /** - * Returns the coordinate value at the given dimension. - */ - @Override - public double getCoordinate(int dimension) { - return coordinates[dimension]; - } - - /** - * Sets the coordinate value at the given dimension. - */ - @Override - public void setCoordinate(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); - } + final var position = new GeneralDirectPosition(gridToCRS.getTargetDimensions()); + gridToCRS.transform(coordinates, 0, position.coordinates, 0, 1); + return position; } /** - * Creates an error message for a grid coordinates out of bounds. - * This method tries to detect the dimension of the out-of-bounds - * coordinate by searching for the dimension with largest error. + * Returns a string representation of this grid coordinates for debugging purposes. * - * @param bounds the expected bounds, or {@code null} if unknown. - * @return message to provide to {@link PointOutsideCoverageException}, - * or {@code null} if the given bounds were null. - */ - final String pointOutsideCoverage(final GridExtent bounds) { - if (bounds == null) { - return null; - } - int axis = 0; - long validMin = 0; - long validMax = 0; - double distance = 0; - for (int i = bounds.getDimension(); --i >= 0;) { - final long low = bounds.getLow(i); - final long high = bounds.getHigh(i); - final double c = coordinates[i]; - double d = low - c; - if (!(d > distance)) { // Use '!' for entering in this block if `d` is NaN. - d = c - high; - if (!(d > distance)) { // Use '!' for skipping this coordinate if `d` is NaN. - continue; - } - } - axis = i; - validMin = low; - validMax = high; - distance = d; - } - final StringBuilder b = new StringBuilder(); - writeCoordinates(b); - return Resources.format(Resources.Keys.GridCoordinateOutsideCoverage_4, - bounds.getAxisIdentification(axis, axis), validMin, validMax, b.toString()); - } - - /** - * Returns a string representation of this grid coordinates for debugging purpose. + * @return a string representation for debugging purposes. */ @Override public String toString() { - final StringBuilder buffer = new StringBuilder("GridCoordinates["); + final var buffer = new StringBuilder("GridCoordinates["); writeCoordinates(buffer); return buffer.append(']').toString(); } @@ -475,6 +372,8 @@ public class FractionalGridCoordinates implements GridCoordinates, Serializable /** * Returns a hash code value for this grid coordinates. + * + * @return a hash code value. */ @Override public int hashCode() { diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java index d58dd22075..e28a389118 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage.java @@ -36,9 +36,10 @@ import org.apache.sis.measure.NumberRange; import org.apache.sis.coverage.BandedCoverage; import org.apache.sis.coverage.SampleDimension; import org.apache.sis.coverage.SubspaceNotSpecifiedException; +import org.apache.sis.coverage.internal.shared.SampleDimensions; import org.apache.sis.image.DataType; import org.apache.sis.image.ImageProcessor; -import org.apache.sis.coverage.internal.shared.SampleDimensions; +import org.apache.sis.geometry.CoordinateFormat; import org.apache.sis.util.collection.DefaultTreeTable; import org.apache.sis.util.collection.TableColumn; import org.apache.sis.util.collection.TreeTable; @@ -57,7 +58,7 @@ import org.opengis.coverage.CannotEvaluateException; * * @author Martin Desruisseaux (IRD, Geomatys) * @author Johann Sorel (Geomatys) - * @version 1.4 + * @version 1.6 * @since 1.0 */ public abstract class GridCoverage extends BandedCoverage { @@ -343,7 +344,11 @@ public abstract class GridCoverage extends BandedCoverage { */ @Override public Evaluator evaluator() { - return new DefaultEvaluator(this); + return new DefaultEvaluator() { + @Override public GridCoverage getCoverage() { + return GridCoverage.this; + } + }; } /** @@ -358,7 +363,7 @@ public abstract class GridCoverage extends BandedCoverage { * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) - * @version 1.3 + * @version 1.6 * * @see GridCoverage#evaluator() * @@ -394,7 +399,7 @@ public abstract class GridCoverage extends BandedCoverage { * * @return the default slice where to perform evaluation, or an empty map if unspecified. */ - Map<Integer,Long> getDefaultSlice(); + Map<Integer, Long> getDefaultSlice(); /** * Sets the default slice where to perform evaluation when the points do not have enough dimensions. @@ -406,7 +411,7 @@ public abstract class GridCoverage extends BandedCoverage { * * @see GridExtent#getSliceCoordinates() */ - void setDefaultSlice(Map<Integer,Long> slice); + void setDefaultSlice(Map<Integer, Long> slice); /** * Converts the specified geospatial position to grid coordinates. If the given position is associated to @@ -511,6 +516,27 @@ public abstract class GridCoverage extends BandedCoverage { */ public abstract RenderedImage render(GridExtent sliceExtent) throws CannotEvaluateException; + /** + * Creates a new object for formatting spatial-temporal coordinates in the domain of this grid coverage. + * The {@linkplain CoordinateFormat#setDefaultCRS default <abbr>CRS</abbr>} of the format is set to the + * <abbr>CRS</abbr> of this coverage, and the {@linkplain CoordinateFormat#setPrecisions precision} of + * each coordinate is set to the {@linkplain GridGeometry#getResolution(boolean) grid resolution}. + * + * @return a coordinate format configured for the domain of this grid coverage. + * + * @since 1.6 + */ + public CoordinateFormat createCoordinateFormat() { + final var cf = new CoordinateFormat(); + if (gridGeometry.isDefined(GridGeometry.CRS)) { + cf.setDefaultCRS(gridGeometry.getCoordinateReferenceSystem()); + } + if (gridGeometry.isDefined(GridGeometry.RESOLUTION)) { + cf.setPrecisions(gridGeometry.getResolution(true)); + } + return cf; + } + /** * Returns a string representation of this grid coverage for debugging purpose. * The returned string is implementation dependent and may change in any future version. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java index 5e4188a2f1..f746d99629 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/GridCoverage2D.java @@ -33,6 +33,7 @@ import static java.lang.Math.min; import static java.lang.Math.addExact; import static java.lang.Math.subtractExact; import static java.lang.Math.toIntExact; +import static java.lang.Math.round; import org.opengis.metadata.spatial.DimensionNameType; import org.opengis.util.NameFactory; import org.opengis.util.InternationalString; @@ -91,7 +92,7 @@ import org.opengis.coverage.PointOutsideCoverageException; * @author Martin Desruisseaux (Geomatys) * @author Johann Sorel (Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.5 + * @version 1.6 * @since 1.1 */ public class GridCoverage2D extends GridCoverage { @@ -503,7 +504,14 @@ public class GridCoverage2D extends GridCoverage { * Creates a new evaluator for the enclosing coverage. */ PixelAccessor() { - super(GridCoverage2D.this); + } + + /** + * Returns the coverage from which this evaluator is fetching sample values. + */ + @Override + public final GridCoverage getCoverage() { + return GridCoverage2D.this; } /** @@ -513,25 +521,33 @@ public class GridCoverage2D extends GridCoverage { */ @Override public double[] apply(final DirectPosition point) throws CannotEvaluateException { + RuntimeException cause = null; try { - final FractionalGridCoordinates gc = toGridPosition(point); - try { - final int x = toIntExact(addExact(gc.getCoordinateValue(xDimension), gridToImageX)); - final int y = toIntExact(addExact(gc.getCoordinateValue(yDimension), gridToImageY)); - return evaluate(data, x, y); - } catch (ArithmeticException | IndexOutOfBoundsException | DisjointExtentException ex) { - if (isNullIfOutside()) { - return null; + final double[] gridCoords = toGridPosition(point); + final double cx = gridCoords[xDimension]; + final double cy = gridCoords[yDimension]; + if (cx >= ValuesAtPointIterator.DOMAIN_MINIMUM && cx <= ValuesAtPointIterator.DOMAIN_MAXIMUM && + cy >= ValuesAtPointIterator.DOMAIN_MINIMUM && cy <= ValuesAtPointIterator.DOMAIN_MAXIMUM) + { + try { + final int x = toIntExact(addExact(round(cx), gridToImageX)); + final int y = toIntExact(addExact(round(cy), gridToImageY)); + final int tx = ImageUtilities.pixelToTileX(data, x); + final int ty = ImageUtilities.pixelToTileY(data, y); + return values = data.getTile(tx, ty).getPixel(x, y, values); + } catch (ArithmeticException | IndexOutOfBoundsException e) { + cause = e; } - throw (PointOutsideCoverageException) new PointOutsideCoverageException( - gc.pointOutsideCoverage(gridGeometry.extent), point).initCause(ex); } - } catch (PointOutsideCoverageException ex) { - ex.setOffendingLocation(point); - throw ex; } catch (RuntimeException | FactoryException | TransformException ex) { throw new CannotEvaluateException(ex.getMessage(), ex); } + if (isNullIfOutside()) { + return null; + } + var ex = new PointOutsideCoverageException(pointOutsideCoverage(point), point); + ex.initCause(cause); + throw ex; } } diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java new file mode 100644 index 0000000000..44f31eb5b5 --- /dev/null +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/coverage/grid/ValuesAtPointIterator.java @@ -0,0 +1,809 @@ +/* + * 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.util.BitSet; +import java.util.Spliterator; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.awt.image.Raster; +import java.awt.image.RenderedImage; +import org.apache.sis.util.internal.shared.Strings; +import static org.apache.sis.coverage.grid.GridCoverage.BIDIMENSIONAL; + +// Specific to the geoapi-3.1 and geoapi-4.0 branches: +import org.opengis.coverage.PointOutsideCoverageException; + + +/** + * An iterator over the sample values evaluated at given coordinates. + * This iterator has many layers. The first layer iterates over slices. + * Then the second layer iterates over images, and a third layer over tiles. + * A last layer returns null values for points that are outside the coverage. + * + * <h2>Limitations</h2> + * Current implementation performs nearest-neighbor sampling only. + * A future version may provide interpolations. + * + * @author Martin Desruisseaux (Geomatys) + * + * @see <a href="https://issues.apache.org/jira/browse/SIS-576">SIS-576</a> + */ +abstract class ValuesAtPointIterator implements Spliterator<double[]> { + /** + * The dimensions of grid dimensions taken as <var>x</var> and <var>y</var> image axes. + * Static constants for now, may become configurable fields in the future. + */ + private static final int X_DIMENSION = 0, Y_DIMENSION = 1; + + /** + * A mask for deciding when a slice width or height become too large for a single read operation. + * This is both for avoiding 32-bits integer overflow (keeping in mind that the maximal image size + * may be the square of this size) and for reducing the risk of reading unnecessary tiles between + * points separated by potentially large empty spaces. + */ + private static final long MAXIMUM_SLICE_SIZE_MASK = ~0x7FFF; + + /** + * The minimum and maximum value that we accept for a {@code double} value before rounding to {@code long}. + * A margin of 2048 is added, which is the smallest change that can be applied on {@code Long.MIN/MAX_VALUE} + * as given by {@code Math.ulp(Long.MIN_VALUE)}. We add this tolerance because this class aim to return the + * {@code long} grid coordinate value closest to the {@code double} value and we consider an 1 <abbr>ULP</abbr> + * error as close enough. + */ + static final double DOMAIN_MINIMUM = Long.MIN_VALUE - 2048d, + DOMAIN_MAXIMUM = Long.MAX_VALUE + 2048d; + + /** + * Grid coordinates of points to evaluate, rounded to nearest integers. + * This array may be shared by many iterators and should not be modified. + * This array is null in the particular case of the {@link Null} subclass. + */ + protected final long[] nearestXY; + + /** + * Index of the first grid coordinate of the next point to evaluate. + * This is incremented by {@value GridCoverage2D#BIDIMENSIONAL} after each evaluation. + */ + protected int indexOfXY; + + /** + * Index after the last coordinate of the last point to evaluate. + */ + protected final int limitOfXY; + + /** + * Supplier of the message of the exception to throw if a point is outside the coverage bounds. + * The function argument is the index of the coordinate tuple which is outside the grid coverage. + * If this supplier is {@code null}, then null arrays will be returned instead of throwing an exception. + */ + protected final IntFunction<String> ifOutside; + + /** + * Creates a new iterator which will traverses a subset of the given grid coordinates. + * Subclasses should initialize {@link #indexOfXY} to the index of the first valid coordinate. + * + * @param nearestXY grid coordinates of points to evaluate, or {@code null}. + * @param limitOfXY index after the last coordinate of the last point to evaluate. + * @param ifOutside supplier of exception message for points outside the coverage bounds, or {@code null}. + */ + protected ValuesAtPointIterator(final long[] nearestXY, final int limitOfXY, final IntFunction<String> ifOutside) { + this.nearestXY = nearestXY; + this.limitOfXY = limitOfXY; + this.ifOutside = ifOutside; + } + + /** + * Creates a new iterator with the given grid coordinates. The {@code gridCoords} array is the result + * of applying the inverse of the "grid to CRS" transform on user-supplied "real world" coordinates, + * then resolving wraparounds. This constructor rounds these grid coordinates to nearest integers. + * + * @param coverage the coverage which will be evaluated. + * @param gridCoords the grid coordinates as floating-point numbers. + * @param numPoints number of points in the array. + * @param ifOutside supplier of exception message for points outside the coverage bounds, or {@code null}. + * @return the iterator. + */ + static ValuesAtPointIterator create(final GridCoverage coverage, final double[] gridCoords, int numPoints, + final IntFunction<String> ifOutside) + { + return Slices.create(coverage, gridCoords, 0, numPoints, ifOutside).shortcut(); + } + + /** + * Returns the number of remaining points to evaluate. + */ + @Override + public final long estimateSize() { + return (limitOfXY - indexOfXY) / BIDIMENSIONAL; + } + + /** + * Returns the number of remaining points to evaluate. + */ + @Override + public final long getExactSizeIfKnown() { + return estimateSize(); + } + + /** + * Returns the characteristics of this iterator. + * The iterator is {@link #SIZED} and {@link #ORDERED}. + * Whether it is {@link #NONNULL} depends on whether there is an {@link #ifOutside} supplier. + */ + @Override + public final int characteristics() { + return (ifOutside == null) ? (SIZED | SUBSIZED | ORDERED) : (SIZED | SUBSIZED | ORDERED | NONNULL); + } + + /** + * Returns a string representation of this iterator for debugging purposes. + */ + @Override + public String toString() { + return Strings.toString(getClass(), "estimateSize", estimateSize(), "nullIfOutside", ifOutside == null); + } + + + + + /** + * Base class of iterators that contains other iterators. + * A {@link Slices} is a group of two-dimensional images. + * An {@link Image} is a group of tiles. + */ + private static abstract class Group extends ValuesAtPointIterator { + /** + * Index of the first grid coordinate of the first point which is inside each child. + * Each value in this array is the index of the first valid element of {@link #nearestXY}. + * Values in this array shall be in strictly increasing order. + */ + private final int[] firstGridCoordOfChildren; + + /** + * Index of the next child in which to evaluate a sequence of points. + * This is an index in the {@link #firstGridCoordOfChildren} array. + */ + private int nextChildIndex; + + /** + * Maximal value (exclusive) of {@link #nextChildIndex}. + */ + private final int upperChildIndex; + + /** + * Iterator over pixel values of the current child. + * This is an instance of {@link Image}, {@link Tile} or {@link Null}. + * This field become {@code null} when the iteration is finished. + */ + protected ValuesAtPointIterator current; + + /** + * Creates a new iterator over a subset of the given iterator. + * This iterator will evaluate a prefix of the sequence of points of the given iterator. + * Caller should change the parent iterator to a suffix after this constructor returned. + * This constructor is for {@link #trySplit()} implementation only. + * + * @param parent the iterator from which to create a prefix. + * @param upperChildIndex index after the last child that this iterator can use. + */ + protected Group(final Group parent, final int upperChildIndex) { + super(parent.nearestXY, parent.firstGridCoordOfChildren[upperChildIndex], parent.ifOutside); + this.indexOfXY = parent.indexOfXY; + this.firstGridCoordOfChildren = parent.firstGridCoordOfChildren; + this.current = parent.current; + this.nextChildIndex = parent.nextChildIndex; + this.upperChildIndex = upperChildIndex; + } + + /** + * Creates a new iterator which will traverse a subset of the given grid coordinates. + * + * @param nearestXY grid coordinates of points to evaluate, or {@code null}. + * @param limitOfXY index after the last coordinate of the last point to evaluate. + * @param ifOutside supplier of exception message for points outside the coverage bounds, or {@code null}. + */ + protected Group(final long[] nearestXY, + final int limitOfXY, + final int[] firstGridCoordOfChildren, + final int upperChildIndex, + final IntFunction<String> ifOutside) + { + super(nearestXY, limitOfXY, ifOutside); + this.firstGridCoordOfChildren = firstGridCoordOfChildren; + this.upperChildIndex = upperChildIndex; + } + + /** + * If this iterator can be replaced by a more direct one, returns the more direct iterator. + * Otherwise returns {@code this} if there are more points to traverse. + * May return {@code null} if the iteration is finished. + */ + protected final ValuesAtPointIterator shortcut() { + return (nextChildIndex >= upperChildIndex) ? current : this; + } + + /** + * Returns an iterator over the points to evaluate in the next child, or {@code null} if none. + * Invoking this method may cause the loading or the computation of a tile. + * + * @throws PointOutsideCoverageException if a point is outside the grid coverage + * and the iterator is not configured for returning {@code null} in such case. + */ + protected final ValuesAtPointIterator nextChild() { + int childIndex = nextChildIndex; + if (childIndex < upperChildIndex) { + indexOfXY = firstGridCoordOfChildren[childIndex]; + int stopAtXY = (++nextChildIndex < upperChildIndex) ? firstGridCoordOfChildren[nextChildIndex] : limitOfXY; + return createChild(childIndex, stopAtXY); + } + return null; + } + + /** + * Creates a child which will iterate over the {@link #nearestXY} coordinates + * from {@link #indexOfXY} inclusive to {@code stopAtXY} exclusive. + * Invoking this method may cause the loading or the computation of a tile. + * + * @param childIndex index of the child to create. + * @param stopAtXY index after the last {@link #nearestXY} value to use. + * @throws PointOutsideCoverageException if a point is outside the grid coverage + * and the iterator is not configured for returning {@code null} in such case. + */ + abstract ValuesAtPointIterator createChild(int childIndex, int stopAtXY); + + /** + * Creates a new iterator covering a prefix of the points. + * + * @param stopAtXY index after the last grid coordinates of the last point covered by the prefix + * @return a new iterator covering the specified prefix of the points. + */ + abstract Group createPrefix(int stopAtXY); + + /** + * Tries to split this iterator. If successful, the returned iterator is a prefix + * of the sequence of points to evaluate, and this iterator become the remaining. + * + * @return an iterator covering a prefix of the points, or {@code null} if this iterator cannot be split. + */ + @Override + public final Spliterator<double[]> trySplit() { + if (current == null) { + return null; + } + // Find the middle of the remaining number of points to evaluate. + int i = nextChildIndex + (upperChildIndex - nextChildIndex) / 2; + i = Arrays.binarySearch(firstGridCoordOfChildren, nextChildIndex, upperChildIndex, i); + if (i < 0) i = ~i; // Tild operator, not minus. It gives the insertion point. + if (i > nextChildIndex && i < upperChildIndex) { + final var prefix = createPrefix(i); + nextChildIndex = prefix.upperChildIndex; + indexOfXY = prefix.limitOfXY; + current = nextChild(); + return prefix; + } else { + return current.trySplit(); // After this call, `current` become a suffix. + } + } + + /** + * Sends the sample values to the caller if the iteration is not finished. + */ + @Override + public final boolean tryAdvance(final Consumer<? super double[]> action) { + while (current != null) { + if (current.tryAdvance(action)) { + return true; + } + current = nextChild(); + } + return false; + } + + /** + * Sends remaining sample values to the caller until the end of the iteration. + */ + @Override + public final void forEachRemaining(final Consumer<? super double[]> action) { + while (current != null) { + current.forEachRemaining(action); + current = nextChild(); + } + } + } + + + + + /** + * An iterator over the sample values of points contained in different slices of a <var>n</var>-dimensional cube. + * This class searches for sequences of consecutive points that are located on the same two-dimensional slice, + * then creates an {@link Image} iterator for each slice. + */ + private static final class Slices extends Group { + /** + * The coverage from which to evaluate the pixel values. + */ + private final GridCoverage coverage; + + /** + * Indices of images in which sequences of points will be evaluated. + * Some array elements may be {@code null} if a sequence of points is + * outside the grid coverage or has NaN coordinate values. + */ + private final GridExtent[] imageExtents; + + /** + * Creates a new iterator over a subset of the given iterator. + * This iterator will evaluate a prefix of the sequence of points of the given iterator. + * Caller should change the parent iterator to a suffix after this constructor returned. + * This constructor is for {@link #trySplit()} implementation only. + * + * @param parent the iterator from which to create a prefix. + * @param stopAt index after the last image that this iterator can use. + */ + private Slices(final Slices parent, final int stopAt) { + super(parent, stopAt); + this.coverage = parent.coverage; + this.imageExtents = parent.imageExtents; + } + + /** + * Workaround for RFE #4093999 ("Relax constraint on placement of this()/super() call in constructors"). + */ + private Slices(final GridCoverage coverage, + final long[] nearestXY, + final int limitOfXY, + final int[] firstGridCoordOfChildren, + final int upperChildIndex, + final GridExtent[] imageExtents, + final IntFunction<String> ifOutside) + { + super(nearestXY, limitOfXY, firstGridCoordOfChildren, upperChildIndex, ifOutside); + this.coverage = coverage; + this.imageExtents = imageExtents; + current = nextChild(); + } + + /** + * Creates a new iterator with the given grid coordinates. The {@code gridCoords} array is the result + * of applying the inverse of the "grid to CRS" transform on user-supplied "real world" coordinates, + * then resolving wraparounds. This constructor rounds these grid coordinates to nearest integers. + * + * @todo Retrofit in above constructor after RFE #4093999. + * + * @param coverage the coverage which will be evaluated. + * @param gridCoords the grid coordinates as floating-point numbers. + * @param gridCoordsOffset index of the first grid coordinate value. + * @param numPoints number of points in the array. + * @param ifOutside supplier of exception message for points outside the coverage bounds, or {@code null}. + */ + static Slices create(final GridCoverage coverage, final double[] gridCoords, int gridCoordsOffset, int numPoints, + final IntFunction<String> ifOutside) + { + final int dimension = coverage.gridGeometry.getDimension(); + final var extentLow = new long[dimension]; + final var extentHigh = new long[dimension]; + final var nearestXY = new long[numPoints * BIDIMENSIONAL]; + var imageExtents = new GridExtent[1]; // Length is 1 in the common case of two-dimensional data. + var imageFirstCoords = new int[1]; + int imageCount = 0; + int indexOfXY = 0; + int limitOfXY = 0; + while (--numPoints >= 0) { + boolean wasOutside = false; + for (int i=0; i<dimension; i++) { + double c = gridCoords[gridCoordsOffset + i]; + wasOutside |= !(c >= DOMAIN_MINIMUM && c <= DOMAIN_MAXIMUM); // Use `!` for catching NaN. + extentLow[i] = Math.round(c); + } + if (wasOutside && ifOutside != null) { + throw new PointOutsideCoverageException(ifOutside.apply(indexOfXY / BIDIMENSIONAL)); + } + long lowerX, upperX, lowerY, upperY; + nearestXY[limitOfXY++] = lowerX = upperX = extentLow[X_DIMENSION]; + nearestXY[limitOfXY++] = lowerY = upperY = extentLow[Y_DIMENSION]; + gridCoordsOffset += dimension; +changeOfSlice: while (numPoints != 0) { + boolean isValid = true; + // Note: following code assumes that X_DIMENSION and Y_DIMENSION are the two first dimensions. + for (int i = BIDIMENSIONAL; i < dimension; i++) { + double c = gridCoords[gridCoordsOffset + i]; + isValid &= (c >= DOMAIN_MINIMUM && c <= DOMAIN_MAXIMUM); + if (Math.round(c) != extentLow[i]) { + break changeOfSlice; + } + } + final double cx = gridCoords[gridCoordsOffset + X_DIMENSION]; + final double cy = gridCoords[gridCoordsOffset + Y_DIMENSION]; + isValid &= (cx >= DOMAIN_MINIMUM && cx <= DOMAIN_MAXIMUM) && + (cy >= DOMAIN_MINIMUM && cy <= DOMAIN_MAXIMUM); + if (isValid == wasOutside) { + break; + } + final long x = Math.round(cx); + final long y = Math.round(cy); + final long xmin = Math.min(x, lowerX); + final long ymin = Math.min(y, lowerY); + final long xmax = Math.max(x, upperX); + final long ymax = Math.max(y, upperY); + if ((((xmax - xmin) | (ymax - ymin)) & MAXIMUM_SLICE_SIZE_MASK) != 0) { + break; + } + lowerX = xmin; + lowerY = ymin; + upperX = xmax; + upperY = ymax; + nearestXY[limitOfXY++] = x; + nearestXY[limitOfXY++] = y; + gridCoordsOffset += dimension; + numPoints--; + } + /* + * Reached the end of a sequence of points in the same image. We will read later a rectangular + * region containing all these points. We do not try to merge with an hypothetic reuse of this + * image later in the iteration, because it would increase the risk that we load too much data + * if the points are spread in distant regions. Instead, we rely on `ComputedImage` cache. + */ + if (imageCount >= imageExtents.length) { + imageExtents = Arrays.copyOf(imageExtents, imageExtents.length * 2); + imageFirstCoords = Arrays.copyOf(imageFirstCoords, imageFirstCoords.length * 2); + } + if (wasOutside) { + // Leave `imageExtents[imageCount]` to null. + imageFirstCoords[imageCount++] = indexOfXY; + indexOfXY = limitOfXY; + continue; + } + System.arraycopy(extentLow, 0, extentHigh, 0, extentHigh.length); + extentLow [X_DIMENSION] = lowerX; + extentLow [Y_DIMENSION] = lowerY; + extentHigh[X_DIMENSION] = upperX; + extentHigh[Y_DIMENSION] = upperY; + imageExtents[imageCount] = new GridExtent(null, extentLow, extentHigh, true); + imageFirstCoords[imageCount++] = indexOfXY; + + // Make grid coordinates relative to the region that we requested. + while (indexOfXY < limitOfXY) { + nearestXY[indexOfXY++] -= lowerX; + nearestXY[indexOfXY++] -= lowerY; + } + } + return new Slices(coverage, nearestXY, limitOfXY, imageFirstCoords, imageCount, imageExtents, ifOutside); + } + + /** + * Creates a child which will iterate over the {@link #nearestXY} coordinates + * from {@link #indexOfXY} inclusive to {@code stopAtXY} exclusive. + */ + @Override + final ValuesAtPointIterator createChild(final int childIndex, final int stopAtXY) { + final GridExtent extent = imageExtents[childIndex]; + if (extent != null) try { + return Image.create(this, stopAtXY, coverage.render(extent)).shortcut(); + } catch (DisjointExtentException cause) { + if (ifOutside != null) { + var e = new PointOutsideCoverageException(ifOutside.apply(indexOfXY / BIDIMENSIONAL)); + e.initCause(cause); + throw e; + } + } + return new Null(indexOfXY, stopAtXY); + } + + /** + * Creates a new iterator covering a prefix of the points. + * This is invoked by {@link #trySplit()} implementation. + */ + @Override + final Group createPrefix(int stopAtXY) { + return new Slices(this, stopAtXY); + } + } + + + + + /** + * An iterator over the sample values of point contained in the same image. + * For performance reasons, this method recycles the same array in calls to + * {@link Consumer#accept(Object)}. + */ + private static final class Image extends Group { + /** + * The image from which to evaluate the pixel values. + */ + private final RenderedImage image; + + /** + * Indices of tiles in which sequences of points will be evaluated. + * The values in this array are (<var>x</var>, <var>y</var>) pairs. + * Therefore, the length of this array is at least twice the number + * of tiles where to get values. + */ + private final int[] tileIndices; + + /** + * Whether a tile at a given index does not exist. It happens when a sequence of points + * is outside the image and the iterator is allowed to return {@code null} in such cases. + * The corresponding values of {@link #tileIndices} are meaningless and should be ignored. + */ + private final BitSet tileIsAbsent; + + /** + * Creates a new iterator over a subset of the given iterator. + * This iterator will evaluate a prefix of the sequence of points of the given iterator. + * Caller should change the parent iterator to a suffix after this constructor returned. + * This constructor is for {@link #trySplit()} implementation only. + * + * @param parent the iterator from which to create a prefix. + * @param stopAt index after the last tile that this iterator can use. + */ + private Image(final Image parent, final int stopAt) { + super(parent, stopAt); + this.image = parent.image; + this.tileIndices = parent.tileIndices; + this.tileIsAbsent = parent.tileIsAbsent; + } + + /** + * Workaround for RFE #4093999 ("Relax constraint on placement of this()/super() call in constructors"). + */ + private Image(final RenderedImage image, + final long[] nearestXY, + final int limitOfXY, + final int[] firstGridCoordOfChildren, + final int upperChildIndex, + final int[] tileIndices, + final BitSet tileIsAbsent, + final IntFunction<String> ifOutside) + { + super(nearestXY, limitOfXY, firstGridCoordOfChildren, upperChildIndex, ifOutside); + this.image = image; + this.tileIndices = tileIndices; + this.tileIsAbsent = tileIsAbsent; + current = nextChild(); + } + + /** + * Creates a new iterator for the specified image. + * + * @todo Retrofit in above constructor after RFE #4093999. + * + * @param parent the parent iterator from which to inherit the grid coordinates. + * @param limitOfXY index after the last coordinate of the last point to evaluate. + * @param image the image from which to get the sample values. + */ + static Image create(final ValuesAtPointIterator parent, final int limitOfXY, final RenderedImage image) { + final long xmin = image.getMinX(); + final long ymin = image.getMinY(); + final long xmax = image.getWidth() + xmin; + final long ymax = image.getHeight() + ymin; + final long tileWidth = image.getTileWidth(); + final long tileHeight = image.getTileHeight(); + final long tileGridXOffset = image.getTileGridXOffset(); + final long tileGridYOffset = image.getTileGridYOffset(); + int[] tileIndices = new int[BIDIMENSIONAL]; + int[] tileFirstCoords = new int[1]; + final var tileIsAbsent = new BitSet(); + final long[] nearestXY = parent.nearestXY; + int tileCount, indexOfXY = parent.indexOfXY; +nextTile: for (tileCount = 0; indexOfXY < limitOfXY; tileCount++) { + if (tileCount >= tileFirstCoords.length) { + tileFirstCoords = Arrays.copyOf(tileFirstCoords, tileFirstCoords.length * 2); + tileIndices = Arrays.copyOf(tileIndices, tileIndices.length * 2); + } + tileFirstCoords[tileCount] = indexOfXY; + boolean wasOutside = false; + do { + long x = nearestXY[indexOfXY++]; + long y = nearestXY[indexOfXY++]; + if (x >= xmin && x < xmax && y >= ymin && y < ymax) { + if (wasOutside) { + indexOfXY -= BIDIMENSIONAL; // Push back those valid coordinates. + break; + } + final long tileX = Math.floorDiv(x - tileGridXOffset, tileWidth); + final long tileY = Math.floorDiv(y - tileGridYOffset, tileHeight); + final long tileXMin = Math.max(xmin, tileX * tileWidth + tileGridXOffset); + final long tileYMin = Math.max(ymin, tileY * tileHeight + tileGridYOffset); + final long tileXMax = Math.min(xmax, (tileX+1) * tileWidth + tileGridXOffset); + final long tileYMax = Math.min(ymax, (tileY+1) * tileHeight + tileGridYOffset); + while (indexOfXY < limitOfXY) { + x = nearestXY[indexOfXY++]; + y = nearestXY[indexOfXY++]; + if (x < tileXMin || x >= tileXMax || y < tileYMin || y >= tileYMax) { + indexOfXY -= BIDIMENSIONAL; // Push back those invalid coordinates. + break; + } + } + final int i = tileCount * BIDIMENSIONAL; + tileIndices[i ] = Math.toIntExact(tileX); + tileIndices[i+1] = Math.toIntExact(tileY); + continue nextTile; + } + /* + * Found a point outside the image. By setting `wasOutside`, we change the behavior + * of this loop for searching the end of the sequence of points that are outside + * (instead of the end of the sequence of points that are on the same tile). + */ + if (parent.ifOutside != null) { + throw new PointOutsideCoverageException(parent.ifOutside.apply(indexOfXY / BIDIMENSIONAL)); + } + wasOutside = true; + } while (indexOfXY < limitOfXY); + tileIsAbsent.set(tileCount); + } + return new Image(image, nearestXY, limitOfXY, tileFirstCoords, tileCount, tileIndices, tileIsAbsent, parent.ifOutside); + } + + /** + * Creates a child which will iterate over the {@link #nearestXY} coordinates + * from {@link #indexOfXY} inclusive to {@code stopAtXY} exclusive. + */ + @Override + final ValuesAtPointIterator createChild(int i, final int stopAtXY) { + if (tileIsAbsent.get(i)) { + return new Null(indexOfXY, stopAtXY); + } + i *= BIDIMENSIONAL; + final int tileX = tileIndices[i]; + final int tileY = tileIndices[i+1]; + return new Tile(this, indexOfXY, stopAtXY, image.getTile(tileX, tileY)); + } + + /** + * Creates a new iterator covering a prefix of the points. + * This is invoked by {@link #trySplit()} implementation. + */ + @Override + final Group createPrefix(int stopAtXY) { + return new Image(this, stopAtXY); + } + } + + + + + /** + * An iterator over the sample values of point contained in the same tile. + * For performance reasons, this method recycles the same array in calls to + * {@link Consumer#accept(Object)}. + */ + private static final class Tile extends ValuesAtPointIterator { + /** + * The tile from which to get the sample values. + */ + private final Raster tile; + + /** + * The sample values. + */ + private double[] samples; + + /** + * Creates a new iterator for the specified tile. + * + * @param parent the parent iterator from which to inherit the grid coordinates. + * @param indexOfXY index of the first coordinate of the next point to evaluate. + * @param limitOfXY index after the last coordinate of the last point to evaluate. + * @param tile the tile from which to get the sample values. + */ + Tile(final ValuesAtPointIterator parent, final int indexOfXY, final int limitOfXY, final Raster tile) { + super(parent.nearestXY, limitOfXY, null); + this.indexOfXY = indexOfXY; + this.tile = tile; + } + + /** + * Splits this iterator if there is enough points (an arbitrary threshold) to make it worth. + * If successful, the returned iterator is the prefix of the sequence of points to evaluate, + * and this iterator become the suffix. + */ + @Override + public Spliterator<double[]> trySplit() { + final int start = indexOfXY; + final int half = ((limitOfXY - start) / 2) & ~1; // Must be even. + if (half >= 10) { // Arbitrary threshold. + return new Tile(this, start, indexOfXY += half, tile); // Prefix. + } + return null; + } + + /** + * Sends the sample values to the caller if the iteration is not finished. + * For performance reasons, this method recycles the same array in calls to {@code accept}. + */ + @Override + public boolean tryAdvance(final Consumer<? super double[]> action) { + if (indexOfXY < limitOfXY) { + final int x = Math.toIntExact(nearestXY[indexOfXY++]); + final int y = Math.toIntExact(nearestXY[indexOfXY++]); + action.accept(samples = tile.getPixel(x, y, samples)); + return true; + } + return false; + } + + /** + * Sends remaining sample values to the caller until the end of the iteration. + * For performance reasons, this method recycles the same array in calls to {@code accept}. + */ + @Override + public void forEachRemaining(final Consumer<? super double[]> action) { + while (indexOfXY < limitOfXY) { + final int x = Math.toIntExact(nearestXY[indexOfXY++]); + final int y = Math.toIntExact(nearestXY[indexOfXY++]); + action.accept(samples = tile.getPixel(x, y, samples)); + } + } + } + + + + + /** + * An iterator which returns only null values. + * This is used for a sequence of points outside the image. + */ + private static final class Null extends ValuesAtPointIterator { + /** + * Creates a new iterator of null elements. + * + * @param indexOfXY index of the first coordinate of the next point to evaluate. + * @param limitOfXY index after the last coordinate of the last point to evaluate. + */ + Null(final int indexOfXY, final int limitOfXY) { + super(null, limitOfXY, null); + this.indexOfXY = indexOfXY; + } + + /** + * Do not split this iterator as the caller would usually do nothing anyway. + * It does not seem worth to let this class be parallelized. + */ + @Override + public Spliterator<double[]> trySplit() { + return null; + } + + /** + * Sends a {@code null} array to the caller if the iteration is not finished. + */ + @Override + public boolean tryAdvance(final Consumer<? super double[]> action) { + if (indexOfXY < limitOfXY) { + indexOfXY += BIDIMENSIONAL; + action.accept(null); + return true; + } + return false; + } + + /** + * Sends {@code null} arrays to the caller until the end of the iteration. + */ + @Override + public void forEachRemaining(final Consumer<? super double[]> action) { + while (indexOfXY < limitOfXY) { + indexOfXY += BIDIMENSIONAL; + action.accept(null); + } + } + } +} diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java index 2b38525bf8..145b08cb89 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.java @@ -180,8 +180,8 @@ public class Resources extends IndexedResourceBundle { public static final short EmptyTileOrImageRegion = 20; /** - * Indices ({3}) are outside grid coverage. The value in dimension {0} shall be between - * {1,number} and {2,number} inclusive. + * Grid coordinates ({3}) are outside the coverage domain. The coordinate in the {0} dimension + * shall be between {1,number} and {2,number} inclusive. */ public static final short GridCoordinateOutsideCoverage_4 = 21; diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties index 3fc96f2d07..1dda1a0cd7 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources.properties @@ -44,7 +44,7 @@ DependencyNotFound_3 = Operation \u201c{0}\u201d requires a \u201c{ DuplicatedSampleDimensionIndex_1 = Sample dimension index {0} is duplicated. EmptyImage = Image has zero pixel. EmptyTileOrImageRegion = Empty tile or image region. -GridCoordinateOutsideCoverage_4 = Indices ({3}) are outside grid coverage. The value in dimension {0} shall be between {1,number} and {2,number} inclusive. +GridCoordinateOutsideCoverage_4 = Grid coordinates ({3}) are outside the coverage domain. The coordinate in the {0} dimension shall be between {1,number} and {2,number} inclusive. GridEnvelopeMustBeNDimensional_1 = The grid envelope must have at least {0} dimensions. GridExtentsAreDisjoint_5 = The specified grid extent is outside the domain. Indices [{3,number} \u2026 {4,number}] specified in dimension {0} do not intersect the [{1,number} \u2026 {2,number}] grid extent. IllegalCategoryRange_2 = Sample value range {1} for \u201c{0}\u201d category is illegal. diff --git a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties index 5610f695c7..356619b3a6 100644 --- a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/internal/Resources_fr.properties @@ -49,7 +49,7 @@ DependencyNotFound_3 = L\u2019op\u00e9ration \u00ab\u202f{0}\u202f\ DuplicatedSampleDimensionIndex_1 = L\u2019index de dimension d\u2019\u00e9chantillonnage {0} est r\u00e9p\u00e9t\u00e9. EmptyImage = L\u2019image a z\u00e9ro pixel. EmptyTileOrImageRegion = La tuile ou la r\u00e9gion de l\u2019image est vide. -GridCoordinateOutsideCoverage_4 = Les indices ({3}) sont en dehors du domaine de la grille. La valeur dans la dimension {0} doit \u00eatre entre {1,number} et {2,number} inclusivement. +GridCoordinateOutsideCoverage_4 = Les coordonn\u00e9es de grille ({3}) sont en dehors du domaine de la grille. La coordonn\u00e9e de la dimension {0} doit \u00eatre entre {1,number} et {2,number} inclusivement. GridEnvelopeMustBeNDimensional_1 = L\u2019enveloppe de la grille doit avoir au moins {0} dimensions. GridExtentsAreDisjoint_5 = L\u2019\u00e9tendue de grille sp\u00e9cifi\u00e9e est en dehors du domaine. Les indices [{3,number} \u2026 {4,number}] qui ont \u00e9t\u00e9 sp\u00e9cifi\u00e9es dans la dimension {0} n\u2019interceptent pas l\u2019\u00e9tendue [{1,number} \u2026 {2,number}] de la grille. IllegalCategoryRange_2 = La plage de valeurs {1} pour la cat\u00e9gorie \u00ab\u202f{0}\u202f\u00bb est ill\u00e9gale. diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java new file mode 100644 index 0000000000..4d54bb30b6 --- /dev/null +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/DefaultEvaluatorTest.java @@ -0,0 +1,294 @@ +/* + * 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.Random; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; +import java.awt.image.DataBuffer; +import javax.measure.IncommensurableException; +import org.opengis.geometry.DirectPosition; +import org.opengis.referencing.cs.CoordinateSystem; +import org.opengis.referencing.operation.MathTransform; +import org.opengis.referencing.operation.TransformException; +import org.opengis.referencing.crs.CoordinateReferenceSystem; +import org.apache.sis.geometry.Envelope2D; +import org.apache.sis.geometry.GeneralDirectPosition; +import org.apache.sis.referencing.cs.CoordinateSystems; +import org.apache.sis.referencing.operation.transform.MathTransforms; + +// Test dependencies +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; +import org.apache.sis.test.TestCase; +import org.apache.sis.test.TestUtilities; +import org.apache.sis.image.TiledImageMock; +import org.apache.sis.referencing.crs.HardCodedCRS; +import org.junit.jupiter.api.TestInstance; + + +/** + * Tests {@link DefaultEvaluator}. + * + * @author Martin Desruisseaux (Geomatys) + */ +@SuppressWarnings("exports") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public final class DefaultEvaluatorTest extends TestCase { + /** + * The random number generator. + */ + private final Random random; + + /** + * Size of the image to test, in pixels. + * Used for computing expected sample values. + */ + private final int width, height; + + /** + * Size of tiles, in pixels. + * Used for computing expected sample values. + */ + private final int tileWidth, tileHeight; + + /** + * Number of tiles on the <var>x</var> axis. + */ + private final int numXTiles; + + /** + * The grid geometry of the coverage used as source data. + */ + private final GridGeometry gridGeometry; + + /** + * Number of bands. + */ + private final int numBands; + + /** + * The evaluator to test. + */ + private final GridCoverage.Evaluator evaluator; + + /** + * The expected values in the first band of each coordinate. + * The length of this array is the number of test points. + */ + private float[] expectedValues; + + /** + * Creates a new test case with a small grid coverage. + * The sample values will be 3 digits numbers of the form "BTYX" where: + * <ul> + * <li><var>B</var> is the band index starting with 1 for the first band.</li> + * <li><var>T</var> is the tile index starting with 1 for the first tile and increasing in a row-major fashion.</li> + * <li><var>Y</var> is the <var>y</var> coordinate (row 0-based index) of the sample value relative to current tile.</li> + * <li><var>X</var> is the <var>x</var> coordinate (column 0-based index) of the sample value relative to current tile.</li> + * </ul> + * + * Image size and tile size are computed in a way that keep each above-cited numbers smaller than 10. + */ + public DefaultEvaluatorTest() { + random = TestUtilities.createRandomNumberGenerator(); + numBands = random.nextInt(3) + 1; + tileWidth = random.nextInt(4) + 6; // Keep lower than 10. + tileHeight = random.nextInt(4) + 6; + width = tileWidth * (random.nextInt(3) + 1) - random.nextInt(4); + height = tileHeight * (random.nextInt(3) + 1) - random.nextInt(4); + final var image = new TiledImageMock( + DataBuffer.TYPE_USHORT, // data type + numBands, // number of values per pixel + random.nextInt(20) - 10, // minX + random.nextInt(20) - 10, // minY + width, height, // image size + tileWidth, tileHeight, // tile size + random.nextInt(20) - 10, // minTileX + random.nextInt(20) - 10, // minTileY + random.nextBoolean()); // banded or interleaved sample model + + image.initializeAllTiles(); + final int dx = random.nextInt(200) - 100; + final int dy = random.nextInt(200) - 100; + gridGeometry = new GridGeometry( + new GridExtent(null, new long[] {dx, dy}, new long[] {dx + width, dy + height}, false), + new Envelope2D(HardCodedCRS.WGS84, 20, -10, 40, 60), GridOrientation.HOMOTHETY); + evaluator = new GridCoverage2D(gridGeometry, null, image).evaluator(); + numXTiles = image.getNumXTiles(); + } + + /** + * Creates test points, together with the expected values. + * The expected values are set in the {@link #expectedValues} field. + * + * @param allowVariations whether to allow points outside the coverage domain and change of <abbr>CRS</abbr>. + * @return the test points. + * @throws TransformException if a test point cannot be computed. + */ + private List<DirectPosition> createTestPoints(final boolean allowVariations) throws TransformException { + final int numPoints = random.nextInt(30) + 20; + final var points = new GeneralDirectPosition[numPoints]; + final float lowerX = gridGeometry.extent.getLow(0); + final float lowerY = gridGeometry.extent.getLow(1); + final float[] gridCoords = new float[2]; + MathTransform gridToCRS = gridGeometry.gridToCRS; + CoordinateReferenceSystem crs = HardCodedCRS.WGS84; + final CoordinateSystem coverageCS = crs.getCoordinateSystem(); + expectedValues = new float[numPoints]; + for (int i=0; i<numPoints; i++) { + /* + * Randomly change the CRS if this change is allowed. + */ + if (allowVariations && random.nextInt(5) == 0) try { + if (random.nextBoolean()) { + crs = HardCodedCRS.WGS84_LATITUDE_FIRST; + var swap = CoordinateSystems.swapAndScaleAxes(coverageCS, crs.getCoordinateSystem()); + gridToCRS = MathTransforms.concatenate(gridGeometry.gridToCRS, MathTransforms.linear(swap)); + } else { + crs = HardCodedCRS.WGS84; + gridToCRS = gridGeometry.gridToCRS; + } + } catch (IncommensurableException e) { + throw new AssertionError(e); + } + final float expected; + if (allowVariations && random.nextInt(5) == 0) { + gridCoords[0] = lowerX + (random.nextBoolean() ? -10 : 10 + width); + gridCoords[1] = lowerY + (random.nextBoolean() ? -10 : 10 + height); + expected = Float.NaN; + } else { + final int x = random.nextInt(width); + final int y = random.nextInt(height); + final int tx = x / tileWidth; + final int ty = y / tileHeight; + gridCoords[0] = lowerX + x + 0.875f * (random.nextFloat() - 0.5f); + gridCoords[1] = lowerY + y + 0.875f * (random.nextFloat() - 0.5f); + expected = (x - tx * tileWidth) + + (y - ty * tileHeight) * 10 + + 100*(numXTiles*ty + tx + 1); + } + expectedValues[i] = expected; + final var point = new GeneralDirectPosition(crs); + gridToCRS.transform(gridCoords, 0, point.coordinates, 0, 1); + points[i] = point; + } + return Arrays.asList(points); + } + + /** + * Compares the actual sample values against the expected values. + * The given stream should compute sample values for the points + * returned by {@link #createTestPoints()}. + * + * @param stream the computed values. + */ + private void runAndCompare(final Stream<double[]> stream) { + final double[][] actual = stream.map((samples) -> (samples != null) ? samples.clone() : null).toArray(double[][]::new); + assertEquals(expectedValues.length, actual.length); + for (int i=0; i<actual.length; i++) { + double expected = expectedValues[i]; + final double[] samples = actual[i]; + if (samples != null) { + assertEquals(numBands, samples.length); + } + for (int band = 0; band < numBands; band++) { + assertEquals(expected += 1000, (samples != null) ? samples[band] : Double.NaN); + } + } + } + + /** + * Tests the {@code apply(DirectPosition)} method. + * Assuming that the {@link GridCoverage2DTest#testEvaluator()} test passes, + * this test verifies that the above test infrastructure works. + * + * @see GridCoverage2DTest#testEvaluator() + * @see GridCoverage2DTest#testEvaluatorWithWraparound() + * @throws TransformException if a test point cannot be computed. + */ + @Test + public void testApply() throws TransformException { + evaluator.setNullIfOutside(false); + assertFalse(evaluator.isNullIfOutside()); + runAndCompare(createTestPoints(false).stream().map(evaluator::apply)); + } + + /** + * Same as {@link #testApply()} but with points outside the grid coverage domain. + * Also tests input points with different <abbr>CRS</abbr>. + * + * @throws TransformException if a test point cannot be computed. + */ + @Test + public void testApplyWithPointOutside() throws TransformException { + evaluator.setNullIfOutside(true); + assertTrue(evaluator.isNullIfOutside()); + runAndCompare(createTestPoints(true).stream().map(evaluator::apply)); + } + + /** + * Tests with random points inside the coverage computed sequentially. + * + * @throws TransformException if a test point cannot be computed. + */ + @Test + public void testSequential() throws TransformException { + evaluator.setNullIfOutside(false); + assertFalse(evaluator.isNullIfOutside()); + runAndCompare(evaluator.stream(createTestPoints(false), false)); + } + + /** + * Same as {@link #testSequential()} but with points outside the grid coverage domain. + * Also tests input points with different <abbr>CRS</abbr>. + * + * @throws TransformException if a test point cannot be computed. + */ + @Test + public void testSequentialWithPointOutside() throws TransformException { + evaluator.setNullIfOutside(true); + assertTrue(evaluator.isNullIfOutside()); + runAndCompare(evaluator.stream(createTestPoints(true), false)); + } + + /** + * Tests with random points inside the coverage computed sequentially. + * + * @throws TransformException if a test point cannot be computed. + */ + @Test + public void testParallel() throws TransformException { + evaluator.setNullIfOutside(false); + assertFalse(evaluator.isNullIfOutside()); + runAndCompare(evaluator.stream(createTestPoints(false), true)); + } + + /** + * Same as {@link #testParallel()} but with points outside the grid coverage domain. + * Also tests input points with different <abbr>CRS</abbr>. + * + * @throws TransformException if a test point cannot be computed. + */ + @Test + public void testParallelWithPointOutside() throws TransformException { + evaluator.setNullIfOutside(true); + assertTrue(evaluator.isNullIfOutside()); + runAndCompare(evaluator.stream(createTestPoints(true), true)); + } +} diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java index 33b49c9e69..36e9b0dc5a 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/FractionalGridCoordinatesTest.java @@ -27,6 +27,7 @@ import org.apache.sis.test.TestCase; * * @author Martin Desruisseaux (Geomatys) */ +@SuppressWarnings("exports") public final class FractionalGridCoordinatesTest extends TestCase { /** * Creates a new test case. @@ -38,11 +39,7 @@ public final 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; + return new FractionalGridCoordinates(new double[] {4, -1.1, 7.6}); } /** @@ -61,6 +58,7 @@ public final class FractionalGridCoordinatesTest extends TestCase { * with default parameter values. */ @Test + @SuppressWarnings("removal") // TODO: make GridExtentTest.assertExtentEquals private. public void testToExtent() { final GridExtent extent = instance().toExtent(null); GridExtentTest.assertExtentEquals(extent, 0, 4, 4); @@ -72,6 +70,7 @@ public final class FractionalGridCoordinatesTest extends TestCase { * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a size of 1. */ @Test + @SuppressWarnings("removal") public void testToExtentSize1() { final GridExtent extent = instance().toExtent(null, 1, 1, 1); GridExtentTest.assertExtentEquals(extent, 0, 4, 4); @@ -83,6 +82,7 @@ public final class FractionalGridCoordinatesTest extends TestCase { * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a size greater than 2. */ @Test + @SuppressWarnings("removal") public void testToExtentSizeN() { final GridExtent extent = instance().toExtent(null, 3, 5, 4); GridExtentTest.assertExtentEquals(extent, 0, 3, 5); @@ -94,6 +94,7 @@ public final class FractionalGridCoordinatesTest extends TestCase { * Tests {@link FractionalGridCoordinates#toExtent(GridExtent, long...)} with a bounds constraint. */ @Test + @SuppressWarnings("removal") public void testToExtentBounded() { final GridExtent bounds = new GridExtent(null, new long[] {0, -1, 0}, new long[] {4, 2, 8}, true); final GridExtent extent = instance().toExtent(bounds, 3, 5, 4); diff --git a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java index d023ff510a..81b6477f2f 100644 --- a/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java +++ b/endorsed/src/org.apache.sis.feature/test/org/apache/sis/coverage/grid/GridCoverage2DTest.java @@ -91,7 +91,7 @@ public class GridCoverage2DTest extends TestCase { final var grid = new GridGeometry(new GridExtent(GRID_SIZE, GRID_SIZE), PixelInCell.CELL_CENTER, gridToCRS, HardCodedCRS.WGS84); - final MathTransform1D toUnits = (MathTransform1D) MathTransforms.linear(0.5, 100); + final var toUnits = (MathTransform1D) MathTransforms.linear(0.5, 100); final SampleDimension sd = new SampleDimension.Builder().setName("Some kind of height") .addQuantitative("data", NumberRange.create(-10, true, 10, true), toUnits, Units.METRE) .build(); @@ -182,7 +182,7 @@ public class GridCoverage2DTest extends TestCase { * * 70 = p * 0.5 + 100 → (70-100)/0.5 = p → p = -60 */ - final WritableRenderedImage image = (WritableRenderedImage) coverage.render(null); + final var image = (WritableRenderedImage) coverage.render(null); final WritableRaster raster = image.getWritableTile(0, 0); raster.setSample(0, 0, 0, 70); raster.setSample(1, 0, 0, 2.5); diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java index b40b9791a5..1e2b084de3 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/CompoundFormat.java @@ -105,7 +105,7 @@ import static org.apache.sis.util.internal.shared.Constants.UTC; * in case of error. * * @author Martin Desruisseaux (Geomatys) - * @version 1.5 + * @version 1.6 * * @param <T> the base type of objects parsed and formatted by this class. * @@ -349,15 +349,31 @@ public abstract class CompoundFormat<T> extends Format implements Localized { public abstract void format(T object, Appendable toAppendTo) throws IOException; /** - * Writes a textual representation of the specified object in the given buffer. - * This method delegates its work to {@link #format(Object, Appendable)}, but - * without propagating {@link IOException}. The I/O exception should never - * occur since we are writing in a {@link StringBuffer}. + * Writes a textual representation of the given object in the given buffer. + * This method delegates the work to {@link #format(Object, Appendable)}. + * If an {@link IOException} occurs (for example, because {@code format(…)} performs + * some I/O operations on other objects than the given {@link StringBuilder}), + * the exception is wrapped in {@link UncheckedIOException}. * - * <div class="note"><b>Note:</b> - * Strictly speaking, an {@link IOException} could still occur if a subclass overrides the above {@code format} - * method and performs some I/O operation outside the given {@link StringBuffer}. However, this is not the intended - * usage of this class and implementers should avoid such unexpected I/O operation.</div> + * @param object the object to format. + * @param toAppendTo where to format the object. + * + * @since 1.6 + */ + public void format(T object, StringBuilder toAppendTo) { + try { + format(object, (Appendable) toAppendTo); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /** + * Writes a textual representation of the specified object in the given buffer. + * This method delegates the work to {@link #format(Object, Appendable)}. + * If an {@link IOException} occurs (for example, because {@code format(…)} performs + * some I/O operations on other objects than the given {@link StringBuffer}), + * the exception is wrapped in {@link UncheckedIOException}. * * @param object the object to format. * @param toAppendTo where to format the object. @@ -373,9 +389,8 @@ public abstract class CompoundFormat<T> extends Format implements Localized { throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalClass_2, valueType, Classes.getClass(object)), e); } catch (IOException e) { /* - * Should never happen when writing into a StringBuffer, unless the user - * override the format(Object, Appendable) method. We do not rethrow an - * AssertionError because of this possibility. + * Should never happen when writing into a StringBuffer, unless the error + * results from another operation than writting in the given buffer. */ throw new UncheckedIOException(e); } diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java index 54ade7a38d..571953421a 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/io/package-info.java @@ -41,7 +41,7 @@ * Unicode supplementary characters}. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.5 + * @version 1.6 * @since 0.3 */ package org.apache.sis.io;
