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 <[email protected]> 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
