/*
 * 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.geometry;


/*
 * Do not add dependency to java.awt.Rectangle2D in this class, because not every platforms
 * support Java2D (e.g. Android),  or applications that do not need it may want to avoid to
 * force installation of the Java2D module (e.g. JavaFX/SWT).
 */
import java.util.Arrays;
import java.util.Iterator;
import java.util.Objects;
import java.time.Instant;
import java.io.Serializable;
import java.lang.reflect.Field;
import org.opengis.referencing.cs.RangeMeaning;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.apache.sis.referencing.internal.shared.TemporalAccessor;
import org.apache.sis.referencing.internal.shared.AxisDirections;
import org.apache.sis.util.ArgumentCheckByAssertion;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.referencing.CRS;
import org.apache.sis.referencing.IdentifiedObjects;

import static org.apache.sis.util.ArgumentChecks.ensureDimensionMatches;
import static org.apache.sis.math.MathFunctions.isSameSign;
import static org.apache.sis.math.MathFunctions.isNegative;
import static org.apache.sis.math.MathFunctions.isNegativeZero;


/**
 * A mutable {@code Envelope} (a minimum bounding box or rectangle) of arbitrary dimension.
 * Regardless of dimension, an {@code Envelope} can be represented without ambiguity
 * as two {@linkplain AbstractDirectPosition direct positions} (coordinate tuples).
 * To encode an {@code Envelope}, it is sufficient to encode these two points.
 *
 * <p>{@code Envelope} uses an arbitrary <i>Coordinate Reference System</i>, which does not need to be geographic.
 * This is different than the {@code GeographicBoundingBox} class provided in the metadata package, which can be used
 * as a kind of envelope restricted to a Geographic CRS having Greenwich prime meridian.</p>
 *
 * This particular implementation of {@code Envelope} is said "General" because it uses
 * coordinates of an arbitrary number of dimensions. This is in contrast with
 * {@link Envelope2D}, which can use only two-dimensional coordinates.
 *
 * <p>A {@code GeneralEnvelope} can be created in various ways:</p>
 * <ul>
 *   <li>{@linkplain #GeneralEnvelope(int) From a given number of dimension}, with all coordinates initialized to 0.</li>
 *   <li>{@linkplain #GeneralEnvelope(double[], double[]) From two coordinate tuples}.</li>
 *   <li>{@linkplain #GeneralEnvelope(Envelope) From a another envelope} (copy constructor).</li>
 *   <li>{@linkplain #GeneralEnvelope(GeographicBoundingBox) From a geographic bounding box}.</li>
 *   <li>{@linkplain #GeneralEnvelope(CharSequence) From a character sequence}
 *       representing a {@code BBOX} or a <i>Well Known Text</i> (WKT) format.</li>
 * </ul>
 *
 * <h2>Crossing the anti-meridian of a Geographic CRS</h2>
 * The <cite>Web Coverage Service</cite> (WCS) specification authorizes (with special treatment)
 * cases where <var>upper</var> &lt; <var>lower</var> at least in the longitude case. They are
 * envelopes crossing the anti-meridian, like the red box below (the green box is the usual case).
 * The default implementation of methods listed in the right column can handle such cases.
 *
 * <div class="horizontal-flow">
 * <div>
 *   <img style="vertical-align: middle" src="doc-files/AntiMeridian.png" alt="Envelope crossing the anti-meridian">
 * </div><div>
 * Supported methods:
 * <ul>
 *   <li>{@link #getMinimum(int)}</li>
 *   <li>{@link #getMaximum(int)}</li>
 *   <li>{@link #getMedian(int)}</li>
 *   <li>{@link #getSpan(int)}</li>
 *   <li>{@link #isEmpty()}</li>
 *   <li>{@link #toSimpleEnvelopes() toSimpleEnvelopes()}</li>
 *   <li>{@link #contains(DirectPosition) contains(DirectPosition)}</li>
 *   <li>{@link #contains(Envelope) contains(Envelope)}</li>
 *   <li>{@link #intersects(Envelope) intersects(Envelope)}</li>
 *   <li>{@link #intersect(Envelope)}</li>
 *   <li>{@link #add(Envelope)}</li>
 *   <li>{@link #add(DirectPosition)}</li>
 * </ul>
 * </div></div>
 *
 * <h2>Envelope validation</h2>
 * If and only if this envelope is associated to a non-null CRS, then constructors and setter methods
 * in this class perform the following checks:
 *
 * <ul>
 *   <li>The number of CRS dimensions must be equal to <code>this.{@linkplain #getDimension()}</code>.</li>
 *   <li>For each dimension <var>i</var>,
 *       <code>{@linkplain #getLower(int) getLower}(i) &gt; {@linkplain #getUpper(int) getUpper}(i)</code> is allowed
 *       only if the {@linkplain org.apache.sis.referencing.cs.DefaultCoordinateSystemAxis#getRangeMeaning() coordinate
 *       system axis range meaning} is {@code WRAPAROUND}.</li>
 * </ul>
 *
 * Note that this class does <em>not</em> require the coordinate values to be between the axis minimum and
 * maximum values. This flexibility exists because out-of-range values happen in practice, while they do
 * not hurt the working of {@code add(…)}, {@code intersect(…)}, {@code contains(…)} and similar methods.
 * This in contrast with the {@code lower > upper} case, which cause the above-cited methods to behave in
 * an unexpected way if the axis does not have wraparound range meaning.
 *
 * @author  Martin Desruisseaux (IRD, Geomatys)
 * @author  Johann Sorel (Geomatys)
 * @version 1.1
 *
 * @see Envelope2D
 * @see org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox
 *
 * @since 0.3
 */
public class GeneralEnvelope extends ArrayEnvelope implements Cloneable, Serializable {
    /**
     * Serial number for inter-operability with different versions.
     */
    private static final long serialVersionUID = 3796799507279068254L;

    /**
     * Used for setting the {@link #coordinates} field during a {@link #clone()} operation only.
     * Will be fetch when first needed.
     */
    private static volatile Field coordinatesField;

    /**
     * Creates a new envelope using the given array of coordinate values. This constructor stores
     * the given reference directly; it does <strong>not</strong> clone the given array. This is
     * the desired behavior for proper working of {@link SubEnvelope}.
     *
     * @param coordinates  the array of coordinate values to store directly (not cloned).
     */
    GeneralEnvelope(final double[] coordinates) {
        super(coordinates);
    }

    /**
     * Constructs an envelope defined by two corners given as direct positions.
     * If at least one corner is associated to a CRS, then the new envelope will also
     * be associated to that CRS.
     *
     * @param  lowerCorner  the limits in the direction of decreasing coordinate values for each dimension.
     * @param  upperCorner  the limits in the direction of increasing coordinate values for each dimension.
     * @throws MismatchedDimensionException if the two positions do not have the same dimension.
     * @throws MismatchedReferenceSystemException if the CRS of the two position are not equal.
     */
    public GeneralEnvelope(final DirectPosition lowerCorner, final DirectPosition upperCorner)
            throws MismatchedDimensionException, MismatchedReferenceSystemException
    {
        super(lowerCorner, upperCorner);
    }

    /**
     * Constructs an envelope defined by two corners given as sequences of coordinate values.
     * The Coordinate Reference System is initially {@code null}.
     *
     * @param  lowerCorner  the limits in the direction of decreasing coordinate values for each dimension.
     * @param  upperCorner  the limits in the direction of increasing coordinate values for each dimension.
     * @throws MismatchedDimensionException if the two sequences do not have the same length.
     */
    public GeneralEnvelope(final double[] lowerCorner, final double[] upperCorner) throws MismatchedDimensionException {
        super(lowerCorner, upperCorner);
    }

    /**
     * Constructs an empty envelope of the specified dimension. All coordinates
     * are initialized to 0 and the coordinate reference system is undefined.
     *
     * @param  dimension  the envelope dimension.
     */
    public GeneralEnvelope(final int dimension) {
        super(dimension);
    }

    /**
     * Constructs an empty envelope with the specified coordinate reference system.
     * All coordinate values are initialized to 0.
     *
     * @param  crs  the coordinate reference system.
     */
    public GeneralEnvelope(final CoordinateReferenceSystem crs) {
        super(crs);
    }

    /**
     * Constructs a new envelope with the same data as the specified envelope.
     *
     * @param envelope  the envelope to copy.
     *
     * @see #castOrCopy(Envelope)
     */
    public GeneralEnvelope(final Envelope envelope) {
        super(envelope);
    }

    /**
     * Constructs a new envelope with the same data as the specified geographic bounding box.
     * The coordinate reference system is set to the
     * {@linkplain org.apache.sis.referencing.CommonCRS#defaultGeographic() default geographic CRS}.
     * Axis order is (<var>longitude</var>, <var>latitude</var>).
     *
     * @param box  the bounding box to copy.
     */
    public GeneralEnvelope(final GeographicBoundingBox box) {
        super(box);
    }

    /**
     * Constructs a new envelope initialized to the values parsed from the given string in
     * {@code BOX} or <i>Well Known Text</i> (WKT) format. The given string is typically
     * a {@code BOX} element like below:
     *
     * {@snippet lang="wkt" :
     *   BOX(-180 -90, 180 90)
     *   }
     *
     * However, this constructor is lenient to other geometry types like {@code POLYGON}.
     * Actually this constructor ignores the geometry type and just applies the following
     * simple rules:
     *
     * <ul>
     *   <li>Character sequences complying to the rules of Java identifiers are skipped.</li>
     *   <li>Coordinates are separated by a coma ({@code ,}) character.</li>
     *   <li>The coordinates in a coordinate tuple are separated by a space.</li>
     *   <li>Coordinate numbers are assumed formatted in US locale.</li>
     *   <li>The coordinate having the highest dimension determines the dimension of this envelope.</li>
     * </ul>
     *
     * This constructor does not check the consistency of the provided text. For example, it does not
     * check that every points in a {@code LINESTRING} have the same dimension. However, this
     * constructor ensures that the parenthesis are balanced, in order to catch some malformed WKT.
     *
     * <h4>Example</h4>
     * The following texts can be parsed by this constructor in addition of the usual {@code BOX} element.
     * This constructor creates the bounding box of those geometries:
     *
     * <ul>
     *   <li>{@code POINT(6 10)}</li>
     *   <li>{@code MULTIPOLYGON(((1 1, 5 1, 1 5, 1 1),(2 2, 3 2, 3 3, 2 2)))}</li>
     *   <li>{@code GEOMETRYCOLLECTION(POINT(4 6),LINESTRING(3 8,7 10))}</li>
     * </ul>
     *
     * @param  wkt  the {@code BOX}, {@code POLYGON} or other kind of element to parse.
     * @throws IllegalArgumentException if the given string cannot be parsed.
     *
     * @see Envelopes#fromWKT(CharSequence)
     * @see Envelopes#toString(Envelope)
     */
    public GeneralEnvelope(final CharSequence wkt) throws IllegalArgumentException {
        super(wkt);
    }

    /**
     * Returns the given envelope as a {@code GeneralEnvelope} instance. If the given envelope
     * is already an instance of {@code GeneralEnvelope}, then it is returned unchanged.
     * Otherwise the coordinate values and the CRS of the given envelope are
     * {@linkplain #GeneralEnvelope(Envelope) copied} in a new {@code GeneralEnvelope}.
     *
     * @param  envelope  the envelope to cast, or {@code null}.
     * @return the values of the given envelope as a {@code GeneralEnvelope} instance.
     *
     * @see AbstractEnvelope#castOrCopy(Envelope)
     * @see ImmutableEnvelope#castOrCopy(Envelope)
     */
    public static GeneralEnvelope castOrCopy(final Envelope envelope) {
        if (envelope == null || envelope instanceof GeneralEnvelope) {
            return (GeneralEnvelope) envelope;
        }
        return new GeneralEnvelope(envelope);
    }

    /**
     * Sets the coordinate reference system in which the coordinate are given.
     * This method <strong>does not</strong> reproject the envelope, and does
     * not check if the envelope is contained in the new domain of validity.
     *
     * <p>If the envelope coordinates need to be transformed to the new CRS, consider
     * using {@link Envelopes#transform(Envelope, CoordinateReferenceSystem)} instead.</p>
     *
     * @param  crs  the new coordinate reference system, or {@code null}.
     * @throws MismatchedDimensionException if the specified CRS doesn't have the expected number of dimensions.
     * @throws IllegalStateException if a range of coordinate values in this envelope is compatible with the given CRS.
     *         See <cite>Envelope validation</cite> in class javadoc for more details.
     */
    public void setCoordinateReferenceSystem(final CoordinateReferenceSystem crs)
            throws MismatchedDimensionException
    {
        if (crs != null) {
            ensureDimensionMatches("crs", getDimension(), crs);
            /*
             * The check performed here shall be identical to ArrayEnvelope.verifyRanges(crs, coordinates)
             * except that it may verify only a subset of the coordinate array and throws a different kind
             * of exception in case of failure.
             */
            final int beginIndex = beginIndex();
            final int endIndex = endIndex();
            final int d = coordinates.length >>> 1;
            for (int i=beginIndex; i<endIndex; i++) {
                final double lower = coordinates[i];
                final double upper = coordinates[i + d];
                if (lower > upper) {
                    final int j = i - beginIndex;
                    if (!isWrapAround(crs, j)) {
                        throw new IllegalStateException(illegalRange(crs, j, lower, upper));
                    }
                }
            }
        }
        this.crs = crs;                                             // Set only on success.
    }

    /**
     * Sets the envelope range along the specified dimension.
     *
     * @param  dimension  the dimension to set.
     * @param  lower      the limit in the direction of decreasing coordinate values.
     * @param  upper      the limit in the direction of increasing coordinate values.
     * @throws IndexOutOfBoundsException if the given index is out of bounds.
     * @throws IllegalArgumentException if {@code lower > upper} and the axis range meaning at the given dimension
     *         is not "wraparound". See <cite>Envelope validation</cite> in class javadoc for more details.
     */
    @Override                                                           // Must also be overridden in SubEnvelope
    public void setRange(final int dimension, final double lower, final double upper)
            throws IndexOutOfBoundsException
    {
        final int d = coordinates.length >>> 1;
        Objects.checkIndex(dimension, d);
        /*
         * The check performed here shall be identical to ArrayEnvelope.verifyRanges(crs, coordinates),
         * except that there is no loop.
         */
        if (lower > upper && crs != null && !isWrapAround(crs, dimension)) {
            throw new IllegalArgumentException(illegalRange(crs, dimension, lower, upper));
        }
        coordinates[dimension + d] = upper;
        coordinates[dimension]     = lower;
    }

    /**
     * Sets the envelope to the specified values, which must be the lower corner coordinates
     * followed by upper corner coordinates. The number of arguments provided shall be twice
     * this {@linkplain #getDimension() envelope dimension}, and minimum shall not be greater
     * than maximum.
     *
     * <div class="note"><b>Example:</b>
     * (<var>x</var><sub>min</sub>, <var>y</var><sub>min</sub>, <var>z</var><sub>min</sub>,
     *  <var>x</var><sub>max</sub>, <var>y</var><sub>max</sub>, <var>z</var><sub>max</sub>)
     * </div>
     *
     * @param corners  coordinates of the new lower corner followed by the new upper corner.
     */
    public void setEnvelope(final double... corners) {
        verifyArrayLength(coordinates.length >>> 1, corners);
        verifyRanges(crs, corners);
        System.arraycopy(corners, 0, coordinates, 0, coordinates.length);
    }

    /**
     * Verifies that the given array of coordinate values has the expected length
     * for the given number of dimensions.
     *
     * @param  dimension  the dimension of the envelope.
     * @param  corners    the user-provided array of coordinate values.
     */
    static void verifyArrayLength(final int dimension, final double[] corners) {
        if ((corners.length & 1) != 0) {
            throw new IllegalArgumentException(Errors.format(
                    Errors.Keys.OddArrayLength_1, corners.length));
        }
        final int d = corners.length >>> 1;
        if (d != dimension) {
            throw new MismatchedDimensionException(Errors.format(
                    Errors.Keys.MismatchedDimension_3, "coordinates", dimension, d));
        }
    }

    /**
     * Sets this envelope to the same coordinate values as the specified envelope.
     * If the given envelope has a non-null Coordinate Reference System (CRS), then
     * the CRS of this envelope will be set to the CRS of the given envelope.
     *
     * @param  envelope  the envelope to copy coordinates from.
     * @throws MismatchedDimensionException if the specified envelope does not have
     *         the expected number of dimensions.
     */
    public void setEnvelope(final Envelope envelope) throws MismatchedDimensionException {
        if (envelope == this) {
            return;     // Optimization for methods chaining like env.setEnvelope(Envelopes.transform(env, crs))
        }
        Objects.requireNonNull(envelope);
        final int beginIndex = beginIndex();
        final int dimension = endIndex() - beginIndex;
        ensureDimensionMatches("envelope", dimension, envelope);
        final int d = coordinates.length >>> 1;
        if (envelope instanceof ArrayEnvelope) {
            /*
             * Optimization for a common case. This code path is used by Envelopes.compound(…).
             * The main intent is to avoid the creation of temporary DirectPosition objects.
             */
            final double[] source = ((ArrayEnvelope) envelope).coordinates;
            final int srcOffset = ((ArrayEnvelope) envelope).beginIndex();
            System.arraycopy(source, srcOffset, coordinates, beginIndex, dimension);
            System.arraycopy(source, srcOffset + (source.length >>> 1), coordinates, beginIndex + d, dimension);
        } else {
            final DirectPosition lower = envelope.getLowerCorner();
            final DirectPosition upper = envelope.getUpperCorner();
            for (int i=0; i<dimension; i++) {
                final int iLower = beginIndex + i;
                final int iUpper = iLower + d;
                coordinates[iLower] = lower.getOrdinate(i);
                coordinates[iUpper] = upper.getOrdinate(i);
            }
        }
        final CoordinateReferenceSystem envelopeCRS = envelope.getCoordinateReferenceSystem();
        if (envelopeCRS != null) {
            crs = envelopeCRS;
            assert crs.getCoordinateSystem().getDimension() == getDimension() : crs;
            assert envelope.getClass() != getClass() || equals(envelope) : envelope;
        }
    }

    /**
     * Sets the lower corner to {@linkplain Double#NEGATIVE_INFINITY negative infinity}
     * and the upper corner to {@linkplain Double#POSITIVE_INFINITY positive infinity}.
     * The {@linkplain #getCoordinateReferenceSystem() coordinate reference system}
     * (if any) stay unchanged.
     */
    public void setToInfinite() {
        final int beginIndex = beginIndex();
        final int endIndex = endIndex();
        final int d = coordinates.length >>> 1;
        Arrays.fill(coordinates, beginIndex,   endIndex,   Double.NEGATIVE_INFINITY);
        Arrays.fill(coordinates, beginIndex+d, endIndex+d, Double.POSITIVE_INFINITY);
    }

    /**
     * Sets all coordinate values to {@linkplain Double#NaN NaN}.
     * The {@linkplain #getCoordinateReferenceSystem() coordinate reference system}
     * (if any) stay unchanged.
     *
     * @see #isAllNaN()
     */
    public void setToNaN() {                   // Must be overridden in SubEnvelope
        Arrays.fill(coordinates, Double.NaN);
        assert isAllNaN() : this;
    }

    /**
     * If this envelope has a temporal component, sets its temporal dimension to the given range.
     * Otherwise this method does nothing. This convenience method converts the given instants to
     * floating point values using {@link org.apache.sis.referencing.crs.DefaultTemporalCRS},
     * then delegates to {@link #setRange(int, double, double)}.
     *
     * <p>Null value means no time limit. More specifically
     * null {@code startTime} is mapped to {@linkplain Double#NEGATIVE_INFINITY −∞} and
     * null {@code endTime}   is mapped to {@linkplain Double#POSITIVE_INFINITY +∞}.
     * This rule makes easy to create <q>is before</q> or <q>is after</q> temporal filters,
     * which can be combined with other envelopes using {@linkplain #intersect(Envelope) intersection}
     * for logical AND, or {@linkplain #add(Envelope) union} for logical OR operations.</p>
     *
     * @param  startTime  the lower temporal value, or {@code null} if unbounded.
     * @param  endTime    the upper temporal value, or {@code null} if unbounded.
     * @return whether the temporal component has been set, or {@code false}
     *         if no temporal dimension has been found in this envelope.
     *
     * @since 1.0
     *
     * @see #getTimeRange()
     * @see Envelopes#toTimeRange(Envelope)
     */
    public boolean setTimeRange(final Instant startTime, final Instant endTime) {
        final TemporalAccessor t = TemporalAccessor.of(crs, 0);
        if (t != null) {
            double lower = t.timeCRS.toValue(startTime);
            double upper = t.timeCRS.toValue(endTime);
            if (Double.isNaN(lower)) lower = Double.NEGATIVE_INFINITY;
            if (Double.isNaN(upper)) upper = Double.POSITIVE_INFINITY;
            setRange(t.dimension, lower, upper);
            return true;
        } else {
            return false;
        }
    }

    /**
     * Translates the envelope by the given vector. For every dimension <var>i</var>, the
     * {@linkplain #getLower(int) lower} and {@linkplain #getUpper(int) upper} values are
     * increased by {@code vector[i]}.
     *
     * <p>This method does not check if the translation result is inside the coordinate system domain
     * (e.g. [-180 … +180]° of longitude). Callers can normalize the envelope when desired by call to
     * the {@link #normalize()} method.</p>
     *
     * @param vector  the translation vector. The length of this array shall be equal to this envelope
     *                {@linkplain #getDimension() dimension}.
     *
     * @since 0.5
     */
    public void translate(final double... vector) {
        Objects.requireNonNull(vector);
        final int beginIndex = beginIndex();
        ensureDimensionMatches("vector", endIndex() - beginIndex, vector);
        final int upperIndex = beginIndex + (coordinates.length >>> 1);
        for (int i=0; i<vector.length; i++) {
            final double t = vector[i];
            coordinates[beginIndex + i] += t;
            coordinates[upperIndex + i] += t;
        }
    }

    /**
     * Adds a point to this envelope. The resulting envelope is the smallest envelope that
     * contains both the original envelope and the specified point.
     *
     * <p>After adding a point, a call to {@link #contains(DirectPosition) contains(DirectPosition)}
     * with the added point as an argument will return {@code true}, except if one of the point
     * coordinates was {@link Double#NaN} in which case the corresponding coordinate has been ignored.</p>
     *
     * <h4>Preconditions</h4>
     * This method assumes that the specified point uses a CRS equivalent to this envelope CRS.
     * For performance reasons, this condition is not verified unless Java assertions are enabled.
     *
     * <h4>Crossing the anti-meridian of a Geographic CRS</h4>
     * This method supports envelopes crossing the anti-meridian. In such cases it is possible to
     * move both envelope borders in order to encompass the given point, as illustrated below (the
     * new point is represented by the {@code +} symbol):
     *
     * <pre class="text">
     *    ─────┐   + ┌─────
     *    ─────┘     └─────</pre>
     *
     * The default implementation moves only the border which is closest to the given point.
     *
     * @param  position  the point to add.
     * @throws MismatchedDimensionException if the given point does not have the expected number of dimensions.
     * @throws AssertionError if assertions are enabled and the envelopes have mismatched CRS.
     */
    @ArgumentCheckByAssertion
    public void add(final DirectPosition position) throws MismatchedDimensionException {
        Objects.requireNonNull(position);
        final int beginIndex = beginIndex();
        final int dimension = endIndex() - beginIndex;
        ensureDimensionMatches("position", dimension, position);
        assert assertEquals(crs, position.getCoordinateReferenceSystem()) : position;
        final int d = coordinates.length >>> 1;
        for (int i=0; i<dimension; i++) {
            final int iLower = beginIndex + i;
            final int iUpper = iLower + d;
            final double value = position.getOrdinate(i);
            final double min = coordinates[iLower];
            final double max = coordinates[iUpper];
            if (!isNegative(max - min)) {                       // Standard case, or NaN.
                if (value < min) coordinates[iLower] = value;
                if (value > max) coordinates[iUpper] = value;
            } else {
                /*
                 * Crossing the anti-meridian. The [max…min] range (note that min/max are interchanged)
                 * is actually an exclusion area. Changes only the closest side.
                 */
                addToClosest(iLower, value, max, min);
            }
        }
        assert contains(position) || isEmpty() || hasNaN(position) : position;
    }

    /**
     * Invoked when a point is added to a range crossing the anti-meridian.
     * In the example below, the new point is represented by the {@code +}
     * symbol. The point is added only on the closest side.
     *
     * <pre class="text">
     *    ─────┐   + ┌─────
     *    ─────┘     └─────</pre>
     *
     * @param  i      the dimension of the coordinate
     * @param  value  the coordinate value to add to this envelope.
     * @param  left   the border on the left side,  which is the <em>max</em> value (yes, this is confusing!)
     * @param  right  the border on the right side, which is the <em>min</em> value (yes, this is confusing!)
     */
    private void addToClosest(int i, final double value, double left, double right) {
        left = value - left;
        if (left > 0) {
            right -= value;
            if (right > 0) {
                if (right > left) {
                    i += (coordinates.length >>> 1);
                }
                coordinates[i] = value;
            }
        }
    }

    /**
     * Adds an envelope object to this envelope.
     * The resulting envelope is the union of the two {@code Envelope} objects.
     *
     * <h4>Preconditions</h4>
     * This method assumes that the specified envelope uses a CRS equivalent to this envelope CRS.
     * For performance reasons, this condition is not verified unless Java assertions are enabled.
     *
     * <h4>Crossing the anti-meridian of a Geographic CRS</h4>
     * This method supports envelopes crossing the anti-meridian. If one or both envelopes cross
     * the anti-meridian, then the result of the {@code add} operation may be an envelope expanding
     * to infinities. In such case, the coordinate range will be either [−∞…∞] or [0…−0] depending on
     * whatever the original range crosses the anti-meridian or not.
     *
     * <h4>Handling of NaN values</h4>
     * {@link Double#NaN} values may be present in any dimension, in the lower coordinate, upper coordinate or both.
     * The behavior of this method in such case depends where the {@code NaN} values appear and whether an envelope
     * spans the anti-meridian:
     *
     * <ul class="verbose">
     *   <li>If this envelope or the given envelope spans anti-meridian in the dimension containing {@code NaN} coordinates,
     *     then this method does not changes the coordinates in that dimension. The rational for such conservative approach
     *     is because union computation depends on whether the other envelope spans anti-meridian too, which is unknown
     *     because at least one envelope bounds is {@code NaN}. Since anti-meridian crossing has been detected in an envelope,
     *     there is suspicion about whether the other envelope could cross anti-meridian too.</li>
     *   <li>Otherwise since the envelope containing real values does not cross anti-meridian in that dimension,
     *     this method assumes that the envelope containing {@code NaN} values does not cross anti-meridian neither.
     *     This assumption is not guaranteed to be true, but cover common cases.
     *     With this assumption in mind:
     *   <ul class="verbose">
     *     <li>All {@code NaN} coordinates in the <em>given</em> envelope are ignored, <i>i.e.</i>
     *       this method does not replace finite coordinates in this envelope by {@code NaN} values from the given envelope.
     *       Note that if only the lower or upper bound is {@code NaN}, the other bound will still
     *       be used for computing union with the assumption described in above paragraph.</li>
     *     <li>All {@code NaN} coordinates in <em>this</em> envelope are left unchanged, <i>i.e.</i> the union will
     *       still contain all the {@code NaN} values that this envelope had before {@code add(Envelope)} invocation.
     *       Note that if only the lower or upper bound is {@code NaN}, the other bound will still
     *       be used for computing union with the assumption described in above paragraph.</li>
     *   </ul></li>
     * </ul>
     *
     * @param  envelope  the {@code Envelope} to add to this envelope.
     * @throws MismatchedDimensionException if the given envelope does not have the expected number of dimensions.
     * @throws AssertionError if assertions are enabled and the envelopes have mismatched CRS.
     *
     * @see Envelopes#union(Envelope...)
     * @see org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox#add(GeographicBoundingBox)
     */
    @ArgumentCheckByAssertion
    public void add(final Envelope envelope) throws MismatchedDimensionException {
        Objects.requireNonNull(envelope);
        final int beginIndex = beginIndex();
        final int dimension = endIndex() - beginIndex;
        ensureDimensionMatches("envelope", dimension, envelope);
        assert assertEquals(crs, envelope.getCoordinateReferenceSystem()) : envelope;
        final DirectPosition lower = envelope.getLowerCorner();
        final DirectPosition upper = envelope.getUpperCorner();
        final int d = coordinates.length >>> 1;
        for (int i=0; i<dimension; i++) {
            final int iLower = beginIndex + i;
            final int iUpper = iLower + d;
            final double min0 = coordinates[iLower];
            final double max0 = coordinates[iUpper];
            final double min1 = lower.getOrdinate(i);
            final double max1 = upper.getOrdinate(i);
            final boolean sp0 = isNegative(max0 - min0);
            final boolean sp1 = isNegative(max1 - min1);
            if (sp0 == sp1) {
                /*
                 * Standard case (for rows in the above pictures), or case where both envelopes
                 * cross the anti-meridian (which is almost the same with an additional post-add
                 * check).
                 *    ┌──────────┐          ┌──────────┐
                 *    │  ┌────┐  │    or    │  ┌───────┼──┐
                 *    │  └────┘  │          │  └───────┼──┘
                 *    └──────────┘          └──────────┘
                 *
                 *    ────┐  ┌────          ────┐  ┌────
                 *    ──┐ │  │ ┌──    or    ────┼──┼─┐┌─
                 *    ──┘ │  │ └──          ────┼──┼─┘└─
                 *    ────┘  └────          ────┘  └────
                 */
                if (min1 < min0) coordinates[iLower] = min1;
                if (max1 > max0) coordinates[iUpper] = max1;
                if (!sp0 || isNegativeUnsafe(coordinates[iUpper] - coordinates[iLower])) {
                    continue;               // We are done, go to the next dimension.
                }
                // If we were crossing the anti-meridian before the union but
                // are not anymore after the union, we actually merged to two
                // sides, so the envelope is spanning to infinities. The code
                // close to the end of this loop will set an infinite range.
            } else if (sp0) {
                /*
                 * Only this envelope spans the anti-meridian; the given envelope is normal or
                 * has NaN values.  First we need to exclude the cases were the given envelope
                 * is fully included in this envelope:
                 *   ──────────┐  ┌─────
                 *     ┌────┐  │  │
                 *     └────┘  │  │
                 *   ──────────┘  └─────
                 */
                if (max1 <= max0) continue;             // This is the case of above picture.
                if (min1 >= min0) continue;             // Like above picture, but on the right side.
                /*
                 * At this point, the given envelope partially overlaps the "exclusion area"
                 * of this envelope or has NaN values. We will move at most one edge of this
                 * envelope, in order to leave as much free space as possible.
                 *    ─────┐      ┌─────
                 *       ┌─┼────┐ │
                 *       └─┼────┘ │
                 *    ─────┘      └─────
                 */
                final double left  = min1 - max0;
                final double right = min0 - max1;
                if (left > 0 || right > 0) {
                    // The < and > checks below are not completly redundant.
                    // The difference is when a value is NaN.
                    if (left > right) coordinates[iLower] = min1;
                    if (right > left) coordinates[iUpper] = max1;     // This is the case illustrated above.
                    continue;                                         // We are done, go to the next dimension.
                }
                // If we reach this point, the given envelope fills completly the "exclusion area"
                // of this envelope. As a consequence this envelope is now spanning to infinities.
                // We will set that fact close to the end of this loop.
            } else {
                /*
                 * Opposite of above case: this envelope is "normal" or has NaN values,
                 * and the given envelope spans to infinities.
                 */
                if (max0 <= max1 || min0 >= min1) {
                    coordinates[iLower] = min1;
                    coordinates[iUpper] = max1;
                    continue;
                }
                final double left  = min0 - max1;
                final double right = min1 - max0;
                if (left > 0 || right > 0) {
                    if (left > right) coordinates[iUpper] = max1;
                    if (right > left) coordinates[iLower] = min1;
                    continue;
                }
            }
            /*
             * If we reach that point, we went in one of the many cases where the envelope
             * has been expanded to infinity.  Declares an infinite range while preserving
             * the "normal" / "anti-meridian crossing" state.
             */
            if (sp0) {
                coordinates[iLower] = +0.0;
                coordinates[iUpper] = -0.0;
            } else {
                coordinates[iLower] = Double.NEGATIVE_INFINITY;
                coordinates[iUpper] = Double.POSITIVE_INFINITY;
            }
        }
        assert contains(envelope) || isEmpty() || hasNaN(envelope) : this;
    }

    /**
     * Sets this envelope to the intersection of this envelope with the specified one.
     *
     * <h4>Preconditions</h4>
     * This method assumes that the specified envelope uses a CRS equivalent to this envelope CRS.
     * For performance reasons, this condition is not verified unless Java assertions are enabled.
     *
     * <h4>Crossing the anti-meridian of a Geographic CRS</h4>
     * This method supports envelopes crossing the anti-meridian.
     *
     * <h4>Handling of NaN values</h4>
     * {@link Double#NaN} values may be present in any dimension, in the lower coordinate, upper coordinate or both.
     * The behavior of this method in such case depends where the {@code NaN} values appear and whether an envelope
     * spans the anti-meridian:
     *
     * <ul class="verbose">
     *   <li>If this envelope or the given envelope spans anti-meridian in the dimension containing {@code NaN} coordinates,
     *     then this method does not changes the coordinates in that dimension. The rational for such conservative approach
     *     is because intersection computation depends on whether the other envelope spans anti-meridian too, which is unknown
     *     because at least one envelope bounds is {@code NaN}. Since anti-meridian crossing has been detected in an envelope,
     *     there is suspicion about whether the other envelope could cross anti-meridian too.</li>
     *   <li>Otherwise since the envelope containing real values does not cross anti-meridian in that dimension,
     *     this method assumes that the envelope containing {@code NaN} values does not cross anti-meridian neither.
     *     This assumption is not guaranteed to be true, but cover common cases.
     *     With this assumption in mind:
     *   <ul class="verbose">
     *     <li>All {@code NaN} coordinates in the <em>given</em> envelope are ignored, <i>i.e.</i>
     *       this method does not replace finite coordinates in this envelope by {@code NaN} values from the given envelope.
     *       Note that if only the lower or upper bound is {@code NaN}, the other bound will still
     *       be used for computing intersection with the assumption described in above paragraph.</li>
     *     <li>All {@code NaN} coordinates in <em>this</em> envelope are left unchanged, <i>i.e.</i> the intersection will
     *       still contain all the {@code NaN} values that this envelope had before {@code intersect(Envelope)} invocation.
     *       Note that if only the lower or upper bound is {@code NaN}, the other bound will still
     *       be used for computing intersection with the assumption described in above paragraph.</li>
     *   </ul></li>
     * </ul>
     *
     * {@link Double#NaN} coordinates may appear as a result of intersection, even if such values were not present
     * in any source envelopes, if the two envelopes do not intersect in some dimensions.
     *
     * @param  envelope  the {@code Envelope} to intersect to this envelope.
     * @throws MismatchedDimensionException if the given envelope does not have the expected number of dimensions.
     * @throws AssertionError if assertions are enabled and the envelopes have mismatched CRS.
     *
     * @see Envelopes#intersect(Envelope...)
     * @see org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox#intersect(GeographicBoundingBox)
     */
    @ArgumentCheckByAssertion
    public void intersect(final Envelope envelope) throws MismatchedDimensionException {
        Objects.requireNonNull(envelope);
        final int beginIndex = beginIndex();
        final int dimension = endIndex() - beginIndex;
        ensureDimensionMatches("envelope", dimension, envelope);
        assert assertEquals(crs, envelope.getCoordinateReferenceSystem()) : envelope;
        final DirectPosition lower = envelope.getLowerCorner();
        final DirectPosition upper = envelope.getUpperCorner();
        final int d = coordinates.length >>> 1;
        for (int i=0; i<dimension; i++) {
            final int iLower = beginIndex + i;
            final int iUpper = iLower + d;
            final double min0  = coordinates[iLower];
            final double max0  = coordinates[iUpper];
            final double min1  = lower.getOrdinate(i);
            final double max1  = upper.getOrdinate(i);
            final double span0 = max0 - min0;
            final double span1 = max1 - min1;
            if (isSameSign(span0, span1)) {                 // Always `false` if any value is NaN.
                /*
                 * First, verify that the two envelopes intersect.
                 *     ┌──────────┐             ┌─────────────┐
                 *     │  ┌───────┼──┐    or    │  ┌───────┐  │
                 *     │  └───────┼──┘          │  └───────┘  │
                 *     └──────────┘             └─────────────┘
                 */
                if ((min1 > max0 || max1 < min0) && !isNegativeUnsafe(span0)) {
                    /*
                     * The check for !isNegative(span0) is because if both envelopes cross the
                     * anti-merdian, then there is always an intersection on both side no matter
                     * what envelope coordinates are because both envelopes extend toward infinities:
                     *     ────┐  ┌────            ────┐  ┌────
                     *     ──┐ │  │ ┌──     or     ────┼──┼─┐┌─
                     *     ──┘ │  │ └──            ────┼──┼─┘└─
                     *     ────┘  └────            ────┘  └────
                     * Since we excluded the above case, entering in this block means that the
                     * envelopes are "normal" and do not intersect, so we set coordinates to NaN.
                     *   ┌────┐
                     *   │    │     ┌────┐
                     *   │    │     └────┘
                     *   └────┘
                     */
                    coordinates[iLower] = coordinates[iUpper] = Double.NaN;
                    continue;
                }
            } else if (!Double.isNaN(span0) && !Double.isNaN(span1)) {
                int intersect = 0;                          // A bitmask of intersections (two bits).
                if (isNegativeUnsafe(span0)) {
                    /*
                     * The first line below checks for the case illustrated below. The second
                     * line does the same check, but with the small rectangle on the right side.
                     *    ─────┐      ┌─────              ──────────┐  ┌─────
                     *       ┌─┼────┐ │           or        ┌────┐  │  │
                     *       └─┼────┘ │                     └────┘  │  │
                     *    ─────┘      └─────              ──────────┘  └─────
                     */
                    if (min1 <= max0) {intersect  = 1; coordinates[iLower] = min1;}
                    if (max1 >= min0) {intersect |= 2; coordinates[iUpper] = max1;}
                } else {
                    // Same as above, but with indices 0 and 1 interchanged.
                    // No need to set coordinate values since they would be the same.
                    if (min0 <= max1) {intersect  = 1;}
                    if (max0 >= min1) {intersect |= 2;}
                }
                /*
                 * Cases 0 and 3 are illustrated below. In case 1 and 2, we will set
                 * only the coordinate value which has not been set by the above code.
                 *
                 *                [intersect=0]          [intersect=3]
                 *              ─────┐     ┌─────      ─────┐     ┌─────
                 *  negative:    max0│ ┌─┐ │min0          ┌─┼─────┼─┐
                 *                   │ └─┘ │              └─┼─────┼─┘
                 *              ─────┘     └─────      ─────┘     └─────
                 *
                 *               max1  ┌─┐  min1          ┌─────────┐
                 * positive:    ─────┐ │ │ ┌─────      ───┼─┐     ┌─┼───
                 *              ─────┘ │ │ └─────      ───┼─┘     └─┼───
                 *                     └─┘                └─────────┘
                 */
                switch (intersect) {
                    default: throw new AssertionError(intersect);
                    case 1: if (max1 < max0) coordinates[iUpper] = max1; continue;
                    case 2: if (min1 > min0) coordinates[iLower] = min1; continue;
                    case 3: // Fall through
                    case 0: {
                        /*
                         * Before to declare the intersection as invalid, verify if the envelope actually spans
                         * the whole Earth. In such case, the intersection is a no-operation (or a copy operation).
                         */
                        final double min, max;
                        final double cycle = getCycle(getAxis(crs, i));
                        if (span1 >= cycle || isNegativeZero(span1)) {        // Negative zero if [+0 … -0] range.
                            min = min0;
                            max = max0;
                        } else if (span0 >= cycle || isNegativeZero(span0)) {
                            min = min1;
                            max = max1;
                        } else {
                            min = Double.NaN;
                            max = Double.NaN;
                        }
                        coordinates[iLower] = min;
                        coordinates[iUpper] = max;
                        continue;
                    }
                }
            } else {
                /*
                 * We reach this point only if at least one value is NaN. It may be in this envelope or in the given envelope.
                 * If one of the two envelopes spans the anti-meridian in current dimension, do nothing for the reasons given
                 * in method javadoc. Otherwise process the non-NaN coordinates like ordinary envelopes, ignoring NaN.
                 */
                if (isNegative(span0) || isNegative(span1)) {
                    continue;
                }
            }
            if (min1 > min0) coordinates[iLower] = min1;
            if (max1 < max0) coordinates[iUpper] = max1;
        }
        // Tests only if the interection result is non-empty.
        assert isEmpty() || hasNaN(envelope) || AbstractEnvelope.castOrCopy(envelope).contains(this) : this;
    }

    /**
     * Ensures that the envelope is contained inside the coordinate system domain.
     * For each dimension, this method compares the coordinate values against the
     * limits of the coordinate system axis for that dimension.
     * If some coordinates are out of range, then there is a choice depending on the
     * {@linkplain CoordinateSystemAxis#getRangeMeaning() axis range meaning}:
     *
     * <ul class="verbose">
     *   <li>If {@link RangeMeaning#EXACT} (typically <em>latitudes</em> coordinates), then values
     *       greater than the {@linkplain CoordinateSystemAxis#getMaximumValue() axis maximal value}
     *       are replaced by the axis maximum, and values smaller than the
     *       {@linkplain CoordinateSystemAxis#getMinimumValue() axis minimal value}
     *       are replaced by the axis minimum.</li>
     *
     *   <li>If {@link RangeMeaning#WRAPAROUND} (typically <em>longitudes</em> coordinates), then
     *       a multiple of the axis range (e.g. 360° for longitudes) is added or subtracted.
     *       Example:
     *       <ul>
     *         <li>the [190 … 200]° longitude range is converted to [-170 … -160]°,</li>
     *         <li>the [170 … 200]° longitude range is converted to [+170 … -160]°.</li>
     *       </ul>
     *       See <cite>Crossing the anti-meridian of a Geographic CRS</cite> in the
     *       class javadoc for more information about the meaning of such range.</li>
     * </ul>
     *
     * <h4>Crossing the anti-meridian of a Geographic CRS</h4>
     * If the envelope is crossing the anti-meridian, then some {@linkplain #getLower(int) lower}
     * coordinate values may become greater than their {@linkplain #getUpper(int) upper} counterpart
     * as a result of this method call. If such effect is undesirable, then this method may be
     * combined with {@link #simplify()} as below:
     *
     * {@snippet lang="java" :
     *     if (envelope.normalize()) {
     *         envelope.simplify();
     *     }
     *     }
     *
     * <h4>Choosing the range of longitude values</h4>
     * Geographic CRS typically have longitude values in the [-180 … +180]° range, but the [0 … 360]°
     * range is also occasionally used. Callers need to ensure that this envelope CRS is associated
     * to axes having the desired {@linkplain CoordinateSystemAxis#getMinimumValue() minimum} and
     * {@linkplain CoordinateSystemAxis#getMaximumValue() maximum value}.
     *
     * <h4>Usage</h4>
     * This method is sometimes useful before to compute the {@linkplain #add(Envelope) union}
     * or {@linkplain #intersect(Envelope) intersection} of envelopes, in order to ensure that
     * both envelopes are defined in the same domain. This method may also be invoked before
     * to project an envelope, since some projections produce {@link Double#NaN} numbers when
     * given an coordinate value out of bounds.
     *
     * @return {@code true} if this envelope has been modified as a result of this method call,
     *         or {@code false} if no change has been done.
     *
     * @see AbstractDirectPosition#normalize()
     * @see WraparoundMethod#NORMALIZE
     */
    public boolean normalize() {
        if (crs == null) {
            return false;
        }
        final int beginIndex = beginIndex();
        return normalize(crs.getCoordinateSystem(), beginIndex, endIndex() - beginIndex, null);
    }

    /**
     * Normalizes only the dimensions returned by the given iterator, or all dimensions if the iterator is null.
     * This is used for normalizing the result of a coordinate operation where a wrap around axis does not
     * necessarily means that the coordinates need to be normalized along that axis.
     *
     * @param  cs          the coordinate system of this envelope CRS (as an argument because sometimes already known).
     * @param  beginIndex  index of the first coordinate value in {@link #coordinates} array. Non-zero for sub-envelopes.
     * @param  count       number of coordinates, i.e. this envelope dimensions.
     * @param  dimensions  the dimensions to check for normalization, or {@code null} for all dimensions.
     * @return {@code true} if this envelope has been modified as a result of this method call.
     */
    final boolean normalize(final CoordinateSystem cs, final int beginIndex, final int count, final Iterator<Integer> dimensions) {
        boolean changed = false;
        final int d = coordinates.length >>> 1;
        for (int j=0; j<count; j++) {
            final int i = (dimensions != null) ? dimensions.next() : j;
            final int iLower = beginIndex + i;
            final int iUpper = iLower + d;
            final CoordinateSystemAxis axis = cs.getAxis(i);
            final double  minimum = axis.getMinimumValue();
            final double  maximum = axis.getMaximumValue();
            final RangeMeaning rm = axis.getRangeMeaning();
            if (rm == RangeMeaning.EXACT) {
                if (coordinates[iLower] < minimum) {coordinates[iLower] = minimum; changed = true;}
                if (coordinates[iUpper] > maximum) {coordinates[iUpper] = maximum; changed = true;}
            } else if (rm == RangeMeaning.WRAPAROUND) {
                final double cycle = maximum - minimum;
                if (cycle > 0 && cycle < Double.POSITIVE_INFINITY) {
                    double o1 = coordinates[iLower];
                    double o2 = coordinates[iUpper];
                    if (Math.abs(o2-o1) >= cycle) {
                        /*
                         * If the range exceed the CS span, then we have to replace it by the
                         * full span, otherwise the range computed by the "else" block is too
                         * small. The full range will typically be [-180 … 180]°. However, we
                         * make a special case if the two bounds are multiple of the CS span,
                         * typically [0 … 360]°. In this case the [0 … -0]° range matches the
                         * original values and is understood by GeneralEnvelope as a range
                         * spanning all the world.
                         */
                        if (o1 != minimum || o2 != maximum) {
                            if ((o1 % cycle) == 0 && (o2 % cycle) == 0) {
                                coordinates[iLower] = +0.0;
                                coordinates[iUpper] = -0.0;
                            } else {
                                coordinates[iLower] = minimum;
                                coordinates[iUpper] = maximum;
                            }
                            changed = true;
                        }
                    } else {
                        o1 = Math.floor((o1 - minimum) / cycle) * cycle;
                        o2 = Math.floor((o2 - minimum) / cycle) * cycle;
                        if (o1 != 0) {coordinates[iLower] -= o1; changed = true;}
                        if (o2 != 0) {coordinates[iUpper] -= o2; changed = true;}
                    }
                }
            }
        }
        return changed;
    }

    /**
     * Ensures that <var>lower</var> ≤ <var>upper</var> for every dimensions.
     * If a {@linkplain #getUpper(int) upper coordinate value} is less than a
     * {@linkplain #getLower(int) lower coordinate value}, then there is a choice:
     *
     * <ul>
     *   <li>If the axis has {@link RangeMeaning#WRAPAROUND}, then:<ul>
     *       <li>the lower coordinate value is set to the {@linkplain CoordinateSystemAxis#getMinimumValue() axis minimum value}, and</li>
     *       <li>the upper coordinate value is set to the {@linkplain CoordinateSystemAxis#getMaximumValue() axis maximum value}.</li>
     *     </ul></li>
     *   <li>Otherwise an {@link IllegalStateException} is thrown.</li>
     * </ul>
     *
     * This method is useful when the envelope needs to be used with libraries that do not support
     * envelopes crossing the anti-meridian.
     *
     * @return {@code true} if this envelope has been modified as a result of this method call,
     *         or {@code false} if no change has been done.
     * @throws IllegalStateException if a upper coordinate value is less than a lower coordinate
     *         value on an axis which does not have the {@code WRAPAROUND} range meaning.
     *
     * @see #toSimpleEnvelopes()
     * @see WraparoundMethod#EXPAND
     */
    public boolean simplify() throws IllegalStateException {
        return apply(WraparoundMethod.EXPAND);
    }

    /**
     * If this envelope is crossing the limit of a wraparound axis, modifies coordinates by application
     * of the specified strategy. This applies typically to longitude values crossing the anti-meridian,
     * but other kinds of wraparound axes may also exist. Possible values are listed below.
     *
     * <table class="sis">
     *   <caption>Legal argument values</caption>
     *   <tr><th>Value</th><th>Action</th></tr>
     *   <tr><td>{@link WraparoundMethod#NONE}:</td>             <td>Do nothing and return {@code false}.</td></tr>
     *   <tr><td>{@link WraparoundMethod#NORMALIZE}:</td>        <td>Delegate to {@link #normalize()}.</td></tr>
     *   <tr><td>{@link WraparoundMethod#EXPAND}:</td>           <td>Equivalent to {@link #simplify()}.</td></tr>
     *   <tr><td>{@link WraparoundMethod#CONTIGUOUS}:</td>       <td>See enumeration javadoc.</td></tr>
     *   <tr><td>{@link WraparoundMethod#CONTIGUOUS_LOWER}:</td> <td>See enumeration javadoc.</td></tr>
     *   <tr><td>{@link WraparoundMethod#CONTIGUOUS_UPPER}:</td> <td>See enumeration javadoc.</td></tr>
     *   <tr><td>{@link WraparoundMethod#SPLIT}:</td>            <td>Throw {@link IllegalArgumentException}.</td></tr>
     * </table>
     *
     * @param  method  the strategy to use for representing a region crossing the anti-meridian or other wraparound limit.
     * @return {@code true} if this envelope has been modified as a result of this method call,
     *         or {@code false} if no change has been done.
     * @throws IllegalStateException if a upper coordinate value is less than a lower coordinate
     *         value on an axis which does not have the {@code WRAPAROUND} range meaning.
     *
     * @since 1.1
     */
    public boolean wraparound(final WraparoundMethod method) throws IllegalStateException {
        switch (method) {
            case EXPAND:
            case CONTIGUOUS:
            case CONTIGUOUS_LOWER:
            case CONTIGUOUS_UPPER: return apply(method);
            case NORMALIZE:        return normalize();
            case NONE:             return false;
            default: throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "method", method));
        }
    }

    /**
     * Implementation of {@link #simplify()} and {@link #wraparound(WraparoundMethod)}.
     * Argument can be only {@link WraparoundMethod#EXPAND} or {@code CONTIGUOUS*}.
     */
    private boolean apply(final WraparoundMethod method) throws IllegalStateException {
        boolean changed = false;
        final int d = coordinates.length >>> 1;
        final int beginIndex = beginIndex();
        final int dimension = endIndex() - beginIndex;
        for (int i=0; i<dimension; i++) {
            final int iLower = beginIndex + i;
            final int iUpper = iLower + d;
            final double lower = coordinates[iLower];
            final double upper = coordinates[iUpper];
            if (isNegative(upper - lower)) {                        // Use `isNegative(…)` for catching [+0 … -0] range.
                final CoordinateSystemAxis axis = getAxis(crs, i);
                if (isWrapAround(axis)) {
                    changed = true;
                    final double minimum = axis.getMinimumValue();
                    final double maximum = axis.getMaximumValue();
                    if (method != WraparoundMethod.EXPAND) {
                        double cycle = maximum - minimum;
                        if (Double.isFinite(cycle)) {
                            cycle *= Math.ceil((lower - upper) / cycle);
                            boolean up = (method == WraparoundMethod.CONTIGUOUS_UPPER);
                            if (!up && method != WraparoundMethod.CONTIGUOUS_LOWER) {
                                up = (upper - minimum <= maximum - lower);
                            }
                            if (up) coordinates[iUpper] += cycle;
                            else    coordinates[iLower] -= cycle;
                            continue;
                        }
                    }
                    coordinates[iLower] = minimum;
                    coordinates[iUpper] = maximum;
                } else {
                    throw new IllegalStateException(Errors.format(Errors.Keys.IllegalCoordinateRange_3,
                            lower, upper, (axis != null) ? axis.getName() : i));
                }
            }
        }
        return changed;
    }

    /**
     * Returns a view over the two horizontal dimensions of this envelope. The horizontal dimensions are
     * {@linkplain CRS#getHorizontalComponent(CoordinateReferenceSystem) inferred from the CRS}. If this
     * method cannot infer the horizontal dimensions, then an {@link IllegalStateException} is thrown.
     *
     * <p>The returned envelope is a <em>view</em>: changes in the returned envelope are reflected in this
     * envelope, and conversely. The returned envelope will have its CRS defined.</p>
     *
     * @return a view over the horizontal components of this envelope. May be {@code this}.
     * @throws IllegalStateException if this method cannot infer the horizontal components of this envelope.
     *
     * @see #subEnvelope(int, int)
     * @see CRS#getHorizontalComponent(CoordinateReferenceSystem)
     *
     * @since 1.1
     */
    public GeneralEnvelope horizontal() throws IllegalStateException {
        if (crs == null) {
            throw new IllegalStateException(Errors.format(Errors.Keys.UnspecifiedCRS));
        }
        final int tgtDim = 2;
        final int dimension = getDimension();
        if (dimension >= tgtDim) {
            final CoordinateReferenceSystem singleCRS = CRS.getHorizontalComponent(crs);
            if (singleCRS == crs) {
                return this;
            }
            if (singleCRS != null) {
                final int i = AxisDirections.indexOfColinear(crs.getCoordinateSystem(), singleCRS.getCoordinateSystem());
                if (i >= 0) {
                    final GeneralEnvelope sub = subEnvelope(i, i+tgtDim);
                    sub.setCoordinateReferenceSystem(singleCRS);
                    return sub;
                }
            }
        }
        String name = IdentifiedObjects.getDisplayName(crs, null);
        if (name == null) name = Integer.toString(dimension) + 'D';
        throw new IllegalStateException(Errors.format(Errors.Keys.NonHorizontalCRS_1, name));
    }

    /*
     * We do not provide vertical() and temporal() methods at this time. The interest of one-dimensional envelopes
     * is not obvious. Furthermore, in the vertical case it is not clear when we should do about ellipsoidal height,
     * and in the temporal case what we should do with envelopes having 2 temporal axes (as seen in meteorological
     * data). Should we return the two temporal axes in two-dimensional envelopes?
     */

    /**
     * Returns a view over this envelope that encompass only some dimensions. The returned object is "live":
     * changes applied on the original envelope is reflected in the sub-envelope view, and conversely.
     *
     * <p>This method is useful for querying and updating only some dimensions.
     * For example, in order to expand only the horizontal component of a four dimensional
     * (<var>x</var>,<var>y</var>,<var>z</var>,<var>t</var>) envelope, one can use:</p>
     *
     * {@snippet lang="java" :
     *     envelope.subEnvelope(0, 2).add(myPosition2D);
     *     }
     *
     * If the sub-envelope needs to be independent from the original envelope, use the following idiom:
     *
     * {@snippet lang="java" :
     *     GeneralEnvelope copy = envelope.subEnvelope(0, 2).clone();
     *     }
     *
     * The sub-envelope is initialized with a {@code null} {@linkplain #getCoordinateReferenceSystem() CRS}.
     * This method does not compute a sub-CRS because it may not be needed, or the sub-CRS may be already
     * known by the caller.
     *
     * @param  beginIndex  the index of the first valid coordinate value of the corners.
     * @param  endIndex    the index after the last valid coordinate value of the corners.
     * @return the sub-envelope of dimension {@code endIndex - beginIndex}.
     * @throws IndexOutOfBoundsException if an index is out of bounds.
     *
     * @see #horizontal()
     * @see org.apache.sis.referencing.CRS#getComponentAt(CoordinateReferenceSystem, int, int)
     */
    // Must be overridden in SubEnvelope
    public GeneralEnvelope subEnvelope(final int beginIndex, final int endIndex) throws IndexOutOfBoundsException {
        Objects.checkFromToIndex(beginIndex, endIndex, coordinates.length >>> 1);
        return new SubEnvelope(coordinates, beginIndex, endIndex);
        /*
         * Do not check if we could return "this" as an optimization, in order to keep
         * the method contract simpler (i.e. the returned envelope CRS is always null).
         */
    }

    /**
     * Returns a deep copy of this envelope.
     *
     * @return a clone of this envelope.
     */
    @Override
    public GeneralEnvelope clone() {
        try {
            Field field = coordinatesField;
            if (field == null) {
                coordinatesField = field = GeneralDirectPosition.getCoordinatesField(ArrayEnvelope.class);
            }
            GeneralEnvelope e = (GeneralEnvelope) super.clone();
            field.set(e, coordinates.clone());
            return e;
        } catch (CloneNotSupportedException | ReflectiveOperationException exception) {
            /*
             * Should not happen, since we are cloneable, the
             * field is known to exist and we made it accessible.
             */
            throw new AssertionError(exception);
        }
    }
}
