blob: cda1c14fe3785f19cbde0e73ead942e640a9313f [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.gazetteer;
import java.util.Arrays;
import java.util.Map;
import java.util.IdentityHashMap;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.stream.Stream;
import java.util.function.Consumer;
import java.util.stream.StreamSupport;
import java.awt.geom.Rectangle2D;
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.crs.SingleCRS;
import org.opengis.referencing.crs.ProjectedCRS;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.referencing.operation.CoordinateOperation;
import org.opengis.referencing.operation.Projection;
import org.opengis.referencing.operation.TransformException;
import org.apache.sis.internal.referencing.provider.TransverseMercator;
import org.apache.sis.internal.referencing.provider.PolarStereographicA;
import org.apache.sis.internal.gazetteer.Resources;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.referencing.NamedIdentifier;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.cs.AxesConvention;
import org.apache.sis.referencing.crs.DefaultProjectedCRS;
import org.apache.sis.internal.referencing.j2d.IntervalRectangle;
import org.apache.sis.math.MathFunctions;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.StringBuilders;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.Workaround;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.resources.Vocabulary;
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.util.Strings;
import org.apache.sis.math.DecimalFunctions;
import org.apache.sis.measure.Longitude;
import org.apache.sis.measure.Latitude;
// Branch-dependent imports
import org.apache.sis.metadata.iso.citation.AbstractParty;
/**
* The Military Grid Reference System (MGRS).
* The MGRS is the geocoordinate standard used by NATO militaries for locating points on the earth.
* It is based on the Universal Transverse Mercator (UTM) and the Universal Polar Stereographic (UPS) projections.
* Despite its name, MGRS is used not only for military purposes; it is used also for organizing Earth Observation
* data in directory trees for example.
*
* <p>MGRS references are sequences of digits and letters like “4Q FJ 12345 67890” (a reference with 1 metre accuracy),
* optionally written with reduced resolution as in “4Q FJ 123 678” (a reference with 100 metres accuracy).
* Those references form a hierarchy of 3 {@linkplain ModifiableLocationType location types}:</p>
*
* <blockquote>
* <b>Grid zone designator</b> (example: “4Q”)<br>
*    └─<b>100 km square identifier</b> (example: “FJ”)<br>
*        └─<b>Grid coordinate</b> (example: “12345 67890”)<br>
* </blockquote>
*
* <p>Conversions between MGRS references and spatial coordinates can be performed by the {@link Coder Coder} inner class.
* The result of decoding a MGRS reference is an envelope rather than a point, but a representative point can be obtained.
* The encoding and decoding processes take in account Norway and Svalbard special cases (they have wider UTM zones for
* historical reasons).</p>
*
* <div class="note"><b>Example:</b>
* the following code:
*
* {@preformat java
* MilitaryGridReferenceSystem system = new MilitaryGridReferenceSystem();
* MilitaryGridReferenceSystem.Coder coder = system.createCoder();
* Location loc = coder.decode("32TNL83");
* System.out.println(loc);
* }
*
* should display (locale may vary):
*
* {@preformat text
* ┌─────────────────────────────────────────────────────────────────┐
* │ Location type: Grid coordinate │
* │ Geographic identifier: 32TNL83 │
* │ West bound: 580,000 m — 9°57′00″E │
* │ Representative value: 585,000 m — 10°00′36″E │
* │ East bound: 590,000 m — 10°04′13″E │
* │ South bound: 4,530,000 m — 40°54′58″N │
* │ Representative value: 4,535,000 m — 40°57′42″N │
* │ North bound: 4,540,000 m — 41°00′27″N │
* │ Coordinate reference system: WGS 84 / UTM zone 32N │
* │ Administrator: North Atlantic Treaty Organization │
* └─────────────────────────────────────────────────────────────────┘
* }
* </div>
*
* <h2>Immutability and thread safety</h2>
* This class is immutable and thus thread-safe.
* However the {@link Coder Coder} instances performing conversions between references and coordinates
* are not thread-safe; it is recommended to create a new {@code Coder} instance for each thread.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
*
* @see CommonCRS#universal(double, double)
* @see <a href="https://en.wikipedia.org/wiki/Military_Grid_Reference_System">Military Grid Reference System on Wikipedia</a>
*
* @since 0.8
* @module
*/
@XmlTransient
public class MilitaryGridReferenceSystem extends ReferencingByIdentifiers {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 8337394374656125471L;
/**
* Height of latitude bands, in degrees.
* Those bands are labeled from {@code 'C'} to {@code 'X'} inclusive, excluding {@code 'I'} and {@code 'O'}.
*/
static final double LATITUDE_BAND_HEIGHT = 8;
/**
* Size of the 100 kilometres squares, in metres.
*/
static final double GRID_SQUARE_SIZE = 100_000;
/**
* Number of letters available for grid rows. Those letters are "ABCDEFGHJKLMNPQRSTUV" (starting at letter
* F for zones of even number), repeated in a cycle. Each row is {@value #GRID_SQUARE_SIZE} metres height.
*/
static final int GRID_ROW_COUNT = 20;
/**
* The number of digits in a one-meter precision when formatting MGRS references.
*
* <p><b>Invariant:</b> the following relationship must hold:
* {@code GRID_SQUARE_SIZE == Math.pow(10, METRE_PRECISION_DIGITS)}
*/
static final int METRE_PRECISION_DIGITS = 5;
/**
* The first of the two letters ({@code 'I'} and {@code 'O'}) excluded in MGRS notation.
* This letter and all following letters shall be shifted by one character. Example:
*
* {@preformat java
* char band = ...;
* if (band >= EXCLUDE_I) {
* band++;
* if (band >= EXCLUDE_O) band++;
* }
* }
*
* or equivalently:
*
* {@preformat java
* char band = ...;
* if (band >= EXCLUDE_I && ++band >= EXCLUDE_O) band++;
* }
*/
private static final char EXCLUDE_I = 'I';
/**
* The second of the two letters ({@code 'I'} and {@code 'O'}) excluded in MGRS notation.
*/
private static final char EXCLUDE_O = 'O';
/**
* The column letters used in Polar Stereographic Projections.
* They are letters A to Z but omitting I, O, D, E, M, N, V, W.
*/
private static final byte[] POLAR_COLUMNS = {
'A','B','C','F','G','H','J','K','L','P','Q','R','S','T','U','X','Y','Z'
};
/**
* The object to use for computing zone number and central meridian. This is a static final
* field in current Apache SIS version, but could become configurable in a future version.
*/
private static final TransverseMercator.Zoner ZONER = TransverseMercator.Zoner.UTM;
/**
* The datum to which to transform the coordinate before formatting the MGRS reference.
* Only the datums enumerated in {@link CommonCRS} are currently supported.
*/
final CommonCRS datum;
/**
* Whether {@link MilitaryGridReferenceSystem.Encoder} should infer the datum from the given coordinates
* instead than using {@link #datum}.
*/
final boolean avoidDatumChange;
/**
* Value to add to the row number in order to have the "A" letter on the northernmost value on Greenwich meridian of
* the Universal Polar Stereographic (UPS) South projection. Value is initially zero and computed when first needed.
* This is derived from the bottom of the 100 kilometres square labeled "A" in Grid Zone Designations A and B.
*/
private transient short southOffset;
/**
* Value to add to the row number in order to have the "A" letter on the southernmost value on Greenwich meridian of
* the Universal Polar Stereographic (UPS) North projection. Value is initially zero and computed when first needed.
* This is derived from the bottom of the 100 kilometres square labeled "A" in Grid Zone Designations Y and Z.
*/
private transient short northOffset;
/**
* Creates a new Military Grid Reference System (MGRS) using the default datum.
* The current Apache SIS version uses the {@linkplain CommonCRS#WGS84 WGS84} datum,
* but this choice may change in the future if there is a need to adapt to new MGRS specifications.
*/
public MilitaryGridReferenceSystem() {
super(properties(), types());
datum = CommonCRS.WGS84;
avoidDatumChange = false;
}
/**
* Creates a new Military Grid Reference System (MGRS) using the specified datum.
* Only the datums enumerated in {@link CommonCRS} are currently supported.
*
* @param properties the properties to be given to the reference system.
* @param datum the datum to which to transform coordinates before formatting the MGRS references,
* or {@code null} for inferring the datum from the CRS associated to each coordinate.
*/
public MilitaryGridReferenceSystem(final Map<String,?> properties, final CommonCRS datum) {
super(properties, types());
this.datum = (datum != null) ? datum : CommonCRS.WGS84;
avoidDatumChange = (datum == null);
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
@Workaround(library="JDK", version="1.8")
private static Map<String,?> properties() {
AbstractParty party = new AbstractParty("North Atlantic Treaty Organization", null);
return properties(new NamedIdentifier(null, "NATO", Resources.formatInternational(Resources.Keys.MGRS), null, null), party);
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
@Workaround(library="JDK", version="1.8")
private static ModifiableLocationType[] types() {
final ModifiableLocationType gzd = new ModifiableLocationType(Resources.formatInternational(Resources.Keys.GridZoneDesignator));
final ModifiableLocationType square = new ModifiableLocationType(Resources.formatInternational(Resources.Keys.SquareIdentifier100));
final ModifiableLocationType coord = new ModifiableLocationType(Resources.formatInternational(Resources.Keys.GridCoordinates));
gzd .addIdentification(Vocabulary.formatInternational(Vocabulary.Keys.Code));
coord .addIdentification(Vocabulary.formatInternational(Vocabulary.Keys.Coordinate));
square.addParent(gzd);
coord .addParent(square);
return new ModifiableLocationType[] {gzd};
}
/**
* Returns the value to add to the row number in order to have the "A" letter on the southernmost or
* northernmost value on Greenwich meridian of the Universal Polar Stereographic (UPS) projection.
* If {@code south} is {@code true}, then this is computed from the northernmost value of UPS South;
* otherwise this is computed from the southernmost value of UPS North. This value is derived from
* the bottom of the 100 kilometres square labeled "A" in Grid Zone Designations A, B, Y and Z.
*/
final int polarOffset(final boolean south) throws TransformException {
// No need to synchronized; not a big deal if computed twice.
short origin = south ? southOffset : northOffset;
if (origin == 0) {
final DirectPosition2D position = new DirectPosition2D(
south ? TransverseMercator.Zoner.SOUTH_BOUNDS
: TransverseMercator.Zoner.NORTH_BOUNDS, 0);
double northing = datum.universal(position.x * 1.01, position.y).getConversionFromBase()
.getMathTransform().transform(position, position).getOrdinate(1);
if (south) {
northing = 2*PolarStereographicA.UPS_SHIFT - northing;
}
northing = Math.floor(northing / GRID_SQUARE_SIZE);
origin = (short) northing;
if (origin != northing) { // Paranoiac check (should never happen).
throw new GazetteerException();
}
if (south) {
southOffset = origin;
} else {
northOffset = origin;
}
}
return origin;
}
/**
* Returns a new object performing conversions between {@code DirectPosition} and MGRS references.
* 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 MGRS references.
*/
public Coder createCoder() {
return new Coder();
}
/**
* 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}.
* 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>
*
* <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 0.8
* @since 0.8
* @module
*/
public class Coder {
/**
* Number of digits to use for formatting the numerical part of a MGRS reference.
*
* @see #getPrecision()
*/
private byte digits;
/**
* The separator to insert between each component of the MGRS identifier, or an empty string if none.
*/
private String separator;
/**
* Same separator, but without leading and trailing spaces.
*/
private String trimmedSeparator;
/**
* Whether the decoded locations should be clipped to the valid area of MGRS cell.
*
* @see #getClipToValidArea()
*/
private boolean clipToValidArea;
/**
* Cached information needed for building a MGRS reference from a direct position in the given CRS.
*/
private final Map<CoordinateReferenceSystem,Encoder> encoders;
/**
* Temporary positions used for encoding. References are kept for avoiding
* to recreate those temporary objects for every reference to parse or format.
*/
transient DirectPosition normalized, geographic;
/**
* A buffer where to create reference, to be reused for each new reference.
*/
final StringBuilder buffer = new StringBuilder(18); // Length of "4 Q FJ 12345 67890" sample value.
/**
* Creates a new coder initialized to the default precision and separator.
*/
protected Coder() {
digits = METRE_PRECISION_DIGITS; // 1 metre precision.
separator = trimmedSeparator = "";
encoders = new IdentityHashMap<>();
clipToValidArea = true;
}
/**
* Creates a new coder initialized to the same setting than the given separator.
* The new instance will share the same {@link #encoders} map than the original instance.
* This is okay only if all calls to {@link #encoder(CoordinateReferenceSystem)} are done
* in the same thread before any call to {@link IteratorAllZones#trySplit()}.
*/
Coder(final Coder other) {
digits = other.digits;
separator = other.separator;
trimmedSeparator = other.trimmedSeparator;
clipToValidArea = other.clipToValidArea;
encoders = other.encoders;
}
/**
* Returns the reference system for which MGRS references will be encoded or decoded.
*
* @return the enclosing reference system.
*/
final MilitaryGridReferenceSystem getReferenceSystem() {
return MilitaryGridReferenceSystem.this;
}
/**
* Returns the precision of the references formatted by this coder.
* This method returns one of the following values:
*
* <table class="sis">
* <caption>MGRS reference precisions</caption>
* <tr><th>Precision (m)</th> <th>Reference example</th></tr>
* <tr><td style="text-align:right">1</td> <td>4 Q FJ 12345 67890</td></tr>
* <tr><td style="text-align:right">10</td> <td>4 Q FJ 1234 6789</td></tr>
* <tr><td style="text-align:right">100</td> <td>4 Q FJ 123 678</td></tr>
* <tr><td style="text-align:right">1000</td> <td>4 Q FJ 12 67</td></tr>
* <tr><td style="text-align:right">10 000</td> <td>4 Q FJ 1 6</td></tr>
* <tr><td style="text-align:right">100 000</td> <td>4 Q FJ</td></tr>
* <tr><td style="text-align:right">(approximated) 1 000 000</td> <td>4 Q</td></tr>
* </table>
*
* Values smaller than 1 (e.g. 0.01 for a centimetre precision) may also be returned
* if that value has been {@linkplain #setPrecision(double) explicitly set},
* but sub-metric precision are usually not used with MGRS.
*
* @return precision of formatted references in metres.
*/
public double getPrecision() {
return MathFunctions.pow10(METRE_PRECISION_DIGITS - digits);
}
/**
* Sets the desired precision of the references formatted by this coder.
* This method rounds the given precision to one of the power of 10
* documented in the {@link #getPrecision()} method.
*
* @param precision the desired precision in metres.
*/
public void setPrecision(final double precision) {
final int p;
try {
p = DecimalFunctions.floorLog10(precision);
} catch (ArithmeticException e) {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "precision", precision), e);
}
// The -3 is an arbitrary limit to millimetre precision.
int n = Math.max(-3, Math.min(METRE_PRECISION_DIGITS + 1, p));
digits = (byte) (METRE_PRECISION_DIGITS - n);
}
/**
* For internal use by other internal classes in this Java source file.
*/
final int digits() {
return digits;
}
/**
* 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.
* By default the separator is an empty string, which produce references like "4QFJ12345678".
*
* @return the separator to insert between each component of the MGRS identifier, or an empty string if none.
*/
public String getSeparator() {
return separator;
}
/**
* Sets 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.
* By default the separator is an empty string, which produce references like "4QFJ12345678".
* If the separator is set to a space, then the references will be formatted like "4 Q FJ 1234 5678".
*
* <p>Note that a MGRS reference is normally written as an entity without spaces, parentheses, dashes,
* or decimal points. Invoking this method with a non-empty separator produces non-conform MGRS, but
* is sometime convenient for readability or for use in file systems (with the {@code '/'} separator).</p>
*
* @param separator the separator to insert between each component of the MGRS identifier.
*/
public void setSeparator(final String separator) {
ArgumentChecks.ensureNonNull("separator", separator);
this.separator = separator;
trimmedSeparator = CharSequences.trimWhitespaces(separator);
}
/**
* Returns whether the decoded locations should be clipped to the valid area.
* The default value is {@code true}.
*
* @return {@code true} if decoded locations are clipped to the valid area.
*/
public boolean getClipToValidArea() {
return clipToValidArea;
}
/**
* Sets whether the decoded locations should be clipped to the valid area.
* MGRS 100 km squares can actually be smaller than 100 km when the square overlaps two UTM zones or
* two latitude bands. We may have half of a square in a zone and the other half in the other zone.
* By default, the {@link #decode(CharSequence)} method clips the square to the zone where it belongs.
* Invoking this method with the {@code false} value disables this behavior.
*
* @param clip whether the decoded locations should be clipped to the valid area.
*/
public void setClipToValidArea(final boolean clip) {
clipToValidArea = clip;
}
/**
* Bridge to {@link MilitaryGridReferenceSystem#datum}
* for the {@link Encoder} and {@link Decoder} classes.
*/
final ProjectedCRS projection(final double latitude, final double longitude) {
return MilitaryGridReferenceSystem.this.datum.universal(latitude, longitude);
}
/**
* 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 FactoryException if the creation of a coordinate operation failed.
* @throws TransformException if the creation of an inverse operation failed.
*/
final Encoder encoder(final CoordinateReferenceSystem crs) throws FactoryException, TransformException {
if (crs == null) {
throw new GazetteerException(Errors.format(Errors.Keys.UnspecifiedCRS));
}
// We can not use encoders.computeIfAbsent(crs, ...) because of checked exceptions.
Encoder encoder = encoders.get(crs);
if (encoder == null) {
encoder = new Encoder(avoidDatumChange ? null : datum, crs);
if (encoders.put(crs, encoder) != null) {
throw new ConcurrentModificationException(); // Opportunistic check.
}
}
return encoder;
}
/**
* Encodes the given position into a MGRS reference.
* The given position must have a Coordinate Reference System (CRS) associated to it.
*
* @param position the coordinate to encode.
* @return MGRS encoding of the given position.
* @throws TransformException if an error occurred while transforming the given coordinate to a MGRS reference.
*/
public String encode(final DirectPosition position) throws TransformException {
ArgumentChecks.ensureNonNull("position", position);
try {
return encoder(position.getCoordinateReferenceSystem()).encode(this, position, true, getSeparator(), digits());
} catch (IllegalArgumentException | FactoryException e) {
throw new GazetteerException(e.getLocalizedMessage(), e);
}
}
/**
* Returns an iterator over all MGRS references that intersect the given envelope.
* The given envelope must have a Coordinate Reference System (CRS) associated to it.
* If the CRS is geographic, the envelope is allowed to span the anti-meridian.
* The MGRS references may be returned in any iteration order.
*
* <div class="note"><b>Possible evolution:</b>
* current implementation does not clip the cells to UPS/UTM valid areas before to test for intersection
* with {@code areaOfInterest}. Consequently the iterator may return slightly more cells than expected.
* A future version may filter the cells more accurately. If an application needs the same set of cells
* than what current the implementation returns, it can invoke <code>{@linkplain #setClipToValidArea
* setClipToValidArea}(false)</code> for preserving current behavior in future Apache SIS versions.</div>
*
* @param areaOfInterest envelope of desired MGRS references.
* @return an iterator over MGRS references intersecting the given area of interest.
* @throws TransformException if an error occurred while transforming the area of interest.
*/
public Iterator<String> encode(final Envelope areaOfInterest) throws TransformException {
ArgumentChecks.ensureNonNull("areaOfInterest", areaOfInterest);
try {
return Spliterators.iterator(new IteratorAllZones(areaOfInterest).simplify());
} catch (IllegalArgumentException | FactoryException e) {
throw new GazetteerException(e);
}
}
/**
* Returns a stream of all MGRS references that intersect the given envelope.
* The given envelope must have a Coordinate Reference System (CRS) associated to it.
* If the CRS is geographic, the envelope is allowed to span the anti-meridian.
* The MGRS references may be returned in any order.
*
* <div class="note"><b>Possible evolution:</b>
* current implementation does not clip the cells to UPS/UTM valid areas before to test for intersection
* with {@code areaOfInterest}. Consequently the iterator may return slightly more cells than expected.
* A future version may filter the cells more accurately. If an application needs the same set of cells
* than what current the implementation returns, it can invoke <code>{@linkplain #setClipToValidArea
* setClipToValidArea}(false)</code> for preserving current behavior in future Apache SIS versions.</div>
*
* @param areaOfInterest envelope of desired MGRS references.
* @param parallel {@code true} for a parallel stream, or {@code false} for a sequential stream.
* @return a stream of MGRS references intersecting the given area of interest.
* @throws TransformException if an error occurred while transforming the area of interest.
*/
public Stream<String> encode(final Envelope areaOfInterest, final boolean parallel) throws TransformException {
ArgumentChecks.ensureNonNull("areaOfInterest", areaOfInterest);
try {
return StreamSupport.stream(new IteratorAllZones(areaOfInterest).simplify(), parallel);
} catch (IllegalArgumentException | FactoryException e) {
throw new GazetteerException(e);
}
}
/**
* Decodes the given MGRS reference into a position and an envelope.
* The Coordinate Reference System (CRS) associated to the returned position depends on the given reference.
*
* <div class="warning"><b>Upcoming API change — generalization</b><br>
* in a future SIS version, the type of returned element may be generalized
* to the {@code org.opengis.referencing.gazetteer.Location} interface.
* This change is pending GeoAPI revision.</div>
*
* @param reference MGRS string to decode.
* @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.
*/
public AbstractLocation decode(final CharSequence reference) throws TransformException {
ArgumentChecks.ensureNonEmpty("reference", reference);
return new Decoder(this, reference);
}
/**
* Iterator over the cells inside all UPS and UTM zones inside a given area of interest.
* Each UPS or UTM zone is processed by a separated iterator, each of them with its own
* {@link Encoder} instance.
*
* @see IteratorOneZone
*/
private final class IteratorAllZones implements Spliterator<String> {
/**
* The iterators over a single UTM zone.
*/
private final Spliterator<String>[] iterators;
/**
* Index of the current iterator.
*/
private int index;
/**
* Index after the last iterator to return.
*/
private int upper;
/**
* Creates a new iterator over MGRS cells in the given area of interest.
* The borders of the given envelope are considered <strong>exclusive</strong>.
*/
@SuppressWarnings({"unchecked", "rawtypes"}) // Generic array creation.
IteratorAllZones(final Envelope areaOfInterest) throws FactoryException, TransformException {
final SingleCRS sourceCRS = CRS.getHorizontalComponent(areaOfInterest.getCoordinateReferenceSystem());
if (sourceCRS == null) {
throw new GazetteerException(org.apache.sis.internal.referencing.Resources.format(
org.apache.sis.internal.referencing.Resources.Keys.NonHorizontalCRS_1, "areaOfInterest"));
}
final int precision = (int) getPrecision();
if (precision <= 0 || precision > (int) GRID_SQUARE_SIZE) {
throw new GazetteerException(Errors.format(Errors.Keys.ValueOutOfRange_4,
"precision", 1, (int) GRID_SQUARE_SIZE, precision));
}
/*
* Convert area of interest (AOI) from an envelope to a Rectangle2D for use with
* Envelope2D.intersect(Rectangle2D) during IteratorOneZone.advance(…) execution.
* We need to use the constructor expecting the two corners in order to preserve
* envelope spanning the anti-meridian.
*/
final IntervalRectangle aoi = new IntervalRectangle(areaOfInterest.getLowerCorner(),
areaOfInterest.getUpperCorner());
/*
* Project the area of interest (AOI) to normalized geographic coordinates for computing UTM zones
* and for IteratorOneZone construction. For computing UTM zones, envelopes that cross the anti-
* meridian should have a negative width. For IteratorOneZone construction, it does not matter.
*/
final Envelope geographicArea = Envelopes.transform(areaOfInterest, datum.normalizedGeographic());
final double φmin = geographicArea.getMinimum(1);
final double φmax = geographicArea.getMaximum(1);
boolean southPole = min < TransverseMercator.Zoner.SOUTH_BOUNDS);
boolean northPole = max > TransverseMercator.Zoner.NORTH_BOUNDS);
boolean southUTM = false;
boolean northUTM = false; // Default value for UPS, to be modified later if UTM.
int zoneStart = 0; // Zero-based (i.e. standard zone number minus 1).
int zoneEnd = 0; // Exclusive.
final int zoneCount = ZONER.zoneCount();
if max >= TransverseMercator.Zoner.SOUTH_BOUNDS && φmin < TransverseMercator.Zoner.NORTH_BOUNDS) {
southUTM = min < 0);
northUTM = max >= 0);
if (geographicArea.getSpan(0) > (Longitude.MAX_VALUE - Longitude.MIN_VALUE) - ZONER.width) {
zoneEnd = zoneCount;
} else {
/*
* Use of lower and upper corners below are not the same than calls to Envelope.getMinimum(0)
* or Envelope.getMaximum(0) if the envelope crosses the anti-meridian.
*/
zoneStart = ZONER.zone(0, geographicArea.getLowerCorner().getOrdinate(0)) - 1; // Inclusive.
zoneEnd = ZONER.zone(0, geographicArea.getUpperCorner().getOrdinate(0)); // Exclusive.
if (zoneEnd < zoneStart) {
zoneEnd += zoneCount; // Envelope crosses the anti-meridian.
}
}
}
/*
* At this point we finished collecting the information that we will need (whether we will
* iterate over the north or south pole, the range of UTM zones, etc). For each UPS or UTM
* zone, we pick a representative φ value for the purpose of CommonCRS.universal(…) method
* call and we prepare an iterator on which we will delegate computations.
*/
upper = zoneEnd - zoneStart;
if (southUTM & northUTM) upper *= 2;
if (southPole) upper += 2;
if (northPole) upper += 2;
iterators = new Spliterator[upper];
int zone = zoneStart;
upper = 0;
do {
final double λ = ZONER.centralMeridian((zone % zoneCount) + 1);
final double φ;
if (southPole) {
φ = Latitude.MIN_VALUE;
southPole = false; // For next iteration.
} else if (southUTM) {
φ = -1;
if (++zone >= zoneEnd) {
zone = zoneStart;
southUTM = false; // For next iteration.
}
} else if (northUTM) {
φ = +1;
if (++zone >= zoneEnd) {
northUTM = false; // For next iteration.
}
} else if (northPole) {
φ = Latitude.MAX_VALUE;
northPole = false; // For loop termination.
} else {
throw new AssertionError();
}
final ProjectedCRS targetCRS = datum.universal(φ, λ);
Spliterator<String> iter = new IteratorOneZone(Coder.this, aoi, geographicArea, sourceCRS, targetCRS, precision);
do iterators[upper++] = iter;
while ((iter = iter.trySplit()) != null);
} while (southPole | northPole | southUTM | northUTM);
}
/**
* Creates an iterator over the first half of the zones covered by the given iterator.
* After construction, the given iterator will cover the second half. This constructor
* is for {@link #trySplit()} method only.
*/
private IteratorAllZones(final IteratorAllZones half) {
iterators = half.iterators;
index = half.index;
upper = (half.upper + index) / 2;
half.index = upper;
}
/**
* If this iterator can be partitioned, returns an iterator covering approximately
* the first half of MGRS references and update this iterator for covering the other
* half. Each iterator will use a disjoint set of projected CRS.
*/
@Override
public Spliterator<String> trySplit() {
return (upper - index >= 2) ? new IteratorAllZones(this).simplify() : null;
}
/**
* If this iterator is backed by only one worker iterator, returns that worker iterator.
* Otherwise returns {@code this}. This method should be invoked after construction.
*/
final Spliterator<String> simplify() {
return (upper - index == 1) ? iterators[index] : this;
}
/**
* Guess the number of elements to be returned. The value returned by this method is very rough,
* and likely greater than the real amount of elements that will actually be returned.
*/
@Override
public long estimateSize() {
long n = 0;
for (int i=index; i<upper; i++) {
n += iterators[i].estimateSize();
}
return n;
}
/**
* Performs the given action on the remaining MGRS reference, if any.
*/
@Override
public boolean tryAdvance(final Consumer<? super String> action) {
while (index < upper) {
if (iterators[index].tryAdvance(action)) {
return true;
}
index++;
}
return false;
}
/**
* Performs the given action on all remaining MGRS references.
*/
@Override
public void forEachRemaining(final Consumer<? super String> action) {
while (index < upper) {
iterators[index++].forEachRemaining(action);
}
}
/**
* Specifies that the list of elements is immutable, that all elements will be distinct
* and that this iterator never return {@code null} element.
*/
@Override
public int characteristics() {
return IMMUTABLE | DISTINCT | NONNULL;
}
}
}
/**
* Iterator over the cells in a single UTM or UPS zone. There is exactly one {@code IteratorOneZone} instance
* for each Universal Polar Stereographic (UPS) or Universal Transverse Mercator (UTM) projection covered by
* the area of interest. A given {@code IteratorOneZone} instance use the same projection for all cells.
*
* <p>This class extends {@link Coder} in order to freeze the configuration (separator, precision, <i>etc</i>)
* to the values they have at iterator creation time, and because if we parallelize the iteration, each iterator
* will need its own {@link Coder#buffer}, {@link Coder#normalized} and {@link Coder#geographic} cache.</p>
*
* @see Coder.IteratorAllZones
*/
private final class IteratorOneZone extends Coder implements Spliterator<String> {
/**
* The region for which to return MGRS codes. This envelope can be in any CRS.
* This shape shall not be modified since the same instance will be shared by
* many {@code IteratorOneZone}s.
*/
private final Rectangle2D areaOfInterest;
/**
* The transform from the CRS of {@link #encoder} to the CRS of {@link #areaOfInterest}.
*/
private final MathTransform2D gridToAOI;
/**
* The encoder to use for creating MGRS codes.
*/
private final Encoder encoder;
/**
* The easting value of the projection natural origin. This information is used for determining
* whether {@link #gridX} is on the left side or on right side of the center of UPS or UTM zone.
* We use this information for choosing the cell corner which is closest to map projection origin.
*/
private final int xCenter;
/**
* The {@link #gridX} value where to stop iteration, exclusive. This value is always greater than
* {@code gridX} (unless the iteration finished), but is not necessarily greater than {@link #xCenter}.
*/
private final int xEnd;
/**
* The first <var>northing</var> value to use in iteration.
* The {@link #gridY} value will need to be reset to this value for each new column.
*/
private int yStart;
/**
* The {@link #gridY} values where to stop iteration, exclusive. Invariants:
* <ul>
* <li>{@code gridY < yEnd} shall be true in the North hemisphere, and
* {@code gridY > yEnd} shall be true in the South hemisphere.</li>
* </ul>
*
* The reason of {@code gridY} relationship dependency to the hemisphere is because
* we try to iterate from equator to the pole.
*/
private int yEnd;
/**
* Position of the next MGRS reference to encode. Position is composed of the latitude band number,
* the row and column indices of current 100 km square, and finally the grid coordinates inside the
* current 100 km square. All those components are inferred from the (easting, northing) values.
*/
private int gridX, gridY;
/**
* The amount of metres to add to {@code gridX} and to add or subtract to {@code gridY} during iteration.
* The sign to use when updating the {@code gridY} value depends on whether we are in the North or South
* hemisphere.
*/
private final int step;
/**
* Whether this iterator should iterates downward over rows. If {@code true}, then {@link #step} shall be
* subtracted to {@link #gridY} instead of added. We iterate downward in UTM south zones in order to go
* from equator to pole. This direction allows some optimizations.
*/
private final boolean downward;
/**
* Whether this iterator is allowed to skip some cells when testing for inclusion in the area of interest.
* Since {@code IteratorOneZone} iterates in UTM zone from equator to pole, the range of longitude values
* will only decrease (the minimal longitude increase and the maximal longitude decrease). Consequently
* if we found a longitude out of range, we don't need to test that longitude again in next row.
*
* <p>This optimization is allowed only under the following conditions:</p>
* <ul>
* <li>{@link #areaOfInterest} is a rectangle in geographic coordinates.</li>
* <li>{@link #areaOfInterest} does not intersect Norway and Svalbard special cases.</li>
* <li>We iterate in a UTM zone, or in a UPS zone contained fully in the upper half or fully in lower half.</li>
* </ul>
*/
private final boolean optimize;
/**
* Latitude band of the last MGRS reference encoded by the iterator.
* This information is used for detecting when we moved to a new latitude band.
*/
private char latitudeBand;
/**
* A MGRS reference which was pending return by {@link #tryAdvance(Consumer)} before to continue iteration.
* This field may be non-null immediately after a change of latitude band, and should be null otherwise.
*/
private String pending;
/**
* Temporary rectangle for computation purpose. Needs to be an implementation from the
* {@link org.apache.sis.geometry} in order to support AOI spanning the anti-meridian.
*/
private final Envelope2D cell = new Envelope2D();
/**
* Returns a new iterator for creating MGRS codes in a single UTM or UPS zone.
* The borders of the {@code areaOfInterest} rectangle are considered <strong>exclusive</strong>.
*
* <p>For envelopes that cross the anti-meridian, it does not matter if {@code geographicArea} uses
* the negative width convention or is expanded to the [-180 … 180]° of longitude range, because it
* will be clipped to the projection domain of validity anyway. However the {@code areaOfInterest}
* should use the negative width convention.</p>
*
* @param areaOfInterest the envelope for which to return MGRS codes. This envelope can be in any CRS.
* @param geographicArea the area of interest transformed into a normalized geographic CRS.
* @param sourceCRS the horizontal part of the {@code areaOfInterest} CRS.
* @param targetCRS the UTM or UPS projected CRS of the zone for which to create MGRS references.
* @param step the amount of metres to add or subtract to grid coordinates during iteration.
*/
IteratorOneZone(final Coder coder, Rectangle2D areaOfInterest, final Envelope geographicArea,
final SingleCRS sourceCRS, final ProjectedCRS targetCRS, final int step)
throws FactoryException, TransformException
{
super(coder);
this.areaOfInterest = areaOfInterest;
this.encoder = encoder(targetCRS);
this.step = step;
/*
* Compute the geographic bounds of the UPS or UTM zone of validity, together with a representative point
* (φ,λ₀). We will need to clip the area of interest to those bounds before to project that area, because
* the UPS and UTM projections can not cover the whole world.
*/
double λmin, λmax, φmin, φmax;
final int zone = Math.abs(encoder.crsZone);
if (zone == Encoder.POLE) {
xCenter = PolarStereographicA.UPS_SHIFT;
if (encoder.crsZone < 0) {
φmin = Latitude.MIN_VALUE;
φmax = TransverseMercator.Zoner.SOUTH_BOUNDS;
} else {
φmin = TransverseMercator.Zoner.NORTH_BOUNDS;
φmax = Latitude.MAX_VALUE;
}
λmin = Longitude.MIN_VALUE;
λmax = Longitude.MAX_VALUE;
} else {
xCenter = (int) ZONER.easting;
if (encoder.crsZone < 0) {
φmin = TransverseMercator.Zoner.SOUTH_BOUNDS;
φmax = 0;
} else {
φmin = 0;
φmax = TransverseMercator.Zoner.NORTH_BOUNDS;
}
final double λ0 = ZONER.centralMeridian(zone);
λmin = λ0 - ZONER.width / 2;
λmax = λ0 + ZONER.width / 2;
}
double t;
boolean clip = false;
if ((t = geographicArea.getMinimum(1)) >= φmin) φmin = t; else clip = true;
if ((t = geographicArea.getMaximum(1)) <= φmax) φmax = t; else clip = true;
if ((t = geographicArea.getMinimum(0)) >= λmin) λmin = t; else clip = true;
if ((t = geographicArea.getMaximum(0)) <= λmax) λmax = t; else clip = true;
boolean isSpecialCase = ZONER.isSpecialCasemin, φmax, λmin, λmax);
if (clip) {
/*
* If we detected that the given area of interest is larger than UPS or UTM domain of validity
* (in which case above code clipped the geographic bounds), project the CRS domain of validity
* to the envelope CRS so we can clip it. Here, "domain of validity" is relative to MGRS grid,
* not necessarily to the ISO 19111 domain of validity.
*/
final IntervalRectangle r = new IntervalRectanglemin, φmin, λmax, φmax);
r.setRect(Shapes2D.transform(CRS.findOperation(geographicArea.getCoordinateReferenceSystem(), sourceCRS, null), r, r));
r.intersect(areaOfInterest);
/*
* We need an envelope that do not cross the anti-meridian. If the specified area of interest (AOI)
* crosses the anti-meridian (which should happen only with geographic envelopes), then the call to
* r.intersect(areaOfInterest) may have overwritten our computation with the AOI longitude range.
* Overwrite again that range with the domain of validity of current UTM zone,
* based on the following assumptions:
*
* 1) 'sourceCRS' is geographic (otherwise the AOI should not cross the anti-meridian).
* 2) the "normalized" AOI longitude range is [-180 … 180]°, in which case intersection
* with [λmin … λmax] is [λmin … λmax] itself.
*/
if (r.xmax < r.xmin) {
r.xmin = λmin;
r.xmax = λmax;
}
areaOfInterest = r; // Never cross the anti-meridian, even if the original AOI did.
}
/*
* At this point, the area of interest has been clipped to the UPS or UTM domain of validity.
* Now we can project that area to the CRS managed by this iterator. All values after projection
* should be positive (because UPS and UTM are designed that way).
*/
final CoordinateOperation op = CRS.findOperation(sourceCRS, targetCRS, null);
final Rectangle2D bounds = Shapes2D.transform(op, areaOfInterest, null);
gridX = (((int) (bounds.getMinX() / step))) * step; // Inclusive
gridY = (((int) (bounds.getMinY() / step))) * step;
xEnd = (((int) Math.ceil(bounds.getMaxX() / step))) * step; // Exclusive
yEnd = (((int) Math.ceil(bounds.getMaxY() / step))) * step;
/*
* Determine if we should iterate on rows upward or downward. The intent is to iterate from equator to pole
* in UTM zones, or from projection center to projection border in UPS cases. Those directions enable some
* optimizations.
*/
if (zone != Encoder.POLE) {
downward = (encoder.crsZone < 0); // Upward in UTM North zones, downward in UTM South zones.
} else {
downward = yEnd <= PolarStereographicA.UPS_SHIFT; // Downward only if AOI is fully in the lower half.
/*
* In the polar case, we can not apply the shortcut documented in 'optimize' if there is a hole
* in the UPS projection center. There is a hole if the latitude of the area of interest does not
* reach the pole, or if the longitude range does not make a full circle around the Earth.
*/
isSpecialCase |= λmin != Longitude.MIN_VALUE ||
λmax != Longitude.MAX_VALUE ||
(encoder.crsZone < 0 ? φmin != Latitude .MIN_VALUE
: φmax != Latitude .MAX_VALUE);
}
if (downward) {
final int y = gridY;
gridY = yEnd - step;
yEnd = y - step;
}
yStart = gridY;
gridToAOI = (MathTransform2D) op.getMathTransform().inverse();
/*
* To be strict, we should also test that the region of interest does not intersect both the upper half
* and lower half of Universal Polar Stereographic (UPS) projection domain. We do not check that because
* we need the 'optimize' flag to have the value that we get after 'trySplit()' execution. This implies
* that invoking 'trySplit()' is mandatory before using this iterator, but IteratorAllZones ensures that.
*/
optimize = !isSpecialCase && Utilities.equalsIgnoreMetadata(geographicArea.getCoordinateReferenceSystem(), sourceCRS);
}
/**
* Creates an iterator for the lower half of a Universal Polar Stereographic (UPS) projection,
* and modifies the given iterator for restricting it to the upper half of UPS projection.
* This method is for {@link #trySplit()} usage only.
*/
private IteratorOneZone(final IteratorOneZone other) {
super(other);
areaOfInterest = other.areaOfInterest;
gridToAOI = other.gridToAOI;
encoder = other.encoder;
optimize = other.optimize;
step = other.step;
gridX = other.gridX;
xCenter = other.xCenter;
xEnd = other.xEnd;
yEnd = other.yStart - step; // Bottom of the zone to iterate, exclusive.
yStart = PolarStereographicA.UPS_SHIFT - step; // Top of the zone to iterate, inclusive.
other.yStart = PolarStereographicA.UPS_SHIFT; // Other iterator will be for the upper half.
other.gridY = other.yStart;
gridY = yStart;
downward = true;
assert !other.downward; // Fail if the other iterator is not for upper half.
}
/**
* If this iterator intersects both the upper and lower half on UPS domain, returns an iterator for the
* lower half and modifies this iterator for the upper half. This method <strong>must</strong> be invoked
* before {@code IteratorOneZone} can be used.
*/
@Override
public Spliterator<String> trySplit() {
if (!downward && Math.abs(encoder.crsZone) == Encoder.POLE && gridY < PolarStereographicA.UPS_SHIFT) {
return new IteratorOneZone(this);
}
return null;
}
/**
* Returns an estimation of the number of cells in the area covered by this iterator. The returned value
* may be greater than the real amount since we do not take in account the fact that the number of cells
* in a row become lower as we approach poles.
*/
@Override
public long estimateSize() {
return (xEnd - (long) gridX) * Math.abs(yEnd - (long) yStart) / (step * (long) step);
}
/**
* Computes the next cell reference, if any. This method computes the bounding box in UPS or UTM
* coordinates, verifies if that box intersects the area of interest, and (if it intersects)
* delegates to {@link Encoder} for creating the MGRS reference.
*/
@Override
public boolean tryAdvance(final Consumer<? super String> action) {
return advance(action, false);
}
/**
* Performs the given action for each remaining MGRS codes.
*/
@Override
public void forEachRemaining(final Consumer<? super String> action) {
advance(action, true);
if (pending != null) {
action.accept(pending);
pending = null;
}
}
/**
* Implementation of {@link #tryAdvance(Consumer)} and {@link #forEachRemaining(Consumer)}.
* The {@code all} argument specifies whether this method is invoked for a single element
* or for all remaining ones.
*/
private boolean advance(final Consumer<? super String> action, final boolean all) {
final int digits = digits();
final String separator = getSeparator();
if (normalized == null) {
normalized = new DirectPosition2D();
}
boolean found = false;
try {
do {
if (pending != null) {
action.accept(pending);
pending = null;
found = true;
continue;
}
/*
* Verifies if the current cell should be accepted.
* To be accepted, the cell must complies with two conditions:
*
* 1) It must be inside the area of interest (AOI). Note that the AOI may be in any CRS.
* 2) It must be inside the area of validity (AOV). Since the constructor clipped the AOI
* to the area of validity, this test #2 is redundant with test #1 if both AOI and AOV
* use the same CRS. However if AOI and AOV do not use the same CRS, then condition #1
* does not automatically implies condition #2, so we test both.
*
* Condition #1 is verified by the call to 'areaOfInterest.intersects(…)' below.
* Condition #2 is verified indirectly by the call to 'encoder.encode(…)', which return null
* if the given point is outside the area of validity.
*
* Since MGRS references are created by calls to Encoder.encode(…, DirectPosition, …), we need
* to select the corner closest to projection center. Otherwise Encoder.encode(…) may consider
* that a position is outside the domain of validity while another corner of the same cell would
* have been ok. We resolve this issue by shifting 'gridX' toward projection center if 'gridX'
* is on the left side.
*/
cell.setRect(gridX, gridY, step, step);
cell.setRect(Shapes2D.transform(gridToAOI, cell, cell));
if (cell.intersects(areaOfInterest)) { // Must be invoked on Envelope2D implementation.
int x = gridX;
int y = gridY;
if (x < xCenter) x += step - 1;
if (downward) y += step - 1;
normalized.setOrdinate(0, x);
normalized.setOrdinate(1, y);
String ref = encoder.encode(this, normalized, false, separator, digits);
if (ref != null) {
/*
* If there is a change of latitude band, we may have missed a cell before this one.
* We verify that by encoding the cell for the position just before current cell and
* comparing the latitude band of the result with our previous latitude band.
*/
char previous = latitudeBand;
latitudeBand = encoder.latitudeBand;
if (latitudeBand != previous && previous != 0) {
pending = ref;
normalized.setOrdinate(1, y + (downward ? +1 : -1));
ref = encoder.encode(this, normalized, false, separator, digits);
if (ref == null || encoder.latitudeBand == previous) {
ref = pending; // No result or same result than previous iteration - cancel.
pending = null;
}
}
action.accept(ref);
found = true;
}
} else if (optimize) {
/*
* If this cell is not in the area of interest (AOI) when iterating away from projection
* origin (from equator to pole in UTM case), then all other cells after this one in the
* same column will not be inside the AOI neither. Set 'gridY' to the limit value so the
* condition below will stop the iteration in this column and move to the next column.
*/
gridY = yEnd;
}
/*
* Move to the next cell. We need to do that regardless if the previous block found a cell or not.
* We move from equator to pole (UTM case) or projection center to projection border (UPS case)
* on the same column, than move one column on the right side when we have reached the last row.
*/
final boolean end = downward ? ((gridY -= step) <= yEnd) // UTM South or lower part of UPS.
: ((gridY += step) >= yEnd); // UTM North or upper part of UPS.
if (end) {
gridY = yStart;
latitudeBand = 0;
if ((gridX += step) >= xEnd) {
break;
}
}
} while (all || !found);
} catch (FactoryException | TransformException e) {
// Should never happen since we clipped the area of interest to UPS or UTM domain of valididty.
throw (ArithmeticException) new ArithmeticException(Errors.format(Errors.Keys.OutsideDomainOfValidity)).initCause(e);
}
return found;
}
/**
* Specifies that the list of elements is immutable, that all elements will be distinct
* and that this iterator never return {@code null} element.
*/
@Override
public int characteristics() {
return IMMUTABLE | DISTINCT | NONNULL;
}
/**
* Returns a string representation of this iterator for debugging purpose.
*/
@Override
public String toString() {
return Strings.toString(getClass(), "zone", encoder.crsZone,
"downward", downward, "yStart", yStart, "yEnd", yEnd, "gridX", gridX, "xEnd", xEnd);
}
}
/**
* Conversions from direct positions to Military Grid Reference System (MGRS) references.
* Each {@code Encoder} instance is configured for one {@code DirectPosition} CRS.
* If a position is given in another CRS, another {@code Encoder} instance must be created.
*
* <h4>Immutability and thread safety</h4>
* 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 0.8
*
* @see <a href="https://en.wikipedia.org/wiki/Military_Grid_Reference_System">Military Grid Reference System on Wikipedia</a>
*
* @since 0.8
* @module
*/
static final class Encoder {
/**
* Special {@link #crsZone} value for the UPS (Universal Polar Stereographic) projection.
* Positive value is used for North pole and negative value for South pole.
*/
private static final int POLE = 100;
/**
* The datum to which to transform the coordinate before formatting the MGRS reference.
* Only the datums enumerated in {@link CommonCRS} are currently supported.
*/
private final CommonCRS datum;
/**
* UTM zone of position CRS (negative for South hemisphere), or {@value #POLE} (negative of positive)
* if the CRS is a Universal Polar Stereographic projection, or 0 if the CRS is not a recognized projection.
* Note that this is not necessarily the same zone than the one to use for formatting any given coordinate
* in that projected CRS, since the {@link TransverseMercator.Zoner#zone(double, double)} method has special
* rules for some latitudes.
*/
final int crsZone;
/**
* Coordinate conversion from the position CRS to a CRS of the same type but with normalized axes,
* or {@code null} if not needed. After conversion, axis directions will be (East, North) and axis
* units will be metres.
*
* <p>This transform should perform only simple operations like swapping axis order and unit conversions.
* It should not perform more complex operations that would require to go back to geographic coordinates.</p>
*/
private final MathTransform toNormalized;
/**
* Coordinate conversion or transformation from the <em>normalized</em> CRS to geographic CRS.
* Axis directions are (North, East) as in EPSG geodetic dataset and axis units are degrees.
* This transform is never {@code null}.
*
* <p>This transform may contain datum change from the position datum to the target {@link #datum}.</p>
*/
private final MathTransform toGeographic;
/**
* A transform from a position in the CRS given at construction time to a position in the CRS identified by
* {@link #actualZone}. This field is updated only when a given position is not located in the zone of the
* CRS given at construction time.
*/
private MathTransform toActualZone;
/**
* The actual zone where the position to encode is located. Legal values are the same than {@link #crsZone}.
* If non-zero, then this is the zone of the {@link #toActualZone} transform. This field is updated only when
* a given position is not located in the zone of the CRS given at construction time.
*/
private int actualZone;
/**
* The latitude band of the last encoded reference.
* This information is provided for {@link IteratorOneZone} purpose.
*/
char latitudeBand;
/**
* Creates a new converter from direct positions to MGRS references.
*
* @param datum the datum to which to transform the coordinate before formatting the MGRS reference,
* or {@code null} for inferring the datum from the given {@code crs}.
* @param crs the coordinate reference system of the coordinates for which to create MGRS references.
* @throws IllegalArgumentException if the given CRS do 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.
*/
Encoder(CommonCRS datum, CoordinateReferenceSystem crs) throws FactoryException, TransformException {
CoordinateReferenceSystem horizontal = CRS.getHorizontalComponent(crs);
if (horizontal == null) {
horizontal = crs;
}
if (datum == null) {
datum = CommonCRS.forDatum(horizontal);
}
this.datum = datum;
if (horizontal instanceof ProjectedCRS) {
ProjectedCRS projCRS = (ProjectedCRS) horizontal;
Projection projection = projCRS.getConversionFromBase();
final OperationMethod method = projection.getMethod();
if (IdentifiedObjects.isHeuristicMatchForName(method, TransverseMercator.NAME)) {
crsZone = ZONER.zone(projection.getParameterValues());
} else if (IdentifiedObjects.isHeuristicMatchForName(method, PolarStereographicA.NAME)) {
crsZone = POLE * PolarStereographicA.isUPS(projection.getParameterValues());
} else {
crsZone = 0; // Neither UTM or UPS projection.
}
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
* have the expected orientations and units, then we build a normalized version of
* that CRS and compute the transformation to that CRS.
*/
final DefaultProjectedCRS userAxisOrder = DefaultProjectedCRS.castOrCopy(projCRS);
projCRS = userAxisOrder.forConvention(AxesConvention.NORMALIZED);
if (crs != horizontal || projCRS != userAxisOrder) {
toNormalized = CRS.findOperation(crs, projCRS, null).getMathTransform();
projection = projCRS.getConversionFromBase();
horizontal = projCRS;
crs = projCRS; // Next step in the chain of transformations.
} else {
toNormalized = null; // ProjectedCRS (UTM or UPS) is already normalized.
}
} else {
toNormalized = null; // ProjectedCRS is neither UTM or UPS — will need full reprojection.
}
/*
* We will also need the transformation from the normalized projected CRS to latitude and
* longitude (in that order) in degrees. We can get this transform directly from the
* projected CRS if its base CRS already has the expected axis orientations and units.
*/
if (crs == horizontal && Utilities.equalsIgnoreMetadata(projCRS.getBaseCRS(), datum.geographic())) {
toGeographic = projection.getMathTransform().inverse();
return;
}
} else {
crsZone = 0;
toNormalized = null;
}
toGeographic = CRS.findOperation(crs, datum.geographic(), null).getMathTransform();
}
/**
* Returns the band letter for the given latitude. It is caller responsibility to ensure that the given latitude
* is between {@value TransverseMercator.Zoner#SOUTH_BOUNDS} and {@value TransverseMercator.Zoner#NORTH_BOUNDS}
* inclusive. The returned letter will be one of {@code "CDEFGHJKLMNPQRSTUVWX"} (note that I and O letters are
* excluded). All bands are 8° height except the X band which is 12° height.
*
* @param φ the latitude in degrees for which to get the band letter.
* @return the band letter for the given latitude.
*/
static char latitudeBand(final double φ) {
int band = 'C' + (int) ((φ - TransverseMercator.Zoner.SOUTH_BOUNDS) / LATITUDE_BAND_HEIGHT);
if (band >= EXCLUDE_I && ++band >= EXCLUDE_O && ++band == 'Y') {
band = 'X'; // Because the last latitude band ('X') is 12° height instead of 8°.
}
assert band >= 'C' && band <= 'X' : band;
return (char) band;
}
/**
* 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.
*
* @param owner the {@code Coder} which own this {@code Encoder}.
* @param position the direct position to format as a MGRS reference.
* @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.
* @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
{
final StringBuilder buffer = owner.buffer;
if (toNormalized != null) {
owner.normalized = position = toNormalized.transform(position, owner.normalized);
}
final DirectPosition geographic = toGeographic.transform(position, owner.geographic);
owner.geographic = geographic; // For reuse in next method calls.
final double λ = geographic.getOrdinate(1);
final double φ = geographic.getOrdinate(0);
final boolean isUTM = φ >= TransverseMercator.Zoner.SOUTH_BOUNDS &&
φ < TransverseMercator.Zoner.NORTH_BOUNDS;
final int zone = isUTM ? ZONER.zone(φ, λ) : POLE;
final int signedZone = MathFunctions.isNegative(φ) ? -zone : zone;
if (signedZone == 0) {
// Zero value at this point is the result of NaN of infinite coordinate value.
throw new GazetteerException(Errors.format(Errors.Keys.NotANumber_1, "longitude"));
}
/*
* If the DirectPosition given to this method is not in the expected Coordinate Reference System,
* transform it now. This may happen because the UTM zone computed above is not the same UTM zone
* than the coordinate one, or because the coordinate is geographic instead than projected.
*/
if (signedZone != crsZone) {
if (!reproject) {
return null;
}
if (signedZone != actualZone) {
actualZone = 0; // In case an exception is thrown on the next line.
toActualZone = CRS.findOperation(datum.geographic(), datum.universal(φ, λ), null).getMathTransform();
actualZone = signedZone;
}
geographic.setOrdinate(1, Longitude.normalize(λ));
owner.normalized = position = toActualZone.transform(geographic, owner.normalized);
}
/*
* Grid Zone Designator (GZD).
*/
buffer.setLength(0);
if (isUTM) {
buffer.append(zone).append(separator);
latitudeBand = latitudeBand(φ);
} else {
latitudeBand = (signedZone < 0) ? 'A' : 'Y';
if >= 0) latitudeBand++;
}
buffer.append(latitudeBand);
/*
* 100 kilometres square identification.
*/
if (digits >= 0) {
final double x = position.getOrdinate(0);
final double y = position.getOrdinate(1);
final double cx = Math.floor(x / GRID_SQUARE_SIZE);
final double cy = Math.floor(y / GRID_SQUARE_SIZE);
int col = (int) cx;
int row = (int) cy;
if (isUTM) {
/*
* Specification said that 100,000-meters columns are lettered from A through Z (omitting I and O)
* starting at the 180° meridian, proceeding easterly for 18°, and repeating for each 18° intervals.
* Since a UTM zone is 6° width, a 18° interval is exactly 3 standard UTM zones. Columns in zone 1
* are A-H, zone 2 are J-R (skipping O), zone 3 are S-Z, then repeating every 3 zones.
*/
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'
* should range from 1 to 8 inclusive.
*/
throw new GazetteerException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
switch (zone % 3) { // First A-H sequence starts at zone number 1.
case 1: col += ('A' - 1); break;
case 2: col += ('J' - 1); if (col >= EXCLUDE_O) col++; break;
case 0: col += ('S' - 1); break;
}
/*
* Rows in odd zones are ABCDEFGHJKLMNPQRSTUV
* Rows in even zones are FGHJKLMNPQRSTUVABCDE
* Those 20 letters are repeated in a cycle.
*/
if ((zone & 1) == 0) {
row += ('F' - 'A');
}
row %= GRID_ROW_COUNT;
// 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.
* The column letters go from A to Z, omitting I, O, D, E, M, N, V, W. Rightmost column in
* grid zones A and Y has column letter Z, and the next column in grid zones B and Z starts
* over with column letter A.
*/
final byte[] columns = POLAR_COLUMNS;
col -= PolarStereographicA.UPS_SHIFT / GRID_SQUARE_SIZE;
if (!(λ >= 0)) { // Same condition than in GZD block. Use of ! is for NaN.
col += columns.length; // Letters Z to A from right to left.
}
if (col < 0 || col >= columns.length) {
throw new GazetteerException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
col = columns[col];
row -= owner.getReferenceSystem().polarOffset(signedZone < 0);
}
row += 'A';
if (row >= EXCLUDE_I && ++row >= EXCLUDE_O) row++;
buffer.append(separator).append(letter(col)).append(letter(row));
/*
* Numerical location at the given precision.
* The specification requires us to truncate the number, not to round it.
*/
if (digits > 0) {
final double 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);
}
}
return buffer.toString();
}
/**
* Returns the given character as a {@code char} if it is a letter, or throws an exception otherwise.
* The exception should never happen, unless the the encoder is used for a planet larger than Earth
* for which we do not have enough letters.
*/
private static char letter(final int c) throws GazetteerException {
if (c >= 'A' && c <= 'Z') return (char) c;
throw new GazetteerException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
/**
* Appends the given value in the given buffer, padding with zero digits in order to get
* the specified total amount of digits.
*/
private static void append(final StringBuilder buffer, final int value, int digits) throws GazetteerException {
if (value >= 0) {
final int p = buffer.length();
digits -= (buffer.append(value).length() - p);
if (digits >= 0) {
StringBuilders.repeat(buffer, p, '0', digits);
return;
}
}
throw new GazetteerException(Errors.format(Errors.Keys.OutsideDomainOfValidity));
}
}
/**
* The result of decoding a MGRS reference.
* The {@linkplain #getPosition() position} represents the centroid of the decoded MGRS reference.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.8
* @module
*/
static final class Decoder extends SimpleLocation.Projected {
/**
* Number of bits reserved for storing the minimal northing value of latitude bands in the
* {@link #ROW_RESOLVER} table.
*/
static final int NORTHING_BITS_COUNT = 4;
/**
* Mask over the {@value #NORTHING_BITS_COUNT} lowest bits.
*/
static final int NORTHING_BITS_MASK = (1 << NORTHING_BITS_COUNT) - 1;
/**
* Mapping from latitude bands to the minimal northing values, together with the set of valid rows.
* There is 20 latitude bands identified by letters C to X inclusively, with letters I and O skipped.
* First, the band letter must be converted to an index <var>i</var> in the [0 … 19] range.
* Then, the {@code ROW_RESOLVER[i]} integer values provides the following information:
*
* <ul>
* <li>
* The lowest {@value #NORTHING_BITS_COUNT} bits gives the minimal northing value of all valid
* coordinates in the latitude band, as a multiple of the number of metres in a full cycle of
* {@value #GRID_ROW_COUNT} rows. That northing value can be computed in metre as below:
*
* {@preformat java
* double northing = (ROW_RESOLVER[i] & NORTHING_BITS_MASK) * (GRID_SQUARE_SIZE * GRID_ROW_COUNT);
* }
* </li><li>
* Given a row number <var>row</var> in the [0 … 19] range, the following expression tells
* if that row can be inside the latitude band:
*
* {@preformat java
* boolean isValidRow = (ROW_RESOLVER[i] & (1 << (row + NORTHING_BITS_COUNT))) != 0;
* }
*
* Note that the same row may be valid in two consecutive latitude bands.
* The trailing {@code _0000} parts make room for {@value #NORTHING_BITS_COUNT} bits.
* </li>
* </ul>
*
* The content of this table is verified by {@code MilitaryGridReferenceSystemTest.verifyDecoderTables()}.
* The same method with minor edition can be used for generating this table.
*/
@SuppressWarnings("PointlessBitwiseExpression")
private static final int[] ROW_RESOLVER = {
/*
* First column is the minimal northing value as a multiple of 2000 km.
* Second column enumerates the valid rows in that latitude band.
* Trailing _0000 are for making room for NORTHING_BITS_COUNT bits.
*/
/* Latitude band C (from 80°S) */ 0 | 0b11111111100000000001_0000,
/* Latitude band D (from 72°S) */ 1 | 0b00000000001111111111_0000,
/* Latitude band E (from 64°S) */ 1 | 0b00111111111100000000_0000,
/* Latitude band F (from 56°S) */ 1 | 0b11100000000001111111_0000,
/* Latitude band G (from 48°S) */ 2 | 0b00001111111111000000_0000,
/* Latitude band H (from 40°S) */ 2 | 0b11111000000000011111_0000,
/* Latitude band J (from 32°S) */ 3 | 0b00000011111111110000_0000,
/* Latitude band K (from 24°S) */ 3 | 0b11111110000000000111_0000,
/* Latitude band L (from 16°S) */ 4 | 0b00000000111111111100_0000,
/* Latitude band M (from 8°S) */ 4 | 0b11111111100000000000_0000,
/* Latitude band N (from 0° ) */ 0 | 0b00000000000111111111_0000,
/* Latitude band P (from 8°N) */ 0 | 0b00111111111100000000_0000,
/* Latitude band Q (from 16°N) */ 0 | 0b11100000000001111111_0000,
/* Latitude band R (from 24°N) */ 1 | 0b00001111111111000000_0000,
/* Latitude band S (from 32°N) */ 1 | 0b11111000000000011111_0000,
/* Latitude band T (from 40°N) */ 2 | 0b00000011111111110000_0000,
/* Latitude band U (from 48°N) */ 2 | 0b11111110000000000111_0000,
/* Latitude band V (from 56°N) */ 3 | 0b00000000111111111100_0000,
/* Latitude band W (from 64°N) */ 3 | 0b11111111110000000000_0000,
/* Latitude band X (from 72°N) */ 3 | 0b10000011111111111111_0000
};
/**
* The Coordinate Reference System of the decoded MGRS reference.
* This is an Universal Transverse Mercator (UTM) or Universal Polar Stereographic (UPS) projection.
*
* @see #getCoordinateReferenceSystem()
*/
private final ProjectedCRS crs;
/**
* Decodes the given MGRS reference.
*
* @param owner the {@code Coder} which is creating this {@code Decoder}.
*/
Decoder(final Coder owner, final CharSequence reference) throws TransformException {
super(owner.getReferenceSystem().rootType(), reference);
final int zone; // UTM zone, or 0 if UPS.
boolean hasSquareIdentification; // Whether a square identification is present.
final double φs; // Southernmost bound of latitude band (UTM only).
final double λ0; // Central meridian of UTM zone (ignoring Norway and Svalbard).
boolean isValid = true; // Whether the given reference passes consistency checks.
final int end = CharSequences.skipTrailingWhitespaces(reference, 0, reference.length());
final int base = CharSequences.skipLeadingWhitespaces (reference, 0, end);
int i = endOfDigits(reference, base, end);
if (i == base && i < end) {
/*
* Universal Polar Stereographic (UPS) case. The reference has no zone number
* and begins directly with 3 parts, where each part is exactly one letter:
*
* part 0 — A zone indicator: A or B for South pole, or Y or Z for North pole.
* part 1 — column letter: A to Z, omitting I, O, D, E, M, N, V, W.
* part 2 — row letter: A to Z, omitting I, O.
*/
zone = 0; // Not used in UPS case.
boolean south = false; // True for A and B zones, false for Y and Z zones.
boolean west = false; // True for A and Y zones, false for B and Z zones.
int col = 0, row = 0; // Parsed row and column indices.
for (int part = 0; part <= 2; part++) {
int c = Character.codePointAt(reference, i);
final int ni = i + Character.charCount(c);
if (isLetter(c) || isLetter(c -= ('a' - 'A'))) {
parse: switch (part) {
case 0: {
switch (c) {
case 'A': south = true; west = true; break;
case 'B': south = true; break;
case 'Y': west = true; break;
case 'Z': break;
default : break parse; // Invalid UPS zone.
}
i = nextComponent(owner, reference, base, ni, end);
continue;
}
case 1: {
col = Arrays.binarySearch(POLAR_COLUMNS, (byte) c);
if (col < 0) break; // Invalid column letter.
if (west) col -= POLAR_COLUMNS.length;
col += PolarStereographicA.UPS_SHIFT / GRID_SQUARE_SIZE;
i = nextComponent(owner, reference, base, ni, end);
continue;
}
case 2: {
if (c >= EXCLUDE_O) c--;
if (c >= EXCLUDE_I) c--;
row = (c - 'A') + owner.getReferenceSystem().polarOffset(south);
i = ni;
continue;
}
}
}
/*
* We reach this point only if the current character is invalid (not a letter,
* or not one of the letters expected for the current part).
*/
final short key;
final CharSequence token;
if (part == 0) {
key = Resources.Keys.IllegalUPSZone_1;
token = reference.subSequence(i, ni);
} else {
key = Resources.Keys.IllegalSquareIdentification_1;
token = CharSequences.token(reference, i);
}
throw new GazetteerException(Resources.format(key, token));
}
crs = owner.projections = (south ? Latitude.MIN_VALUE : Latitude.MAX_VALUE), 0);
minX = col * GRID_SQUARE_SIZE;
minY = row * GRID_SQUARE_SIZE;
hasSquareIdentification = true;
λ0 = 0;
} else {
/*
* Universal Transverse Mercator (UTM) case.
*/
zone = parseInt(reference, base, i, Resources.Keys.IllegalUTMZone_1);
if (zone < 1 || zone > 60) {
throw new GazetteerException(Resources.format(Resources.Keys.IllegalUTMZone_1, zone));
}
/*
* Parse the sub-sequence made of letters. That sub-sequence can have one or three parts.
* The first part is mandatory and the two other parts are optional, but if the two last
* parts are omitted, then they must be omitted together.
*
* part 0 — latitude band: C-X (excluding I and O) for UTM. Other letters (A, B, Y, Z) are for UPS.
* part 1 — column letter: A-H in zone 1, J-R (skipping O) in zone 2, S-Z in zone 3, then repeat.
* part 2 — row letter: ABCDEFGHJKLMNPQRSTUV in odd zones, FGHJKLMNPQRSTUVABCDE in even zones.
*/
int latitudeBand = -1; // The latitude band in [0 … 19] range.
int col = 1, row = 0;
hasSquareIdentification = true;
for (int part = 0; part <= 2; part++) {
if (part == 1 && i >= end) {
hasSquareIdentification = false;
break; // Allow to stop parsing only after part 1.
}
i = nextComponent(owner, reference, base, i, end);
int c = Character.codePointAt(reference, i);
final int ni = i + Character.charCount(c);
if (!isLetter(c) && !isLetter(c -= ('a' - 'A'))) {
final short key;
final CharSequence token;
if (part == 0) {
key = Resources.Keys.IllegalLatitudeBand_1;
token = reference.subSequence(i, ni);
} else {
key = Resources.Keys.IllegalSquareIdentification_1;
token = CharSequences.token(reference, i);
}
throw new GazetteerException(Resources.format(key, token));
}
/*
* At this point, 'c' is a valid letter. First, applies a correction for the fact that 'I' and 'O'
* letters were excluded. Next, the conversion to latitude or 100 000 meters grid indices depends
* on which part we are parsing. The formulas used below are about the same than in Encoder class,
* with terms moved on the other side of the equations.
*/
if (c >= EXCLUDE_O) c--;
if (c >= EXCLUDE_I) c--;
switch (part) {
case 0: {
latitudeBand = (c - 'C');
break;
}
case 1: {
switch (zone % 3) { // First A-H sequence starts at zone number 1.
case 1: col = c - ('A' - 1); break;
case 2: col = c - ('J' - 2); break; // -2 because 'I' has already been excluded.
case 0: col = c - ('S' - 3); break; // -3 because 'I' and 'O' have been excluded.
}
break;
}
case 2: {
if ((zone & 1) != 0) {
row = c - 'A';
} else {
row = c - 'F';
if (row < 0) {
row += GRID_ROW_COUNT;
}
}
break;
}
}
i = ni;
}
/*
* Create a UTM projection for exactly the zone specified in the MGRS reference, regardless the
* Norway and Svalbard special cases. Then fetch the smallest northing value that the latitude
* band may contain in that UTM zone. This is the projection of the geographic coordinate on the
* central meridian in the North hemisphere, or on the UTM zone border in the South hemisphere
* (because the zone center is always closer to equator and the zone borders closer to poles).
* This estimation is needed because the 100 kilometres square identification is insufficient;
* we may need to add some multiple of 2000 kilometres (20 squares).
*/
φs = latitudeBand * LATITUDE_BAND_HEIGHT + TransverseMercator.Zoner.SOUTH_BOUNDS;
if (latitudeBand < 0 || latitudeBand >= ROW_RESOLVER.length) {
throw new GazetteerException(Resources.format(Resources.Keys.IllegalLatitudeBand_1, Encoder.latitudeBands)));
}
λ0 = ZONER.centralMeridian(zone);
crs = owner.projection(Math.signums), λ0);
final int info = ROW_RESOLVER[latitudeBand]; // Contains the above-cited northing value.
if (hasSquareIdentification) {
int rowBit = 1 << (row + NORTHING_BITS_COUNT); // Bit mask of the row to check for existence.
isValid = (info & rowBit) != 0; // Whether the row exists in the latitude band.
if (isValid) {
/*
* Get the bit mask of the first invalid row north of current row. For example if 'info' has
* the following value and the current 'rowBit' mask has its bit set at the position of (1),
* then after this line of code the 'rowBit' bit will be set at the position of (2).
*
* 11111000000000011111
* ^ ^ ^
* (3) (2) (1)
*
* This line works by forcing to 1 all bits on the right side of (1) then searching the lowest
* zero bit (actually inverting everything and searching the lowest one bit, since there is no
* "lowestZeroBit" method in java.lang.Integer).
*
* The purpose is to verify if there another, distinct, sequence of 1 values on the left of
* current sequence. We will check that (after this block) by verifying if there is any bit
* set to 1 on the left side of zero bits, like the bit at position (3) in above figure. If
* such distinct sequence exists, then that sequence is the current cycle of rows while the
* sequence on the right side is a new cycle of rows.
*
* Note that if there is no zero bit on the left side, then rowBit = 0.
* This implies ~(rowBit - 1) == 0, which is okay for next line of code.
* Note: ~(rowBit - 1) == -rowBit
*/
rowBit = Integer.lowestOneBit(~(info | (rowBit - 1)));
}
if ((info & -rowBit) != 0) { // Test if there is valid rows on the left side of sequence of zero bits.
row += GRID_ROW_COUNT; // Left bits were from previous cycle, which means that we started a new cycle.
}
}
row += (info & NORTHING_BITS_MASK) * GRID_ROW_COUNT; // Add the pre-computed northing value.
minX = col * GRID_SQUARE_SIZE;
minY = row * GRID_SQUARE_SIZE;
}
/*
* If we have not yet reached the end of string, parse the numerical location.
* That location is normally encoded as a single number with an even number of digits.
* The first half is the easting and the second half is the northing, both relative to the
* 100 kilometer square. However some variants of MGRS use a separator, in which case we get
* two distinct numbers. In both cases, the resolution is determined by the amount of digits.
*/
final double sx, sy; // Scale factors for converting MGRS values in to easting and northing in metres.
if (i < end) {
i = nextComponent(owner, reference, base, i, end);
int s = endOfDigits(reference, i, end);
final double x, y;
if (s >= end) {
int length = s - i;
if ((length & 1) != 0) {
throw new GazetteerException(Resources.format(Resources.Keys.OddGridCoordinateLength_1,
reference.subSequence(i, s)));
}
final int h = i + (length >>>= 1);
sx = sy = MathFunctions.pow10(METRE_PRECISION_DIGITS - length);
x = parseCoordinate(reference, i, h, sx);
y = parseCoordinate(reference, h, s, sy);
} else {
sx = MathFunctions.pow10(METRE_PRECISION_DIGITS - (s - i));
x = parseCoordinate(reference, i, s, sx);
i = nextComponent(owner, reference, base, s, end);
s = endOfDigits(reference, i, end);
sy = MathFunctions.pow10(METRE_PRECISION_DIGITS - (s - i));
y = parseCoordinate(reference, i, s, sy);
if (s < end) {
throw new GazetteerException(Errors.format(Errors.Keys.UnexpectedCharactersAfter_2,
reference.subSequence(base, s), CharSequences.trimWhitespaces(reference, s, end)));
}
}
minX += x;
minY += y;
} else if (hasSquareIdentification) {
sx = sy = GRID_SQUARE_SIZE;
} else {
/*
* Not used for scaling anymore. Choose value that will cause the point to be in zone zenter.
* The '- GRID_SQUARE_SIZE' in 'sx' is because the westernmost 100-km grid squares are in the
* column at index 1 (the column at index 0 is outside the UTM zone).
*/
sx = (ZONER.easting - GRID_SQUARE_SIZE) * 2;
sy = ZONER.northing;
}
maxX = minX + sx;
maxY = minY + sy;
/*
* At this point the non-clipped projected envelope has been computed. Now compute the geographic envelope.
* We need this information for clipping the projected envelope to the domain of validity of UTM zone.
*/
if (!hasSquareIdentification) {
if (zone != 0) {
if s < 0) {
southBoundLatitude = TransverseMercator.Zoner.SOUTH_BOUNDS;
northBoundLatitude = 0;
} else {
southBoundLatitude = 0;
northBoundLatitude = TransverseMercator.Zoner.NORTH_BOUNDS;
}
westBoundLongitude = λ0 - ZONER.width / 2;
eastBoundLongitude = λ0 + ZONER.width / 2;
} else {
if s < 0) {
southBoundLatitude = Latitude.MIN_VALUE;
northBoundLatitude = TransverseMercator.Zoner.SOUTH_BOUNDS;
} else {
southBoundLatitude = TransverseMercator.Zoner.NORTH_BOUNDS;
northBoundLatitude = Latitude.MAX_VALUE;
}
westBoundLongitude = Longitude.MIN_VALUE;
eastBoundLongitude = Longitude.MAX_VALUE;
}
} else {
final MathTransform projection = crs.getConversionFromBase().getMathTransform();
computeGeographicBoundingBox(projection.inverse());
/*
* Update the LocationType according the precision.
* This update is mostly for documentation purpose.
* There is three levels:
*
* - Grid zone designator (if hasSquareIdentification == false)
* - 100 km square identifier (if resolution == 100 km)
* - Grid coordinate (if resolution < 100 km)
*/
setTypeToChild(); // Replace "Grid zone designator" by "100 km square identifier".
if (sx < GRID_SQUARE_SIZE || sy < GRID_SQUARE_SIZE) {
setTypeToChild(); // Replace "100 km square identifier" by "Grid coordinate".
}
/*
* At this point we finished computing the position. Now perform error detection, by verifying
* if the given 100 kilometres square identification is consistent with grid zone designation.
* We verify both φ and λ, but the verification of φ is actually redundant with the check of
* 100 km square validity that we did previously with the help of ROW_RESOLVER bitmask.
* We check φ anyway in case of bug.
*/
if (isValid && zone != 0) {
isValid = (northBoundLatitude >= φs) && (southBoundLatitude < upperBounds));
if (isValid) {
/*
* Verification of UTM zone. We allow a tolerance for latitudes close to a pole because
* not all users may apply the UTM special rules for Norway and Svalbard. Anyway, using
* the neighbor zone at those high latitudes is less significant. For other latitudes,
* we allow a tolerance if the point is close to a line of zone change.
*/
final double λ = (westBoundLongitude + eastBoundLongitude) / 2;
final double φ = (southBoundLatitude + northBoundLatitude) / 2;
int zoneError = ZONER.zone(φ, λ) - zone;
if (zoneError != 0) {
final int zc = ZONER.zoneCount();
if (zoneError < zc/-2) zoneError += zc; // If change of zone crosses the anti-meridian.
if (zoneError > zc/+2) zoneError -= zc;
if (ZONER.isSpecialCase(zone, φ)) {
isValid = Math.abs(zoneError) <= 2; // Tolerance in zone numbers for high latitudes.
} else {
final double rλ = Math.IEEEremainder - ZONER.origin, ZONER.width); // Distance to closest zone change, in degrees of longitude.
final double cv = (minX - ZONER.easting) / - λ0); // Approximate conversion factor from degrees to metres.
isValid = (Math.abs(rλ) * cv <= sx); // Be tolerant if distance in metres is less than resolution.
if (isValid) {
isValid = (zoneError == (rλ < 0 ? -1 : +1)); // Verify also that the error is on the side of the zone change.
}
}
}
}
}
/*
* At this point we finished verifying the cell validity using the coordinates specified by the
* MGRS reference. If the cell is valid, we can now check for cells that are on a zone border.
* Those cells will be clipped to the zone valid area.
*/
if (isValid && owner.getClipToValidArea()) {
final boolean changed;
if (zone != 0) {
double width = ZONER.width;
if (!ZONER.isSpecialCase(zone, φs)) width /= 2; // Be strict only if not Norway or Svalbard.
changed = clipGeographicBoundingBox0 - width, φs,
λ0 + width, upperBounds));
} else if s < 0) {
changed = clipGeographicBoundingBox(Longitude.MIN_VALUE, Latitude.MIN_VALUE,
Longitude.MAX_VALUE, TransverseMercator.Zoner.SOUTH_BOUNDS);
} else {
changed = clipGeographicBoundingBox(Longitude.MIN_VALUE, TransverseMercator.Zoner.NORTH_BOUNDS,
Longitude.MAX_VALUE, Latitude.MAX_VALUE);
}
if (changed) {
clipProjectedEnvelope(projection, sx / 100, sy / 100);
}
}
}
if (!isValid) {
final String gzd;
try {
gzd = owner.encoder(crs).encode(owner, getDirectPosition(), true, "", 0);
} catch (IllegalArgumentException | FactoryException e) {
throw new GazetteerException(e.getLocalizedMessage(), e);
}
final CharSequence ref = reference.subSequence(base, end);
throw new ReferenceVerifyException(Resources.format(Resources.Keys.InconsistentWithGZD_2, ref, gzd));
}
}
/**
* Skips spaces, then the separator if present (optional).
*
* @param reference the reference to parse.
* @param base index where the parsing began. Used for formatting error message only.
* @param start current parsing position.
* @param end where the parsing is expected to end.
* @return position where to continue parsing (with spaces skipped).
* @throws GazetteerException if this method unexpectedly reached the end of string.
*/
private static int nextComponent(final Coder owner, final CharSequence reference,
final int base, int start, final int end) throws GazetteerException
{
start = CharSequences.skipLeadingWhitespaces(reference, start, end);
if (start < end) {
if (!CharSequences.regionMatches(reference, start, owner.trimmedSeparator)) {
return start; // Separator not found, but it was optional.
}
start += owner.trimmedSeparator.length();
start = CharSequences.skipLeadingWhitespaces(reference, start, end);
if (start < end) {
return start;
}
}
throw new GazetteerException(Errors.format(Errors.Keys.UnexpectedEndOfString_1, reference.subSequence(base, end)));
}
/**
* Returns {@code true} if the given character is a valid upper case ASCII letter, excluding I and O.
*/
private static boolean isLetter(final int c) {
return (c >= 'A' && c <= 'Z') && c != EXCLUDE_I && c != EXCLUDE_O;
}
/**
* Returns the index after the last digit in a sequence of ASCII characters.
* Leading whitespaces must have been skipped before to invoke this method.
*/
private static int endOfDigits(final CharSequence reference, int i, final int end) {
while (i < end) {
final char c = reference.charAt(i); // Code-point API not needed here because we restrict to ASCII.
if (c < '0' || c > '9') { // Do not use Character.isDigit(…) because we restrict to ASCII.
break;
}
i++;
}
return i;
}
/**
* Parses part of the given character sequence as an integer.
*
* @param reference the MGRS reference to parse.
* @param start index of the first character to parse as an integer.
* @param end index after the last character to parse as an integer.
* @param errorKey {@link Resources.Keys} value to use in case of error.
* The error message string shall accept exactly one argument.
* @return the parsed integer.
* @throws GazetteerException if the string can not be parsed as an integer.
*/
private static int parseInt(final CharSequence reference, final int start, final int end, final short errorKey)
throws GazetteerException
{
NumberFormatException cause = null;
final CharSequence part;
if (start == end) {
part = CharSequences.token(reference, start);
} else {
part = reference.subSequence(start, end);
try {
return Integer.parseInt(part.toString());
} catch (NumberFormatException e) {
cause = e;
}
}
throw new GazetteerException(Resources.format(errorKey, part), cause);
}
/**
* Parses part of the given character sequence as a grid coordinate.
* The resolution is determined by the amount of digits.
*
* @param reference the MGRS reference to parse.
* @param start index of the first character to parse as a grid coordinate.
* @param end index after the last character to parse as a grid coordinate.
* @param scale value of {@code MathFunctions.pow10(METRE_PRECISION_DIGITS - (end - start))}.
* @return the parsed grid coordinate (also referred to as rectangular coordinates).
* @throws GazetteerException if the string can not be parsed as a grid coordinate.
*/
private static double parseCoordinate(final CharSequence reference,
final int start, final int end, final double scale) throws GazetteerException
{
return parseInt(reference, start, end, Resources.Keys.IllegalGridCoordinate_1) * scale;
}
/**
* Returns the upper bound of the latitude band specified by the given lower bound.
*/
static double upperBound(final double φ) {
return φ < TransverseMercator.Zoner.SVALBARD_BOUNDS ? φ + LATITUDE_BAND_HEIGHT
: TransverseMercator.Zoner.NORTH_BOUNDS;
}
/**
* Returns the Coordinate Reference System of the decoded MGRS reference.
* This is an Universal Transverse Mercator (UTM) or Universal Polar Stereographic (UPS) projection.
*/
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return crs;
}
}
}