You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2019/01/29 18:52:14 UTC
[sis] 03/03: Retrofit GridChange into GridDerivation.
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 68bb94cdbcec799cc96b7bd697a3f7f65ae8ce5d
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Tue Jan 29 19:32:59 2019 +0100
Retrofit GridChange into GridDerivation.
---
.../org/apache/sis/coverage/grid/GridChange.java | 498 --------------------
.../apache/sis/coverage/grid/GridDerivation.java | 499 ++++++++++++++++++---
.../org/apache/sis/coverage/grid/GridExtent.java | 2 +-
.../apache/sis/coverage/grid/GridChangeTest.java | 104 -----
.../sis/coverage/grid/GridDerivationTest.java | 60 ++-
.../org/apache/sis/test/suite/RasterTestSuite.java | 1 -
.../apache/sis/storage/netcdf/GridResource.java | 18 +-
7 files changed, 515 insertions(+), 667 deletions(-)
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
deleted file mode 100644
index c2deef2..0000000
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridChange.java
+++ /dev/null
@@ -1,498 +0,0 @@
-/*
- * 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.Locale;
-import java.io.Serializable;
-import org.opengis.util.FactoryException;
-import org.opengis.referencing.datum.PixelInCell;
-import org.opengis.referencing.operation.Matrix;
-import org.opengis.referencing.operation.MathTransform;
-import org.opengis.referencing.operation.CoordinateOperation;
-import org.opengis.referencing.operation.TransformException;
-import org.opengis.referencing.operation.NoninvertibleTransformException;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
-import org.apache.sis.referencing.operation.matrix.Matrices;
-import org.apache.sis.geometry.Envelopes;
-import org.apache.sis.util.collection.DefaultTreeTable;
-import org.apache.sis.util.collection.TableColumn;
-import org.apache.sis.util.collection.TreeTable;
-import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.util.resources.Errors;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.CharSequences;
-import org.apache.sis.util.Classes;
-import org.apache.sis.util.Debug;
-
-
-/**
- * Information about the conversion from one grid geometry to another grid geometry.
- * This class holds the {@link MathTransform}s converting cell coordinates from the
- * source grid to cell coordinates in the target grid, together with the grid extent
- * that results from this conversion.
- *
- * <div class="note"><b>Usage:</b>
- * This class can be helpful for implementation of
- * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, int...)}.
- * Example:
- *
- * {@preformat java
- * class MyDataStorage extends GridCoverageResource {
- * @Override
- * public GridCoverage read(GridGeometry domain, int... range) throws DataStoreException {
- * GridChange change = new GridChange(domain, getGridGeometry());
- * GridExtent toRead = change.getTargetExtent();
- * int[] subsampling = change.getTargetSubsamplings());
- * // Do reading here.
- * }
- * }
- * }
- * </div>
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-public class GridChange implements Serializable {
- /**
- * For cross-version compatibility.
- */
- private static final long serialVersionUID = -7819047186271172885L;
-
- /**
- * The conversions from source grid to target grid, mapping cell corners or cell centers.
- */
- private final MathTransform mapCorners, mapCenters;
-
- /**
- * Intersection of the two grid geometry extents, in units of the target grid geometry.
- * This is the expected ranges after conversions from source grid coordinates to target
- * grid coordinates, clipped to target extent and ignoring {@linkplain #scales}.
- *
- * @see #getTargetExtent()
- */
- private final GridExtent targetExtent;
-
- /**
- * The target grid geometry specified to the constructor. The extent of that geometry
- * is not necessarily the {@link #targetExtent}.
- *
- * @see #getTargetGeometry(int...)
- */
- private final GridGeometry givenGeometry;
-
- /**
- * An estimation of the multiplication factors when converting cell coordinates from source
- * grid to target grid. Those factors appear in the order of <em>target</em> grid axes.
- * May be {@code null} if the conversion is identity.
- *
- * @see #getTargetSubsamplings()
- */
- private final double[] scales;
-
- /**
- * Creates a description of the conversion from given source grid geometry to given target grid geometry.
- * The following information are mandatory:
- * <ul>
- * <li>Source {@linkplain GridGeometry#getExtent() extent}.</li>
- * <li>Source "{@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS}" conversion.</li>
- * <li>Target "grid to CRS" conversion.</li>
- * </ul>
- *
- * The following information are optional but recommended:
- * <ul>
- * <li>Source coordinate reference system.</li>
- * <li>Target {@linkplain GridGeometry#getCoordinateReferenceSystem() coordinate reference system}.</li>
- * <li>Target {@linkplain GridGeometry#getExtent() extent}.</li>
- * </ul>
- *
- * This constructor assumes {@link GridRoundingMode#ENCLOSING} and no margin.
- *
- * @param source the source grid geometry.
- * @param target the target grid geometry.
- * @throws IncompleteGridGeometryException if a "grid to CRS" conversion is not defined,
- * or if the {@code source} does not specify an {@linkplain GridGeometry#getExtent() extent}.
- * @throws TransformException if an error occurred during conversion from source to target.
- */
- public GridChange(final GridGeometry source, final GridGeometry target) throws TransformException {
- this(source, target, GridRoundingMode.ENCLOSING, null);
- }
-
- /**
- * Creates a description of the conversion from given source grid geometry to given target grid geometry.
- * The following information are mandatory:
- * <ul>
- * <li>Source {@linkplain GridGeometry#getExtent() extent}.</li>
- * <li>Source "{@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS}" conversion.</li>
- * <li>Target "grid to CRS" conversion.</li>
- * </ul>
- *
- * The following information are optional but recommended:
- * <ul>
- * <li>Source coordinate reference system.</li>
- * <li>Target {@linkplain GridGeometry#getCoordinateReferenceSystem() coordinate reference system}.</li>
- * <li>Target {@linkplain GridGeometry#getExtent() extent}.</li>
- * </ul>
- *
- * An optional {@code margin} can be specified for increasing the size of the grid extent computed by this constructor.
- * For example if the caller wants to apply bilinear interpolations in an image, it will need 1 more pixel on each
- * image border. If the caller wants to apply bi-cubic interpolations, it will need 2 more pixels on each image border.
- * If the {@code margin} array length is shorter than the target dimension, then zero is assumed for all missing dimensions.
- *
- * @param source the source grid geometry.
- * @param target the target grid geometry.
- * @param rounding controls behavior of rounding from floating point values to integers.
- * @param margin if non-null, expand the extent by that amount of cells on each target dimension.
- * @throws IncompleteGridGeometryException if a "grid to CRS" conversion is not defined,
- * or if the {@code source} does not specify an {@linkplain GridGeometry#getExtent() extent}.
- * @throws TransformException if an error occurred during conversion from source to target.
- */
- public GridChange(final GridGeometry source, final GridGeometry target, final GridRoundingMode rounding, final int... margin)
- throws TransformException
- {
- ArgumentChecks.ensureNonNull("source", source);
- ArgumentChecks.ensureNonNull("target", target);
- ArgumentChecks.ensureNonNull("rounding", rounding);
- givenGeometry = target;
- if (target.equals(source)) {
- // Optimization for a common case.
- mapCorners = mapCenters = MathTransforms.identity(target.getDimension());
- targetExtent = target.getExtent(); // May throw IncompleteGridGeometryException.
- } else {
- final CoordinateOperation crsChange;
- try {
- crsChange = Envelopes.findOperation(source.envelope, target.envelope);
- } catch (FactoryException e) {
- throw new TransformException(Errors.format(Errors.Keys.CanNotTransformEnvelope), e);
- }
- final GridExtent domain = source.getExtent(); // May throw IncompleteGridGeometryException.
- mapCorners = path(source, crsChange, target, PixelInCell.CELL_CORNER);
- mapCenters = path(source, crsChange, target, PixelInCell.CELL_CENTER);
- final GridExtent extent = new GridExtent(domain.toCRS(mapCorners, mapCenters), rounding, margin, target.extent, null);
- targetExtent = extent.equals(target.extent) ? target.extent : extent.equals(domain) ? domain : extent;
- /*
- * Get an estimation of the scale factors when converting from source to target.
- * If all scale factors are 1, we will not store the array for consistency with
- * above block for identity case.
- */
- final double[] resolution = GridGeometry.resolution(mapCenters, domain);
- for (int i=resolution.length; --i >= 0;) {
- if (resolution[i] != 1) {
- scales = resolution;
- return;
- }
- }
- }
- scales = null;
- }
-
- /**
- * Returns the concatenation of all transformation steps from the given source to the given target.
- *
- * @param source the source grid geometry.
- * @param crsChange the change of coordinate reference system, or {@code null} if none.
- * @param target the target grid geometry.
- * @param anchor whether we want the transform for cell corner or cell center.
- */
- private static MathTransform path(final GridGeometry source, final CoordinateOperation crsChange,
- final GridGeometry target, final PixelInCell anchor) throws NoninvertibleTransformException
- {
- MathTransform step1 = source.getGridToCRS(anchor);
- MathTransform step2 = target.getGridToCRS(anchor);
- if (crsChange != null) {
- step1 = MathTransforms.concatenate(step1, crsChange.getMathTransform());
- }
- if (step1.equals(step2)) { // Optimization for a common case.
- return MathTransforms.identity(step1.getSourceDimensions());
- } else {
- return MathTransforms.concatenate(step1, step2.inverse());
- }
- }
-
- /**
- * Returns an <em>estimation</em> of the scale factor when converting source grid coordinates
- * to target grid coordinates. This is for information purpose only since this method combines
- * potentially different scale factors for all dimensions.
- *
- * @return an <em>estimation</em> of the scale factor for all dimensions.
- */
- public double getGlobalScale() {
- if (scales != null) {
- double sum = 0;
- int count = 0;
- for (final double value : scales) {
- if (Double.isFinite(value)) {
- sum += value;
- count++;
- }
- }
- if (count != 0) {
- return sum / count;
- }
- }
- return 1;
- }
-
- /**
- * Returns the conversion from source grid coordinates to target grid coordinates.
- * The {@code anchor} argument specifies whether the conversion maps cell centers
- * or cell corners of both grids.
- *
- * @param anchor the cell part to map (center or corner).
- * @return conversion from source grid coordinates to target grid coordinates.
- *
- * @see GridGeometry#getGridToCRS(PixelInCell)
- */
- public MathTransform getConversion(final PixelInCell anchor) {
- if (PixelInCell.CELL_CENTER.equals(anchor)) {
- return mapCenters;
- } else if (PixelInCell.CELL_CORNER.equals(anchor)) {
- return mapCorners;
- } else {
- return PixelTranslation.translate(mapCenters, PixelInCell.CELL_CENTER, anchor);
- }
- }
-
- /**
- * Returns the intersection of the two grid geometry extents, in units of the target grid cells.
- * This is the expected ranges of grid coordinates after conversions from source to target grid,
- * clipped to target grid extent and ignoring {@linkplain #getTargetSubsamplings() subsamplings}.
- *
- * @return intersection of grid geometry extents in units of target cells.
- */
- public GridExtent getTargetExtent() {
- return targetExtent;
- }
-
- /**
- * Returns an <em>estimation</em> of the steps for accessing cells along each axis of target grid.
- * Given a {@linkplain #getConversion(PixelInCell) conversion} from source grid coordinates
- * (<var>x</var>, <var>y</var>, <var>z</var>) to target grid coordinates
- * (<var>x′</var>, <var>y′</var>, <var>z′</var>) defined as below (generalize to as many dimensions as needed):
- *
- * <ul>
- * <li><var>x′</var> = s₀⋅<var>x</var></li>
- * <li><var>y′</var> = s₁⋅<var>y</var></li>
- * <li><var>z′</var> = s₂⋅<var>z</var></li>
- * </ul>
- *
- * Then this method returns {|s₀|, |s₁|, |s₂|} rounded toward zero and clamped to 1
- * (i.e. all values in the returned array are strictly positive, no zero values).
- * It means that an iteration over source grid coordinates with a step Δ<var>x</var>=1
- * corresponds approximately to an iteration in target grid coordinates with a step of Δ<var>x′</var>=s₀,
- * a step Δ<var>y</var>=1 corresponds approximately to a step Δ<var>y′</var>=s₁, <i>etc.</i>
- * If the conversion changes grid axis order, then the order of elements in the returned array
- * is the order of axes in the {@linkplain #getTargetExtent() target range}.
- *
- * <p>In a <em>inverse</em> conversion from target to source grid, the value returned by this
- * method would be the subsampling to apply while reading the target grid.</p>
- *
- * @return an <em>estimation</em> of the steps for accessing cells along each axis of target range.
- */
- public int[] getTargetSubsamplings() {
- final int[] subsamplings;
- if (scales == null) {
- subsamplings = new int[targetExtent.getDimension()];
- Arrays.fill(subsamplings, 1);
- } else {
- subsamplings = new int[scales.length];
- for (int i=0; i<subsamplings.length; i++) {
- subsamplings[i] = Math.max(1, (int) Math.nextUp(scales[i])); // Really want rounding toward 0.
- }
- }
- return subsamplings;
- }
-
- /**
- * Returns the grid geometry resulting from subsampling the target grid with the given periods.
- * The {@code periods} argument is usually the array returned by {@link #getTargetSubsamplings()}, but
- * not necessarily. The {@linkplain GridGeometry#getExtent() extent} of the returned grid geometry will
- * be derived from {@link #getTargetExtent()} as below for each dimension <var>i</var>:
- *
- * <ul>
- * <li>The {@linkplain GridExtent#getLow(int) low} is divided by {@code periods[i]}, rounded toward zero.</li>
- * <li>The {@linkplain GridExtent#getSize(int) size} is divided by {@code periods[i]}, rounded toward zero.</li>
- * <li>The {@linkplain GridExtent#getHigh(int) high} is recomputed from above low and size.</li>
- * </ul>
- *
- * The {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform is scaled accordingly
- * in order to map approximately to the same {@linkplain GridGeometry#getEnvelope() envelope}.
- *
- * @param periods the subsampling to apply on each grid dimension. All values shall be greater than zero.
- * If the array length is shorter than the number of dimensions, missing values are assumed to be 1.
- * @return a grid geometry derived from the target geometry with the given subsampling.
- * @throws TransformException if an error occurred during computation of target grid geometry.
- */
- public GridGeometry getTargetGeometry(final int... periods) throws TransformException {
- GridExtent extent = getTargetExtent();
- double[] factors = null;
- Matrix toGiven = null;
- if (periods != null) {
- // Validity of the periods values will be verified by GridExtent.subsampling(…) invoked below.
- final GridExtent unscaled = extent;
- final int dimension = extent.getDimension();
- for (int i = Math.min(dimension, periods.length); --i >= 0;) {
- final int s = periods[i];
- if (s != 1) {
- if (factors == null) {
- extent = extent.subsample(periods);
- factors = new double[dimension];
- Arrays.fill(factors, 1);
- if (!extent.startsAtZero()) {
- toGiven = Matrices.createIdentity(dimension + 1);
- }
- }
- final double sd = s;
- factors[i] = sd;
- if (toGiven != null) {
- toGiven.setElement(i, i, sd);
- toGiven.setElement(i, dimension, unscaled.getLow(i) - extent.getLow(i) * sd);
- }
- }
- }
- }
- final MathTransform mt;
- if (factors == null) {
- if (extent == givenGeometry.extent) {
- return givenGeometry;
- }
- mt = null;
- } else {
- mt = (toGiven != null) ? MathTransforms.linear(toGiven) : MathTransforms.scale(factors);
- }
- /*
- * Assuming:
- *
- * • All low coordinates = 0
- * • h₁ the high coordinate before subsampling
- * • h₂ the high coordinates after subsampling
- * • c a conversion factor from grid indices to "real world" coordinates
- * • s a subsampling ≧ 1
- *
- * Then the envelope upper bounds x is:
- *
- * • x = (h₁ + 1) × c
- * • x = (h₂ + f) × c⋅s which implies h₂ = h₁/s and f = 1/s
- *
- * If we modify the later equation for integer division instead than real numbers, we have:
- *
- * • x = (h₂ + f) × c⋅s where h₂ = floor(h₁/s) and f = ((h₁ mod s) + 1)/s
- *
- * Because s ≧ 1, then f ≦ 1. But the f value actually used by GridExtent.toCRS(…) is hard-coded to 1
- * since it assumes that all cells are whole, i.e. it does not take in account that the last cell may
- * actually be fraction of a cell. Since 1 ≧ f, the computed envelope may be larger. This explains the
- * need for envelope clipping performed by GridGeometry constructor.
- */
- return new GridGeometry(givenGeometry, extent, mt);
- }
-
- /**
- * Returns a hash code value for this grid change.
- *
- * @return hash code value.
- */
- @Override
- public int hashCode() {
- /*
- * The mapCorners is closely related to mapCenters, so we omit it.
- * The scales array is derived from mapCenters, so we omit it too.
- */
- return mapCorners.hashCode() + 59 * targetExtent.hashCode();
- }
-
- /**
- * Compares this grid change with the given object for equality.
- *
- * @param other the other object to compare with this grid change.
- * @return {@code true} if both objects are equal.
- */
- @Override
- public boolean equals(final Object other) {
- if (other == this) {
- return true;
- }
- if (other != null && other.getClass() == getClass()) {
- final GridChange that = (GridChange) other;
- return mapCorners.equals(that.mapCorners) &&
- mapCenters.equals(that.mapCenters) &&
- targetExtent.equals(that.targetExtent) &&
- Arrays.equals(scales, that.scales);
- }
- return false;
- }
-
- /**
- * Returns a tree representation of this grid change. The tree representation
- * is for debugging purpose only and may change in any future SIS version.
- *
- * @param locale the locale to use for textual labels.
- * @return a tree representation of this grid change.
- */
- @Debug
- private TreeTable toTree(final Locale locale) {
- final TableColumn<CharSequence> column = TableColumn.VALUE_AS_TEXT;
- final TreeTable tree = new DefaultTreeTable(column);
- final TreeTable.Node root = tree.getRoot();
- root.setValue(column, Classes.getShortClassName(this));
- /*
- * GridChange (example)
- * └─Target range
- * ├─Dimension 0: [ 2000 … 5475] (3476 cells)
- * └─Dimension 1: [-1000 … 7999] (9000 cells)
- */
- TreeTable.Node section = root.newChild();
- section.setValue(column, "Target range");
- final StringBuilder buffer = new StringBuilder(256);
- getTargetExtent().appendTo(buffer, Vocabulary.getResources(locale));
- for (final CharSequence line : CharSequences.splitOnEOL(buffer)) {
- String text = line.toString().trim();
- if (!text.isEmpty()) {
- section.newChild().setValue(column, text);
- }
- }
- /*
- * GridChange (example)
- * └─Target subsamplings
- * ├─{50, 300}
- * └─Global ≈ 175.0
- */
- buffer.setLength(0);
- buffer.append('{');
- for (int s : getTargetSubsamplings()) {
- if (buffer.length() > 1) buffer.append(", ");
- buffer.append(s);
- }
- section = root.newChild();
- section.setValue(column, "Target subsamplings");
- section.newChild().setValue(column, buffer.append('}').toString()); buffer.setLength(0);
- section.newChild().setValue(column, buffer.append("Global ≈ ").append((float) getGlobalScale()).toString());
- return tree;
- }
-
- /**
- * Returns a string representation of this grid change for debugging purpose.
- * The returned string is implementation dependent and may change in any future version.
- *
- * @return a string representation of this grid change for debugging purpose.
- */
- @Override
- public String toString() {
- return toTree(null).toString();
- }
-}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
index 36991ed..e4ffb09 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridDerivation.java
@@ -16,13 +16,17 @@
*/
package org.apache.sis.coverage.grid;
+import java.util.Arrays;
+import java.util.Locale;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.util.FactoryException;
+import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.TransformSeparator;
@@ -33,8 +37,16 @@ import org.apache.sis.geometry.GeneralDirectPosition;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.internal.raster.Resources;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.ArraysExt;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTable;
// Branch-dependent imports
import org.opengis.coverage.PointOutsideCoverageException;
@@ -48,13 +60,14 @@ import org.opengis.coverage.PointOutsideCoverageException;
*
* <ol>
* <li>{@link #rounding(GridRoundingMode)} and/or {@link #margin(int...)} in any order</li>
- * <li>{@link #subgrid(Envelope, double...)}</li>
+ * <li>{@link #subgrid(GridGeometry)} or {@link #subgrid(Envelope, double...)}</li>
+ * <li>{@link #subsample(int...)}</li>
* <li>{@link #slice(DirectPosition)} and/or {@link #sliceByRatio(double, int...)}</li>
* </ol>
*
* Then the grid geometry is created by a call to {@link #build()}.
- * Alternatively, {@link #buildExtent()} can be invoked if only the {@link GridExtent} is desired
- * instead than the full {@link GridGeometry}.
+ * Alternatively, {@link #getIntersection()} can be invoked if only the {@link GridExtent} is desired
+ * instead than the full {@link GridGeometry} and no subsampling is applied.
*
* <p>All methods in this class preserve the number of dimensions. For example the {@link #slice(DirectPosition)} method sets
* the {@linkplain GridExtent#getSize(int) grid size} to 1 in all dimensions specified by the <cite>slice point</cite>,
@@ -77,18 +90,39 @@ public class GridDerivation {
protected final GridGeometry base;
/**
- * If {@link #subgrid(Envelope, double...)} or {@link #slice(DirectPosition)} has been invoked, the method name.
- * This is used for preventing those methods to be invoked twice or out-of-order, which is currently not supported.
+ * Controls behavior of rounding from floating point values to integers.
+ *
+ * @see #rounding(GridRoundingMode)
*/
- private String subGridSetter;
+ private GridRoundingMode rounding;
+
+ /**
+ * If non-null, the extent will be expanded by that amount of cells on each grid dimension.
+ *
+ * @see #margin(int...)
+ */
+ private int[] margin;
+
+ // ──────── COMPUTED BY METHODS IN THIS CLASS ─────────────────────────────────────────────────────────────────────
+ /**
+ * The sub-extent of {@link #base} grid geometry to use for the new grid geometry. This is the intersection of
+ * {@code base.extent} with any area of interest specified to a {@link #subgrid(Envelope, double...)} method,
+ * potentially with some grid size set to 1 by a {@link #slice(DirectPosition)} method.
+ * This extent is <strong>not</strong> subsampled for a given resolution.
+ *
+ * <p>This extent is initialized to {@code base.extent} if no slice or sub-grid has been requested.
+ * This field may be {@code null} if the base grid geometry does not define any extent.
+ * A successful call to {@link GridGeometry#requireGridToCRS()} guarantees that this field is non-null.</p>
+ */
+ private GridExtent baseExtent;
/**
- * The sub-extent computed by a {@link #slice(DirectPosition)} or {@link #subgrid(Envelope, double...)} methods,
- * or {@code base.extent} if no slice or sub-grid has been requested. This field may be {@code null} if the base
- * grid geometry does not define any extent. A successful call to {@link GridGeometry#requireGridToCRS()} guarantees
- * that this field is non-null.
+ * Same as {@link #baseExtent}, but takes resolution or subsampling in account.
+ * This is {@code null} if no subsampling has been applied.
+ *
+ * @todo if a {@linkplain #margin} has been specified, then we need to perform an additional clipping.
*/
- private GridExtent subExtent;
+ private GridExtent subsampledExtent;
/**
* The conversion from the subsampled grid to the original grid, or {@code null} if no subsampling is applied.
@@ -104,16 +138,19 @@ public class GridDerivation {
private int[] modifiedDimensions;
/**
- * If non-null, the extent will be expanded by that amount of cells on each grid dimension.
+ * An estimation of the multiplication factors when converting cell coordinates from {@code gridOfInterest} to {@link #base}
+ * grid. Those factors appear in the order of <em>base</em> grid axes. May be {@code null} if the conversion is identity.
+ * This is sometime redundant with {@link #toBase} but not always.
*
- * @see #margin(int...)
+ * @see #getSubsamplings()
*/
- private int[] margin;
+ private double[] scales;
/**
- * Controls behavior of rounding from floating point values to integers.
+ * If {@link #subgrid(Envelope, double...)} or {@link #slice(DirectPosition)} has been invoked, the method name.
+ * This is used for preventing those methods to be invoked twice or out-of-order, which is currently not supported.
*/
- private GridRoundingMode rounding;
+ private String subGridSetter;
/**
* Creates a new builder for deriving a grid geometry from the specified base.
@@ -124,9 +161,9 @@ public class GridDerivation {
*/
protected GridDerivation(final GridGeometry base) {
ArgumentChecks.ensureNonNull("base", base);
- this.base = base;
- subExtent = base.extent; // May be null.
- rounding = GridRoundingMode.NEAREST;
+ this.base = base;
+ baseExtent = base.extent; // May be null.
+ rounding = GridRoundingMode.NEAREST;
}
/**
@@ -173,7 +210,7 @@ public class GridDerivation {
*
* For each dimension <var>i</var> of the grid computed by above methods, the {@linkplain GridExtent#getLow(int) low} grid
* coordinate is subtracted by {@code cellCount[i]} and the {@linkplain GridExtent#getHigh(int) high} grid coordinate is
- * increased by {@code cellCount[i]}. The result is intersected with the extent of the {@linkplain #base} grid geometry
+ * increased by {@code cellCount[i]}. The result is intersected with the extent of the {@link #base} grid geometry
* given to the constructor.
*
* <div class="note"><b>Use case:</b>
@@ -208,6 +245,117 @@ public class GridDerivation {
}
/**
+ * Adapts the base grid for the geographic area and resolution of the given grid geometry.
+ * After this method invocation, {@code GridDerivation} will hold information about conversion
+ * from the given {@code gridOfInterest} to the {@link #base} grid geometry.
+ * Those information include the {@link MathTransform}s converting cell coordinates
+ * from the {@code gridOfInterest} to cell coordinates in the {@code base} grid,
+ * together with the grid extent that results from this conversion.
+ *
+ * <div class="note"><b>Usage:</b>
+ * This method can be helpful for implementation of
+ * {@link org.apache.sis.storage.GridCoverageResource#read(GridGeometry, int...)}.
+ * Example:
+ *
+ * {@preformat java
+ * class MyDataStorage extends GridCoverageResource {
+ * @Override
+ * public GridCoverage read(GridGeometry domain, int... range) throws DataStoreException {
+ * GridDerivation change = getGridGeometry().derive().subgrid(domain);
+ * GridExtent toRead = change.buildExtent();
+ * int[] subsampling = change.getSubsamplings());
+ * // Do reading here.
+ * }
+ * }
+ * }
+ * </div>
+ *
+ * The following information are mandatory:
+ * <ul>
+ * <li>{@linkplain GridGeometry#getExtent() Extent} in {@code gridOfInterest}.</li>
+ * <li>{@linkplain GridGeometry#getGridToCRS(PixelInCell) Grid to CRS} conversion in {@code gridOfInterest}.</li>
+ * <li>{@linkplain GridGeometry#getGridToCRS(PixelInCell) Grid to CRS} conversion in {@link #base} grid.</li>
+ * </ul>
+ *
+ * The following information are optional but recommended:
+ * <ul>
+ * <li>{@linkplain GridGeometry#getCoordinateReferenceSystem() Coordinate reference system} in {@code gridOfInterest}.</li>
+ * <li>{@linkplain GridGeometry#getCoordinateReferenceSystem() Coordinate reference system} in {@link #base} grid.</li>
+ * <li>{@linkplain GridGeometry#getExtent() Extent} in {@link #base} grid.</li>
+ * </ul>
+ *
+ * An optional {@link #margin(int...) margin} can be specified for increasing the size of the grid extent computed by this method.
+ * For example if the caller wants to apply bilinear interpolations in an image, (s)he will need 1 more pixel on each image border.
+ * If the caller wants to apply bi-cubic interpolations, (s)he will need 2 more pixels on each image border.
+ *
+ * <p>Notes:</p>
+ * <ul>
+ * <li>This method can be invoked only once.</li>
+ * <li>This method can not be used together with {@link #subgrid(Envelope, double...)}.</li>
+ * <li>If a non-default rounding mode is desired, it should be {@linkplain #rounding(GridRoundingMode) specified}
+ * before to invoke this method.</li>
+ * <li>This method does not reduce the number of dimensions of the grid geometry.
+ * For dimensionality reduction, see {@link GridGeometry#reduce(int...)}.</li>
+ * </ul>
+ *
+ * @param gridOfInterest the area of interest and desired resolution as a grid geometry.
+ * @return {@code this} for method call chaining.
+ * @throws IncompleteGridGeometryException if a mandatory property of a grid geometry is absent.
+ * @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
+ * @throws IllegalStateException if a {@link #subgrid(Envelope, double...) subgrid(…)} or {@link #slice(DirectPosition) slice(…)}
+ * method has already been invoked.
+ *
+ * @see #getSubsamplings()
+ * @see #subsample(int...)
+ */
+ public GridDerivation subgrid(final GridGeometry gridOfInterest) {
+ ArgumentChecks.ensureNonNull("gridOfInterest", gridOfInterest);
+ ensureSubgridNotSet();
+ subGridSetter = "subgrid";
+ if (!base.equals(gridOfInterest)) {
+ final MathTransform mapCorners, mapCenters;
+ final GridExtent domain = gridOfInterest.getExtent(); // May throw IncompleteGridGeometryException.
+ try {
+ final CoordinateOperation crsChange;
+ crsChange = Envelopes.findOperation(gridOfInterest.envelope, base.envelope); // Any envelope may be null.
+ mapCorners = path(gridOfInterest, crsChange, base, PixelInCell.CELL_CORNER);
+ mapCenters = path(gridOfInterest, crsChange, base, PixelInCell.CELL_CENTER);
+ clipExtent(domain.toCRS(mapCorners, mapCenters));
+ } catch (FactoryException | TransformException e) {
+ throw new IllegalGridGeometryException(e, "gridOfInterest");
+ }
+ if (baseExtent != base.extent && baseExtent.equals(gridOfInterest.extent)) {
+ baseExtent = gridOfInterest.extent; // Share common instance.
+ }
+ scales = GridGeometry.resolution(mapCenters, domain);
+ }
+ return this;
+ }
+
+ /**
+ * Returns the concatenation of all transformation steps from the given source to the given target.
+ *
+ * @param source the source grid geometry.
+ * @param crsChange the change of coordinate reference system, or {@code null} if none.
+ * @param target the target grid geometry.
+ * @param anchor whether we want the transform for cell corner or cell center.
+ */
+ private static MathTransform path(final GridGeometry source, final CoordinateOperation crsChange,
+ final GridGeometry target, final PixelInCell anchor) throws NoninvertibleTransformException
+ {
+ MathTransform step1 = source.getGridToCRS(anchor);
+ MathTransform step2 = target.getGridToCRS(anchor);
+ if (crsChange != null) {
+ step1 = MathTransforms.concatenate(step1, crsChange.getMathTransform());
+ }
+ if (step1.equals(step2)) { // Optimization for a common case.
+ return MathTransforms.identity(step1.getSourceDimensions());
+ } else {
+ return MathTransforms.concatenate(step1, step2.inverse());
+ }
+ }
+
+ /**
* Requests a grid geometry over a sub-region of the base grid geometry and optionally with subsampling.
* The given envelope does not need to be expressed in the same coordinate reference system (CRS)
* than {@linkplain GridGeometry#getCoordinateReferenceSystem() the CRS of the base grid geometry};
@@ -221,7 +369,7 @@ public class GridDerivation {
* <p>Notes:</p>
* <ul>
* <li>This method can be invoked only once.</li>
- * <li>This method can not be used together with {@link #slice(DirectPosition)}.</li>
+ * <li>This method can not be used together with {@link #subgrid(GridGeometry)}.</li>
* <li>If a non-default rounding mode is desired, it should be {@linkplain #rounding(GridRoundingMode) specified}
* before to invoke this method.</li>
* <li>This method does not reduce the number of dimensions of the grid geometry.
@@ -237,8 +385,8 @@ public class GridDerivation {
* @return {@code this} for method call chaining.
* @throws IncompleteGridGeometryException if the base grid geometry has no extent or no "grid to CRS" transform.
* @throws IllegalGridGeometryException if an error occurred while converting the envelope coordinates to grid coordinates.
- * @throws IllegalStateException if {@code subgrid(Envelope, double...)} or {@link #slice(DirectPosition)}
- * has already been invoked.
+ * @throws IllegalStateException if a {@link #subgrid(GridGeometry) subgrid(…)} or {@link #slice(DirectPosition) slice(…)}
+ * method has already been invoked.
*
* @see GridExtent#subsample(int[])
*/
@@ -268,17 +416,17 @@ public class GridDerivation {
* If no area of interest has been specified, or if the result is identical to the original extent,
* then we will keep the reference to the original GridExtent (i.e. we share existing instances).
*/
- dimension = subExtent.getDimension();
+ dimension = baseExtent.getDimension();
GeneralEnvelope indices = null;
if (areaOfInterest != null) {
indices = Envelopes.transform(cornerToCRS.inverse(), areaOfInterest);
- setSubExtent(indices, subExtent);
+ clipExtent(indices);
}
if (indices == null || indices.getDimension() != dimension) {
indices = new GeneralEnvelope(dimension);
}
for (int i=0; i<dimension; i++) {
- indices.setRange(i, subExtent.getLow(i), subExtent.getHigh(i) + 1.0);
+ indices.setRange(i, baseExtent.getLow(i), baseExtent.getHigh(i) + 1.0);
}
/*
* Convert the target resolutions to grid cell subsamplings and adjust the extent consequently.
@@ -318,21 +466,22 @@ public class GridDerivation {
* grid coordinates. If we had no rounding, the conversion would be only a scale. But because of rounding,
* we need a small translation for the difference between the "real" coordinate and the integer coordinate.
*
- * TODO: need to clip to base.extent, taking in account the difference in resolution.
+ * TODO: need to clip to baseExtent, taking in account the difference in resolution.
*/
if (modified) {
- final GridExtent unscaled = subExtent;
- setSubExtent(indices, null);
+ subsampledExtent = new GridExtent(indices, rounding, null, null, modifiedDimensions);
+ if (baseExtent.equals(subsampledExtent)) subsampledExtent = baseExtent;
m = Matrices.createIdentity(dimension + 1);
for (int k=0; k<resolution.length; k++) {
final double s = resolution[k];
if (s > 1) { // Also for skipping NaN values.
final int i = (modifiedDimensions != null) ? modifiedDimensions[k] : k;
m.setElement(i, i, s);
- m.setElement(i, dimension, unscaled.getLow(i) - subExtent.getLow(i) * s);
+ m.setElement(i, dimension, baseExtent.getLow(i) - subsampledExtent.getLow(i) * s);
}
}
toBase = MathTransforms.linear(m);
+ scales = resolution; // For information purpose only.
}
}
} catch (FactoryException | TransformException e) {
@@ -366,11 +515,12 @@ public class GridDerivation {
}
/**
- * Returns the point of interest of current {@link #subExtent}, keeping only the remaining
+ * Returns the point of interest of current {@link #baseExtent}, keeping only the remaining
* dimensions after {@link #dropUnusedDimensions(MathTransform, int)} execution.
+ * The position is in units of {@link #base} grid coordinates.
*/
private double[] getPointOfInterest() {
- final double[] pointOfInterest = subExtent.getPointOfInterest();
+ final double[] pointOfInterest = baseExtent.getPointOfInterest();
if (modifiedDimensions == null) {
return pointOfInterest;
}
@@ -382,16 +532,80 @@ public class GridDerivation {
}
/**
- * Sets {@link #subExtent} to the given envelope.
+ * Sets {@link #baseExtent} to the given envelope clipped to the previous extent.
+ * This method shall be invoked for clipping only, without any subsampling applied.
+ *
+ * @param indices the envelope to intersect in units of {@link #base} grid coordinates.
+ */
+ private void clipExtent(final GeneralEnvelope indices) {
+ final GridExtent sub = new GridExtent(indices, rounding, margin, baseExtent, modifiedDimensions);
+ if (!sub.equals(baseExtent)) {
+ baseExtent = sub;
+ }
+ }
+
+ /**
+ * Applies a subsampling on the grid geometry to build.
+ * The {@code subsamplings} argument is often the array returned by {@link #getSubsamplings()}, but not necessarily.
+ * The {@linkplain GridGeometry#getExtent() extent} of the {@linkplain #build() built} grid geometry will be derived
+ * from {@link #getIntersection()} as below for each dimension <var>i</var>:
+ *
+ * <ul>
+ * <li>The {@linkplain GridExtent#getLow(int) low} is divided by {@code subsamplings[i]}, rounded toward zero.</li>
+ * <li>The {@linkplain GridExtent#getSize(int) size} is divided by {@code subsamplings[i]}, rounded toward zero.</li>
+ * <li>The {@linkplain GridExtent#getHigh(int) high} is recomputed from above low and size.</li>
+ * </ul>
+ *
+ * The {@linkplain GridGeometry#getGridToCRS(PixelInCell) grid to CRS} transform is scaled accordingly
+ * in order to map approximately to the same {@linkplain GridGeometry#getEnvelope() envelope}.
*
- * @param indices the envelope to use for setting the grid extent.
- * @param enclosing the enclosing grid extent if a subsampling is not yet applied, {@code null} otherwise.
+ * @param subsamplings the subsampling to apply on each grid dimension. All values shall be greater than zero.
+ * If the array length is shorter than the number of dimensions, missing values are assumed to be 1.
+ * @return {@code this} for method call chaining.
+ * @throws IllegalStateException if a subsampling has already been set,
+ * for example by a call to {@link #subgrid(Envelope, double...) subgrid(…)}.
+ *
+ * @see #subgrid(GridGeometry)
+ * @see #getSubsamplings()
+ * @see GridExtent#subsample(int...)
*/
- private void setSubExtent(final GeneralEnvelope indices, final GridExtent enclosing) {
- final GridExtent sub = new GridExtent(indices, rounding, margin, enclosing, modifiedDimensions);
- if (!sub.equals(subExtent)) {
- subExtent = sub;
+ public GridDerivation subsample(final int... subsamplings) {
+ ArgumentChecks.ensureNonNull("subsamplings", subsamplings);
+ if (toBase != null) {
+ throw new IllegalStateException(Errors.format(Errors.Keys.ValueAlreadyDefined_1, "subsamplings"));
+ }
+ final GridExtent extent = (baseExtent != null) ? baseExtent : base.getExtent();
+ Matrix affine = null;
+ scales = null;
+ if (subsamplings != null) {
+ // Validity of the subsamplings values will be verified by GridExtent.subsample(…) invoked below.
+ final int dimension = extent.getDimension();
+ for (int i = Math.min(dimension, subsamplings.length); --i >= 0;) {
+ final int s = subsamplings[i];
+ if (s != 1) {
+ if (scales == null) {
+ subsampledExtent = extent.subsample(subsamplings);
+ scales = new double[dimension];
+ Arrays.fill(scales, 1);
+ if (!subsampledExtent.startsAtZero()) {
+ affine = Matrices.createIdentity(dimension + 1);
+ }
+ }
+ final double sd = s;
+ scales[i] = sd;
+ if (affine != null) {
+ affine.setElement(i, i, sd);
+ affine.setElement(i, dimension, extent.getLow(i) - subsampledExtent.getLow(i) * sd);
+ }
+ }
+ }
}
+ if (affine != null) {
+ toBase = MathTransforms.linear(affine);
+ } else if (scales != null) {
+ toBase = MathTransforms.scale(scales);
+ }
+ return this;
}
/**
@@ -448,7 +662,14 @@ public class GridDerivation {
final int dimension = cornerToCRS.getTargetDimensions();
ArgumentChecks.ensureDimensionMatches("slicePoint", dimension, slicePoint);
cornerToCRS = dropUnusedDimensions(cornerToCRS, dimension);
- subExtent = subExtent.slice(cornerToCRS.inverse().transform(slicePoint, null), modifiedDimensions);
+ DirectPosition gridPoint = cornerToCRS.inverse().transform(slicePoint, null);
+ if (subsampledExtent != null) {
+ subsampledExtent = subsampledExtent.slice(gridPoint, modifiedDimensions);
+ }
+ if (toBase != null) {
+ gridPoint = toBase.transform(gridPoint, gridPoint);
+ }
+ baseExtent = baseExtent.slice(gridPoint, modifiedDimensions);
} catch (FactoryException e) {
throw new IllegalGridGeometryException(Resources.format(Resources.Keys.CanNotMapToGridDimensions), e);
} catch (TransformException e) {
@@ -473,16 +694,33 @@ public class GridDerivation {
public GridDerivation sliceByRatio(final double sliceRatio, final int... dimensionsToKeep) {
ArgumentChecks.ensureBetween("sliceRatio", 0, 1, sliceRatio);
ArgumentChecks.ensureNonNull("dimensionsToKeep", dimensionsToKeep);
- final GridExtent extent = (subExtent != null) ? subExtent : base.getExtent();
+ subGridSetter = "sliceByRatio";
+ final GridExtent extent = (baseExtent != null) ? baseExtent : base.getExtent();
final GeneralDirectPosition slicePoint = new GeneralDirectPosition(extent.getDimension());
+ baseExtent = sliceByRatio(extent, slicePoint, sliceRatio, dimensionsToKeep);
+ if (subsampledExtent != null) {
+ subsampledExtent = sliceByRatio(subsampledExtent, slicePoint, sliceRatio, dimensionsToKeep);
+ }
+ return this;
+ }
+
+ /**
+ * Returns a slice of the given grid extent computed by a ratio between 0 and 1 inclusive.
+ * This is for {@link #sliceByRatio(double, int...)} implementation only.
+ *
+ * @param extent the extent to slice.
+ * @param slicePoint a pre-allocated direct position to be overwritten by this method.
+ */
+ private static GridExtent sliceByRatio(final GridExtent extent, final GeneralDirectPosition slicePoint,
+ final double sliceRatio, final int[] dimensionsToKeep)
+ {
for (int i=0; i<slicePoint.ordinates.length; i++) {
slicePoint.ordinates[i] = sliceRatio * (extent.getSize(i) - 1) + extent.getLow(i);
}
for (int i=0; i<dimensionsToKeep.length; i++) {
slicePoint.ordinates[dimensionsToKeep[i]] = Double.NaN;
}
- subExtent = extent.slice(slicePoint, null);
- return this;
+ return extent.slice(slicePoint, null);
}
/*
@@ -495,11 +733,35 @@ public class GridDerivation {
/**
* Builds a grid geometry with the configuration specified by the other methods in this {@code GridDerivation} class.
*
- * @return the modified grid geometry. May be the {@linkplain #base} grid geometry if no change apply.
+ * @return the modified grid geometry. May be the {@link #base} grid geometry if no change apply.
*/
public GridGeometry build() {
- if (toBase != null || subExtent != base.extent) try {
- return new GridGeometry(base, subExtent, toBase);
+ /*
+ * Assuming:
+ *
+ * • All low coordinates = 0
+ * • h₁ the high coordinate before subsampling
+ * • h₂ the high coordinates after subsampling
+ * • c a conversion factor from grid indices to "real world" coordinates
+ * • s a subsampling ≧ 1
+ *
+ * Then the envelope upper bounds x is:
+ *
+ * • x = (h₁ + 1) × c
+ * • x = (h₂ + f) × c⋅s which implies h₂ = h₁/s and f = 1/s
+ *
+ * If we modify the later equation for integer division instead than real numbers, we have:
+ *
+ * • x = (h₂ + f) × c⋅s where h₂ = floor(h₁/s) and f = ((h₁ mod s) + 1)/s
+ *
+ * Because s ≧ 1, then f ≦ 1. But the f value actually used by GridExtent.toCRS(…) is hard-coded to 1
+ * since it assumes that all cells are whole, i.e. it does not take in account that the last cell may
+ * actually be fraction of a cell. Since 1 ≧ f, the computed envelope may be larger. This explains the
+ * need for envelope clipping performed by GridGeometry constructor.
+ */
+ final GridExtent extent = (subsampledExtent != null) ? subsampledExtent : baseExtent;
+ if (toBase != null || extent != base.extent) try {
+ return new GridGeometry(base, extent, toBase);
} catch (TransformException e) {
throw new IllegalGridGeometryException(e, "envelope");
}
@@ -507,12 +769,147 @@ public class GridDerivation {
}
/**
- * Returns the extent of the modified grid geometry. This method is more efficient than
- * {@link #build()} if only the grid extent is desired instead than the full grid geometry.
+ * Returns the extent of the modified grid geometry, ignoring subsamplings or changes in resolution.
+ * This is the intersection of the {@link #base} grid geometry with the (grid or geospatial) envelope
+ * given to a {@link #subgrid(Envelope, double...) subgrid(…)} method,
+ * expanded by the {@linkplain #margin(int...) specified margin} (if any)
+ * and potentially with some {@linkplain GridExtent#getSize(int) grid sizes} set to 1
+ * if a {@link #slice(DirectPosition) slice(…)} method has been invoked.
+ * The returned extent is in units of the {@link #base} grid cells, i.e.
+ * {@linkplain #getSubsamplings() subsamplings} are ignored.
+ *
+ * @return intersection of grid geometry extents in units of {@link #base} grid cells.
+ */
+ public GridExtent getIntersection() {
+ return (baseExtent != null) ? baseExtent : base.getExtent();
+ }
+
+ /**
+ * Returns an <em>estimation</em> of the steps for accessing cells along each axis of base grid.
+ * Given a conversion from {@code gridOfInterest} grid coordinates
+ * (<var>x</var>, <var>y</var>, <var>z</var>) to {@link #base} grid coordinates
+ * (<var>x′</var>, <var>y′</var>, <var>z′</var>) defined as below (generalize to as many dimensions as needed):
+ *
+ * <ul>
+ * <li><var>x′</var> = s₀⋅<var>x</var></li>
+ * <li><var>y′</var> = s₁⋅<var>y</var></li>
+ * <li><var>z′</var> = s₂⋅<var>z</var></li>
+ * </ul>
+ *
+ * Then this method returns {|s₀|, |s₁|, |s₂|} rounded toward zero and clamped to 1
+ * (i.e. all values in the returned array are strictly positive, no zero values).
+ * It means that an iteration over {@code gridOfInterest} grid coordinates with a step Δ<var>x</var>=1
+ * corresponds approximately to an iteration in {@link #base} grid coordinates with a step of Δ<var>x′</var>=s₀,
+ * a step Δ<var>y</var>=1 corresponds approximately to a step Δ<var>y′</var>=s₁, <i>etc.</i>
+ * If the conversion changes grid axis order, then the order of elements in the returned array
+ * is the order of axes in the {@link #base} grid.
+ *
+ * @return an <em>estimation</em> of the steps for accessing cells along each axis of {@link #base} grid.
+ *
+ * @see #subgrid(GridGeometry)
+ * @see #subgrid(Envelope, double...)
+ * @see #subsample(int...)
+ */
+ public int[] getSubsamplings() {
+ final int[] subsamplings;
+ if (scales == null) {
+ subsamplings = new int[getIntersection().getDimension()];
+ Arrays.fill(subsamplings, 1);
+ } else {
+ subsamplings = new int[scales.length];
+ for (int i=0; i<subsamplings.length; i++) {
+ subsamplings[i] = Math.max(1, (int) Math.nextUp(scales[i])); // Really want rounding toward 0.
+ }
+ }
+ return subsamplings;
+ }
+
+ /**
+ * Returns an <em>estimation</em> of the scale factor when converting sub-grid coordinates to {@link #base} grid coordinates.
+ * This is for information purpose only since this method combines potentially different scale factors for all dimensions.
+ *
+ * @return an <em>estimation</em> of the scale factor for all dimensions.
+ *
+ * @see #subgrid(GridGeometry)
+ * @see #subgrid(Envelope, double...)
+ */
+ public double getGlobalScale() {
+ if (scales != null) {
+ double sum = 0;
+ int count = 0;
+ for (final double value : scales) {
+ if (Double.isFinite(value)) {
+ sum += value;
+ count++;
+ }
+ }
+ if (count != 0) {
+ return sum / count;
+ }
+ }
+ return 1;
+ }
+
+ /**
+ * Returns a tree representation of this {@code GridDerivation}.
+ * The tree representation is for debugging purpose only and may change in any future SIS version.
+ *
+ * @param locale the locale to use for textual labels.
+ * @return a tree representation of this {@code GridDerivation}.
+ */
+ @Debug
+ private TreeTable toTree(final Locale locale) {
+ final TableColumn<CharSequence> column = TableColumn.VALUE_AS_TEXT;
+ final TreeTable tree = new DefaultTreeTable(column);
+ final TreeTable.Node root = tree.getRoot();
+ root.setValue(column, Classes.getShortClassName(this));
+ final StringBuilder buffer = new StringBuilder(256);
+ /*
+ * GridDerivation (example)
+ * └─Intersection
+ * ├─Dimension 0: [ 2000 … 5475] (3476 cells)
+ * └─Dimension 1: [-1000 … 7999] (9000 cells)
+ */
+ if (baseExtent != null) {
+ TreeTable.Node section = root.newChild();
+ section.setValue(column, "Intersection");
+ getIntersection().appendTo(buffer, Vocabulary.getResources(locale));
+ for (final CharSequence line : CharSequences.splitOnEOL(buffer)) {
+ String text = line.toString().trim();
+ if (!text.isEmpty()) {
+ section.newChild().setValue(column, text);
+ }
+ }
+ }
+ /*
+ * GridDerivation (example)
+ * └─Subsamplings
+ * ├─{50, 300}
+ * └─Global ≈ 175.0
+ */
+ if (scales != null) {
+ buffer.setLength(0);
+ buffer.append('{');
+ for (int s : getSubsamplings()) {
+ if (buffer.length() > 1) buffer.append(", ");
+ buffer.append(s);
+ }
+ TreeTable.Node section = root.newChild();
+ section.setValue(column, "Subsamplings");
+ section.newChild().setValue(column, buffer.append('}').toString()); buffer.setLength(0);
+ section.newChild().setValue(column, buffer.append("Global ≈ ").append((float) getGlobalScale()).toString());
+ }
+ return tree;
+ }
+
+ /**
+ * Returns a string representation of this {@code GridDerivation} for debugging purpose.
+ * The returned string is implementation dependent and may change in any future version.
*
- * @return the modified grid geometry extent.
+ * @return a string representation of this {@code GridDerivation} for debugging purpose.
*/
- public GridExtent buildExtent() {
- return (subExtent != null) ? subExtent : base.getExtent();
+ @Override
+ public String toString() {
+ return toTree(null).toString();
}
}
diff --git a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
index 2b59b37..73f976d 100644
--- a/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
+++ b/core/sis-raster/src/main/java/org/apache/sis/coverage/grid/GridExtent.java
@@ -943,7 +943,7 @@ public class GridExtent implements Serializable {
* @return the subsampled extent, or {@code this} is subsampling results in the same extent.
* @throws IllegalArgumentException if a period is not greater than zero.
*
- * @see GridDerivation#subgrid(Envelope, double...)
+ * @see GridDerivation#subsample(int...)
*/
public GridExtent subsample(final int... periods) {
ArgumentChecks.ensureNonNull("periods", periods);
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridChangeTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridChangeTest.java
deleted file mode 100644
index 4a609f4..0000000
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridChangeTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.sis.coverage.grid;
-
-import org.opengis.referencing.datum.PixelInCell;
-import org.opengis.referencing.operation.TransformException;
-import org.apache.sis.referencing.operation.transform.MathTransforms;
-import org.apache.sis.referencing.operation.matrix.Matrix3;
-import org.apache.sis.geometry.GeneralEnvelope;
-import org.apache.sis.test.DependsOn;
-import org.apache.sis.test.TestCase;
-import org.junit.Test;
-
-import static org.apache.sis.test.ReferencingAssert.*;
-import static org.apache.sis.coverage.grid.GridExtentTest.assertExtentEquals;
-
-
-/**
- * Tests {@link GridChange}.
- *
- * @author Martin Desruisseaux (Geomatys)
- * @version 1.0
- * @since 1.0
- * @module
- */
-@DependsOn({GridExtentTest.class, GridGeometryTest.class})
-public final strictfp class GridChangeTest extends TestCase {
- /**
- * Creates a grid geometry with the given extent and scale for testing purpose.
- * An arbitrary translation of (2,3) is added to the "grid to CRS" conversion.
- */
- private static GridGeometry grid(int xmin, int ymin, int xmax, int ymax, int xScale, int yScale) throws TransformException {
- GridExtent extent = new GridExtent(null, new long[] {xmin, ymin}, new long[] {xmax, ymax}, true);
- Matrix3 gridToCRS = new Matrix3();
- gridToCRS.m00 = xScale;
- gridToCRS.m11 = yScale;
- gridToCRS.m02 = 200; // Arbitrary translation.
- gridToCRS.m12 = 500;
- return new GridGeometry(extent, PixelInCell.CELL_CORNER, MathTransforms.linear(gridToCRS), null);
- }
-
- /**
- * Tests the construction from grid geometries having a linear "grid to CRS" conversion.
- *
- * @throws TransformException if an error occurred while computing the grid geometry.
- */
- @Test
- public void testLinear2D() throws TransformException {
- GridGeometry source = grid( 10, -20, 110, 180, 100, -300); // Envelope x: [1200 … 11300] y: [-53800 … 6500]
- GridGeometry target = grid(2000, -1000, 9000, 8000, 2, -1); // Envelope x: [4200 … 18202] y: [ -7501 … 1500]
- GridChange change = new GridChange(source, target); // Envelope x: [4200 … 11300] y: [ -7501 … 1500]
- GridExtent extent = change.getTargetExtent();
- assertExtentEquals(extent, 0, 2000, 5549); // Subrange of target extent.
- assertExtentEquals(extent, 1, -1000, 8000);
- assertArrayEquals("subsamplings", new int[] {50, 300}, change.getTargetSubsamplings()); // s = scaleSource / scaleTarget
- /*
- * Scale factors in following matrix shall be the same than above subsamplings.
- * Translation appears only with PixelInCell different than the one used at construction.
- */
- Matrix3 c = new Matrix3();
- c.m00 = 50;
- c.m11 = 300;
- assertMatrixEquals("CELL_CORNER", c, MathTransforms.getMatrix(change.getConversion(PixelInCell.CELL_CORNER)), STRICT);
- c.m02 = 24.5;
- c.m12 = 149.5;
- assertMatrixEquals("CELL_CENTER", c, MathTransforms.getMatrix(change.getConversion(PixelInCell.CELL_CENTER)), STRICT);
- /*
- * If we do not ask for subsamplings, the 'gridToCRS' transforms shall be the same than the 'target' geometry.
- * The envelope is the intersection of the envelopes of 'source' and 'target' geometries, documented above.
- */
- GridGeometry tg = change.getTargetGeometry();
- assertSame("extent", extent, tg.getExtent());
- assertSame("CELL_CORNER", target.getGridToCRS(PixelInCell.CELL_CORNER), tg.getGridToCRS(PixelInCell.CELL_CORNER));
- assertSame("CELL_CENTER", target.getGridToCRS(PixelInCell.CELL_CENTER), tg.getGridToCRS(PixelInCell.CELL_CENTER));
- GeneralEnvelope expected = new GeneralEnvelope(2);
- expected.setRange(0, 4200, 11300);
- expected.setRange(1, -7501, 1500);
- assertEnvelopeEquals(expected, tg.getEnvelope(), STRICT);
- /*
- * If we ask for subsamplings, then the envelope should be approximately the same or smaller. Note that without
- * the clipping documented in GridExtent(GridExtent, int...) constructor, the envelope could be larger.
- */
- tg = change.getTargetGeometry(50, 300);
- assertEnvelopeEquals(expected, tg.getEnvelope(), STRICT);
- assertMatrixEquals("gridToCRS", new Matrix3(
- 100, 0, 200,
- 0, -300, 600,
- 0, 0, 1), MathTransforms.getMatrix(tg.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
- }
-}
diff --git a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
index 1f93d3b..865f551 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/coverage/grid/GridDerivationTest.java
@@ -74,7 +74,59 @@ public final strictfp class GridDerivationTest extends TestCase {
envelope.setRange(0, -70.001, +80.002);
envelope.setRange(1, 4.997, 15.003);
assertExtentEquals(new long[] {370, 40, 4},
- new long[] {389, 339, 10}, grid.derive().subgrid(envelope).buildExtent());
+ new long[] {389, 339, 10}, grid.derive().subgrid(envelope).getIntersection());
+ }
+
+ /**
+ * Creates a grid geometry with the given extent and scale for testing purpose.
+ * An arbitrary translation of (2,3) is added to the "grid to CRS" conversion.
+ */
+ private static GridGeometry grid(int xmin, int ymin, int xmax, int ymax, int xScale, int yScale) throws TransformException {
+ GridExtent extent = new GridExtent(null, new long[] {xmin, ymin}, new long[] {xmax, ymax}, true);
+ Matrix3 gridToCRS = new Matrix3();
+ gridToCRS.m00 = xScale;
+ gridToCRS.m11 = yScale;
+ gridToCRS.m02 = 200; // Arbitrary translation.
+ gridToCRS.m12 = 500;
+ return new GridGeometry(extent, PixelInCell.CELL_CORNER, MathTransforms.linear(gridToCRS), null);
+ }
+
+ /**
+ * Tests the construction from grid geometries having a linear "grid to CRS" conversion.
+ *
+ * @throws TransformException if an error occurred while computing the grid geometry.
+ */
+ @Test
+ public void testSubgridFromOtherGrid() throws TransformException {
+ GridGeometry source = grid( 10, -20, 110, 180, 100, -300); // Envelope x: [1200 … 11300] y: [-53800 … 6500]
+ GridGeometry target = grid(2000, -1000, 9000, 8000, 2, -1); // Envelope x: [4200 … 18202] y: [ -7501 … 1500]
+ GridDerivation change = target.derive().subgrid(source); // Envelope x: [4200 … 11300] y: [ -7501 … 1500]
+ GridExtent extent = change.getIntersection();
+ GridExtentTest.assertExtentEquals(extent, 0, 2000, 5549); // Subrange of target extent.
+ GridExtentTest.assertExtentEquals(extent, 1, -1000, 8000);
+ assertArrayEquals("subsamplings", new int[] {50, 300}, change.getSubsamplings()); // s = scaleSource / scaleTarget
+ /*
+ * If we do not ask for subsamplings, the 'gridToCRS' transforms shall be the same than the 'target' geometry.
+ * The envelope is the intersection of the envelopes of 'source' and 'target' geometries, documented above.
+ */
+ GridGeometry tg = change.build();
+ assertSame("extent", extent, tg.getExtent());
+ assertSame("CELL_CORNER", target.getGridToCRS(PixelInCell.CELL_CORNER), tg.getGridToCRS(PixelInCell.CELL_CORNER));
+ assertSame("CELL_CENTER", target.getGridToCRS(PixelInCell.CELL_CENTER), tg.getGridToCRS(PixelInCell.CELL_CENTER));
+ GeneralEnvelope expected = new GeneralEnvelope(2);
+ expected.setRange(0, 4200, 11300);
+ expected.setRange(1, -7501, 1500);
+ assertEnvelopeEquals(expected, tg.getEnvelope(), STRICT);
+ /*
+ * If we ask for subsamplings, then the envelope should be approximately the same or smaller. Note that without
+ * the clipping documented in GridExtent(GridExtent, int...) constructor, the envelope could be larger.
+ */
+ tg = change.subsample(50, 300).build();
+ assertEnvelopeEquals(expected, tg.getEnvelope(), STRICT);
+ assertMatrixEquals("gridToCRS", new Matrix3(
+ 100, 0, 200,
+ 0, -300, 600,
+ 0, 0, 1), MathTransforms.getMatrix(tg.getGridToCRS(PixelInCell.CELL_CORNER)), STRICT);
}
/**
@@ -108,7 +160,7 @@ public final strictfp class GridDerivationTest extends TestCase {
final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84);
envelope.setRange(0, -70.001, +80.002);
envelope.setRange(1, -4.997, 15.003);
- final GridExtent actual = grid.derive().subgrid(envelope).buildExtent();
+ final GridExtent actual = grid.derive().subgrid(envelope).getIntersection();
assertEquals(extent.getAxisType(0), actual.getAxisType(0));
assertExtentEquals(new long[] { 56, 69, 2},
new long[] {130, 73, 4}, actual);
@@ -121,7 +173,7 @@ public final strictfp class GridDerivationTest extends TestCase {
*/
@Test
@DependsOnMethod("testSubExtent")
- public void testSubgrid() throws TransformException {
+ public void testSubgridFromEnvelope() throws TransformException {
final GeneralEnvelope envelope = new GeneralEnvelope(HardCodedCRS.WGS84_φλ);
envelope.setRange(0, -70, +80);
envelope.setRange(1, 5, 15);
@@ -204,7 +256,7 @@ public final strictfp class GridDerivationTest extends TestCase {
*/
@Test
@Ignore("TODO: not yet fixed.")
- public void testSubGridCrossingAntiMeridian() {
+ public void testSubgridCrossingAntiMeridian() {
final GridGeometry grid = new GridGeometry(
new GridExtent(200, 180), PixelInCell.CELL_CORNER,
MathTransforms.linear(new Matrix3(
diff --git a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
index 2a8b4ee..8cdacd2 100644
--- a/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
+++ b/core/sis-raster/src/test/java/org/apache/sis/test/suite/RasterTestSuite.java
@@ -35,7 +35,6 @@ import org.junit.BeforeClass;
org.apache.sis.coverage.grid.GridExtentTest.class,
org.apache.sis.coverage.grid.GridGeometryTest.class,
org.apache.sis.coverage.grid.GridDerivationTest.class,
- org.apache.sis.coverage.grid.GridChangeTest.class,
org.apache.sis.coverage.CategoryTest.class,
org.apache.sis.coverage.CategoryListTest.class,
org.apache.sis.coverage.SampleDimensionTest.class,
diff --git a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
index 06c1d7c..fdffc18 100644
--- a/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
+++ b/storage/sis-netcdf/src/main/java/org/apache/sis/storage/netcdf/GridResource.java
@@ -38,8 +38,8 @@ import org.apache.sis.internal.netcdf.VariableRole;
import org.apache.sis.internal.storage.AbstractGridResource;
import org.apache.sis.internal.storage.ResourceOnFileSystem;
import org.apache.sis.coverage.SampleDimension;
-import org.apache.sis.coverage.grid.GridChange;
import org.apache.sis.coverage.grid.GridCoverage;
+import org.apache.sis.coverage.grid.GridDerivation;
import org.apache.sis.internal.raster.RasterFactory;
import org.apache.sis.storage.DataStoreException;
import org.apache.sis.storage.DataStoreContentException;
@@ -372,9 +372,9 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
final DataBuffer imageBuffer;
final SampleDimension[] selected = new SampleDimension[range.length];
try {
- final Buffer[] samples = new Buffer[range.length];
- final GridChange change = new GridChange(domain, gridGeometry);
- final int[] subsamplings = change.getTargetSubsamplings();
+ final Buffer[] samples = new Buffer[range.length];
+ final GridDerivation change = gridGeometry.derive().subgrid(domain);
+ final int[] subsamplings = change.getSubsamplings();
SampleDimension.Builder builder = null;
/*
* Iterate over netCDF variables in the order they appear in the file, not in the order requested
@@ -399,20 +399,22 @@ final class GridResource extends AbstractGridResource implements ResourceOnFileS
}
if (values == null) {
// Optional.orElseThrow() below should never fail since Variable.read(…) wraps primitive array.
- values = variable.read(change.getTargetExtent(), subsamplings).buffer().get();
+ values = variable.read(change.getIntersection(), subsamplings).buffer().get();
}
selected[j] = def;
samples[j] = values;
}
}
}
- domain = change.getTargetGeometry(subsamplings);
+ domain = change.subsample(subsamplings).build();
imageBuffer = RasterFactory.wrap(dataType.rasterDataType, samples);
- } catch (TransformException e) {
- throw new DataStoreReferencingException(e);
} catch (IOException e) {
throw new DataStoreException(e);
} catch (RuntimeException e) { // Many exceptions thrown by RasterFactory.wrap(…).
+ final Throwable cause = e.getCause();
+ if (cause instanceof TransformException) {
+ throw new DataStoreReferencingException(cause);
+ }
throw new DataStoreContentException(e);
}
if (imageBuffer == null) {