blob: 6d6a0bb7edd9210bd7ec73d945c0e7e3d0c6ea84 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.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.Objects;
import java.io.Serializable;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.cs.RangeMeaning;
import org.opengis.referencing.cs.CoordinateSystemAxis;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.*;
import static org.apache.sis.math.MathFunctions.isNegative;
import static org.apache.sis.internal.referencing.Formulas.isPoleToPole;
/**
* Base class of envelopes backed by an array. The coordinate values are stored in the {@link #coordinates} array.
* The coordinate values of the lower corner are stored in the array portion from index {@link #beginIndex()}
* inclusive to index {@link #endIndex()} exclusive. The coordinate values of the upper corner are stored in
* the array portion from index {@code beginIndex() + d} inclusive to index {@code endIndex() + d} exclusive
* where {@code d = coordinates.length >>> 1}.
*
* <p>Unless otherwise indicated by a "{@code // Must be overridden in SubEnvelope}" comment, every methods
* in {@code ArrayEnvelope} and subclasses must take in account the {@code beginIndex} and {@code endIndex}
* bounds. A few methods ignore the bounds for performance reason, so they need a dedicated implementation
* in {@link SubEnvelope}.</p>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 1.0
* @since 0.3
* @module
*/
class ArrayEnvelope extends AbstractEnvelope implements Serializable {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = 3685400961133848112L;
/**
* Coordinate values of lower and upper corners. Except for {@link SubEnvelope}, the length
* of this array is twice the number of dimensions. The first half contains the lower corner,
* while the second half contains the upper corner.
*/
final double[] coordinates;
/**
* The coordinate reference system, or {@code null}.
*/
CoordinateReferenceSystem crs;
/**
* 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).
*/
ArrayEnvelope(final double[] coordinates) {
this.coordinates = coordinates;
}
/*
* Constructors below this point have public access because if we decided to make class
* ArrayEnvelope public, then we would probably want to make those constructors public too.
*/
/**
* 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 ArrayEnvelope(final DirectPosition lowerCorner, final DirectPosition upperCorner)
throws MismatchedDimensionException, MismatchedReferenceSystemException
{
crs = getCommonCRS(lowerCorner, upperCorner); // This performs also an argument check.
final int dimension = lowerCorner.getDimension();
ensureDimensionMatches("crs", dimension, crs);
ensureSameDimension(dimension, upperCorner.getDimension());
coordinates = new double[dimension * 2];
for (int i=0; i<dimension; i++) {
coordinates[i ] = lowerCorner.getOrdinate(i);
coordinates[i + dimension] = upperCorner.getOrdinate(i);
}
verifyRanges(crs, coordinates);
}
/**
* 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 ArrayEnvelope(final double[] lowerCorner, final double[] upperCorner) throws MismatchedDimensionException {
ensureNonNull("lowerCorner", lowerCorner);
ensureNonNull("upperCorner", upperCorner);
ensureSameDimension(lowerCorner.length, upperCorner.length);
coordinates = Arrays.copyOf(lowerCorner, lowerCorner.length + upperCorner.length);
System.arraycopy(upperCorner, 0, coordinates, lowerCorner.length, upperCorner.length);
}
/**
* 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 ArrayEnvelope(final int dimension) {
coordinates = new double[dimension * 2];
}
/**
* Constructs an empty envelope with the specified coordinate reference system.
* All coordinate values are initialized to 0.
*
* @param crs the coordinate reference system.
*/
public ArrayEnvelope(final CoordinateReferenceSystem crs) {
ensureNonNull("crs", crs);
coordinates = new double[crs.getCoordinateSystem().getDimension() * 2];
this.crs = crs;
}
/**
* Constructs a new envelope with the same data than the specified envelope.
*
* @param envelope the envelope to copy.
*/
public ArrayEnvelope(final Envelope envelope) {
ensureNonNull("envelope", envelope);
/*
* Do not optimize with `if (envelope instanceof ArrayEnvelope)` because subclasses may change the semantic.
* In particular the SubEnvelope subclass uses only a subrange of this array. If we still want to optimize,
* we would have to test for specific classes. It is probably not worth at this stage.
*/
crs = envelope.getCoordinateReferenceSystem();
final int dimension = envelope.getDimension();
coordinates = new double[dimension * 2];
final DirectPosition lowerCorner = envelope.getLowerCorner();
final DirectPosition upperCorner = envelope.getUpperCorner();
for (int i=0; i<dimension; i++) {
coordinates[i] = lowerCorner.getOrdinate(i);
coordinates[i+dimension] = upperCorner.getOrdinate(i);
}
verifyRanges(crs, coordinates);
}
/**
* Constructs a new envelope with the same data than 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 ArrayEnvelope(final GeographicBoundingBox box) {
ensureNonNull("box", box);
coordinates = new double[] {
box.getWestBoundLongitude(),
box.getSouthBoundLatitude(),
box.getEastBoundLongitude(),
box.getNorthBoundLatitude()
};
if (Boolean.FALSE.equals(box.getInclusion())) {
ArraysExt.swap(coordinates, 0, coordinates.length >>> 1);
if (!isPoleToPole(coordinates[1], coordinates[3])) {
ArraysExt.swap(coordinates, 1, (coordinates.length >>> 1) + 1);
}
}
crs = CommonCRS.defaultGeographic();
verifyRanges(crs, coordinates);
}
/**
* Constructs a new envelope initialized to the values parsed from the given string in
* {@code BOX} or <cite>Well Known Text</cite> (WKT) format. The given string is typically
* a {@code BOX} element like below:
*
* {@preformat wkt
* BOX(-180 -90, 180 90)
* }
*
* However this constructor is lenient to other geometry types like {@code POLYGON}.
* See the javadoc of the {@link GeneralEnvelope#GeneralEnvelope(CharSequence) GeneralEnvelope}
* constructor for more information.
*
* @param wkt the {@code BOX}, {@code POLYGON} or other kind of element to parse.
* @throws IllegalArgumentException if the given string can not be parsed.
*/
public ArrayEnvelope(final CharSequence wkt) throws IllegalArgumentException {
ensureNonNull("wkt", wkt);
int levelParenth = 0; // Number of opening parenthesis: (
int levelBracket = 0; // Number of opening brackets: [
int dimLimit = 4; // The length of minimum and maximum arrays.
int maxDimension = 0; // The number of valid entries in the minimum and maximum arrays.
final int length = CharSequences.skipTrailingWhitespaces(wkt, 0, wkt.length());
double[] minimum = new double[dimLimit];
double[] maximum = new double[dimLimit];
int dimension = 0;
int c;
scan: for (int i=CharSequences.skipLeadingWhitespaces(wkt, 0, length); i<length; i+=Character.charCount(c)) {
c = Character.codePointAt(wkt, i);
if (Character.isUnicodeIdentifierStart(c)) {
do {
i += Character.charCount(c);
if (i >= length) break scan;
c = Character.codePointAt(wkt, i);
}
while (Character.isUnicodeIdentifierPart(c));
}
if (Character.isSpaceChar(c)) {
continue;
}
switch (c) {
case ',': dimension=0; continue;
case '(': ++levelParenth; dimension=0; continue;
case '[': ++levelBracket; dimension=0; continue;
case ')': if (--levelParenth<0) fail(wkt,'('); dimension=0; continue;
case ']': if (--levelBracket<0) fail(wkt,'['); dimension=0; continue;
}
/*
* At this point we have skipped the leading keyword (BOX, POLYGON, etc.),
* the spaces and the parenthesis if any. We should be at the beginning of
* a number. Search the first separator character (which determine the end
* of the number) and parse the number.
*/
final int start = i;
boolean flush = false;
scanNumber: while ((i += Character.charCount(c)) < length) {
c = wkt.charAt(i);
if (Character.isSpaceChar(c)) {
break;
}
switch (c) {
case ',': flush=true; break scanNumber;
case ')': if (--levelParenth<0) fail(wkt,'('); flush=true; break scanNumber;
case ']': if (--levelBracket<0) fail(wkt,'['); flush=true; break scanNumber;
}
}
/*
* Parsing the number may throw a NumberFormatException. But the later is an
* IllegalArgumentException subclass, so we are compliant with the contract.
*/
final double value = Double.parseDouble(wkt.subSequence(start, i).toString());
/*
* Adjust the minimum and maximum value using the number that we parsed,
* increasing the arrays size if necessary. Remember the maximum number
* of dimensions we have found so far.
*/
if (dimension == maxDimension) {
if (dimension == dimLimit) {
dimLimit *= 2;
minimum = Arrays.copyOf(minimum, dimLimit);
maximum = Arrays.copyOf(maximum, dimLimit);
}
minimum[dimension] = maximum[dimension] = value;
maxDimension = ++dimension;
} else {
if (value < minimum[dimension]) minimum[dimension] = value;
if (value > maximum[dimension]) maximum[dimension] = value;
dimension++;
}
if (flush) {
dimension = 0;
}
}
if (levelParenth != 0) fail(wkt, ')');
if (levelBracket != 0) fail(wkt, ']');
coordinates = ArraysExt.resize(minimum, maxDimension << 1);
System.arraycopy(maximum, 0, coordinates, maxDimension, maxDimension);
}
/**
* Throws an exception for unmatched parenthesis during WKT parsing.
*/
private static void fail(final CharSequence wkt, char missing) {
throw new IllegalArgumentException(Errors.format(
Errors.Keys.NonEquilibratedParenthesis_2, wkt, missing));
}
/**
* Makes sure the specified dimensions are identical.
*/
static void ensureSameDimension(final int dim1, final int dim2) throws MismatchedDimensionException {
if (dim1 != dim2) {
throw new MismatchedDimensionException(Errors.format(
Errors.Keys.MismatchedDimension_2, dim1, dim2));
}
}
/**
* Verifies the validity of the range of coordinates values in the given array.
* If the given CRS is null, then this method conservatively does nothing.
* Otherwise this method performs the following verifications:
*
* <ul>
* <li>{@code lower > upper} is allowed only for axes having {@link RangeMeaning#WRAPAROUND}.</li>
* </ul>
*
* This method does <strong>not</strong> verify if the coordinate values are between the axis minimum and
* maximum values. This is because out-of-range values exist in practice but do not impact the working
* of {@code add(…)}, {@code intersect(…)}, {@code contains(…)} and similar methods. This in contrast
* with the checks listed above, where failure to met those conditions will cause the methods to
* behave in an unexpected way.
*
* <h4>Implementation consistency</h4>
* The checks performed by this method shall be consistent with the checks performed by the following methods:
* <ul>
* <li>{@link GeneralEnvelope#setCoordinateReferenceSystem(CoordinateReferenceSystem)}</li>
* <li>{@link GeneralEnvelope#setRange(int, double, double)}</li>
* <li>{@link SubEnvelope#setRange(int, double, double)}</li>
* </ul>
*
* @param crs the coordinate reference system, or {@code null}.
* @param coordinates the array of coordinate values to verify.
*/
static void verifyRanges(final CoordinateReferenceSystem crs, final double[] coordinates) {
if (crs != null) {
final int dimension = coordinates.length >>> 1;
for (int i=0; i<dimension; i++) {
final double lower = coordinates[i];
final double upper = coordinates[i + dimension];
if (lower > upper && !isWrapAround(crs, i)) {
throw new IllegalArgumentException(illegalRange(crs, i, lower, upper));
}
}
}
}
/**
* Creates an error message for an illegal coordinates range at the given dimension.
* This is used for formatting the exception message.
*/
static String illegalRange(final CoordinateReferenceSystem crs,
final int dimension, final double lower, final double upper)
{
Object name = IdentifiedObjects.getName(getAxis(crs, dimension), null);
if (name == null) {
name = dimension; // Paranoiac fallback (name should never be null).
}
return Errors.format(Errors.Keys.IllegalCoordinateRange_3, lower, upper, name);
}
/**
* Returns the index of the first valid coordinate value of the lower corner in the {@link #coordinates} array.
* This is always 0, unless this envelope is a {@link SubEnvelope}.
*
* <p>See {@link #endIndex()} for the list of methods that need to be also overridden
* if this {@code beginIndex()} method is overridden.</p>
*/
int beginIndex() {
return 0;
}
/**
* Returns the index after the last valid coordinate value of the lower corner in the {@link #coordinates} array.
* This is always {@code coordinates.length >>> 1}, unless this envelope is a {@link SubEnvelope}.
*
* <p>Unless otherwise indicated by a "{@code // Must be overridden in SubEnvelope}" comment, every methods
* in {@code ArrayEnvelope} and subclasses must take in account the {@code beginIndex} and {@code endIndex}
* bounds. The methods listed below ignore the bounds for performance reason, so they need to be overridden
* in {@link SubEnvelope}:</p>
*
* <ul>
* <li>{@link #getDimension()}</li>
* <li>{@link #getLower(int)}</li>
* <li>{@link #getUpper(int)}</li>
* <li>{@link #isAllNaN()}</li>
* <li>{@link #hashCode()}</li>
* <li>{@link #equals(Object)}</li>
* </ul>
*/
int endIndex() {
return coordinates.length >>> 1;
}
/**
* Returns the length of coordinate sequence (the number of entries) in this envelope.
* This information is available even when the {@linkplain #getCoordinateReferenceSystem()
* coordinate reference system} is unknown.
*
* @return the dimensionality of this envelope.
*/
@Override // Must also be overridden in SubEnvelope
public int getDimension() {
return coordinates.length >>> 1;
}
/**
* Returns the envelope coordinate reference system, or {@code null} if unknown.
* If non-null, it shall be the same as {@linkplain #getLowerCorner() lower corner}
* and {@linkplain #getUpperCorner() upper corner} CRS.
*
* @return the envelope CRS, or {@code null} if unknown.
*/
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
assert crs == null || crs.getCoordinateSystem().getDimension() == getDimension();
return crs;
}
/**
* {@inheritDoc}
*/
@Override // Must also be overridden in SubEnvelope
public double getLower(final int dimension) throws IndexOutOfBoundsException {
ensureValidIndex(coordinates.length >>> 1, dimension);
return coordinates[dimension];
}
/**
* {@inheritDoc}
*/
@Override // Must also be overridden in SubEnvelope
public double getUpper(final int dimension) throws IndexOutOfBoundsException {
final int d = coordinates.length >>> 1;
ensureValidIndex(d, dimension);
return coordinates[dimension + d];
}
/**
* {@inheritDoc}
*/
@Override
public double getMinimum(final int dimension) throws IndexOutOfBoundsException {
ensureValidIndex(endIndex(), dimension);
final int i = dimension + beginIndex();
double lower = coordinates[i];
if (isNegative(coordinates[i + (coordinates.length >>> 1)] - lower)) { // Special handling for -0.0
final CoordinateSystemAxis axis = getAxis(crs, dimension);
lower = isWrapAround(axis) ? axis.getMinimumValue() : Double.NaN;
}
return lower;
}
/**
* {@inheritDoc}
*/
@Override
public double getMaximum(final int dimension) throws IndexOutOfBoundsException {
ensureValidIndex(endIndex(), dimension);
final int i = dimension + beginIndex();
double upper = coordinates[i + (coordinates.length >>> 1)];
if (isNegative(upper - coordinates[i])) { // Special handling for -0.0
final CoordinateSystemAxis axis = getAxis(crs, dimension);
upper = isWrapAround(axis) ? axis.getMaximumValue() : Double.NaN;
}
return upper;
}
/**
* {@inheritDoc}
*/
@Override
public double getMedian(final int dimension) throws IndexOutOfBoundsException {
ensureValidIndex(endIndex(), dimension);
final int i = dimension + beginIndex();
final double minimum = coordinates[i];
final double maximum = coordinates[i + (coordinates.length >>> 1)];
double median = 0.5 * (minimum + maximum);
if (isNegative(maximum - minimum)) { // Special handling for -0.0
median = fixMedian(getAxis(crs, dimension), median);
}
return median;
}
/**
* {@inheritDoc}
*/
@Override
public double getSpan(final int dimension) throws IndexOutOfBoundsException {
ensureValidIndex(endIndex(), dimension);
final int i = dimension + beginIndex();
double span = coordinates[i + (coordinates.length >>> 1)] - coordinates[i];
if (isNegative(span)) { // Special handling for -0.0
span = fixSpan(getAxis(crs, dimension), span);
}
return span;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isEmpty() {
final int beginIndex = beginIndex();
final int endIndex = endIndex();
if (beginIndex == endIndex) {
return true;
}
final int d = coordinates.length >>> 1;
for (int i=beginIndex; i<endIndex; i++) {
final double span = coordinates[i+d] - coordinates[i];
if (!(span > 0)) { // Use '!' in order to catch NaN
if (!(isNegative(span) && isWrapAround(crs, i - beginIndex))) {
return true;
}
}
}
assert !isAllNaN() : this;
return false;
}
/**
* {@inheritDoc}
*/
@Override // Must also be overridden in SubEnvelope
public boolean isAllNaN() {
for (int i=0; i<coordinates.length; i++) {
if (!Double.isNaN(coordinates[i])) {
return false;
}
}
assert isEmpty() : this;
return true;
}
/**
* {@inheritDoc}
*/
@Override // Must also be overridden in SubEnvelope
public int hashCode() {
int code = Arrays.hashCode(coordinates);
if (crs != null) {
code += crs.hashCode();
}
assert code == hashCodeByAPI();
return code;
}
/**
* Computes the hash code value using the public API instead than direct access to the
* {@link #coordinates} array. This method is invoked from {@link SubEnvelope}.
*/
final int hashCodeByAPI() {
return super.hashCode();
}
/**
* {@inheritDoc}
*/
@Override // Must also be overridden in SubEnvelope
public boolean equals(final Object object) {
if (object != null && object.getClass() == getClass()) {
final ArrayEnvelope that = (ArrayEnvelope) object;
return Arrays.equals(this.coordinates, that.coordinates) &&
Objects.equals(this.crs, that.crs);
}
return false;
}
/**
* Compares the given object for equality using the public API instead than direct access
* to the {@link #coordinates} array. This method is invoked from {@link SubEnvelope}.
*/
final boolean equalsByAPI(final Object object) {
return super.equals(object);
}
/**
* {@inheritDoc}
*/
@Override
public String toString() {
return toString(this, ArraysExt.isSinglePrecision(coordinates));
}
}