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 <[email protected]>
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")} < {@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));
}