When computing inverse projection, temporarily disable the setting of out-of-domain coordinates to NaN.
We use the Inverter class as a flag for identifying those situations.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix1.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix1.java
index 8504238..ff93401 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix1.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix1.java
@@ -30,7 +30,7 @@
* └ ┘</pre></blockquote>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.4
+ * @version 1.1
*
* @see Matrix2
* @see Matrix3
@@ -39,8 +39,7 @@
* @since 0.4
* @module
*/
-@SuppressWarnings("CloneableClassWithoutClone") // No field in this class needs clone.
-public final class Matrix1 extends MatrixSIS {
+public class Matrix1 extends MatrixSIS {
/**
* Serial number for inter-operability with different versions.
*/
@@ -260,6 +259,14 @@
}
/**
+ * {@inheritDoc}
+ */
+ @Override
+ public Matrix1 clone() {
+ return (Matrix1) super.clone();
+ }
+
+ /**
* Returns {@code true} if the specified object is of type {@code Matrix1} and
* all of the data members are equal to the corresponding data members in this matrix.
*
@@ -268,7 +275,7 @@
*/
@Override
public boolean equals(final Object object) {
- if (object instanceof Matrix1) {
+ if (object != null && object.getClass() == getClass()) {
final Matrix1 that = (Matrix1) object;
return Numerics.equals(m00, that.m00);
}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix2.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix2.java
index 04e0f51..c3f455a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix2.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix2.java
@@ -31,7 +31,7 @@
* └ ┘</pre></blockquote>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.4
+ * @version 1.1
*
* @see Matrix1
* @see Matrix3
@@ -40,8 +40,7 @@
* @since 0.4
* @module
*/
-@SuppressWarnings("CloneableClassWithoutClone") // No field in this class needs clone.
-public final class Matrix2 extends MatrixSIS {
+public class Matrix2 extends MatrixSIS {
/**
* Serial number for inter-operability with different versions.
*/
@@ -284,6 +283,14 @@
}
/**
+ * {@inheritDoc}
+ */
+ @Override
+ public Matrix2 clone() {
+ return (Matrix2) super.clone();
+ }
+
+ /**
* Returns {@code true} if the specified object is of type {@code Matrix2} and
* all of the data members are equal to the corresponding data members in this matrix.
*
@@ -292,7 +299,7 @@
*/
@Override
public boolean equals(final Object object) {
- if (object instanceof Matrix2) {
+ if (object != null && object.getClass() == getClass()) {
final Matrix2 that = (Matrix2) object;
return Numerics.equals(this.m00, that.m00) &&
Numerics.equals(this.m01, that.m01) &&
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix3.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix3.java
index 6edfb8f..d246bc0 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix3.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix3.java
@@ -30,7 +30,7 @@
* └ ┘</pre></blockquote>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.6
+ * @version 1.1
*
* @see Matrix1
* @see Matrix2
@@ -39,8 +39,7 @@
* @since 0.4
* @module
*/
-@SuppressWarnings("CloneableClassWithoutClone") // No field in this class needs clone.
-public final class Matrix3 extends MatrixSIS {
+public class Matrix3 extends MatrixSIS {
/**
* Serial number for inter-operability with different versions.
*/
@@ -298,4 +297,12 @@
swap = m02; m02 = m20; m20 = swap;
swap = m12; m12 = m21; m21 = swap;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Matrix3 clone() {
+ return (Matrix3) super.clone();
+ }
}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix4.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix4.java
index 8093b87..2f14f39 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix4.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/Matrix4.java
@@ -31,7 +31,7 @@
* └ ┘</pre></blockquote>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.6
+ * @version 1.1
*
* @see Matrix1
* @see Matrix2
@@ -40,8 +40,7 @@
* @since 0.4
* @module
*/
-@SuppressWarnings("CloneableClassWithoutClone") // No field in this class needs clone.
-public final class Matrix4 extends MatrixSIS {
+public class Matrix4 extends MatrixSIS {
/**
* Serial number for inter-operability with different versions.
*/
@@ -336,4 +335,12 @@
swap = m13; m13 = m31; m31 = swap;
swap = m23; m23 = m32; m32 = swap;
}
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Matrix4 clone() {
+ return (Matrix4) super.clone();
+ }
}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/package-info.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/package-info.java
index 0005bb1..db87112 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/package-info.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/matrix/package-info.java
@@ -69,7 +69,7 @@
* Like SIS, Vecmath is optimized for small matrices of interest for 2D and 3D graphics.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.1
* @since 0.4
* @module
*/
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Inverter.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Inverter.java
new file mode 100644
index 0000000..d928119
--- /dev/null
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Inverter.java
@@ -0,0 +1,105 @@
+/*
+ * 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.operation.projection;
+
+import org.apache.sis.internal.referencing.Resources;
+import org.apache.sis.referencing.operation.matrix.Matrix2;
+
+import static java.lang.Math.abs;
+
+
+/**
+ * A temporary Jacobian matrix where to write the derivative of a forward projection.
+ * This Jacobian matrix is used for calculation of inverse projection when no inverse
+ * formulas is available, or when the inverse formula is too approximate (for example
+ * because eccentricity is too high). This class processes as below:
+ *
+ * <ol>
+ * <li>Given a first estimation of (λ,φ), compute the forward projection (x′,y′)
+ * for that estimation together with the Jacobian matrix at that position.</li>
+ * <li>Compute the errors compared to the specified (x,y) values.</li>
+ * <li>Convert that (Δx,Δy) error into a (Δλ,Δφ) error using the inverse of the Jacobian matrix.</li>
+ * <li>Correct (λ,φ) and continue iteratively until the error is small enough.</li>
+ * </ol>
+ *
+ * This algorithm described in EPSG guidance note for {@link Orthographic} projection
+ * but can be applied to any map projection, not only orthographic.
+ *
+ * <p>This algorithm is defined in a {@link Matrix2} subclass for allowing map projection
+ * implementations to use {@code if (derivative instanceof Inverter)} check for detecting
+ * when a {@code transform} method is invoked for the purpose of an inverse projection.</p>
+ *
+ * @author Martin Desruisseaux (Geomatys)
+ * @version 1.1
+ *
+ * @ee <a href="https://issues.apache.org/jira/browse/SIS-478">SIS-478</a>
+ *
+ * @since 1.1
+ * @module
+ */
+@SuppressWarnings({"CloneableClassWithoutClone", "serial"})
+final class Inverter extends Matrix2 {
+ /**
+ * Creates a new matrix initialized to identity.
+ */
+ Inverter() {
+ }
+
+ /**
+ * Computes the inverse of the given projection. Before this method is invoked,
+ * the {@code dstPts[dstOff]} and {@code dstPts[dstOff+1]} array elements must
+ * contain the initial λ and φ estimations respectively. After method returns,
+ * the same array elements contain the refined λ and φ values.
+ *
+ * <p>Note: restricted to {@link Orthographic} projection for now,
+ * but may be generalized to any projection in a future version.</p>
+ *
+ * @param projection the forward projection for which to compute an inverse projection.
+ * @param x the easting value from {@code srcPts[srcOff]}.
+ * @param y the northing value from {@code srcPts[srcOff+1]}.
+ * @param dstPts the array where to refine the (λ,φ) values.
+ * @param dstOff index of the λ element in the {@code dstPts} array.
+ * @throws ProjectionException if the iterative algorithm does not converge.
+ */
+ final void inverseTransform(final Orthographic projection, final double x, final double y,
+ final double[] dstPts, final int dstOff) throws ProjectionException
+ {
+ double λ = dstPts[dstOff ];
+ double φ = dstPts[dstOff+1];
+ for (int it=NormalizedProjection.MAXIMUM_ITERATIONS; --it >= 0;) {
+ final double cosφ = projection.transform(dstPts, dstOff, dstPts, dstOff, this);
+ final double ΔE = x - dstPts[dstOff ];
+ final double ΔN = y - dstPts[dstOff+1];
+ final double D = (m01 * m10) - (m00 * m11); // Determinant.
+ final double Δλ = (m01*ΔN - m11*ΔE) / D;
+ final double Δφ = (m10*ΔE - m00*ΔN) / D;
+ dstPts[dstOff ] = λ += Δλ;
+ dstPts[dstOff+1] = φ += Δφ;
+ /*
+ * Following condition uses ! for stopping iteration on NaN values.
+ * We do not use Math.max(…) because we want to check NaN separately
+ * for φ and λ.
+ */
+ if (!(abs(Δφ) > NormalizedProjection.ANGULAR_TOLERANCE ||
+ abs(Δλ*cosφ) > NormalizedProjection.ANGULAR_TOLERANCE))
+ {
+ return;
+ }
+ }
+ throw new ProjectionException(Resources.format(Resources.Keys.NoConvergence));
+ }
+}
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java
index 9e3d024..87b9705 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/operation/projection/Orthographic.java
@@ -25,7 +25,6 @@
import org.apache.sis.referencing.operation.matrix.Matrix2;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.ContextualParameters;
-import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.util.Workaround;
@@ -150,13 +149,16 @@
* <div class="note"><b>Implementation note:</b>
* in other map projections, we use a different class for ellipsoidal formulas.
* But the orthographic projection is a bit different; for this one it is more
- * convenient to use a flag.</div>
+ * convenient to use {@code if} statements.</div>
*
+ * @param derivative where to store the Jacobian matrix, or {@code null} if none.
+ * If this matrix is an {@link Inverter} instance, we take that as a flag
+ * meaning to not set out-of-range results to NaN.
* @return {@code cos(φ)}, useful for error tolerance check.
*/
- private double transform(final double[] srcPts, final int srcOff,
- final double[] dstPts, final int dstOff,
- final Matrix2 derivative)
+ final double transform(final double[] srcPts, final int srcOff,
+ final double[] dstPts, final int dstOff,
+ final Matrix2 derivative)
{
final double λ = srcPts[srcOff ];
final double φ = srcPts[srcOff+1];
@@ -174,7 +176,7 @@
* then the point is on the opposite hemisphere and should be clipped.
*/
double rν;
- if (cosc >= 0) {
+ if (cosc >= 0 || derivative instanceof Inverter) {
x = cosφ * sinλ; // Snyder (20-3)
y = mℯ2_cosφ0*sinφ - sinφ0*cosφ_cosλ; // Snyder (20-4) × (1 – ℯ²)
rν = 1;
@@ -203,7 +205,7 @@
derivative.m00 = cosφ_cosλ / rν; // ∂E/∂λ
derivative.m01 = -sinφ*sinλ * ρ; // ∂E/∂φ
derivative.m10 = sinφ0 * x; // ∂N/∂λ
- derivative.m11 = (cosφ0 * cosφ + sinφ0_sinφ*cosλ) * ρ; // ∂N/∂φ
+ derivative.m11 = (cosφ0*cosφ + sinφ0_sinφ*cosλ) * ρ; // ∂N/∂φ
}
return cosφ;
}
@@ -238,29 +240,9 @@
*
* See https://issues.apache.org/jira/browse/SIS-478
*/
- final Matrix2 J = new Matrix2(); // Jacobian matrix.
- double λ = dstPts[dstOff ];
- double φ = dstPts[dstOff+1];
- for (int it=0; it<MAXIMUM_ITERATIONS; it++) {
- final double cosφ = transform(dstPts, dstOff, dstPts, dstOff, J);
- final double ΔE = x - dstPts[dstOff ];
- final double ΔN = y - dstPts[dstOff+1];
- final double D = (J.m01 * J.m10) - (J.m00 * J.m11); // Determinant.
- final double Δλ = (J.m01*ΔN - J.m11*ΔE) / D;
- final double Δφ = (J.m10*ΔE - J.m00*ΔN) / D;
- dstPts[dstOff ] = λ += Δλ;
- dstPts[dstOff+1] = φ += Δφ;
- /*
- * Following condition uses ! for stopping iteration on NaN values.
- * We do not use Math.max(…) because we want to check NaN separately
- * for φ and λ.
- */
- if (!(abs(Δφ) > ANGULAR_TOLERANCE || abs(cosφ*abs(Δλ)) > ANGULAR_TOLERANCE)) {
- dstPts[dstOff] = IEEEremainder(dstPts[dstOff], PI);
- return;
- }
- }
- throw new ProjectionException(Resources.format(Resources.Keys.NoConvergence));
+ final Inverter j = new Inverter(); // Jacobian matrix.
+ j.inverseTransform(this, x, y, dstPts, dstOff);
+ dstPts[dstOff] = IEEEremainder(dstPts[dstOff], PI);
}
}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/OrthographicTest.java b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/OrthographicTest.java
index 42fc7c5..434d55b 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/OrthographicTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/operation/projection/OrthographicTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.sis.referencing.operation.projection;
+import org.opengis.util.FactoryException;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.referencing.operation.transform.CoordinateDomain;
import org.apache.sis.internal.referencing.provider.MapProjection;
@@ -144,4 +145,18 @@
verifyInDomain(CoordinateDomain.GEOGRAPHIC_RADIANS_SOUTH, 753524735);
verifyDerivative(toRadians(5), toRadians(-85));
}
+
+ /**
+ * Tests the <cite>"Orthographic"</cite> (EPSG:9840) projection method.
+ * This test is defined in GeoAPI conformance test suite.
+ *
+ * @throws FactoryException if an error occurred while creating the map projection.
+ * @throws TransformException if an error occurred while projecting a coordinate.
+ *
+ * @see org.opengis.test.referencing.ParameterizedTransformTest#testOrthographic()
+ */
+ @Test
+ public void runGeoapiTest() throws FactoryException, TransformException {
+ createGeoApiTest(new org.apache.sis.internal.referencing.provider.Orthographic()).testOrthographic();
+ }
}