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(); + } + }; } /**
