/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2004-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.
*
* This package contains documentation from OpenGIS specifications.
* OpenGIS consortium's work is fully acknowledged here.
*/
package org.geotools.referencing.cs;
import java.util.Map;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Collections;
import javax.measure.unit.SI;
import javax.measure.unit.Unit;
import javax.measure.converter.UnitConverter;
import javax.measure.converter.ConversionException;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.Matrix;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.util.InternationalString;
import org.geotools.util.Utilities;
import org.geotools.measure.Measure;
import org.geotools.referencing.AbstractIdentifiedObject;
import org.geotools.referencing.operation.matrix.GeneralMatrix;
import org.geotools.referencing.wkt.Formatter;
import org.geotools.resources.Classes;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Vocabulary;
/**
* The set of coordinate system axes that spans a given coordinate space. A coordinate system (CS)
* is derived from a set of (mathematical) rules for specifying how coordinates in a given space
* are to be assigned to points. The coordinate values in a coordinate tuple shall be recorded in
* the order in which the coordinate system axes are recorded, whenever those
* coordinates use a coordinate reference system that uses this coordinate system.
* <p>
* This class is conceptually <cite>abstract</cite>, even if it is technically possible to
* instantiate it. Typical applications should create instances of the most specific subclass with
* {@code Default} prefix instead. An exception to this rule may occurs when it is not possible to
* identify the exact type. For example it is not possible to infer the exact coordinate system from
* <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
* Known Text</cite></A> is some cases (e.g. in a {@code LOCAL_CS} element). In such exceptional
* situation, a plain {@code AbstractCS} object may be instantiated.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*
* @see DefaultCoordinateSystemAxis
* @see javax.measure.unit.Unit
* @see org.geotools.referencing.datum.AbstractDatum
* @see org.geotools.referencing.crs.AbstractCRS
*/
public class AbstractCS extends AbstractIdentifiedObject implements CoordinateSystem {
/**
* Serial number for interoperability with different versions.
*/
private static final long serialVersionUID = 6757665252533744744L;
/**
* Base axis to use for checking directions. This is used in order to trap
* inconsistency like an axis named "Northing" with South direction.
*/
private static final DefaultCoordinateSystemAxis[] DIRECTION_CHECKS = {
DefaultCoordinateSystemAxis.NORTHING,
DefaultCoordinateSystemAxis.EASTING,
DefaultCoordinateSystemAxis.SOUTHING,
DefaultCoordinateSystemAxis.WESTING
};
/**
* The axis for this coordinate system at the specified dimension.
*/
private final CoordinateSystemAxis[] axis;
/**
* The unit for measuring distance in this coordinate system, or {@code null} if none.
* Will be computed only when first needed.
*/
private transient Unit<?> distanceUnit;
/**
* Constructs a new coordinate system with the same values than the specified one.
* This copy constructor provides a way to wrap an arbitrary implementation into a
* Geotools one or a user-defined one (as a subclass), usually in order to leverage
* some implementation-specific API. This constructor performs a shallow copy,
* i.e. the properties are not cloned.
*
* @param cs The coordinate system to copy.
*
* @since 2.2
*/
public AbstractCS(final CoordinateSystem cs) {
super(cs);
if (cs instanceof AbstractCS) {
axis = ((AbstractCS) cs).axis;
} else {
axis = new CoordinateSystemAxis[cs.getDimension()];
for (int i=0; i<axis.length; i++) {
axis[i] = cs.getAxis(i);
}
}
}
/**
* Constructs a coordinate system from a name.
*
* @param name The coordinate system name.
* @param axis The set of axis.
*/
public AbstractCS(final String name, final CoordinateSystemAxis[] axis) {
this(Collections.singletonMap(NAME_KEY, name), axis);
}
/**
* Constructs a coordinate system from a set of properties. The properties map is given
* unchanged to the {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map)
* super-class constructor}.
*
* @param properties Set of properties. Should contains at least {@code "name"}.
* @param axis The set of axis.
*/
public AbstractCS(final Map<String,?> properties, final CoordinateSystemAxis[] axis) {
super(properties);
ensureNonNull("axis", axis);
this.axis = axis.clone();
for (int i=0; i<axis.length; i++) {
ensureNonNull("axis", axis, i);
final AxisDirection direction = axis[i].getDirection();
ensureNonNull("direction", direction);
/*
* Ensures that axis direction and units are compatible with the
* coordinate system to be created. For example CartesianCS will
* accepts only linear or dimensionless units.
*/
if (!isCompatibleDirection(direction)) {
// TOOD: localize name()
throw new IllegalArgumentException(Errors.format(
ErrorKeys.ILLEGAL_AXIS_ORIENTATION_$2, direction.name(), getClass()));
}
final Unit<?> unit = axis[i].getUnit();
ensureNonNull("unit", unit);
if (!isCompatibleUnit(direction, unit)) {
throw new IllegalArgumentException(Errors.format(
ErrorKeys.INCOMPATIBLE_UNIT_$1, unit));
}
/*
* Ensures there is no axis along the same direction
* (e.g. two North axis, or an East and a West axis).
*/
final AxisDirection check = direction.absolute();
if (!check.equals(AxisDirection.OTHER)) {
for (int j=i; --j>=0;) {
if (check.equals(axis[j].getDirection().absolute())) {
// TODO: localize name()
final String nameI = axis[i].getDirection().name();
final String nameJ = axis[j].getDirection().name();
throw new IllegalArgumentException(Errors.format(
ErrorKeys.COLINEAR_AXIS_$2, nameI, nameJ));
}
}
}
/*
* Checks for some inconsistency in naming and direction. For example if the axis
* is named "Northing", then the direction must be North. Exceptions to this rule
* are the directions along a meridian from a pole. For example a "Northing" axis
* may have a "South along 180 deg" direction.
*/
final String name = axis[i].getName().getCode();
for (int j=0; j<DIRECTION_CHECKS.length; j++) {
final DefaultCoordinateSystemAxis candidate = DIRECTION_CHECKS[j];
if (candidate.nameMatches(name)) {
final AxisDirection expected = candidate.getDirection();
if (!direction.equals(expected)) {
DirectionAlongMeridian m = DirectionAlongMeridian.parse(direction);
/*
* Note: for the check below, maybe it would have be nice to use:
*
* if (m == null || m.baseDirection.equals(expected.opposite())
*
* but the EPSG database contains many axis named "Northing" with
* direction like "South along 180 deg", so it doesn't seem to be
* considered as a contradiction...
*/
if (m == null) {
throw new IllegalArgumentException(Errors.format(
ErrorKeys.INCONSISTENT_AXIS_ORIENTATION_$2,
name, direction.name()));
}
}
}
}
}
}
/**
* Creates a name for the predefined constants in subclasses. The name is an unlocalized String
* object. However, since this method is used for creation of convenience objects only (not for
* objects created from an "official" database), the "unlocalized" name is actually choosen
* according the user's locale at class initialization time. The same name is also added in
* a localizable form as an alias. Since the {@link #nameMatches} convenience method checks
* the alias, it still possible to consider two objects are equivalent even if their names
* were formatted in different locales.
*/
static Map<String,Object> name(final int key) {
final Map<String,Object> properties = new HashMap<String,Object>(4);
final InternationalString name = Vocabulary.formatInternational(key);
properties.put(NAME_KEY, name.toString());
properties.put(ALIAS_KEY, name);
return properties;
}
/**
* Returns {@code true} if the specified axis direction is allowed for this coordinate
* system. This method is invoked at construction time for checking argument validity.
* The default implementation returns {@code true} for all axis directions. Subclasses
* will overrides this method in order to put more restrictions on allowed axis directions.
*
* @param direction The direction to test for compatibility.
* @return {@code true} if the given direction is compatible with this coordinate system.
*/
protected boolean isCompatibleDirection(final AxisDirection direction) {
return true;
}
/**
* Returns {@code true} is the specified unit is legal for the specified axis direction.
* This method is invoked at construction time for checking units compatibility. The default
* implementation returns {@code true} in all cases. Subclasses can override this method and
* check for compatibility with {@linkplain SI#METER meter} or
* {@linkplain NonSI#DEGREE_ANGLE degree} units.
*
* @param direction The direction of the axis having the given unit.
* @param unit The unit to test for compatibility.
* @return {@code true} if the given unit is compatible with this coordinate system.
*
* @since 2.2
*/
protected boolean isCompatibleUnit(final AxisDirection direction, final Unit<?> unit) {
return true;
}
/**
* Returns the dimension of the coordinate system.
* This is the number of axis.
*/
public int getDimension() {
return axis.length;
}
/**
* Returns the axis for this coordinate system at the specified dimension.
*
* @param dimension The zero based index of axis.
* @return The axis at the specified dimension.
* @throws IndexOutOfBoundsException if {@code dimension} is out of bounds.
*/
public CoordinateSystemAxis getAxis(final int dimension) throws IndexOutOfBoundsException {
return axis[dimension];
}
/**
* Returns the axis direction for the specified coordinate system.
*
* @param cs The coordinate system.
* @return The axis directions for the specified coordinate system.
*/
private static AxisDirection[] getAxisDirections(final CoordinateSystem cs) {
final AxisDirection[] axis = new AxisDirection[cs.getDimension()];
for (int i=0; i<axis.length; i++) {
axis[i] = cs.getAxis(i).getDirection();
}
return axis;
}
/**
* Returns an affine transform between two coordinate systems. Only units and
* axis order (e.g. transforming from
* ({@linkplain AxisDirection#NORTH NORTH},{@linkplain AxisDirection#WEST WEST}) to
* ({@linkplain AxisDirection#EAST EAST},{@linkplain AxisDirection#NORTH NORTH}
* are taken in account.
* <p>
* <b>Example:</b> If coordinates in {@code sourceCS} are (<var>x</var>,<var>y</var>) pairs
* in metres and coordinates in {@code targetCS} are (-<var>y</var>,<var>x</var>) pairs in
* centimetres, then the transformation can be performed as below:
*
* <pre><blockquote>
* [-y(cm)] [ 0 -100 0 ] [x(m)]
* [ x(cm)] = [ 100 0 0 ] [y(m)]
* [ 1 ] [ 0 0 1 ] [1 ]
* </blockquote></pre>
*
* @param sourceCS The source coordinate system.
* @param targetCS The target coordinate system.
* @return The conversion from {@code sourceCS} to {@code targetCS} as
* an affine transform. Only axis direction and units are taken in account.
* @throws IllegalArgumentException if axis doesn't matches, or the CS doesn't have the
* same geometry.
* @throws ConversionException if the unit conversion is non-linear.
*/
public static Matrix swapAndScaleAxis(final CoordinateSystem sourceCS,
final CoordinateSystem targetCS)
throws IllegalArgumentException, ConversionException
{
if (!Classes.sameInterfaces(sourceCS.getClass(), targetCS.getClass(), CoordinateSystem.class)) {
throw new IllegalArgumentException(Errors.format(
ErrorKeys.INCOMPATIBLE_COORDINATE_SYSTEM_TYPE));
}
final AxisDirection[] sourceAxis = getAxisDirections(sourceCS);
final AxisDirection[] targetAxis = getAxisDirections(targetCS);
final GeneralMatrix matrix = new GeneralMatrix(sourceAxis, targetAxis);
assert Arrays.equals(sourceAxis, targetAxis) == matrix.isIdentity() : matrix;
/*
* The previous code computed a matrix for swapping axis. Usually, this
* matrix contains only 0 and 1 values with only one "1" value by row.
* For example, the matrix operation for swapping x and y axis is:
*
* [y] [ 0 1 0 ] [x]
* [x] = [ 1 0 0 ] [y]
* [1] [ 0 0 1 ] [1]
*
* Now, take in account units conversions. Each matrix's element (j,i)
* is multiplied by the conversion factor from sourceCS.getUnit(i) to
* targetCS.getUnit(j). This is an element-by-element multiplication,
* not a matrix multiplication. The last column is processed in a special
* way, since it contains the offset values.
*/
final int sourceDim = matrix.getNumCol()-1;
final int targetDim = matrix.getNumRow()-1;
assert sourceDim == sourceCS.getDimension() : sourceCS;
assert targetDim == targetCS.getDimension() : targetCS;
for (int j=0; j<targetDim; j++) {
final Unit<?> targetUnit = targetCS.getAxis(j).getUnit();
for (int i=0; i<sourceDim; i++) {
final double element = matrix.getElement(j,i);
if (element == 0) {
// There is no dependency between source[i] and target[j]
// (i.e. axis are orthogonal).
continue;
}
final Unit<?> sourceUnit = sourceCS.getAxis(i).getUnit();
if (Utilities.equals(sourceUnit, targetUnit)) {
// There is no units conversion to apply
// between source[i] and target[j].
continue;
}
final UnitConverter converter = sourceUnit.getConverterTo(targetUnit);
if (!converter.isLinear()) {
throw new ConversionException(Errors.format(
ErrorKeys.NON_LINEAR_UNIT_CONVERSION_$2, sourceUnit, targetUnit));
}
final double offset = converter.convert(0);
// JSR-275 final double scale = converter.derivative(0);
final double scale = converter.convert(1) - offset;
matrix.setElement(j,i, element*scale);
matrix.setElement(j,sourceDim, matrix.getElement(j,sourceDim) + element*offset);
}
}
return matrix;
}
/**
* Returns a coordinate system with "standard" axis order and units.
* Most of the time, this method returns one of the predefined constants with axis in
* (<var>longitude</var>,<var>latitude</var>) or (<var>X</var>,<var>Y</var>) order,
* and units in degrees or metres. In some particular cases like
* {@linkplain org.opengis.referencing.cs.CartesianCS Cartesian CS}, this method may
* create a new instance on the fly. In every cases this method attempts to return a
* <A HREF="http://en.wikipedia.org/wiki/Right_hand_rule">right-handed</A> coordinate
* system, but this is not garanteed.
* <p>
* This method is typically used together with {@link #swapAndScaleAxis swapAndScaleAxis}
* for the creation of a transformation step before some
* {@linkplain org.opengis.referencing.operation.MathTransform math transform}.
* Example:
*
* <blockquote><pre>
* Matrix step1 = swapAndScaleAxis(sourceCS, standard(sourceCS));
* Matrix step2 = ... some transform operating on standard axis ...
* Matrix step3 = swapAndScaleAxis(standard(targetCS), targetCS);
* </pre></blockquote>
*
* A rational for standard axis order and units is explained in the <cite>Axis units and
* direction</cite> section in the {@linkplain org.geotools.referencing.operation.projection
* description of map projection package}.
*
* @param cs The coordinate system.
* @return A constant similar to the specified {@code cs} with "standard" axis.
* @throws IllegalArgumentException if the specified coordinate system is unknow to this method.
*
* @since 2.2
*/
public static CoordinateSystem standard(final CoordinateSystem cs)
throws IllegalArgumentException
{
return PredefinedCS.standard(cs);
}
/**
* Suggests an unit for measuring distances in this coordinate system. The default
* implementation scans all {@linkplain CoordinateSystemAxis#getUnit axis units},
* ignoring angular ones (this also implies ignoring {@linkplain Unit#ONE dimensionless} ones).
* If more than one non-angular unit is found, the default implementation returns the "largest"
* one (e.g. kilometers instead of meters).
*
* @return Suggested distance unit.
* @throws ConversionException if some non-angular units are incompatibles.
*/
final Unit<?> getDistanceUnit() throws ConversionException {
Unit<?> unit = distanceUnit; // Avoid the need for synchronization.
if (unit == null) {
for (int i=0; i<axis.length; i++) {
final Unit<?> candidate = axis[i].getUnit();
if (candidate!=null && !candidate.isCompatible(SI.RADIAN)) {
// TODO: checks the unit scale type (keeps RATIO only).
if (unit != null) {
final UnitConverter converter = candidate.getConverterTo(unit);
if (!converter.isLinear()) {
// TODO: use the localization provided in 'swapAxis'. We could also
// do a more intelligent work by checking the unit scale type.
throw new ConversionException("Unit conversion is non-linear");
}
// JSR-275 final double scale = converter.derivative(0);
final double scale = converter.convert(1) - converter.convert(0);
if (Math.abs(scale) <= 1) {
// The candidate is a "smaller" unit than the current one
// (e.g. "m" instead of "km"). Keeps the "largest" unit.
continue;
}
}
unit = candidate;
}
}
distanceUnit = unit;
}
return unit;
}
/**
* Convenience method for checking object dimension validity.
*
* @param name The name of the argument to check.
* @param coordinates The coordinate array to check.
* @throws MismatchedDimensionException if the coordinate doesn't have the expected dimension.
*/
final void ensureDimensionMatch(final String name, final double[] coordinates)
throws MismatchedDimensionException
{
if (coordinates.length != axis.length) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$3, name, coordinates.length, axis.length));
}
}
/**
* Computes the distance between two points. This method is not available for all coordinate
* systems. For example, {@linkplain DefaultEllipsoidalCS ellipsoidal CS} doesn't have
* suffisient information.
*
* @param coord1 Coordinates of the first point.
* @param coord2 Coordinates of the second point.
* @return The distance between {@code coord1} and {@code coord2}.
* @throws UnsupportedOperationException if this coordinate system can't compute distances.
* @throws MismatchedDimensionException if a coordinate doesn't have the expected dimension.
*
* @todo Provides a localized message in the exception.
*/
public Measure distance(final double[] coord1, final double[] coord2)
throws UnsupportedOperationException, MismatchedDimensionException
{
throw new UnsupportedOperationException();
}
/**
* Returns all axis in the specified unit. This method is used for implementation of
* {@code usingUnit} methods in subclasses.
*
* @param unit The unit for the new axis.
* @return New axis using the specified unit, or {@code null} if current axis fits.
* @throws IllegalArgumentException If the specified unit is incompatible with the expected one.
*
* @see DefaultCartesianCS#usingUnit
* @see DefaultEllipsoidalCS#usingUnit
*/
final CoordinateSystemAxis[] axisUsingUnit(final Unit<?> unit) throws IllegalArgumentException {
CoordinateSystemAxis[] newAxis = null;
for (int i=0; i<axis.length; i++) {
CoordinateSystemAxis a = axis[i];
if (!unit.equals(a.getUnit())) {
DefaultCoordinateSystemAxis converted;
if (a instanceof DefaultCoordinateSystemAxis) {
converted = (DefaultCoordinateSystemAxis) a;
} else {
converted = new DefaultCoordinateSystemAxis(a);
a = converted; // For detecting changes.
}
converted = converted.usingUnit(unit);
if (converted != a) {
if (newAxis == null) {
newAxis = new CoordinateSystemAxis[axis.length];
System.arraycopy(axis, 0, newAxis, 0, i);
}
newAxis[i] = converted;
}
}
}
return newAxis;
}
/**
* Returns every axis from the specified coordinate system as instance of
* {@link DefaultCoordinateSystemAxis}. This allow usage of some methods
* specific to that implementation.
*/
private static DefaultCoordinateSystemAxis[] getDefaultAxis(final CoordinateSystem cs) {
final DefaultCoordinateSystemAxis[] axis = new DefaultCoordinateSystemAxis[cs.getDimension()];
for (int i=0; i<axis.length; i++) {
final CoordinateSystemAxis a = cs.getAxis(i);
DefaultCoordinateSystemAxis c = DefaultCoordinateSystemAxis.getPredefined(a);
if (c == null) {
if (a instanceof DefaultCoordinateSystemAxis) {
c = (DefaultCoordinateSystemAxis) a;
} else {
c = new DefaultCoordinateSystemAxis(a);
}
}
axis[i] = c;
}
return axis;
}
/**
* Returns {@code true} if every axis in the specified {@code userCS} are colinear with axis
* in this coordinate system. The comparaison is insensitive to axis order and units. What
* matter is axis names (because they are fixed by ISO 19111 specification) and directions.
* <p>
* If this method returns {@code true}, then there is good chances that this CS can be used
* together with {@code userCS} as arguments to {@link #swapAndScaleAxis swapAndScaleAxis}.
* <p>
* This method should not be public because current implementation is not fully consistent
* for every pair of CS. It tries to check the opposite direction in addition of the usual
* one, but only a few pre-defined axis declare their opposite. This method should be okay
* when invoked on pre-defined CS declared in this package. {@link PredefinedCS} uses this
* method only that way.
*/
final boolean axisColinearWith(final CoordinateSystem userCS) {
if (userCS.getDimension() != getDimension()) {
return false;
}
final DefaultCoordinateSystemAxis[] axis0 = getDefaultAxis(this);
final DefaultCoordinateSystemAxis[] axis1 = getDefaultAxis(userCS);
next: for (int i=0; i<axis0.length; i++) {
final DefaultCoordinateSystemAxis direct = axis0[i];
final DefaultCoordinateSystemAxis opposite = direct.getOpposite();
for (int j=0; j<axis1.length; j++) {
final DefaultCoordinateSystemAxis candidate = axis1[j];
if (candidate != null) {
if (candidate.equals(direct, false, false) || (opposite != null &&
candidate.equals(opposite, false, false)))
{
axis1[j] = null; // Flags as already compared.
continue next;
}
}
}
return false;
}
assert directionColinearWith(userCS);
return true;
}
/**
* Compares directions only, without consideration for the axis name.
*/
final boolean directionColinearWith(final CoordinateSystem userCS) {
if (userCS.getDimension() != axis.length) {
return false;
}
final AxisDirection[] checks = new AxisDirection[axis.length];
for (int i=0; i<checks.length; i++) {
checks[i] = userCS.getAxis(i).getDirection().absolute();
}
next: for (int i=0; i<axis.length; i++) {
final AxisDirection direction = axis[i].getDirection().absolute();
for (int j=0; j<checks.length; j++) {
final AxisDirection candidate = checks[j];
if (candidate != null && candidate.equals(direction)) {
checks[j] = null; // Flags as already compared.
continue next;
}
}
return false;
}
return true;
}
/**
* Compares the specified object with this coordinate system for equality.
*
* @param object The object to compare to {@code this}.
* @param compareMetadata {@code true} for performing a strict comparaison, or
* {@code false} for comparing only properties relevant to transformations.
* @return {@code true} if both objects are equal.
*/
@Override
public boolean equals(final AbstractIdentifiedObject object, final boolean compareMetadata) {
if (object == this) {
return true; // Slight optimization.
}
if (super.equals(object, compareMetadata)) {
final AbstractCS that = (AbstractCS) object;
return equals(this.axis, that.axis, compareMetadata);
}
return false;
}
/**
* Returns a hash value for this coordinate system.
*
* @return The hash code value. This value doesn't need to be the same
* in past or future versions of this class.
*/
@Override
public int hashCode() {
int code = (int)serialVersionUID;
for (int i=0; i<axis.length; i++) {
code = code*37 + axis[i].hashCode();
}
return code;
}
/**
* Format the inner part of a
* <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
* Known Text</cite> (WKT)</A> element. Note that WKT is not yet defined for coordinate system.
* Current implementation list the axis contained in this CS.
*
* @param formatter The formatter to use.
* @return The WKT element name. Current implementation default to the class name.
*/
@Override
protected String formatWKT(final Formatter formatter) {
for (int i=0; i<axis.length; i++) {
formatter.append(axis[i]);
}
formatter.setInvalidWKT(CoordinateSystem.class);
return super.formatWKT(formatter);
}
}