blob: a3441d4775d0e14b5c369c81512c89bb75a2670a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.operation.matrix;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Path2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.geom.RectangularShape;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.util.Static;
import org.apache.sis.util.ArgumentChecks;
import static java.lang.Math.*;
import static java.awt.geom.AffineTransform.*;
/**
* Bridge between {@link Matrix} and Java2D {@link AffineTransform} instances.
* Those {@code AffineTransform} instances can be viewed as 3×3 matrices.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.4
* @since 0.4
* @module
*/
public final class AffineTransforms2D extends Static {
/**
* Do not allows instantiation of this class.
*/
private AffineTransforms2D() {
}
/**
* Returns the given matrix as a Java2D affine transform.
* If the given matrix is already an instance of {@link AffineTransform}, then it is returned directly.
* Otherwise the values are copied in a new {@code AffineTransform} instance.
*
* @param matrix the matrix to returns as an affine transform, or {@code null}.
* @return the matrix argument if it can be safely casted (including {@code null} argument),
* or a copy of the given matrix otherwise.
* @throws IllegalArgumentException if the given matrix size is not 3×3 or if the matrix is not affine.
*
* @see Matrices#isAffine(Matrix)
*/
public static AffineTransform castOrCopy(final Matrix matrix) throws IllegalArgumentException {
if (matrix == null || matrix instanceof AffineTransform) {
return (AffineTransform) matrix;
}
MatrixSIS.ensureSizeMatch(3, 3, matrix);
if (!Matrices.isAffine(matrix)) {
throw new IllegalStateException(Resources.format(Resources.Keys.NotAnAffineTransform));
}
return new AffineTransform(matrix.getElement(0,0), matrix.getElement(1,0),
matrix.getElement(0,1), matrix.getElement(1,1),
matrix.getElement(0,2), matrix.getElement(1,2));
}
/**
* Creates a 3×3 matrix from the given affine transform.
*
* @param transform the affine transform to copy as a matrix.
* @return a matrix containing the same terms than the given affine transform.
*/
public static Matrix3 toMatrix(final AffineTransform transform) {
return new Matrix3(transform.getScaleX(), transform.getShearX(), transform.getTranslateX(),
transform.getShearY(), transform.getScaleY(), transform.getTranslateY(),
0, 0, 1);
}
/**
* Transforms the given shape.
* This method is similar to {@link AffineTransform#createTransformedShape(Shape)} except that:
*
* <ul>
* <li>It tries to preserve the shape kind when possible. For example if the given shape
* is an instance of {@link RectangularShape} and the given transform does not involve
* rotation, then the returned shape may be some instance of the same class.</li>
* <li>It tries to recycle the given object if {@code overwrite} is {@code true}.</li>
* </ul>
*
* @param transform the affine transform to use.
* @param shape the shape to transform, or {@code null}.
* @param allowOverwrite if {@code true}, this method is allowed to overwrite {@code shape} with the
* transform result. If {@code false}, then {@code shape} is never modified.
* @return the transform of the given shape, or {@code null} if the given shape was null.
* May or may not be the same instance than the given shape.
*
* @see AffineTransform#createTransformedShape(Shape)
*/
public static Shape transform(final AffineTransform transform, Shape shape, boolean allowOverwrite) {
ArgumentChecks.ensureNonNull("transform", transform);
if (shape == null) {
return null;
}
final int type = transform.getType();
if (type == TYPE_IDENTITY) {
return shape;
}
/*
* If there is only scale, flip, quadrant rotation or translation,
* then we can optimize the transformation of rectangular shapes.
*/
if ((type & (TYPE_GENERAL_ROTATION | TYPE_GENERAL_TRANSFORM)) == 0) {
// For a Rectangle input, the output should be a rectangle as well.
if (shape instanceof Rectangle2D) {
final Rectangle2D rect = (Rectangle2D) shape;
return transform(transform, rect, allowOverwrite ? rect : null);
}
/*
* For other rectangular shapes, we restrict to cases without
* rotation or flip because we don't know if the shape is symmetric.
*/
if ((type & (TYPE_FLIP | TYPE_MASK_ROTATION)) == 0) {
if (shape instanceof RectangularShape) {
RectangularShape rect = (RectangularShape) shape;
if (!allowOverwrite) {
rect = (RectangularShape) rect.clone();
}
final Rectangle2D frame = rect.getFrame();
rect.setFrame(transform(transform, frame, frame));
return rect;
}
}
}
if (shape instanceof Path2D) {
final Path2D path = (Path2D) shape;
if (allowOverwrite) {
path.transform(transform);
} else {
shape = path.createTransformedShape(transform);
}
} else if (shape instanceof Area) {
final Area area = (Area) shape;
if (allowOverwrite) {
area.transform(transform);
} else {
shape = area.createTransformedArea(transform);
}
} else {
shape = new Path2D.Double(shape, transform);
}
return shape;
}
/**
* Calculates a rectangle which entirely contains the direct transform of {@code bounds}.
* This operation is equivalent to the following code, except that it can reuse the
* given {@code dest} rectangle and is potentially more efficient:
*
* {@preformat java
* return transform.createTransformedShape(bounds).getBounds2D();
* }
*
* Note that if the given rectangle is an image bounds, then the given transform shall map the
* <strong>upper-left corner</strong> of pixels (as in Java2D usage), not the center of pixels
* (OGC usage).
*
* @param transform the affine transform to use.
* @param bounds the rectangle to transform, or {@code null}.
* this rectangle will not be modified except if {@code dest} references the same object.
* @param dest rectangle in which to place the result. If {@code null}, a new rectangle will be created.
* @return the direct transform of the {@code bounds} rectangle, or {@code null} if {@code bounds} was null.
*
* @see org.apache.sis.geometry.Shapes2D#transform(MathTransform2D, Rectangle2D, Rectangle2D)
*/
public static Rectangle2D transform(final AffineTransform transform,
final Rectangle2D bounds, final Rectangle2D dest)
{
ArgumentChecks.ensureNonNull("transform", transform);
if (bounds == null) {
return null;
}
double xmin = Double.POSITIVE_INFINITY;
double ymin = Double.POSITIVE_INFINITY;
double xmax = Double.NEGATIVE_INFINITY;
double ymax = Double.NEGATIVE_INFINITY;
final Point2D.Double point = new Point2D.Double();
for (int i=0; i<4; i++) {
point.x = (i & 1) == 0 ? bounds.getMinX() : bounds.getMaxX();
point.y = (i & 2) == 0 ? bounds.getMinY() : bounds.getMaxY();
transform.transform(point, point);
if (point.x < xmin) xmin = point.x;
if (point.x > xmax) xmax = point.x;
if (point.y < ymin) ymin = point.y;
if (point.y > ymax) ymax = point.y;
}
if (dest != null) {
dest.setRect(xmin, ymin, xmax-xmin, ymax-ymin);
return dest;
}
return new Rectangle2D.Double(xmin, ymin, xmax-xmin, ymax-ymin);
}
/**
* Calculates a rectangle which entirely contains the inverse transform of {@code bounds}.
* This operation is equivalent to the following code, except that it can reuse the
* given {@code dest} rectangle and is potentially more efficient:
*
* {@preformat java
* return createInverse().createTransformedShape(bounds).getBounds2D();
* }
*
* @param transform the affine transform to use.
* @param bounds the rectangle to transform, or {@code null}.
* this rectangle will not be modified except if {@code dest} references the same object.
* @param dest rectangle in which to place the result. If {@code null}, a new rectangle will be created.
* @return the inverse transform of the {@code bounds} rectangle, or {@code null} if {@code bounds} was null.
* @throws NoninvertibleTransformException if the affine transform can't be inverted.
*/
public static Rectangle2D inverseTransform(final AffineTransform transform,
final Rectangle2D bounds, final Rectangle2D dest) throws NoninvertibleTransformException
{
ArgumentChecks.ensureNonNull("transform", transform);
if (bounds == null) {
return null;
}
double xmin = Double.POSITIVE_INFINITY;
double ymin = Double.POSITIVE_INFINITY;
double xmax = Double.NEGATIVE_INFINITY;
double ymax = Double.NEGATIVE_INFINITY;
final Point2D.Double point = new Point2D.Double();
for (int i=0; i<4; i++) {
point.x = (i & 1) == 0 ? bounds.getMinX() : bounds.getMaxX();
point.y = (i & 2) == 0 ? bounds.getMinY() : bounds.getMaxY();
transform.inverseTransform(point, point);
if (point.x < xmin) xmin = point.x;
if (point.x > xmax) xmax = point.x;
if (point.y < ymin) ymin = point.y;
if (point.y > ymax) ymax = point.y;
}
if (dest != null) {
dest.setRect(xmin, ymin, xmax-xmin, ymax-ymin);
return dest;
}
return new Rectangle2D.Double(xmin, ymin, xmax-xmin, ymax-ymin);
}
/**
* Calculates the inverse transform of a point without applying the translation components.
* In other words, calculates the inverse transform of a displacement vector.
*
* @param transform the affine transform to use.
* @param vector the vector to transform stored as a point.
* this point will not be modified except if {@code dest} references the same object.
* @param dest point in which to place the result. If {@code null}, a new point will be created.
* @return the inverse transform of the {@code vector}, or {@code null} if {@code source} was null.
* @throws NoninvertibleTransformException if the affine transform can't be inverted.
*/
public static Point2D inverseDeltaTransform(final AffineTransform transform,
final Point2D vector, final Point2D dest) throws NoninvertibleTransformException
{
ArgumentChecks.ensureNonNull("transform", transform);
if (vector == null) {
return null;
}
final double m00 = transform.getScaleX();
final double m11 = transform.getScaleY();
final double m01 = transform.getShearX();
final double m10 = transform.getShearY();
final double det = m00*m11 - m01*m10;
if (!(abs(det) > Double.MIN_VALUE)) {
throw new NoninvertibleTransformException(null);
}
final double x0 = vector.getX();
final double y0 = vector.getY();
final double x = (x0*m11 - y0*m01) / det;
final double y = (y0*m00 - x0*m10) / det;
if (dest != null) {
dest.setLocation(x, y);
return dest;
}
return new Point2D.Double(x, y);
}
/**
* Returns an estimation about whether the specified transform swaps <var>x</var> and <var>y</var> axes.
* This method assumes that the specified affine transform is built from arbitrary translations, scales or
* rotations, but no shear. It returns {@code +1} if the (<var>x</var>, <var>y</var>) axis order seems to be
* preserved, {@code -1} if the transform seems to swap axis to the (<var>y</var>, <var>x</var>) axis order,
* or {@code 0} if this method can not make a decision.
*
* @param transform the affine transform to inspect.
* @return {@code true} if the given transform seems to swap axis order.
*/
public static int getSwapXY(final AffineTransform transform) {
ArgumentChecks.ensureNonNull("transform", transform);
final int flip = getFlip(transform);
if (flip != 0) {
final double scaleX = getScaleX0(transform);
final double scaleY = getScaleY0(transform) * flip;
final double y = abs(transform.getShearY()/scaleY - transform.getShearX()/scaleX);
final double x = abs(transform.getScaleY()/scaleY + transform.getScaleX()/scaleX);
if (x > y) return +1;
if (x < y) return -1;
// At this point, we may have (x == y) or some NaN value.
}
return 0;
}
/**
* Returns an estimation of the rotation angle in radians. This method assumes that the specified affine
* transform is built from arbitrary translations, scales or rotations, but no shear. If a flip has been
* applied, then this method assumes that the flipped axis is the <var>y</var> one in <cite>source CRS</cite>
* space. For a <cite>grid to world CRS</cite> transform, this is the row number in grid coordinates.
*
* @param transform the affine transform to inspect.
* @return an estimation of the rotation angle in radians,
* or {@link Double#NaN NaN} if the angle can not be estimated.
*/
public static double getRotation(final AffineTransform transform) {
ArgumentChecks.ensureNonNull("transform", transform);
final int flip = getFlip(transform);
if (flip != 0) {
final double scaleX = getScaleX0(transform);
final double scaleY = getScaleY0(transform) * flip;
return atan2(transform.getShearY()/scaleY - transform.getShearX()/scaleX,
transform.getScaleY()/scaleY + transform.getScaleX()/scaleX);
}
return Double.NaN;
}
/**
* Returns {@code -1} if one axis has been flipped, {@code +1} if no axis has been flipped, or 0 if unknown.
* A flipped axis in an axis with direction reversed (typically the <var>y</var> axis). This method assumes
* that the specified affine transform is built from arbitrary translations, scales or rotations, but no shear.
* Note that it is not possible to determine which of the <var>x</var> or <var>y</var> axis has been flipped.
*
* <p>This method can be used in order to set the sign of a scale according the flipping state.
* The example below choose to apply the sign on the <var>y</var> scale, but this is an arbitrary
* (while common) choice:</p>
*
* {@preformat java
* double scaleX0 = getScaleX0(transform);
* double scaleY0 = getScaleY0(transform);
* int flip = getFlip(transform);
* if (flip != 0) {
* scaleY0 *= flip;
* // ... continue the process here.
* }
* }
*
* This method is similar to the following code, except that this method distinguishes
* between "unflipped" and "unknown" states.
*
* {@preformat java
* boolean flipped = (tr.getType() & TYPE_FLIP) != 0;
* }
*
* @param transform the affine transform to inspect.
* @return -1 if an axis has been flipped, +1 if no flipping, or 0 if unknown.
*/
public static int getFlip(final AffineTransform transform) {
ArgumentChecks.ensureNonNull("transform", transform);
final double scaleX = Math.signum(transform.getScaleX());
final double scaleY = Math.signum(transform.getScaleY());
final double shearX = Math.signum(transform.getShearX());
final double shearY = Math.signum(transform.getShearY());
if (scaleX == scaleY && shearX == -shearY) return +1;
if (scaleX == -scaleY && shearX == shearY) return -1;
return 0;
}
/**
* Returns the magnitude of scale factor <var>x</var> by canceling the
* effect of eventual flip and rotation. This factor is calculated by:
*
* <p><img src="doc-files/scaleX0.png" alt="Scale factor on x axis"></p>
*
* @param transform the affine transform to inspect.
* @return the magnitude of scale factor <var>x</var>.
*/
public static double getScaleX0(final AffineTransform transform) {
ArgumentChecks.ensureNonNull("transform", transform);
final double scale = transform.getScaleX();
final double shear = transform.getShearX();
if (shear == 0) return abs(scale); // Optimization for a very common case.
if (scale == 0) return abs(shear); // Not as common as above, but still common enough.
return hypot(scale, shear);
}
/**
* Returns the magnitude of scale factor <var>y</var> by canceling the
* effect of eventual flip and rotation. This factor is calculated by:
*
* <p><img src="doc-files/scaleY0.png" alt="Scale factor on y axis"></p>
*
* @param transform the affine transform to inspect.
* @return the magnitude of scale factor <var>y</var>.
*/
public static double getScaleY0(final AffineTransform transform) {
ArgumentChecks.ensureNonNull("transform", transform);
final double scale = transform.getScaleY();
final double shear = transform.getShearY();
if (shear == 0) return abs(scale); // Optimization for a very common case.
if (scale == 0) return abs(shear); // Not as common as above, but still common enough.
return hypot(scale, shear);
}
}