blob: 89880ba706fc537000dc261dd913f2dd3fc3183e [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.Locale;
import java.util.Objects;
import java.util.Optional;
import java.time.Instant;
import java.text.NumberFormat;
import java.math.RoundingMode;
import java.io.Serializable;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.awt.image.RenderedImage; // For javadoc only.
import org.opengis.util.FactoryException;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.geometry.Envelope;
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.opengis.referencing.crs.DerivedCRS;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.cs.CoordinateSystem;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.measure.AngleFormat;
import org.apache.sis.measure.Latitude;
import org.apache.sis.measure.Longitude;
import org.apache.sis.geometry.Envelopes;
import org.apache.sis.geometry.GeneralEnvelope;
import org.apache.sis.geometry.ImmutableEnvelope;
import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.crs.AbstractCRS;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.MathTransforms;
import org.apache.sis.referencing.operation.transform.LinearTransform;
import org.apache.sis.referencing.operation.transform.PassThroughTransform;
import org.apache.sis.referencing.privy.ExtendedPrecisionMatrix;
import org.apache.sis.referencing.privy.DirectPositionView;
import org.apache.sis.referencing.privy.TemporalAccessor;
import org.apache.sis.referencing.privy.AxisDirections;
import org.apache.sis.metadata.privy.ReferencingServices;
import org.apache.sis.feature.internal.Resources;
import org.apache.sis.util.LenientComparable;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Debug;
import org.apache.sis.util.privy.DoubleDouble;
import org.apache.sis.util.privy.Numerics;
import org.apache.sis.util.collection.TreeTable;
import org.apache.sis.util.collection.TableColumn;
import org.apache.sis.util.collection.DefaultTreeTable;
import org.apache.sis.util.collection.BackingStoreException;
import org.apache.sis.util.resources.Vocabulary;
import org.apache.sis.util.resources.Messages;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.io.TableAppender;
import org.apache.sis.xml.NilObject;
import org.apache.sis.xml.NilReason;
import static org.apache.sis.referencing.CRS.findOperation;
// Specific to the geoapi-3.1 and geoapi-4.0 branches:
import org.opengis.coordinate.MismatchedDimensionException;
/**
* Valid extent of grid coordinates together with the transform from those grid coordinates
* to real world coordinates. {@code GridGeometry} contains:
*
* <ul class="verbose">
* <li>A {@linkplain #getExtent() grid extent} (a.k.a. <i>grid envelope</i>),
* often inferred from the {@link RenderedImage} size.</li>
* <li>A {@linkplain #getGridToCRS grid to CRS} transform,
* which may be inferred from the grid extent and the georeferenced envelope.</li>
* <li>A {@linkplain #getEnvelope() georeferenced envelope}, which can be inferred
* from the grid extent and the <i>grid to CRS</i> transform.</li>
* <li>An optional {@linkplain #getCoordinateReferenceSystem() Coordinate Reference System} (CRS)
* specified as part of the georeferenced envelope.
* This CRS is the target of the <i>grid to CRS</i> transform.</li>
* <li>An <em>estimation</em> of {@link #getResolution(boolean) grid resolution} along each CRS axes,
* computed from the <i>grid to CRS</i> transform and eventually from the grid extent.</li>
* <li>An {@linkplain #isConversionLinear indication of whether conversion for some axes is linear or not}.</li>
* </ul>
*
* The first three properties should be mandatory,
* but are allowed to be temporarily absent during grid coverage construction.
* Temporarily absent properties are allowed because they may be inferred from a wider context.
* For example, a {@code GridGeometry} knows nothing about {@link RenderedImage},
* but {@code GridCoverage2D} has this information and may use it for providing a missing grid extent.
* By default, any request for an undefined property will throw an {@link IncompleteGridGeometryException}.
* In order to check if a property is defined, use {@link #isDefined(int)}.
*
* <h2>Non-linear referencing</h2>
* A key property is the {@linkplain #getGridToCRS(PixelInCell) "grid to CRS"} conversion,
* which defines how to map pixel coordinates to "real world" coordinates such as latitudes and longitudes.
* This relationship is often linear (an affine transform), but {@linkplain #isConversionLinear not necessarily};
* {@code GridGeometry} accepts non-linear conversions as well. Non-linear conversions may occur with images
* using {@linkplain org.apache.sis.referencing.operation.builder.LocalizationGridBuilder localization grids},
* but non-linear conversions should not be used for expressing map projections (projections should be specified
* in the {@linkplain #getCoordinateReferenceSystem() Coordinate Reference System} (CRS) instead).
*
* <p>Some applications cannot handle non-linear "grid to CRS" conversions.
* For example, encoding an image in a GeoTIFF file is much simpler if the "grid to CRS" conversion is linear.
* The {@link DomainLinearizer} class can be used for replacing non-linear conversions by linear approximations.</p>
*
* <h2>Multi-threading</h2>
* {@code GridGeometry} instances are immutable and thread-safe.
* The same instance can be shared by different {@link GridCoverage} instances.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @author Johann Sorel (Geomatys)
* @version 1.5
* @since 1.0
*/
public class GridGeometry implements LenientComparable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -954786616001606624L;
/**
* A bitmask to specify the validity of the Coordinate Reference System property.
*
* @see #isDefined(int)
* @see #getCoordinateReferenceSystem()
*/
public static final int CRS = 1;
/**
* A bitmask to specify the validity of the geodetic envelope property.
*
* @see #isDefined(int)
* @see #getEnvelope()
*/
public static final int ENVELOPE = 2;
/**
* A bitmask to specify the validity of the grid extent property.
*
* @see #isDefined(int)
* @see #getExtent()
*/
public static final int EXTENT = 4;
/**
* A bitmask to specify the validity of the <q>grid to CRS</q> transform.
*
* @see #isDefined(int)
* @see #getGridToCRS(PixelInCell)
*/
public static final int GRID_TO_CRS = 8;
/**
* A bitmask to specify the validity of the grid resolution.
*
* @see #isDefined(int)
* @see #getResolution(boolean)
*/
public static final int RESOLUTION = 16;
/**
* A bitmask to specify the validity of the geographic bounding box.
* This information can sometimes be derived from the envelope and the CRS.
* It is an optional element even with a complete grid geometry since the
* coordinate reference system is not required to have an horizontal component.
*
* @see #getGeographicExtent()
*/
public static final int GEOGRAPHIC_EXTENT = 32;
/**
* A bitmask to specify the validity of the temporal period.
* This information can sometimes be derived from the envelope and the CRS.
* It is an optional element even with a complete grid geometry since the
* coordinate reference system is not required to have a temporal component.
*
* @see #getTemporalExtent()
*/
public static final int TEMPORAL_EXTENT = 64;
/**
* The valid domain of a grid coverage, or {@code null} if unknown. The lowest valid grid coordinate is zero
* for {@link java.awt.image.BufferedImage}, but may be non-zero for arbitrary {@link RenderedImage}.
* A grid with 512 cells can have a minimum coordinate of 0 and maximum of 511.
*
* @see #EXTENT
* @see #getExtent()
*/
protected final GridExtent extent;
/**
* The geodetic envelope, or {@code null} if unknown. If non-null, this envelope is usually the grid {@link #extent}
* {@linkplain #gridToCRS transformed} to real world coordinates. The Coordinate Reference System} (CRS) of this
* envelope defines the "real world" CRS of this grid geometry.
*
* @see #ENVELOPE
* @see #getEnvelope()
*/
protected final ImmutableEnvelope envelope;
/**
* The conversion from grid indices to "real world" coordinates, or {@code null} if unknown.
* If non-null, the conversion shall map {@linkplain PixelInCell#CELL_CENTER cell center}.
* This conversion is usually, but not necessarily, affine.
*
* @see #CRS
* @see #getGridToCRS(PixelInCell)
* @see PixelInCell#CELL_CENTER
*/
@SuppressWarnings("serial") // Most SIS implementations are serializable.
protected final MathTransform gridToCRS;
/**
* Same conversion as {@link #gridToCRS} but from {@linkplain PixelInCell#CELL_CORNER cell corner}
* instead of center. This transform is preferable to {@code gridToCRS} for transforming envelopes.
*
* @serial This field is serialized because it may be a value specified explicitly at construction time,
* in which case it can be more accurate than a computed value.
*/
@SuppressWarnings("serial") // Most SIS implementations are serializable.
final MathTransform cornerToCRS;
/**
* An <em>estimation</em> of the grid resolution, in units of the CRS axes.
* Computed from {@link #gridToCRS}, eventually together with {@link #extent}.
* May be {@code null} if unknown. If non-null, the array length is equal to
* the number of CRS dimensions.
*
* @see #RESOLUTION
* @see #getResolution(boolean)
*/
protected final double[] resolution;
/**
* Whether the conversions from grid coordinates to the CRS are linear, for each target axis.
* The bit located at {@code 1L << dimension} is set to 1 when the conversion at that dimension is non-linear.
* The dimension indices are those of the CRS, not the grid. The use of {@code long} type limits the capacity
* to 64 dimensions. But actually {@code GridGeometry} can contain more dimensions provided that index of the
* last non-linear dimension is not greater than 64.
*
* @see #isConversionLinear(int...)
*/
final long nonLinears;
/**
* The geographic bounding box as an unmodifiable metadata instance, or {@code null} if not yet computed.
* If no geographic extent can be computed for the {@linkplain #envelope}, then this is set to {@link NilObject}.
*
* @see #GEOGRAPHIC_EXTENT
* @see #getGeographicExtent()
*/
private transient volatile GeographicBoundingBox geographicBBox;
/**
* The start time and end time, or {@code null} if not yet computed. If there is no time range, then the array is empty.
* If there is only a start time or an end time, then the array length is 1. Otherwise the array length is 2.
*
* @see #TEMPORAL_EXTENT
* @see #getTemporalExtent()
*/
@SuppressWarnings("VolatileArrayField") // Safe because array will not be modified after construction.
private transient volatile Instant[] timeRange;
/**
* An "empty" grid geometry with no value defined. All getter methods invoked on this instance will cause
* {@link IncompleteGridGeometryException} to be thrown. This instance can be used as a place-holder when
* the grid geometry cannot be obtained.
*/
public static final GridGeometry UNDEFINED = new GridGeometry();
/**
* Constructor for {@link #UNDEFINED} singleton only.
*/
private GridGeometry() {
extent = null;
gridToCRS = null;
cornerToCRS = null;
envelope = null;
resolution = null;
nonLinears = 0;
}
/**
* Creates a new grid geometry with the same values as the given grid geometry.
* This is a copy constructor for subclasses.
*
* @param other the other grid geometry to copy.
*/
protected GridGeometry(final GridGeometry other) {
extent = other.extent;
gridToCRS = other.gridToCRS;
cornerToCRS = other.cornerToCRS;
envelope = other.envelope;
resolution = other.resolution;
nonLinears = other.nonLinears;
}
/**
* Creates a new grid geometry derived from the given grid geometry with a new extent and a modified transform.
* This constructor is used for creating a grid geometry over a subregion (for example with the grid extent
* computed by {@link GridDerivation#subgrid(Envelope, double...)}) or grid geometry for a subsampled raster.
*
* <h4>Conversion between old and new grid geometry</h4>
* If {@code toOther} is non-null, it defines the conversion from grid coordinates of given {@code extent} to grid
* coordinates of {@code other}. That transform should be a {@linkplain MathTransforms#scale(double...) scale} and
* a {@linkplain MathTransforms#translation(double...) translation} only, but more complex transforms are accepted.
* The {@link #cornerToCRS} transform of the new grid geometry will be set to the following concatenation:
*
* <blockquote>{@code this.cornerToCRS} = {@code toOther} → {@code other.cornerToCRS}</blockquote>
*
* The new {@linkplain #getEnvelope() grid geometry envelope} will be computed from the new extent and transform,
* then {@linkplain GeneralEnvelope#intersect(Envelope) clipped} to the envelope of the other grid geometry.
* This clip is for preventing the envelope to become larger under the effect of subsampling because
* {@linkplain GridExtent#subsample(int[]) each cell become larger}. The clip is not applied when {@code toOther}
* is {@code null} because in such case, we presume that the grid extent has been changed for another reason than
* subsampling (e.g. application of a margin, in which case we want the envelope to be expanded).
*
* @param other the other grid geometry to copy.
* @param extent the new extent for the grid geometry to construct, or {@code null} if none.
* @param toOther transform from this grid coordinates to {@code other} grid coordinates, or {@code null} if none.
* @throws NullPointerException if {@code extent} is {@code null} and the other grid geometry contains no other information.
* @throws TransformException if the math transform cannot compute the geospatial envelope from the grid extent.
*
* @see GridDerivation#subgrid(Envelope, double...)
*
* @since 1.2
*/
public GridGeometry(final GridGeometry other, final GridExtent extent, final MathTransform toOther) throws TransformException {
final int dimension = other.getDimension();
this.extent = extent;
ensureDimensionMatches(dimension, extent);
if (toOther == null || toOther.isIdentity()) {
gridToCRS = other.gridToCRS;
cornerToCRS = other.cornerToCRS;
resolution = other.resolution;
nonLinears = other.nonLinears;
} else {
/*
* The `toOther` transform applies on `cornerToCRS` because the corner of upper-left pixel before scaling
* is still the corner of upper-left pixel after scaling, while "pixel center" is no longer the center of
* the same pixel. We adjust `toOther` instead of invoking `PixelTranslation.translate(cornerToCRS, …)`
* because we do not know which of `cornerToCRS` or `gridToCRS` has less NaN values.
*/
if (other.gridToCRS != null) {
final MathTransform centerShift = MathTransforms.concatenate(
MathTransforms.uniformTranslation(dimension, +0.5), toOther,
MathTransforms.uniformTranslation(dimension, -0.5));
cornerToCRS = MathTransforms.concatenate(toOther, other.cornerToCRS);
gridToCRS = MathTransforms.concatenate(centerShift, other.gridToCRS);
resolution = resolution(gridToCRS, extent, PixelInCell.CELL_CENTER);
nonLinears = findNonLinearTargets(gridToCRS);
} else {
cornerToCRS = null;
gridToCRS = null;
resolution = resolution(toOther, extent, PixelInCell.CELL_CENTER); // Save resolution even if `gridToCRS` is null.
nonLinears = findNonLinearTargets(toOther);
}
}
/*
* Recompute the envelope and clip only if a sub-sampling may have been applied (toOther != null).
* The reason for clipping is because subsampling may cause cells to appear larger.
*/
@SuppressWarnings("LocalVariableHidesMemberVariable")
ImmutableEnvelope envelope = other.envelope; // We will share the same instance if possible.
ImmutableEnvelope computed = computeEnvelope(gridToCRS, getCoordinateReferenceSystem(envelope),
toOther == null ? null : envelope); // Clip.
if (computed == null || !computed.equals(envelope)) {
envelope = computed;
}
this.envelope = envelope;
if (envelope == null && gridToCRS == null) {
ArgumentChecks.ensureNonNull("extent", extent);
}
}
/*
* Do not provide convenience constructor without PixelInCell or PixelOrientation argument.
* Experience shows that 0.5 pixel offsets in image georeferencing is a recurrent problem.
* We really want to force developers to think about whether their `gridToCRS` transform
* locates pixel corner or pixel center.
*/
/**
* Creates a new grid geometry from a grid extent and a mapping from cell coordinates to "real world" coordinates.
* At least one of {@code extent}, {@code gridToCRS} or {@code crs} arguments shall be non-null.
* If {@code gridToCRS} is non-null, then {@code anchor} shall be non-null too with one of the following values:
*
* <ul>
* <li>{@link PixelInCell#CELL_CENTER} if conversions of cell indices by {@code gridToCRS} give "real world"
* coordinates close to the center of each cell.</li>
* <li>{@link PixelInCell#CELL_CORNER} if conversions of cell indices by {@code gridToCRS} give "real world"
* coordinates at the corner of each cell. The cell corner is the one for which all grid indices have the
* smallest values (closest to negative infinity).</li>
* </ul>
*
* <h4>API note</h4>
* There is no default value for {@code anchor} because experience shows that images shifted by ½ pixel
* (with pixels that may be tens of kilometres large) is a recurrent problem. We want to encourage developers
* to always think about wether their <i>grid to CRS</i> transform is mapping pixel corner or center.
*
* <div class="warning"><b>Upcoming API generalization:</b>
* the {@code extent} type of this method may be changed to {@code GridEnvelope} interface in a future Apache SIS version.
* This is pending <a href="https://github.com/opengeospatial/geoapi/issues/36">GeoAPI update</a>.
* In addition, the {@code PixelInCell} code list currently defined in the {@code org.opengis.referencing.datum} package
* may move in another package in a future GeoAPI version because this type is no longer defined by the ISO 19111 standard
* after the 2018 revision.</div>
*
* @param extent the valid extent of grid coordinates, or {@code null} if unknown.
* @param anchor {@linkplain PixelInCell#CELL_CENTER Cell center} for OGC conventions or
* {@linkplain PixelInCell#CELL_CORNER cell corner} for Java2D/JAI conventions.
* @param gridToCRS the mapping from grid coordinates to "real world" coordinates, or {@code null} if unknown.
* @param crs the coordinate reference system of the "real world" coordinates, or {@code null} if unknown.
* @throws NullPointerException if {@code extent}, {@code gridToCRS} and {@code crs} arguments are all null.
* @throws MismatchedDimensionException if the math transform and the CRS do not have consistent dimensions.
* @throws IllegalGridGeometryException if the math transform cannot compute the geospatial envelope or resolution from the grid extent.
*/
public GridGeometry(final GridExtent extent, final PixelInCell anchor, final MathTransform gridToCRS, final CoordinateReferenceSystem crs) {
if (gridToCRS != null) {
ensureDimensionMatches(gridToCRS.getSourceDimensions(), extent);
ArgumentChecks.ensureDimensionMatches("crs", gridToCRS.getTargetDimensions(), crs);
} else if (crs == null) {
ArgumentChecks.ensureNonNull("extent", extent);
}
try {
this.extent = extent;
this.gridToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
this.envelope = computeEnvelope(gridToCRS, crs, null); // `gridToCRS` specified by the user, not `this.gridToCRS`.
this.resolution = resolution(gridToCRS, extent, anchor); // `gridToCRS` or `cornerToCRS` does not matter here.
this.nonLinears = findNonLinearTargets(gridToCRS);
} catch (TransformException e) {
throw new IllegalGridGeometryException(e, "gridToCRS");
}
}
/**
* Computes the envelope with the given coordinate reference system. This method is invoked from constructors.
* The {@link #extent}, {@link #gridToCRS} and {@link #cornerToCRS} fields must be set before this method is invoked.
*
* @param specified the transform specified by the user. This is not necessarily {@link #gridToCRS}.
* @param crs the coordinate reference system to declare in the envelope. May be {@code null}.
* @param limits if non-null, intersect with that envelope. The CRS must be the same as {@code crs}.
*/
private ImmutableEnvelope computeEnvelope(final MathTransform specified, final CoordinateReferenceSystem crs,
final Envelope limits) throws TransformException
{
final GeneralEnvelope env;
if (extent != null && cornerToCRS != null) {
env = extent.toEnvelope(cornerToCRS, specified, limits);
env.setCoordinateReferenceSystem(crs);
if (limits != null) {
env.intersect(limits);
}
} else if (crs != null) {
env = new GeneralEnvelope(crs);
env.setToNaN();
} else {
return null;
}
return new ImmutableEnvelope(env);
}
/**
* Creates a new grid geometry from a geospatial envelope and a mapping from cell coordinates to "real world" coordinates.
* At least one of {@code gridToCRS} or {@code envelope} arguments shall be non-null.
* If {@code gridToCRS} is non-null, then {@code anchor} shall be non-null too with one of the values documented in the
* {@link #GridGeometry(GridExtent, PixelInCell, MathTransform, CoordinateReferenceSystem) constructor expecting a grid
* extent}.
*
* <p>The given envelope shall encompass all cell surfaces, from the left border of leftmost cell to the right border
* of the rightmost cell and similarly along other axes. This constructor tries to store a geospatial envelope close
* to the specified envelope, but there is no guarantee that the envelope returned by {@link #getEnvelope()} will be
* equal to the given envelope. The envelope stored in the new {@code GridGeometry} may be slightly smaller, larger or
* shifted because the floating point values used in geospatial envelope cannot always be mapped to the integer
* coordinates used in {@link GridExtent}.
* The rules for deciding whether coordinates should be rounded toward nearest integers,
* to {@linkplain Math#floor(double) floor} or to {@linkplain Math#ceil(double) ceil} values
* are specified by the {@link GridRoundingMode} argument.</p>
*
* <p>Because of the uncertainties explained in above paragraph, this constructor should be used only in last resort,
* when the grid extent is unknown. For determinist results, developers should prefer the
* {@linkplain #GridGeometry(GridExtent, PixelInCell, MathTransform, CoordinateReferenceSystem) constructor using grid extent}
* as much as possible. In particular, this constructor is not suitable for computing grid geometry of tiles in a tiled image,
* because the above-cited uncertainties may result in apparently random black lines between tiles.</p>
*
* <div class="warning"><b>Upcoming API change:</b>
* The {@code PixelInCell} code list currently defined in the {@code org.opengis.referencing.datum} package
* may move in another package in a future GeoAPI version because this type is no longer defined by the
* ISO 19111 standard after the 2018 revision. This code list may be taken by ISO 19123 in a future revision.</div>
*
* @param anchor {@linkplain PixelInCell#CELL_CENTER Cell center} for OGC conventions or
* {@linkplain PixelInCell#CELL_CORNER cell corner} for Java2D/JAI conventions.
* @param gridToCRS the mapping from grid coordinates to "real world" coordinates, or {@code null} if unknown.
* @param envelope the geospatial envelope, including its coordinate reference system if available.
* There is no guarantee that the envelope actually stored in the {@code GridGeometry}
* will be equal to this specified envelope.
* @param rounding controls behavior of rounding from floating point values to integers.
* @throws IllegalGridGeometryException if the math transform cannot compute the grid extent or the resolution.
*/
public GridGeometry(final PixelInCell anchor, final MathTransform gridToCRS, final Envelope envelope, final GridRoundingMode rounding) {
if (gridToCRS == null) {
ArgumentChecks.ensureNonNull("envelope", envelope);
} else {
ArgumentChecks.ensureDimensionMatches("envelope", gridToCRS.getTargetDimensions(), envelope);
}
ArgumentChecks.ensureNonNull("rounding", rounding);
this.gridToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CENTER);
this.cornerToCRS = PixelTranslation.translate(gridToCRS, anchor, PixelInCell.CELL_CORNER);
Matrix scales = MathTransforms.getMatrix(gridToCRS);
int numToIgnore = 1;
if (envelope != null && cornerToCRS != null) {
GeneralEnvelope env;
try {
env = Envelopes.transform(cornerToCRS.inverse(), envelope);
extent = new GridExtent(env, rounding, GridClippingMode.STRICT, null, null, null, null);
env = extent.toEnvelope(cornerToCRS, gridToCRS, envelope);
// Use `gridToCRS` specified by the user, not `this.gridToCRS`.
} catch (TransformException e) {
throw new IllegalGridGeometryException(e, "gridToCRS");
}
env.setCoordinateReferenceSystem(envelope.getCoordinateReferenceSystem());
this.envelope = new ImmutableEnvelope(env);
if (scales == null) try {
// `gridToCRS` cannot be null if `cornerToCRS` is non-null.
scales = gridToCRS.derivative(new DirectPositionView.Double(extent.getPointOfInterest(anchor)));
numToIgnore = 0;
} catch (TransformException e) {
recoverableException("<init>", e);
}
} else {
this.extent = null;
this.envelope = ImmutableEnvelope.castOrCopy(envelope);
}
resolution = (scales != null) ? resolution(scales, numToIgnore) : null;
nonLinears = findNonLinearTargets(gridToCRS);
}
/**
* Ensures that the given dimension is equal to the expected value. If not, throws an exception.
* This method assumes that the argument name is {@code "extent"}.
*
* @param extent the extent to validate, or {@code null} if none.
* @param expected the expected number of dimension.
*/
private static void ensureDimensionMatches(final int expected, final GridExtent extent) throws MismatchedDimensionException {
if (extent != null) {
final int dimension = extent.getDimension();
if (dimension != expected) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_3, "extent", expected, dimension));
}
}
}
/**
* Invoked when a recoverable exception occurred. Those exceptions must be minor enough
* that they can be silently ignored in most cases.
*
* @param caller the method where exception occurred.
* @param exception the exception that occurred.
*/
static void recoverableException(final String caller, final TransformException exception) {
Logging.recoverableException(GridExtent.LOGGER, GridGeometry.class, caller, exception);
}
/**
* Creates an axis-aligned grid geometry with an extent and an envelope.
* This constructor can be used when the <i>grid to CRS</i> transform is unknown.
* If only the coordinate reference system is known, then the envelope coordinates can be
* {@linkplain GeneralEnvelope#isAllNaN() all NaN}.
*
* <p>The main purpose of this constructor is to create "desired" grid geometries, for example for use in
* {@linkplain org.apache.sis.storage.GridCoverageResource#read(GridGeometry, int...) read} or
* {@linkplain org.apache.sis.coverage.grid.GridCoverageProcessor#resample resample} operations.
* For grid geometries describing preexisting data, it is safer and more flexible to use one of
* the constructors expecting a {@link MathTransform} argument.</p>
*
* <h4>Dimension order</h4>
* The given envelope shall always declare dimensions in same order as the given extent.
* This constructor may reorder axes if {@code orientation} is (for example) {@link GridOrientation#DISPLAY},
* but in such case this constructor will derive itself an envelope and a CRS with reordered axes.
*
* @param extent the valid extent of grid coordinates, or {@code null} if unknown.
* @param envelope the envelope together with CRS of the "real world" coordinates, or {@code null} if unknown.
* @param orientation high-level description of desired characteristics of the {@code gridToCRS} transform.
* Ignored (can be null) if {@code envelope} is null.
* @throws NullPointerException if {@code extent} and {@code envelope} arguments are both null,
* or if {@code envelope} is non-null but {@code orientation} is null.
*
* @see <a href="https://en.wikipedia.org/wiki/Axis-aligned_object">Axis-aligned object on Wikipedia</a>
*
* @since 1.1
*/
public GridGeometry(GridExtent extent, final Envelope envelope, final GridOrientation orientation) {
nonLinears = 0;
/*
* Potentially change axis order and orientation according the given `GridOrientation` (which may be null).
* Current code assumes that units of measurement are unchanged, which should be true for CRSs built using
* AxisConvention.DISPLAY_ORIENTED.
*/
long flip = 0; // Bitmask specifying whether to reverse axis in each dimension.
ImmutableEnvelope target = null; // May have different axis order than the specified `envelope` CRS.
int[] sourceDimensions = null; // Indices in source envelope of axes colinear with the target envelope.
if (envelope != null) {
ArgumentChecks.ensureNonNull("orientation", orientation);
if (orientation.crsVariant != null) {
final AbstractCRS sourceCRS = AbstractCRS.castOrCopy(envelope.getCoordinateReferenceSystem());
if (sourceCRS != null) {
final AbstractCRS targetCRS = sourceCRS.forConvention(orientation.crsVariant);
if (targetCRS != sourceCRS) {
final CoordinateSystem sourceCS = sourceCRS.getCoordinateSystem();
final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
sourceDimensions = AxisDirections.indicesOfLenientMapping(sourceCS, targetCS);
if (sourceDimensions != null) {
final double[] lowerCorner = new double[sourceDimensions.length];
final double[] upperCorner = new double[sourceDimensions.length];
for (int i=0; i < sourceDimensions.length; i++) {
final int s = sourceDimensions[i];
lowerCorner[i] = envelope.getMinimum(s);
upperCorner[i] = envelope.getMaximum(s);
if (sourceCS.getAxis(s).getDirection() != targetCS.getAxis(i).getDirection()) {
flip |= Numerics.bitmask(i);
}
}
target = new ImmutableEnvelope(lowerCorner, upperCorner, targetCRS);
}
}
}
}
}
if (target == null) {
target = ImmutableEnvelope.castOrCopy(envelope);
}
/*
* If the envelope contains no useful information, then the grid extent is mandatory.
* We do that for forcing grid geometries to contain at least one information.
*/
boolean nilEnvelope = true;
if (target == null || ((nilEnvelope = target.isAllNaN()) && target.getCoordinateReferenceSystem() == null)) {
if (target != null && nilEnvelope) {
throw new NullPointerException(Errors.format(Errors.Keys.UnspecifiedCRS));
}
ArgumentChecks.ensureNonNull("extent", extent);
this.envelope = null;
} else {
this.envelope = target;
if (extent != null) {
// A non-null `sourceDimensions` implies non-null `orientation`.
if (sourceDimensions != null && orientation.canReorderGridAxis) {
if (!ArraysExt.isRange(0, sourceDimensions)) {
extent = extent.reorder(sourceDimensions);
}
sourceDimensions = null;
}
if (!nilEnvelope) {
/*
* If we have both the extent and an envelope with at least one non-NaN coordinates,
* create the `cornerToCRS` transform. The `gridToCRS` calculation uses the knowledge
* that all scale factors are on diagonal, which allows simpler calculation than full
* matrix multiplication. Use double-double arithmetic everywhere.
*/
@SuppressWarnings("null") // `!nilEnvelope` implies non-null `orientation`.
final MatrixSIS affine = extent.cornerToCRS(target, orientation.flippedAxes ^ flip, sourceDimensions);
cornerToCRS = MathTransforms.linear(affine);
final int srcDim = cornerToCRS.getSourceDimensions(); // Translation column in matrix.
final int tgtDim = cornerToCRS.getTargetDimensions(); // Number of matrix rows before last row.
resolution = new double[tgtDim];
for (int j=0; j<tgtDim; j++) {
final int i = (sourceDimensions != null) ? sourceDimensions[j] : j;
DoubleDouble scale = DoubleDouble.of(affine.getNumber(j, i), true);
DoubleDouble offset = DoubleDouble.of(affine.getNumber(j, srcDim), true);
resolution[j] = Math.abs(scale.doubleValue());
offset = offset.add(scale.scalb(-1));
affine.setNumber(j, srcDim, offset);
}
gridToCRS = MathTransforms.linear(affine);
this.extent = extent;
return;
}
}
}
this.extent = extent;
gridToCRS = null;
cornerToCRS = null;
resolution = null;
}
/**
* Creates a grid geometry with only an envelope.
*
* @param envelope the envelope together with CRS of the "real world" coordinates.
* @throws IllegalArgumentException if the envelope has no CRS and only NaN coordinate values.
*
* @since 1.4
*/
public GridGeometry(final Envelope envelope) {
// Implicit null value check below.
this.envelope = ImmutableEnvelope.castOrCopy(envelope);
if (this.envelope.isAllNaN() && this.envelope.getCoordinateReferenceSystem() == null) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyArgument_1, "envelope"));
}
extent = null;
gridToCRS = null;
cornerToCRS = null;
resolution = null;
nonLinears = 0;
}
/**
* Creates a new grid geometry from the given components.
* This constructor performs no verification (unless assertions are enabled).
*/
GridGeometry(final GridExtent extent, final MathTransform gridToCRS, final MathTransform cornerToCRS,
final ImmutableEnvelope envelope, final double[] resolution, final long nonLinears)
{
this.extent = extent;
this.gridToCRS = gridToCRS;
this.cornerToCRS = cornerToCRS;
this.envelope = envelope;
this.resolution = resolution;
this.nonLinears = nonLinears;
if (gridToCRS != null) {
assert (extent == null) || gridToCRS.getSourceDimensions() == extent.getDimension();
assert (envelope == null) || gridToCRS.getTargetDimensions() == envelope.getDimension();
assert (resolution == null) || gridToCRS.getTargetDimensions() == resolution.length;
}
}
/**
* Creates a new grid geometry as the concatenation of the two specified grid geometries.
* The number of dimensions of the new grid geometry is the sum of the number of dimensions
* of the two specified grid geometries. The dimensions of the lower grid geometry are first,
* followed by the dimensions of the upper grid geometry.
*
* @param lower the grid geometry providing the lowest dimensions.
* @param upper the grid geometry providing the highest dimensions.
* @throws IncompleteGridGeometryException if a property presents in one grid geometry is absent in the other.
* @throws IllegalArgumentException if the concatenation results in duplicated
* {@linkplain GridExtent#getAxisType(int) grid axis types}.
* @throws FactoryException if the geodetic factory failed to create the compound CRS.
*
* @see #selectDimensions(int...)
*
* @since 1.5
*/
public GridGeometry(final GridGeometry lower, final GridGeometry upper) throws FactoryException {
/*
* Concatenate the extents first because they perform a check for axis duplication.
* We set the extent to null only if both `lower` and `upper` have a null extent.
* Otherwise, invoke `getExtent()` for causing an `IncompleteGridGeometryException`
* to be thrown if only one grid geometry has an extent.
*/
extent = (lower.extent == null && upper.extent == null) ? null
: new GridExtent(lower.getExtent(), upper.getExtent());
/*
* Concatenate the "grid to CRS" transforms and derived information
* (resolutions and non-linearity).
*/
if (lower.gridToCRS == null && upper.gridToCRS == null) {
gridToCRS = null;
cornerToCRS = null;
} else {
gridToCRS = compound(lower, upper, PixelInCell.CELL_CENTER);
cornerToCRS = compound(lower, upper, PixelInCell.CELL_CORNER);
}
nonLinears = lower.nonLinears | (upper.nonLinears << lower.getDimension());
resolution = (lower.resolution == null && upper.resolution == null) ? null
: ArraysExt.concatenate(lower.getResolution(true), upper.getResolution(true));
/*
* The check for presence/absence of envelope is more complex because an envelope
* may be considered missing, but still be non-null in order to store the CRS.
*/
if (lower.envelope == null && upper.envelope == null) {
envelope = null;
} else {
Envelope e1 = lower.envelope; if (e1 == null) e1 = lower.getEnvelope();
Envelope e2 = upper.envelope; if (e2 == null) e2 = upper.getEnvelope();
envelope = ImmutableEnvelope.castOrCopy(Envelopes.compound(e1, e2));
}
}
/**
* Aggregates the dimensions of the "grid to CRS" transforms of the given grid geometries.
*/
private static MathTransform compound(final GridGeometry lower, final GridGeometry upper, final PixelInCell anchor) {
return MathTransforms.compound(lower.getGridToCRS(anchor), upper.getGridToCRS(anchor));
}
/**
* Converts a "grid to CRS" transform from "cell corner" convention to "cell center" convention.
* This is a helper method for use of {@link #GridGeometry(GridExtent, MathTransform, MathTransform,
* ImmutableEnvelope, double[], long)} constructor.
*
* @param gridToCRS the transform to convert, or {@code null} if none.
* @return the converted transform, or {@code null} if the given transform was null.
*/
private static MathTransform cornerToCenter(final MathTransform gridToCRS) {
return PixelTranslation.translate(gridToCRS, PixelInCell.CELL_CORNER, PixelInCell.CELL_CENTER);
}
/**
* Returns the number of dimensions of the <em>grid</em>. This is typically the same
* than the number of {@linkplain #getEnvelope() envelope} dimensions or the number of
* {@linkplain #getCoordinateReferenceSystem() coordinate reference system} dimensions,
* but not necessarily.
*
* @return the number of grid dimensions.
*
* @see #selectDimensions(int[])
* @see GridExtent#getDimension()
*/
public final int getDimension() {
if (extent != null) {
return extent.getDimension(); // Most reliable source since that method is final.
} else if (gridToCRS != null) {
return gridToCRS.getSourceDimensions();
} else if (envelope != null) {
/*
* Last resort only since we have no guarantee that the envelope dimension is the same
* than the grid dimension (see above javadoc). The envelope should never be null at
* this point since the constructor verified that at least one argument was non-null,
* except if this method is invoked on UNDEFINED.
*/
return envelope.getDimension();
} else {
throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
}
}
/**
* Returns the number of dimensions of the <em>CRS</em>. This is typically the same as the
* number of {@linkplain #getDimension() grid dimensions}, but not necessarily.
*/
final int getTargetDimension() {
if (envelope != null) {
return envelope.getDimension(); // Most reliable source since that class is final.
} else if (gridToCRS != null) {
return gridToCRS.getTargetDimensions();
} else {
/*
* Last resort only since we have no guarantee that the grid dimension is the same
* then the CRS dimension (converse of the rational in getDimension() method).
*/
return extent.getDimension();
}
}
/**
* Returns the valid coordinate range of a grid coverage. The lowest valid grid coordinate is zero
* for {@link java.awt.image.BufferedImage}, but may be non-zero for arbitrary {@link RenderedImage}.
* A grid with 512 cells can have a minimum coordinate of 0 and maximum of 511.
*
* <div class="warning"><b>Upcoming API generalization:</b>
* the return type of this method may be changed to {@code GridEnvelope} interface in a future Apache SIS version.
* This is pending <a href="https://github.com/opengeospatial/geoapi/issues/36">GeoAPI update</a>.</div>
*
* @return the valid extent of grid coordinates (never {@code null}).
* @throws IncompleteGridGeometryException if this grid geometry has no extent —
* i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #EXTENT})</code> returned {@code false}.
*/
public GridExtent getExtent() {
if (extent != null) {
return extent;
}
throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
}
/**
* Returns the conversion from grid coordinates to "real world" coordinates.
* The conversion is often an affine transform, but not necessarily.
* Conversions from cell indices to geospatial coordinates can be performed for example as below:
*
* {@snippet lang="java" :
* MathTransform gridToCRS = gridGeometry.getGridToCRS(PixelInCell.CELL_CENTER);
* DirectPosition indicesOfCell = new GeneralDirectPosition(2, 3, 4):
* DirectPosition aPixelCenter = gridToCRS.transform(indicesOfCell, null);
* }
*
* Callers must specify whether they want the "real world" coordinates of cell center or cell corner.
* The cell corner is the one for which all grid indices have the smallest values (closest to negative infinity).
* As a rule of thumb:
*
* <ul>
* <li>Use {@link PixelInCell#CELL_CENTER} for transforming <em>points</em>.</li>
* <li>Use {@link PixelInCell#CELL_CORNER} for transforming <em>envelopes</em>
* with inclusive lower coordinates and <strong>exclusive</strong> upper coordinates.</li>
* </ul>
*
* <h4>API note</h4>
* There is no default value for {@code anchor} because experience shows that images shifted by ½ pixel
* (with pixels that may be tens of kilometres large) is a recurrent problem. We want to encourage developers
* to always think about wether the desired <i>grid to CRS</i> transform shall map pixel corner or center.
*
* @param anchor the cell part to map (center or corner).
* @return the conversion from grid coordinates to "real world" coordinates (never {@code null}).
* @throws IllegalArgumentException if the given {@code anchor} is not a known code list value.
* @throws IncompleteGridGeometryException if this grid geometry has no transform —
* i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #GRID_TO_CRS})</code> returned {@code false}.
*/
public MathTransform getGridToCRS(final PixelInCell anchor) {
final MathTransform mt;
switch (anchor) {
case CELL_CENTER: mt = gridToCRS; break;
case CELL_CORNER: mt = cornerToCRS; break;
default: mt = PixelTranslation.translate(gridToCRS, PixelInCell.CELL_CENTER, anchor); break;
}
if (mt != null) {
return mt;
}
throw incomplete(GRID_TO_CRS, Resources.Keys.UnspecifiedTransform);
}
/**
* Returns a linear approximation of the conversion from grid coordinates to "real world" coordinates.
* If the value returned by {@link #getGridToCRS(PixelInCell)} is already an instance of {@link LinearTransform},
* then it is returned as is. Otherwise this method computes the tangent of the transform at the grid extent
* {@linkplain GridExtent#getPointOfInterest(PixelInCell) point of interest} (usually the center of the grid).
*
* @param anchor the cell part to map (center or corner).
* @return linear approximation of the conversion from grid coordinates to "real world" coordinates.
* @throws IllegalArgumentException if the given {@code anchor} is not a known code list value.
* @throws IncompleteGridGeometryException if this grid geometry has no transform,
* or if the transform is non-linear but this grid geometry has no extent.
* @throws TransformException if an error occurred while computing the tangent.
*
* @since 1.3
*/
public LinearTransform getLinearGridToCRS(final PixelInCell anchor) throws TransformException {
final MathTransform tr = getGridToCRS(anchor);
if (tr instanceof LinearTransform) {
return (LinearTransform) tr;
}
return MathTransforms.linear(MathTransforms.getMatrix(tr,
new DirectPositionView.Double(getExtent().getPointOfInterest(anchor))));
}
/*
* Do not provide a convenience `getGridToCRS()` method without PixelInCell or PixelOrientation argument.
* Experience shows that 0.5 pixel offset in image localization is a recurrent problem. We really want to
* force developers to think about whether they want a gridToCRS transform locating pixel corner or center.
*/
/**
* Returns the coordinate reference system of the given envelope if defined, or {@code null} if none.
* Contrarily to {@link #getCoordinateReferenceSystem()}, this method does not throw exception.
*/
private static CoordinateReferenceSystem getCoordinateReferenceSystem(final Envelope envelope) {
return (envelope != null) ? envelope.getCoordinateReferenceSystem() : null;
}
/**
* Returns the "real world" coordinate reference system.
*
* @return the coordinate reference system (never {@code null}).
* @throws IncompleteGridGeometryException if this grid geometry has no CRS —
* i.e. <code>{@linkplain #isDefined isDefined}({@linkplain #CRS})</code> returned {@code false}.
*/
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
final CoordinateReferenceSystem crs = getCoordinateReferenceSystem(envelope);
if (crs != null) return crs;
throw incomplete(CRS, Resources.Keys.UnspecifiedCRS);
}
/**
* Returns the bounding box of "real world" coordinates for this grid geometry.
* This envelope is computed from the {@linkplain #getExtent() grid extent}, which is
* {@linkplain #getGridToCRS(PixelInCell) transformed} to the "real world" coordinate system.
* The initial envelope encompasses all cell surfaces, from the left border of leftmost cell
* to the right border of the rightmost cell and similarly along other axes.
* If this grid geometry is a {@linkplain GridDerivation#subgrid(Envelope, double...) subgrid}, then the envelope is also
* {@linkplain GeneralEnvelope#intersect(Envelope) clipped} to the envelope of the original (non subsampled) grid geometry.
*
* @return the bounding box in "real world" coordinates (never {@code null}).
* @throws IncompleteGridGeometryException if this grid geometry has no envelope —
* i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #ENVELOPE})</code> returned {@code false}.
*/
public Envelope getEnvelope() {
if (envelope != null && !envelope.isAllNaN()) {
return envelope;
}
throw incomplete(ENVELOPE, (extent == null) ? Resources.Keys.UnspecifiedGridExtent : Resources.Keys.UnspecifiedTransform);
}
/**
* Returns the "real world" bounding box of this grid geometry transformed to the given CRS.
* This envelope is computed from the {@linkplain #getExtent() grid extent} if available,
* or from the {@linkplain #getEnvelope() envelope} otherwise.
*
* @param crs the desired coordinate reference system for the returned envelope.
* @return the bounding box in "real world" coordinates (never {@code null}).
* @throws IncompleteGridGeometryException if this grid geometry has no extent and no envelope.
* @throws TransformException if the envelope cannot be transformed to the specified CRS.
*
* @since 1.2
*/
public Envelope getEnvelope(final CoordinateReferenceSystem crs) throws TransformException {
ArgumentChecks.ensureNonNull("crs", crs);
final int bitmask; // CRS, EXTENT or GRID_TO_CRS
final short errorKey; // Resource key for error message.
final CoordinateReferenceSystem sourceCRS = getCoordinateReferenceSystem(envelope);
if (Utilities.equalsIgnoreMetadata(sourceCRS, crs)) {
return envelope;
} else if (sourceCRS == null) {
bitmask = CRS;
errorKey = Resources.Keys.UnspecifiedCRS;
} else if (extent == null && envelope == null) {
bitmask = EXTENT;
errorKey = Resources.Keys.UnspecifiedGridExtent;
} else if (cornerToCRS == null && envelope == null) {
bitmask = GRID_TO_CRS;
errorKey = Resources.Keys.UnspecifiedTransform;
} else try {
/*
* At this point the envelope should never be null because of invariants enforced by constructors.
* But we nevertheless perform some paranoiac checks. If we fail to transform the envelope, its okay.
* The main transform is the one operating on grid extent. The envelope transformation is for taking
* in account singularity points (mostly poles) and in case this grid geometry is a sub-grid geometry,
* in which case the envelope may have been clipped and we want to keep that clip.
*/
final boolean onlyEnvelope = (extent == null || cornerToCRS == null);
final CoordinateOperation op = findOperation(sourceCRS, crs, geographicBBox());
Envelope clip;
try {
clip = Envelopes.transform(op, envelope);
if (onlyEnvelope) return clip;
} catch (TransformException e) {
if (onlyEnvelope) throw e;
recoverableException("getEnvelope", e);
clip = null;
}
MathTransform tr = MathTransforms.concatenate(cornerToCRS, op.getMathTransform());
final GeneralEnvelope env = extent.toEnvelope(tr, tr, clip);
env.setCoordinateReferenceSystem(op.getTargetCRS());
env.normalize();
if (clip != null) {
env.intersect(clip);
}
return env;
} catch (FactoryException e) {
throw new TransformException(e);
}
throw incomplete(bitmask, errorKey);
}
/**
* Returns the approximate latitude and longitude coordinates of the grid.
* The prime meridian is Greenwich, but the geodetic reference frame is not necessarily WGS 84.
* This is computed from the {@linkplain #getEnvelope() envelope} if the coordinate reference system
* contains an horizontal component such as a geographic or projected CRS.
*
* <h4>API note</h4>
* This method does not throw {@link IncompleteGridGeometryException} because the geographic extent
* may be absent even with a complete grid geometry. Grid geometries are not required to have a
* spatial component on Earth surface; a raster could be a vertical profile for example.
*
* @return the geographic bounding box in "real world" coordinates.
*/
public Optional<GeographicBoundingBox> getGeographicExtent() {
return Optional.ofNullable(geographicBBox());
}
/**
* Returns the {@link #geographicBBox} value or {@code null} if none.
* This method computes the box when first needed.
*/
final GeographicBoundingBox geographicBBox() {
GeographicBoundingBox bbox = geographicBBox;
if (bbox == null) {
if (getCoordinateReferenceSystem(envelope) != null && !envelope.isAllNaN()) {
DefaultGeographicBoundingBox db;
NilReason reason = null;
try {
db = ReferencingServices.getInstance().setBounds(envelope, null, null);
} catch (TransformException e) {
db = null;
reason = NilReason.INAPPLICABLE;
}
if (db != null) {
db.transitionTo(DefaultGeographicBoundingBox.State.FINAL);
bbox = db;
} else {
if (reason == null) reason = NilReason.MISSING;
bbox = reason.createNilObject(GeographicBoundingBox.class);
}
geographicBBox = bbox;
}
}
return (bbox instanceof NilObject) ? null : bbox;
}
/**
* Returns the start time and end time of coordinates of the grid.
* If the grid has no temporal dimension, then this method returns an empty array.
* If only the start time or end time is defined, then returns an array of length 1.
* Otherwise this method returns an array of length 2 with the start time in the first element
* and the end time in the last element.
*
* @return time range as an array of length 0 (if none), 1 or 2.
*/
public Instant[] getTemporalExtent() {
Instant[] times = timeRange();
if (times.length != 0) {
times = times.clone();
}
return times;
}
/**
* Returns the {@link #timeRange} value without cloning.
* This method computes the time range when first needed.
*/
private Instant[] timeRange() {
Instant[] times = timeRange;
if (times == null) {
final TemporalAccessor t = TemporalAccessor.of(getCoordinateReferenceSystem(envelope), 0);
times = (t != null) ? t.getTimeBounds(envelope) : TemporalAccessor.EMPTY;
timeRange = times;
}
return times;
}
/**
* Returns an <em>estimation</em> of the grid resolution, in units of the coordinate reference system axes.
* The length of the returned array is the number of CRS dimensions, with {@code resolution[0]}
* being the resolution along the first CRS axis, {@code resolution[1]} the resolution along the second CRS
* axis, <i>etc</i>. Note that this axis order is not necessarily the same as grid axis order.
*
* <p>If the resolution at CRS dimension <var>i</var> is not a constant factor
* (i.e. the <code>{@linkplain #isConversionLinear(int...) isConversionLinear}(i)</code> returns {@code false}),
* then {@code resolution[i]} is set to one of the following values:</p>
*
* <ul>
* <li>{@link Double#NaN} if {@code allowEstimates} is {@code false}.</li>
* <li>An arbitrary representative resolution otherwise.
* Current implementation computes the resolution at {@link GridExtent#getPointOfInterest(PixelInCell) grid center},
* but different implementations may use alternative algorithms.</li>
* </ul>
*
* @param allowEstimates whether to provide some values even for resolutions that are not constant factors.
* @return an <em>estimation</em> of the grid resolution (never {@code null}).
* @throws IncompleteGridGeometryException if this grid geometry has no resolution —
* i.e. <code>{@linkplain #isDefined(int) isDefined}({@linkplain #RESOLUTION})</code> returned {@code false}.
*/
public double[] getResolution(final boolean allowEstimates) {
if (resolution != null) {
final double[] res = resolution.clone();
if (!allowEstimates) {
long nonLinearDimensions = nonLinears;
while (nonLinearDimensions != 0) {
final int i = Long.numberOfTrailingZeros(nonLinearDimensions);
nonLinearDimensions &= ~(1L << i);
res[i] = Double.NaN;
}
}
return res;
}
throw incomplete(RESOLUTION, (gridToCRS == null) ? Resources.Keys.UnspecifiedTransform : Resources.Keys.UnspecifiedGridExtent);
}
/**
* Computes the resolution for the given grid extent and transform, or returns {@code null} if unknown.
* Resolutions are given in order of target axes and give a scale factor from source to target coordinates.
* If the {@code gridToCRS} transform is linear, we do not even need to check the grid extent; it can be null.
* Otherwise (if the transform is non-linear) the extent is necessary. The easiest way to estimate a resolution
* is then to ask for the derivative at some arbitrary point (the point of interest).
*
* <p>Note that for this computation, it does not matter if {@code gridToCRS} is the user-specified
* transform or the {@code this.gridToCRS} field value; both should produce equivalent results.</p>
*
* @param gridToCRS a transform for which to compute the resolution, or {@code null} if none.
* @param domain the domain for which to get a resolution, or {@code null} if none.
* If non-null, must be the source of {@code gridToCRS}.
* @param anchor the pixel corner versus pixel center convention to use.
* @return the resolutions as positive numbers. May contain NaN values.
*/
static double[] resolution(final MathTransform gridToCRS, final GridExtent domain, final PixelInCell anchor) {
final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
if (matrix != null) {
return resolution(matrix, 1);
} else if (domain != null && gridToCRS != null) try {
return resolution(gridToCRS.derivative(new DirectPositionView.Double(domain.getPointOfInterest(anchor))), 0);
} catch (TransformException e) {
recoverableException("resolution", e);
}
return null;
}
/**
* Computes the resolutions from the given matrix. This is the magnitude of each row vector.
* Resolutions are given in order of target axes.
*
* @param gridToCRS Jacobian matrix or affine transform for which to compute the resolution.
* @param numToIgnore number of rows and columns to ignore at the end of the matrix.
* This is 0 if the matrix is a derivative (i.e. we ignore nothing), or 1 if the matrix
* is an affine transform (i.e. we ignore the translation column and the [0 0 … 1] row).
* @return the resolutions as positive numbers. May contain NaN values.
*/
private static double[] resolution(final Matrix gridToCRS, final int numToIgnore) {
final double[] resolution = new double[gridToCRS.getNumRow() - numToIgnore];
final double[] buffer = new double[gridToCRS.getNumCol() - numToIgnore];
for (int j=0; j<resolution.length; j++) {
for (int i=0; i<buffer.length; i++) {
buffer[i] = gridToCRS.getElement(j,i);
}
resolution[j] = MathFunctions.magnitude(buffer);
}
return resolution;
}
/**
* Indicates whether the <i>grid to CRS</i> conversion is linear for all the specified CRS axes.
* The conversion from grid coordinates to real world coordinates is often linear for some dimensions,
* typically the horizontal ones at indices 0 and 1. But the vertical dimension (usually at index 2)
* is often non-linear, for example with data at 0, 5, 10, 100 and 1000 metres.
*
* @param targets indices of CRS axes. This is not necessarily the same as indices of grid axes.
* @return {@code true} if the conversion from grid coordinates to "real world" coordinates is linear
* for all the given CRS dimension.
*/
public boolean isConversionLinear(final int... targets) {
final int dimension = getTargetDimension();
long mask = 0;
for (final int d : targets) {
mask |= Numerics.bitmask(Objects.checkIndex(d, dimension));
}
return (nonLinears & mask) == 0;
}
/**
* Guesses which target dimensions may be non-linear. We currently don't have an API for finding the non-linear dimensions.
* Current implementation assumes that everything else than {@code LinearTransform} and pass-through dimensions are non-linear.
* This is not always true (e.g. in a Mercator projection, the "longitude → easting" part is linear too), but should be okay
* for {@code GridGeometry} purposes.
*
* <p>We keep trace of non-linear dimensions in a bitmask, with bits of non-linear dimensions set to 1.
* This limit us to 64 dimensions, which is assumed more than enough. Note that {@code GridGeometry} can
* contain more dimensions provided that index of the last non-linear dimension is not greater than 64.</p>
*
* @param gridToCRS the transform to "real world" coordinates, or {@code null} if unknown.
* @return a bitmask of dimensions, or 0 (i.e. conversion assumed fully linear) if the given transform was null.
*/
private static long findNonLinearTargets(final MathTransform gridToCRS) {
long nonLinearDimensions = 0;
for (final MathTransform step : MathTransforms.getSteps(gridToCRS)) {
final Matrix mat = MathTransforms.getMatrix(step);
if (mat != null) {
/*
* For linear transforms there are no bits to set. However if some bits were set by a previous
* iteration, we may need to move them (for example the transform may swap axes). We take the
* current bitmasks as source dimensions and find what are the target dimensions for them.
*/
long mask = nonLinearDimensions;
nonLinearDimensions = 0;
while (mask != 0) {
final int i = Long.numberOfTrailingZeros(mask); // Source dimension of non-linear part
for (int j = mat.getNumRow() - 1; --j >= 0;) { // Possible target dimensions
if (mat.getElement(j, i) != 0) {
if (j >= Long.SIZE) {
throw excessiveDimension(gridToCRS);
}
nonLinearDimensions |= (1L << j);
}
}
mask &= ~(1L << i);
}
} else if (step instanceof PassThroughTransform) {
/*
* Assume that all modified coordinates use non-linear transform. We do not inspect the
* sub-transform recursively because if it had a non-linear step, PassThroughTransform
* should have moved that step outside the sub-transform for easier concatenation with
* the LinearTransforms before of after that PassThroughTransform.
*/
long mask = 0;
final int dimIncrease = step.getTargetDimensions() - step.getSourceDimensions();
final int maxBits = Long.SIZE - Math.max(dimIncrease, 0);
for (final int i : ((PassThroughTransform) step).getModifiedCoordinates()) {
if (i >= maxBits) {
throw excessiveDimension(gridToCRS);
}
mask |= (1L << i);
}
/*
* The mask we just computed identifies non-linear source dimensions, but we need target
* dimensions. They are usually the same (the pass-through coordinate values do not have
* their order changed). However, we have a difficulty if the number of dimensions changes.
* We know that the change happen in the sub-transform, but we do not know where exactly.
* For example if the mask is 001010 and the number of dimensions increases by 1, we know
* that we still have "00" at the beginning and "0" at the end of the mask, but we don't
* know what happen between the two. Does "101" become "1101" or "1011"? We conservatively
* take "1111", i.e. we unconditionally set all bits in the middle to 1.
*
* Mathematics:
* (Long.highestOneBit(mask) << 1) - 1
* is a mask identifying all source dimensions before trailing pass-through dimensions.
*
* maskHigh = (Long.highestOneBit(mask) << (dimIncrease + 1)) - 1
* is a mask identifying all target dimensions before trailing pass-through dimensions.
*
* maskLow = Long.lowestOneBit(mask) - 1
* is a mask identifying all leading pass-through dimensions (both source and target).
*
* maskHigh & ~maskLow
* is a mask identifying only target dimensions after leading pass-through and before
* trailing pass-through dimensions. In our case, all 1 bits in maskLow are also 1 bits
* in maskHigh. So we can rewrite as
*
* maskHigh - maskLow
* and the -1 terms cancel each other.
*/
if (dimIncrease != 0) {
mask = (Long.highestOneBit(mask) << (dimIncrease + 1)) - Long.lowestOneBit(mask);
}
nonLinearDimensions |= mask;
} else {
/*
* Not a known transform. Assume all dimensions may become non-linear.
*/
final int dimension = gridToCRS.getTargetDimensions();
if (dimension > Long.SIZE) {
throw excessiveDimension(gridToCRS);
}
return Numerics.bitmask(dimension) - 1;
}
}
return nonLinearDimensions;
}
/**
* Invoked when the number of non-linear dimensions exceeds the {@code GridGeometry} capacity.
*/
static ArithmeticException excessiveDimension(final MathTransform gridToCRS) {
return new ArithmeticException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1, gridToCRS.getTargetDimensions()));
}
/**
* Invoked when a property has been requested for which we have for information.
*
* @param bitmask the requested property, for assertion purpose.
* @param errorKey the resource key to use in error message.
* @return the exception to be thrown by the caller.
*/
private IncompleteGridGeometryException incomplete(final int bitmask, final short errorKey) {
assert getClass() != GridGeometry.class || !isDefined(bitmask);
return new IncompleteGridGeometryException(Resources.format(errorKey));
}
/**
* Verifies that this grid geometry defines an {@linkplain #extent} and a {@link #cornerToCRS} transform.
* They are the information required for mapping the grid to a spatiotemporal envelope or position.
* Note that this implies that {@link #envelope} is non-null (but not necessarily that its CRS is non-null).
*
* @param center {@code true} for "center to CRS" transform, {@code false} for "corner to CRS" transform.
* @return {@link #gridToCRS} or {@link #cornerToCRS}.
*/
final MathTransform requireGridToCRS(final boolean center) throws IncompleteGridGeometryException {
if (extent == null) {
throw incomplete(EXTENT, Resources.Keys.UnspecifiedGridExtent);
}
final MathTransform mt = center ? gridToCRS : cornerToCRS;
if (mt == null) {
throw incomplete(GRID_TO_CRS, Resources.Keys.UnspecifiedTransform);
}
return mt;
}
/**
* Returns {@code true} if all the properties specified by the argument are set.
* If this method returns {@code true}, then invoking the corresponding getter
* methods will not throw {@link IncompleteGridGeometryException}.
*
* @param bitmask any combination of {@link #CRS}, {@link #ENVELOPE}, {@link #EXTENT},
* {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
* @return {@code true} if all specified properties are defined (i.e. invoking the
* corresponding getter methods will not throw {@link IncompleteGridGeometryException}).
* @throws IllegalArgumentException if the specified bitmask is not a combination of known masks.
*
* @see #getCoordinateReferenceSystem()
* @see #getEnvelope()
* @see #getExtent()
* @see #getResolution(boolean)
* @see #getGridToCRS(PixelInCell)
*/
public boolean isDefined(final int bitmask) {
if ((bitmask & ~(CRS | ENVELOPE | EXTENT | GRID_TO_CRS | RESOLUTION | GEOGRAPHIC_EXTENT | TEMPORAL_EXTENT)) != 0) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
}
return ((bitmask & CRS) == 0 || (null != getCoordinateReferenceSystem(envelope)))
&& ((bitmask & ENVELOPE) == 0 || (null != envelope && !envelope.isAllNaN()))
&& ((bitmask & EXTENT) == 0 || (null != extent))
&& ((bitmask & GRID_TO_CRS) == 0 || (null != gridToCRS))
&& ((bitmask & RESOLUTION) == 0 || (null != resolution))
&& ((bitmask & GEOGRAPHIC_EXTENT) == 0 || (null != geographicBBox()))
&& ((bitmask & TEMPORAL_EXTENT) == 0 || (timeRange().length != 0));
}
/**
* Returns {@code true} if this grid geometry contains only a grid extent and no other information.
* Note: if {@link #gridToCRS} is {@code null}, then {@link #cornerToCRS} and {@link #resolution}
* should be null as well.
*/
final boolean isExtentOnly() {
return gridToCRS == null && envelope == null && extent != null;
}
/**
* Returns {@code true} if this grid geometry contains only an envelope and no other information.
* Note: if {@link #gridToCRS} is {@code null}, then {@link #cornerToCRS} and {@link #resolution}
* should be null as well.
*/
final boolean isEnvelopeOnly() {
return gridToCRS == null && extent == null && envelope != null;
}
/**
* Returns an object that can be used for creating a new grid geometry derived from this grid geometry.
* {@code GridDerivation} does not change the state of this {@code GridGeometry} but instead creates
* new instances as needed. Examples of modifications include clipping to a sub-area or applying a sub-sampling.
*
* <p>Each {@code GridDerivation} instance can be used only once and should be used in a single thread.
* {@code GridDerivation} preserves the number of dimensions. For example, {@linkplain GridDerivation#slice slicing}
* sets the {@linkplain GridExtent#getSize(int) grid size} to 1 in all dimensions specified by a <i>slice point</i>,
* but does not remove those dimensions from the grid geometry. For dimensionality reduction, see {@link #selectDimensions(int[])}.</p>
*
* <h4>Example</h4>
* For clipping this grid geometry to a sub-area, one can use:
*
* {@snippet lang="java" :
* GridGeometry gg = ...;
* Envelope areaOfInterest = ...;
* gg = gg.derive().rounding(GridRoundingMode.ENCLOSING)
* .subgrid(areaOfInterest).build();
* }
*
* @return an object for deriving a grid geometry from {@code this}.
*/
public GridDerivation derive() {
return new GridDerivation(this);
}
/**
* Creates a new grid geometry upsampled by the given number of cells along each grid dimensions.
* This method multiplies {@linkplain GridExtent#getLow(int) low} and {@linkplain GridExtent#getHigh(int) high}
* coordinates by the given periods, then scales the {@link #getGridToCRS(PixelInCell) grid to CRS} transform
* for compensating the grid change.
* The grid geometry {@link #getEnvelope() envelope} is preserved after upsampling.
*
* <h4>Number of arguments</h4>
* The {@code periods} array length should be equal to the {@linkplain #getDimension() number of grid dimensions}.
* If the array is shorter, missing values default to 1 (i.e. samplings in unspecified dimensions are unchanged).
* If the array is longer, extraneous values are ignored.
*
* @param periods the upsampling. Length shall be equal to the number of dimension and all values shall be greater than zero.
* @return the upsampled grid geometry, or {@code this} is upsampling results in the same extent.
* @throws IllegalArgumentException if a period is not greater than zero.
*
* @see GridExtent#upsample(int...)
* @since 1.3
*/
public GridGeometry upsample(final int... periods) {
GridExtent newExtent = extent;
if (newExtent != null) {
newExtent = newExtent.upsample(periods);
if (newExtent == extent) return this; // Unchanged.
}
boolean changed = false; // Additional check in case `extent` was null.
MathTransform newGridToCRS = null;
double[] newResolution = null;
if (cornerToCRS != null) {
final int tgtDim = cornerToCRS.getTargetDimensions();
final int srcDim = cornerToCRS.getSourceDimensions();
MatrixSIS matrix = Matrices.copy(MathTransforms.getMatrix(cornerToCRS));
final boolean isNonLinear = (matrix == null);
if (isNonLinear) {
matrix = Matrices.create(tgtDim+1, srcDim+1, ExtendedPrecisionMatrix.CREATE_IDENTITY);
}
/*
* By dividing the matrix elements directly, we avoid some numerical errors.
* This is because 100*0.1 is not exactly equal to 100/10.
* Each period value must be multiplied by a full column.
*/
newResolution = resolution.clone();
for (int i = Math.min(srcDim, periods.length); --i >= 0;) {
final double p = periods[i];
if (p != 1) {
newResolution[i] /= p;
final DoubleDouble pd = DoubleDouble.of(p, true);
for (int j=0; j<tgtDim; j++) {
DoubleDouble e = DoubleDouble.of(matrix.getNumber(j, i), true);
matrix.setNumber(j, i, e.divide(pd));
changed = true;
}
}
}
newGridToCRS = MathTransforms.linear(matrix);
if (isNonLinear) {
newGridToCRS = MathTransforms.concatenate(newGridToCRS, cornerToCRS);
}
}
if (!changed) return this;
return new GridGeometry(newExtent, cornerToCenter(newGridToCRS), newGridToCRS, envelope, newResolution, nonLinears);
}
/**
* Translates grid coordinates by the given number of cells without changing "real world" coordinates.
* The returned grid has the same {@linkplain GridExtent#getSize(int) size} than this grid,
* i.e. both low and high grid coordinates are displaced by the same number of cells.
* The "grid to CRS" transforms are adjusted accordingly in order to map to the same
* "real world" coordinates.
*
* <h4>Number of arguments</h4>
* The {@code translation} array length should be equal to the {@linkplain #getDimension() number of dimensions}.
* If the array is shorter, missing values default to 0 (i.e. no translation in unspecified dimensions).
* If the array is longer, extraneous values are ignored.
*
* @param translation translation to apply on each grid axis in order.
* @return a grid geometry whose coordinates (both low and high ones) and
* the "grid to CRS" transforms have been translated by given amounts.
* If the given translation is a no-op (no value or only 0 ones), then this grid is returned as is.
* @throws ArithmeticException if the translation results in coordinates that overflow 64-bits integer.
*
* @see GridExtent#translate(long...)
*
* @since 1.3
*/
public GridGeometry shiftGrid(final long... translation) {
ArgumentChecks.ensureNonNull("translation", translation);
GridExtent newExtent = extent;
if (newExtent != null) {
newExtent = newExtent.translate(translation);
if (newExtent == extent) return this;
}
MathTransform t1 = gridToCRS;
MathTransform t2 = cornerToCRS;
if (t1 != null || t2 != null) {
boolean isZero = true;
final double[] vector = new double[getDimension()];
for (int i=Math.min(vector.length, translation.length); --i >= 0;) {
isZero &= (translation[i] == 0);
vector[i] = Math.negateExact(translation[i]);
}
if (isZero) return this;
final MathTransform t = MathTransforms.translation(vector);
t1 = MathTransforms.concatenate(t, t1);
t2 = MathTransforms.concatenate(t, t2);
}
return new GridGeometry(newExtent, t1, t2, envelope, resolution, nonLinears);
}
/**
* Returns a grid geometry with the given grid extent, which implies a new "real world" computation.
* The "grid to CRS" transforms and the resolution stay the same as this {@code GridGeometry}.
* The "real world" envelope is recomputed for the new grid extent using the "grid to CRS" transforms.
*
* <p>The given extent is taken verbatim; this method does no clipping.
* The given extent does not need to intersect the extent of this grid geometry.</p>
*
* @param newExtent extent of the grid geometry to return.
* @return grid geometry with the given extent. May be {@code this} if there is no change.
* @throws TransformException if the geospatial envelope cannot be recomputed with the new grid extent.
*
* @since 1.3
*/
public GridGeometry relocate(final GridExtent newExtent) throws TransformException {
if (newExtent.equals(extent)) {
return this;
}
ensureDimensionMatches(getDimension(), newExtent);
final ImmutableEnvelope relocated;
if (cornerToCRS != null) {
final GeneralEnvelope env = newExtent.toEnvelope(cornerToCRS, gridToCRS, null);
env.setCoordinateReferenceSystem(getCoordinateReferenceSystem(envelope));
relocated = new ImmutableEnvelope(env);
} else {
relocated = envelope; // Either null or contains only the CRS.
}
return new GridGeometry(newExtent, gridToCRS, cornerToCRS, relocated, resolution, nonLinears);
}
/**
* Returns a grid geometry that encompass only some dimensions of this grid geometry.
* The specified dimensions will be copied into a new grid geometry if necessary.
* The selection is applied on {@linkplain #getExtent() grid extent} dimensions;
* they are not necessarily the same as the {@linkplain #getEnvelope() envelope} dimensions.
* The given dimensions must be in strictly ascending order without duplicated values.
* The number of dimensions of the sub grid geometry will be {@code dimensions.length}.
*
* <p>This method performs a <i>dimensionality reduction</i>.
* This method cannot be used for changing dimension order.
* The converse operation is the {@linkplain #GridGeometry(GridGeometry, GridGeometry) concatenation}.</p>
*
* @param indices the grid (not CRS) dimensions to select, in strictly increasing order.
* @return the sub-grid geometry, or {@code this} if the given array contains all dimensions of this grid geometry.
* @throws IndexOutOfBoundsException if an index is out of bounds.
*
* @see GridExtent#getSubspaceDimensions(int)
* @see GridExtent#selectDimensions(int[])
* @see org.apache.sis.referencing.CRS#selectDimensions(CoordinateReferenceSystem, int[])
*
* @since 1.3
*/
public GridGeometry selectDimensions(int... indices) {
indices = GridExtent.verifyDimensions(indices, getDimension());
if (indices != null) try {
return new SliceGeometry(this, null, indices, null).reduce(null, -1);
} catch (FactoryException e) {
throw new BackingStoreException(e);
}
return this;
}
/**
* Creates a one-, two- or three-dimensional coordinate reference system for cell indices in the grid.
* This method returns a CRS which is derived from the "real world" CRS or a subset of it.
* If the "real world" CRS is an instance of {@link org.opengis.referencing.crs.SingleCRS},
* then the derived CRS has the following properties:
*
* <ul>
* <li>{@link DerivedCRS#getBaseCRS()} is {@link #getCoordinateReferenceSystem()}.</li>
* <li>{@link DerivedCRS#getConversionFromBase()} is the inverse of {@link #getGridToCRS(PixelInCell)}.</li>
* </ul>
*
* Otherwise if the "real world" CRS is an instance of {@link org.opengis.referencing.crs.CompoundCRS},
* then only the first {@link org.opengis.referencing.crs.SingleCRS} (the head) is used.
* This is usually (but not necessarily) the horizontal component of the spatial CRS.
* The result is usually two-dimensional, but 1 and 3 dimensions are also possible.
*
* <p>Because of above relationship, it is possible to use the derived CRS in a chain of operations
* with (for example) {@link org.apache.sis.referencing.CRS#findOperation CRS.findOperation(…)}.</p>
*
* @param name name of the CRS to create.
* @param anchor the cell part to map (center or corner).
* @return a derived CRS for coordinates (cell indices) associated to the grid extent.
* @throws IncompleteGridGeometryException if the CRS, grid extent or "grid to CRS" transform is missing.
*
* @since 1.3
*/
public DerivedCRS createImageCRS(final String name, final PixelInCell anchor) {
ArgumentChecks.ensureNonEmpty("name", name);
try {
return GridExtentCRS.forCoverage(name, this, anchor, null);
} catch (FactoryException e) {
throw new BackingStoreException(e);
} catch (NoninvertibleTransformException e) {
throw new IllegalStateException(e);
}
}
/**
* Creates a transform from cell coordinates in this grid to cell coordinates in the given grid.
* The returned transform handles change of Coordinate Reference System and wraparound axes
* (e.g. longitude axis crossing the ±180° meridian) if applicable.
*
* <p><b>Note:</b> the transform created by this method may be non-invertible.</p>
*
* @param target the grid which will be the target of returned transform.
* @param anchor {@linkplain PixelInCell#CELL_CENTER Cell center} for OGC conventions or
* {@linkplain PixelInCell#CELL_CORNER cell corner} for Java2D/JAI conventions.
* @return transform from cell coordinates in this grid to cell coordinates in the given grid.
* @throws TransformException if the math transform cannot be created.
*
* @since 1.1
*/
public MathTransform createTransformTo(final GridGeometry target, final PixelInCell anchor) throws TransformException {
ArgumentChecks.ensureNonNull("target", target);
ArgumentChecks.ensureNonNull("anchor", anchor);
/*
* Inverse `source` and `target` because `CoordinateOperationFinder.inverse(…)` does an
* effort for using `WraparoundTransform` only if needed (contrarily to `gridToCRS(…)`).
*/
final CoordinateOperationFinder finder = new CoordinateOperationFinder(target, this);
finder.verifyPresenceOfCRS(false);
finder.setAnchor(anchor);
final MathTransform tr;
try {
tr = finder.inverse();
} catch (FactoryException e) {
throw new TransformException(e);
}
return MathTransforms.concatenate(getGridToCRS(anchor), tr);
}
/**
* Returns a hash value for this grid geometry. This value needs not to remain
* consistent between different implementations of the same class.
*/
@Override
public int hashCode() {
int code = (int) serialVersionUID;
if (gridToCRS != null) {
code += gridToCRS.hashCode();
}
if (extent != null) {
code += extent.hashCode();
}
// We do not check the envelope since it has a determinist relationship with other attributes.
return code;
}
/**
* Compares the specified object with this grid geometry for equality.
* This method delegates to {@code equals(object, ComparisonMode.STRICT)}.
*
* @param object the object to compare with.
* @return {@code true} if the given object is equal to this grid geometry.
*/
@Override
public boolean equals(final Object object) {
return equals(object, ComparisonMode.STRICT);
}
/**
* Compares the specified object with this grid geometry for equality.
* If the mode is {@link ComparisonMode#IGNORE_METADATA} or more flexible,
* then the {@linkplain GridExtent#getAxisType(int) axis types} are ignored.
*
* @param object the object to compare with this grid geometry for equality.
* @param mode the strictness level of the comparison.
* @return {@code true} if the given object is equal to this grid geometry.
*
* @since 1.1
*/
@Override
public boolean equals(final Object object, final ComparisonMode mode) {
if (object == this) {
return true;
}
if (object instanceof GridGeometry) {
final GridGeometry that = (GridGeometry) object;
if ((mode != ComparisonMode.STRICT || getClass().equals(object.getClass()))
&& Utilities.deepEquals(extent, that.extent, mode)
&& Utilities.deepEquals(gridToCRS, that.gridToCRS, mode))
{
final ImmutableEnvelope othenv = that.envelope;
if (!mode.isApproximate()) {
return Utilities.deepEquals(envelope, othenv, mode);
}
if ((envelope == null) == (othenv == null) &&
Utilities.deepEquals(getCoordinateReferenceSystem(envelope),
getCoordinateReferenceSystem(othenv), mode))
{
return equalsApproximately(othenv);
}
}
}
return false;
}
/**
* Returns whether the given envelope is equal to this grid geometry envelope with a tolerance threshold
* computed from grid resolution. If this grid geometry has no envelope, then this method arbitrarily
* returns {@code true} (this unusual behavior is required by {@link #equals(Object, ComparisonMode)}).
*/
final boolean equalsApproximately(final ImmutableEnvelope othenv) {
if (envelope != null) {
for (int i=envelope.getDimension(); --i >= 0;) {
// Arbitrary threshold of ½ pixel.
final double ε = (resolution != null) ? resolution[i] * 0.5 : 0;
if (!MathFunctions.epsilonEqual(envelope.getLower(i), othenv.getLower(i), ε) ||
!MathFunctions.epsilonEqual(envelope.getUpper(i), othenv.getUpper(i), ε))
{
return false;
}
}
}
return true;
}
/**
* Returns the default set of flags to use for {@link #toString()} implementations.
* Current implementation returns all core properties, augmented with only derived
* properties that are defined.
*/
final int defaultFlags() {
int flags = EXTENT | GRID_TO_CRS | CRS;
if (null != envelope) flags |= ENVELOPE;
if (null != resolution) flags |= RESOLUTION;
if (null != geographicBBox()) flags |= GEOGRAPHIC_EXTENT;
if (timeRange().length != 0) flags |= TEMPORAL_EXTENT;
return flags;
}
/**
* Returns a string representation of this grid geometry.
* The returned string is implementation dependent and may change in any future version.
* Current implementation is equivalent to a call to {@link #toTree(Locale, int)} with
* at least {@link #EXTENT}, {@link #ENVELOPE} and {@link #CRS} flags.
* Whether more flags are present or not is unspecified.
*/
@Override
public String toString() {
return toTree(Locale.getDefault(), defaultFlags()).toString();
}
/**
* Returns a tree representation of some elements of this grid geometry.
* The tree representation is for debugging or logging purposes
* and may change in any future SIS version.
*
* @param locale the locale to use for textual labels.
* @param bitmask combination of {@link #EXTENT}, {@link #ENVELOPE}, {@link #CRS}, {@link #GRID_TO_CRS},
* {@link #RESOLUTION}, {@link #GEOGRAPHIC_EXTENT} and {@link #TEMPORAL_EXTENT}.
* @return a tree representation of the specified elements.
*/
@Debug
public TreeTable toTree(final Locale locale, final int bitmask) {
ArgumentChecks.ensureNonNull("locale", locale);
final TreeTable tree = new DefaultTreeTable(TableColumn.VALUE_AS_TEXT);
final TreeTable.Node root = tree.getRoot();
root.setValue(TableColumn.VALUE_AS_TEXT, Classes.getShortClassName(this));
formatTo(locale, Vocabulary.forLocale(locale), bitmask, root);
return tree;
}
/**
* Formats a string representation of this grid geometry in the specified tree.
*/
final void formatTo(final Locale locale, final Vocabulary vocabulary, final int bitmask, final TreeTable.Node root) {
if ((bitmask & ~(EXTENT | ENVELOPE | CRS | GRID_TO_CRS | RESOLUTION | GEOGRAPHIC_EXTENT | TEMPORAL_EXTENT)) != 0) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.IllegalArgumentValue_2, "bitmask", bitmask));
}
try {
new Formatter(locale, vocabulary, bitmask, root).format();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
/**
* Helper class for formatting a {@link GridGeometry} instance.
*/
private final class Formatter {
/**
* Combination of {@link #EXTENT}, {@link #ENVELOPE}, {@link #CRS}, {@link #GRID_TO_CRS},
* {@link #RESOLUTION}, {@link #GEOGRAPHIC_EXTENT} and {@link #TEMPORAL_EXTENT}.
*/
private final int bitmask;
/**
* Temporary buffer for formatting node values.
*/
private final StringBuilder buffer;
/**
* Where to write the {@link GridGeometry} string representation.
*/
private final TreeTable.Node root;
/**
* The section under the {@linkplain #root} where to write elements.
* This is updated when {@link #section(int, short, boolean, boolean)} is invoked.
*/
private TreeTable.Node section;
/**
* Localized words.
*/
private final Vocabulary vocabulary;
/**
* The locale for the texts, numbers (except grid extent) and dates.
*/
private final Locale locale;
/**
* The coordinate reference system, or {@code null} if none.
*/
private final CoordinateReferenceSystem crs;
/**
* The coordinate system, or {@code null} if none.
*/
private final CoordinateSystem cs;
/**
* Creates a new formatter for the given combination of {@link #EXTENT}, {@link #ENVELOPE},
* {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
*/
Formatter(final Locale locale, final Vocabulary vocabulary, final int bitmask, final TreeTable.Node out) {
this.root = out;
this.bitmask = bitmask;
this.buffer = new StringBuilder(256);
this.locale = locale;
this.vocabulary = vocabulary;
this.crs = getCoordinateReferenceSystem(envelope);
this.cs = (crs != null) ? crs.getCoordinateSystem() : null;
}
/**
* Formats a string representation of the enclosing {@link GridGeometry} instance
* in the buffer specified at construction time.
*/
final void format() throws IOException {
/*
* Example: Grid extent
* ├─ Dimension 0: [370 … 389] (20 cells)
* └─ Dimension 1: [ 41 … 340] (300 cells)
*
* We need to use the implementation provided by GridExtent in order
* to format correctly the unsigned long numbers for very large sizes.
*/
if (section(EXTENT, Vocabulary.Keys.GridExtent, true, false)) {
extent.appendTo(buffer, vocabulary);
writeNodes();
}
/*
* Example: Geographic extent
*  ├─Lower bound: 80°00′00″S 180°00′00″W 2019-05-01T21:00:00Z
*  └─Upper bound: 90°00′00″N 180°00′00″E 2019-05-02T00:00:00Z
*
* The angle and date/time patterns are fixed for now, with a precision equivalent to about 30 metres.
* The angles are rounded toward up and down for making sure that the box encloses fully the coverage.
*/
if (section(GEOGRAPHIC_EXTENT, Vocabulary.Keys.GeographicExtent, false, false) ||
section(TEMPORAL_EXTENT, Vocabulary.Keys.TemporalExtent, false, false))
{
final TableAppender table = new TableAppender(buffer, " ");
final AngleFormat nf = new AngleFormat("DD°MM′SS″", locale);
final GeographicBoundingBox bbox = ((bitmask & GEOGRAPHIC_EXTENT) != 0) ? geographicBBox() : null;
double westBoundLongitude = Double.NaN;
double eastBoundLongitude = Double.NaN;
final Instant[] times = ((bitmask & TEMPORAL_EXTENT) != 0) ? timeRange() : TemporalAccessor.EMPTY;
vocabulary.appendLabel(Vocabulary.Keys.LowerBound, table);
table.setCellAlignment(TableAppender.ALIGN_RIGHT);
if (bbox != null) {
nf.setRoundingMode(RoundingMode.FLOOR);
westBoundLongitude = bbox.getWestBoundLongitude();
table.nextColumn(); table.append(nf.format(new Latitude(bbox.getSouthBoundLatitude())));
table.nextColumn(); table.append(nf.format(new Longitude(westBoundLongitude)));
}
if (times.length >= 1) {
table.nextColumn();
table.append(times[0].toString());
}
table.nextLine();
table.setCellAlignment(TableAppender.ALIGN_LEFT);
vocabulary.appendLabel(Vocabulary.Keys.UpperBound, table);
table.setCellAlignment(TableAppender.ALIGN_RIGHT);
if (bbox != null) {
nf.setRoundingMode(RoundingMode.CEILING);
eastBoundLongitude = bbox.getEastBoundLongitude();
table.nextColumn(); table.append(nf.format(new Latitude(bbox.getNorthBoundLatitude())));
table.nextColumn(); table.append(nf.format(new Longitude(eastBoundLongitude)));
}
if (times.length >= 2) {
table.nextColumn();
table.append(times[1].toString());
}
table.flush();
if (Longitude.isWraparound(westBoundLongitude, eastBoundLongitude)) {
vocabulary.appendLabel(Vocabulary.Keys.Note, buffer);
buffer.append(' ')
.append(Messages.forLocale(locale).getString(Messages.Keys.BoxCrossesAntiMeridian));
}
writeNodes();
}
/*
* Example: Envelope
* ├─ Geodetic latitude: -69.75 … 80.25 ∆φ = 0.5°
* └─ Geodetic longitude: 4.75 … 14.75 ∆λ = 0.5°
*
* The minimum number of fraction digits is the number required for differentiating two consecutive cells.
* The maximum number of fraction digits avoids to print more digits than the precision of `double` type.
* Those numbers vary for each line depending on the envelope values and the resolution at that line.
*/
if (section(ENVELOPE, Vocabulary.Keys.Envelope, true, false)) {
final boolean appendResolution = (bitmask & RESOLUTION) != 0 && resolution != null;
final TableAppender table = new TableAppender(buffer, "");
final int dimension = envelope.getDimension();
final NumberFormat nf = NumberFormat.getNumberInstance(locale);
for (int i=0; i<dimension; i++) {
final double lower = envelope.getLower(i);
final double upper = envelope.getUpper(i);
final double delta = (resolution != null) ? resolution[i] : Double.NaN;
nf.setMinimumFractionDigits(Numerics.fractionDigitsForDelta(delta));
nf.setMaximumFractionDigits(Numerics.suggestFractionDigits(lower, upper) - 1); // The -1 is for rounding errors.
final CoordinateSystemAxis axis = (cs != null) ? cs.getAxis(i) : null;
final String name = (axis != null) ? axis.getName().getCode() : vocabulary.getString(Vocabulary.Keys.Dimension_1, i);
table.append(name).append(": ").nextColumn();
table.setCellAlignment(TableAppender.ALIGN_RIGHT);
table.append(nf.format(lower)).nextColumn();
table.setCellAlignment(TableAppender.ALIGN_LEFT);
table.append(" … ").append(nf.format(upper));
if (appendResolution) {
final boolean isLinear = (i < Long.SIZE) && (nonLinears & (1L << i)) == 0;
table.nextColumn();
table.append(" ∆");
if (axis != null) {
table.append(axis.getAbbreviation());
}
table.nextColumn();
table.append(' ').append(isLinear ? '=' : '≈').append(' ');
appendResolution(table, nf, delta, i);
}
table.nextLine();
}
table.flush();
writeNodes();
} else if (section(RESOLUTION, Vocabulary.Keys.Resolution, true, false)) {
/*
* Example: Resolution
* └─ 0.5° × 0.5°
*
* Formatted only as a fallback if the envelope was not formatted.
* Otherwise, this information is already part of above envelope.
*/
String separator = "";
final NumberFormat nf = NumberFormat.getNumberInstance(locale);
for (int i=0; i < resolution.length; i++) {
appendResolution(buffer.append(separator), nf, resolution[i], i);
separator = " × ";
}
writeNode();
}
/*
* Example: Coordinate reference system
* └─ EPSG:4326 — WGS 84 (φ,λ)
*/
if (section(CRS, Vocabulary.Keys.CoordinateRefSys, true, false)) {
final Identifier id = IdentifiedObjects.getIdentifier(crs, null);
if (id != null) {
buffer.append(IdentifiedObjects.toString(id)).append(" — ");
}
buffer.append(crs.getName().getCode());
writeNode();
}
/*
* Example: Conversion
* └─ 2D → 2D non linear in 2
*/
final Matrix matrix = MathTransforms.getMatrix(gridToCRS);
if (section(GRID_TO_CRS, Vocabulary.Keys.Conversion, true, matrix != null)) {
if (matrix != null) {
writeNode(Matrices.toString(matrix));
} else {
buffer.append(gridToCRS.getSourceDimensions()).append("D → ")
.append(gridToCRS.getTargetDimensions()).append("D ");
long nonLinearDimensions = nonLinears;
String separator = Resources.forLocale(locale)
.getString(Resources.Keys.NonLinearInDimensions_1, Long.bitCount(nonLinearDimensions));
while (nonLinearDimensions != 0) {
final int i = Long.numberOfTrailingZeros(nonLinearDimensions);
nonLinearDimensions &= ~(1L << i);
buffer.append(separator).append(' ')
.append(cs != null ? cs.getAxis(i).getName().getCode() : String.valueOf(i));
separator = ",";
}
writeNode();
}
}
}
/**
* Starts a new section for the given property.
*
* @param property one of {@link #EXTENT}, {@link #ENVELOPE}, {@link #CRS}, {@link #GRID_TO_CRS} and {@link #RESOLUTION}.
* @param title the {@link Vocabulary} key for the title to show for this section, if formatted.
* @param mandatory whether to write "undefined" if the property is undefined.
* @param cellCenter whether to add a "origin in cell center" text in the title. This is relevant only for conversion.
* @return {@code true} if the caller shall format the value.
*/
private boolean section(final int property, final short title, final boolean mandatory, final boolean cellCenter) {
if ((bitmask & property) != 0) {
CharSequence text = vocabulary.getString(title);
if (cellCenter) {
text = buffer.append(text).append(" (")
.append(vocabulary.getString(Vocabulary.Keys.OriginInCellCenter).toLowerCase(locale))
.append(')').toString();
buffer.setLength(0);
}
section = root.newChild();
section.setValue(TableColumn.VALUE_AS_TEXT, text);
if (isDefined(property)) {
return true;
}
if (mandatory) {
writeNode(vocabulary.getString(Vocabulary.Keys.Unspecified));
}
}
return false;
}
/**
* Appends a single line as a node in the current section.
*/
private void writeNode(final CharSequence line) {
String text = line.toString().trim();
if (!text.isEmpty()) {
section.newChild().setValue(TableColumn.VALUE_AS_TEXT, text);
}
}
/**
* Appends a node with current {@link #buffer} content as a single line, then clears the buffer.
*/
private void writeNode() {
writeNode(buffer);
buffer.setLength(0);
}
/**
* Appends nodes with current {@link #buffer} content as multi-lines text, then clears the buffer.
*/
private void writeNodes() {
for (final CharSequence line : CharSequences.splitOnEOL(buffer)) {
writeNode(line);
}
buffer.setLength(0);
}
/**
* Appends a single value on the resolution line, together with its unit of measurement.
*
* @param out where to write the resolution.
* @param nf number format to use for writing the number.
* @param res the resolution to write, or {@link Double#NaN}.
* @param dim index of the coordinate system axis of the resolution.
*/
private void appendResolution(final Appendable out, final NumberFormat nf, final double res, final int dim) throws IOException {
if (Double.isNaN(res)) {
out.append('?');
} else {
nf.setMaximumFractionDigits(Numerics.suggestFractionDigits(res) / 2);
out.append(nf.format(res));
}
if (cs != null) {
final String unit = String.valueOf(cs.getAxis(dim).getUnit());
if (unit.isEmpty() || Character.isLetterOrDigit(unit.codePointAt(0))) {
out.append(' ');
}
out.append(unit);
}
}
}
}