From commits-return-11873-apmail-sis-commits-archive=sis.apache.org@sis.apache.org Sun Apr 7 19:07:26 2019
Return-Path:
The coordinate reference system must be specified in the {@code areaOfInterest} - * specified at construction time, or (as a fallback) in the given {@code domainOfValidity}. - * If none of those envelopes have a CRS, then this method does nothing.
+ * Sets {@link #crs} to the given value if it is non-null, or to the {@link #domainOfValidity} CRS otherwise. + * If no non-null CRS is available, returns {@code false} for instructing caller to terminate immediately. * - *This method does not intersect the area of interest with the domain of validity. - * It is up to the caller to compute that intersection after this method call, if desired.
- * - * @param domainOfValidity the domain of validity, or {@code null} if none. - * @param validToAOI if the envelopes do not use the same CRS, the transformation from {@code domainOfValidity} - * to {@code areaOfInterest}. Otherwise {@code null}. This method does not check by itself if - * a coordinate operation is needed; it must be supplied. - * @return this object, allowing to chain {@link #result(MathTransform)} operation. - * @throws TransformException if an envelope transformation was required but failed. - * - * @see GeneralEnvelope#simplify() + * @return whether a non-null CRS has been set. */ - public WraparoundAdjustment shiftInto(Envelope domainOfValidity, MathTransform validToAOI) throws TransformException { - CoordinateReferenceSystem crs = areaOfInterest.getCoordinateReferenceSystem(); + private boolean setIfNonNull(CoordinateReferenceSystem crs) { if (crs == null) { - crs = domainOfValidity.getCoordinateReferenceSystem(); // Assumed to apply to AOI too. + assert domainToAOI == null || domainToAOI.isIdentity(); + crs = domainOfValidity.getCoordinateReferenceSystem(); // Assumed to apply to AOI or POI too. if (crs == null) { - return this; + return false; } } - /* - * If the coordinate reference system is a projected CRS, it will not have any wraparound axis. - * We need to perform the verification in its base geographic CRS instead, and remember that we - * may need to transform the result later. - */ - final MathTransform projection; - final DirectPosition lowerCorner; - final DirectPosition upperCorner; - GeneralEnvelope shifted = null; // To be initialized to a copy of 'areaOfInterest' when first needed. + this.crs = crs; + return true; + } + + /** + * If the coordinate reference system is a projected CRS, replaces it by another CRS where wraparound axes can + * be identified. The wraparound axes are identifiable in base geographic CRS. If such replacement is applied, + * remember that we may need to transform the result later. + * + * @return whether the replacement has been done. If {@code true}, then {@link #geographicToAOI} is non-null. + */ + private boolean replaceCRS() { if (crs instanceof ProjectedCRS) { final ProjectedCRS p = (ProjectedCRS) crs; - crs = p.getBaseCRS(); - projection = p.getConversionFromBase().getMathTransform(); - shifted = Envelopes.transform(projection.inverse(), areaOfInterest); - lowerCorner = shifted.getLowerCorner(); - upperCorner = shifted.getUpperCorner(); - if (validToAOI == null) { - validToAOI = MathTransforms.identity(projection.getTargetDimensions()); - } + crs = p.getBaseCRS(); // Geographic, so a wraparound axis certainly exists. + geographicToAOI = p.getConversionFromBase().getMathTransform(); + return true; } else { - projection = null; - lowerCorner = areaOfInterest.getLowerCorner(); - upperCorner = areaOfInterest.getUpperCorner(); + return false; } - /* - * We will not read 'areaOfInterest' anymore after we got its two corner points. - * The following loop search for "wraparound" axis. - */ - final CoordinateSystem cs = crs.getCoordinateSystem(); - for (int i=cs.getDimension(); --i >= 0;) { - final double period = range(cs, i); - if (period > 0) { - /* - * Found an axis (typically the longitude axis) with wraparound range meaning. - * We are going to need the domain of validity in the same CRS than the AOI. - * Transform that envelope when first needed. - */ - if (validToAOI != null) { - if (projection != null) { - validToAOI = MathTransforms.concatenate(validToAOI, projection.inverse()); - } - if (!validToAOI.isIdentity()) { - domainOfValidity = Envelopes.transform(validToAOI, domainOfValidity); - } - validToAOI = null; - } - /* - * "Unroll" the range. For example if we have [+160 … -170]° of longitude, we can replace by [160 … 190]°. - * We do not change the 'lower' or 'upper' value now in order to avoid rounding error. Instead we compute - * how many periods we need to add to those values. We adjust the side which results in the value closest - * to zero, in order to reduce rounding error if no more adjustment is done in the next block. - */ - final double lower = lowerCorner.getOrdinate(i); - final double upper = upperCorner.getOrdinate(i); - double lowerCycles = 0; // In number of periods. - double upperCycles = 0; - double delta = upper - lower; - if (MathFunctions.isNegative(delta)) { // Use 'isNegative' for catching [+0 … -0] range. - final double cycles = (delta == 0) ? -1 : Math.floor(delta / period); // Always negative. - delta = cycles * period; - if (Math.abs(lower + delta) < Math.abs(upper - delta)) { - lowerCycles = cycles; // Will subtract periods to 'lower'. - } else { - upperCycles = -cycles; // Will add periods to 'upper'. - } - } - /* - * The range may be before or after the domain of validity. Compute the distance from current - * lower/upper coordinate to the coordinate of validity domain (the sign tells us whether we - * are before or after). The cases can be: - * - * ┌─────────────┬────────────┬────────────────────────────┬───────────────────────────────┐ - * │lowerIsBefore│upperIsAfter│ Meaning │ Action │ - * ├─────────────┼────────────┼────────────────────────────┼───────────────────────────────┤ - * │ false │ false │ AOI is inside valid area │ Nothing to do │ - * │ true │ true │ AOI encompasses valid area │ Nothing to do │ - * │ true │ false │ AOI on left of valid area │ Add positive amount of period │ - * │ false │ true │ AOI on right of valid area │ Add negative amount of period │ - * └─────────────┴────────────┴────────────────────────────┴───────────────────────────────┘ - * - * We try to compute multiples of 'periods' instead than just adding or subtracting 'periods' once in - * order to support images that cover more than one period, for example images over 720° of longitude. - * It may happen for example if an image shows data under the trajectory of a satellite. - */ - final double validStart = domainOfValidity.getMinimum(i); - final double validEnd = domainOfValidity.getMaximum(i); - final double lowerToValidStart = ((validStart - lower) / period) - lowerCycles; // In number of periods. - final double upperToValidEnd = ((validEnd - upper) / period) - upperCycles; - final boolean lowerIsBefore = (lowerToValidStart > 0); - final boolean upperIsAfter = (upperToValidEnd < 0); - if (lowerIsBefore != upperIsAfter) { - final double upperToValidStart = ((validStart - upper) / period) - upperCycles; - final double lowerToValidEnd = ((validEnd - lower) / period) - lowerCycles; - if (lowerIsBefore) { - /* - * Notation: ⎣x⎦=floor(x) and ⎡x⎤=ceil(x). - * - * We need to add an integer amount of 'period' to both sides in order to move the range - * inside the valid area. We need ⎣lowerToValidStart⎦ for reaching the point where: - * - * (validStart - period) < (new lower) ≦ validStart - * - * But we may add more because there will be no intersection without following condition: - * - * (new upper) ≧ validStart - * - * That second condition is met by ⎡upperToValidStart⎤. However adding more may cause the - * range to move the AOI completely on the right side of the domain of validity. We prevent - * that with a third condition: - * - * (new lower) < validEnd - */ - final double cycles = Math.min(Math.floor(lowerToValidEnd), - Math.max(Math.floor(lowerToValidStart), - Math.ceil (upperToValidStart))); - /* - * If after the shift we see that the following condition hold: - * - * (new lower) + period < validEnd - * - * Then we may have a situation like below: - * ┌────────────────────────────────────────────┐ - * │ Domain of validity │ - * └────────────────────────────────────────────┘ - * ┌────────────────────┐ ┌───── - * │ Area of interest │ │ AOI - * └────────────────────┘ └───── - * ↖……………………………………………………………period……………………………………………………………↗︎ - * - * The user may be requesting two extremums of the domain of validity. We can not express - * that with a single envelope. Instead, we will expand the Area Of Interest to encompass - * the full domain of validity. - */ - if (cycles + 1 < lowerToValidEnd) { - upperCycles += Math.ceil(upperToValidEnd); + } + + /** + * Transforms {@link #domainOfValidity} to the same CRS than the Area Of Interest (AOI) or Point Of Interest (POI). + * This method should be invoked only when the caller detected a wraparound axis. This method transforms the domain + * the first time it is invoked, and does nothing on all subsequent calls. + */ + private void transformDomainToAOI() throws TransformException { + if (!isDomainTransformed) { + isDomainTransformed = true; + MathTransform domainToGeographic = domainToAOI; + if (domainToGeographic == null) { + domainToGeographic = geographicToAOI; + } else if (geographicToAOI != null) { + domainToGeographic = MathTransforms.concatenate(domainToGeographic, geographicToAOI.inverse()); + } + if (domainToGeographic != null && !domainToGeographic.isIdentity()) { + domainOfValidity = Envelopes.transform(domainToGeographic, domainOfValidity); + } + } + } + + /** + * Returns the final transform to apply on the AOI or POI before to return it to the user. + */ + private MathTransform toFinal() throws TransformException { + MathTransform mt = domainToAny; + if (isResultTransformed && geographicToAOI != null) { + mt = MathTransforms.concatenate(geographicToAOI, mt); + } + return mt; + } + + /** + * Computes an envelope with coordinates equivalent to the given {@code areaOfInterest}, but + * potentially shifted for intersecting the domain of validity specified at construction time. + * The dimensions that may be shifted are the ones having an axis with wraparound meaning. + * In order to perform this operation, the envelope may be temporarily converted to a geographic CRS + * and converted back to its original CRS. + * + *The coordinate reference system should be specified in the {@code areaOfInterest}, + * or (as a fallback) in the {@code domainOfValidity} specified at construction time.
+ * + *This method does not intersect the area of interest with the domain of validity. + * It is up to the caller to compute that intersection after this method call, if desired.
+ * + * @param areaOfInterest the envelope to potentially shift toward domain of validity. + * If a shift is needed, then given envelope will be replaced by a new envelope; + * the given envelope will not be modified. + * @return envelope potentially expanded or shifted toward the domain of validity. + * @throws TransformException if a coordinate conversion failed. + * + * @see GeneralEnvelope#simplify() + */ + public GeneralEnvelope shift(Envelope areaOfInterest) throws TransformException { + if (setIfNonNull(areaOfInterest.getCoordinateReferenceSystem())) { + /* + * If the coordinate reference system is a projected CRS, it will not have any wraparound axis. + * We need to perform the verification in its base geographic CRS instead, and remember that we + * may need to transform the result later. + */ + final DirectPosition lowerCorner; + final DirectPosition upperCorner; + GeneralEnvelope shifted; // To be initialized to a copy of 'areaOfInterest' when first needed. + if (replaceCRS()) { + shifted = Envelopes.transform(geographicToAOI.inverse(), areaOfInterest); + lowerCorner = shifted.getLowerCorner(); + upperCorner = shifted.getUpperCorner(); + } else { + shifted = null; + lowerCorner = areaOfInterest.getLowerCorner(); + upperCorner = areaOfInterest.getUpperCorner(); + } + /* + * We will not read 'areaOfInterest' anymore after we got its two corner points (except for creating + * a copy if `shifted` is still null). The following loop searches for "wraparound" axes. + */ + final CoordinateSystem cs = crs.getCoordinateSystem(); + for (int i=cs.getDimension(); --i >= 0;) { + final double period = range(cs, i); + if (period > 0) { + /* + * Found an axis (typically the longitude axis) with wraparound range meaning. + * We are going to need the domain of validity in the same CRS than the AOI. + * Transform that envelope when first needed. + */ + transformDomainToAOI(); + /* + * "Unroll" the range. For example if we have [+160 … -170]° of longitude, we can replace by [160 … 190]°. + * We do not change the 'lower' or 'upper' value now in order to avoid rounding error. Instead we compute + * how many periods we need to add to those values. We adjust the side which results in the value closest + * to zero, in order to reduce rounding error if no more adjustment is done in the next block. + */ + final double lower = lowerCorner.getOrdinate(i); + final double upper = upperCorner.getOrdinate(i); + double lowerCycles = 0; // In number of periods. + double upperCycles = 0; + double delta = upper - lower; + if (MathFunctions.isNegative(delta)) { // Use 'isNegative' for catching [+0 … -0] range. + final double cycles = (delta == 0) ? -1 : Math.floor(delta / period); // Always negative. + delta = cycles * period; + if (Math.abs(lower + delta) < Math.abs(upper - delta)) { + lowerCycles = cycles; // Will subtract periods to 'lower'. } else { - upperCycles += cycles; + upperCycles = -cycles; // Will add periods to 'upper'. } - lowerCycles += cycles; - } else { - /* - * Same reasoning than above with sign reverted and lower/upper variables interchanged. - * In this block, 'upperToValidEnd' and 'lowerToValidEnd' are negative, contrarily to - * above block where they were positive. - */ - final double cycles = Math.max(Math.ceil (upperToValidStart), - Math.min(Math.ceil (upperToValidEnd), - Math.floor(lowerToValidEnd))); - if (cycles - 1 > upperToValidStart) { - lowerCycles += Math.floor(lowerToValidStart); - } else { + } + /* + * The range may be before or after the domain of validity. Compute the distance from current + * lower/upper coordinate to the coordinate of validity domain (the sign tells us whether we + * are before or after). The cases can be: + * + * ┌─────────────┬────────────┬────────────────────────────┬───────────────────────────────┐ + * │lowerIsBefore│upperIsAfter│ Meaning │ Action │ + * ├─────────────┼────────────┼────────────────────────────┼───────────────────────────────┤ + * │ false │ false │ AOI is inside valid area │ Nothing to do │ + * │ true │ true │ AOI encompasses valid area │ Nothing to do │ + * │ true │ false │ AOI on left of valid area │ Add positive amount of period │ + * │ false │ true │ AOI on right of valid area │ Add negative amount of period │ + * └─────────────┴────────────┴────────────────────────────┴───────────────────────────────┘ + * + * We try to compute multiples of 'periods' instead than just adding or subtracting 'periods' once in + * order to support images that cover more than one period, for example images over 720° of longitude. + * It may happen for example if an image shows data under the trajectory of a satellite. + */ + final double validStart = domainOfValidity.getMinimum(i); + final double validEnd = domainOfValidity.getMaximum(i); + final double lowerToValidStart = ((validStart - lower) / period) - lowerCycles; // In number of periods. + final double upperToValidEnd = ((validEnd - upper) / period) - upperCycles; + final boolean lowerIsBefore = (lowerToValidStart > 0); + final boolean upperIsAfter = (upperToValidEnd < 0); + if (lowerIsBefore != upperIsAfter) { + final double upperToValidStart = ((validStart - upper) / period) - upperCycles; + final double lowerToValidEnd = ((validEnd - lower) / period) - lowerCycles; + if (lowerIsBefore) { + /* + * Notation: ⎣x⎦=floor(x) and ⎡x⎤=ceil(x). + * + * We need to add an integer amount of 'period' to both sides in order to move the range + * inside the valid area. We need ⎣lowerToValidStart⎦ for reaching the point where: + * + * (validStart - period) < (new lower) ≦ validStart + * + * But we may add more because there will be no intersection without following condition: + * + * (new upper) ≧ validStart + * + * That second condition is met by ⎡upperToValidStart⎤. However adding more may cause the + * range to move the AOI completely on the right side of the domain of validity. We prevent + * that with a third condition: + * + * (new lower) < validEnd + */ + final double cycles = Math.min(Math.floor(lowerToValidEnd), + Math.max(Math.floor(lowerToValidStart), + Math.ceil (upperToValidStart))); + /* + * If after the shift we see that the following condition hold: + * + * (new lower) + period < validEnd + * + * Then we may have a situation like below: + * ┌────────────────────────────────────────────┐ + * │ Domain of validity │ + * └────────────────────────────────────────────┘ + * ┌────────────────────┐ ┌───── + * │ Area of interest │ │ AOI + * └────────────────────┘ └───── + * ↖……………………………………………………………period……………………………………………………………↗︎ + * + * The user may be requesting two extremums of the domain of validity. We can not express + * that with a single envelope. Instead, we will expand the Area Of Interest to encompass + * the full domain of validity. + */ + if (cycles + 1 < lowerToValidEnd) { + upperCycles += Math.ceil(upperToValidEnd); + } else { + upperCycles += cycles; + } lowerCycles += cycles; + } else { + /* + * Same reasoning than above with sign reverted and lower/upper variables interchanged. + * In this block, 'upperToValidEnd' and 'lowerToValidEnd' are negative, contrarily to + * above block where they were positive. + */ + final double cycles = Math.max(Math.ceil (upperToValidStart), + Math.min(Math.ceil (upperToValidEnd), + Math.floor(lowerToValidEnd))); + if (cycles - 1 > upperToValidStart) { + lowerCycles += Math.floor(lowerToValidStart); + } else { + lowerCycles += cycles; + } + upperCycles += cycles; } - upperCycles += cycles; } - } - /* - * If there is change to apply, copy the envelope when first needed and set the fields. - * If we never enter in this block, then 'areaOfInterest' will stay the envelope given - * at construction time. - */ - if (lowerCycles != 0 || upperCycles != 0) { - if (shifted == null) { - shifted = new GeneralEnvelope(areaOfInterest); + /* + * If there is change to apply, copy the envelope when first needed and set the fields. + * If we never enter in this block, then 'areaOfInterest' will stay the envelope given + * at construction time. + */ + if (lowerCycles != 0 || upperCycles != 0) { + isResultTransformed = true; + if (shifted == null) { + shifted = new GeneralEnvelope(areaOfInterest); + } + areaOfInterest = shifted; // 'shifted' may have been set before the loop. + shifted.setRange(i, lower + lowerCycles * period, // TODO: use Math.fma in JDK9. + upper + upperCycles * period); } - areaOfInterest = shifted; // 'shifted' may have been set before the loop. - geographicToAOI = projection; - shifted.setRange(i, lower + lowerCycles * period, // TODO: use Math.fma in JDK9. - upper + upperCycles * period); } } } - return this; + return Envelopes.transform(toFinal(), areaOfInterest); } /** - * Returns the (potentially shifted and/or expanded) area of interest converted by the given transform. + * Computes a position with coordinates equivalent to the given {@code pointOfInterest}, but + * potentially shifted to interior of the domain of validity specified at construction time. + * The dimensions that may be shifted are the ones having an axis with wraparound meaning. + * In order to perform this operation, the position may be temporarily converted to a geographic CRS + * and converted back to its original CRS. + * + *The coordinate reference system should be specified in the {@code pointOfInterest}, + * or (as a fallback) in the {@code domainOfValidity} specified at construction time.
* - * @param mt a transform from the CRS of the {@code areaOfInterest} given to the constructor. - * @return the area of interest transformed by the given {@code MathTransform}. - * @throws TransformException if the transformation failed. + * @param pointOfInterest the position to potentially shift to domain of validity interior. + * If a shift is needed, then the given position will be replaced by a new position; + * the given position will not be modified. + * @return position potentially shifted to the domain of validity interior. + * @throws TransformException if a coordinate conversion failed. */ - public GeneralEnvelope result(MathTransform mt) throws TransformException { - if (geographicToAOI != null) { - mt = MathTransforms.concatenate(geographicToAOI, mt); + public DirectPosition shift(DirectPosition pointOfInterest) throws TransformException { + if (setIfNonNull(pointOfInterest.getCoordinateReferenceSystem())) { + DirectPosition shifted; + if (replaceCRS()) { + shifted = geographicToAOI.inverse().transform(pointOfInterest, null); + } else { + shifted = pointOfInterest; // To be replaced by a copy of 'pointOfInterest' when first needed. + } + final CoordinateSystem cs = crs.getCoordinateSystem(); + for (int i=cs.getDimension(); --i >= 0;) { + final double period = range(cs, i); + if (period > 0) { + transformDomainToAOI(); + final double x = shifted.getOrdinate(i); + double delta = domainOfValidity.getMinimum(i) - x; + if (delta > 0) { // Test for point before domain of validity. + delta = Math.ceil(delta / period); + } else { + delta = domainOfValidity.getMaximum(i) - x; + if (delta < 0) { // Test for point after domain of validity. + delta = Math.floor(delta / period); + } else { + continue; + } + } + if (delta != 0) { + isResultTransformed = true; + if (shifted == pointOfInterest) { + shifted = new GeneralDirectPosition(pointOfInterest); + } + pointOfInterest = shifted; // 'shifted' may have been set before the loop. + shifted.setOrdinate(i, x + delta * period); // TODO: use Math.fma in JDK9. + } + } + } } - return Envelopes.transform(mt, areaOfInterest); + return toFinal().transform(pointOfInterest, null); } } diff --git a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java index ba454f5..813ccdb 100644 --- a/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java +++ b/core/sis-referencing/src/test/java/org/apache/sis/internal/referencing/WraparoundAdjustmentTest.java @@ -58,13 +58,12 @@ public final strictfp class WraparoundAdjustmentTest extends TestCase { private static Envelope adjustWraparoundAxes(Envelope areaOfInterest, Envelope domainOfValidity, MathTransform validToAOI) throws TransformException { - WraparoundAdjustment adj = new WraparoundAdjustment(areaOfInterest); - adj.shiftInto(domainOfValidity, validToAOI); - return adj.result(MathTransforms.identity(2)); + WraparoundAdjustment adj = new WraparoundAdjustment(domainOfValidity, validToAOI, MathTransforms.identity(2)); + return adj.shift(areaOfInterest); } /** - * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)} + * Tests {@link WraparoundAdjustment#shift(Envelope)} * with an envelope crossing the anti-meridian. * * @throws TransformException should never happen since this test does not transform coordinates. @@ -88,7 +87,7 @@ public final strictfp class WraparoundAdjustmentTest extends TestCase { } /** - * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)} + * Tests {@link WraparoundAdjustment#shift(Envelope)} * with an envelope which is outside the grid valid area. * * @throws TransformException should never happen since this test does not transform coordinates. @@ -108,7 +107,7 @@ public final strictfp class WraparoundAdjustmentTest extends TestCase { } /** - * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)} + * Tests {@link WraparoundAdjustment#shift(Envelope)} * with an envelope shifted by 360° before or after the grid valid area. * * @throws TransformException should never happen since this test does not transform coordinates. @@ -144,7 +143,7 @@ public final strictfp class WraparoundAdjustmentTest extends TestCase { } /** - * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)} + * Tests {@link WraparoundAdjustment#shift(Envelope)} * with an envelope that cause the method to expand the area of interest. Illustration: * * {@preformat text @@ -178,7 +177,7 @@ public final strictfp class WraparoundAdjustmentTest extends TestCase { } /** - * Tests {@link WraparoundAdjustment#shiftInto(Envelope, MathTransform)} with a projected envelope. + * Tests {@link WraparoundAdjustment#shift(Envelope)} with a projected envelope. * * @throws TransformException if an error occurred while projecting a coordinate. */