| /* |
| * 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.Set; |
| import java.util.ConcurrentModificationException; |
| import org.opengis.geometry.Envelope; |
| import org.opengis.geometry.DirectPosition; |
| import org.opengis.geometry.MismatchedDimensionException; |
| import org.opengis.referencing.cs.CoordinateSystem; |
| import org.opengis.referencing.cs.CoordinateSystemAxis; |
| import org.opengis.referencing.crs.CoordinateReferenceSystem; |
| import org.opengis.referencing.operation.CoordinateOperation; |
| import org.opengis.referencing.operation.Matrix; |
| import org.opengis.referencing.operation.MathTransform; |
| import org.opengis.referencing.operation.TransformException; |
| import org.opengis.referencing.operation.OperationNotFoundException; |
| import org.opengis.referencing.operation.NoninvertibleTransformException; |
| import org.opengis.util.FactoryException; |
| import org.opengis.metadata.extent.GeographicBoundingBox; |
| import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; |
| import org.apache.sis.referencing.CRS; |
| import org.apache.sis.referencing.operation.AbstractCoordinateOperation; |
| import org.apache.sis.referencing.operation.transform.AbstractMathTransform; |
| import org.apache.sis.internal.metadata.ReferencingServices; |
| import org.apache.sis.internal.referencing.CoordinateOperations; |
| import org.apache.sis.internal.referencing.DirectPositionView; |
| import org.apache.sis.internal.system.Loggers; |
| import org.apache.sis.util.logging.Logging; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.Utilities; |
| import org.apache.sis.util.Static; |
| import org.apache.sis.math.MathFunctions; |
| |
| import static org.apache.sis.util.StringBuilders.trimFractionalPart; |
| |
| |
| /** |
| * Transforms envelopes to new Coordinate Reference Systems, and miscellaneous utilities. |
| * |
| * <div class="section">Envelope transformations</div> |
| * All {@code transform(…)} methods in this class take in account the curvature of the transformed shape. |
| * For example the shape of a geographic envelope (figure below on the left side) is not rectangular in a |
| * conic projection (figure below on the right side). In order to get the envelope represented by the red |
| * rectangle, projecting the four corners of the geographic envelope is not sufficient since we would miss |
| * the southerner part. |
| * |
| * <table class="sis"> |
| * <caption>Example of curvature induced by a map projection</caption> |
| * <tr> |
| * <th>Envelope before map projection</th> |
| * <th>Shape of the projected envelope</th> |
| * </tr><tr> |
| * <td><img src="doc-files/GeographicArea.png" alt="Envelope in a geographic CRS"></td> |
| * <td><img src="doc-files/ConicArea.png" alt="Shape of the envelope transformed in a conic projection"></td> |
| * </tr> |
| * </table> |
| * |
| * Apache SIS tries to detect the curvature by transforming intermediate points in addition to the corners. |
| * While optional, it is strongly recommended that all {@code MathTransform} implementations involved in the |
| * operation (directly or indirectly) support {@linkplain MathTransform#derivative(DirectPosition) derivative}, |
| * for more accurate calculation of curve extremum. This is the case of most Apache SIS implementations. |
| * |
| * <p>The {@code transform(…)} methods in this class expect an arbitrary {@link Envelope} with <strong>one</strong> |
| * of the following arguments: {@link MathTransform}, {@link CoordinateOperation} or {@link CoordinateReferenceSystem}. |
| * The recommended method is the one expecting a {@code CoordinateOperation} object, |
| * since it contains sufficient information for handling the cases of envelopes that encompass a pole. |
| * The method expecting a {@code CoordinateReferenceSystem} object is merely a convenience method that |
| * infers the coordinate operation itself, but at the cost of performance if the same operation needs |
| * to be applied on many envelopes.</p> |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @author Johann Sorel (Geomatys) |
| * @version 1.0 |
| * |
| * @see org.apache.sis.metadata.iso.extent.Extents |
| * @see CRS |
| * |
| * @since 0.3 |
| * @module |
| */ |
| public final class Envelopes extends Static { |
| /** |
| * Do not allow instantiation of this class. |
| */ |
| private Envelopes() { |
| } |
| |
| /** |
| * Puts together a list of envelopes, each of them using an independent coordinate reference system. |
| * The dimension of the returned envelope is the sum of the dimension of all components. |
| * If all components have a coordinate reference system, then the returned envelope will |
| * have a compound coordinate reference system. |
| * |
| * @param components the envelopes to aggregate in a single envelope, in the given order. |
| * @return the aggregation of all given envelopes. |
| * @throws FactoryException if the geodetic factory failed to create the compound CRS. |
| * |
| * @see org.apache.sis.referencing.CRS#compound(CoordinateReferenceSystem...) |
| * @see org.apache.sis.referencing.operation.transform.MathTransforms#compound(MathTransform...) |
| * |
| * @since 1.0 |
| */ |
| public static Envelope compound(final Envelope... components) throws FactoryException { |
| ArgumentChecks.ensureNonNull("components", components); |
| int sum = 0; |
| for (int i=0; i<components.length; i++) { |
| final Envelope env = components[i]; |
| ArgumentChecks.ensureNonNullElement("components", i, env); |
| sum += env.getDimension(); |
| } |
| final GeneralEnvelope compound = new GeneralEnvelope(sum); |
| CoordinateReferenceSystem[] crsComponents = null; |
| int firstAffectedCoordinate = 0; |
| for (int i=0; i<components.length; i++) { |
| final Envelope env = components[i]; |
| final int dim = env.getDimension(); |
| compound.subEnvelope(firstAffectedCoordinate, firstAffectedCoordinate += dim).setEnvelope(env); |
| if (i == 0) { |
| final CoordinateReferenceSystem crs = env.getCoordinateReferenceSystem(); |
| if (crs != null) { |
| crsComponents = new CoordinateReferenceSystem[components.length]; |
| crsComponents[0] = crs; |
| } |
| } else if (crsComponents != null) { |
| if ((crsComponents[i] = env.getCoordinateReferenceSystem()) == null) { |
| crsComponents = null; // Not all CRS are non-null. |
| } |
| } |
| } |
| if (firstAffectedCoordinate != sum) { |
| // Should never happen unless the number of dimensions of an envelope changed during iteration. |
| throw new ConcurrentModificationException(); |
| } |
| if (crsComponents != null) { |
| compound.setCoordinateReferenceSystem(CRS.compound(crsComponents)); |
| } |
| return compound; |
| } |
| |
| /** |
| * Computes the union of all given envelopes, transforming them to a common CRS if necessary. |
| * If all envelopes use the same CRS ({@link ComparisonMode#IGNORE_METADATA ignoring metadata}) |
| * or if the CRS of all envelopes is {@code null}, then the {@linkplain GeneralEnvelope#add(Envelope) |
| * union is computed} without transforming any envelope. Otherwise all envelopes are transformed to a |
| * {@linkplain CRS#suggestCommonTarget common CRS} before union is computed. |
| * The CRS of the returned envelope may different than the CRS of all given envelopes. |
| * |
| * @param envelopes the envelopes for which to compute union. Null elements are ignored. |
| * @return union of given envelopes, or {@code null} if the given array does not contain non-null elements. |
| * @throws TransformException if this method can not determine a common CRS, or if a transformation failed. |
| * |
| * @see GeneralEnvelope#add(Envelope) |
| * |
| * @since 1.0 |
| */ |
| public static GeneralEnvelope union(final Envelope... envelopes) throws TransformException { |
| return EnvelopeReducer.UNION.reduce(envelopes); |
| } |
| |
| /** |
| * Finds a mathematical operation from the CRS of the given source envelope to the CRS of the given target envelope. |
| * For non-null georeferenced envelopes, this method is equivalent to the following code with {@code areaOfInterest} |
| * computed as the union of the two envelopes: |
| * |
| * <blockquote><code>return {@linkplain CRS#findOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, |
| * GeographicBoundingBox)} CRS.findOperation(source.getCoordinateReferenceSystem(), target.getCoordinateReferenceSystem(), |
| * <var>areaOfInterest</var>)</code></blockquote> |
| * |
| * If at least one envelope is null or has no CRS, then this method returns {@code null}. |
| * |
| * @param source the source envelope, or {@code null}. |
| * @param target the target envelope, or {@code null}. |
| * @return the mathematical operation from {@code source} CRS to {@code target} CRS, |
| * or {@code null} if at least one argument is null or has no CRS. |
| * @throws OperationNotFoundException if no operation was found between the given pair of CRS. |
| * @throws FactoryException if the operation can not be created for another reason. |
| * |
| * @see CRS#findOperation(CoordinateReferenceSystem, CoordinateReferenceSystem, GeographicBoundingBox) |
| * |
| * @since 1.0 |
| */ |
| public static CoordinateOperation findOperation(final Envelope source, final Envelope target) throws FactoryException { |
| if (source != null && target != null) { |
| final CoordinateReferenceSystem sourceCRS, targetCRS; |
| if ((sourceCRS = source.getCoordinateReferenceSystem()) != null && |
| (targetCRS = target.getCoordinateReferenceSystem()) != null) |
| { |
| /* |
| * Computing the area of interest (AOI) unconditionally would be harmless, but it is useless if the CRS |
| * are the same since the result will be identity anyway. Conversely we could skip AOI computation more |
| * often for example with the following condition instead of !=: |
| * |
| * !Utilities.deepEquals(sourceCRS, targetCRS, ComparisonMode.ALLOW_VARIANT) |
| * |
| * but it would not be very advantageous if testing the condition is almost as long as computing the AOI. |
| * For now we keep != as a very cheap test which will work quite often. |
| */ |
| DefaultGeographicBoundingBox areaOfInterest = null; |
| if (sourceCRS != targetCRS) try { |
| final DefaultGeographicBoundingBox targetAOI; |
| final ReferencingServices converter = ReferencingServices.getInstance(); |
| areaOfInterest = converter.setBounds(source, null, "findOperation"); // Should be first. |
| targetAOI = converter.setBounds(target, null, "findOperation"); |
| if (areaOfInterest == null) { |
| areaOfInterest = targetAOI; |
| } else if (targetAOI != null) { |
| areaOfInterest.add(targetAOI); |
| } |
| } catch (TransformException e) { |
| /* |
| * Note: we may succeed to transform 'source' and fail to transform 'target' to geographic bounding box, |
| * but the opposite is unlikely because 'source' should not have less dimensions than 'target'. |
| */ |
| Logging.recoverableException(Logging.getLogger(Loggers.GEOMETRY), Envelopes.class, "findOperation", e); |
| } |
| return CRS.findOperation(sourceCRS, targetCRS, areaOfInterest); |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Computes the intersection of all given envelopes, transforming them to a common CRS if necessary. |
| * If all envelopes use the same CRS ({@link ComparisonMode#IGNORE_METADATA ignoring metadata}) |
| * or if the CRS of all envelopes is {@code null}, then the {@linkplain GeneralEnvelope#intersect(Envelope) |
| * intersection is computed} without transforming any envelope. Otherwise all envelopes are transformed to a |
| * {@linkplain CRS#suggestCommonTarget common CRS} before intersection is computed. |
| * The CRS of the returned envelope may different than the CRS of all given envelopes. |
| * |
| * @param envelopes the envelopes for which to compute intersection. Null elements are ignored. |
| * @return intersection of given envelopes, or {@code null} if the given array does not contain non-null elements. |
| * @throws TransformException if this method can not determine a common CRS, or if a transformation failed. |
| * |
| * @see GeneralEnvelope#intersect(Envelope) |
| * |
| * @since 1.0 |
| */ |
| public static GeneralEnvelope intersect(final Envelope... envelopes) throws TransformException { |
| return EnvelopeReducer.INTERSECT.reduce(envelopes); |
| } |
| |
| /** |
| * Invoked when a recoverable exception occurred. Those exceptions must be minor enough |
| * that they can be silently ignored in most cases. |
| */ |
| static void recoverableException(final Class<? extends Static> caller, final TransformException exception) { |
| Logging.recoverableException(Logging.getLogger(Loggers.GEOMETRY), caller, "transform", exception); |
| } |
| |
| /** |
| * A buckle method for calculating derivative and coordinate transformation in a single step, |
| * if the given {@code derivative} argument is {@code true}. |
| * |
| * @param transform the transform to use. |
| * @param srcPts the array containing the source coordinate at offset 0. |
| * @param dstPts the array into which the transformed coordinate is returned. |
| * @param dstOff the offset to the location of the transformed point that is stored in the destination array. |
| * @param derivate {@code true} for computing the derivative, or {@code false} if not needed. |
| * @return the matrix of the transform derivative at the given source position, |
| * or {@code null} if the {@code derivate} argument is {@code false}. |
| * @throws TransformException if the point can not be transformed |
| * or if a problem occurred while calculating the derivative. |
| */ |
| @SuppressWarnings("null") |
| static Matrix derivativeAndTransform(final MathTransform transform, final double[] srcPts, |
| final double[] dstPts, final int dstOff, final boolean derivate) throws TransformException |
| { |
| if (transform instanceof AbstractMathTransform) { |
| return ((AbstractMathTransform) transform).transform(srcPts, 0, dstPts, dstOff, derivate); |
| } |
| // Derivative must be calculated before to transform the coordinate. |
| final Matrix derivative = derivate ? transform.derivative( |
| new DirectPositionView.Double(srcPts, 0, transform.getSourceDimensions())) : null; |
| transform.transform(srcPts, 0, dstPts, dstOff, 1); |
| return derivative; |
| } |
| |
| /** |
| * Transforms the given envelope to the specified CRS. If any argument is null, or if the |
| * {@linkplain GeneralEnvelope#getCoordinateReferenceSystem() envelope CRS} is null or the |
| * same instance than the given target CRS, then the given envelope is returned unchanged. |
| * Otherwise a new transformed envelope is returned. |
| * |
| * <div class="section">Performance tip</div> |
| * If there is many envelopes to transform with the same source and target CRS, then it is more efficient |
| * to get the {@link CoordinateOperation} or {@link MathTransform} instance once and invoke one of the |
| * others {@code transform(…)} methods. |
| * |
| * @param envelope the envelope to transform (may be {@code null}). |
| * @param targetCRS the target CRS (may be {@code null}). |
| * @return a new transformed envelope, or directly {@code envelope} if no change was required. |
| * @throws TransformException if a transformation was required and failed. |
| * |
| * @since 0.5 |
| */ |
| public static Envelope transform(Envelope envelope, final CoordinateReferenceSystem targetCRS) |
| throws TransformException |
| { |
| if (envelope != null && targetCRS != null) { |
| final CoordinateReferenceSystem sourceCRS = envelope.getCoordinateReferenceSystem(); |
| if (sourceCRS != targetCRS) { |
| if (sourceCRS == null) { |
| // Slight optimization: just copy the given Envelope. |
| envelope = new GeneralEnvelope(envelope); |
| ((GeneralEnvelope) envelope).setCoordinateReferenceSystem(targetCRS); |
| } else { |
| // TODO: create an CoordinateOperationContext with the envelope as geographic area. |
| // May require that we optimize the search for CoordinateOperation with non-null context first. |
| // See https://issues.apache.org/jira/browse/SIS-442 |
| final CoordinateOperation operation; |
| try { |
| operation = CoordinateOperations.factory().createOperation(sourceCRS, targetCRS); |
| } catch (FactoryException exception) { |
| throw new TransformException(Errors.format(Errors.Keys.CanNotTransformEnvelope), exception); |
| } |
| envelope = transform(operation, envelope); |
| } |
| assert Utilities.deepEquals(targetCRS, envelope.getCoordinateReferenceSystem(), ComparisonMode.DEBUG); |
| } |
| } |
| return envelope; |
| } |
| |
| /** |
| * Transforms an envelope using the given math transform. |
| * The transformation is only approximated: the returned envelope may be bigger than necessary, |
| * or smaller than required if the bounding box contains a pole. |
| * The coordinate reference system of the returned envelope will be null. |
| * |
| * <div class="section">Limitation</div> |
| * This method can not handle the case where the envelope contains the North or South pole, |
| * or when it crosses the ±180° longitude, because {@link MathTransform} does not carry sufficient information. |
| * For a more robust envelope transformation, use {@link #transform(CoordinateOperation, Envelope)} instead. |
| * |
| * @param transform the transform to use. |
| * @param envelope envelope to transform, or {@code null}. This envelope will not be modified. |
| * @return the transformed envelope, or {@code null} if {@code envelope} was null. |
| * @throws TransformException if a transform failed. |
| * |
| * @see #transform(CoordinateOperation, Envelope) |
| * |
| * @since 0.5 |
| */ |
| public static GeneralEnvelope transform(final MathTransform transform, final Envelope envelope) |
| throws TransformException |
| { |
| ArgumentChecks.ensureNonNull("transform", transform); |
| return (envelope != null) ? transform(transform, envelope, null) : null; |
| } |
| |
| /** |
| * Implementation of {@link #transform(MathTransform, Envelope)} with the opportunity to |
| * save the projected center coordinate. |
| * |
| * @param targetPt after this method call, the center of the source envelope projected to the target CRS. |
| * The length of this array must be the number of target dimensions. |
| * May be {@code null} if this information is not needed. |
| */ |
| @SuppressWarnings("null") |
| private static GeneralEnvelope transform(final MathTransform transform, |
| final Envelope envelope, |
| final double[] targetPt) |
| throws TransformException |
| { |
| if (transform.isIdentity()) { |
| /* |
| * Slight optimization: Just copy the envelope. Note that we need to set the CRS |
| * to null because we don't know what the target CRS was supposed to be. Even if |
| * an identity transform often imply that the target CRS is the same one than the |
| * source CRS, it is not always the case. The metadata may be differents, or the |
| * transform may be a datum shift without Bursa-Wolf parameters, etc. |
| */ |
| final GeneralEnvelope transformed = new GeneralEnvelope(envelope); |
| transformed.setCoordinateReferenceSystem(null); |
| if (targetPt != null) { |
| for (int i=envelope.getDimension(); --i>=0;) { |
| targetPt[i] = transformed.getMedian(i); |
| } |
| } |
| return transformed; |
| } |
| /* |
| * Checks argument validity: envelope and math transform dimensions must be consistent. |
| */ |
| final int sourceDim = transform.getSourceDimensions(); |
| final int targetDim = transform.getTargetDimensions(); |
| if (envelope.getDimension() != sourceDim) { |
| throw new MismatchedDimensionException(Errors.format(Errors.Keys.MismatchedDimension_2, |
| sourceDim, envelope.getDimension())); |
| } |
| /* |
| * Allocates all needed objects. The power of 3 below is because the following `while` loop |
| * uses a `pointIndex` to be interpreted as a number in base 3 (see the comment inside the loop). |
| * The coordinate tuple to transform must be initialized to the minimal coordinate values. |
| * This coordinate will be updated in the `switch` statement inside the `while` loop. |
| */ |
| if (sourceDim >= 20) { // Maximal value supported by Formulas.pow3(int) is 19. |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.ExcessiveNumberOfDimensions_1)); |
| } |
| int pointIndex = 0; |
| boolean isDerivativeSupported = true; |
| GeneralEnvelope transformed = null; |
| final Matrix[] derivatives = new Matrix[Math.toIntExact(MathFunctions.pow(3, sourceDim))]; |
| final double[] coordinates = new double[derivatives.length * targetDim]; |
| final double[] sourcePt = new double[sourceDim]; |
| for (int i=sourceDim; --i>=0;) { |
| sourcePt[i] = envelope.getMinimum(i); |
| } |
| // A window over a single coordinate in the `coordinates` array. |
| final DirectPositionView ordinatesView = new DirectPositionView.Double(coordinates, 0, targetDim); |
| /* |
| * Iterates over every minimal, maximal and median coordinate values (3 points) along each |
| * dimension. The total number of iterations is 3 ^ (number of source dimensions). |
| */ |
| transformPoint: while (true) { |
| /* |
| * Compute the derivative (optional operation). If this operation fails, we will |
| * set a flag to 'false' so we don't try again for all remaining points. We try |
| * to compute the derivative and the transformed point in a single operation if |
| * we can. If we can not, we will compute those two information separately. |
| * |
| * Note that the very last point to be projected must be the envelope center. |
| * There is usually no need to calculate the derivative for that last point, |
| * but we let it does anyway for safety. |
| */ |
| final int offset = pointIndex * targetDim; |
| try { |
| derivatives[pointIndex] = derivativeAndTransform(transform, |
| sourcePt, coordinates, offset, isDerivativeSupported); |
| } catch (TransformException e) { |
| if (!isDerivativeSupported) { |
| throw e; // Derivative were already disabled, so something went wrong. |
| } |
| isDerivativeSupported = false; |
| transform.transform(sourcePt, 0, coordinates, offset, 1); |
| recoverableException(Envelopes.class, e); // Log only if the above call was successful. |
| } |
| /* |
| * The transformed point has been saved for future reuse after the enclosing |
| * 'while' loop. Now add the transformed point to the destination envelope. |
| */ |
| if (transformed == null) { |
| transformed = new GeneralEnvelope(targetDim); |
| for (int i=0; i<targetDim; i++) { |
| final double value = coordinates[offset + i]; |
| transformed.setRange(i, value, value); |
| } |
| } else { |
| ordinatesView.offset = offset; |
| transformed.add(ordinatesView); |
| } |
| /* |
| * Get the next point coordinate. The 'coordinateIndex' variable is an index in base 3 |
| * having a number of digits equals to the number of source dimensions. For example a |
| * 4-D space have indexes ranging from "0000" to "2222" (numbers in base 3). The digits |
| * are then mapped to minimal (0), maximal (1) or central (2) coordinates. The outer loop |
| * stops when the counter roll back to "0000". Note that 'targetPt' must keep the value |
| * of the last projected point, which must be the envelope center identified by "2222" |
| * in the 4-D case. |
| */ |
| int indexBase3 = ++pointIndex; |
| for (int dim=sourceDim; --dim>=0; indexBase3 /= 3) { |
| switch (indexBase3 % 3) { |
| case 0: sourcePt[dim] = envelope.getMinimum(dim); break; // Continue the loop. |
| case 1: sourcePt[dim] = envelope.getMaximum(dim); continue transformPoint; |
| case 2: sourcePt[dim] = envelope.getMedian (dim); continue transformPoint; |
| default: throw new AssertionError(indexBase3); // Should never happen |
| } |
| } |
| break; |
| } |
| assert pointIndex == derivatives.length : pointIndex; |
| /* |
| * At this point we finished to build an envelope from all sampled positions. Now iterate |
| * over all points. For each point, iterate over all line segments from that point to a |
| * neighbor median point. Use the derivate information for approximating the transform |
| * behavior in that area by a cubic curve. We can then find analytically the curve extremum. |
| * |
| * The same technic is applied in transform(MathTransform, Rectangle2D), except that in |
| * the Rectangle2D case the calculation was bundled right inside the main loop in order |
| * to avoid the need for storage. |
| */ |
| DirectPosition temporary = null; |
| final DirectPositionView sourceView = new DirectPositionView.Double(sourcePt, 0, sourceDim); |
| final CurveExtremum extremum = new CurveExtremum(); |
| for (pointIndex=0; pointIndex < derivatives.length; pointIndex++) { |
| final Matrix D1 = derivatives[pointIndex]; |
| if (D1 != null) { |
| int indexBase3 = pointIndex, power3 = 1; |
| for (int i=sourceDim; --i>=0; indexBase3 /= 3, power3 *= 3) { |
| final int digitBase3 = indexBase3 % 3; |
| if (digitBase3 != 2) { // Process only if we are not already located on the median along the dimension i. |
| final int medianIndex = pointIndex + power3 * (2 - digitBase3); |
| final Matrix D2 = derivatives[medianIndex]; |
| if (D2 != null) { |
| final double xmin = envelope.getMinimum(i); |
| final double xmax = envelope.getMaximum(i); |
| final double x2 = envelope.getMedian (i); |
| final double x1 = (digitBase3 == 0) ? xmin : xmax; |
| final int offset1 = targetDim * pointIndex; |
| final int offset2 = targetDim * medianIndex; |
| for (int j=0; j<targetDim; j++) { |
| extremum.resolve(x1, coordinates[offset1 + j], D1.getElement(j,i), |
| x2, coordinates[offset2 + j], D2.getElement(j,i)); |
| boolean isP2 = false; |
| do { // Executed exactly twice, one for each extremum point. |
| final double x = isP2 ? extremum.ex2 : extremum.ex1; |
| if (x > xmin && x < xmax) { |
| final double y = isP2 ? extremum.ey2 : extremum.ey1; |
| if (y < transformed.getMinimum(j) || |
| y > transformed.getMaximum(j)) |
| { |
| /* |
| * At this point, we have determined that adding the extremum point |
| * would expand the envelope. However we will not add that point |
| * directly because its position may not be quite right (since we |
| * used a cubic curve approximation). Instead, we project the point |
| * on the envelope border which is located vis-à-vis the extremum. |
| */ |
| for (int ib3 = pointIndex, dim = sourceDim; --dim >= 0; ib3 /= 3) { |
| final double coordinate; |
| if (dim == i) { |
| coordinate = x; // Position of the extremum. |
| } else switch (ib3 % 3) { |
| case 0: coordinate = envelope.getMinimum(dim); break; |
| case 1: coordinate = envelope.getMaximum(dim); break; |
| case 2: coordinate = envelope.getMedian (dim); break; |
| default: throw new AssertionError(ib3); // Should never happen. |
| } |
| sourcePt[dim] = coordinate; |
| } |
| temporary = transform.transform(sourceView, temporary); |
| transformed.add(temporary); |
| } |
| } |
| } while ((isP2 = !isP2) == true); |
| } |
| } |
| } |
| } |
| derivatives[pointIndex] = null; // Let GC do its job earlier. |
| } |
| } |
| if (targetPt != null) { |
| // Copy the coordinate of the center point. |
| System.arraycopy(coordinates, coordinates.length - targetDim, targetPt, 0, targetDim); |
| } |
| return transformed; |
| } |
| |
| /** |
| * Transforms an envelope using the given coordinate operation. |
| * The transformation is only approximated: the returned envelope may be bigger than the |
| * smallest possible bounding box, but should not be smaller in most cases. |
| * |
| * <p>This method can handle the case where the envelope contains the North or South pole, |
| * or when it cross the ±180° longitude.</p> |
| * |
| * <div class="note"><b>Note:</b> |
| * If the envelope CRS is non-null, then the caller should ensure that the operation source CRS |
| * is the same than the envelope CRS. In case of mismatch, this method transforms the envelope |
| * to the operation source CRS before to apply the operation. This extra step may cause a lost |
| * of accuracy. In order to prevent this method from performing such pre-transformation (if not desired), |
| * callers can ensure that the envelope CRS is {@code null} before to call this method.</div> |
| * |
| * @param operation the operation to use. |
| * @param envelope envelope to transform, or {@code null}. This envelope will not be modified. |
| * @return the transformed envelope, or {@code null} if {@code envelope} was null. |
| * @throws TransformException if a transform failed. |
| * |
| * @see #transform(MathTransform, Envelope) |
| * |
| * @since 0.5 |
| */ |
| @SuppressWarnings("null") |
| public static GeneralEnvelope transform(final CoordinateOperation operation, Envelope envelope) |
| throws TransformException |
| { |
| ArgumentChecks.ensureNonNull("operation", operation); |
| if (envelope == null) { |
| return null; |
| } |
| boolean isOperationComplete = true; |
| final CoordinateReferenceSystem sourceCRS = operation.getSourceCRS(); |
| if (sourceCRS != null) { |
| final CoordinateReferenceSystem crs = envelope.getCoordinateReferenceSystem(); |
| if (crs != null && !Utilities.equalsIgnoreMetadata(crs, sourceCRS)) { |
| /* |
| * Argument-check: the envelope CRS seems inconsistent with the given operation. |
| * However we need to push the check a little bit further, since 3D-GeographicCRS |
| * are considered not equal to CompoundCRS[2D-GeographicCRS + ellipsoidal height]. |
| * Checking for identity MathTransform is a more powerfull (but more costly) check. |
| * Since we have the MathTransform, perform an opportunist envelope transform if it |
| * happen to be required. |
| */ |
| final MathTransform mt; |
| try { |
| mt = CoordinateOperations.factory().createOperation(crs, sourceCRS).getMathTransform(); |
| } catch (FactoryException e) { |
| throw new TransformException(Errors.format(Errors.Keys.CanNotTransformEnvelope), e); |
| } |
| if (!mt.isIdentity()) { |
| isOperationComplete = false; |
| envelope = transform(mt, envelope); |
| } |
| } |
| } |
| MathTransform mt = operation.getMathTransform(); |
| final double[] centerPt = new double[mt.getTargetDimensions()]; |
| final GeneralEnvelope transformed = transform(mt, envelope, centerPt); |
| /* |
| * If the source envelope crosses the expected range of valid coordinates, also projects |
| * the range bounds as a safety. Example: if the source envelope goes from 150 to 200°E, |
| * some map projections will interpret 200° as if it was -160°, and consequently produce |
| * an envelope which do not include the 180°W extremum. We will add those extremum points |
| * explicitly as a safety. It may leads to bigger than necessary target envelope, but the |
| * contract is to include at least the source envelope, not to return the smallest one. |
| */ |
| if (sourceCRS != null) { |
| final CoordinateSystem cs = sourceCRS.getCoordinateSystem(); |
| if (cs != null) { // Should never be null, but check as a paranoiac safety. |
| DirectPosition sourcePt = null; |
| DirectPosition targetPt = null; |
| final int dimension = cs.getDimension(); |
| for (int i=0; i<dimension; i++) { |
| final CoordinateSystemAxis axis = cs.getAxis(i); |
| if (axis == null) { // Should never be null, but check as a paranoiac safety. |
| continue; |
| } |
| final double min = envelope.getMinimum(i); |
| final double max = envelope.getMaximum(i); |
| final double v1 = axis.getMinimumValue(); |
| final double v2 = axis.getMaximumValue(); |
| final boolean b1 = (v1 > min && v1 < max); |
| final boolean b2 = (v2 > min && v2 < max); |
| if (!b1 && !b2) { |
| continue; |
| } |
| if (sourcePt == null) { |
| sourcePt = new GeneralDirectPosition(dimension); |
| for (int j=0; j<dimension; j++) { |
| sourcePt.setOrdinate(j, envelope.getMedian(j)); |
| } |
| } |
| if (b1) { |
| sourcePt.setOrdinate(i, v1); |
| transformed.add(targetPt = mt.transform(sourcePt, targetPt)); |
| } |
| if (b2) { |
| sourcePt.setOrdinate(i, v2); |
| transformed.add(targetPt = mt.transform(sourcePt, targetPt)); |
| } |
| sourcePt.setOrdinate(i, envelope.getMedian(i)); |
| } |
| } |
| } |
| /* |
| * Now takes the target CRS in account... |
| */ |
| final CoordinateReferenceSystem targetCRS = operation.getTargetCRS(); |
| if (targetCRS == null) { |
| return transformed; |
| } |
| transformed.setCoordinateReferenceSystem(targetCRS); |
| final CoordinateSystem targetCS = targetCRS.getCoordinateSystem(); |
| if (targetCS == null) { |
| // It should be an error, but we keep this method tolerant. |
| return transformed; |
| } |
| /* |
| * Checks for singularity points. For example the south pole is a singularity point in |
| * geographic CRS because is is located at the maximal value allowed by one particular |
| * axis, namely latitude. This point is not a singularity in the stereographic projection, |
| * because axes extends toward infinity in all directions (mathematically) and because the |
| * South pole has nothing special apart being the origin (0,0). |
| * |
| * Algorithm: |
| * |
| * 1) Inspect the target axis, looking if there is any bounds. If bounds are found, get |
| * the coordinates of singularity points and project them from target to source CRS. |
| * |
| * Example: If the transformed envelope above is (80 … 85°S, 10 … 50°W), and if the |
| * latitude in the target CRS is bounded at 90°S, then project (90°S, 30°W) |
| * to the source CRS. Note that the longitude is set to the the center of |
| * the envelope longitude range (more on this below). |
| * |
| * 2) If the singularity point computed above is inside the source envelope, add that |
| * point to the target (transformed) envelope. |
| * |
| * 3) If step #2 added the point, iterate over all other axes. If an other bounded axis |
| * is found and that axis is of kind "WRAPAROUND", test for inclusion the same point |
| * than the point tested at step #1, except for the coordinate of the axis found in |
| * this step. That coordinate is set to the minimal and maximal values of that axis. |
| * |
| * Example: If the above steps found that the point (90°S, 30°W) need to be included, |
| * then this step #3 will also test the points (90°S, 180°W) and (90°S, 180°E). |
| * |
| * NOTE: we test (-180°, centerY), (180°, centerY), (centerX, -90°) and (centerX, 90°) |
| * at step #1 before to test (-180°, -90°), (180°, -90°), (-180°, 90°) and (180°, 90°) |
| * at step #3 because the later may not be supported by every projections. For example |
| * if the target envelope is located between 20°N and 40°N, then a Mercator projection |
| * may fail to transform the (-180°, 90°) coordinate while the (-180°, 30°) coordinate |
| * is a valid point. |
| */ |
| TransformException warning = null; |
| AbstractEnvelope generalEnvelope = null; |
| DirectPosition sourcePt = null; |
| DirectPosition targetPt = null; |
| long includedMinValue = 0; // A bitmask for each dimension. |
| long includedMaxValue = 0; |
| long isWrapAroundAxis = 0; |
| long dimensionBitMask = 1; |
| final int dimension = targetCS.getDimension(); |
| poles: for (int i=0; i<dimension; i++, dimensionBitMask <<= 1) { |
| final CoordinateSystemAxis axis = targetCS.getAxis(i); |
| if (axis == null) { // Should never be null, but check as a paranoiac safety. |
| continue; |
| } |
| boolean testMax = false; // Tells if we are testing the minimal or maximal value. |
| do { |
| final double extremum = testMax ? axis.getMaximumValue() : axis.getMinimumValue(); |
| if (!Double.isFinite(extremum)) { |
| /* |
| * The axis is unbounded. It should always be the case when the target CRS is |
| * a map projection, in which case this loop will finish soon and this method |
| * will do nothing more (no object instantiated, no MathTransform inversed...) |
| */ |
| continue; |
| } |
| if (targetPt == null) { |
| try { |
| mt = mt.inverse(); |
| } catch (NoninvertibleTransformException exception) { |
| /* |
| * If the transform is non invertible, this method can't do anything. This |
| * is not a fatal error because the envelope has already be transformed by |
| * the caller. We lost the check for singularity points performed by this |
| * method, but it make no difference in the common case where the source |
| * envelope didn't contains any of those points. |
| * |
| * Note that this exception is normal if target dimension is smaller than |
| * source dimension, since the math transform can not reconstituate the |
| * lost dimensions. So we don't log any warning in this case. |
| */ |
| if (dimension >= mt.getSourceDimensions()) { |
| warning = exception; |
| } |
| break poles; |
| } |
| targetPt = new GeneralDirectPosition(mt.getSourceDimensions()); |
| for (int j=0; j<dimension; j++) { |
| targetPt.setOrdinate(j, centerPt[j]); |
| } |
| // TODO: avoid the hack below if we provide a contains(DirectPosition) |
| // method in the GeoAPI org.opengis.geometry.Envelope interface. |
| generalEnvelope = AbstractEnvelope.castOrCopy(envelope); |
| } |
| targetPt.setOrdinate(i, extremum); |
| try { |
| sourcePt = mt.transform(targetPt, sourcePt); |
| } catch (TransformException exception) { |
| /* |
| * This exception may be normal. For example if may occur when projecting |
| * the latitude extremums with a cylindrical Mercator projection. Do not |
| * log any message (unless logging level is fine) and try the other points. |
| */ |
| if (warning == null) { |
| warning = exception; |
| } else { |
| warning.addSuppressed(exception); |
| } |
| continue; |
| } |
| if (generalEnvelope.contains(sourcePt)) { |
| transformed.add(targetPt); |
| if (testMax) includedMaxValue |= dimensionBitMask; |
| else includedMinValue |= dimensionBitMask; |
| } |
| } while ((testMax = !testMax) == true); |
| /* |
| * Keep trace of axes of kind WRAPAROUND, except if the two extremum values of that |
| * axis have been included in the envelope (in which case the next step after this |
| * loop doesn't need to be executed for that axis). |
| */ |
| if ((includedMinValue & includedMaxValue & dimensionBitMask) == 0 && CoordinateOperations.isWrapAround(axis)) { |
| isWrapAroundAxis |= dimensionBitMask; |
| } |
| // Restore 'targetPt' to its initial state, which is equals to 'centerPt'. |
| if (targetPt != null) { |
| targetPt.setOrdinate(i, centerPt[i]); |
| } |
| } |
| /* |
| * Step #3 described in the above "Algorithm" section: iterate over all dimensions |
| * of type "WRAPAROUND" for which minimal or maximal axis values have not yet been |
| * included in the envelope. The set of axes is specified by a bitmask computed in |
| * the above loop. We examine only the points that have not already been included |
| * in the envelope. |
| */ |
| final long includedBoundsValue = (includedMinValue | includedMaxValue); |
| if (includedBoundsValue != 0) { |
| while (isWrapAroundAxis != 0) { |
| final int wrapAroundDimension = Long.numberOfTrailingZeros(isWrapAroundAxis); |
| dimensionBitMask = 1 << wrapAroundDimension; |
| isWrapAroundAxis &= ~dimensionBitMask; // Clear now the bit, for the next iteration. |
| final CoordinateSystemAxis wrapAroundAxis = targetCS.getAxis(wrapAroundDimension); |
| final double min = wrapAroundAxis.getMinimumValue(); |
| final double max = wrapAroundAxis.getMaximumValue(); |
| /* |
| * Iterate over all axes for which a singularity point has been previously found, |
| * excluding the "wrap around axis" currently under consideration. |
| */ |
| for (long am=(includedBoundsValue & ~dimensionBitMask), bm; am != 0; am &= ~bm) { |
| bm = Long.lowestOneBit(am); |
| final int axisIndex = Long.numberOfTrailingZeros(bm); |
| final CoordinateSystemAxis axis = targetCS.getAxis(axisIndex); |
| /* |
| * switch (c) { |
| * case 0: targetPt = (..., singularityMin, ..., wrapAroundMin, ...) |
| * case 1: targetPt = (..., singularityMin, ..., wrapAroundMax, ...) |
| * case 2: targetPt = (..., singularityMax, ..., wrapAroundMin, ...) |
| * case 3: targetPt = (..., singularityMax, ..., wrapAroundMax, ...) |
| * } |
| */ |
| for (int c=0; c<4; c++) { |
| /* |
| * Set the coordinate value along the axis having the singularity point |
| * (cases c=0 and c=2). If the envelope did not included that point, |
| * then skip completely this case and the next one, i.e. skip c={0,1} |
| * or skip c={2,3}. |
| */ |
| double value = max; |
| if ((c & 1) == 0) { // 'true' if we are testing "wrapAroundMin". |
| if (((c == 0 ? includedMinValue : includedMaxValue) & bm) == 0) { |
| c++; // Skip also the case for "wrapAroundMax". |
| continue; |
| } |
| targetPt.setOrdinate(axisIndex, (c == 0) ? axis.getMinimumValue() : axis.getMaximumValue()); |
| value = min; |
| } |
| targetPt.setOrdinate(wrapAroundDimension, value); |
| try { |
| sourcePt = mt.transform(targetPt, sourcePt); |
| } catch (TransformException exception) { |
| if (warning == null) { |
| warning = exception; |
| } else { |
| warning.addSuppressed(exception); |
| } |
| continue; |
| } |
| if (generalEnvelope.contains(sourcePt)) { |
| transformed.add(targetPt); |
| } |
| } |
| targetPt.setOrdinate(axisIndex, centerPt[axisIndex]); |
| } |
| targetPt.setOrdinate(wrapAroundDimension, centerPt[wrapAroundDimension]); |
| } |
| } |
| /* |
| * At this point we finished envelope transformation. Verify if some coordinates need to be "wrapped around" |
| * as a result of the coordinate operation. This is usually the longitude axis where the source CRS uses |
| * the [-180 … +180]° range and the target CRS uses the [0 … 360]° range, or the converse. We do not wrap |
| * around if the source and target axes use the same range (e.g. the longitude stay [-180 … +180]°) in order |
| * to reduce the risk of discontinuities. If the user really wants unconditional wrap around, (s)he can call |
| * GeneralEnvelope.normalize(). |
| */ |
| final Set<Integer> wrapAroundChanges; |
| if (isOperationComplete && operation instanceof AbstractCoordinateOperation) { |
| wrapAroundChanges = ((AbstractCoordinateOperation) operation).getWrapAroundChanges(); |
| } else { |
| wrapAroundChanges = CoordinateOperations.wrapAroundChanges(sourceCRS, targetCS); |
| } |
| transformed.normalize(targetCS, 0, wrapAroundChanges.size(), wrapAroundChanges.iterator()); |
| if (warning != null) { |
| recoverableException(Envelopes.class, warning); |
| } |
| return transformed; |
| } |
| |
| /** |
| * Returns the bounding box of a geometry defined in <cite>Well Known Text</cite> (WKT) format. |
| * This method does not check the consistency of the provided WKT. For example it does not check |
| * that every points in a {@code LINESTRING} have the same dimension. However this method |
| * ensures that the parenthesis are balanced, in order to catch some malformed WKT. |
| * |
| * <p>Example:</p> |
| * <ul> |
| * <li>{@code BOX(-180 -90, 180 90)} (not really a geometry, but understood by many software products)</li> |
| * <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> |
| * |
| * See {@link GeneralEnvelope#GeneralEnvelope(CharSequence)} for more information about the |
| * parsing rules. |
| * |
| * @param wkt the {@code BOX}, {@code POLYGON} or other kind of element to parse. |
| * @return the envelope of the given geometry. |
| * @throws FactoryException if the given WKT can not be parsed. |
| * |
| * @see #toString(Envelope) |
| * @see CRS#fromWKT(String) |
| * @see org.apache.sis.io.wkt |
| */ |
| public static Envelope fromWKT(final CharSequence wkt) throws FactoryException { |
| ArgumentChecks.ensureNonNull("wkt", wkt); |
| try { |
| return new GeneralEnvelope(wkt); |
| } catch (IllegalArgumentException e) { |
| throw new FactoryException(Errors.format( |
| Errors.Keys.UnparsableStringForClass_2, Envelope.class), e); |
| } |
| } |
| |
| /** |
| * Formats the given envelope as a {@code BOX} element. The output is like below, |
| * where <var>n</var> is the {@linkplain Envelope#getDimension() number of dimensions} |
| * (omitted if equals to 2): |
| * |
| * <blockquote>{@code BOX}<var>n</var>{@code D(}{@linkplain Envelope#getLowerCorner() lower |
| * corner}{@code ,} {@linkplain Envelope#getUpperCorner() upper corner}{@code )}</blockquote> |
| * |
| * <div class="note"><b>Note:</b> |
| * The {@code BOX} element is not part of the standard <cite>Well Known Text</cite> (WKT) format. |
| * However it is understood by many software libraries, for example GDAL and PostGIS.</div> |
| * |
| * The string returned by this method can be {@linkplain GeneralEnvelope#GeneralEnvelope(CharSequence) |
| * parsed} by the {@code GeneralEnvelope} constructor. |
| * |
| * @param envelope the envelope to format. |
| * @return this envelope as a {@code BOX} or {@code BOX3D} (most typical dimensions) element. |
| * |
| * @see #fromWKT(CharSequence) |
| * @see org.apache.sis.io.wkt |
| */ |
| public static String toString(final Envelope envelope) { |
| return AbstractEnvelope.toString(envelope, false); |
| } |
| |
| /** |
| * Formats the given envelope as a {@code POLYGON} element in the <cite>Well Known Text</cite> |
| * (WKT) format. {@code POLYGON} can be used as an alternative to {@code BOX} when the element |
| * needs to be considered as a standard WKT geometry. |
| * |
| * <p>The string returned by this method can be {@linkplain GeneralEnvelope#GeneralEnvelope(CharSequence) |
| * parsed} by the {@code GeneralEnvelope} constructor.</p> |
| * |
| * @param envelope the envelope to format. |
| * @return the envelope as a {@code POLYGON} in WKT format. |
| * @throws IllegalArgumentException if the given envelope can not be formatted. |
| * |
| * @see org.apache.sis.io.wkt |
| */ |
| public static String toPolygonWKT(final Envelope envelope) throws IllegalArgumentException { |
| /* |
| * Get the dimension, ignoring the trailing ones which have infinite values. |
| */ |
| int dimension = envelope.getDimension(); |
| while (dimension != 0) { |
| final double length = envelope.getSpan(dimension - 1); |
| if (Double.isFinite(length)) { |
| break; |
| } |
| dimension--; |
| } |
| if (dimension < 2) { |
| throw new IllegalArgumentException(Errors.format(Errors.Keys.EmptyEnvelope2D)); |
| } |
| final StringBuilder buffer = new StringBuilder("POLYGON("); |
| String separator = "("; |
| for (int corner = 0; corner < CORNERS.length; corner += 2) { |
| for (int i=0; i<dimension; i++) { |
| final double value; |
| switch (i) { |
| case 0: // Fall through |
| case 1: value = CORNERS[corner+i] ? envelope.getMaximum(i) : envelope.getMinimum(i); break; |
| default: value = envelope.getMedian(i); break; |
| } |
| trimFractionalPart(buffer.append(separator).append(value)); |
| separator = " "; |
| } |
| separator = ", "; |
| } |
| return buffer.append("))").toString(); |
| } |
| |
| /** |
| * Enumeration of the 4 corners in an envelope, with repetition of the first point. |
| * The values are (x,y) pairs with {@code false} meaning "minimal value" and {@code true} |
| * meaning "maximal value". This is used by {@link #toPolygonWKT(Envelope)} only. |
| */ |
| private static final boolean[] CORNERS = { |
| false, false, |
| false, true, |
| true, true, |
| true, false, |
| false, false |
| }; |
| } |