/* (c) 2014 Open Source Geospatial Foundation - all rights reserved * (c) 2014 OpenPlans * This code is licensed under the GPL 2.0 license, available at the root * application directory. */ package org.geoserver.wcs2_0; import org.geotools.geometry.AbstractEnvelope; import org.geotools.geometry.GeneralEnvelope; import org.geotools.referencing.CRS; import org.geotools.referencing.cs.DefaultCoordinateSystemAxis; import org.opengis.geometry.Envelope; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.cs.CoordinateSystem; import org.opengis.referencing.cs.CoordinateSystemAxis; /** * A custom {@link Envelope} that allows to set a min value of longitude higher than the max one in * selected methods to deal with the dateline crossing case * * @author Andrea Aime - GeoSolutions * */ public class WCSEnvelope extends AbstractEnvelope { private static final int LONGIDUTE_NOT_FOUND = -1; CoordinateReferenceSystem crs; double[] ordinates; int dimensions; int longitudeDimension = LONGIDUTE_NOT_FOUND; /** * Creates an empty envelope based on the given coordinate reference system */ public WCSEnvelope(CoordinateReferenceSystem crs) { if (crs == null) { throw new IllegalArgumentException( "WCSEnvelope coordinate reference system cannot be null"); } this.crs = crs; // initialize the longitude dimension, if we have a longitude, that is CoordinateSystem cs = crs.getCoordinateSystem(); this.dimensions = cs.getDimension(); this.ordinates = new double[dimensions * 2]; for (int i = 0; i < dimensions; i++) { CoordinateSystemAxis axis = cs.getAxis(i); if (CRS.equalsIgnoreMetadata(axis, DefaultCoordinateSystemAxis.LONGITUDE)) { longitudeDimension = i; break; } } } /** * Copies an existing envelope * * @param other */ public WCSEnvelope(Envelope other) { this(other.getCoordinateReferenceSystem()); for (int d = 0; d < dimensions; d++) { setRange(d, other.getMinimum(d), other.getMaximum(d)); } } /** * Sets the range for the given dimension. If the dimension is the longitude, it is allowed to * set a minimum greater than the maximum, this envelope will be assumed to span the dateline * * @param dimension * @param minimum * @param maximum * @throws IndexOutOfBoundsException */ public void setRange(int dimension, double minimum, double maximum) throws IndexOutOfBoundsException { if (minimum > maximum && (longitudeDimension != LONGIDUTE_NOT_FOUND && dimension != longitudeDimension)) { // Make an empty envelope (min == max) // while keeping it legal (min <= max). minimum = maximum = 0.5 * (minimum + maximum); } else if (dimension >= 0 && dimension < ordinates.length / 2) { ordinates[dimension + ordinates.length / 2] = maximum; ordinates[dimension] = minimum; } else { throw indexOutOfBounds(dimension); } } /** * Returns true if the envelope has a empty span on at least one dimension. A span is empty if * its zero or negative, but in case a dimension is the longitude, a negative span will be * treated as a dateline crossing, and thus treated as non empty * * */ public boolean isEmpty() { for (int i = 0; i < dimensions; i++) { double span = getSpan(i); if (span == 0) { return true; } else if (span < 0 && i != longitudeDimension) { return true; } } return false; } @Override public CoordinateReferenceSystem getCoordinateReferenceSystem() { return crs; } @Override public int getDimension() { return dimensions; } @Override public double getMinimum(int dimension) throws IndexOutOfBoundsException { if (dimension < this.dimensions) { return ordinates[dimension]; } else { throw indexOutOfBounds(dimension); } } private IndexOutOfBoundsException indexOutOfBounds(int dimension) { return new IndexOutOfBoundsException("Invalid dimension " + dimension); } @Override public double getMaximum(int dimension) throws IndexOutOfBoundsException { if (dimension < this.dimensions) { return ordinates[dimension + ordinates.length / 2]; } else { throw indexOutOfBounds(dimension); } } @Override public double getMedian(int dimension) throws IndexOutOfBoundsException { if (dimension < ordinates.length / 2) { return 0.5 * (ordinates[dimension] + ordinates[dimension + ordinates.length / 2]); } else { throw indexOutOfBounds(dimension); } } @Override public double getSpan(int dimension) throws IndexOutOfBoundsException { if (dimension < ordinates.length / 2) { return ordinates[dimension + ordinates.length / 2] - ordinates[dimension]; } else { throw indexOutOfBounds(dimension); } } /** * Returns a list of envelopes that avoid the dateline crossing "odd" representation, that is, * in that case two envelopes will be returned covering the portion before and after the * dateline * * */ public GeneralEnvelope[] getNormalizedEnvelopes() { if (!isCrossingDateline()) { return new GeneralEnvelope[] { new GeneralEnvelope(this) }; } else { GeneralEnvelope e1 = new GeneralEnvelope(crs); GeneralEnvelope e2 = new GeneralEnvelope(crs); for (int i = 0; i < dimensions; i++) { if (i == longitudeDimension) { e1.setRange(i, getMinimum(i), 180); if (getSpan(longitudeDimension) < 0) { e2.setRange(i, -180, getMaximum(i)); } else { e2.setRange(i, -180, getMaximum(i) - 360); } } else { e1.setRange(i, getMinimum(i), getMaximum(i)); e2.setRange(i, getMinimum(i), getMaximum(i)); } } return new GeneralEnvelope[] { e1, e2 }; } } /** * Checks if this envelope intersects the provided one, taking into account the case of dateline * crossing. * * @param other */ public void intersect(GeneralEnvelope other) { assert other.getDimension() == dimensions : other; assert CRS.equalsIgnoreMetadata(crs, other.getCoordinateReferenceSystem()) : other; if (isCrossingDateline()) { GeneralEnvelope[] normalizedEnvelopes = getNormalizedEnvelopes(); for (GeneralEnvelope ge : normalizedEnvelopes) { ge.intersect(other); } for (int i = 0; i < dimensions; i++) { if (i == longitudeDimension) { if (normalizedEnvelopes[0].getSpan(i) == 0) { ordinates[i] = normalizedEnvelopes[1].getMinimum(i); ordinates[i + dimensions] = normalizedEnvelopes[1].getMaximum(i); } else if (normalizedEnvelopes[1].getSpan(i) == 0) { ordinates[i] = normalizedEnvelopes[0].getMinimum(i); ordinates[i + dimensions] = normalizedEnvelopes[0].getMaximum(i); } else { ordinates[i] = normalizedEnvelopes[0].getMinimum(i); ordinates[i + dimensions] = normalizedEnvelopes[1].getMaximum(i); } } else { ordinates[i] = normalizedEnvelopes[0].getMinimum(i); ordinates[i + dimensions] = normalizedEnvelopes[0].getMaximum(i); } } } else { for (int i = 0; i < dimensions; i++) { double min = Math.max(ordinates[i], other.getMinimum(i)); double max = Math.min(ordinates[i + dimensions], other.getMaximum(i)); if (min > max) { // Make an empty envelope (min==max) // while keeping it legal (min<=max). min = max = 0.5 * (min + max); } ordinates[i] = min; ordinates[i + dimensions] = max; } } } /** * Returns true if this envelope is crossing the dateline * * */ public boolean isCrossingDateline() { // TODO: handle the case the envelope is in a projected system and still // crossing the dateline (e.g. polar, or mercator centered around the dateline) return longitudeDimension != LONGIDUTE_NOT_FOUND && (getSpan(longitudeDimension) < 0 || (getMinimum(longitudeDimension) < 180 && getMaximum(longitudeDimension) > 180)); } /** * Returns true if the specified dimension index is matching the longitude axis * * @param dimension * */ public boolean isLongitude(int dimension) { return longitudeDimension == dimension; } }