| /* |
| * 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() { |
| * @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() { |
| * @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() { |
| * @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; |
| } |
| } |