/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2005-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2007-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotoolkit.coverage.sql;
import java.util.Map;
import java.util.HashMap;
import java.util.Objects;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.awt.Dimension;
import java.awt.geom.Rectangle2D;
import java.awt.geom.AffineTransform;
import javax.measure.IncommensurableException;
import org.opengis.util.FactoryException;
import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform1D;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.MathTransformFactory;
import org.opengis.referencing.operation.OperationNotFoundException;
import org.apache.sis.util.ArraysExt;
import org.geotoolkit.util.Utilities;
import org.apache.sis.util.logging.Logging;
import org.geotoolkit.coverage.grid.GeneralGridEnvelope;
import org.geotoolkit.coverage.grid.GeneralGridGeometry;
import org.apache.sis.internal.metadata.AxisDirections;
import org.geotoolkit.internal.sql.table.SpatialDatabase;
import org.apache.sis.referencing.crs.DefaultCompoundCRS;
import org.apache.sis.referencing.crs.DefaultTemporalCRS;
import org.apache.sis.referencing.crs.DefaultGeographicCRS;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.AffineTransforms2D;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
import org.geotoolkit.resources.Errors;
import org.apache.sis.referencing.cs.CoordinateSystems;
/**
* The horizontal, vertical and temporal components of a {@link GridGeometryEntry} CRS.
* The CRS are built from the SRID declared in the {@code "GridGeometries"} table, linked
* to the values declared in the PostGIS {@code "spatial_ref_sys"} table.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.20
*
* @since 3.10 (derived from Seagis)
* @module
*/
final class SpatialRefSysEntry {
/**
* Small tolerance factor for comparisons of floating point values.
* An angular value of 1E-8° is approximatively 1 millimetre on Earth.
*/
static final double EPS = 1E-8;
/**
* The horizontal and vertical SRID declared in the database.
*/
final int horizontalSRID, verticalSRID;
/**
* The horizontal CRS. This is created by {@link #createSpatioTemporalCRS}.
*
* @see #horizontalSRID
*/
private SingleCRS horizontalCRS;
/**
* The vertical CRS, or {@code null} if none.
* This is created by {@link #createSpatioTemporalCRS}.
*
* @see #verticalSRID
*/
private VerticalCRS verticalCRS;
/**
* The temporal CRS, or {@code null} if none.
*/
final DefaultTemporalCRS temporalCRS;
/**
* The {@link #spatioTemporalCRS} without the temporal component.
* This is created by {@link #createSpatioTemporalCRS}.
*
* @see #getSpatioTemporalCRS(boolean)
*/
private CoordinateReferenceSystem spatialCRS;
/**
* The coordinate reference system made of the combination of all the above.
* This is created by {@link #createSpatioTemporalCRS}.
*
* @see #getSpatioTemporalCRS(boolean)
*/
private CoordinateReferenceSystem spatioTemporalCRS;
/**
* The database horizontal CRS. This is a copy of the {@link SpatialDatabase#horizontalCRS}
* reference and is initialized by {@link #createSpatioTemporalCRS}.
*
* @see #getDatabaseCRS()
*/
private SingleCRS databaseCRS;
/**
* Whatever default grid range computation should be performed on transforms relative to pixel
* center or relative to pixel corner. This is a copy of the {@link SpatialDatabase#pixelInCell}
* reference and is initialized by {@link #createSpatioTemporalCRS}.
*/
private PixelInCell pixelInCell;
/**
* The transform to the database horizontal CRS. The database uses a single CRS for
* indexing the whole {@code GridGeometries} table, while individual records may use
* different CRS.
* <p>
* This is created by {@link #createSpatioTemporalCRS}.
*
* @see #toDatabaseHorizontalCRS()
*/
private MathTransform2D toDatabaseHorizontalCRS;
/**
* The transform to the database vertical CRS.
* This is created by {@link #createSpatioTemporalCRS}.
*
* @see #toDatabaseVerticalCRS()
*/
private MathTransform1D toDatabaseVerticalCRS;
/**
* The coordinate system of the {@link #horizontalCRS} using a positive range of longitude
* values ([0…360]°) instead than the default [-180 … 180]° range. This field is non-null
* only if the horizontal CRS is geographic, not projected.
*
* @since 3.20
*/
private CoordinateSystem shiftedCS;
/**
* Constructs a new entry for the given SRID.
*
* @param horizontalSRID The SRID of the horizontal CRS, or {@code 0} if none.
* @param verticalSRID The SRID of the vertical CRS, or {@code 0} if none.
* @param temporalCRS The temporal CRS, or {@code null} if none.
*/
SpatialRefSysEntry(final int horizontalSRID, final int verticalSRID, final DefaultTemporalCRS temporalCRS) {
this.horizontalSRID = horizontalSRID;
this.verticalSRID = verticalSRID;
this.temporalCRS = temporalCRS;
}
/**
* If all CRS are initialized, returns 0. Otherwise returns the code of the first
* uninitialized CRS: 1 for horizontal, 2 for vertical, 3 for temporal or 4 for the
* 4D CRS. This method is used only for checking error conditions.
*/
final int uninitialized() {
if (horizontalCRS == null && horizontalSRID != 0) return 1;
if (verticalCRS == null && verticalSRID != 0) return 2;
if (temporalCRS == null) return 3;
if (spatioTemporalCRS == null) return 4;
return 0;
}
/**
* Creates the horizontal and vertical CRS from the factory bundled in the given table.
* The CRS are fetched only after the {@code SpatialRefSysEntry} creation (instead than
* at creation time) in order to have a chance to search for an existing entry in the
* cache before to create the CRS.
*
* @param database The database.
* @throws FactoryException if an error occurred while creating the CRS.
*/
final void createSpatioTemporalCRS(final SpatialDatabase database) throws FactoryException {
assert uninitialized() != 0 : this;
databaseCRS = database.horizontalCRS;
pixelInCell = database.pixelInCell;
/*
* Get the CRS components (Horizontal, Vertical, Temporal) from the PostGIS SRID,
* except the temporal component which was explicitly given at construction time.
*/
final CRSAuthorityFactory factory = database.getCRSAuthorityFactory();
if (horizontalSRID != 0) {
final CoordinateReferenceSystem crs =
factory.createCoordinateReferenceSystem(String.valueOf(horizontalSRID));
try {
horizontalCRS = (SingleCRS) crs;
} catch (ClassCastException e) {
throw new FactoryException(Errors.format(
Errors.Keys.IllegalClass_2, crs.getClass(), SingleCRS.class), e);
}
}
if (verticalSRID != 0) {
verticalCRS = factory.createVerticalCRS(String.valueOf(verticalSRID));
}
/*
* Assemble the components together in a spatio-temporal Compound CRS.
*/
int count = 0;
SingleCRS[] elements = new SingleCRS[3];
if (horizontalCRS != null) elements[count++] = horizontalCRS;
if (verticalCRS != null) elements[count++] = verticalCRS;
if (temporalCRS != null) elements[count++] = temporalCRS;
switch (count) {
case 0: {
throw new FactoryException(Errors.format(Errors.Keys.UnspecifiedCrs));
}
case 1: {
spatioTemporalCRS = elements[0];
if (spatioTemporalCRS != temporalCRS) {
spatialCRS = spatioTemporalCRS;
}
break;
}
default: {
final SingleCRS headCRS = elements[0];
elements = ArraysExt.resize(elements, count);
Map<String,?> properties = IdentifiedObjects.getProperties(headCRS);
if (verticalCRS != null) {
String name = headCRS.getName().getCode();
name = name + " + " + verticalCRS.getName().getCode();
final Map<String,Object> copy = new HashMap<>(properties);
copy.put(CoordinateReferenceSystem.NAME_KEY, name);
properties = copy;
}
final CRSFactory crsFactory = database.getCRSFactory();
spatioTemporalCRS = crsFactory.createCompoundCRS(properties, elements);
if (temporalCRS == null) {
spatialCRS = spatioTemporalCRS;
} else {
if (--count == 1) {
spatialCRS = elements[0];
} else {
elements = ArraysExt.resize(elements, count);
spatialCRS = crsFactory.createCompoundCRS(properties, elements);
}
}
}
}
assert CRS.getHorizontalComponent(spatioTemporalCRS) == CRS.getHorizontalComponent(spatialCRS);
assert CRS.getVerticalComponent (spatioTemporalCRS, false) == CRS.getVerticalComponent (spatialCRS, false);
/*
* Get the MathTransforms from coverage CRS to database CRS.
*/
SingleCRS sourceCRS = horizontalCRS;
SingleCRS targetCRS = database.horizontalCRS;
if (sourceCRS != null && targetCRS != null) {
toDatabaseHorizontalCRS = (MathTransform2D) CRS.findOperation(sourceCRS, targetCRS, null).getMathTransform();
}
sourceCRS = verticalCRS;
targetCRS = database.verticalCRS;
if (sourceCRS != null && targetCRS != null) {
MathTransform tr;
try {
tr = CRS.findOperation(sourceCRS, targetCRS, null).getMathTransform();
} catch (OperationNotFoundException e) {
final Matrix matrix;
try {
matrix = CoordinateSystems.swapAndScaleAxes(sourceCRS.getCoordinateSystem(),
targetCRS.getCoordinateSystem());
} catch (IncommensurableException | IllegalArgumentException ignore) {
throw e;
}
tr = database.getMathTransformFactory().createAffineTransform(matrix);
/*
* Be lenient with vertical transformations, because many of them are not yet
* implemented (e.g. "mean sea level" to "ellipsoidal"). Log a warning without
* stack trace in order to not scare the user too much. Use GridGeometryTable
* as the source class since SpatialRefSysEntry is too low level.
*/
Logging.log(GridGeometryTable.class, "createEntry",
new LogRecord(Level.WARNING, e.getLocalizedMessage()));
}
toDatabaseVerticalCRS = (MathTransform1D) tr;
}
/*
* At this point, all CRS have been initialized. Now find the longitude dimension
* and instantiate a coordinate system using a [0…360]° range of longitude values.
*/
if (horizontalCRS instanceof GeographicCRS) {
final EllipsoidalCS cs = ((GeographicCRS) horizontalCRS).getCoordinateSystem();
final int i = AxisDirections.indexOfColinear(cs, AxisDirection.EAST);
if (i >= 0) {
final DefaultEllipsoidalCS geotk = DefaultEllipsoidalCS.castOrCopy(cs);
shiftedCS = geotk.forConvention(AxesConvention.POSITIVE_RANGE);
if (shiftedCS == geotk) {
shiftedCS = null;
}
}
}
}
/**
* Returns the coordinate reference system, which may be up to 4-dimensional.
* The {@link #createSpatioTemporalCRS} method must have been invoked before this method.
*
* @param includeTime {@code true} if the CRS should include the time component,
* or {@code false} for a spatial-only CRS.
* @param needsLongitudeShift {@code true} if the grid geometry needs longitude
* values in the [0…360]° range instead than the default [-180 … 180]° range.
*/
public CoordinateReferenceSystem getSpatioTemporalCRS(final boolean includeTime,
final boolean needsLongitudeShift)
{
assert uninitialized() == 0 : this;
CoordinateReferenceSystem crs = includeTime ? spatioTemporalCRS : spatialCRS;
if (needsLongitudeShift) {
if (crs instanceof GeographicCRS) {
final DefaultGeographicCRS geotk = DefaultGeographicCRS.castOrCopy((GeographicCRS) crs);
crs = geotk.forConvention(AxesConvention.POSITIVE_RANGE);
if (!includeTime) spatialCRS = geotk;
else spatioTemporalCRS = geotk;
} else if (crs instanceof CompoundCRS) {
final DefaultCompoundCRS geotk = DefaultCompoundCRS.castOrCopy((CompoundCRS) crs);
crs = geotk.forConvention(AxesConvention.POSITIVE_RANGE);
if (!includeTime) spatialCRS = geotk;
else spatioTemporalCRS = geotk;
}
}
return crs;
}
/**
* Returns the database horizontal CRS used in PostGIS geometry columns.
*/
final SingleCRS getDatabaseCRS() {
assert uninitialized() == 0 : this;
return databaseCRS;
}
/**
* Returns the transform from this entry horizontal CRS to the database horizontal CRS,
* or {@code null} if none.
*/
final MathTransform2D toDatabaseHorizontalCRS() {
assert uninitialized() == 0 : this;
return toDatabaseHorizontalCRS;
}
/**
* Returns the transform from this entry vertical CRS to the database vertical CRS,
* or {@code null} if none.
*/
final MathTransform1D toDatabaseVerticalCRS() {
assert uninitialized() == 0 : this;
return toDatabaseVerticalCRS;
}
/**
* Returns whatever default grid range computation should be performed on transforms
* relative to pixel center or relative to pixel corner.
*/
final PixelInCell getPixelInCell() {
assert uninitialized() == 0 : this;
return pixelInCell;
}
/**
* Returns {@code true} if the given grid geometry seems to use a longitude values in
* the [0…360]° range instead than the default [-180 … 180]° range.
*
* @since 3.20
*/
final boolean needsLongitudeShift(final Dimension size, final AffineTransform gridToCRS) {
assert uninitialized() == 0 : this;
final CoordinateSystem shiftedCS = this.shiftedCS; // Protect from changes.
if (shiftedCS != null) {
Rectangle2D bounds = new Rectangle2D.Double(0, 0, size.width, size.height);
bounds = AffineTransforms2D.transform(gridToCRS, bounds, bounds);
final CoordinateSystem standardCS = horizontalCRS.getCoordinateSystem();
for (int i=0; i<=1; i++) {
final CoordinateSystemAxis standardAxis = standardCS.getAxis(i);
final CoordinateSystemAxis shiftedAxis = shiftedCS .getAxis(i);
if (standardAxis != shiftedAxis) {
final double min, max;
switch (i) {
case 0: min = bounds.getMinX(); max = bounds.getMaxX(); break;
case 1: min = bounds.getMinY(); max = bounds.getMaxY(); break;
default: throw new AssertionError(i);
}
if (min+EPS >= shiftedAxis .getMinimumValue() && // Always >= 0 for shifted axis.
max+EPS >= standardAxis.getMaximumValue() &&
max-EPS <= shiftedAxis .getMaximumValue())
{
return true;
}
}
}
}
return false;
}
/**
* Returns a grid geometry for the given horizontal size and transform, and the given vertical
* ordinate values. The given factory is used for creating the <cite>grid to CRS</cite>
* transform. Dimensions are handled as below:
* <p>
* <ul>
* <li>The horizontal dimension is setup using the {@code size} and {@code gridToCRS} parameters.</li>
* <li>The coefficients for the vertical axis assume that the vertical ordinates are evenly
* spaced. This is not always true; a special processing will be performed later by
* {@link GridCoverageEntry}.</li>
* <li>The time dimension, if any, is left to the identity transform.</li>
* </ul>
* <p>
* The {@link #createSpatioTemporalCRS} method must have been invoked before this method.
*
* @param includeTime {@code true} if the CRS should include the time component,
* or {@code false} for a spatial-only CRS.
* @param needsLongitudeShift {@code true} if the grid geometry needs longitude
* values in the [0…360]° range instead than the default [-180 … 180]° range.
*/
final GeneralGridGeometry createGridGeometry(final Dimension size, final AffineTransform gridToCRS,
final double[] altitudes, final MathTransformFactory mtFactory, final boolean includeTime,
final boolean needsLongitudeShift) throws FactoryException
{
assert uninitialized() == 0 : this;
final CoordinateReferenceSystem crs = getSpatioTemporalCRS(includeTime, needsLongitudeShift);
final int dim = crs.getCoordinateSystem().getDimension();
final int[] lower = new int[dim];
final int[] upper = new int[dim];
final Matrix matrix = Matrices.createIdentity(dim + 1);
int verticalDim = 0;
if (horizontalCRS != null) {
copy(gridToCRS, matrix);
verticalDim = horizontalCRS.getCoordinateSystem().getDimension();
}
if (verticalCRS != null && altitudes != null) {
int n = altitudes.length;
if (n != 0) {
upper[verticalDim] = n;
final double offset = altitudes[0];
final double scale = (--n == 0) ? 0 : (altitudes[n] - offset) / n;
matrix.setElement(verticalDim, verticalDim, scale); // May be negative.
matrix.setElement(verticalDim, dim, offset);
}
}
upper[0] = size.width;
upper[1] = size.height;
final GridEnvelope gridExtent = new GeneralGridEnvelope(lower, upper, false);
return new GeneralGridGeometry(gridExtent, pixelInCell, mtFactory.createAffineTransform(matrix), crs);
}
/**
* Copies the affine transform coefficients into the two first dimensions of the affine
* transform represented by the target matrix.
*/
static void copy(final AffineTransform source, final Matrix target) {
final int dim = target.getNumCol() - 1;
target.setElement(0, 0, source.getScaleX());
target.setElement(1, 1, source.getScaleY());
target.setElement(0, 1, source.getShearX());
target.setElement(1, 0, source.getShearY());
target.setElement(0, dim, source.getTranslateX());
target.setElement(1, dim, source.getTranslateY());
}
/**
* Returns the dimension for the <var>z</var> axis, or {@code -1} if none.
* The {@code #createSpatialCRS} method must have been invoked once before
* this method is invoked.
*/
final int zDimension() {
assert uninitialized() == 0 : this;
return (verticalCRS == null) ? -1 : (horizontalCRS == null) ? 0 :
horizontalCRS.getCoordinateSystem().getDimension();
}
/**
* Returns a hash code value for this entry. The value must be determined only from the
* arguments given at construction time, i.e. it must be unchanged by call to any method
* in this class.
*/
@Override
public int hashCode() {
// 100003 is a prime number assumed large enough for avoiding overlapping between SRID.
return Utilities.hash(temporalCRS, horizontalSRID + 100003*verticalSRID);
}
/**
* Compares this entry with the specified object for equality. The comparison must include
* only the arguments given at construction time, which are final. The other arguments
* (computed only when first needed) must not be compared.
*/
@Override
public boolean equals(final Object object) {
if (object instanceof SpatialRefSysEntry) {
final SpatialRefSysEntry that = (SpatialRefSysEntry) object;
return this.horizontalSRID == that.horizontalSRID &&
this.verticalSRID == that.verticalSRID &&
Objects.equals(this.temporalCRS, that.temporalCRS);
}
return false;
}
/**
* Returns a string representation for debugging purpose.
*/
@Override
public String toString() {
return getClass().getSimpleName() + "[h=" + horizontalSRID + ", v=" + verticalSRID + ']';
}
}