blob: 6996a9273cc41d89a55db10be4faec729a97d93a [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.referencing.cs;
import java.util.Arrays;
import java.util.Objects;
import javax.measure.Unit;
import javax.measure.UnitConverter;
import javax.measure.quantity.Length;
import javax.measure.IncommensurableException;
import org.opengis.referencing.cs.RangeMeaning;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CartesianCS;
import org.opengis.referencing.cs.EllipsoidalCS;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.measure.Angle;
import org.apache.sis.measure.Longitude;
import org.apache.sis.measure.ElevationAngle;
import org.apache.sis.measure.Units;
import org.apache.sis.util.Static;
import org.apache.sis.util.Classes;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.NullArgumentException;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.internal.system.Modules;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.internal.referencing.AxisDirections;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
/**
* Utility methods working on {@link CoordinateSystem} objects and their axes.
* Those methods allow for example to {@linkplain #angle estimate an angle between two axes}
* or {@linkplain #swapAndScaleAxes determining the change of axis directions and units}
* between two coordinate systems.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
* @since 0.4
* @module
*/
public final class CoordinateSystems extends Static {
/**
* Do not allow instantiation of this class.
*/
private CoordinateSystems() {
}
/**
* Returns an axis direction code from the given direction name.
* Names are case-insensitive. They may be:
*
* <ul>
* <li>Cardinal directions like <cite>"north"</cite> and <cite>"east"</cite>.</li>
* <li>Inter-cardinal directions <cite>"north-east"</cite> and <cite>"south-south-east"</cite>,
* using either {@code '-'}, {@code '_'} or spaces as separator between the cardinal points.</li>
* <li>Directions from a pole like <cite>"South along 180 deg"</cite> and <cite>"South along 90° East"</cite>,
* using either the {@code "deg"} or {@code "°"} symbol. Note that the meridian is not necessarily relative
* to Greenwich (see {@link #directionAlongMeridian directionAlongMeridian(…)} for more information).</li>
* </ul>
*
* @param name the direction name (e.g. "north", "north-east", <i>etc.</i>).
* @return the axis direction for the given name.
* @throws IllegalArgumentException if the given name is not a known axis direction.
*/
public static AxisDirection parseAxisDirection(String name) throws IllegalArgumentException {
ArgumentChecks.ensureNonNull("name", name);
name = CharSequences.trimWhitespaces(name);
AxisDirection candidate = AxisDirections.valueOf(name);
if (candidate != null) {
return candidate;
}
/*
* Some EPSG direction names are of the form "South along 180 deg". We check that the
* direction before "along" is valid and create a new axis direction if it is. We can
* not just replace "South along 180 deg" by "South" because the same CRS may use two
* of those directions. For example EPSG:32661 has the following axis direction:
*
* South along 180 deg
* South along 90 deg East
*/
final DirectionAlongMeridian meridian = DirectionAlongMeridian.parse(name);
if (meridian != null) {
candidate = meridian.getDirection();
assert candidate == AxisDirections.valueOf(meridian.toString());
return candidate;
}
throw new IllegalArgumentException(Resources.format(Resources.Keys.UnknownAxisDirection_1, name));
}
/**
* Returns an axis direction from a pole along a meridian.
* The given meridian is usually, but not necessarily, relative to the Greenwich meridian.
*
* <div class="note"><b>Example:</b>
* {@code directionAlongMeridian(AxisDirection.SOUTH, -90)} returns an axis direction for
* <cite>“South along 90°W”</cite>.</div>
*
* <div class="section">Reference meridian</div>
* The reference meridian depends on the context. It is usually the prime meridian of the
* {@linkplain org.apache.sis.referencing.datum.DefaultGeodeticDatum geodetic datum} of the
* {@linkplain org.apache.sis.referencing.crs.DefaultGeographicCRS geographic CRS} instance
* that contains (through its coordinate system) the axes having those directions.
* This policy is consistent with
* <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#40">WKT 2 specification §7.5.4(iv)</a>.
*
* @param baseDirection the base direction, which must be {@link AxisDirection#NORTH} or {@link AxisDirection#SOUTH}.
* @param meridian the meridian in degrees, relative to a unspecified (usually Greenwich) prime meridian.
* Meridians in the East hemisphere are positive and meridians in the West hemisphere are negative.
* @return the axis direction along the given meridian.
*
* @since 0.6
*/
public static AxisDirection directionAlongMeridian(final AxisDirection baseDirection, final double meridian) {
return new DirectionAlongMeridian(baseDirection, meridian).getDirection();
}
/**
* Returns the arithmetic (counterclockwise) angle from the first axis direction to the second direction.
* This method returns a value between -180° and +180°, or {@code null} if no angle can be computed.
*
* <div class="section">Horizontal directions</div>
* For any pair of compass directions which are not opposite directions, a positive angle denotes
* a right-handed system while a negative angle denotes a left-handed system. Examples:
*
* <ul>
* <li>The angle from {@link AxisDirection#EAST EAST} to {@link AxisDirection#NORTH NORTH} is 90°</li>
* <li>The angle from {@link AxisDirection#SOUTH SOUTH} to {@link AxisDirection#WEST WEST} is -90°</li>
* <li>The angle from <cite>"North along 90° East"</cite> to <cite>"North along 0°"</cite> is 90°.</li>
* </ul>
*
* <div class="note"><b>Note:</b>
* in the case of directions like <cite>“South along 90°W”</cite>, the caller is responsible to make sure
* that the meridians are relative to the same prime meridian. This is the case if the axes are part of
* the same {@code CoordinateSystem} instance.</div>
*
* <div class="section">Horizontal and vertical directions</div>
* By convention this method defines the angle from any compass direction to the {@link AxisDirection#UP UP}
* vertical direction as 90°, and the angle of any compass direction to the {@link AxisDirection#DOWN DOWN}
* vertical direction as -90°. The sign of those angles gives no indication about whether the coordinate system
* is right-handed or left-handed. Those angles are returned as instances of {@link ElevationAngle}.
*
* <p>All angles are approximations since this method does not take the Earth ellipsoidal or geoidal shape in
* account.</p>
*
* <div class="section">Invariants</div>
* For any non-null return value:
* <ul>
* <li>{@code angle(A, A) = 0°}</li>
* <li>{@code angle(A, opposite(A)) = ±180°}</li>
* <li>{@code angle(A, B) = -angle(B, A)}</li>
* </ul>
*
* @param source the source axis direction.
* @param target the target axis direction.
* @return the arithmetic angle (in degrees) of the rotation to apply on a line pointing toward
* the source direction in order to make it point toward the target direction, or
* {@code null} if this value can not be computed.
*/
public static Angle angle(final AxisDirection source, final AxisDirection target) {
ArgumentChecks.ensureNonNull("source", source);
ArgumentChecks.ensureNonNull("target", target);
/*
* Check for NORTH, SOUTH, EAST, EAST-NORTH-EAST, etc.
* Checked first because this is the most common case.
*/
int c = AxisDirections.angleForCompass(source, target);
if (c != Integer.MIN_VALUE) {
return new Angle(c * (360.0 / AxisDirections.COMPASS_COUNT));
}
/*
* Check for GEOCENTRIC_X, GEOCENTRIC_Y, GEOCENTRIC_Z.
*/
c = AxisDirections.angleForGeocentric(source, target);
if (c != Integer.MIN_VALUE) {
return new Angle(c * 90);
}
/*
* Check for DISPLAY_UP, DISPLAY_DOWN, etc. assuming a flat screen.
* Note that we do not check for grid directions (COLUMN_POSITIVE,
* ROW_POSITIVE, etc.) because the grid geometry may be anything.
*/
c = AxisDirections.angleForDisplay(source, target);
if (c != Integer.MIN_VALUE) {
return new Angle(c * (360 / AxisDirections.DISPLAY_COUNT));
}
/*
* Check for "South along 90° East", etc. directions. Note that this
* check may perform a relatively costly parsing of axis direction name.
* (NOTE: the check for 'isUserDefined' is performed outside DirectionAlongMeridian for
* avoiding class initialization of the later in the common case where we do not need it).
*/
final DirectionAlongMeridian srcMeridian, tgtMeridian;
srcMeridian = AxisDirections.isUserDefined(source) ? DirectionAlongMeridian.parse(source) : null;
tgtMeridian = AxisDirections.isUserDefined(target) ? DirectionAlongMeridian.parse(target) : null;
if (srcMeridian != null && tgtMeridian != null) {
return new Angle(srcMeridian.angle(tgtMeridian));
}
/*
* Check for UP and DOWN, with special case if one of the direction is horizontal
* (either a compass direction of a direction along a meridian).
*/
final boolean srcVrt = AxisDirections.isVertical(source);
final boolean tgtVrt = AxisDirections.isVertical(target);
if (tgtVrt) {
if (srcVrt) {
return new Angle(source.equals(target) ? 0 : target.equals(AxisDirection.UP) ? 180 : -180);
} else if (AxisDirections.isCompass(source) || srcMeridian != null) {
return target.equals(AxisDirection.UP) ? ElevationAngle.ZENITH : ElevationAngle.NADIR;
}
} else if (srcVrt) {
if (AxisDirections.isCompass(target) || tgtMeridian != null) {
return source.equals(AxisDirection.UP) ? ElevationAngle.NADIR : ElevationAngle.ZENITH;
}
}
return null;
}
/**
* Returns an affine transform between two coordinate systems.
* Only units and axes 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 by this method.
*
* <div class="section">Conditions</div>
* The two coordinate systems must implement the same GeoAPI coordinate system interface.
* For example if {@code sourceCS} is a {@link org.opengis.referencing.cs.CartesianCS},
* then {@code targetCS} must be a {@code CartesianCS} too.
*
* <div class="note"><b>Example:</b>
* If coordinates in {@code sourceCS} are (<var>x</var>,<var>y</var>) tuples in metres
* and coordinates in {@code targetCS} are (<var>-y</var>,<var>x</var>) tuples in centimetres,
* then the transformation can be performed as below:
*
* {@preformat math
* ┌ ┐ ┌ ┐ ┌ ┐
* │-y(cm)│ │ 0 -100 0 │ │ x(m)│
* │ x(cm)│ = │ 100 0 0 │ │ y(m)│
* │ 1 │ │ 0 0 1 │ │ 1 │
* └ ┘ └ ┘ └ ┘
* }
* </div>
*
* @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 the CS are not of the same type, or axes do not match.
* @throws IncommensurableException if the units are not compatible, or the conversion is non-linear.
*
* @see Matrices#createTransform(AxisDirection[], AxisDirection[])
*/
@SuppressWarnings("fallthrough")
public static Matrix swapAndScaleAxes(final CoordinateSystem sourceCS,
final CoordinateSystem targetCS)
throws IllegalArgumentException, IncommensurableException
{
ArgumentChecks.ensureNonNull("sourceCS", sourceCS);
ArgumentChecks.ensureNonNull("targetCS", targetCS);
if (!Classes.implementSameInterfaces(sourceCS.getClass(), targetCS.getClass(), CoordinateSystem.class)) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.IncompatibleCoordinateSystemTypes));
}
final AxisDirection[] srcAxes = getAxisDirections(sourceCS);
final AxisDirection[] dstAxes = getAxisDirections(targetCS);
final MatrixSIS matrix = Matrices.createTransform(srcAxes, dstAxes);
assert Arrays.equals(srcAxes, dstAxes) == matrix.isIdentity() : matrix;
/*
* The previous code computed a matrix for swapping axes. 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 axes 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; // == sourceCS.getDimension()
final int targetDim = matrix.getNumRow() - 1; // == targetCS.getDimension()
for (int j=0; j<targetDim; j++) {
final Unit<?> targetUnit = targetCS.getAxis(j).getUnit();
for (int i=0; i<sourceDim; i++) {
if (matrix.getElement(j,i) == 0) {
// There is no dependency between source[i] and target[j]
// (i.e. axes are orthogonal).
continue;
}
final Unit<?> sourceUnit = sourceCS.getAxis(i).getUnit();
if (Objects.equals(sourceUnit, targetUnit)) {
// There is no units conversion to apply
// between source[i] and target[j].
continue;
}
Number scale = 1;
Number offset = 0;
final Number[] coefficients = Units.coefficients(sourceUnit.getConverterToAny(targetUnit));
switch (coefficients != null ? coefficients.length : -1) {
case 2: scale = coefficients[1]; // Fall through
case 1: offset = coefficients[0]; // Fall through
case 0: break;
default: throw new IncommensurableException(Resources.format(
Resources.Keys.NonLinearUnitConversion_2, sourceUnit, targetUnit));
}
final DoubleDouble element = DoubleDouble.castOrCopy(matrix.getNumber(j,i));
final DoubleDouble r = new DoubleDouble(element);
r.multiplyGuessError(scale);
matrix.setNumber(j, i, r);
r.setFrom(element);
r.multiplyGuessError(offset);
r.addGuessError(matrix.getNumber(j, sourceDim));
matrix.setNumber(j, sourceDim, r);
}
}
return matrix;
}
/**
* Returns a coordinate system derived from the given one but with a modified list of axes.
* The axes may be filtered (excluding some axes), reordered or have their unit and direction modified.
*
* <div class="note"><b>Example:</b>
* for replacing all angular units of a coordinate system to degrees (regardless what the original
* angular units were) while leaving other kinds of units unchanged, one can write:
*
* {@preformat java
* CoordinateSystem cs = ...;
* cs = CoordinateSystems.replaceAxes(cs, new AxisFilter() {
* &#64;Override
* public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
* if (Units.isAngular(unit)) {
* unit = Units.DEGREE;
* }
* return unit;
* }
* });
* }</div>
*
* <div class="section">Coordinate system normalization</div>
* This method is often used together with {@link #swapAndScaleAxes swapAndScaleAxes(…)} for normalizing the
* coordinate values given to a {@linkplain org.apache.sis.referencing.operation.transform.AbstractMathTransform
* math transform}.
*
* <div class="note"><b>Example:</b>
* {@preformat java
* CoordinateSystem sourceCS = ...;
* CoordinateSystem targetCS = ...;
* Matrix step1 = swapAndScaleAxes(sourceCS, replaceAxes(sourceCS, AxisConvention.NORMALIZED));
* Matrix step2 = ...; // some transform working on coordinates with standard axis order and unit.
* Matrix step3 = swapAndScaleAxes(replaceAxes(targetCS, AxisConvention.NORMALIZED), targetCS);
* }</div>
*
* A rational for normalized axis order and units is explained in the <cite>Axis units and direction</cite> section
* in the description of the {@linkplain org.apache.sis.referencing.operation.projection map projection package}.
*
* @param cs the coordinate system, or {@code null}.
* @param filter the modifications to apply on coordinate system axes.
* @return the modified coordinate system as a new instance,
* or {@code cs} if the given coordinate system was null or does not need any change.
* @throws IllegalArgumentException if the specified coordinate system can not be filtered.
* It may be because the coordinate system would contain an illegal number of axes,
* or because an axis would have an unexpected direction or unexpected unit of measurement.
*
* @see AxesConvention#NORMALIZED
*
* @since 0.6
*/
public static CoordinateSystem replaceAxes(final CoordinateSystem cs, final AxisFilter filter) {
ArgumentChecks.ensureNonNull("filter", filter);
if (cs != null) {
final CoordinateSystem newCS;
if (filter instanceof AxesConvention) {
if (cs instanceof AbstractCS) {
// User may have overridden the 'forConvention' method.
return ((AbstractCS) cs).forConvention((AxesConvention) filter);
} else {
newCS = Normalizer.forConvention(cs, (AxesConvention) filter);
}
} else {
newCS = Normalizer.normalize(cs, filter, false);
}
if (newCS != null) {
return newCS;
}
}
return cs;
}
/**
* Returns a coordinate system derived from the given one but with all linear units replaced by the given unit.
* Non-linear units (e.g. angular or scale units) are left unchanged.
*
* <p>This convenience method is equivalent to the following code:</p>
* {@preformat java
* return CoordinateSystems.replaceAxes(cs, new AxisFilter() {
* &#64;Override public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
* return Units.isLinear(unit) ? newUnit : unit;
* }
* });
* }
*
* @param cs the coordinate system in which to replace linear units, or {@code null}.
* @param newUnit the new linear unit.
* @return the modified coordinate system as a new instance, or {@code null} if the given {@code cs} was null,
* or {@code cs} if all linear units were already equal to the given one.
*
* @see Units#isLinear(Unit)
*
* @since 0.7
*/
public static CoordinateSystem replaceLinearUnit(final CoordinateSystem cs, final Unit<Length> newUnit) {
ArgumentChecks.ensureNonNull("newUnit", newUnit);
return CoordinateSystems.replaceAxes(cs, new AxisFilter() {
@Override public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
return Units.isLinear(unit) ? newUnit : unit;
}
});
}
/**
* Returns a coordinate system derived from the given one but with all angular units replaced by the given unit.
* Non-angular units (e.g. linear or scale units) are left unchanged.
*
* <p>This convenience method is equivalent to the following code:</p>
* {@preformat java
* return CoordinateSystems.replaceAxes(cs, new AxisFilter() {
* &#64;Override public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
* return Units.isAngular(unit) ? newUnit : unit;
* }
* });
* }
*
* @param cs the coordinate system in which to replace angular units, or {@code null}.
* @param newUnit the new angular unit.
* @return the modified coordinate system as a new instance, or {@code null} if the given {@code cs} was null,
* or {@code cs} if all angular units were already equal to the given one.
*
* @see Units#isAngular(Unit)
*
* @since 0.7
*/
public static CoordinateSystem replaceAngularUnit(final CoordinateSystem cs, final Unit<javax.measure.quantity.Angle> newUnit) {
ArgumentChecks.ensureNonNull("newUnit", newUnit);
return CoordinateSystems.replaceAxes(cs, new AxisFilter() {
@Override public Unit<?> getUnitReplacement(CoordinateSystemAxis axis, Unit<?> unit) {
return Units.isAngular(unit) ? newUnit : unit;
}
});
}
/**
* Returns the axis directions for the specified coordinate system.
* This method guarantees that the returned array is non-null and does not contain any null direction.
*
* @param cs the coordinate system.
* @return the axis directions for the specified coordinate system.
* @throws NullArgumentException if {@code cs} is null, or one of its axes is null,
* or a value returned by {@link CoordinateSystemAxis#getDirection()} is null.
*
* @since 0.8
*/
public static AxisDirection[] getAxisDirections(final CoordinateSystem cs) {
ArgumentChecks.ensureNonNull("cs", cs);
final AxisDirection[] directions = new AxisDirection[cs.getDimension()];
for (int i=0; i<directions.length; i++) {
final CoordinateSystemAxis axis = cs.getAxis(i);
ArgumentChecks.ensureNonNullElement("cs", i, cs);
ArgumentChecks.ensureNonNullElement("cs[#].direction", i, directions[i] = axis.getDirection());
}
return directions;
}
/**
* Returns the EPSG code of a coordinate system using the units and directions of given axes.
* This method ignores axis metadata (names, abbreviation, identifiers, remarks, <i>etc.</i>).
* The axis minimum and maximum values are checked only if the
* {@linkplain CoordinateSystemAxis#getRangeMeaning() range meaning} is "wraparound".
* If no suitable coordinate system is known to Apache SIS, then this method returns {@code null}.
*
* <p>Current implementation uses a hard-coded list of known coordinate systems;
* it does not yet scan the EPSG database (this may change in future Apache SIS version).
* The current list of known coordinate systems is given below.</p>
*
* <table>
* <caption>Known coordinate systems (CS)</caption>
* <tr><th>EPSG</th> <th>CS type</th> <th colspan="3">Axis directions</th> <th>Horizontal unit</th></tr>
* <tr><td>6424</td> <td>Ellipsoidal</td> <td>east</td> <td>north</td> <td></td> <td>degree</td></tr>
* <tr><td>6422</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td> <td></td> <td>degree</td></tr>
* <tr><td>6425</td> <td>Ellipsoidal</td> <td>east</td> <td>north</td> <td></td> <td>grads</td></tr>
* <tr><td>6403</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td> <td></td> <td>grads</td></tr>
* <tr><td>6429</td> <td>Ellipsoidal</td> <td>east</td> <td>north</td> <td></td> <td>radian</td></tr>
* <tr><td>6428</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td> <td></td> <td>radian</td></tr>
* <tr><td>6426</td> <td>Ellipsoidal</td> <td>east</td> <td>north</td> <td>up</td> <td>degree</td></tr>
* <tr><td>6423</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td> <td>up</td> <td>degree</td></tr>
* <tr><td>6427</td> <td>Ellipsoidal</td> <td>east</td> <td>north</td> <td>up</td> <td>grads</td></tr>
* <tr><td>6421</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td> <td>up</td> <td>grads</td></tr>
* <tr><td>6431</td> <td>Ellipsoidal</td> <td>east</td> <td>north</td> <td>up</td> <td>radian</td></tr>
* <tr><td>6430</td> <td>Ellipsoidal</td> <td>north</td> <td>east</td> <td>up</td> <td>radian</td></tr>
* <tr><td>4400</td> <td>Cartesian</td> <td>east</td> <td>north</td> <td></td> <td>metre</td></tr>
* <tr><td>4500</td> <td>Cartesian</td> <td>north</td> <td>east</td> <td></td> <td>metre</td></tr>
* <tr><td>4491</td> <td>Cartesian</td> <td>west</td> <td>north</td> <td></td> <td>metre</td></tr>
* <tr><td>4501</td> <td>Cartesian</td> <td>north</td> <td>west</td> <td></td> <td>metre</td></tr>
* <tr><td>6503</td> <td>Cartesian</td> <td>west</td> <td>south</td> <td></td> <td>metre</td></tr>
* <tr><td>6501</td> <td>Cartesian</td> <td>south</td> <td>west</td> <td></td> <td>metre</td></tr>
* <tr><td>1039</td> <td>Cartesian</td> <td>east</td> <td>north</td> <td></td> <td>foot</td></tr>
* <tr><td>1029</td> <td>Cartesian</td> <td>north</td> <td>east</td> <td></td> <td>foot</td></tr>
* <tr><td>4403</td> <td>Cartesian</td> <td>east</td> <td>north</td> <td></td> <td>Clarke’s foot</td></tr>
* <tr><td>4502</td> <td>Cartesian</td> <td>north</td> <td>east</td> <td></td> <td>Clarke’s foot</td></tr>
* <tr><td>4497</td> <td>Cartesian</td> <td>east</td> <td>north</td> <td></td> <td>US survey foot</td></tr>
* </table>
*
* @param type the type of coordinate system for which an EPSG code is desired, as a GeoAPI interface.
* @param axes axes for which a coordinate system EPSG code is desired.
* @return EPSG codes for a coordinate system using the given axes (ignoring metadata), or {@code null} if unknown
* to this method. Note that a null value does not mean that a more extensive search in the EPSG database
* would not find a matching coordinate system.
*
* @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createCoordinateSystem(String)
*
* @since 1.0
*/
@SuppressWarnings("fallthrough")
public static Integer getEpsgCode(final Class<? extends CoordinateSystem> type, final CoordinateSystemAxis... axes) {
ArgumentChecks.ensureNonNull("type", type);
ArgumentChecks.ensureNonNull("axes", axes);
forDim: switch (axes.length) {
case 3: {
if (!Units.METRE.equals(axes[2].getUnit())) break; // Restriction in our hard-coded list of codes.
// Fall through
}
case 2: {
final Unit<?> unit = axes[0].getUnit();
if (unit != null && unit.equals(axes[1].getUnit())) {
final boolean isAngular = Units.isAngular(unit);
if ((isAngular && type.isAssignableFrom(EllipsoidalCS.class)) ||
Units.isLinear(unit) && type.isAssignableFrom(CartesianCS.class))
{
/*
* Current implementation defines EPSG codes for EllipsoidalCS and CartesianCS only.
* Those two coordinate system types can be differentiated by the unit of the two first axes.
* If a future implementation supports more CS types, above condition will need to be updated.
*/
final AxisDirection[] directions = new AxisDirection[axes.length];
for (int i=0; i<directions.length; i++) {
final CoordinateSystemAxis axis = axes[i];
ArgumentChecks.ensureNonNullElement("axes", i, axis);
directions[i] = axis.getDirection();
if (isAngular && RangeMeaning.WRAPAROUND.equals(axis.getRangeMeaning())) try {
final UnitConverter uc = unit.getConverterToAny(Units.DEGREE);
final double min = uc.convert(axis.getMinimumValue());
final double max = uc.convert(axis.getMaximumValue());
if ((min > Double.NEGATIVE_INFINITY && Math.abs(min - Longitude.MIN_VALUE) > Formulas.ANGULAR_TOLERANCE) ||
(max < Double.POSITIVE_INFINITY && Math.abs(max - Longitude.MAX_VALUE) > Formulas.ANGULAR_TOLERANCE))
{
break forDim;
}
} catch (IncommensurableException e) { // Should never happen since we checked that units are angular.
Logging.unexpectedException(Logging.getLogger(Modules.REFERENCING), CoordinateSystems.class, "getEpsgCode", e);
break forDim;
}
}
return getEpsgCode(unit, directions);
}
}
}
}
return null;
}
/**
* Returns the EPSG code of a coordinate system using the given unit and axis directions.
* This convenience method performs the work documented in {@link #getEpsgCode(Class, CoordinateSystemAxis...)},
* but requiring only a frequently used subset of information.
* If no suitable coordinate system is known to Apache SIS, then this method returns {@code null}.
*
* <p>Current implementation uses a hard-coded list of known coordinate systems;
* it does not yet scan the EPSG database (this may change in future Apache SIS version).
* The current list of known coordinate systems is documented {@linkplain #getEpsgCode(Class,
* CoordinateSystemAxis...) above}.</p>
*
* @param unit desired unit of measurement. For three-dimensional ellipsoidal coordinate system,
* this is the unit for the horizontal axes only; the vertical axis is in metres.
* @param directions desired axis directions.
* @return EPSG codes for a coordinate system using the given axis directions and unit of measurement,
* or {@code null} if unknown to this method. Note that a null value does not mean that a more
* extensive search in the EPSG database would not find a matching coordinate system.
*
* @see Units#getEpsgCode(Unit, boolean)
* @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createCoordinateSystem(String)
*
* @since 0.8
*/
public static Integer getEpsgCode(final Unit<?> unit, final AxisDirection... directions) {
ArgumentChecks.ensureNonNull("unit", unit);
ArgumentChecks.ensureNonNull("directions", directions);
final int code = Codes.lookup(unit, directions);
return (code != 0) ? code : null;
}
}