sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Skeleton of GeodeticCalculator, not yet functional. The idea is taken from Geotk, but all formulas have been discarted and methods are rewritten. The initial version will use only spherical formulas published on Wikipedia. The formulas to use for the ellipsoidal versions will be determined later. More discussion on https://issues.apache.org/jira/browse/SIS-386
Date Fri, 10 May 2019 19:18:44 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new f8161e1  Skeleton of GeodeticCalculator, not yet functional. The idea is taken from
Geotk, but all formulas have been discarted and methods are rewritten. The initial version
will use only spherical formulas published on Wikipedia. The formulas to use for the ellipsoidal
versions will be determined later. More discussion on https://issues.apache.org/jira/browse/SIS-386
f8161e1 is described below

commit f8161e1ec4b31c489856671815f02650849572e9
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri May 10 21:14:51 2019 +0200

    Skeleton of GeodeticCalculator, not yet functional. The idea is taken from Geotk, but
all formulas have been discarted and methods are rewritten.
    The initial version will use only spherical formulas published on Wikipedia. The formulas
to use for the ellipsoidal versions will be determined later.
    More discussion on https://issues.apache.org/jira/browse/SIS-386
---
 .../org/apache/sis/console/TransformCommand.java   |   2 +-
 .../org/apache/sis/openoffice/Transformer.java     |   2 +-
 .../sis/referencing/gazetteer/LocationFormat.java  |   2 +-
 .../apache/sis/internal/referencing/Formulas.java  |  43 +++-
 .../internal/referencing/PositionTransformer.java  | 230 ++++++++++++++++++
 .../internal/referencing/ReferencingUtilities.java |  69 +++++-
 .../internal/referencing/ServicesForMetadata.java  |   4 +-
 .../main/java/org/apache/sis/referencing/CRS.java  |   4 +-
 .../apache/sis/referencing/GeodeticCalculator.java | 268 +++++++++++++++++++++
 .../factory/GeodeticAuthorityFactory.java          |   8 +-
 .../referencing/ReferencingUtilitiesTest.java      |   6 +-
 ide-project/NetBeans/nbproject/genfiles.properties |   2 +-
 ide-project/NetBeans/nbproject/project.xml         |   1 +
 13 files changed, 603 insertions(+), 38 deletions(-)

diff --git a/application/sis-console/src/main/java/org/apache/sis/console/TransformCommand.java
b/application/sis-console/src/main/java/org/apache/sis/console/TransformCommand.java
index 90f03cd..ad036bd 100644
--- a/application/sis-console/src/main/java/org/apache/sis/console/TransformCommand.java
+++ b/application/sis-console/src/main/java/org/apache/sis/console/TransformCommand.java
@@ -214,7 +214,7 @@ final class TransformCommand extends FormattedOutputCommand {
                 }
             }
             try {
-                final GeographicCRS domainOfValidityCRS = ReferencingUtilities.toNormalizedGeographicCRS(sourceCRS);
+                final GeographicCRS domainOfValidityCRS = ReferencingUtilities.toNormalizedGeographicCRS(sourceCRS,
false, false);
                 if (domainOfValidityCRS != null) {
                     toDomainOfValidity = CRS.findOperation(sourceCRS, domainOfValidityCRS,
null).getMathTransform();
                     areaOfInterest = computeAreaOfInterest(points);
diff --git a/application/sis-openoffice/src/main/java/org/apache/sis/openoffice/Transformer.java
b/application/sis-openoffice/src/main/java/org/apache/sis/openoffice/Transformer.java
index 7df0065..e4e087c 100644
--- a/application/sis-openoffice/src/main/java/org/apache/sis/openoffice/Transformer.java
+++ b/application/sis-openoffice/src/main/java/org/apache/sis/openoffice/Transformer.java
@@ -72,7 +72,7 @@ final class Transformer {
         /*
          * Computes the area of interest.
          */
-        final GeographicCRS domainCRS = ReferencingUtilities.toNormalizedGeographicCRS(sourceCRS);
+        final GeographicCRS domainCRS = ReferencingUtilities.toNormalizedGeographicCRS(sourceCRS,
false, false);
         if (domainCRS != null) {
             final MathTransform toDomainOfValidity = CRS.findOperation(sourceCRS, domainCRS,
null).getMathTransform();
             final int dimension = toDomainOfValidity.getSourceDimensions();
diff --git a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/LocationFormat.java
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/LocationFormat.java
index e97a479..6febadd 100644
--- a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/LocationFormat.java
+++ b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/LocationFormat.java
@@ -278,7 +278,7 @@ public class LocationFormat extends TabularFormat<Location> {
                     }
                 }
                 if (bbox != null) {     // Compute geographic position only if there is a
geographic bounding box.
-                    GeographicCRS geogCRS = ReferencingUtilities.toNormalizedGeographicCRS(posCRS);
+                    GeographicCRS geogCRS = ReferencingUtilities.toNormalizedGeographicCRS(posCRS,
false, false);
                     if (geogCRS != null) {
                         geopos = transform(position, posCRS, geogCRS);
                     }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
index ff4e8f6..95f8fb0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/Formulas.java
@@ -20,6 +20,7 @@ import org.apache.sis.util.Static;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.internal.util.Numerics;
 import org.opengis.referencing.datum.Ellipsoid;
+import org.apache.sis.referencing.datum.DefaultEllipsoid;
 
 import static java.lang.Math.*;
 import static org.apache.sis.math.MathFunctions.atanh;
@@ -97,19 +98,6 @@ public final class Formulas extends Static {
     }
 
     /**
-     * Returns the size of a planet described by the given ellipsoid compared to earth.
-     * This method returns a ratio of given planet authalic radius compared to WGS84.
-     * This can be used for adjusting {@link #LINEAR_TOLERANCE} and {@link #ANGULAR_TOLERANCE}
to another planet.
-     *
-     * @param  planet  ellipsoid of the other planet to compare to Earth.
-     * @return ratio of planet authalic radius on WGS84 authalic radius.
-     */
-    public static double scaleComparedToEarth(final Ellipsoid planet) {
-        return getAuthalicRadius(planet.getSemiMajorAxis(),
-                                 planet.getSemiMinorAxis()) / 6371007.180918474;
-    }
-
-    /**
      * Returns 3ⁿ for very small (less than 10) positive values of <var>n</var>.
      * Note that this method overflow for any value equals or greater than 20.
      *
@@ -142,6 +130,35 @@ public final class Formulas extends Static {
     }
 
     /**
+     * Returns the size of a planet described by the given ellipsoid compared to earth.
+     * This method returns a ratio of given planet authalic radius compared to WGS84.
+     * This can be used for adjusting {@link #LINEAR_TOLERANCE} and {@link #ANGULAR_TOLERANCE}
to another planet.
+     *
+     * @param  planet  ellipsoid of the other planet to compare to Earth, or {@code null}.
+     * @return ratio of planet authalic radius on WGS84 authalic radius, or {@code NaN} if
the given ellipsoid is null.
+     */
+    public static double scaleComparedToEarth(final Ellipsoid planet) {
+        return getAuthalicRadius(planet) / 6371007.180918474;
+    }
+
+    /**
+     * Returns the radius of a hypothetical sphere having the same surface than the given
ellipsoid.
+     *
+     * @param  ellipsoid  the ellipsoid for which to get the radius, or {@code null}.
+     * @return the authalic radius, or {@link Double#NaN} if the given ellipsoid is null.
+     */
+    public static double getAuthalicRadius(final Ellipsoid ellipsoid) {
+        if (ellipsoid == null) {
+            return Double.NaN;
+        } else if (ellipsoid instanceof DefaultEllipsoid) {
+            return ((DefaultEllipsoid) ellipsoid).getAuthalicRadius();      // Give a chance
to subclasses to override.
+        } else {
+            return getAuthalicRadius(ellipsoid.getSemiMajorAxis(),
+                                     ellipsoid.getSemiMinorAxis());
+        }
+    }
+
+    /**
      * Returns the radius of a hypothetical sphere having the same surface than the ellipsoid
      * specified by the given axis length.
      *
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
new file mode 100644
index 0000000..04cbbce
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/PositionTransformer.java
@@ -0,0 +1,230 @@
+/*
+ * 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.internal.referencing;
+
+import org.opengis.util.FactoryException;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.geometry.MismatchedDimensionException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.referencing.operation.CoordinateOperation;
+import org.opengis.referencing.operation.CoordinateOperationFactory;
+import org.opengis.referencing.operation.MathTransform;
+import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.referencing.operation.transform.MathTransforms;
+import org.apache.sis.internal.system.DefaultFactories;
+import org.apache.sis.geometry.GeneralDirectPosition;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.Utilities;
+
+
+/**
+ * A direct position capable to {@linkplain #transform transform} another position from its
arbitrary CRS to
+ * {@linkplain #getCoordinateReferenceSystem() the CRS of this position}. This class caches
the last transform
+ * used in order to improve performance when {@linkplain CoordinateOperation#getSourceCRS()
source}
+ * and {@linkplain CoordinateOperation#getTargetCRS() target} CRS do not change often.
+ * Using this class is faster than invoking <code>{@linkplain CoordinateOperationFactory#createOperation
+ * CoordinateOperationFactory.createOperation}(lastCRS, targetCRS)</code> for every
points.
+ *
+ * <ul class="verbose">
+ *   <li><b>Note 1:</b>
+ *   This class is advantageous on a performance point of view only if the same instance
of
+ *   {@code PositionTransformer} is used for transforming many points between arbitrary CRS
+ *   and this {@linkplain #getCoordinateReferenceSystem() position CRS}.</li>
+ *
+ *   <li><b>Note 2:</b>
+ *   This convenience class is useful when the source and target CRS are <em>not likely</em>
to change often.
+ *   If you are sure that the source and target CRS will not change at all for a given set
of positions,
+ *   then using {@link CoordinateOperation} directly gives better performances. This is because
this class
+ *   checks if the CRS changed before every transformations, which may be costly.</li>
+ * </ul>
+ *
+ * This class should not appear in a public API. It is used as a helper private field in
more complex classes.
+ * For example suppose that {@code MyClass} needs to perform its internal working in some
particular CRS,
+ * but we want robust API accepting whatever CRS the client uses. {@code MyClass} can be
written as below:
+ *
+ * {@preformat java
+ *     public class MyClass {
+ *         private static final CoordinateReferenceSystem PUBLIC_CRS = ...
+ *         private static final CoordinateReferenceSystem INTERNAL_CRS = ...
+ *
+ *         private final PositionTransformer myPosition =
+ *                 new PositionTransformer(PUBLIC_CRS, INTERNAL_CRS, null);
+ *
+ *         public void setPosition(DirectPosition position) throws TransformException {
+ *             // The position CRS is usually PUBLIC_CRS, but code below will work even if
it is not.
+ *             myPosition.transform(position);
+ *         }
+ *
+ *         public DirectPosition getPosition() throws TransformException {
+ *             return myPosition.inverseTransform(PUBLIC_CRS);
+ *         }
+ *     }
+ * }
+ *
+ * This class is not thread-safe.
+ *
+ * @author  Martin Desruisseaux (IRD, Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+@SuppressWarnings({"serial", "CloneableClassWithoutClone"})         // Not intended to be
serialized and nothing to clone.
+public final class PositionTransformer extends GeneralDirectPosition {
+    /**
+     * The factory to use for creating new coordinate operation.
+     */
+    private final CoordinateOperationFactory factory;
+
+    /**
+     * The default CRS to assume when {@link #transform(DirectPosition)} has been invoked
without associated CRS.
+     * This is often the same than the {@linkplain #getCoordinateReferenceSystem() target
CRS}, but not necessarily.
+     */
+    public final CoordinateReferenceSystem defaultCRS;
+
+    /**
+     * The last CRS of a position given to {@link #transform(DirectPosition)}, or {@code
null}.
+     * This is used as the source CRS of the coordinate operation. The {@code targetCRS}
will
+     * be the {@linkplain #getCoordinateReferenceSystem() CRS associated with this position}.
+     *
+     * @see #setSourceCRS(CoordinateReferenceSystem)
+     */
+    private transient CoordinateReferenceSystem lastCRS;
+
+    /**
+     * The forward and inverse transforms. Will be created only when first needed.
+     * Those fields are left to {@code null} value if the transform is identity.
+     *
+     * @see #setSourceCRS(CoordinateReferenceSystem)
+     */
+    private transient MathTransform forward, inverse;
+
+    /**
+     * Creates a new position which will contain the result of coordinate transformations
to the given CRS.
+     * The {@linkplain #getCoordinateReferenceSystem() CRS associated with this position}
will be initially
+     * set to {@code targetCRS}.
+     *
+     * @param  defaultCRS  the CRS to take as the source when <code>{@link #transform
transform}(position)</code>
+     *         is invoked with a position without associated CRS. If {@code null}, default
to {@code targetCRS}.
+     * @param  targetCRS  the {@linkplain #getCoordinateReferenceSystem() CRS associated
with this position}. Will be the target
+     *         of {@linkplain #transform coordinate transformations} until the next call
to {@link #setCoordinateReferenceSystem
+     *         setCoordinateReferenceSystem(…)} or {@link #setLocation(DirectPosition)
setLocation}. Can not be null.
+     * @param  factory  the factory to use for creating coordinate operations, or {@code
null} for the default.
+     */
+    public PositionTransformer(final CoordinateReferenceSystem defaultCRS, final CoordinateReferenceSystem
targetCRS,
+            final CoordinateOperationFactory factory)
+    {
+        super(targetCRS);
+        ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
+        this.defaultCRS = (defaultCRS != null) ? defaultCRS : targetCRS;
+        this.lastCRS    = defaultCRS;
+        this.factory    = (factory != null) ? factory : DefaultFactories.forBuildin(CoordinateOperationFactory.class);
+    }
+
+    /**
+     * Sets the coordinate reference system in which the coordinate is given.
+     * The given CRS will be used as:
+     *
+     * <ul>
+     *   <li>the {@linkplain CoordinateOperation#getTargetCRS() target CRS} for every
call to {@link #transform(DirectPosition)},</li>
+     *   <li>the {@linkplain CoordinateOperation#getSourceCRS() source CRS} for every
call to {@link #inverseTransform()}.</li>
+     * </ul>
+     *
+     * @param  targetCRS  the new CRS for this direct position.
+     * @throws MismatchedDimensionException if the specified CRS does not have the expected
number of dimensions.
+     */
+    @Override
+    public void setCoordinateReferenceSystem(final CoordinateReferenceSystem targetCRS) throws
MismatchedDimensionException {
+        ArgumentChecks.ensureNonNull("targetCRS", targetCRS);
+        super.setCoordinateReferenceSystem(targetCRS);
+        forward = null;
+        inverse = null;
+    }
+
+    /**
+     * Sets the {@link #lastCRS} field and creates the associated {@link #forward} transform.
+     * This method does not create yet the {@link #inverse} transform, since it may not be
needed.
+     */
+    private void setSourceCRS(final CoordinateReferenceSystem crs) throws TransformException
{
+        final CoordinateReferenceSystem targetCRS = getCoordinateReferenceSystem();
+        final CoordinateOperation operation;
+        try {
+            operation = factory.createOperation(crs, targetCRS);
+        } catch (FactoryException exception) {
+            throw new TransformException(exception.getLocalizedMessage(), exception);
+        }
+        /*
+         * Note: `lastCRS` should be set last, when we are sure that all other fields
+         * are set to their correct values. This is in order to keep this instance in
+         * a consistent state in case an exception is thrown.
+         */
+        forward = operation.getMathTransform();
+        inverse = null;
+        lastCRS = crs;
+        if (forward.isIdentity()) {
+            forward = null;
+        }
+    }
+
+    /**
+     * Transforms a given position from its CRS to the CRS of this {@code PositionTransformer}.
+     * If the CRS associated to the given position is {@code null}, then that CRS is assumed
to
+     * be the default CRS specified at construction time. Otherwise if that CRS is not equal
to
+     * the {@linkplain #getCoordinateReferenceSystem() CRS associated with this position},
then
+     * a coordinates transformations are applied. The result may be stored in this instance.
+     *
+     * @param  position  a position using an arbitrary CRS, or {@code null}. This object
will not be modified.
+     * @return the transformed position, either {@code this} or the given position (which
may be {@code null}).
+     * @throws TransformException if a coordinate transformation was required and failed.
+     */
+    public DirectPosition transform(final DirectPosition position) throws TransformException
{
+        if (position != null) {
+            CoordinateReferenceSystem userCRS = position.getCoordinateReferenceSystem();
+            if (userCRS == null) {
+                userCRS = defaultCRS;
+            }
+            /*
+             * A projection may be required. Check if it is the same one than the one used
last time this method
+             * has been invoked. If the specified position uses a new CRS, then get the transformation
and save
+             * it in case the next call to this method would use again the same transformation.
+             */
+            if (!Utilities.equalsIgnoreMetadata(lastCRS, userCRS)) {
+                setSourceCRS(userCRS);
+            }
+            if (forward != null) {
+                return forward.transform(position, this);
+            }
+        }
+        return position;
+    }
+
+    /**
+     * Returns a new point with the same coordinates than this one, but transformed to the
default CRS.
+     * This method never returns {@code this}, so the returned point does not need to be
cloned.
+     *
+     * @return the same position as {@code this}, but transformed to the default CRS.
+     * @throws TransformException if a coordinate transformation was required and failed.
+     */
+    public DirectPosition inverseTransform() throws TransformException {
+        if (!Utilities.equalsIgnoreMetadata(lastCRS, defaultCRS)) {
+            setSourceCRS(defaultCRS);
+        }
+        if (inverse == null) {
+            inverse = (forward != null) ? forward.inverse() : MathTransforms.identity(getDimension());
+        }
+        return inverse.transform(this, null);
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
index b66cec7..1d47591 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ReferencingUtilities.java
@@ -46,9 +46,11 @@ import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.IdentifiedObjects;
+import org.apache.sis.referencing.AbstractIdentifiedObject;
 import org.apache.sis.referencing.datum.DefaultPrimeMeridian;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
+import org.apache.sis.referencing.cs.DefaultEllipsoidalCS;
 import org.apache.sis.referencing.operation.transform.DefaultMathTransformFactory.Context;
 
 import static java.util.Collections.singletonMap;
@@ -147,6 +149,23 @@ public final class ReferencingUtilities extends Static {
     }
 
     /**
+     * Returns the GeoAPI interface implemented by the given object, or the implementation
class
+     * if the interface is unknown.
+     *
+     * @param  object  the object for which to get the GeoAPI interface, or {@code null}.
+     * @return GeoAPI interface or implementation class of the given object, or {@code null}
if the given object is null.
+     */
+    public static Class<?> getInterface(final IdentifiedObject object) {
+        if (object == null) {
+            return null;
+        } else if (object instanceof AbstractIdentifiedObject) {
+             return ((AbstractIdentifiedObject) object).getInterface();
+        } else {
+             return object.getClass();
+        }
+    }
+
+    /**
      * Copies all {@link SingleCRS} components from the given source to the given collection.
      * For each {@link CompoundCRS} element found in the iteration, this method replaces
the
      * {@code CompoundCRS} by its {@linkplain CompoundCRS#getComponents() components}, which
@@ -203,6 +222,17 @@ public final class ReferencingUtilities extends Static {
 
     /**
      * Returns the ellipsoid used by the given coordinate reference system, or {@code null}
if none.
+     * More specifically:
+     *
+     * <ul>
+     *   <li>If the given CRS is an instance of {@link SingleCRS} and its datum is
a {@link GeodeticDatum},
+     *       then this method returns the datum ellipsoid.</li>
+     *   <li>Otherwise if the given CRS is an instance of {@link CompoundCRS}, then
this method
+     *       invokes itself recursively for each component until a geodetic datum is found.</li>
+     *   <li>Otherwise this method returns {@code null}.</li>
+     * </ul>
+     *
+     * Note that this method does not check if there is more than one ellipsoid (it should
never be the case).
      *
      * @param  crs  the coordinate reference system for which to get the ellipsoid.
      * @return the ellipsoid, or {@code null} if none.
@@ -262,16 +292,18 @@ public final class ReferencingUtilities extends Static {
     }
 
     /**
-     * Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>)
axis order in decimal degrees.
+     * Derives a geographic CRS with (<var>longitude</var>, <var>latitude</var>)
axis in the specified order and in decimal degrees.
      * If no such CRS can be obtained or created, returns {@code null}.
      *
      * <p>This method does not set the prime meridian to Greenwich.
      * Meridian rotation, if needed, shall be performed by the caller.</p>
      *
-     * @param  crs  a source CRS, or {@code null}.
+     * @param  crs      a source CRS, or {@code null}.
+     * @param  latlon   {@code true} for (latitude, longitude) axis order, or {@code false} for
(longitude, latitude).
+     * @param  allow3D  whether this method is allowed to return three-dimensional CRS (with
ellipsoidal height).
      * @return a two-dimensional geographic CRS with standard axes, or {@code null} if none.
      */
-    public static GeographicCRS toNormalizedGeographicCRS(CoordinateReferenceSystem crs)
{
+    public static GeographicCRS toNormalizedGeographicCRS(CoordinateReferenceSystem crs,
final boolean latlon, final boolean allow3D) {
         /*
          * ProjectedCRS instances always have a GeographicCRS as their base.
          * More generally, derived CRS are always derived from a base, which
@@ -283,21 +315,42 @@ public final class ReferencingUtilities extends Static {
         if (crs instanceof GeodeticCRS) {
             /*
              * At this point we usually have a GeographicCRS, but it could also be a GeocentricCRS.
+             * If we can let `forConvention` do its job, do that first since it may return
a cached
+             * instance. If the CRS is a `GeographicCRS` but not a `DefaultGeographicCRS`,
create a
+             * CRS in this code instead than invoking `DefaultGeographicCRS.castOrCopy(…)`
in order
+             * to create only one CRS instead of two.
              */
-            if (crs instanceof DefaultGeographicCRS && crs.getCoordinateSystem().getDimension()
== 2) {
+            final CoordinateSystem cs = crs.getCoordinateSystem();
+            if (!latlon && crs instanceof DefaultGeographicCRS && (allow3D
|| cs.getDimension() == 2)) {
                 return ((DefaultGeographicCRS) crs).forConvention(AxesConvention.NORMALIZED);
             }
-            final CoordinateSystem cs = CommonCRS.defaultGeographic().getCoordinateSystem();
-            if (crs instanceof GeographicCRS && Utilities.equalsIgnoreMetadata(cs,
crs.getCoordinateSystem())) {
+            /*
+             * Get a normalized coordinate system with the number of dimensions authorized
by the
+             * `allow3D` argument. We do not check if we can invoke `cs.forConvention(…)`
because
+             * it is unlikely that `cs` will be an instance of `DefaultEllipsoidalCS` is
`crs` is
+             * not a `DefaultGeographicCRS`. The code below has more chances to use cached
instance.
+             */
+            EllipsoidalCS normalizedCS;
+            if (allow3D && cs.getDimension() >= 3) {
+                normalizedCS = CommonCRS.WGS84.geographic3D().getCoordinateSystem();
+                if (!latlon) {
+                    normalizedCS = DefaultEllipsoidalCS.castOrCopy(normalizedCS).forConvention(AxesConvention.NORMALIZED);
+                }
+            } else if (latlon) {
+                normalizedCS = CommonCRS.WGS84.geographic().getCoordinateSystem();
+            } else {
+                normalizedCS = CommonCRS.defaultGeographic().getCoordinateSystem();
+            }
+            if (crs instanceof GeographicCRS && Utilities.equalsIgnoreMetadata(normalizedCS,
cs)) {
                 return (GeographicCRS) crs;
             }
             return new DefaultGeographicCRS(
                     singletonMap(DefaultGeographicCRS.NAME_KEY, NilReferencingObject.UNNAMED),
-                    ((GeodeticCRS) crs).getDatum(), (EllipsoidalCS) cs);
+                    ((GeodeticCRS) crs).getDatum(), normalizedCS);
         }
         if (crs instanceof CompoundCRS) {
             for (final CoordinateReferenceSystem e : ((CompoundCRS) crs).getComponents())
{
-                final GeographicCRS candidate = toNormalizedGeographicCRS(e);
+                final GeographicCRS candidate = toNormalizedGeographicCRS(e, latlon, allow3D);
                 if (candidate != null) {
                     return candidate;
                 }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
index 2ea2184..839177b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/internal/referencing/ServicesForMetadata.java
@@ -312,7 +312,7 @@ public final class ServicesForMetadata extends ReferencingServices {
             final String findOpCaller) throws TransformException
     {
         final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem();
-        GeographicCRS normalizedCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs);
+        GeographicCRS normalizedCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs,
false, false);
         if (normalizedCRS == null) {
             if (crs != null) {
                 normalizedCRS = CommonCRS.defaultGeographic();
@@ -403,7 +403,7 @@ public final class ServicesForMetadata extends ReferencingServices {
                 box = new DefaultGeographicBoundingBox();
                 spatialExtents.add(box);
             }
-            GeographicCRS normalizedCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs);
+            GeographicCRS normalizedCRS = ReferencingUtilities.toNormalizedGeographicCRS(crs,
false, false);
             if (normalizedCRS == null) {
                 normalizedCRS = CommonCRS.defaultGeographic();
             }
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
index 054685c..a1e900b 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/CRS.java
@@ -850,7 +850,7 @@ public final class CRS extends Static {
                  * the amount of transformation needed.
                  */
                 final SingleCRS targetCRS = getHorizontalComponent(crs);
-                final GeographicCRS sourceCRS = ReferencingUtilities.toNormalizedGeographicCRS(targetCRS);
+                final GeographicCRS sourceCRS = ReferencingUtilities.toNormalizedGeographicCRS(targetCRS,
false, false);
                 if (sourceCRS != null) {
                     envelope = merged = new GeneralEnvelope(bounds);
                     merged.translate(-getGreenwichLongitude(sourceCRS), 0);
@@ -1057,7 +1057,7 @@ public final class CRS extends Static {
     }
 
     /**
-     * If the given CRS would quality as horizontal except for its number of dimensions,
returns that number.
+     * If the given CRS would qualify as horizontal except for its number of dimensions,
returns that number.
      * Otherwise returns 0. The number of dimensions can only be 2 or 3.
      */
     private static int horizontalCode(final CoordinateReferenceSystem crs) {
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
new file mode 100644
index 0000000..884c4a9
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/GeodeticCalculator.java
@@ -0,0 +1,268 @@
+/*
+ * 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;
+
+import org.opengis.referencing.datum.Ellipsoid;
+import org.opengis.referencing.operation.TransformException;
+import org.opengis.referencing.crs.GeographicCRS;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.opengis.geometry.coordinate.Position;
+import org.opengis.geometry.DirectPosition;
+
+import org.apache.sis.measure.Latitude;
+import org.apache.sis.internal.referencing.PositionTransformer;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.ArgumentChecks;
+
+import static java.lang.Math.*;
+
+
+/**
+ * Performs geodetic calculations on a sphere or an ellipsoid.
+ * This class calculates the following properties:
+ *
+ * <ul>
+ *   <li>Distance and azimuth between two points.</li>
+ *   <li>Point located at a given distance and azimuth from another point.</li>
+ * </ul>
+ *
+ * The calculation uses the following information:
+ *
+ * <ul>
+ *   <li>The {@linkplain #setStartingPosition(Position) starting position}, which is
always considered valid.
+ *     It is initially set at (0,0) and can only be changed to another valid value.</li>
+ *   <li>One of the following
+ *     (the latest specified property overrides the other property and determines what will
be calculated):
+ *     <ul>
+ *       <li>the {@linkplain #setDestinationPosition(Position) destination position},
or</li>
+ *       <li>an {@linkplain #setDirection(double, double) azimuth and distance}.</li>
+ *     </ul>
+ *   </li>
+ * </ul>
+ *
+ * This class is not thread-safe. If geodetic calculations are needed in a multi-threads
environment,
+ * then a distinct instance of {@code GeodeticCalculator} needs to be created for each thread.
+ *
+ * @version 1.0
+ * @since 1.0
+ * @module
+ */
+public class GeodeticCalculator {
+    /**
+     * The transform from user coordinates to geodetic coordinates used in computation.
+     * This object also holds the following information:
+     *
+     * <ul>
+     *   <li>{@link PositionTransformer#defaultCRS} is the default CRS for all methods
receiving a
+     *       {@link Position} argument if the given position does not specify its own CRS.</li>
+     *   <li>{@link PositionTransformer#getCoordinateReferenceSystem()} is the CRS
of all methods
+     *       receiving (φ,λ) arguments as {@code double} values.</li>
+     * </ul>
+     */
+    private final PositionTransformer userToGeodetic;
+
+    /**
+     * The ellipsoid on which geodetic computations are performed.
+     * This ellipsoid is inferred from the coordinate reference system specified at construction
time.
+     */
+    protected final Ellipsoid ellipsoid;
+
+    /**
+     * The radius of a hypothetical sphere having the same surface than the {@linkplain #ellipsoid}.
+     * Used for the approximation using spherical formulas.
+     * Subclasses using ellipsoidal formulas will ignore this field.
+     *
+     * @see org.apache.sis.referencing.datum.DefaultEllipsoid#getAuthalicRadius()
+     */
+    private final double radius;
+
+    /**
+     * The (<var>latitude</var>, <var>longitude</var>) coordinates
of the first point <strong>in radians</strong>.
+     * This point is set by {@link #setStartingPosition(double, double)}.
+     */
+    private double φ1, λ1;
+
+    /**
+     * The (<var>latitude</var>, <var>longitude</var>) coordinates
of the destination point <strong>in radians</strong>.
+     * This point is set by {@link #setDestinationPosition(double, double)}.
+     */
+    private double φ2, λ2;
+
+    /**
+     * The distance and azimuth from the starting point ({@link #φ1},{@link #λ1}) to the
destination point ({@link #φ2},{@link #λ2}).
+     * The distance is in the same units than ellipsoid axes and the azimuth is in radians.
+     */
+    private double distance, azimuth;
+
+    /**
+     * Tells if the destination point is valid.
+     * This is {@code false} if {@link #φ2} and {@link #λ2} need to be computed.
+     */
+    private boolean isDestinationValid;
+
+    /**
+     * Tells if the azimuth and the distance are valid.
+     * This is {@code false} if {@link #distance} and {@link #azimuth} need to be computed.
+     */
+    private boolean isDirectionValid;
+
+    /**
+     * Constructs a new geodetic calculator expecting coordinates in the supplied CRS.
+     * The geodetic formulas implemented by this {@code GeodeticCalculator} base class assume
a spherical model.
+     * This constructor is for subclasses computing geodesy on an ellipsoid or other figure
of the Earth.
+     * Users should invoke {@link #create(CoordinateReferenceSystem)} instead, which will
choose a subtype
+     * based on the given coordinate reference system.
+     *
+     * @param  crs  the reference system for the {@link Position} arguments and return values.
+     */
+    protected GeodeticCalculator(final CoordinateReferenceSystem crs) {
+        ArgumentChecks.ensureNonNull("crs", crs);
+        final GeographicCRS geographic = ReferencingUtilities.toNormalizedGeographicCRS(crs,
true, true);
+        if (geographic == null) {
+            throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalCRSType_1,
ReferencingUtilities.getInterface(crs)));
+        }
+        ellipsoid      = ReferencingUtilities.getEllipsoid(crs);
+        radius         = Formulas.getAuthalicRadius(ellipsoid);
+        userToGeodetic = new PositionTransformer(crs, geographic, null);
+    }
+
+    /**
+     * Creates a new geodetic calculator associated with default ellipsoid.
+     * The ellipsoid and the geodetic datum of {@linkplain #getPositionCRS() position CRS}
+     * are the ones associated to {@link CommonCRS#defaultGeographic()}.
+     * The axes are (<var>latitude</var>, <var>longitude</var>) in
degrees.
+     *
+     * @return a new geodetic calculator using the default geodetic datum.
+     */
+    public static GeodeticCalculator create() {
+        return new GeodeticCalculator(CommonCRS.DEFAULT.geographic());
+    }
+
+    /**
+     * Constructs a new geodetic calculator expecting coordinates in the supplied CRS.
+     * All {@code GeodeticCalculator} methods having a {@link Position} argument
+     * or return value will use that specified CRS.
+     * That CRS is the value returned by {@link #getPositionCRS()}.
+     *
+     * @param  crs  the reference system for the {@link Position} objects.
+     * @return a new geodetic calculator using the specified CRS.
+     */
+    public static GeodeticCalculator create(final CoordinateReferenceSystem crs) {
+        return new GeodeticCalculator(crs);
+    }
+
+    /**
+     * Returns the default CRS of {@link Position} instances. For every method expecting
a {@code Position} argument,
+     * if the {@linkplain DirectPosition#getCoordinateReferenceSystem() CRS of that position}
is unspecified,
+     * then the CRS given by this method is assumed.
+     * Conversely every method returning a {@code Position} value will use this CRS.
+     *
+     * <p>This is the CRS specified at construction time.
+     * This CRS is not necessarily geographic; it may be projected or geocentric.</p>
+     *
+     * @return the default CRS for {@link Position} instances.
+     */
+    public CoordinateReferenceSystem getPositionCRS() {
+        return userToGeodetic.getCoordinateReferenceSystem();
+    }
+
+    /**
+     * Returns the coordinate reference system for all methods expecting (φ,λ) as {@code
double} values.
+     * This CRS always use (<var>latitude</var>, <var>longitude</var>)
axis order in degrees.
+     *
+     * @return the coordinate reference system of (φ,λ) coordinates.
+     */
+    public GeographicCRS getGeographicCRS() {
+        return (GeographicCRS) userToGeodetic.defaultCRS;
+    }
+
+    /**
+     * Sets the starting point as geographic coordinates. The azimuth and geodesic distance
values
+     * will be updated as a side effect of this call. They will be recomputed the next time
that
+     * {@link #getAzimuth()} or {@link #getGeodesicDistance()} are invoked.
+     *
+     * @param  latitude   the latitude in decimal degrees between -90 and  +90°
+     * @param  longitude  the longitude in decimal degrees.
+     * @throws IllegalArgumentException if the latitude is out of bounds.
+     */
+    public void setStartingPosition(final double latitude, final double longitude) {
+        ArgumentChecks.ensureBetween("latitude", Latitude.MIN_VALUE, Latitude.MAX_VALUE,
latitude);
+        ArgumentChecks.ensureFinite("longitude", longitude);
+        φ1 = toRadians(latitude);
+        λ1 = toRadians(longitude);
+        isDestinationValid = false;
+        isDirectionValid   = false;
+    }
+
+    /**
+     * Sets the starting point in any CRS. The coordinates will be transformed to geographic
coordinates and
+     * given to {@link #setStartingPosition(double, double)}. If the given point is not associated
to a CRS,
+     * then the CRS specified at construction time is assumed.
+     *
+     * @param  position  the starting point in any coordinate reference system.
+     * @throws TransformException if the coordinates can not be transformed.
+     */
+    public void setStartingPosition(final Position position) throws TransformException {
+        final DirectPosition p = userToGeodetic.transform(position.getDirectPosition());
+        setStartingPosition(p.getOrdinate(0), p.getOrdinate(1));
+    }
+
+    /**
+     * Returns the starting point in the CRS specified at construction time.
+     *
+     * @return the starting point in user CRS.
+     * @throws TransformException if the coordinates can not be transformed to user CRS.
+     */
+    public DirectPosition getStartingPosition() throws TransformException {
+        userToGeodetic.setOrdinate(0, toDegrees(φ1));
+        userToGeodetic.setOrdinate(1, toDegrees(λ1));
+        return userToGeodetic.inverseTransform();
+    }
+
+    /**
+     * Sets the destination point as geographic coordinates. The azimuth and geodesic distance
values
+     * will be updated as a side effect of this call. They will be recomputed the next time
that
+     * {@link #getAzimuth()} or {@link #getGeodesicDistance()} are invoked.
+     *
+     * @param  latitude   the latitude in decimal degrees between -90 and  +90°
+     * @param  longitude  the longitude in decimal degrees.
+     * @throws IllegalArgumentException if the latitude is out of bounds.
+     */
+    public void setDestinationPosition(final double latitude, final double longitude) {
+        ArgumentChecks.ensureBetween("latitude", Latitude.MIN_VALUE, Latitude.MAX_VALUE,
latitude);
+        ArgumentChecks.ensureFinite("longitude", longitude);
+        φ2 = toRadians(latitude);
+        λ2 = toRadians(longitude);
+        isDestinationValid = false;
+        isDirectionValid   = false;
+    }
+
+    /**
+     * Sets the destination point in any CRS. The coordinates will be transformed to geographic
coordinates and
+     * given to {@link #setDestinationPosition(double, double)}. If the given point is not
associated to a CRS,
+     * then the CRS specified at construction time is assumed.
+     *
+     * @param  position  the destination point in any coordinate reference system.
+     * @throws TransformException if the coordinates can not be transformed.
+     */
+    public void setDestinationPosition(final Position position) throws TransformException
{
+        final DirectPosition p = userToGeodetic.transform(position.getDirectPosition());
+        setDestinationPosition(p.getOrdinate(0), p.getOrdinate(1));
+    }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
index c1d0366..19c2e1a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticAuthorityFactory.java
@@ -31,6 +31,7 @@ import org.opengis.parameter.ParameterDescriptor;
 import org.opengis.util.FactoryException;
 import org.opengis.util.InternationalString;
 import org.apache.sis.internal.util.Citations;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
 import org.apache.sis.referencing.AbstractIdentifiedObject;
 import org.apache.sis.util.iso.SimpleInternationalString;
 import org.apache.sis.util.iso.DefaultNameSpace;
@@ -1284,12 +1285,7 @@ public abstract class GeodeticAuthorityFactory extends AbstractFactory
implement
          * Get the actual type of the object. Returns the GeoAPI type if possible,
          * or fallback on the implementation class otherwise.
          */
-        final Class<?> actual;
-        if (object instanceof AbstractIdentifiedObject) {
-            actual = ((AbstractIdentifiedObject) object).getInterface();
-        } else {
-            actual = object.getClass();
-        }
+        final Class<?> actual = ReferencingUtilities.getInterface(object);
         /*
          * Get the authority from the object if possible, in order to avoid a call
          * to the potentially costly (for EPSGDataAccess) getAuthority() method.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
index a9b1143..7e01c6c 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/ReferencingUtilitiesTest.java
@@ -74,13 +74,13 @@ public final strictfp class ReferencingUtilitiesTest extends TestCase
{
     private static void assertNormalizedEqualsWGS84(final String message, final boolean createExpected,
             final CoordinateReferenceSystem crs)
     {
-        final GeographicCRS normalizedCRS = toNormalizedGeographicCRS(crs);
+        final GeographicCRS normalizedCRS = toNormalizedGeographicCRS(crs, false, false);
         assertTrue(message, Utilities.equalsIgnoreMetadata(HardCodedCRS.WGS84, normalizedCRS));
         assertEquals("New CRS instance expected:", createExpected, normalizedCRS != HardCodedCRS.WGS84);
     }
 
     /**
-     * Tests {@link ReferencingUtilities#toNormalizedGeographicCRS(CoordinateReferenceSystem)}.
+     * Tests {@link ReferencingUtilities#toNormalizedGeographicCRS(CoordinateReferenceSystem,
boolean, boolean)}.
      */
     @Test
     public void testToNormalizedGeographicCRS() {
@@ -89,7 +89,7 @@ public final strictfp class ReferencingUtilitiesTest extends TestCase {
         assertNormalizedEqualsWGS84("Shall extract the 2D component.", false, HardCodedCRS.GEOID_4D);
         assertNormalizedEqualsWGS84("Shall build a the 2D component.", true,  HardCodedCRS.WGS84_3D);
         assertNormalizedEqualsWGS84("Shall normalize axis order.",     true,  HardCodedCRS.WGS84_φλ);
-        assertNull(toNormalizedGeographicCRS(null));
+        assertNull(toNormalizedGeographicCRS(null, false, false));
     }
 
     /**
diff --git a/ide-project/NetBeans/nbproject/genfiles.properties b/ide-project/NetBeans/nbproject/genfiles.properties
index dbff901..0e08e7c 100644
--- a/ide-project/NetBeans/nbproject/genfiles.properties
+++ b/ide-project/NetBeans/nbproject/genfiles.properties
@@ -3,6 +3,6 @@
 build.xml.data.CRC32=58e6b21c
 build.xml.script.CRC32=462eaba0
 build.xml.stylesheet.CRC32=28e38971@1.53.1.46
-nbproject/build-impl.xml.data.CRC32=8f8309b2
+nbproject/build-impl.xml.data.CRC32=55754ab4
 nbproject/build-impl.xml.script.CRC32=a7689f96
 nbproject/build-impl.xml.stylesheet.CRC32=3a2fa800@1.89.1.48
diff --git a/ide-project/NetBeans/nbproject/project.xml b/ide-project/NetBeans/nbproject/project.xml
index 7627d51..5b9530d 100644
--- a/ide-project/NetBeans/nbproject/project.xml
+++ b/ide-project/NetBeans/nbproject/project.xml
@@ -80,6 +80,7 @@
         <spellchecker-wordlist xmlns="http://www.netbeans.org/ns/spellchecker-wordlist/1">
             <word>accessor</word>
             <word>antimeridian</word>
+            <word>authalic</word>
             <word>bilevel</word>
             <word>bitmask</word>
             <word>boolean</word>


Mime
View raw message