sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 02/03: Provide a way to compute the number of fraction digits to show based on the desired accuracy.
Date Sat, 18 May 2019 13:15:18 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

commit 3f5a29f8c161d7d69a9f79c624d90383d9c8a85c
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sat May 18 15:09:55 2019 +0200

    Provide a way to compute the number of fraction digits to show based on the desired accuracy.
---
 .../org/apache/sis/geometry/CoordinateFormat.java  | 195 ++++++++++++++++++++-
 .../apache/sis/geometry/CoordinateFormatTest.java  |  15 ++
 .../org/apache/sis/internal/util/Numerics.java     |  15 ++
 .../java/org/apache/sis/measure/AngleFormat.java   |  73 +++++++-
 .../org/apache/sis/measure/AngleFormatTest.java    |  22 ++-
 5 files changed, 316 insertions(+), 4 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
index 7ae3bdb..7e851f9 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/geometry/CoordinateFormat.java
@@ -40,8 +40,13 @@ import org.opengis.referencing.cs.CoordinateSystem;
 import org.opengis.referencing.cs.CoordinateSystemAxis;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.TemporalCRS;
+import org.opengis.referencing.datum.Ellipsoid;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.internal.referencing.ReferencingUtilities;
+import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.util.LocalizedParseException;
 import org.apache.sis.internal.util.Numerics;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
@@ -77,7 +82,7 @@ import org.apache.sis.io.CompoundFormat;
  * transform the position} before to format it.</p>
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see AngleFormat
  * @see org.apache.sis.measure.UnitFormat
@@ -272,11 +277,18 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
          * Otherwise (if a CRS is given), infer the format subclasses from the axes.
          */
         final CoordinateSystem cs = crs.getCoordinateSystem();
+        if (cs == null) {
+            return;                                    // Paranoiac check (should never be
null).
+        }
         final int dimension = cs.getDimension();
         final byte[]   types   = new byte  [dimension];
         final Format[] formats = new Format[dimension];
         for (int i=0; i<dimension; i++) {
             final CoordinateSystemAxis axis = cs.getAxis(i);
+            if (axis == null) {                                               // Paranoiac
check.
+                formats[i] = getFormat(Number.class);
+                continue;
+            }
             final Unit<?> unit = axis.getUnit();
             /*
              * Formatter for angular units. Target unit is DEGREE_ANGLE.
@@ -370,6 +382,187 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
     }
 
     /**
+     * Adjusts the number of fraction digits to show in coordinates for achieving the given
precision.
+     * The {@link NumberFormat} and {@link AngleFormat} are configured for coordinates expressed
in the
+     * {@linkplain #getDefaultCRS() default coordinate reference system} defined at the moment
this method is invoked.
+     * The number of fraction digits is <em>not</em> updated if a different CRS
is specified after this method call
+     * or if the coordinates to format are associated to a different CRS.
+     *
+     * <p>The given resolution will be converted to the units used by coordinate system
axes. For example if a 10 metres
+     * resolution is specified but the {@linkplain #getDefaultCRS() default CRS} axes use
kilometres, then this method
+     * converts the resolution to 0.01 kilometre and uses that value for inferring that coordinates
should be formatted
+     * with 2 fraction digits. If the resolution is specified in an angular units such as
degrees, this method uses the
+     * {@linkplain org.apache.sis.referencing.datum.DefaultEllipsoid#getAuthalicRadius()
ellipsoid authalic radius} for
+     * computing an equivalent resolution in linear units. For example if the ellipsoid of
default CRS is WGS84,
+     * then this method considers a resolution of 1 second of angle as equivalent to a resolution
of about 31 meters.
+     * Conversions work also in the opposite direction (from linear to angular units) and
are also used for choosing
+     * which angle fields (degrees, minutes or seconds) to show.</p>
+     *
+     * @param  resolution  the desired resolution.
+     * @param  unit        unit of the desired resolution.
+     *
+     * @see NumberFormat#setMaximumFractionDigits(int)
+     * @see AngleFormat#setPrecision(double, boolean)
+     *
+     * @since 1.0
+     */
+    @SuppressWarnings("null")
+    public void setPrecision(double resolution, Unit<?> unit) {
+        ArgumentChecks.ensureFinite("resolution", resolution);
+        ArgumentChecks.ensureNonNull("unit", unit);
+        resolution = Math.abs(resolution);
+        if (Units.isTemporal(unit)) {
+            return;                                 // Setting temporal resolution is not
yet implemented.
+        }
+        /*
+         * If the given resolution is linear (for example in metres), compute an equivalent
resolution in degrees
+         * assuming a sphere of radius computed from the CRS.  Conversely if the resolution
is angular (typically
+         * in degrees), computes an equivalent linear resolution. For all other kind of units,
do nothing.
+         */
+        Resolution specified = new Resolution(resolution, unit, Units.isAngular(unit));
+        Resolution related   = null;
+        IncommensurableException error = null;
+        if (specified.isAngular || Units.isLinear(unit)) try {
+            related = specified.related(ReferencingUtilities.getEllipsoid(defaultCRS));
+        } catch (IncommensurableException e) {
+            error = e;
+        }
+        /*
+         * We now have the requested resolution in both linear and angular units, if equivalence
has been established.
+         * Convert those resolutions to the units actually used by the CRS. If some axes
use different units, keep the
+         * units which result in the finest resolution.
+         */
+        boolean relatedUsed = false;
+        if (defaultCRS != null) {
+            final CoordinateSystem cs = defaultCRS.getCoordinateSystem();
+            if (cs != null) {                                                   // Paranoiac
check (should never be null).
+                final int dimension = cs.getDimension();
+                for (int i=0; i<dimension; i++) {
+                    final CoordinateSystemAxis axis = cs.getAxis(i);
+                    if (axis != null) {                                         // Paranoiac
check.
+                        final Unit<?> axisUnit = axis.getUnit();
+                        if (axisUnit != null) try {
+                            final double maxValue = Math.max(Math.abs(axis.getMinimumValue()),
+                                                             Math.abs(axis.getMaximumValue()));
+                            if (!specified.forAxis(maxValue, axisUnit) && related
!= null) {
+                                relatedUsed |= related.forAxis(maxValue, axisUnit);
+                            }
+                        } catch (IncommensurableException e) {
+                            if (error == null) error = e;
+                            else error.addSuppressed(e);
+                        }
+                    }
+                }
+            }
+        }
+        if (error != null) {
+            Logging.unexpectedException(Logging.getLogger(Loggers.MEASURE), CoordinateFormat.class,
"setPrecision", error);
+        }
+        specified.setPrecision(this);
+        if (relatedUsed) {
+            related.setPrecision(this);
+        }
+    }
+
+    /**
+     * Desired resolution in a given units, together with methods for converting to the units
of a coordinate system axis.
+     * This is a helper class for {@link CoordinateFormat#setPrecision(double, Unit)} implementation.
An execution of that
+     * method typically creates two instances of this {@code Resolution} class: one for the
resolution in metres and another
+     * one for the resolution in degrees.
+     */
+    private static final class Resolution {
+        /** The desired resolution in the unit of measurement given by {@link #unit}. */
+        private double resolution;
+
+        /** Maximal absolute value that we may format in unit of measurement given by {@link
#unit}. */
+        private double magnitude;
+
+        /** Unit of measurement of {@link #resolution} or {@link #magnitude}. */
+        private Unit<?> unit;
+
+        /** Whether {@link #unit} is an angular unit. */
+        final boolean isAngular;
+
+        /** Creates a new instance initialized to the given resolution. */
+        Resolution(final double resolution, final Unit<?> unit, final boolean isAngular)
{
+            this.resolution = resolution;
+            this.unit       = unit;
+            this.isAngular  = isAngular;
+        }
+
+        /**
+         * If this resolution is in metres, returns equivalent resolution in degrees. Or
conversely if this resolution
+         * is in degrees, returns an equivalent resolution in metres. Other linear and angular
units are accepted too;
+         * they will be converted as needed.
+         *
+         * @param  ellipsoid  the ellipsoid, or {@code null} if none.
+         * @return the related resolution, or {@code null} if none.
+         */
+        Resolution related(final Ellipsoid ellipsoid) throws IncommensurableException {
+            final double radius = Formulas.getAuthalicRadius(ellipsoid);
+            if (radius > 0) {                                       // Indirectly filter
null ellipsoid.
+                Unit<?> relatedUnit = ellipsoid.getAxisUnit();      // Angular if `unit`
is linear, or linear if `unit` is angular.
+                if (relatedUnit != null) {                          // Paranoiac check (should
never be null).
+                    double related;
+                    if (isAngular) {
+                        // Linear resolution  =  angular resolution in radians  ×  radius.
+                        related = unit.getConverterToAny(Units.RADIAN).convert(resolution)
* radius;
+                    } else {
+                        // Angular resolution in radians  =  linear resolution  /  radius
+                        related = Math.toDegrees(unit.getConverterToAny(relatedUnit).convert(resolution)
/ radius);
+                        relatedUnit = Units.DEGREE;
+                    }
+                    return new Resolution(related, relatedUnit, !isAngular);
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Adjusts the resolution units for the given coordinate system axis. This methods
select the units which
+         * result in the smallest absolute value of {@link #resolution}.
+         *
+         * @param  maxValue  the maximal absolute value that a coordinate on the axis may
have.
+         * @param  axisUnit  {@link CoordinateSystemAxis#getUnit()}.
+         * @return whether the given axis unit is compatible with the expected unit.
+         */
+        boolean forAxis(double maxValue, final Unit<?> axisUnit) throws IncommensurableException
{
+            if (!axisUnit.isCompatible(unit)) {
+                return false;
+            }
+            final UnitConverter c = unit.getConverterToAny(axisUnit);
+            final double r = Math.abs(c.convert(resolution));
+            if (r < resolution) {
+                resolution = r;                                         // To units producing
the smallest value.
+                unit = axisUnit;
+            } else {
+                maxValue = Math.abs(c.inverse().convert(maxValue));     // From axis units
to selected units.
+            }
+            if (maxValue > magnitude) {
+                magnitude = maxValue;
+            }
+            return true;
+        }
+
+        /**
+         * Configures the {@link NumberFormat} or {@link AngleFormat} for a number of fraction
digits
+         * sufficient for the given resolution.
+         */
+        void setPrecision(final CoordinateFormat owner) {
+            final Format format = owner.getFormat(isAngular ? Angle.class : Number.class);
+            if (format instanceof NumberFormat) {
+                if (resolution == 0) resolution = 1E-6;                     // Arbitrary
value.
+                final int p = Numerics.suggestFractionDigits(resolution);
+                final int m = Numerics.suggestFractionDigits(Math.ulp(magnitude));
+                ((NumberFormat) format).setMinimumFractionDigits(Math.min(p, m));
+                ((NumberFormat) format).setMaximumFractionDigits(p);
+            } else if (format instanceof AngleFormat) {
+                ((AngleFormat) format).setPrecision(resolution, true);
+            }
+        }
+    }
+
+    /**
      * Returns the pattern for number, angle or date fields. The given {@code valueType}
should be
      * {@code Number.class}, {@code Angle.class}, {@code Date.class} or a sub-type of the
above.
      * This method may return {@code null} if the underlying format can not provide a pattern.
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java
index ff58b15..7a30075 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/geometry/CoordinateFormatTest.java
@@ -24,6 +24,7 @@ import java.text.ParseException;
 import java.io.IOException;
 import org.opengis.geometry.DirectPosition;
 import org.apache.sis.measure.Angle;
+import org.apache.sis.measure.Units;
 import org.apache.sis.referencing.crs.HardCodedCRS;
 import org.apache.sis.test.mock.VerticalCRSMock;
 import org.apache.sis.test.DependsOnMethod;
@@ -256,6 +257,20 @@ public final strictfp class CoordinateFormatTest extends TestCase {
     }
 
     /**
+     * Tests {@link CoordinateFormat#setPrecision(double, Unit)}.
+     */
+    @Test
+    public void testSetPrecision() {
+        final CoordinateFormat format = new CoordinateFormat(Locale.FRANCE, null);
+        final DirectPosition2D pos = new DirectPosition2D(40.123456789, 9.87654321);
+        format.setDefaultCRS(HardCodedCRS.WGS84_φλ);
+        format.setPrecision(0.01, Units.GRAD);
+        assertEquals("40°07,4′N 9°52,6′E", format.format(pos));
+        format.setPrecision(0.01, Units.METRE);
+        assertEquals("40°07′24,4444″N 9°52′35,5556″E", format.format(pos));
+    }
+
+    /**
      * Tests {@link CoordinateFormat#clone()}, then verifies that the clone has the same
configuration
      * than the original object.
      */
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
index 7e21708..d59513b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/Numerics.java
@@ -26,6 +26,7 @@ import org.apache.sis.util.Static;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.math.DecimalFunctions;
+import org.apache.sis.math.MathFunctions;
 import org.apache.sis.math.Statistics;
 import org.apache.sis.math.Vector;
 import org.opengis.referencing.operation.Matrix;    // For javadoc
@@ -500,6 +501,20 @@ public final class Numerics extends Static {
     }
 
     /**
+     * Suggests an amount of fraction digits for data having the given precision.
+     *
+     * @param  precision  desired precision.
+     * @return suggested amount of fraction digits for the given precision.
+     *
+     * @since 1.0
+     */
+    public static int suggestFractionDigits(final double precision) {
+        int p = toExp10(MathFunctions.getExponent(precision));
+        if (MathFunctions.pow10(p+1) <= abs(precision)) p++;
+        return Math.max(0, -p);
+    }
+
+    /**
      * Suggests an amount of fraction digits for data having the given statistics.
      * This method uses heuristic rules that may be modified in any future SIS version.
      *
diff --git a/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
index 7055d3b..f56d5f9 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/AngleFormat.java
@@ -31,6 +31,7 @@ import org.apache.sis.util.Localized;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.util.Strings;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.internal.util.LocalizedParseException;
 
 import static java.lang.Double.NaN;
@@ -117,7 +118,7 @@ import static org.apache.sis.math.DecimalFunctions.fractionDigitsForDelta;
  * </div>
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  *
  * @see Angle
  * @see Latitude
@@ -415,7 +416,7 @@ public class AngleFormat extends Format implements Localized {
         degreesFieldWidth   = 1;
         minutesFieldWidth   = 2;
         secondsFieldWidth   = 2;
-        fractionFieldWidth  = 16;  // Number of digits for accurate representation of 1″
ULP.
+        fractionFieldWidth  = 16;       // Number of digits for representation up to Math.ulp(1).
         optionalFields      = (1 << DEGREES_FIELD) | (1 << MINUTES_FIELD) | (1
<< SECONDS_FIELD);
         degreesSuffix       = "°";
         minutesSuffix       = "′";
@@ -790,6 +791,74 @@ public class AngleFormat extends Format implements Localized {
     }
 
     /**
+     * Adjusts the number of fraction digits, and optionally the visible fields, for the
given precision.
+     * If the {@code allowFieldChanges} argument is {@code false}, then this method adjusts
only the
+     * {@linkplain #setMinimumFractionDigits(int) minimum} and {@linkplain #setMinimumFractionDigits(int)
+     * maximum fraction digits} in order to show angles with at least the specified precision.
+     * But if the {@code allowFieldChanges} argument is {@code true}, then this method may
change the
+     * set of fields (degrees, minutes or seconds) to show before to adjust the number of
fraction digits.
+     * In that case, this method selects the first row in the following table where the precision
matches the condition:
+     *
+     * <table class="sis">
+     *   <caption>Selected fields for given precision</caption>
+     *   <tr><th>Precision</th> <th>Fields</th></tr>
+     *   <tr><td>≧ 1°</td>      <td>D°</td></tr>
+     *   <tr><td>≧ ⅒°</td>      <td>D.d°</td></tr>
+     *   <tr><td>≧ 1′</td>      <td>D°MM′</td></tr>
+     *   <tr><td>≧ ⅒′</td>      <td>D°MM.m′</td></tr>
+     *   <tr><td>≧ 1″</td>      <td>D°MM′SS″</td></tr>
+     *   <tr><td>≧ ⅒″</td>      <td>D°MM′SS.s″</td></tr>
+     *   <tr><td>other</td>     <td>D°MM′SS.ss…″</td></tr>
+     * </table>
+     *
+     * @param  resolution  the desired angle resolution, in decimal degrees.
+     * @param  allowFieldChanges  whether this method is allowed to change the set of fields
(degrees, minutes or seconds).
+     *
+     * @since 1.0
+     */
+    @SuppressWarnings("PointlessBitwiseExpression")
+    public void setPrecision(double resolution, final boolean allowFieldChanges) {
+        ArgumentChecks.ensureFinite("resolution", resolution);
+        resolution = Math.abs(resolution);
+        if (resolution == 0) {
+            // Restore same setting than constructor.
+            resolution = 1E-16;                                                     // Math.ulp(1)
≈ 2E-16.
+        }
+        final int significandFractionDigits;
+        if (allowFieldChanges ? resolution >= 0.1 : minutesFieldWidth == 0) {
+            significandFractionDigits = 14;                                         // Math.ulp(360)
≈ 6E-14.
+            if (allowFieldChanges) {
+                minutesFieldWidth = 0;
+                secondsFieldWidth = 0;
+                optionalFields = (1 << MINUTES_FIELD) | (1 << SECONDS_FIELD);
+            }
+        } else {
+            resolution = Math.nextUp(resolution * 60);                              // nextUp(…)
in case of 0.5 ULP error.
+            if (allowFieldChanges ? resolution >= 0.1 : secondsFieldWidth == 0) {
+                significandFractionDigits = 12;                                     // Math.ulp(360*60)
≈ 4E-12.
+                if (allowFieldChanges) {
+                    if (minutesFieldWidth == 0) {
+                        minutesFieldWidth = 2;
+                    }
+                    secondsFieldWidth = 0;
+                    optionalFields = (1 << SECONDS_FIELD);
+                }
+            } else {
+                resolution = Math.nextUp(resolution * 60);                          // nextUp(…)
in case of 0.5 ULP error.
+                significandFractionDigits = 10;                                     // Math.ulp(360*60*60)
≈ 2E-10.
+                if (allowFieldChanges) {
+                    if (minutesFieldWidth == 0) minutesFieldWidth = 2;
+                    if (secondsFieldWidth == 0) secondsFieldWidth = 2;
+                    optionalFields = 0;
+                }
+            }
+        }
+        final int p = Numerics.suggestFractionDigits(resolution);
+        fractionFieldWidth = (byte) p;
+        minimumFractionDigits = (byte) Math.min(significandFractionDigits, p);
+    }
+
+    /**
      * Returns the minimum number of digits allowed in the fraction portion of the last field.
      * This value can be set by the repetition of {@code 'd'}, {@code 'm'} or {@code 's'}
symbol
      * in the pattern.
diff --git a/core/sis-utility/src/test/java/org/apache/sis/measure/AngleFormatTest.java b/core/sis-utility/src/test/java/org/apache/sis/measure/AngleFormatTest.java
index 4bf43ce..fe4fab2 100644
--- a/core/sis-utility/src/test/java/org/apache/sis/measure/AngleFormatTest.java
+++ b/core/sis-utility/src/test/java/org/apache/sis/measure/AngleFormatTest.java
@@ -34,7 +34,7 @@ import static org.apache.sis.test.TestUtilities.*;
  * Tests parsing and formatting done by the {@link AngleFormat} class.
  *
  * @author  Martin Desruisseaux (MPO, IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.3
  * @module
  */
@@ -322,6 +322,26 @@ public final strictfp class AngleFormatTest extends TestCase {
     }
 
     /**
+     * Tests {@link AngleFormat#setPrecision(double, boolean)}.
+     */
+    @Test
+    public void testSetPrecision() {
+        final AngleFormat f = new AngleFormat(Locale.CANADA);
+        f.setPrecision(1,        true); assertEquals("D°",         f.toPattern());
+        f.setPrecision(1./10,    true); assertEquals("D.d°",       f.toPattern());
+        f.setPrecision(1./60,    true); assertEquals("D°MM′",      f.toPattern());
+        f.setPrecision(1./600,   true); assertEquals("D°MM.m′",    f.toPattern());
+        f.setPrecision(1./3600,  true); assertEquals("D°MM′SS″",   f.toPattern());
+        f.setPrecision(1./4000,  true); assertEquals("D°MM′SS.s″", f.toPattern());
+        f.setPrecision(1./100,   true); assertEquals("D°MM.m′",    f.toPattern());
+        f.setPrecision(1./8000, false); assertEquals("D°MM.mmm′",  f.toPattern());
+        f.setPrecision(1./1000, false); assertEquals("D°MM.mm′",   f.toPattern());
+        f.setPrecision(10,       true); assertEquals("D°",         f.toPattern());
+        f.setPrecision(1./1000, false); assertEquals("D.ddd°",     f.toPattern());
+        f.setPrecision(1./1001, false); assertEquals("D.dddd°",    f.toPattern());
+    }
+
+    /**
      * Tests the field position while formatting an angle.
      */
     @Test


Mime
View raw message