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: Make CoordinateFormat.parse(…) consistent with CoordinateFormat.format(…) regarding direction and accuracy information.
Date Mon, 27 Apr 2020 13:15:58 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 38ab0ef  Make CoordinateFormat.parse(…) consistent with CoordinateFormat.format(…)
regarding direction and accuracy information.
38ab0ef is described below

commit 38ab0ef676d60067362f5fc4885c641465dbbeff
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Mon Apr 27 15:12:21 2020 +0200

    Make CoordinateFormat.parse(…) consistent with CoordinateFormat.format(…) regarding
direction and accuracy information.
---
 .../org/apache/sis/geometry/CoordinateFormat.java  | 257 +++++++++++++++------
 .../apache/sis/geometry/CoordinateFormatTest.java  |  45 +++-
 .../java/org/apache/sis/measure/UnitFormat.java    |   2 +-
 3 files changed, 230 insertions(+), 74 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 f872b91..a7522e6 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
@@ -303,8 +303,11 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
      * Units symbols to append after coordinate values for each dimension, including leading
space.
      * This is used only for coordinates to be formatted as ordinary numbers with {@link
NumberFormat}.
      * This array is non-null only if at least one dimension needs to format its coordinates
that way.
-     * The unit symbol may be followed by axis direction symbol used for axes on the ground
-     * ("E", "N", "SW", <i>etc.</i>) so the complete symbol may be for example
"km E".
+     *
+     * <p>Units symbols may be followed by axis {@linkplain #directionSymbols direction
symbols} used
+     * for axes on the ground ("E", "N", "SW", <i>etc.</i>) so the complete symbol
may be for example
+     * "km E". Those direction symbols are stored in a separated array; they are not part
of elements
+     * of this {@code unitSymbols} array.</p>
      *
      * <p>This array is created by {@link #createFormats(CoordinateReferenceSystem)},
which is invoked before
      * parsing or formatting in a different CRS than last operation, and stay unmodified
after creation.</p>
@@ -312,6 +315,22 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
     private transient String[] unitSymbols;
 
     /**
+     * Directions symbols ("E", "N", "SW", <i>etc.</i>) to append after coordinate
values for some dimensions,
+     * including leading space. This is used only for some coordinates formatted with {@link
NumberFormat}.
+     * This array is non-null only if at least one dimension needs to format its coordinates
that way.
+     * The length of this array is twice the number of dimensions. The array contains this
tuple:
+     *
+     * <ol>
+     *   <li>Symbol of axis direction (at even indices)</li>
+     *   <li>Symbol in the direction opposite to axis direction (at odd indices)</li>
+     * </ol>
+     *
+     * <p>This array is created by {@link #createFormats(CoordinateReferenceSystem)},
which is invoked before
+     * parsing or formatting in a different CRS than last operation, and stay unmodified
after creation.</p>
+     */
+    private transient String[] directionSymbols;
+
+    /**
      * Text to append to the coordinate values for giving an indication about accuracy, or
{@code null} if none.
      * Example: " ± 1 m" (note the leading space). This is determined by the {@link #groundAccuracy}
value.
      * If {@link #desiredPrecisions} array is non-null, then accuracy is shown only if a
precision is smaller.
@@ -432,6 +451,7 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
         units              = null;
         toFormatUnit       = null;
         unitSymbols        = null;
+        directionSymbols   = null;
         epochs             = null;
         negate             = 0L;
         lastCRS            = crs;
@@ -512,28 +532,25 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
              * a non-zero value by previous case. If not, the default value (zero) is the
one we want.
              */
             formats[i] = getFormat(Number.class);
-            StringBuilder symbol = null;
             if (unit != null) {
                 if (units == null) {
                     units = new Unit<?>[dimension];
                 }
                 units[i] = unit;
-                final String s = getFormat(Unit.class).format(unit);
-                if (!s.isEmpty()) {
-                    symbol = new StringBuilder().append(QuantityFormat.SEPARATOR).append(s);
+                final String symbol = getFormat(Unit.class).format(unit);
+                if (!symbol.isEmpty()) {
+                    if (unitSymbols == null) {
+                        unitSymbols = new String[dimension];
+                    }
+                    unitSymbols[i] = QuantityFormat.SEPARATOR + symbol;
                 }
             }
             if (AxisDirections.isCompass(direction)) {
-                if (symbol == null) {
-                    symbol = new StringBuilder();
-                }
-                symbol.append(Characters.NO_BREAK_SPACE).append(CharSequences.camelCaseToAcronym(direction.identifier()));
-            }
-            if (symbol != null) {
-                if (unitSymbols == null) {
-                    unitSymbols = new String[dimension];
+                if (directionSymbols == null) {
+                    directionSymbols = new String[dimension * 2];
                 }
-                unitSymbols[i] = symbol.toString();
+                directionSymbols[i*2]     = symbol(direction);
+                directionSymbols[i*2 + 1] = symbol(AxisDirections.opposite(direction));
             }
         }
         this.types    = types;
@@ -542,6 +559,15 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
     }
 
     /**
+     * Returns the symbol ("E", "N", "SW", <i>etc.</i>) for given axis direction.
+     */
+    private static String symbol(final AxisDirection direction) {
+        // Following cast uses or knowledge of `camelCaseToAcronym` implementation.
+        return ((StringBuilder) CharSequences.camelCaseToAcronym(direction.identifier()))
+                .insert(0, Characters.NO_BREAK_SPACE).toString();
+    }
+
+    /**
      * Returns a clone of the format at the specified dimension. Format instances are cloned
only when first needed.
      * The clones are needed when we want to change the format pattern (number of fraction
digits, <i>etc.</i>) for
      * only one dimension, without impacting other dimensions that may use the same format.
@@ -685,7 +711,7 @@ public class CoordinateFormat extends CompoundFormat<DirectPosition>
{
                 if (!(p < Double.POSITIVE_INFINITY)) p = 0;                 // Use ! for
replacing NaN.
                 if (desiredPrecisions[i] != (desiredPrecisions[i] = p)) {
                     // Precision changed. Keep format up to date.
-                    if (isPrecisionApplied && i < formats.length) {
+                    if (isPrecisionApplied) {
                         applyPrecision(i);
                     }
                 }
@@ -1296,18 +1322,27 @@ abort:  if (dimensions != 0 && groundAccuracy != null) try
{
         }
         /*
          * The format to use for each coordinate has been computed by `configure`. The format
array length
-         * should match the number of dimensions in the given position if the DirectPosition
is consistent
-         * with its CRS, but we will nevertheless verify has a paranoiac check.  If there
is no CRS, or if
-         * the DirectPosition dimension is (illegally) greater than the CRS dimension, then
we will format
-         * the coordinate as a number.
+         * should match the number of dimensions in the given position assuming that the
DirectPosition is
+         * consistent with its CRS. If there is no CRS, or if the DirectPosition dimension
is (illegally)
+         * greater than the CRS dimension, then we will format the coordinate as a plain
number.
          */
         final int dimension = position.getDimension();
         for (int i=0; i < dimension; i++) {
             double value = position.getOrdinate(i);
-            final Object object;
+            final Object valueObject;
+            final String unit, direction;
             final Format f;
-            if (formats != null && i < formats.length) {
+            if (formats != null && i < formats.length) {    // The < check
is a safety against illegal DirectPosition.
                 f = formats[i];
+                unit = (unitSymbols != null) ? unitSymbols[i] : null;
+                if (directionSymbols == null) {
+                    direction = null;
+                } else if (value < 0) {
+                    value = -value;
+                    direction = directionSymbols[i*2 + 1];
+                } else {
+                    direction = directionSymbols[i*2];
+                }
                 if (isNegative(i)) {
                     value = -value;
                 }
@@ -1318,15 +1353,16 @@ abort:  if (dimensions != 0 && groundAccuracy != null) try
{
                     }
                 }
                 switch (types[i]) {
-                    default:        object = Double.valueOf(value); break;
-                    case LONGITUDE: object = new Longitude (value); break;
-                    case LATITUDE:  object = new Latitude  (value); break;
-                    case ANGLE:     object = new Angle     (value); break;
-                    case DATE:      object = new Date(Math.addExact(Math.round(value), epochs[i]));
break;
+                    default:        valueObject = Double.valueOf(value); break;
+                    case LONGITUDE: valueObject = new Longitude (value); break;
+                    case LATITUDE:  valueObject = new Latitude  (value); break;
+                    case ANGLE:     valueObject = new Angle     (value); break;
+                    case DATE:      valueObject = new Date(Math.addExact(Math.round(value),
epochs[i])); break;
                 }
             } else {
-                object = value;
+                valueObject = value;
                 f = getDefaultFormat();
+                unit = direction = null;
             }
             /*
              * At this point we got the value to format together with the Format instance
to use.
@@ -1334,16 +1370,12 @@ abort:  if (dimensions != 0 && groundAccuracy != null) try
{
             if (i != 0) {
                 toAppendTo.append(separator);
             }
-            if (f.format(object, destination, dummy) != toAppendTo) {
+            if (f.format(valueObject, destination, dummy) != toAppendTo) {
                 toAppendTo.append(destination);
                 destination.setLength(0);
             }
-            if (unitSymbols != null && i < unitSymbols.length) {
-                final String symbol = unitSymbols[i];
-                if (symbol != null) {
-                    toAppendTo.append(symbol);
-                }
-            }
+            if (unit      != null) toAppendTo.append(unit);
+            if (direction != null) toAppendTo.append(direction);
         }
         /*
          * Finished to format the all coordinate values. Appends the accuracy if
@@ -1489,6 +1521,11 @@ skipSep:    if (i != 0) {
                 }
                 throw new LocalizedParseException(getLocale(), type, text, pos);
             }
+            /*
+             * The value part (number, angle or date) has been parsed successfully.
+             * Get the numerical value. The unit of measurement may not be the same
+             * than the one expected by the CRS (we will convert later).
+             */
             double value;
             if (object instanceof Angle) {
                 value = ((Angle) object).degrees();
@@ -1498,46 +1535,120 @@ skipSep:    if (i != 0) {
                 value = ((Number) object).doubleValue();
             }
             /*
-             * The conversions and sign reversal applied below shall be in exact reverse
order than
-             * in the 'format(…)' method. However we have one additional step compared
to format(…):
-             * the unit written after the coordinate value may not be the same than the unit
declared
-             * in the CRS axis, so we have to parse the unit and convert the value before
to apply
-             * the reverse of 'format(…)' steps.
+             * The value sign may need to be adjusted if the value is followed by a direction
symbol
+             * such as "N", "E" or "SW". Get the symbols that are allowed for current coordinate.
+             * We will check for their presence after the unit symbol, or immediately after
the value
+             * if there is no unit symbol.
+             */
+            String direction = null;
+            String opposite  = null;
+            if (directionSymbols != null) {
+                direction = directionSymbols[i*2    ];
+                opposite  = directionSymbols[i*2 + 1];
+            }
+            /*
+             * The unit written after the coordinate value may not be the same than the unit
declared
+             * in the CRS axis, so we have to parse the unit and convert the value before
to apply the
+             * change of sign.
              */
-            if (units != null) {
-                final Unit<?> target = units[i];
-                if (target != null) {
-                    final int base = subPos.getIndex();
-                    int index = base;
+            final Unit<?> target;
+parseUnit:  if (units != null && (target = units[i]) != null) {
+                final int base = subPos.getIndex();
+                int index = base;                       // Will become start index of unit
symbol.
+                /*
+                 * Skip whitespaces using Character.isSpaceChar(…), not Character.isWhitespace(…),
+                 * because we need to skip also the non-breaking space (Characters.NO_BREAK_SPACE).
+                 * If we can not parse the unit after those spaces, we will revert to the
original
+                 * position + spaces skipped (absence of unit will not be considered an error).
+                 */
+                int c;
+                for (;;) {
+                    if (index >= asString.length()) {
+                        break parseUnit;                // Found only spaces until end of
string.
+                    }
+                    c = asString.codePointAt(index);
+                    if (!Character.isSpaceChar(c)) break;
+                    index += Character.charCount(c);
+                }
+                /*
+                 * Now the `index` should be positioned on the first character of the unit
symbol.
+                 * Before to parse the unit, verify if a direction symbol is found after
the unit.
+                 * We need to do this check because unit symbol and direction symbol are
separated
+                 * by a no-break space, which causes `UnitFormat` to try to parse them together
as
+                 * a unique unit symbol.
+                 */
+                int stopAt = index;                     // Will become stop index of unit
symbol.
+                int nextAt = -1;                        // Will become start index of next
coordinate.
+searchDir:      if (direction != null) {
+                    do {
+                        stopAt += Character.charCount(c);
+                        if (stopAt >= asString.length()) {
+                            break searchDir;
+                        }
+                        c = asString.codePointAt(stopAt);
+                    } while (!Character.isSpaceChar(c));
                     /*
-                     * Skip whitespaces using Character.isSpaceChar(…), not Character.isWhitespace(…),
-                     * because we need to skip also the non-breaking space (Characters.NO_BREAK_SPACE).
-                     * If we can not parse the unit after those spaces, we will revert to
the original
-                     * position (absence of unit will not be considered an error).
+                     * Found the first space character, which may be a no-break space.
+                     * Check for direction symbol here. This strategy is based on the
+                     * fact that the direction symbol starts with a no-break space.
                      */
-                    while (index < asString.length()) {
-                        final int c = asString.codePointAt(index);
-                        if (Character.isSpaceChar(c)) {
-                            index += Character.charCount(c);
-                            continue;
-                        }
+                    if (asString.regionMatches(true, stopAt, direction, 0, direction.length()))
{
+                        nextAt = stopAt + direction.length();
+                    } else if (asString.regionMatches(true, stopAt, opposite, 0, opposite.length()))
{
+                        nextAt = stopAt + opposite.length();
+                        value = -value;
+                    }
+                }
+                /*
+                 * Parse the unit symbol now. The `nextAt` value determines whether a direction
symbol
+                 * has been found, in which case we need to exclude the direction from the
text parsed
+                 * by `UnitFormat`.
+                 */
+                final Format f = getFormat(Unit.class);
+                final Object unit;
+                try {
+                    if (nextAt < 0) {
                         subPos.setIndex(index);
-                        final Object unit = getFormat(Unit.class).parseObject(asString, subPos);
-                        if (unit == null) {
-                            subPos.setIndex(base);
-                            subPos.setErrorIndex(-1);
-                        } else try {
-                            value = ((Unit<?>) unit).getConverterToAny(target).convert(value);
-                        } catch (IncommensurableException e) {
-                            index += offset;
-                            pos.setIndex(start);
-                            pos.setErrorIndex(index);
-                            throw (ParseException) new ParseException(e.getMessage(), index).initCause(e);
-                        }
-                        break;
+                        unit = f.parseObject(asString, subPos);     // Let `UnitFormat` decide
where to stop parsing.
+                    } else {
+                        unit = f.parseObject(asString.substring(index, stopAt));
+                        subPos.setIndex(nextAt);
+                        direction = opposite = null;
                     }
+                    if (unit == null) {
+                        subPos.setIndex(base);
+                        subPos.setErrorIndex(-1);
+                    } else {
+                        value = ((Unit<?>) unit).getConverterToAny(target).convert(value);
+                    }
+                } catch (ParseException | IncommensurableException e) {
+                    index += offset;
+                    pos.setIndex(start);
+                    pos.setErrorIndex(index);
+                    if (e instanceof ParseException) {
+                        throw (ParseException) e;
+                    }
+                    throw (ParseException) new ParseException(e.getMessage(), index).initCause(e);
+                }
+            }
+            /*
+             * At this point either the unit of measurement has been parsed, or there is
no unit.
+             * If the direction symbol ("E", "N", "SW", etc.) has not been found before,
check now.
+             */
+            if (direction != null) {
+                int index = subPos.getIndex();
+                if (asString.regionMatches(true, index, direction, 0, direction.length()))
{
+                    index += direction.length();
+                } else if (asString.regionMatches(true, index, opposite, 0, opposite.length()))
{
+                    index += opposite.length();
+                    value = -value;
                 }
+                subPos.setIndex(index);
             }
+            /*
+             * The conversions and sign reversal applied below shall be in reverse order
+             * than the operations applied by the 'format(…)' method.
+             */
             if (toFormatUnit != null) {
                 final UnitConverter c = toFormatUnit[i];
                 if (c != null) {
@@ -1549,6 +1660,16 @@ skipSep:    if (i != 0) {
             }
             coordinates[i] = value;
         }
+        /*
+         * If accuracy information is appended after the coordinates (e.g. " ± 3 km"),
skip that text.
+         */
+        if (accuracyText != null) {
+            final int index = subPos.getIndex();
+            final int lg = accuracyText.length();
+            if (asString.regionMatches(true, index, accuracyText, 0, lg)) {
+                subPos.setIndex(index + lg);
+            }
+        }
         final GeneralDirectPosition position = new GeneralDirectPosition(coordinates);
         position.setCoordinateReferenceSystem(defaultCRS);
         return position;
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 b08f26c..3b5cd05 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
@@ -127,19 +127,36 @@ public final strictfp class CoordinateFormatTest extends TestCase {
     }
 
     /**
-     * Tests formatting a 2-dimensional projected coordinate.
+     * Tests formatting 2-dimensional projected coordinates.
      */
     @Test
     @DependsOnMethod("testFormatUnknownCRS")
     public void testFormatProjected() {
         final CoordinateFormat format = new CoordinateFormat(Locale.US, null);
         format.setDefaultCRS(HardCodedConversions.mercator());
-        DirectPosition2D position = new DirectPosition2D(-100, 300);
-        assertEquals("-100 m E 300 m N", format.format(position));
+        assertEquals("100 m W 300 m N", format.format(new DirectPosition2D(-100,
300)));
+        assertEquals("200 m E 100 m S", format.format(new DirectPosition2D(200, -100)));
     }
 
     /**
-     * Tests formatting a 4-dimensional geographic coordinate.
+     * Tests parsing 2-dimensional projected coordinates.
+     * This method is the converse of {@link #testFormatProjected()}.
+     *
+     * @throws ParseException if the parsing failed.
+     */
+    @Test
+    @DependsOnMethod("testParseUnknownCRS")
+    public void testParseProjected() throws ParseException {
+        final CoordinateFormat format = new CoordinateFormat(Locale.US, null);
+        format.setDefaultCRS(HardCodedConversions.mercator());
+        DirectPosition pos = format.parse("100 m W 300 m N", new ParsePosition(0));
+        assertArrayEquals(new double[] {-100, 300}, pos.getCoordinate(), STRICT);
+        pos = format.parse("200 m E 100 m S", new ParsePosition(0));
+        assertArrayEquals(new double[] {200, -100}, pos.getCoordinate(), STRICT);
+    }
+
+    /**
+     * Tests formatting 4-dimensional geographic coordinates.
      */
     @Test
     @DependsOnMethod("testFormatUnknownCRS")
@@ -178,7 +195,7 @@ public final strictfp class CoordinateFormatTest extends TestCase {
     }
 
     /**
-     * Tests parsing a 4-dimensional geographic coordinate.
+     * Tests parsing 4-dimensional geographic coordinates.
      * This method is the converse of {@link #testFormatGeographic4D()}.
      *
      * @throws ParseException if the parsing failed.
@@ -303,6 +320,24 @@ public final strictfp class CoordinateFormatTest extends TestCase {
     }
 
     /**
+     * Tests {@link CoordinateFormat#setGroundAccuracy(Quantity)}.
+     *
+     * @throws ParseException if parsing failed.
+     */
+    @Test
+    public void testSetGroundAccuracy() throws ParseException {
+        final CoordinateFormat format = new CoordinateFormat(Locale.FRANCE, null);
+        final DirectPosition2D pos = new DirectPosition2D(40.123456789, 9.87654321);
+        format.setDefaultCRS(HardCodedCRS.WGS84_φλ);
+        format.setPrecisions(0.05, 0.0001);
+        format.setGroundAccuracy(Quantities.create(3, Units.KILOMETRE));
+        assertEquals("40°07′N 9°52′35,6″E ± 3 km", format.format(pos));
+
+        final DirectPosition p = format.parseObject("40°07′N 9°52′35,6″E ± 3 km");
+        assertArrayEquals(new double[] {40.1166, 9.8765}, p.getCoordinate(), 0.0001);
+    }
+
+    /**
      * 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/measure/UnitFormat.java b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
index 2ee18bc..55929af 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/measure/UnitFormat.java
@@ -1187,7 +1187,7 @@ scan:   for (int n; i < end; i += n) {
                     // else fall through.
                 }
                 /*
-                 * For any character that are is not an operator or parenthesis, either continue
the scanning of
+                 * For any character that is not an operator or parenthesis, either continue
the scanning of
                  * characters or stop it, depending on whether the character is valid for
a unit symbol or not.
                  * In the later case, we consider that we reached the end of a unit symbol.
                  */


Mime
View raw message