| /* |
| * 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> |
| * |
| * <div class="section">Immutability and thread safety</div> |
| * 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> |
| * |
| * <div class="section">Immutability and thread safety</div> |
| * 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.isSpecialCase(φmin, φ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 IntervalRectangle(λmin, φ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. |
| * |
| * <div class="section">Immutability and thread safety</div> |
| * 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.projection(φs = (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.latitudeBand(φs))); |
| } |
| λ0 = ZONER.centralMeridian(zone); |
| crs = owner.projection(Math.signum(φs), λ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 < upperBound(φs)); |
| 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 = clipGeographicBoundingBox(λ0 - width, φs, |
| λ0 + width, upperBound(φs)); |
| } 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; |
| } |
| } |
| } |