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 ff9f96a15ab972ec6803d58610668fc11db7c8c9
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Jun 23 18:07:35 2022 +0200

    Provide a base class for the encoders of referencing by identifiers.
    It requires the addition of encoder-neutral methods for specifying the 
desired precision,
    which in turn requires the capability to convert angular precision to 
linear precision.
---
 .../gazetteer/GeohashReferenceSystem.java          | 176 +++++++++++++++++++--
 .../gazetteer/MilitaryGridReferenceSystem.java     | 176 ++++++++++++++++++---
 .../gazetteer/ReferencingByIdentifiers.java        | 124 ++++++++++++++-
 .../sis/referencing/gazetteer/package-info.java    |   2 +-
 .../gazetteer/GeohashReferenceSystemTest.java      | 111 ++++++++++++-
 .../gazetteer/MilitaryGridReferenceSystemTest.java |  32 +++-
 .../gazetteer/ReferencingByIdentifiersTest.java    |   8 +-
 7 files changed, 587 insertions(+), 42 deletions(-)

diff --git 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
index b2b1e488d8..e031987af4 100644
--- 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
+++ 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystem.java
@@ -16,19 +16,28 @@
  */
 package org.apache.sis.referencing.gazetteer;
 
+import javax.measure.Unit;
+import javax.measure.Quantity;
+import javax.measure.quantity.Length;
+import javax.measure.IncommensurableException;
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.util.FactoryException;
+import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.crs.GeographicCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.operation.CoordinateOperation;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.geometry.DirectPosition;
+import org.apache.sis.measure.Units;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Longitude;
+import org.apache.sis.measure.Quantities;
 import org.apache.sis.referencing.CRS;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.referencing.cs.AxesConvention;
 import org.apache.sis.referencing.crs.DefaultGeographicCRS;
+import org.apache.sis.internal.referencing.Formulas;
+import org.apache.sis.internal.util.Numerics;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.Workaround;
 import org.apache.sis.util.ComparisonMode;
@@ -51,7 +60,7 @@ import org.opengis.referencing.gazetteer.LocationType;
  *
  * @author  Chris Mattmann (JPL)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see <a href="https://en.wikipedia.org/wiki/Geohash";>Geohash on 
Wikipedia</a>
  *
@@ -192,13 +201,14 @@ public class GeohashReferenceSystem extends 
ReferencingByIdentifiers {
      *
      * @return a new object performing conversions between {@link 
DirectPosition} and geohashes.
      */
+    @Override
     public Coder createCoder() {
         return new Coder();
     }
 
     /**
      * Conversions between direct positions and geohashes. Each {@code Coder} 
instance can read codes
-     * at arbitrary precision, but formats at the {@linkplain #setHashLength 
specified precision}.
+     * at arbitrary precision, but formats at the {@linkplain 
#setHashLength(int) specified precision}.
      * The same {@code Coder} instance can be reused for reading or writing 
many geohashes.
      *
      * <h2>Immutability and thread safety</h2>
@@ -207,17 +217,15 @@ public class GeohashReferenceSystem extends 
ReferencingByIdentifiers {
      *
      * @author  Chris Mattmann (JPL)
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 0.8
+     * @version 1.3
      * @since   0.8
      * @module
      */
-    public class Coder {
+    public class Coder extends ReferencingByIdentifiers.Coder {
         /**
          * Amount of letters or digits to format in the geohash.
-         * Stored as a {@code byte} on the assumption that attempts to create
-         * geohashes of more then 255 characters is likely to be an error 
anyway.
          */
-        private byte length;
+        private int length;
 
         /**
          * A buffer of length {@link #length}, created when first needed.
@@ -259,10 +267,100 @@ public class GeohashReferenceSystem extends 
ReferencingByIdentifiers {
          */
         public void setHashLength(final int length) {
             ArgumentChecks.ensureBetween("length", 1, 255, length);
-            this.length = (byte) length;
+            this.length = length;
             buffer = null;                                  // Will recreate a 
new buffer when first needed.
         }
 
+        /**
+         * Returns an approximate precision of the geohashes formatted by this 
coder.
+         * Values are in units of ellipsoid axis length (typically metres). If 
the location is unspecified,
+         * then this method returns a value for the "worst case" scenario, 
which is at equator.
+         * The actual precision is sometime (but not always) better for 
coordinates closer to a pole.
+         *
+         * @param  position  where to evaluate the precision, or {@code null} 
for equator.
+         * @return approximate precision of formatted geohashes.
+         *
+         * @since 1.3
+         */
+        @Override
+        public Quantity<Length> getPrecision(DirectPosition position) {
+            final Ellipsoid ellipsoid = 
normalizedCRS.getDatum().getEllipsoid();
+            final Unit<Length> unit = ellipsoid.getAxisUnit();
+            final int latNumBits = (5*length) >>> 1;            // Number of 
bits for latitude value.
+            final int lonNumBits = latNumBits + (length & 1);   // Longitude 
has 1 more bit when length is odd.
+            if (position != null) try {
+                position = toGeographic(position);
+                double φ = Math.toRadians(position.getOrdinate(1));
+                double a = Math.PI/2 * Formulas.getRadius(ellipsoid, φ);   // 
Arc length of 90° using radius at φ.
+                double b = Math.cos(φ) * (2*a) / (1 << lonNumBits);        // 
Precision along longitude axis.
+                a /= (1 << latNumBits);                                    // 
Precision along latitude axis.
+                return Quantities.create(Math.max(a, b), unit);
+            } catch (FactoryException | TransformException e) {
+                recoverableException(Coder.class, "getPrecision", e);
+            }
+            double a = Math.PI * ellipsoid.getSemiMajorAxis() / (1 << 
lonNumBits);      // Worst case scenario.
+            return Quantities.create(a, unit);
+        }
+
+        /**
+         * Sets the desired precision of the identifiers formatted by this 
coder.
+         * The given value is converted to a string length.
+         *
+         * @param  precision  the desired precision in a linear or angular 
unit.
+         * @param  position   location where the specified precision is 
desired, or {@code null} for the equator.
+         * @throws IncommensurableException if the given precision does not 
use linear or angular units.
+         *
+         * @since 1.3
+         */
+        @Override
+        public void setPrecision(final Quantity<?> precision, DirectPosition 
position) throws IncommensurableException {
+            ArgumentChecks.ensureNonNull("precision", precision);
+            double p = precision.getValue().doubleValue();
+            final Unit<?> unit = precision.getUnit();
+            double numLat=0, numLon=0;                        // Number of 
distinct latitude and longitude values.
+            if (Units.isAngular(unit)) {
+                p = unit.getConverterToAny(Units.DEGREE).convert(p);           
 // Requested precision in degrees.
+                numLat = Latitude .MAX_VALUE / p;
+                numLon = Longitude.MAX_VALUE / p;
+            } else {
+                final Ellipsoid ellipsoid = 
normalizedCRS.getDatum().getEllipsoid();
+                p = unit.getConverterToAny(ellipsoid.getAxisUnit()).convert(p);
+                if (position != null) try {
+                    position = toGeographic(position);
+                    double φ = Math.toRadians(position.getOrdinate(1));
+                    numLat   = Math.PI/2 * Formulas.getRadius(ellipsoid, φ) / 
p;
+                    numLon   = Math.cos(φ) * (2*numLat);
+                } catch (FactoryException | TransformException e) {
+                    recoverableException(Coder.class, "setPrecision", e);
+                    position = null;
+                }
+                if (position == null) {
+                    numLat = Math.PI/2 * ellipsoid.getSemiMajorAxis() / p;     
        // Worst case scenario.
+                    numLon = 2*numLat;
+                }
+            }
+            int latNumBits=0, lonNumBits;
+            if (numLat > 0) {
+                final long b = Math.round(numLat);
+                latNumBits = (Long.SIZE-1) - Long.numberOfLeadingZeros(b);
+                if ((1L << latNumBits) != b) latNumBits++;
+                length = Math.max(Numerics.ceilDiv(latNumBits << 1, 5), 1);
+            }
+            if (numLon > numLat) {
+                final long b = Math.round(numLon);
+                lonNumBits = (Long.SIZE-1) - Long.numberOfLeadingZeros(b);
+                if ((1L << lonNumBits) != b) lonNumBits++;
+                if (lonNumBits == latNumBits+1 && (length & 1) != 0) {
+                    /*
+                     * If length is odd, longitude has one more bit than 
latitude.
+                     * If the latitude had enough bits, then length is 
sufficient.
+                     */
+                } else {
+                    length = Math.max(Numerics.ceilDiv(lonNumBits << 1, 5), 1);
+                }
+            }
+        }
+
         /**
          * Encodes the given latitude and longitude into a geohash.
          * This method does <strong>not</strong> take in account the axis 
order and units of the coordinate
@@ -281,7 +379,7 @@ public class GeohashReferenceSystem extends 
ReferencingByIdentifiers {
             final int highestOneBit = format.highestOneBit;
             char[] geohash = buffer;
             if (geohash == null) {
-                buffer = geohash = new char[Byte.toUnsignedInt(length)];
+                buffer = geohash = new char[length];
             }
             /*
              * The current implementation assumes a two-dimensional 
coordinates. The 'isEven' boolean takes
@@ -336,20 +434,63 @@ public class GeohashReferenceSystem extends 
ReferencingByIdentifiers {
          * @return geohash encoding of the given position.
          * @throws TransformException if an error occurred while transforming 
the given coordinate to a geohash reference.
          */
+        @Override
         public String encode(DirectPosition position) throws 
TransformException {
             ArgumentChecks.ensureNonNull("position", position);
-            final CoordinateReferenceSystem ps = 
position.getCoordinateReferenceSystem();
-            if (ps != null && !normalizedCRS.equals(ps, 
ComparisonMode.IGNORE_METADATA)) {
-                if (lastOp == null || 
!Utilities.equalsIgnoreMetadata(lastOp.getSourceCRS(), ps)) try {
-                    lastOp = CRS.findOperation(ps, normalizedCRS, null);
-                } catch (FactoryException e) {
-                    throw new GazetteerException(e.getLocalizedMessage(), e);
-                }
-                position = lastOp.getMathTransform().transform(position, null);
+            try {
+                position = toGeographic(position);
+            } catch (FactoryException e) {
+                throw new GazetteerException(e.getLocalizedMessage(), e);
+            }
+            return encode(position.getOrdinate(1), position.getOrdinate(0));
+        }
+
+        /**
+         * Encodes the given position into a geohash with the given precision.
+         * This is equivalent to invoking {@link #setPrecision(Quantity, 
DirectPosition)}
+         * before {@link #encode(DirectPosition)}, except that it is 
potentially more efficient.
+         *
+         * @param  position   the coordinate to encode.
+         * @param  precision  the desired precision in a linear or angular 
unit.
+         * @return geohash encoding of the given position.
+         * @throws IncommensurableException if the given precision does not 
use linear or angular units.
+         * @throws TransformException if an error occurred while transforming 
the given coordinate to a geohash reference.
+         *
+         * @since 1.3
+         */
+        @Override
+        public String encode(DirectPosition position, final Quantity<?> 
precision)
+                throws IncommensurableException, TransformException
+        {
+            ArgumentChecks.ensureNonNull("position",  position);
+            ArgumentChecks.ensureNonNull("precision", precision);
+            try {
+                position = toGeographic(position);
+            } catch (FactoryException e) {
+                throw new GazetteerException(e.getLocalizedMessage(), e);
             }
+            setPrecision(precision, position);
             return encode(position.getOrdinate(1), position.getOrdinate(0));
         }
 
+        /**
+         * Transforms the given position to the {@link #normalizedCRS}.
+         * If the position does not specify a CRS, then it is assumed already 
normalized.
+         *
+         * @param  position  the position to transform.
+         * @return the transformed position.
+         */
+        private DirectPosition toGeographic(final DirectPosition position) 
throws FactoryException, TransformException {
+            final CoordinateReferenceSystem ps = 
position.getCoordinateReferenceSystem();
+            if (ps == null || normalizedCRS.equals(ps, 
ComparisonMode.IGNORE_METADATA)) {
+                return position;
+            }
+            if (lastOp == null || 
!Utilities.equalsIgnoreMetadata(lastOp.getSourceCRS(), ps)) {
+                lastOp = CRS.findOperation(ps, normalizedCRS, null);
+            }
+            return lastOp.getMathTransform().transform(position, null);
+        }
+
         /**
          * Decodes the given geohash into a latitude and a longitude.
          * The axis order depends on the coordinate reference system of the 
enclosing {@link GeohashReferenceSystem}.
@@ -358,6 +499,7 @@ public class GeohashReferenceSystem extends 
ReferencingByIdentifiers {
          * @return a new geographic coordinate for the given geohash.
          * @throws TransformException if an error occurred while parsing the 
given string.
          */
+        @Override
         public Location decode(final CharSequence geohash) throws 
TransformException {
             ArgumentChecks.ensureNonEmpty("geohash", geohash);
             return new Decoder(geohash, coordinates);
diff --git 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
index 7a9790140e..11f6704277 100644
--- 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
+++ 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystem.java
@@ -27,10 +27,15 @@ import java.util.stream.Stream;
 import java.util.function.Consumer;
 import java.util.stream.StreamSupport;
 import java.awt.geom.Rectangle2D;
+import javax.measure.Unit;
+import javax.measure.Quantity;
+import javax.measure.quantity.Length;
+import javax.measure.IncommensurableException;
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.util.FactoryException;
 import org.opengis.geometry.Envelope;
 import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.datum.Ellipsoid;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.crs.CoordinateReferenceSystem;
@@ -66,11 +71,14 @@ import org.apache.sis.geometry.Shapes2D;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.DirectPosition2D;
+import org.apache.sis.internal.referencing.Formulas;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.math.DecimalFunctions;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.Latitude;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
 
 import static java.util.logging.Logger.getLogger;
 
@@ -137,7 +145,7 @@ import org.opengis.referencing.gazetteer.LocationType;
  * are not thread-safe; it is recommended to create a new {@code Coder} 
instance for each thread.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.3
  *
  * @see CommonCRS#universal(double, double)
  * @see <a 
href="https://en.wikipedia.org/wiki/Military_Grid_Reference_System";>Military 
Grid Reference System on Wikipedia</a>
@@ -341,6 +349,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
      *
      * @return a new object performing conversions between {@link 
DirectPosition} and MGRS references.
      */
+    @Override
     public Coder createCoder() {
         return new Coder();
     }
@@ -348,7 +357,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
     /**
      * Conversions between direct positions and references in the Military 
Grid Reference System (MGRS).
      * Each {@code Coder} instance can read references at arbitrary precision, 
but formats at the
-     * {@linkplain #setPrecision specified precision}.
+     * {@linkplain #setPrecision(double) specified precision}.
      * The same {@code Coder} instance can be reused for reading or writing 
many MGRS references.
      *
      * <p>See the {@link MilitaryGridReferenceSystem} enclosing class for 
usage example.</p>
@@ -358,11 +367,11 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
      * or synchronization must be applied by the caller.
      *
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 0.8
+     * @version 1.3
      * @since   0.8
      * @module
      */
-    public class Coder {
+    public class Coder extends ReferencingByIdentifiers.Coder {
         /**
          * Number of digits to use for formatting the numerical part of a MGRS 
reference.
          *
@@ -468,6 +477,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
          * documented in the {@link #getPrecision()} method.
          *
          * @param  precision  the desired precision in metres.
+         * @throws ArithmeticException if the given precision is zero, 
negative, infinity or NaN.
          */
         public void setPrecision(final double precision) {
             final int p;
@@ -488,6 +498,73 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
             return digits;
         }
 
+        /**
+         * Returns the precision of the references formatted by this coder.
+         * This method returns the same value as {@link #getPrecision()} but 
as a quantity.
+         *
+         * @param  position  ignored (can be null).
+         * @return precision of formatted references in metres.
+         *
+         * @since 1.3
+         */
+        @Override
+        public Quantity<Length> getPrecision(DirectPosition position) {
+            return Quantities.create(getPrecision(), Units.METRE);
+        }
+
+        /**
+         * Sets the desired precision of the references formatted by this 
coder.
+         * If the given quantity uses angular units, it is converted to an 
approximate precision in metres
+         * at the latitude of given position. Then this method delegates to 
{@link #setPrecision(double)}.
+         *
+         * @param  precision  the desired precision in a linear or angular 
unit.
+         * @param  position   location where the specified precision is 
desired, or {@code null} for the equator.
+         * @throws IncommensurableException if the given precision does not 
use linear or angular units.
+         * @throws ArithmeticException if the precision is zero, negative, 
infinity or NaN.
+         *
+         * @since 1.3
+         */
+        @Override
+        public void setPrecision(final Quantity<?> precision, DirectPosition 
position) throws IncommensurableException {
+            ArgumentChecks.ensureNonNull("precision", precision);
+            double p = precision.getValue().doubleValue();
+            final Unit<?> unit = precision.getUnit();
+            if (Units.isAngular(unit)) {
+                final Ellipsoid ellipsoid = getEllipsoid();
+                double radius = 0;
+                if (position != null) {
+                    final CoordinateReferenceSystem crs = 
position.getCoordinateReferenceSystem();
+                    if (crs != null) try {
+                        final double φ  = encoder(crs).getLatitude(this, 
position);
+                        final double φr = Math.toRadians(φ);
+                        radius = Formulas.getRadius(ellipsoid, φr);
+                        if (φ >= TransverseMercator.Zoner.SOUTH_BOUNDS &&
+                            φ <  TransverseMercator.Zoner.NORTH_BOUNDS)
+                        {
+                            radius *= Math.cos(φr);
+                        }
+
+                    } catch (IllegalArgumentException | FactoryException | 
TransformException e) {
+                        recoverableException(Coder.class, "setPrecision", e);
+                    }
+                }
+                if (!(radius > 0)) {
+                    radius = ellipsoid.getSemiMajorAxis();                  // 
Worst case scenario.
+                }
+                p = unit.getConverterToAny(Units.RADIAN).convert(p) * radius;
+            } else {
+                p = unit.getConverterToAny(Units.METRE).convert(p);
+            }
+            setPrecision(p);
+        }
+
+        /**
+         * Returns the ellipsoid of the geodetic datum of MGRS identifiers.
+         */
+        final Ellipsoid getEllipsoid() {
+            return datum.geographic().getDatum().getEllipsoid();
+        }
+
         /**
          * Returns the separator to insert between each component of the MGRS 
identifier.
          * Components are zone number, latitude band, 100 000-metres square 
identifier and numerical values.
@@ -552,7 +629,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
          * Returns the encoder for the given coordinate reference system.
          * All calls to this method must be done in the same thread.
          *
-         * @throws IllegalArgumentException if the given CRS do not use one of 
the supported datums.
+         * @throws IllegalArgumentException if the given CRS does not use one 
of the supported datums.
          * @throws FactoryException if the creation of a coordinate operation 
failed.
          * @throws TransformException if the creation of an inverse operation 
failed.
          */
@@ -579,10 +656,48 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
          * @return MGRS encoding of the given position.
          * @throws TransformException if an error occurred while transforming 
the given coordinate to a MGRS reference.
          */
+        @Override
         public String encode(final DirectPosition position) throws 
TransformException {
             ArgumentChecks.ensureNonNull("position", position);
             try {
-                return 
encoder(position.getCoordinateReferenceSystem()).encode(this, position, true, 
getSeparator(), digits());
+                return encoder(position.getCoordinateReferenceSystem())
+                        .encode(this, position, true, getSeparator(), 
digits(), 0);
+            } catch (IllegalArgumentException | FactoryException e) {
+                throw new GazetteerException(e.getLocalizedMessage(), e);
+            }
+        }
+
+        /**
+         * Encodes the given position into a MGRS reference with the given 
precision.
+         * This is equivalent to invoking {@link #setPrecision(Quantity, 
DirectPosition)}
+         * before {@link #encode(DirectPosition)}, except that it is 
potentially more efficient.
+         *
+         * @param  position   the coordinate to encode.
+         * @param  precision  the desired precision in a linear or angular 
unit.
+         * @return MGRS encoding of the given position.
+         * @throws ArithmeticException if the precision is zero, negative, 
infinity or NaN.
+         * @throws IncommensurableException if the given precision does not 
use linear or angular units.
+         * @throws TransformException if an error occurred while transforming 
the given coordinate to a MGRS reference.
+         *
+         * @since 1.3
+         */
+        @Override
+        public String encode(final DirectPosition position, final Quantity<?> 
precision)
+                throws IncommensurableException, TransformException
+        {
+            ArgumentChecks.ensureNonNull("position",  position);
+            ArgumentChecks.ensureNonNull("precision", precision);
+            double p = precision.getValue().doubleValue();
+            final Unit<?> unit = precision.getUnit();
+            if (Units.isAngular(unit)) {
+                p = unit.getConverterToAny(Units.RADIAN).convert(p);
+            } else {
+                setPrecision(unit.getConverterToAny(Units.METRE).convert(p));
+                p = 0;
+            }
+            try {
+                return encoder(position.getCoordinateReferenceSystem())
+                        .encode(this, position, true, getSeparator(), 
digits(), p);
             } catch (IllegalArgumentException | FactoryException e) {
                 throw new GazetteerException(e.getLocalizedMessage(), e);
             }
@@ -649,6 +764,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
          * @return a new position with the longitude at coordinate 0 and 
latitude at coordinate 1.
          * @throws TransformException if an error occurred while parsing the 
given string.
          */
+        @Override
         public Location decode(final CharSequence reference) throws 
TransformException {
             ArgumentChecks.ensureNonEmpty("reference", reference);
             return new Decoder(this, reference);
@@ -1228,7 +1344,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                         if (downward)    y += step - 1;
                         normalized.setOrdinate(0, x);
                         normalized.setOrdinate(1, y);
-                        String ref = encoder.encode(this, normalized, false, 
separator, digits);
+                        String ref = encoder.encode(this, normalized, false, 
separator, digits, 0);
                         if (ref != null) {
                             /*
                              * If there is a change of latitude band, we may 
have missed a cell before this one.
@@ -1240,7 +1356,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                             if (latitudeBand != previous && previous != 0) {
                                 pending = ref;
                                 normalized.setOrdinate(1, y + (downward ? +1 : 
-1));
-                                ref = encoder.encode(this, normalized, false, 
separator, digits);
+                                ref = encoder.encode(this, normalized, false, 
separator, digits, 0);
                                 if (ref == null || encoder.latitudeBand == 
previous) {
                                     ref = pending;  // No result or same 
result than previous iteration - cancel.
                                     pending = null;
@@ -1312,7 +1428,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
      * or synchronization must be applied by the caller.
      *
      * @author  Martin Desruisseaux (Geomatys)
-     * @version 0.8
+     * @version 1.3
      *
      * @see <a 
href="https://en.wikipedia.org/wiki/Military_Grid_Reference_System";>Military 
Grid Reference System on Wikipedia</a>
      *
@@ -1327,7 +1443,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
         private static final int POLE = 100;
 
         /**
-         * The datum to which to transform the coordinate before formatting 
the MGRS reference.
+         * The datum to which to transform the coordinates before formatting 
the MGRS reference.
          * Only the datums enumerated in {@link CommonCRS} are currently 
supported.
          */
         private final CommonCRS datum;
@@ -1413,7 +1529,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                 if (crsZone != 0) {
                     /*
                      * Usually, the projected CRS already has (E,N) axis 
orientations with metres units,
-                     * so we let 'toNormalized' to null. In the rarer cases 
where the CRS axes do not
+                     * so we let `toNormalized` to null. In the rarer cases 
where the CRS axes do not
                      * have the expected orientations and units, then we build 
a normalized version of
                      * that CRS and compute the transformation to that CRS.
                      */
@@ -1464,6 +1580,18 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
             return (char) band;
         }
 
+        /**
+         * Returns the latitude in degrees of given position.
+         * This is used only for estimating the precision.
+         */
+        final double getLatitude(final Coder owner, DirectPosition position) 
throws TransformException {
+            if (toNormalized != null) {
+                owner.normalized = position = toNormalized.transform(position, 
owner.normalized);
+            }
+            owner.geographic = position = toGeographic.transform(position, 
owner.geographic);
+            return position.getOrdinate(0);
+        }
+
         /**
          * Encodes the given position into a MGRS reference. It is caller 
responsibility to ensure that
          * the position CRS is the same than the CRS specified at this {@code 
Encoder} creation time.
@@ -1473,13 +1601,13 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
          * @param  reproject  whether this method is allowed to reproject 
{@code position} when needed.
          * @param  separator  the separator to insert between each component 
of the MGRS identifier.
          * @param  digits     number of digits to use for formatting the 
numerical part of a MGRS reference.
+         * @param  precision  angular precision in radians, or 0. If non-zero, 
it will override {@code digits}.
          * @return the value of {@code buffer.toString()}, or {@code null} if 
a reprojection was necessary
          *         but {@code reproject} is {@code false}.
          */
-        String encode(final Coder owner, DirectPosition position, final 
boolean reproject,
-                final String separator, final int digits) throws 
FactoryException, TransformException
+        String encode(final Coder owner, DirectPosition position, final 
boolean reproject, final String separator,
+                      int digits, double precision) throws FactoryException, 
TransformException
         {
-            final StringBuilder buffer = owner.buffer;
             if (toNormalized != null) {
                 owner.normalized = position = toNormalized.transform(position, 
owner.normalized);
             }
@@ -1512,9 +1640,21 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                 geographic.setOrdinate(1, Longitude.normalize(λ));
                 owner.normalized = position = 
toActualZone.transform(geographic, owner.normalized);
             }
+            /*
+             * If an angular precision has been specified, override the number 
of digits with a value computed
+             * from that precision. We do that here for opportunistically 
using the latitude value computed above.
+             */
+            if (precision > 0) {
+                final double φr = Math.toRadians(φ);
+                precision *= Formulas.getRadius(owner.getEllipsoid(), φr);     
 // Convert precision to metres.
+                if (isUTM) precision *= Math.cos(φr);
+                owner.setPrecision(precision);
+                digits = owner.digits();
+            }
             /*
              * Grid Zone Designator (GZD).
              */
+            final StringBuilder buffer = owner.buffer;
             buffer.setLength(0);
             if (isUTM) {
                 buffer.append(zone).append(separator);
@@ -1544,7 +1684,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                     if (col < 1 || col > 8) {
                         /*
                          * UTM northing values at the equator range from 
166021 to 833979 meters approximately
-                         * (WGS84 ellipsoid). Consequently 'cx' ranges from 
approximately 1.66 to 8.34, so 'c'
+                         * (WGS84 ellipsoid). Consequently `cx` ranges from 
approximately 1.66 to 8.34, so `col`
                          * should range from 1 to 8 inclusive.
                          */
                         throw new 
GazetteerException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
@@ -1563,7 +1703,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                         row += ('F' - 'A');
                     }
                     row %= GRID_ROW_COUNT;
-                    // Row calculation to be completed after the 'else' block.
+                    // Row calculation to be completed after the `else` block.
                 } else {
                     /*
                      * Universal Polar Stereographic (UPS) case. Row letters 
go from A to Z, omitting I and O.
@@ -1590,7 +1730,7 @@ public class MilitaryGridReferenceSystem extends 
ReferencingByIdentifiers {
                  * The specification requires us to truncate the number, not 
to round it.
                  */
                 if (digits > 0) {
-                    final double precision = 
MathFunctions.pow10(METRE_PRECISION_DIGITS - digits);
+                    precision = MathFunctions.pow10(METRE_PRECISION_DIGITS - 
digits);
                     append(buffer.append(separator), (int) ((x - cx * 
GRID_SQUARE_SIZE) / precision), digits);
                     append(buffer.append(separator), (int) ((y - cy * 
GRID_SQUARE_SIZE) / precision), digits);
                 }
@@ -2081,7 +2221,7 @@ parse:                  switch (part) {
             if (!isValid) {
                 final String gzd;
                 try {
-                    gzd = owner.encoder(crs).encode(owner, 
getDirectPosition(), true, "", 0);
+                    gzd = owner.encoder(crs).encode(owner, 
getDirectPosition(), true, "", 0, 0);
                 } catch (IllegalArgumentException | FactoryException e) {
                     throw new GazetteerException(e.getLocalizedMessage(), e);
                 }
diff --git 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
index 923f3fb406..2142c996cb 100644
--- 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
+++ 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiers.java
@@ -20,10 +20,16 @@ import java.util.Map;
 import java.util.List;
 import java.util.Objects;
 import java.util.HashMap;
+import java.util.logging.Logger;
+import javax.measure.Quantity;
+import javax.measure.IncommensurableException;
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.util.InternationalString;
+import org.opengis.geometry.DirectPosition;
+import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.referencing.AbstractReferenceSystem;
 import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.logging.Logging;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.Utilities;
 import org.apache.sis.util.iso.Types;
@@ -32,11 +38,13 @@ import org.apache.sis.io.wkt.Formatter;
 import org.apache.sis.io.wkt.ElementKind;
 import org.apache.sis.metadata.iso.extent.Extents;
 import org.apache.sis.internal.referencing.WKTUtilities;
+import org.apache.sis.internal.system.Modules;
 import org.apache.sis.io.wkt.FormattableObject;
 import org.apache.sis.util.resources.Vocabulary;
 
 // Branch-dependent imports
 import org.opengis.metadata.citation.Party;
+import org.opengis.referencing.gazetteer.Location;
 import org.opengis.referencing.gazetteer.LocationType;
 import org.opengis.referencing.gazetteer.ReferenceSystemUsingIdentifiers;
 
@@ -52,7 +60,7 @@ import 
org.opengis.referencing.gazetteer.ReferenceSystemUsingIdentifiers;
  * without synchronization.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  *
  * @see ModifiableLocationType
  * @see AbstractLocation
@@ -61,7 +69,7 @@ import 
org.opengis.referencing.gazetteer.ReferenceSystemUsingIdentifiers;
  * @module
  */
 @XmlTransient
-public class ReferencingByIdentifiers extends AbstractReferenceSystem 
implements ReferenceSystemUsingIdentifiers {
+public abstract class ReferencingByIdentifiers extends AbstractReferenceSystem 
implements ReferenceSystemUsingIdentifiers {
     /**
      * Serial number for inter-operability with different versions.
      */
@@ -240,6 +248,118 @@ public class ReferencingByIdentifiers extends 
AbstractReferenceSystem implements
         return locationTypes.get(0);
     }
 
+    /**
+     * Returns a new object performing conversions between {@code 
DirectPosition} and identifiers.
+     * The returned object is <strong>not</strong> thread-safe; a new instance 
must be created
+     * for each thread, or synchronization must be applied by the caller.
+     *
+     * @return a new object performing conversions between {@link 
DirectPosition} and identifiers.
+     *
+     * @since 1.3
+     */
+    public abstract Coder createCoder();
+
+    /**
+     * Conversions between direct positions and identifiers.
+     * Each {@code Coder} instance can read references at arbitrary precision,
+     * but formats at the {@linkplain #setPrecision specified approximate 
precision}.
+     * The same {@code Coder} instance can be reused for reading or writing 
many identifiers.
+     *
+     * <h2>Immutability and thread safety</h2>
+     * This class is <strong>not</strong> thread-safe. A new instance must be 
created for each thread,
+     * or synchronization must be applied by the caller.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @version 1.3
+     * @since   1.3
+     * @module
+     */
+    public abstract static class Coder {
+        /**
+         * Creates a new instance.
+         */
+        protected Coder() {
+        }
+
+        /**
+         * Returns approximate precision of the identifiers formatted by this 
coder at the given location.
+         * The returned value is typically a length in linear unit (e.g. 
metres).
+         * Precisions in angular units should be converted to linear units at 
the specified location.
+         * If the location is {@code null}, then this method should return a 
precision for the worst case scenario.
+         *
+         * @param  position  where to evaluate the precision, or {@code null} 
for the worst case scenario.
+         * @return approximate precision in metres of formatted identifiers.
+         */
+        public abstract Quantity<?> getPrecision(DirectPosition position);
+
+        /**
+         * Sets the desired precision of the identifiers formatted by this 
coder.
+         * The given value is converted to coder-specific representation (e.g. 
number of digits).
+         * The value returned by {@link #getPrecision(DirectPosition)} may be 
different than the
+         * value specified to this method.
+         *
+         * @param  precision  the desired precision.
+         * @param  position   location where the specified precision is 
desired, or {@code null} for the worst case scenario.
+         * @throws IncommensurableException if the given precision uses 
incompatible units of measurement.
+         */
+        public abstract void setPrecision(Quantity<?> precision, 
DirectPosition position) throws IncommensurableException;
+
+        /**
+         * A combined method which sets the encoder precision to the given 
value, then formats the given position.
+         * The default implementation is equivalent to the following code:
+         *
+         * {@preformat java
+         *     setPrecision(precision, position);
+         *     return encode(position);
+         * }
+         *
+         * Subclasses should override with more efficient implementation,
+         * for example by transforming the given position only once.
+         *
+         * @param  position   the coordinate to encode.
+         * @param  precision  the desired precision.
+         * @return identifier of the given position.
+         * @throws IncommensurableException if the given precision uses 
incompatible units of measurement.
+         * @throws TransformException if an error occurred while transforming 
the given coordinate to an identifier.
+         */
+        public String encode(DirectPosition position, Quantity<?> precision) 
throws IncommensurableException, TransformException {
+            setPrecision(precision, position);
+            return encode(position);
+        }
+
+        /**
+         * Encodes the given position into an identifier.
+         * The given position must have a Coordinate Reference System (CRS) 
associated to it.
+         *
+         * @param  position  the coordinate to encode.
+         * @return identifier of the given position.
+         * @throws TransformException if an error occurred while transforming 
the given coordinate to an identifier.
+         */
+        public abstract String encode(DirectPosition position) throws 
TransformException;
+
+        /**
+         * Decodes the given identifier into a latitude and a longitude.
+         * The axis order depends on the coordinate reference system of the 
enclosing {@link ReferencingByIdentifiers}.
+         *
+         * @param  identifier  identifier string to decode.
+         * @return a new geographic coordinate for the given identifier.
+         * @throws TransformException if an error occurred while parsing the 
given string.
+         */
+        public abstract Location decode(CharSequence identifier) throws 
TransformException;
+
+        /**
+         * Logs a warning for a recoverable error while transforming a 
position. This is used for implementations
+         * of method such as {@link #getPrecision(DirectPosition)}, which can 
fallback on "worst case" scenario.
+         *
+         * @param caller  the class that wanted to transform a position.
+         * @param method  the method that wanted to transform a position.
+         * @param e       the transformation error.
+         */
+        static void recoverableException(final Class<?> caller, final String 
method, final Exception e) {
+            
Logging.recoverableException(Logger.getLogger(Modules.REFERENCING_BY_IDENTIFIERS),
 caller, method, e);
+        }
+    }
+
     /**
      * Compares this reference system with the specified object for equality.
      * If the {@code mode} argument value is {@link ComparisonMode#STRICT 
STRICT} or
diff --git 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/package-info.java
 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/package-info.java
index f74a09be01..182f0a752b 100644
--- 
a/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/package-info.java
+++ 
b/core/sis-referencing-by-identifiers/src/main/java/org/apache/sis/referencing/gazetteer/package-info.java
@@ -31,7 +31,7 @@
  *
  * @author  Chris Mattmann (JPL)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.1
+ * @version 1.3
  * @since   0.8
  * @module
  */
diff --git 
a/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
 
b/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
index 61be1e34ff..4af4c36307 100644
--- 
a/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
+++ 
b/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/GeohashReferenceSystemTest.java
@@ -17,10 +17,15 @@
 package org.apache.sis.referencing.gazetteer;
 
 import java.util.Locale;
+import javax.measure.Quantity;
+import javax.measure.quantity.Length;
+import javax.measure.IncommensurableException;
 import org.opengis.referencing.operation.TransformException;
 import org.opengis.geometry.DirectPosition;
 import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.referencing.CommonCRS;
+import org.apache.sis.measure.Quantities;
+import org.apache.sis.measure.Units;
 import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
@@ -39,7 +44,7 @@ import org.opengis.referencing.gazetteer.LocationType;
  *
  * @author  Ross Laidlaw
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.1
  * @module
  */
@@ -50,6 +55,13 @@ public final strictfp class GeohashReferenceSystemTest 
extends TestCase {
      */
     private static final double TOLERANCE = 0.000001;
 
+    /**
+     * WGS84 semi-major axis length divided by semi-minor axis length.
+     * This is used for estimating how the precision changes when moving
+     * from equator to a pole.
+     */
+    private static final double B_A = 6356752.314245179 / 6378137;
+
     /**
      * Returns a reference system instance to test.
      */
@@ -87,6 +99,103 @@ public final strictfp class GeohashReferenceSystemTest 
extends TestCase {
         new Place("Space Needle",          -122.349100, 47.620400, 
"c22yzvh0gmfy")
     };
 
+    /**
+     * Tests the {@link 
GeohashReferenceSystem.Coder#getPrecision(DirectPosition)} method.
+     * Values published in Wikipedia are used as references, with more 
precision digits
+     * added from SIS computation.
+     *
+     * @throws TransformException if an exception occurred while initializing 
the reference system.
+     */
+    @Test
+    public void testGetPrecision() throws TransformException {
+        final GeohashReferenceSystem.Coder coder = instance().createCoder();
+        verifyGetPrecision(coder, 1, 2504689,    0.5);
+        verifyGetPrecision(coder, 2,  626172,    0.5);
+        verifyGetPrecision(coder, 3,   78272,    0.5);
+        verifyGetPrecision(coder, 4,   19568,    0.5);
+        verifyGetPrecision(coder, 5,    2446,    0.5);
+        verifyGetPrecision(coder, 6,     611.5,  0.05);
+        verifyGetPrecision(coder, 7,      76.44, 0.005);
+        verifyGetPrecision(coder, 8,      19.11, 0.005);
+    }
+
+    /**
+     * A single test case of {@link #testGetPrecision()} for the given hash 
string length.
+     */
+    private static void verifyGetPrecision(final GeohashReferenceSystem.Coder 
coder,
+            final int length, final double expected, final double tolerance)
+    {
+        coder.setHashLength(length);
+        Quantity<Length> worstCase = coder.getPrecision(null);
+        Quantity<Length> atEquator = coder.getPrecision(new 
DirectPosition2D(0,  0));
+        Quantity<Length> atPole    = coder.getPrecision(new 
DirectPosition2D(0, 90));
+        Quantity<Length> somewhere = coder.getPrecision(new 
DirectPosition2D(0, 40));
+        assertEquals(Units.METRE, worstCase.getUnit());
+        assertEquals(Units.METRE, atEquator.getUnit());
+        assertEquals(Units.METRE, atPole   .getUnit());
+        assertEquals(Units.METRE, somewhere.getUnit());
+        assertEquals(expected, worstCase.getValue().doubleValue(), tolerance);
+        assertEquals(expected, atEquator.getValue().doubleValue(), tolerance);
+        /*
+         * If the length is even, then longitude values have one more bit than 
latitudes,
+         * which compensate for the fact that the range of longitude values is 
twice the
+         * range of latitude values. Consequently both coordinate values 
should have the
+         * same precision at equator, and moving to the pole changes only the 
radius.
+         * Otherwise longitude error is twice larger than latitude error. At 
the pole,
+         * the longitude error vanishes and only the latitude error matter.
+         */
+        final double f = (length & 1) != 0 ? B_A : B_A/2;
+        assertEquals(f * expected, atPole.getValue().doubleValue(), tolerance);
+        /*
+         * Value should be somewhere between the two extrems. We use a simple 
average for this test.
+         */
+        final double estimate = (atEquator.getValue().doubleValue() + 
atPole.getValue().doubleValue()) / 2;
+        assertEquals(estimate, somewhere.getValue().doubleValue(), estimate / 
25);
+    }
+
+    /**
+     * Tests the {@link GeohashReferenceSystem.Coder#setPrecision(Quantity, 
DirectPosition)} method.
+     * Values used as a reference are the same as {@link #testGetPrecision()}.
+     *
+     * @throws TransformException if an exception occurred while initializing 
the reference system.
+     * @throws IncommensurableException if a precision uses incompatible units 
of measurement.
+     */
+    @Test
+    public void testSetPrecision() throws TransformException, 
IncommensurableException {
+        final GeohashReferenceSystem.Coder coder = instance().createCoder();
+        verifySetPrecision(coder, 1, 2504689);
+        verifySetPrecision(coder, 2,  626172);
+        verifySetPrecision(coder, 3,   78272);
+        verifySetPrecision(coder, 4,   19568);
+        verifySetPrecision(coder, 5,    2446);
+        verifySetPrecision(coder, 6,     611.5);
+        verifySetPrecision(coder, 7,      76.44);
+        verifySetPrecision(coder, 8,      19.11);
+    }
+
+    /**
+     * Verifies the value computed by {@link 
GeohashReferenceSystem.Coder#getPrecision()}
+     * for the given hash string length.
+     */
+    private static void verifySetPrecision(final GeohashReferenceSystem.Coder 
coder,
+            final int length, final double precision) throws 
IncommensurableException
+    {
+        final Length atEquator = Quantities.create(precision,     Units.METRE);
+        final Length atPole    = Quantities.create(precision*B_A, Units.METRE);
+        coder.setPrecision(atEquator, null);
+        assertEquals(length, coder.getHashLength());
+        coder.setPrecision(atEquator, new DirectPosition2D(0, 0));
+        assertEquals(length, coder.getHashLength());
+        coder.setPrecision(atPole, new DirectPosition2D(0, 90));
+        assertEquals(length, coder.getHashLength());
+        /*
+         * Request a slightly finer precision at equator.
+         * It requires a longer hash code, except for the 2 first cases.
+         */
+        coder.setPrecision(atPole, null);
+        assertEquals(length < 3 ? length : length+1, coder.getHashLength());
+    }
+
     /**
      * Tests the {@link GeohashReferenceSystem.Coder#encode(double, double)} 
method.
      *
diff --git 
a/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
 
b/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
index 675a134b52..2a76f68343 100644
--- 
a/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
+++ 
b/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/MilitaryGridReferenceSystemTest.java
@@ -25,6 +25,7 @@ import java.util.Random;
 import java.util.Iterator;
 import java.util.Collections;
 import java.lang.reflect.Field;
+import javax.measure.IncommensurableException;
 import org.opengis.referencing.crs.ProjectedCRS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.TransformException;
@@ -34,6 +35,7 @@ import org.apache.sis.geometry.DirectPosition2D;
 import org.apache.sis.geometry.Envelope2D;
 import org.apache.sis.geometry.GeneralEnvelope;
 import org.apache.sis.internal.referencing.provider.TransverseMercator;
+import org.apache.sis.measure.Quantities;
 import org.apache.sis.referencing.CommonCRS;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
@@ -41,6 +43,8 @@ import org.apache.sis.test.TestCase;
 import org.apache.sis.test.TestUtilities;
 import org.junit.Test;
 
+import static 
org.apache.sis.internal.metadata.ReferencingServices.NAUTICAL_MILE;
+import static org.apache.sis.measure.Units.ARC_MINUTE;
 import static org.junit.Assert.*;
 
 // Branch-dependent imports
@@ -52,7 +56,7 @@ import org.opengis.referencing.gazetteer.LocationType;
  * Tests {@link MilitaryGridReferenceSystem}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -510,6 +514,32 @@ public final strictfp class 
MilitaryGridReferenceSystemTest extends TestCase {
         assertEquals("48P", coder.encode(position));
     }
 
+    /**
+     * Tests encoding of the same coordinate at various precision specified as 
an angular value.
+     * The encoder is expected to transform the angular value into a linear 
value.
+     *
+     * @throws IncommensurableException if the quantity type is not accepted.
+     * @throws TransformException if an error occurred while computing the 
MGRS label.
+     */
+    @Test
+    @DependsOnMethod("testPrecision")
+    public void testAngularPrecision() throws IncommensurableException, 
TransformException {
+        final MilitaryGridReferenceSystem.Coder coder = coder();
+        final DirectPosition2D position = new 
DirectPosition2D(CommonCRS.WGS84.universal(13, 103));
+        position.x =  377299;
+        position.y = 1483035;
+        coder.setPrecision(Quantities.create(1010 / NAUTICAL_MILE, 
ARC_MINUTE), null);
+        assertEquals(1000, coder.getPrecision(), STRICT);
+        assertEquals("48PUV7783", coder.encode(position));
+        /*
+         * Same value closer to a pole. It forces the encoder to use a finer 
precision,
+         * because a degree of longitude represent a smaller distance.
+         */
+        coder.setPrecision(Quantities.create(1010 / NAUTICAL_MILE, 
ARC_MINUTE), position);
+        assertEquals(100, coder.getPrecision(), STRICT);
+        assertEquals("48PUV772830", coder.encode(position));
+    }
+
     /**
      * Tests encoding of the same coordinate with various separators, mixed 
with various precisions.
      *
diff --git 
a/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiersTest.java
 
b/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiersTest.java
index 7ab4c8f573..17e686900b 100644
--- 
a/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiersTest.java
+++ 
b/core/sis-referencing-by-identifiers/src/test/java/org/apache/sis/referencing/gazetteer/ReferencingByIdentifiersTest.java
@@ -32,7 +32,7 @@ import static org.apache.sis.test.Assert.*;
  * Tests {@link ReferencingByIdentifiers}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.3
  * @since   0.8
  * @module
  */
@@ -50,7 +50,11 @@ public final strictfp class ReferencingByIdentifiersTest 
extends TestCase {
         
assertNull(properties.put(ReferencingByIdentifiers.DOMAIN_OF_VALIDITY_KEY, new 
DefaultExtent("UK", null, null, null)));
         assertNull(properties.put(ReferencingByIdentifiers.THEME_KEY, 
"property"));
         assertNull(properties.put(ReferencingByIdentifiers.OVERALL_OWNER_KEY, 
new DefaultOrganisation("Office for National Statistics", null, null, null)));
-        return new ReferencingByIdentifiers(properties, 
LocationTypeTest.create(inherit));
+        return new ReferencingByIdentifiers(properties, 
LocationTypeTest.create(inherit)) {
+            @Override public ReferencingByIdentifiers.Coder createCoder() {
+                throw new UnsupportedOperationException();
+            }
+        };
     }
 
     /**

Reply via email to