blob: 7a98cbd9d6e502e1a2f2dc4b08cfb08e02868dbc [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.gazetteer;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.coordinate.Position;
import org.opengis.metadata.extent.GeographicExtent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.referencing.gazetteer.LocationType;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.apache.sis.geometry.DirectPosition2D;
import org.apache.sis.referencing.CommonCRS;
import org.apache.sis.util.resources.Errors;
/**
* A location described by an unmodifiable direct position that defines the centroid of an envelope.
* This class encompasses most information in a single object, which make it lightweight to create
* (less pressure on the garbage collector). However this is not a clear separation of responsibility,
* so this class should not be in public API.
*
* <p>Subclasses <strong>must</strong> override the following methods if the above coordinate reference
* system is not a geographic CRS with (<var>longitude</var>, <var>latitude</var>) axes in degrees:</p>
* <ul>
* <li>{@link #getCoordinateReferenceSystem()}</li>
* <li>{@link #getWestBoundLongitude()}</li>
* <li>{@link #getEastBoundLongitude()}</li>
* <li>{@link #getSouthBoundLatitude()}</li>
* <li>{@link #getNorthBoundLatitude()}</li>
* </ul>
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.8
* @module
*/
class SimpleLocation extends AbstractLocation implements DirectPosition, Envelope, GeographicBoundingBox {
/**
* The westernmost bound of the envelope, or {@code NaN} if not yet computed.
*
* @see #getLowerCorner()
* @see #getWestBoundLongitude()
*/
protected double minX = Double.NaN;
/**
* The southernmost bound of the envelope, or {@code NaN} if not yet computed.
*
* @see #getLowerCorner()
* @see #getSouthBoundLatitude()
*/
protected double minY = Double.NaN;
/**
* The easternmost bound of the envelope, or {@code NaN} if not yet computed.
*
* @see #getUpperCorner()
* @see #getEastBoundLongitude()
*/
protected double maxX = Double.NaN;
/**
* The northernmost bound of the envelope, or {@code NaN} if not yet computed.
*
* @see #getUpperCorner()
* @see #getNorthBoundLatitude()
*/
protected double maxY = Double.NaN;
/**
* Creates a new location for the given geographic identifier.
* This constructor accepts {@code null} arguments, but this is not recommended.
*
* @param type the description of the nature of this geographic identifier.
* @param identifier the geographic identifier to be returned by {@link #getGeographicIdentifier()}.
*/
SimpleLocation(final LocationType type, final CharSequence identifier) {
super(type, identifier);
}
/**
* Returns a description of the location instance.
* In this simple implementation, this instance is its own geographic extent.
*/
@Override
public GeographicExtent getGeographicExtent() {
return this;
}
/**
* Returns an envelope that encompass the location.
* In this simple implementation, this instance is its own envelope.
*/
@Override
public final Envelope getEnvelope() {
return this;
}
/**
* Returns coordinates of a centroid point for the location instance.
* In this simple implementation, this instance is its own centroid coordinate.
*/
@Override
public final Position getPosition() {
return this;
}
/**
* Returns the direct position, which is itself.
*/
@Override
public final DirectPosition getDirectPosition() {
return this;
}
/**
* Returns the coordinate reference system the envelope and the position.
* Default implementation returns {@link CommonCRS#defaultGeographic()}.
* Subclasses must override this method if they use another CRS.
*/
@Override
public CoordinateReferenceSystem getCoordinateReferenceSystem() {
return CommonCRS.defaultGeographic();
}
/**
* Returns the number of dimensions, which is fixed to 2.
*/
@Override
public final int getDimension() {
return 2;
}
/**
* Returns the coordinates of the centroid.
*/
@Override
public final double[] getCoordinate() {
return new double[] {getOrdinate(0), getOrdinate(1)};
}
/**
* Returns the centroid coordinate value for the specified dimension.
*/
@Override
public final double getOrdinate(final int dimension) {
return getMedian(dimension);
}
/**
* Returns the minimal coordinate value for the specified dimension.
*/
@Override
public final double getMinimum(final int dimension) {
switch (dimension) {
case 0: return minX;
case 1: return minY;
default: throw new IndexOutOfBoundsException(indexOutOfBounds(dimension));
}
}
/**
* Returns the maximal coordinate value for the specified dimension.
*/
@Override
public final double getMaximum(final int dimension) {
switch (dimension) {
case 0: return maxX;
case 1: return maxY;
default: throw new IndexOutOfBoundsException(indexOutOfBounds(dimension));
}
}
/**
* Returns the median coordinate value for the specified dimension.
*/
@Override
public final double getMedian(final int dimension) {
switch (dimension) {
case 0: return (minX + maxX) / 2;
case 1: return (minY + maxY) / 2;
default: throw new IndexOutOfBoundsException(indexOutOfBounds(dimension));
}
}
/**
* Returns the envelope width or height along the specified dimension.
*/
@Override
public final double getSpan(final int dimension) {
switch (dimension) {
case 0: return (maxX - minX);
case 1: return (maxY - minY);
default: throw new IndexOutOfBoundsException(indexOutOfBounds(dimension));
}
}
/**
* Returns the error message for an index out of bounds.
*/
private static String indexOutOfBounds(final int dimension) {
return Errors.format(Errors.Keys.IndexOutOfBounds_1, dimension);
}
/**
* Returns a copy of the lower-left corner.
*/
@Override
public final DirectPosition getLowerCorner() {
return new DirectPosition2D(getCoordinateReferenceSystem(), minX, minY);
}
/**
* Returns a copy of the upper-right corner.
*/
@Override
public final DirectPosition getUpperCorner() {
return new DirectPosition2D(getCoordinateReferenceSystem(), maxX, maxY);
}
/**
* Do not allow modification of the direct position.
*/
@Override
public final void setOrdinate(int dimension, double value) {
throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, DirectPosition.class));
}
/**
* Indication of whether the bounding polygon encompasses an area covered by the data
* (<cite>inclusion</cite>) or an area where data is not present (<cite>exclusion</cite>).
* The value is fixed to {@code true}.
*/
@Override
public final Boolean getInclusion() {
return Boolean.TRUE;
}
/**
* Returns the westernmost longitude in degrees. Default implementation assumes that the
* {@linkplain #getCoordinateReferenceSystem() coordinate reference system} is geographic
* with (<var>longitude</var>, <var>latitude</var>) axes in degrees.
* Subclasses <strong>must</strong> override if this is not the case.
*/
@Override
public double getWestBoundLongitude() {
return minX;
}
/**
* Returns the easternmost longitude in degrees. Default implementation assumes that the
* {@linkplain #getCoordinateReferenceSystem() coordinate reference system} is geographic
* with (<var>longitude</var>, <var>latitude</var>) axes in degrees.
* Subclasses <strong>must</strong> override if this is not the case.
*/
@Override
public double getEastBoundLongitude() {
return maxX;
}
/**
* Returns the southernmost latitude in degrees. Default implementation assumes that the
* {@linkplain #getCoordinateReferenceSystem() coordinate reference system} is geographic
* with (<var>longitude</var>, <var>latitude</var>) axes in degrees.
* Subclasses <strong>must</strong> override if this is not the case.
*/
@Override
public double getSouthBoundLatitude() {
return minY;
}
/**
* Returns the northernmost latitude in degrees. Default implementation assumes that the
* {@linkplain #getCoordinateReferenceSystem() coordinate reference system} is geographic
* with (<var>longitude</var>, <var>latitude</var>) axes in degrees.
* Subclasses <strong>must</strong> override if this is not the case.
*/
@Override
public double getNorthBoundLatitude() {
return maxY;
}
/**
* A {@code SimpleLocation} for non-geographic CRS.
* Subclasses should invoke {@link #computeGeographicBoundingBox(MathTransform)} after the
* {@link #minX}, {@link #minY}, {@link #maxX} and {@link #maxY} fields have been set.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
* @since 0.8
* @module
*/
abstract static class Projected extends SimpleLocation implements GeographicBoundingBox {
/**
* The western-most coordinate of the limit of the dataset extent.
* The value is expressed in longitude in decimal degrees (positive east).
*/
protected double westBoundLongitude = Double.NaN;
/**
* The eastern-most coordinate of the limit of the dataset extent.
* The value is expressed in longitude in decimal degrees (positive east).
*/
protected double eastBoundLongitude = Double.NaN;
/**
* The southern-most coordinate of the limit of the dataset extent.
* The value is expressed in latitude in decimal degrees (positive north).
*/
protected double southBoundLatitude = Double.NaN;
/**
* The northern-most, coordinate of the limit of the dataset extent.
* The value is expressed in latitude in decimal degrees (positive north).
*/
protected double northBoundLatitude = Double.NaN;
/**
* Creates a new location for the given geographic identifier.
* This constructor accepts {@code null} arguments, but this is not recommended.
*
* @param type the description of the nature of this geographic identifier.
* @param identifier the geographic identifier to be returned by {@link #getGeographicIdentifier()}.
*/
Projected(final LocationType type, final CharSequence identifier) {
super(type, identifier);
}
/**
* Returns the western-most coordinate of the limit of the dataset extent.
* The value is expressed in longitude in decimal degrees (positive east).
*/
@Override
public final double getWestBoundLongitude() {
return westBoundLongitude;
}
/**
* Returns the eastern-most coordinate of the limit of the dataset extent.
* The value is expressed in longitude in decimal degrees (positive east).
*/
@Override
public final double getEastBoundLongitude() {
return eastBoundLongitude;
}
/**
* Returns the southern-most coordinate of the limit of the dataset extent.
* The value is expressed in latitude in decimal degrees (positive north).
*/
@Override
public final double getSouthBoundLatitude() {
return southBoundLatitude;
}
/**
* Returns the northern-most, coordinate of the limit of the dataset extent.
* The value is expressed in latitude in decimal degrees (positive north).
*/
@Override
public final double getNorthBoundLatitude() {
return northBoundLatitude;
}
/**
* Computes the geographic bounding box from the current values of {@link #minX}, {@link #minY}, {@link #maxX}
* and {@link #maxY} fields. This method performs a work similar to the {@code Envelopes.transform(…)} methods
* but using a much simpler (and faster) algorithm: this method projects only the 4 corners, without any check
* for the number of dimensions, projection of median coordinates, use of projection derivatives for locating
* the envelope extremum or special checks for polar cases. This method is okay only when the current envelope
* is the envelope of a cell of a grid that divide the projection area in a very regular way (for example with
* the guarantee that the projection central meridian will never be in the middle of grid cell, <i>etc</i>).
*
* <p>If a geographic bounding box was already defined before invoking this method, then it will be expanded
* (if needed) for encompassing the bounding box computed by this method.</p>
*
* @param inverse the transform from projected coordinates to geographic coordinates.
* @throws TransformException if a coordinate operation failed.
*
* @see org.apache.sis.geometry.Envelopes#transform(MathTransform, Envelope)
*/
final void computeGeographicBoundingBox(final MathTransform inverse) throws TransformException {
final double[] points = new double[] {
minX, minY,
minX, maxY,
maxX, minY,
maxX, maxY
};
inverse.transform(points, 0, points, 0, 4);
for (int i=0; i < points.length;) {
final double φ = points[i++];
final double λ = points[i++];
if (Double.isNaN(φ) || Double.isNaN(λ)) {
throw new TransformException(Errors.format(Errors.Keys.CanNotTransformEnvelope));
}
if (!(φ >= southBoundLatitude)) southBoundLatitude = φ; // Use '!' for accepting NaN.
if (!(φ <= northBoundLatitude)) northBoundLatitude = φ;
if (!(λ >= westBoundLongitude)) westBoundLongitude = λ;
if (!(λ <= eastBoundLongitude)) eastBoundLongitude = λ;
}
}
/**
* Clips the geographic bounding box to the given area. If the bounding box changed as a result of this method
* call, then caller should consider invoking {@link #clipProjectedEnvelope(MathTransform, double, double)}.
*
* @return whether the geographic bounding box changed as a result of this method call.
*/
final boolean clipGeographicBoundingBox(final double λmin, final double φmin, final double λmax, final double φmax) {
boolean changed = false;
if (westBoundLongitude < λmin) {westBoundLongitude = λmin; changed = true;}
if (eastBoundLongitude > λmax) {eastBoundLongitude = λmax; changed = true;}
if (southBoundLatitude < φmin) {southBoundLatitude = φmin; changed = true;}
if (northBoundLatitude > φmax) {northBoundLatitude = φmax; changed = true;}
return changed;
}
/**
* Projects the geographic bounding box and clips the current envelope to the result of that projection.
* This method should be invoked when {@link #clipGeographicBoundingBox(double, double, double, double)}
* returned {@code true}.
*
* @param forward the transform from geographic coordinates to projected coordinates.
* @param tx tolerance threshold in easting values for changing {@link #minX} or {@link #maxX}.
* @param ty tolerance threshold in northing values for changing {@link #minY} or {@link #maxY}.
* @throws TransformException if a coordinate operation failed.
*/
final void clipProjectedEnvelope(final MathTransform forward, final double tx, final double ty) throws TransformException {
final double[] points = new double[] {
southBoundLatitude, westBoundLongitude,
northBoundLatitude, westBoundLongitude,
southBoundLatitude, eastBoundLongitude,
northBoundLatitude, eastBoundLongitude
};
forward.transform(points, 0, points, 0, 4);
double xmin, ymin, xmax, ymax;
xmin = xmax = points[0];
ymin = ymax = points[1];
for (int i=2; i < points.length;) {
final double x = points[i++];
final double y = points[i++];
if (x < xmin) xmin = x;
if (x > xmax) xmax = x;
if (y < ymin) ymin = y;
if (y > ymax) ymax = y;
}
if (xmin > minX + tx) minX = xmin;
if (xmax < maxX - tx) maxX = xmax;
if (ymin > minY + ty) minY = ymin;
if (ymax < maxY - ty) maxY = ymax;
}
}
/**
* Converts the current envelope using the given math transform.
* The given transform usually performs nothing more than axis swapping or unit conversions.
*
* @param mt the math transform to use for conversion.
* @param buffer a temporary buffer of length 8 or more.
* @throws TransformException if an error occurred while converting the points.
*/
final void convert(final MathTransform mt, final double[] buffer) throws TransformException {
buffer[3] = buffer[7] = maxY;
buffer[4] = buffer[6] = maxX;
buffer[1] = buffer[5] = minY;
buffer[0] = buffer[2] = minX;
minX = maxX = minY = maxY = Double.NaN;
mt.transform(buffer, 0, buffer, 0, 4);
for (int i=0; i<8;) {
final double x = buffer[i++];
final double y = buffer[i++];
if (Double.isNaN(x) || Double.isNaN(y)) {
throw new TransformException(Errors.format(Errors.Keys.CanNotTransformEnvelope));
}
if (!(x >= minX)) minX = x; // Use '!' for accepting NaN.
if (!(x <= maxX)) maxX = x;
if (!(y >= minY)) minY = y;
if (!(y <= maxY)) maxY = y;
}
}
}