use of org.apache.sis.referencing.operation.AbstractCoordinateOperation in project sis by apache.
the class EPSGFactoryTest method testTransformation.
/**
* Tests "BD72 to WGS 84 (1)" (EPSG:1609) transformation. This one has an unusual unit for the
* "Scale difference" parameter (EPSG:8611). The value is 0.999999 and the unit is "unity" (EPSG:9201)
* instead of the usual "parts per million" (EPSG:9202).
*
* @throws FactoryException if an error occurred while querying the factory.
*/
@Test
@DependsOnMethod("testSimpleTransformation")
public void testTransformation() throws FactoryException {
final EPSGFactory factory = TestFactorySource.factory;
assumeNotNull(factory);
final CoordinateOperation operation = factory.createCoordinateOperation("1609");
assertEpsgNameAndIdentifierEqual("BD72 to WGS 84 (1)", 1609, operation);
assertEquals(1.0, ((AbstractCoordinateOperation) operation).getLinearAccuracy(), STRICT);
assertSame("Operation shall be cached", operation, factory.createCoordinateOperation("1609"));
}
use of org.apache.sis.referencing.operation.AbstractCoordinateOperation in project sis by apache.
the class Shapes2D method transform.
/**
* Transforms a rectangular envelope using the given coordinate operation.
* The transformation is only approximative: 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 rectangle contains the North or South pole,
* or when it cross the ±180° longitude.</p>
*
* @param operation the operation to use. Source and target dimension must be 2.
* @param envelope the rectangle to transform (may be {@code null}).
* @param destination the destination rectangle (may be {@code envelope}).
* If {@code null}, a new rectangle will be created and returned.
* @return {@code destination}, or a new rectangle if {@code destination} was non-null and {@code envelope} was null.
* @throws TransformException if a transform failed.
*
* @see #transform(MathTransform2D, Rectangle2D, Rectangle2D)
* @see Envelopes#transform(CoordinateOperation, Envelope)
*/
@SuppressWarnings("null")
public static Rectangle2D transform(final CoordinateOperation operation, final Rectangle2D envelope, Rectangle2D destination) throws TransformException {
ArgumentChecks.ensureNonNull("operation", operation);
if (envelope == null) {
return null;
}
final MathTransform transform = operation.getMathTransform();
if (!(transform instanceof MathTransform2D)) {
throw new MismatchedDimensionException(Errors.format(Errors.Keys.IllegalPropertyValueClass_3, "transform", MathTransform2D.class, MathTransform.class));
}
MathTransform2D mt = (MathTransform2D) transform;
final double[] center = new double[2];
destination = transform(mt, envelope, destination, center);
/*
* If the source envelope crosses the expected range of valid coordinates, also projects
* the range bounds as a safety. See the comments in transform(Envelope, ...).
*/
final CoordinateReferenceSystem sourceCRS = operation.getSourceCRS();
if (sourceCRS != null) {
final CoordinateSystem cs = sourceCRS.getCoordinateSystem();
if (cs != null && cs.getDimension() == 2) {
// Paranoiac check.
CoordinateSystemAxis axis = cs.getAxis(0);
double min = envelope.getMinX();
double max = envelope.getMaxX();
Point2D.Double pt = null;
for (int i = 0; i < 4; i++) {
if (i == 2) {
axis = cs.getAxis(1);
min = envelope.getMinY();
max = envelope.getMaxY();
}
final double v = (i & 1) == 0 ? axis.getMinimumValue() : axis.getMaximumValue();
if (!(v > min && v < max)) {
continue;
}
if (pt == null) {
pt = new Point2D.Double();
}
if ((i & 2) == 0) {
pt.x = v;
pt.y = envelope.getCenterY();
} else {
pt.x = envelope.getCenterX();
pt.y = v;
}
destination.add(mt.transform(pt, pt));
}
}
}
/*
* Now take the target CRS in account.
*/
final CoordinateReferenceSystem targetCRS = operation.getTargetCRS();
if (targetCRS == null) {
return destination;
}
final CoordinateSystem targetCS = targetCRS.getCoordinateSystem();
if (targetCS == null || targetCS.getDimension() != 2) {
// It should be an error, but we keep this method tolerant.
return destination;
}
/*
* Checks for singularity points. See the Envelopes.transform(CoordinateOperation, Envelope)
* method for comments about the algorithm. The code below is the same algorithm adapted for
* the 2D case and the related objects (Point2D, Rectangle2D, etc.).
*
* The 'border' variable in the loop below is used in order to compress 2 dimensions
* and 2 extremums in a single loop, in this order: (xmin, xmax, ymin, ymax).
*/
TransformException warning = null;
Point2D sourcePt = null;
Point2D targetPt = null;
// A bitmask for each (dimension, extremum) pairs.
int includedBoundsValue = 0;
for (int border = 0; border < 4; border++) {
// 2 dimensions and 2 extremums compacted in a flag.
// The dimension index being examined.
final int dimension = border >>> 1;
final CoordinateSystemAxis axis = targetCS.getAxis(dimension);
if (axis == null) {
// Should never be null, but check as a paranoiac safety.
continue;
}
final double extremum = (border & 1) == 0 ? axis.getMinimumValue() : axis.getMaximumValue();
if (Double.isInfinite(extremum) || Double.isNaN(extremum)) {
continue;
}
if (targetPt == null) {
try {
mt = mt.inverse();
} catch (NoninvertibleTransformException exception) {
Envelopes.recoverableException(Shapes2D.class, exception);
return destination;
}
targetPt = new Point2D.Double();
}
switch(dimension) {
case 0:
targetPt.setLocation(extremum, center[1]);
break;
case 1:
targetPt.setLocation(center[0], extremum);
break;
default:
throw new AssertionError(border);
}
try {
sourcePt = mt.transform(targetPt, sourcePt);
} catch (TransformException exception) {
if (warning == null) {
warning = exception;
} else {
warning.addSuppressed(exception);
}
continue;
}
if (envelope.contains(sourcePt)) {
destination.add(targetPt);
includedBoundsValue |= (1 << border);
}
}
/*
* Iterate over all dimensions of type "WRAPAROUND" for which minimal or maximal axis
* values have not yet been included in the envelope. We could inline this check inside
* the above loop, but we don't in order to have a chance to exclude the dimensions for
* which the point have already been added.
*
* See transform(CoordinateOperation, Envelope) for more comments about the algorithm.
*/
if (includedBoundsValue != 0) {
/*
* Bits mask transformation:
* 1) Swaps the two dimensions (YyXx → XxYy)
* 2) Insert a space between each bits (XxYy → X.x.Y.y.)
* 3) Fill the space with duplicated values (X.x.Y.y. → XXxxYYyy)
*
* In terms of bit positions 1,2,4,8 (not bit values), we have:
*
* 8421 → 22881144
* i.e. (ymax, ymin, xmax, xmin) → (xmax², ymax², xmin², ymin²)
*
* Now look at the last part: (xmin², ymin²). The next step is to perform a bitwise
* AND operation in order to have only both of the following conditions:
*
* Borders not yet added to the envelope: ~(ymax, ymin, xmax, xmin)
* Borders in which a singularity exists: (xmin, xmin, ymin, ymin)
*
* The same operation is repeated on the next 4 bits for (xmax, xmax, ymax, ymax).
*/
int toTest = ((includedBoundsValue & 1) << 3) | ((includedBoundsValue & 4) >>> 1) | ((includedBoundsValue & 2) << 6) | ((includedBoundsValue & 8) << 2);
// Duplicate the bit values.
toTest |= (toTest >>> 1);
toTest &= ~(includedBoundsValue | (includedBoundsValue << 4));
/*
* Forget any axes that are not of kind "WRAPAROUND". Then get the final
* bit pattern indicating which points to test. Iterate over that bits.
*/
if ((toTest & 0x33333333) != 0 && !CoordinateOperations.isWrapAround(targetCS.getAxis(0)))
toTest &= 0xCCCCCCCC;
if ((toTest & 0xCCCCCCCC) != 0 && !CoordinateOperations.isWrapAround(targetCS.getAxis(1)))
toTest &= 0x33333333;
while (toTest != 0) {
final int border = Integer.numberOfTrailingZeros(toTest);
final int bitMask = 1 << border;
// Clear now the bit, for the next iteration.
toTest &= ~bitMask;
final int dimensionToAdd = (border >>> 1) & 1;
final CoordinateSystemAxis toAdd = targetCS.getAxis(dimensionToAdd);
final CoordinateSystemAxis added = targetCS.getAxis(dimensionToAdd ^ 1);
double x = (border & 1) == 0 ? toAdd.getMinimumValue() : toAdd.getMaximumValue();
double y = (border & 4) == 0 ? added.getMinimumValue() : added.getMaximumValue();
if (dimensionToAdd != 0) {
final double t = x;
x = y;
y = t;
}
targetPt.setLocation(x, y);
try {
sourcePt = mt.transform(targetPt, sourcePt);
} catch (TransformException exception) {
if (warning == null) {
warning = exception;
} else {
warning.addSuppressed(exception);
}
continue;
}
if (envelope.contains(sourcePt)) {
destination.add(targetPt);
}
}
}
/*
* At this point we finished envelope transformation. Verify if some ordinates 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. In such case we
* set the rectangle to the full range (we do not use the mechanism documented in Envelope2D) because most
* Rectangle2D implementations do not support spanning the anti-meridian. This results in larger rectangle
* than what would be possible with GeneralEnvelope or Envelope2D, but we try to limit the situation where
* this expansion is applied.
*/
final Set<Integer> wrapAroundChanges;
if (operation instanceof AbstractCoordinateOperation) {
wrapAroundChanges = ((AbstractCoordinateOperation) operation).getWrapAroundChanges();
} else {
wrapAroundChanges = CoordinateOperations.wrapAroundChanges(sourceCRS, targetCS);
}
for (int dim : wrapAroundChanges) {
// Empty in the vast majority of cases.
final CoordinateSystemAxis axis = targetCS.getAxis(dim);
final double minimum = axis.getMinimumValue();
final double maximum = axis.getMaximumValue();
final double o1, o2;
if (dim == 0) {
o1 = destination.getMinX();
o2 = destination.getMaxX();
} else {
o1 = destination.getMinY();
o2 = destination.getMaxY();
}
if (o1 < minimum || o2 > maximum) {
final double span = maximum - minimum;
if (dim == 0) {
destination.setRect(minimum, destination.getY(), span, destination.getHeight());
} else {
destination.setRect(destination.getX(), minimum, destination.getWidth(), span);
}
}
}
if (warning != null) {
Envelopes.recoverableException(Shapes2D.class, warning);
}
return destination;
}
use of org.apache.sis.referencing.operation.AbstractCoordinateOperation in project sis by apache.
the class Envelopes method transform.
/**
* Transforms an envelope using the given coordinate operation.
* The transformation is only approximative: 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 {
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 ordinate of the axis found in this
* step. That ordinate 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;
// A bitmask for each dimension.
long includedMinValue = 0;
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;
}
// Tells if we are testing the minimal or maximal value.
boolean testMax = false;
do {
final double extremum = testMax ? axis.getMaximumValue() : axis.getMinimumValue();
if (Double.isInfinite(extremum) || Double.isNaN(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;
// Clear now the bit, for the next iteration.
isWrapAroundAxis &= ~dimensionBitMask;
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 ordinate 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) {
// Skip also the case for "wrapAroundMax".
c++;
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 ordinates 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;
}
Aggregations