blob: 14147de8df3c391fef98e58b578c1fb023bf183c [file] [log] [blame]
/*
* 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.function.Supplier;
import javax.measure.Quantity;
import javax.measure.quantity.Length;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.util.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.privy.CoordinateOperations;
import org.apache.sis.referencing.privy.WraparoundApplicator;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.WraparoundTransform;
import org.apache.sis.geometry.AbstractDirectPosition;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.image.ImageProcessor;
import org.apache.sis.measure.Quantities;
import org.apache.sis.measure.Units;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.privy.Numerics;
/**
* Finds a transform from grid cells in a source coverage to geospatial positions in the CRS of a target coverage.
* This class differs from {@link CRS#findOperation CRS#findOperation(…)} because of the gridded aspect of inputs.
* {@linkplain GridGeometry grid geometries} give more information about how referencing is applied on datasets.
* With them, this class provides three additional benefits:
*
* <ul class="verbose">
* <li>Detect dimensions where target coordinates are constrained to constant values because
* the {@linkplain GridExtent#getSize(int) grid size} is only one cell in those dimensions.
* This is an important because it makes possible to find operations normally impossible:
* we can still produce an operation to a target CRS even if some dimensions have no corresponding
* source CRS.</li>
*
* <li>Detect how to handle wraparound axes. For example if a raster spans 150° to 200° of longitude,
* this class understands that -170° of longitude should be translated to 190° for thar particular
* raster. This will work even if the minimum and maximum values declared in the longitude axis do
* not match that range.</li>
*
* <li>Use the area of interest and grid resolution for refining the coordinate operation between two CRS.</li>
* </ul>
*
* <b>Note:</b> except for the {@link #gridToGrid()} convenience method,
* this class does not provide the complete chain of operations from grid to grid.
* It provides only the operation from <em>cell indices</em> in source grid to <em>coordinates in the CRS</em>
* of destination grid. Callers must add the last step (conversion from target CRS to cell indices) themselves.
*
* @author Alexis Manin (Geomatys)
* @author Martin Desruisseaux (Geomatys)
*/
final class CoordinateOperationFinder implements Supplier<double[]> {
/**
* Whether the operation is between cell centers or cell corners.
*
* @see #setAnchor(PixelInCell)
*/
private PixelInCell anchor;
/**
* The grid geometry which is the source/target of the coordinate operation to find.
*/
private final GridGeometry source, target;
/**
* The target coordinate values, computed only if needed. This is computed by {@link #get()}, which is
* itself invoked indirectly by {@link org.apache.sis.referencing.operation.CoordinateOperationFinder}.
* The value is cached in case {@code get()} is invoked many times during the same finder execution.
*
* @see #get()
*/
private double[] coordinates;
/**
* The coordinate operation from source to target CRS, computed when first needed.
* May be {@code null} if there is no information about the source and target CRS.
* Note that identity operation is not equivalent to {@code null} because identity operation will still
* be checked for wraparound axes (because the CRS is known), while null operation will have no check.
*
* @see #knowChangeOfCRS
* @see #changeOfCRS()
*/
private CoordinateOperation changeOfCRS;
/**
* Whether the {@link #changeOfCRS} operation has been determined.
* Note that result of determining that operation may be {@code null}, which is why we need this flag.
*/
private boolean knowChangeOfCRS;
/**
* The {@link #changeOfCRS} transform together with {@link WraparoundTransform} if needed.
* The wraparound is used for handling images crossing the anti-meridian.
*
* @see #gridToCRS()
*/
private MathTransform forwardChangeOfCRS;
/**
* Inverse of {@link #changeOfCRS} transform together with {@link WraparoundTransform} if needed.
* The wraparound is used for handling images crossing the anti-meridian.
*
* <p>Contrarily to {@link #forwardChangeOfCRS}, the process that determine this {@code inverseChangeOfCRS}
* transform should check if wraparound is really needed. This is because {@code inverseChangeOfCRS} will be
* used much more extensively (for every pixels) than other transforms.</p>
*
* @see #isWraparoundNeeded
* @see #isWraparoundApplied
* @see #inverse()
*/
private MathTransform inverseChangeOfCRS;
/**
* Transform from “grid coordinates of the source” to “geospatial coordinates of the target”.
* This is the concatenation of {@link #source} "grid to CRS" with {@link #forwardChangeOfCRS},
* possibly with wraparound handling and cached for reuse by {@link #inverse()}:
*
* {@snippet lang="java" :
* forwardChangeOfCRS = changeOfCRS.getMathTransform();
* // + wraparound handling if applicable.
* gridToCRS = source.getGridToCRS(anchor);
* gridToCRS = MathTransforms.concatenate(gridToCRS, forwardChangeOfCRS);
* }
*
* @see #gridToCRS()
*/
private MathTransform gridToCRS;
/**
* Transform from the target CRS to the source grid, with {@link WraparoundTransform} applied if needed.
* This is the concatenation of {@link #inverseChangeOfCRS} with inverse of {@link #source} "grid to CRS",
* possibly with wraparound handling:
*
* {@snippet lang="java" :
* inverseChangeOfCRS = forwardChangeOfCRS.inverse();
* // + wraparound handling if applicable.
* crsToGrid = gridToCRS.inverse();
* crsToGrid = MathTransforms.concatenate(inverseChangeOfCRS, crsToGrid);
* }
*
* @see #inverse()
* @see #applyWraparound(MathTransform)
*/
private MathTransform crsToGrid;
/**
* Whether the {@link #isWraparoundNeeded} value has been determined. This flag controls whether to perform
* a more extensive check of wraparound occurrence. This flag should be {@code false} the first time that
* {@link #inverse()} is invoked and {@code true} the next time.
*/
private boolean isWraparoundNeedVerified;
/**
* Whether {@link #inverseChangeOfCRS} needs to include a {@link WraparoundTransform} step. We do this check
* only for {@link #inverseChangeOfCRS} because it is the transform which will be executed for every pixels.
* By contrast, {@link #forwardChangeOfCRS} will systematically contain a {@link WraparoundTransform} step
* because we use it only for transforming envelopes and for the test that determines the value of
* this {@code isWraparoundNeeded} flag.
*/
private boolean isWraparoundNeeded;
/**
* Whether {@link WraparoundTransform} has been applied on {@link #inverseChangeOfCRS}. This field complements
* {@link #isWraparoundNeeded} because a delay may exist between the time we detected that wraparound is needed
* and the time we applied the necessary operation steps.
*
* <p>Note that despite this field name, a {@code true} value does not imply that {@link #inverseChangeOfCRS}
* and {@link #crsToGrid} transforms really contain some {@link WraparoundTransform} steps. It only means that
* the {@link WraparoundApplicator#forDomainOfUse WraparoundApplicator.forDomainOfUse(…)} method has been invoked.
* That method may have decided to not insert any wraparound steps.</p>
*
* @see #applyWraparound(MathTransform)
*/
private boolean isWraparoundApplied;
/**
* Whether to disable completely all wraparounds checks.
* If {@code true}, then calculation done in this class should be equivalent to following code:
*
* {@snippet lang="java" :
* forwardChangeOfCRS = changeOfCRS.getMathTransform();
* inverseChangeOfCRS = forwardChangeOfCRS.inverse();
* gridToCRS = source.getGridToCRS(anchor);
* crsToGrid = gridToCRS.inverse();
* gridToCRS = MathTransforms.concatenate(gridToCRS, forwardChangeOfCRS);
* crsToGrid = MathTransforms.concatenate(inverseChangeOfCRS, crsToGrid);
* }
*
* <b>Tip:</b> searching usage of this field should help to identify code doing wraparound handling.
*
* @see #nowraparound()
*/
private boolean isWraparoundDisabled;
/**
* Creates a new finder initialized to {@link PixelInCell#CELL_CORNER} anchor.
*
* @param source the grid geometry which is the source of the coordinate operation to find.
* @param target the grid geometry which is the target of the coordinate operation to find.
*/
CoordinateOperationFinder(final GridGeometry source, final GridGeometry target) {
this.source = source;
this.target = target;
this.anchor = PixelInCell.CELL_CORNER;
}
/**
* Verifies the presence of a CRS considered mandatory,
* unless the CRS of the opposite grid is also missing.
*
* @param rs {@code true} is source CRS is mandatory, {@code false} if target CRS is mandatory.
* @throws IncompleteGridGeometryException if a mandatory CRS is missing.
*/
final void verifyPresenceOfCRS(final boolean rs) {
if ((rs ? target : source).isDefined(GridGeometry.CRS)) {
if ((rs ? source : target).getCoordinateReferenceSystem() == null) {
throw new IncompleteGridGeometryException();
}
}
}
/**
* Sets whether operations will be between cell centers or cell corners.
* This method must be invoked before any other method in this class.
* The {@link PixelInCell#CELL_CORNER} value should be used first
* in order to cache values computed relative to pixel corners.
*
* @param newValue whether operations will be between cell centers or cell corners.
*/
final void setAnchor(final PixelInCell newValue) {
/*
* If `coordinates` is non-null, it means that `CoordinateOperationContext.getConstantCoordinates()`
* has been invoked during previous `gridToCRS()` execution, which implies that `changeOfCRS` depends
* on the `anchor` value. In such case we will need to recompute all fields that depend on `changeOfCRS`.
*/
anchor = newValue;
gridToCRS = null;
crsToGrid = null;
if (coordinates != null) {
coordinates = null;
changeOfCRS = null;
forwardChangeOfCRS = null;
inverseChangeOfCRS = null;
knowChangeOfCRS = false;
// Do not clear `isWraparoundNeeded`; its value is still valid.
}
}
/**
* Disables completely all wraparounds operation.
*
* @see #isWraparoundDisabled
*/
final void nowraparound() {
gridToCRS = null; // For forcing recomputation.
crsToGrid = null;
forwardChangeOfCRS = null;
inverseChangeOfCRS = null;
isWraparoundNeeded = false;
isWraparoundApplied = true;
isWraparoundNeedVerified = true;
isWraparoundDisabled = true;
}
/**
* Returns the target of the "corner to CRS" transform.
* May be {@code null} if the neither the source and target grid geometry define a CRS.
*/
final CoordinateReferenceSystem getTargetCRS() {
return (changeOfCRS != null) ? changeOfCRS.getTargetCRS() :
source.isDefined(GridGeometry.CRS) ? source.getCoordinateReferenceSystem() : null;
}
/**
* Returns the coordinate operation from source CRS to target CRS. It may be the identity operation.
* We try to take envelopes in account because the operation choice may depend on the geographic area.
*
* @todo Specify also the desired resolution, computed from target grid geometry. It will require
* more direct use of {@link org.apache.sis.referencing.operation.CoordinateOperationContext}.
* As a side effect, we could remove {@link CoordinateOperations#CONSTANT_COORDINATES} hack.
*
* @return operation from source CRS to target CRS, or {@code null} if a CRS is not specified.
* @throws FactoryException if no operation can be found between the source and target CRS.
* @throws TransformException if some coordinates cannot be transformed to the specified target.
*/
private CoordinateOperation changeOfCRS() throws FactoryException, TransformException {
if (!knowChangeOfCRS) {
final Envelope sourceEnvelope = source.envelope;
final Envelope targetEnvelope = target.envelope;
try {
CoordinateOperations.CONSTANT_COORDINATES.set(this);
if (sourceEnvelope != null && targetEnvelope != null) {
changeOfCRS = Envelopes.findOperation(sourceEnvelope, targetEnvelope);
}
if (changeOfCRS == null && source.isDefined(GridGeometry.CRS) && target.isDefined(GridGeometry.CRS)) {
final CoordinateReferenceSystem sourceCRS = source.getCoordinateReferenceSystem();
/*
* Unconditionally create operation even if CRS are the same. A non-null operation trig
* the check for wraparound axes, which is necessary even if the transform is identity.
*/
DefaultGeographicBoundingBox areaOfInterest = null;
if (sourceEnvelope != null || targetEnvelope != null) try {
areaOfInterest = new DefaultGeographicBoundingBox();
areaOfInterest.setBounds(targetEnvelope != null ? targetEnvelope : sourceEnvelope);
} catch (TransformException e) {
areaOfInterest = null;
recoverableException("changeOfCRS", e);
}
changeOfCRS = CRS.findOperation(sourceCRS, target.getCoordinateReferenceSystem(), areaOfInterest);
}
} catch (BackingStoreException e) { // May be thrown by getConstantCoordinates().
throw e.unwrapOrRethrow(TransformException.class);
} finally {
CoordinateOperations.CONSTANT_COORDINATES.remove();
}
knowChangeOfCRS = true;
}
return changeOfCRS;
}
/**
* Computes the transform from “grid coordinates of the source” to “grid coordinates of the target”.
* This is a concatenation of {@link #gridToCRS()} with target "CRS to grid" transform.
*
* @return operation from source grid indices to target grid indices.
* @throws FactoryException if no operation can be found between the source and target CRS.
* @throws TransformException if some coordinates cannot be transformed to the specified target.
* @throws IncompleteGridGeometryException if required CRS or a "grid to CRS" information is missing.
*/
final MathTransform gridToGrid() throws FactoryException, TransformException {
final MathTransform step1 = gridToCRS();
final MathTransform step2 = target.getGridToCRS(anchor);
if (step1.equals(step2)) { // Optimization for a common case.
return MathTransforms.identity(step1.getSourceDimensions());
} else {
return MathTransforms.concatenate(step1, step2.inverse());
}
}
/**
* Computes the transform from “grid coordinates of the source” to “geospatial coordinates of the target”.
* It may be the identity operation. We try to take envelopes in account because the operation choice may
* depend on the geographic area.
*
* <p>The transform returned by this method applies wraparound checks systematically on every axes having
* wraparound range. This method does not verify whether those checks are needed (i.e. whether wraparound
* can possibly happen). This is okay because this transform is used only for transforming envelopes;
* it is not used for transforming pixel coordinates.</p>
*
* <h4>Implementation note</h4>
* After invocation of this method, the following fields are valid:
* <ul>
* <li>{@link #changeOfCRS} — cached for {@link #inverse()} usage.</li>
* <li>{@link #forwardChangeOfCRS} — cached for next invocation of this {@code gridToCRS()} method.</li>
* </ul>
*
* @return operation from source grid indices to target geospatial coordinates.
* @throws FactoryException if no operation can be found between the source and target CRS.
* @throws TransformException if some coordinates cannot be transformed to the specified target.
* @throws IncompleteGridGeometryException if required CRS or a "grid to CRS" information is missing.
*/
final MathTransform gridToCRS() throws FactoryException, TransformException {
if (gridToCRS == null) {
/*
* The following line may throw IncompleteGridGeometryException, which is desired because
* if that transform is missing, we cannot continue (we have no way to guess it).
*/
gridToCRS = source.getGridToCRS(anchor);
final CoordinateOperation changeOfCRS = changeOfCRS();
if (changeOfCRS != null) {
/*
* At this point we have a "grid → source CRS" transform. Append a "source CRS → target CRS" transform,
* which may be identity. A wraparound may be applied for keeping target coordinates inside the expected
* target domain.
*/
apply: if (forwardChangeOfCRS == null) {
forwardChangeOfCRS = changeOfCRS.getMathTransform();
if (!isWraparoundDisabled) {
DirectPosition sourceMedian = median(source, forwardChangeOfCRS);
DirectPosition targetMedian = median(target, null);
if (targetMedian == null) {
if (sourceMedian == null) {
break apply;
}
targetMedian = sourceMedian;
sourceMedian = null;
}
final WraparoundApplicator ap = new WraparoundApplicator(sourceMedian,
targetMedian, changeOfCRS.getTargetCRS().getCoordinateSystem());
forwardChangeOfCRS = ap.forDomainOfUse(forwardChangeOfCRS);
}
}
gridToCRS = MathTransforms.concatenate(gridToCRS, forwardChangeOfCRS);
}
}
return gridToCRS;
}
/**
* Computes the transform from “geospatial coordinates of the target” to “grid coordinates of the source”.
* This is similar to invoking {@link MathTransform#inverse()} on {@link #gridToCRS()}, except in the way
* wraparounds are handled.
*
* @return operation from target geospatial coordinates to source grid indices.
* @throws FactoryException if no operation can be found between the source and target CRS.
* @throws TransformException if some coordinates cannot be transformed.
*/
final MathTransform inverse() throws FactoryException, TransformException {
final MathTransform sourceCrsToGrid = source.getGridToCRS(anchor).inverse();
final CoordinateOperation changeOfCRS = changeOfCRS();
if (changeOfCRS == null) {
return sourceCrsToGrid;
}
if (inverseChangeOfCRS == null) {
inverseChangeOfCRS = changeOfCRS.getMathTransform().inverse();
if (!isWraparoundDisabled) {
isWraparoundApplied = false;
if (!isWraparoundNeedVerified) {
isWraparoundNeedVerified = true;
/*
* Need to compute transform with wraparound checks, but contrarily to `gridToCRS()` we do not want
* `WraparoundTransform` to be systematically inserted. This is for performance reasons, because the
* transform returned by this method will be applied on every pixels of destination image. We create
* both transforms with and without wraparound, and check if their results differ.
*
* We give precedence to corners specified by target extent. The corners specified by source extent
* are used only as a fallback if the target extent has not been specified, in which case we assume
* that caller will fallback on source extent transformed to target coordinates. The target extent
* is preferred because it may cover only a sub-region of the source, or conversely it may be world.
* If smaller, wraparound may become useless (i.e. sub-region may not cross anti-meridian anymore).
* If larger with [-180 … +180]° longitude range, the use of source extent may fail to detect that
* a part of the raster need to be rendered on each side of the [-180 … +180]° range.
*/
final MathTransform inverseNoWrap = inverseChangeOfCRS;
final MathTransform crsToGridNoWrap = MathTransforms.concatenate(inverseNoWrap, sourceCrsToGrid);
if (target.isDefined(GridGeometry.EXTENT | GridGeometry.CRS)) {
if (applyWraparound(sourceCrsToGrid)) {
isWraparoundNeeded = isWraparoundNeeded(target.getExtent(),
target.getGridToCRS(anchor), crsToGridNoWrap, null);
}
} else if (source.isDefined(GridGeometry.EXTENT)) {
isWraparoundNeeded = isWraparoundNeeded(source.getExtent(),
gridToCRS(), crsToGridNoWrap, sourceCrsToGrid);
}
if (!isWraparoundNeeded) {
inverseChangeOfCRS = inverseNoWrap; // Discard the transform that was applying wraparound.
crsToGrid = crsToGridNoWrap;
}
}
if (isWraparoundNeeded) {
applyWraparound(sourceCrsToGrid); // Update `inverseChangeOfCRS` if possible.
}
}
}
/*
* Here, `inverseChangeOfCRS` already contains the wraparound step if needed.
*/
if (crsToGrid == null) {
crsToGrid = MathTransforms.concatenate(inverseChangeOfCRS, sourceCrsToGrid);
}
return crsToGrid;
}
/**
* Verifies whether wraparound is needed for a "CRS to grid" transform.
* This method converts coordinates of all corners of a grid (source or target) to the target CRS,
* then (potentially back) to the source grid. This method uses one transform applying wraparounds
* and another transform without wraparounds. By checking whether grid coordinates are equal with
* both transforms, we determine if wraparound is necessary or not.
*
* @param extent the grid extent which is providing all corners to project.
* @param extentToCRS transform from {@code extent} to target CRS.
* @param crsToGridNoWrap inverse of {@code gridToCRS} but without handling of wraparound axes.
* @param sourceCrsToGrid if {@code extent} is {@link #target} extent, shall be {@code null}.
* If {@code extent} is {@link #source} extent, shall be the transform to
* concatenate with {@link #inverseChangeOfCRS} for creating {@link #crsToGrid}.
* @return whether wraparound transform seems needed.
* @throws TransformException if an error occurred while transforming coordinates.
*/
private boolean isWraparoundNeeded(final GridExtent extent, final MathTransform extentToCRS,
final MathTransform crsToGridNoWrap, final MathTransform sourceCrsToGrid)
throws FactoryException, TransformException
{
final boolean mapCorner = (anchor == PixelInCell.CELL_CORNER);
final int extentDim = extent.getDimension();
final int gridDim = crsToGridNoWrap.getTargetDimensions();
final double[] buffer = new double[Math.max(extentToCRS.getTargetDimensions(), gridDim)];
final double[] reference = new double[Math.max(extentDim, gridDim)];
final double[] withoutWrap = new double[gridDim];
long maskOfUppers = Numerics.bitmask(extentDim);
while (--maskOfUppers != 0) {
for (int i=0; i<extentDim; i++) {
final long bit = 1L << i;
long cc; // Grid coordinate of a corner.
if ((maskOfUppers & bit) == 0) {
cc = extent.getLow(i);
} else {
cc = extent.getHigh(i); // Inclusive.
if (mapCorner && cc != Long.MAX_VALUE) cc++; // Make exclusive.
}
reference[i] = cc;
}
/*
* Transform corner from the extent to target CRS, then (potentially back) to source grid coordinates.
* We do not concatenate the forward and inverse transforms because we do not want MathTransformFactory
* to simplify the transformation chain (e.g. replacing "Mercator → Inverse of Mercator" by an identity
* transform), because such simplification would erase wraparound effects.
*/
extentToCRS.transform(reference, 0, buffer, 0, 1); // To coordinates in target CRS.
crsToGridNoWrap.transform(buffer, 0, withoutWrap, 0, 1); // To coordinates in source grid.
/*
* The reference must be a corner in the `source` grid. If the given extent was from `target` grid,
* convert to source grid coordinates by completing the "target → CRS → source" chain of transforms.
* The `crsToGrid` transform includes the wraparound, contrarily to `crsToGridNoWrap` used above.
*/
if (sourceCrsToGrid == null) {
// `applyWraparound()` already invoked by caller.
crsToGrid.transform(buffer, 0, reference, 0, 1);
}
/*
* Compare coordinates without wraparound with the reference. If they differ, we may consider that
* wraparounds are needed.
*/
boolean isBufferTransformed = false;
for (int i=0; i<gridDim; i++) {
final double error = Math.abs(withoutWrap[i] - reference[i]);
if (!(error <= 1)) { // Use `!` for catching NaN.
if (sourceCrsToGrid == null) {
/*
* If the `extent` specified in argument was the target extent, then the `reference`
* corner has already been computed using a transform that include wraparound checks.
* We do not consider NaN in `reference` as a need for wraparound because NaN values
* may occur when an operation between two CRS reduces the number of dimensions.
*/
if (!Double.isNaN(reference[i])) {
return true;
}
} else {
/*
* If the `extent` specified in argument was the source extent, then the `reference`
* corner has been computed with a transform that does not use `inverseChangeOfCRS`.
* So it may be worth to double-check with another calculation of the corner, this
* time including wraparound steps.
*/
if (!isBufferTransformed) {
isBufferTransformed = true;
if (!applyWraparound(sourceCrsToGrid)) {
return false;
}
crsToGrid.transform(buffer, 0, buffer, 0, 1);
}
if (Math.abs(buffer[i] - reference[i]) < (error <= Double.MAX_VALUE ? error : 1)) {
return true;
}
}
}
}
}
return false;
}
/**
* Inserts {@link WraparoundTransform} steps into {@link #inverseChangeOfCRS} transform if possible.
* IF this method returns {@code true}, then the {@link #inverseChangeOfCRS} and {@link #crsToGrid}
* fields have been updated to transforms applying wraparound.
*
* @param sourceCrsToGrid value of {@code source.getGridToCRS(anchor).inverse()}.
* @return whether at least one wraparound step has been added.
* @throws TransformException if some coordinates cannot be transformed.
*/
private boolean applyWraparound(final MathTransform sourceCrsToGrid) throws FactoryException, TransformException {
if (!isWraparoundApplied) {
isWraparoundApplied = true;
DirectPosition sourceMedian = median(source, null);
DirectPosition targetMedian = median(target, inverseChangeOfCRS);
if (sourceMedian == null) {
sourceMedian = targetMedian;
targetMedian = null;
}
if (sourceMedian != null) {
final MathTransform inverseNoWrap = inverseChangeOfCRS;
final WraparoundApplicator ap = new WraparoundApplicator(targetMedian,
sourceMedian, changeOfCRS().getSourceCRS().getCoordinateSystem());
inverseChangeOfCRS = ap.forDomainOfUse(inverseNoWrap);
if (inverseChangeOfCRS != inverseNoWrap) {
crsToGrid = MathTransforms.concatenate(inverseChangeOfCRS, sourceCrsToGrid);
return true;
}
}
}
return false;
}
/**
* Returns the point of interest converted to the Coordinate Reference System.
* If the grid does not define a point of interest or does not define a CRS,
* then this method returns {@code null}.
*
* @param grid the source or target grid providing the point of interest.
* @param changeOfCRS transform from source CRS to target CRS, or {@code null} if none.
*/
private static DirectPosition median(final GridGeometry grid, final MathTransform changeOfCRS) throws TransformException {
if (!grid.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS)) {
return null;
}
return new AbstractDirectPosition() {
/** The coordinates, computed when first needed. */
private double[] coordinates;
/** Returns the number of dimensions. */
@Override public int getDimension() {
return coordinates().length;
}
/** Returns the coordinate tuple, computed when first needed. */
@SuppressWarnings("ReturnOfCollectionOrArrayField")
private double[] coordinates() {
if (coordinates == null) try {
final double[] poi = grid.getExtent().getPointOfInterest(PixelInCell.CELL_CENTER);
MathTransform tr = grid.getGridToCRS(PixelInCell.CELL_CENTER);
if (changeOfCRS != null) {
tr = MathTransforms.concatenate(tr, changeOfCRS);
}
coordinates = new double[tr.getTargetDimensions()];
tr.transform(poi, 0, coordinates, 0, 1);
} catch (TransformException e) {
throw new BackingStoreException(e);
}
return coordinates;
}
/**
* Returns the median rounded to a value having an exact representation in base 2 using about 10 bits.
* The intent is to reduce the risk of rounding errors with add/subtract operations.
*/
@Override public double getCoordinate(final int i) {
final double m = coordinates()[i];
final int power = 10 - Math.getExponent(m);
return Math.scalb(Math.rint(Math.scalb(m, power)), -power);
}
};
}
/**
* Invoked when the target CRS has some dimensions that the source CRS does not have.
* For example, this is invoked during the conversion from (<var>x</var>, <var>y</var>)
* coordinates to (<var>x</var>, <var>y</var>, <var>t</var>). If constant values can
* be given to the missing dimensions, than those values are returned. Otherwise this
* method returns {@code null}.
*
* <p>The returned array has a length equals to the number of dimensions in the target CRS.
* Only coordinates in dimensions without source (<var>t</var> in the above example) will be used.
* All other coordinate values will be ignored.</p>
*
* @see org.apache.sis.referencing.operation.CoordinateOperationContext#getConstantCoordinates()
*/
@Override
@SuppressWarnings("ReturnOfCollectionOrArrayField")
public double[] get() {
if (coordinates == null && target.isDefined(GridGeometry.EXTENT | GridGeometry.GRID_TO_CRS)) {
final MathTransform tr = target.getGridToCRS(anchor);
coordinates = new double[tr.getTargetDimensions()];
double[] gc = new double[tr.getSourceDimensions()];
Arrays.fill(gc, Double.NaN);
final GridExtent extent = target.getExtent();
for (int i=0; i<gc.length; i++) {
final long low = extent.getLow(i);
if (low == extent.getHigh(i)) {
gc[i] = low;
}
}
/*
* At this point, the only grid coordinates with finite values are the ones where the
* grid size is one cell (i.e. conversion to target CRS can produce only one value).
* After conversion with `getGidToCRS(anchor)`, the corresponding target dimensions
* will have non-NaN coordinate values only if they do not depend on any dimension
* other than the one having a grid size of 1.
*/
try {
tr.transform(gc, 0, coordinates, 0, 1);
} catch (TransformException e) {
throw new BackingStoreException(e);
}
}
return coordinates;
}
/**
* Configures the accuracy hints on the given processor.
*
* <h4>Prerequisite</h4>
* This method assumes that {@link #gridToCRS()} or {@link #inverse()}
* has already been invoked before this method.
*/
final void setAccuracyOf(final ImageProcessor processor) {
final double accuracy = CRS.getLinearAccuracy(changeOfCRS);
if (accuracy > 0) {
Length qm = Quantities.create(accuracy, Units.METRE);
Quantity<?>[] hints = processor.getPositionalAccuracyHints(); // Array is already a copy.
for (int i=0; i<hints.length; i++) {
if (Units.isLinear(hints[i].getUnit())) {
hints[i] = qm;
qm = null;
}
}
if (qm != null) {
hints = ArraysExt.append(hints, qm);
}
processor.setPositionalAccuracyHints(hints); // Null elements will be ignored.
}
}
/**
* Invoked when an ignorable exception occurred.
*
* @param caller the method where the exception occurred.
* @param e the ignorable exception.
*/
private static void recoverableException(final String caller, final Exception e) {
Logging.recoverableException(GridExtent.LOGGER, CoordinateOperationFinder.class, caller, e);
}
}