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: If UnitAngle["degree", 0.017453292519943295] is specified with too low precision (e.g. 0..01745329252), replace the low precision value by the expected value. https://issues.apache.org/jira/browse/SIS-377
Date Sun, 21 Oct 2018 20:54:03 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 eee6198  If UnitAngle["degree", 0.017453292519943295] is specified with too low precision
(e.g. 0..01745329252), replace the low precision value by the expected value. https://issues.apache.org/jira/browse/SIS-377
eee6198 is described below

commit eee6198480905552017ebce7a420f45e031d2922
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Oct 21 22:05:18 2018 +0200

    If UnitAngle["degree", 0.017453292519943295] is specified with too low precision (e.g.
0..01745329252), replace the low precision value by the expected value.
    https://issues.apache.org/jira/browse/SIS-377
---
 .../java/org/apache/sis/io/wkt/AbstractParser.java |   2 +-
 .../apache/sis/io/wkt/GeodeticObjectParser.java    |   6 +-
 .../org/apache/sis/io/wkt/MathTransformParser.java |  85 ++++++++++-
 .../org/apache/sis/io/wkt/ComparisonWithEPSG.java  | 161 +++++++++++++++++++++
 .../sis/io/wkt/GeodeticObjectParserTest.java       |   4 +-
 .../sis/referencing/factory/TestFactorySource.java |   2 +-
 .../sis/test/suite/ReferencingTestSuite.java       |   1 +
 .../java/org/apache/sis/math/DecimalFunctions.java |  83 ++++++++++-
 .../org/apache/sis/math/DecimalFunctionsTest.java  |  28 +++-
 .../src/test/java/org/apache/sis/test/Assert.java  |   4 +-
 10 files changed, 362 insertions(+), 14 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/AbstractParser.java b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
index 6cda543..eff03e6 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/AbstractParser.java
@@ -330,7 +330,7 @@ abstract class AbstractParser implements Parser {
     }
 
     /**
-     * Parses the given unit symbol.
+     * Parses the given unit name or symbol.
      */
     final Unit<?> parseUnit(final String text) throws ParserException {
         if (unitFormat == null) {
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 5c14e27..52ca5c5 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -90,7 +90,7 @@ import static java.util.Collections.singletonMap;
  * @author  Rémi Eve (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.6
  * @module
  */
@@ -621,6 +621,8 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
      * @return the {@code "UNIT"} element as an {@link Unit} object, or {@code null} if none.
      * @throws ParseException if the {@code "UNIT"} can not be parsed.
      *
+     * @see #parseUnit(Element)
+     *
      * @todo Authority code is currently discarded after parsing. We may consider to create
a subclass of
      *       {@link Unit} which implements {@link IdentifiedObject} in a future version.
      */
@@ -634,7 +636,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
         }
         final String name   = element.pullString("name");
         final double factor = element.pullDouble("factor");
-        Unit<Q> unit   = baseUnit.multiply(factor);
+        Unit<Q> unit   = baseUnit.multiply(completeUnitFactor(baseUnit, factor));
         Unit<?> verify = parseUnitID(element);
         element.close(ignoredElements);
         /*
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
index 1594d04..293decc 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/io/wkt/MathTransformParser.java
@@ -18,6 +18,7 @@ package org.apache.sis.io.wkt;
 
 import java.util.Map;
 import java.util.Collections;
+import java.util.Arrays;
 import java.util.Locale;
 import java.text.DateFormat;
 import java.text.NumberFormat;
@@ -40,8 +41,9 @@ import org.opengis.referencing.operation.OperationMethod;
 import org.apache.sis.internal.metadata.WKTKeywords;
 import org.apache.sis.internal.metadata.ReferencingServices;
 import org.apache.sis.internal.util.Constants;
-import org.apache.sis.measure.Units;
+import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.UnitFormat;
+import org.apache.sis.measure.Units;
 import org.apache.sis.util.Numbers;
 import org.apache.sis.util.resources.Errors;
 
@@ -55,7 +57,7 @@ import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * @author  Rémi Eve (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Rueben Schulz (UBC)
- * @version 0.8
+ * @version 1.0
  *
  * @see <a href="http://www.geoapi.org/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html">Well
Know Text specification</a>
  *
@@ -86,6 +88,27 @@ class MathTransformParser extends AbstractParser {
     };
 
     /**
+     * Some conversion factors applied to {@link #UNIT_KEYWORDS} for which rounding errors
are found in practice.
+     * Some Well Known Texts define factors with low accuracy, as in {@code ANGLEUNIT["degree",
0.01745329252]}.
+     * This causes the parser to fail to recognize that the unit is degree and to convert
angles with that factor.
+     * This may result in surprising behavior like <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>.
+     * This array is a workaround for that problem, adding the missing accuracy to factors.
Only factors having many
+     * digits need to appear here. For example there is no need to declare the conversion
factor for foot (0.3048)
+     * because that factor requires only 4 fraction digits, which are usually present in
WKT.
+     *
+     * <p>Values in each array <strong>must</strong> be sorted in ascending
order.</p>
+     */
+    private static final double[][] CONVERSION_FACTORS = {
+        {0.3047972654,              // Clarke's foot
+         0.30480060960121924,       // US survey foot
+         1609.3472186944375},       // US survey mile
+        {Math.PI/(180*60*60),       // Arc-second:  4.84813681109536E-6
+         Math.PI/(180*60),          // Arc-minute:  2.908882086657216E-4
+         Math.PI/(200),             // Grad:        1.5707963267948967E-2
+         Math.PI/(180)}             // Degree:      1.7453292519943295E-2
+    };
+
+    /**
      * The factory to use for creating math transforms.
      */
     final MathTransformFactory mtFactory;
@@ -227,6 +250,8 @@ class MathTransformParser extends AbstractParser {
      * @param  parent  the parent element.
      * @return the {@code "UNIT"} element, or {@code null} if none.
      * @throws ParseException if the {@code "UNIT"} can not be parsed.
+     *
+     * @see GeodeticObjectParser#parseScaledUnit(Element, String, Unit)
      */
     final Unit<?> parseUnit(final Element parent) throws ParseException {
         final Element element = parent.pullElement(OPTIONAL, UNIT_KEYWORDS);
@@ -234,14 +259,23 @@ class MathTransformParser extends AbstractParser {
             return null;
         }
         final String  name   = element.pullString("name");
-        final double  factor = element.pullDouble("factor");
+        double        factor = element.pullDouble("factor");
         final int     index  = element.getKeywordIndex() - 1;
         final Unit<?> unit   = parseUnitID(element);
         element.close(ignoredElements);
         if (unit != null) {
             return unit;
         }
+        /*
+         * Conversion factor can be applied only if the base dimension (angle, linear, scale,
etc.) is known.
+         * However before to apply that factor, we may need to fix rounding errors found
in some WKT strings.
+         * In particular, the conversion factor for degrees is sometime written as 0.01745329252
instead of
+         * 0.017453292519943295.
+         */
         if (index >= 0 && index < BASE_UNITS.length) {
+            if (index < CONVERSION_FACTORS.length) {
+                factor = completeUnitFactor(CONVERSION_FACTORS[index], factor);
+            }
             return BASE_UNITS[index].multiply(factor);
         }
         // If we can not infer the base type, we have to rely on the name.
@@ -254,6 +288,51 @@ class MathTransformParser extends AbstractParser {
     }
 
     /**
+     * If the unit conversion factor specified in the Well Known Text is missing some fraction
digits,
+     * try to complete them. The main use case is to replace 0.01745329252 by 0.017453292519943295
in
+     * degree units.
+     *
+     * @param  predefined  some known conversion factors, in ascending order.
+     * @param  factor      the conversion factor specified in the Well Known Text element.
+     * @return the conversion factor to use.
+     */
+    private static double completeUnitFactor(final double[] predefined, final double factor)
{
+        int i = Arrays.binarySearch(predefined, factor);
+        if (i < 0) {
+            i = Math.max(~i, 1);
+            double accurate = predefined[i-1];
+            if (i < predefined.length) {
+                double next = predefined[i];
+                if (next - factor < factor - accurate) {
+                    accurate = next;
+                }
+            }
+            if (DecimalFunctions.equalsIgnoreMissingFractionDigits(accurate, factor)) {
+                return accurate;
+            }
+        }
+        return factor;
+    }
+
+    /**
+     * If the unit conversion factor specified in the Well Known Text is missing some fraction
digits,
+     * try to complete them. The main use case is to replace 0.01745329252 by 0.017453292519943295
in
+     * degree units.
+     *
+     * @param  baseUnit  the base unit for which to complete the conversion factor.
+     * @param  factor    the conversion factor specified in the Well Known Text element.
+     * @return the conversion factor to use.
+     */
+    static double completeUnitFactor(final Unit<?> baseUnit, final double factor) {
+        for (int i=CONVERSION_FACTORS.length; --i>=0;) {
+            if (BASE_UNITS[i] == baseUnit) {
+                return completeUnitFactor(CONVERSION_FACTORS[i], factor);
+            }
+        }
+        return factor;
+    }
+
+    /**
      * Parses a sequence of {@code "PARAMETER"} elements.
      *
      * @param  element             the parent element containing the parameters to parse.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ComparisonWithEPSG.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ComparisonWithEPSG.java
new file mode 100644
index 0000000..a53adc7
--- /dev/null
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/ComparisonWithEPSG.java
@@ -0,0 +1,161 @@
+/*
+ * 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.io.wkt;
+
+import org.opengis.util.FactoryException;
+import org.opengis.referencing.crs.CoordinateReferenceSystem;
+import org.apache.sis.referencing.factory.TestFactorySource;
+import org.apache.sis.referencing.factory.sql.EPSGFactory;
+import org.apache.sis.referencing.CRS;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.TestCase;
+import org.junit.BeforeClass;
+import org.junit.AfterClass;
+import org.junit.Test;
+
+import static org.apache.sis.test.Assert.*;
+import static org.junit.Assume.assumeNotNull;
+
+
+/**
+ * Compares the result of some WKT parsing with the expected result from EPSG database.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+@DependsOn(WKTParserTest.class)
+public final strictfp class ComparisonWithEPSG extends TestCase {
+    /**
+     * Creates the factory to use for all tests in this class.
+     *
+     * @throws FactoryException if an error occurred while creating the factory.
+     */
+    @BeforeClass
+    public static void createFactory() throws FactoryException {
+        TestFactorySource.createFactory();
+    }
+
+    /**
+     * Forces release of JDBC connections after the tests in this class.
+     *
+     * @throws FactoryException if an error occurred while closing the connections.
+     */
+    @AfterClass
+    public static void close() throws FactoryException {
+        TestFactorySource.close();
+    }
+
+    /**
+     * Tests "Campo Inchauspe / Argentina 7" (EPSG:22197).
+     * This projection has a <cite>"Latitude of natural origin"</cite> at the
south pole.
+     *
+     * @throws FactoryException if an error occurred while creating the CRS.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>
+     */
+    @Test
+    public void testLatitudeAtPole() throws FactoryException {
+        compare("PROJCRS[\"Campo Inchauspe / Argentina 7\",\n" +
+                "  BASEGEODCRS[\"Campo Inchauspe\",\n" +
+                "    DATUM[\"Campo Inchauspe\",\n" +
+                "      ELLIPSOID[\"International 1924\",6378388,297,LENGTHUNIT[\"metre\",1.0]]]],\n"
+
+                "  CONVERSION[\"Argentina zone 7\",\n" +
+                "    METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],\n" +
+                "    PARAMETER[\"Latitude of natural origin\",-90,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Longitude of natural origin\",-54,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Scale factor at natural origin\",1,SCALEUNIT[\"unity\",1.0]],\n"
+
+                "    PARAMETER[\"False easting\",7500000,LENGTHUNIT[\"metre\",1.0]],\n" +
+                "    PARAMETER[\"False northing\",0,LENGTHUNIT[\"metre\",1.0]]],\n" +
+                "  CS[cartesian,2],\n" +
+                "    AXIS[\"northing (X)\",north,ORDER[1]],\n" +
+                "    AXIS[\"easting (Y)\",east,ORDER[2]],\n" +
+                "    LENGTHUNIT[\"metre\",1.0],\n" +
+                "  ID[\"EPSG\",22197]]", 22197);
+    }
+
+    /**
+     * Tests "Pulkovo 1942 / 3-degree Gauss-Kruger CM 180E" (EPSG:2636).
+     * This projection has a <cite>"Longitude of natural origin"</cite> at the
anti-meridian.
+     *
+     * @throws FactoryException if an error occurred while creating the CRS.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>
+     */
+    @Test
+    public void testLongitudeAtAntiMeridian() throws FactoryException {
+        compare("PROJCRS[\"Pulkovo 1942 / 3-degree Gauss-Kruger CM 180E\",\n" +
+                "  BASEGEODCRS[\"Pulkovo 1942\",\n" +
+                "    DATUM[\"Pulkovo 1942\",\n" +
+                "      ELLIPSOID[\"Krassowsky 1940\",6378245,298.3,LENGTHUNIT[\"metre\",1.0]]]],\n"
+
+                "  CONVERSION[\"3-degree Gauss-Kruger CM 180\",\n" +
+                "    METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],\n" +
+                "    PARAMETER[\"Latitude of natural origin\",0,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Longitude of natural origin\",180,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Scale factor at natural origin\",1,SCALEUNIT[\"unity\",1.0]],\n"
+
+                "    PARAMETER[\"False easting\",500000,LENGTHUNIT[\"metre\",1.0]],\n" +
+                "    PARAMETER[\"False northing\",0,LENGTHUNIT[\"metre\",1.0]]],\n" +
+                "  CS[cartesian,2],\n" +
+                "    AXIS[\"northing (X)\",north,ORDER[1]],\n" +
+                "    AXIS[\"easting (Y)\",east,ORDER[2]],\n" +
+                "    LENGTHUNIT[\"metre\",1.0],\n" +
+                "  ID[\"EPSG\",2636]]", 2636);
+    }
+
+    /**
+     * Tests "Belge 1950 (Brussels) / Belge Lambert 50" (EPSG:21500).
+     * This projection has a <cite>"Latitude of false origin"</cite> at the anti-meridian.
+     *
+     * @throws FactoryException if an error occurred while creating the CRS.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>
+     */
+    @Test
+    public void testLambert() throws FactoryException {
+        compare("PROJCRS[\"Belge 1950 (Brussels) / Belge Lambert 50\",\n" +
+                "  BASEGEODCRS[\"Belge 1950 (Brussels)\",\n" +
+                "    DATUM[\"Reseau National Belge 1950 (Brussels)\",\n" +
+                "      ELLIPSOID[\"International 1924\",6378388,297,LENGTHUNIT[\"metre\",1.0]]],\n"
+
+                "    PRIMEM[\"Brussels\",4.367975,ANGLEUNIT[\"degree\",0.01745329252]]],\n"
+
+                "  CONVERSION[\"Belge Lambert 50\",\n" +
+                "    METHOD[\"Lambert Conic Conformal (2SP)\",ID[\"EPSG\",9802]],\n" +
+                "    PARAMETER[\"Latitude of false origin\",90,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Longitude of false origin\",0,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Latitude of 1st standard parallel\",49.833333333333,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Latitude of 2nd standard parallel\",51.166666666667,ANGLEUNIT[\"degree\",0.01745329252]],\n"
+
+                "    PARAMETER[\"Easting at false origin\",150000,LENGTHUNIT[\"metre\",1.0]],\n"
+
+                "    PARAMETER[\"Northing at false origin\",5400000,LENGTHUNIT[\"metre\",1.0]]],\n"
+
+                "  CS[cartesian,2],\n" +
+                "    AXIS[\"easting (X)\",east,ORDER[1]],\n" +
+                "    AXIS[\"northing (Y)\",north,ORDER[2]],\n" +
+                "    LENGTHUNIT[\"metre\",1.0],\n" +
+                "  ID[\"EPSG\",21500]]", 21500);
+    }
+
+    /**
+     * Compares a projected CRS parsed from a WKT with a the CRS built from EPSG database.
+     * The later is taken as the reference.
+     */
+    private static void compare(final String wkt, final int epsg) throws FactoryException
{
+        final CoordinateReferenceSystem crs = CRS.fromWKT(wkt);
+        final EPSGFactory factory = TestFactorySource.factory;
+        assumeNotNull(factory);
+        final CoordinateReferenceSystem reference = factory.createProjectedCRS(Integer.toString(epsg));
+        assertEqualsIgnoreMetadata(reference, crs);
+    }
+}
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
index 2c9be9b..12ef351 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/GeodeticObjectParserTest.java
@@ -56,7 +56,7 @@ import static org.apache.sis.internal.util.StandardDateFormat.MILLISECONDS_PER_D
  * Tests {@link GeodeticObjectParser}.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.6
  * @module
  */
@@ -1119,7 +1119,7 @@ public final strictfp class GeodeticObjectParserTest extends TestCase
{
                "GEOGCS[“WGS 84”,\n" +
                "  DATUM[“World Geodetic System 1984”,\n" +
                "    SPHEROID[“WGS84”, 6378137.0, 298.257223563, Ext1[“foo”], Ext2[“bla”]]],\n"
+
-               "    PRIMEM[“Greenwich”, 0.0, Intruder[“unknown”], UNIT[“degree”,
0.01745]],\n" +    // Truncated scale factor.
+               "    PRIMEM[“Greenwich”, 0.0, Intruder[“unknown”], UNIT[“degree”,
0.01746]],\n" +    // Inaccurate scale factor.
                "  UNIT[“degree”, 0.017453292519943295], Intruder[“foo”]]");
 
         verifyGeographicCRS(0, crs);
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/TestFactorySource.java
b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/TestFactorySource.java
index 0747e0e..f5c0922 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/TestFactorySource.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/TestFactorySource.java
@@ -151,7 +151,7 @@ public final strictfp class TestFactorySource {
             factory = null;
             final int n = ((ConcurrentAuthorityFactory) af).countAvailableDataAccess();
             af.close();
-            assertBetween("Since we ran all tests sequantially, should have no more than
1 Data Access Object (DAO).", 0, 1, n);
+            assertBetween("Since we ran all tests sequentially, should have no more than
1 Data Access Object (DAO).", 0, 1, n);
         }
     }
 }
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
index efbd5ee..3a6f889 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/test/suite/ReferencingTestSuite.java
@@ -198,6 +198,7 @@ import org.junit.BeforeClass;
     org.apache.sis.io.wkt.GeodeticObjectParserTest.class,
     org.apache.sis.io.wkt.WKTFormatTest.class,
     org.apache.sis.io.wkt.WKTParserTest.class,
+    org.apache.sis.io.wkt.ComparisonWithEPSG.class,
 
     // Geodetic object creations from authority codes.
     org.apache.sis.referencing.factory.GIGS2001.class,
diff --git a/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java b/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java
index 74d3076..58f79b6 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/math/DecimalFunctions.java
@@ -46,7 +46,7 @@ import static org.apache.sis.internal.util.Numerics.SIGNIFICAND_SIZE;
  * since base 10 is not more "real" than base 2 for natural phenomenon.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.0
  *
  * @see MathFunctions#pow10(int)
  * @see Math#log10(double)
@@ -56,7 +56,7 @@ import static org.apache.sis.internal.util.Numerics.SIGNIFICAND_SIZE;
  */
 public final class DecimalFunctions extends Static {
     /**
-     * The greatest power of 10 such as {@code Math.pow(10, E10_FOR_ZERO) == 0}.
+     * The greatest power of 10 such as {@code Math.pow(10, EXPONENT_FOR_ZERO) == 0}.
      * This is the exponent in {@code parseDouble("1E-324")} &lt; {@link Double#MIN_VALUE},
      * which is stored as zero because non-representable as a {@code double} value.
      * The next power, {@code parseDouble("1E-323")}, is a non-zero {@code double} value.
@@ -476,4 +476,83 @@ public final class DecimalFunctions extends Static {
         }
         return digits;
     }
+
+    /**
+     * Returns {@code true} if the given numbers or equal or differ only by {@code accurate}
+     * having more non-zero trailing decimal fraction digits than {@code approximate}.
+     *
+     * <table class="sis">
+     *   <caption>Examples</caption>
+     *   <tr><th>Accurate</th> <th>Approximate</th> <th>Result</th>
<th>Comment</th></tr>
+     *   <tr><td>0.123456</td> <td>0.123</td>       <td>true</td>
  <td>Differ on in digits not specified by {@code approximate}.</td></tr>
+     *   <tr><td>0.123456</td> <td>0.123000</td>    <td>true</td>
  <td>This method can no distinguish missing digits from trailing zeros.</td></tr>
+     *   <tr><td>0.123456</td> <td>0.123001</td>    <td>false</td>
 <td>No missing digits, and some of them differ.</td></tr>
+     *   <tr><td>0.123</td>    <td>0.123456</td>    <td>false</td>
 <td>{@code approximate} and {@code accurate} can not be interchanged.</td></tr>
+     * </table>
+     *
+     * <div class="note"><b>Use case:</b>
+     * this method is useful when {@code approximate} is a number parsed by {@link Double#parseDouble(String)}
+     * and the data producer may have rounded too many fraction digits when formatting the
numbers.
+     * In some cases we can suspect what the real value may be and want to ensure that a
replacement
+     * would not contradict the provided value. This happen for example in Well Known Text
format,
+     * where the following element is sometime written with the conversion factor rounded:
+     *
+     * {@preformat wkt
+     *   AngleUnit["degree", 0.017453292519943295]      // Expected
+     *   AngleUnit["degree", 0.01745329252]             // Given by some providers
+     * }
+     * </div>
+     *
+     * @param  accurate     the most accurate number.
+     * @param  approximate  the number which may have missing decimal fraction digits.
+     * @return whether the two number are equal, ignoring missing decimal fraction digits
in {@code approximate}.
+     *
+     * @since 1.0
+     */
+    public static boolean equalsIgnoreMissingFractionDigits(double accurate, double approximate)
{
+        final double delta = Math.abs(accurate - approximate);
+        if (delta < 1) {
+            /*
+             * Compute the position of the first digit that differ, expressed as a power
of 10.
+             * For example if the numbers are 0.123 and 0.12378, then the first digit to
differ
+             * is 7 at position 10⁻⁴. Consequently the position of the last same digit
is 10⁻³.
+             * Dividing numbers by that last position result in numbers where all the different
+             * digits are fraction digits (123 and 123.78 in above example).
+             */
+            int p = Numerics.toExp10(MathFunctions.getExponent(delta));     // Rounded twice
toward floor (may be too low).
+            p = Math.max(p - (EXPONENT_FOR_ZERO + 1), 0);                   // Convert to
index in POW10 array.
+            if (p+1 < POW10.length && POW10[p+1] <= delta) p++;           
 // If p was too low, adjust.
+            p = (-2*EXPONENT_FOR_ZERO - 3) - p;                             // Index of power
of opposite sign - 1.
+            if (p >= 0 && p < POW10.length) {
+                double scale = POW10[p];                                    // Factor for
moving difference to fraction digits.
+                assert delta*scale >= 0.1 : delta;
+                final double diffInFractions = approximate * scale;
+                /*
+                 * The difference should not be in any digit provided by 'approximate'.
+                 * This means that after we moved the difference in fraction digits,
+                 * the approximate number should have no such fractions. We use 1 ULP
+                 * tolerance because the string representation of 'double' type has a
+                 * 0.5 ULP accuracy and the multiplication adds a 0.5 ULP rounding error.
+                 */
+                approximate = Math.rint(diffInFractions);
+                if (Math.abs(approximate - diffInFractions) <= Math.ulp(diffInFractions))
{
+                    /*
+                     * At this point we determined that all difference (now stored as fraction
digits)
+                     * are decimal fraction digits that were not specified in the approximate
number.
+                     * We will compare the approximate number with the accurate one ignoring
those digits,
+                     * but before doing so we may need to adjust too aggressive scale factor.
For example
+                     * if the approximate number is 0.123 and the accurate one is 0.123004,
then the scale
+                     * factor of 100000 is too aggressive; it should be 1000.
+                     */
+                    while (approximate % 10 == 0 && scale >= 10) {
+                        approximate /= 10;
+                        scale /= 10;
+                    }
+                    accurate *= scale;
+                    return Math.abs(approximate - accurate) <= 0.5;
+                }
+            }
+        }
+        return Double.doubleToLongBits(accurate) == Double.doubleToLongBits(approximate);
+    }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java
b/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java
index 441f575..1eb8ea6 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/math/DecimalFunctionsTest.java
@@ -33,7 +33,7 @@ import static org.apache.sis.math.DecimalFunctions.*;
  * Tests the {@link DecimalFunctions} static methods.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.4
+ * @version 1.0
  * @since   0.4
  * @module
  */
@@ -267,4 +267,30 @@ public final strictfp class DecimalFunctionsTest extends TestCase {
         assertEquals("Expected no rounding", 14, fractionDigitsForValue( 179.12499999999824,
2));
         assertEquals("Expected no rounding", 14, fractionDigitsForValue( 179.12499997999999,
3));
     }
+
+    /**
+     * Tests {@link DecimalFunctions#equalsIgnoreMissingFractionDigits(double, double)}.
+     * This test uses the conversion factor from degrees to radians as a use case.
+     * This factor is written as {@code ANGLEUNIT["degree", 0.01745329252]} in some
+     * Well Known Texts, while we expect 7 more digits for IEEE 754 double precision.
+     *
+     * @see <a href="https://issues.apache.org/jira/browse/SIS-377">SIS-377</a>
+     */
+    @Test
+    public void testEqualsIgnoreMissingFractionDigits() {
+        // Examples given in equalsIgnoreMissingFractionDigits comments.
+        assertTrue (equalsIgnoreMissingFractionDigits(0.123456, 0.123));
+        assertFalse(equalsIgnoreMissingFractionDigits(0.12378,  0.123));
+        assertTrue (equalsIgnoreMissingFractionDigits(0.12378,  0.124));
+        assertTrue (equalsIgnoreMissingFractionDigits(0.123004, 0.123));
+        assertTrue (equalsIgnoreMissingFractionDigits(0.123001, 0.123));
+        assertFalse(equalsIgnoreMissingFractionDigits(0.123456, 0.123001));
+        assertFalse(equalsIgnoreMissingFractionDigits(0.123,    0.123001));
+        assertFalse(equalsIgnoreMissingFractionDigits(0.123,    0.123456));
+
+        // Required for SIS-377 fix.
+        assertTrue (equalsIgnoreMissingFractionDigits(0.017453292519943295, 0.01745329252));
+        assertFalse(equalsIgnoreMissingFractionDigits(0.017453292519943295, 0.01745329251));
+        assertFalse(equalsIgnoreMissingFractionDigits(0.017453292519943295, 0.01745329253));
+    }
 }
diff --git a/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java b/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java
index 8c551bc..93bb208 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/test/Assert.java
@@ -86,7 +86,7 @@ public strictfp class Assert extends org.opengis.test.Assert {
     public static void assertAlmostEquals(final Object expected, final Object actual) {
         assertFalse("Shall not be strictly equals",          Utilities.deepEquals(expected,
actual, ComparisonMode.STRICT));
         assertFalse("Shall be slightly different",           Utilities.deepEquals(expected,
actual, ComparisonMode.IGNORE_METADATA));
-        assertTrue ("Shall be approximately equals",       Utilities.deepEquals(expected,
actual, ComparisonMode.DEBUG));
+        assertTrue ("Shall be approximately equals",         Utilities.deepEquals(expected,
actual, ComparisonMode.DEBUG));
         assertTrue ("DEBUG inconsistent with APPROXIMATIVE", Utilities.deepEquals(expected,
actual, ComparisonMode.APPROXIMATIVE));
     }
 
@@ -98,7 +98,7 @@ public strictfp class Assert extends org.opengis.test.Assert {
      * @param  actual    the actual object.
      */
     public static void assertEqualsIgnoreMetadata(final Object expected, final Object actual)
{
-        assertTrue("Shall be approximately equals",       Utilities.deepEquals(expected,
actual, ComparisonMode.DEBUG));
+        assertTrue("Shall be approximately equals",         Utilities.deepEquals(expected,
actual, ComparisonMode.DEBUG));
         assertTrue("DEBUG inconsistent with APPROXIMATIVE", Utilities.deepEquals(expected,
actual, ComparisonMode.APPROXIMATIVE));
         assertTrue("Shall be equal, ignoring metadata",     Utilities.deepEquals(expected,
actual, ComparisonMode.IGNORE_METADATA));
     }


Mime
View raw message