blob: c0ed69088b4d4e5069e0d8d94c6328b88ab90baf [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.transform;
import java.awt.Shape;
import java.awt.geom.Point2D;
import java.awt.geom.Line2D;
import java.awt.geom.QuadCurve2D;
import java.awt.geom.Path2D;
import java.awt.geom.PathIterator;
import java.awt.geom.AffineTransform;
import java.awt.geom.IllegalPathStateException;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.MathTransform2D;
import org.opengis.referencing.operation.TransformException;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.referencing.j2d.ShapeUtilities;
/**
* Base class for math transforms that are known to be two-dimensional in all cases.
* Two-dimensional math transforms are not required to extend this class,
* however doing so may simplify their implementation.
*
* <p>The simplest way to implement this abstract class is to provide an implementation for the following methods
* only:</p>
* <ul>
* <li>{@link #transform(double[], int, double[], int, boolean)}</li>
* </ul>
*
* However more performance may be gained by overriding the other {@code transform} methods as well.
*
* <div class="section">Immutability and thread safety</div>
* All Apache SIS implementations of {@code MathTransform2D} are immutable and thread-safe.
* It is highly recommended that third-party implementations be immutable and thread-safe too.
* This means that unless otherwise noted in the javadoc, {@code MathTransform2D} instances can
* be shared by many objects and passed between threads without synchronization.
*
* <div class="section">Serialization</div>
* {@code MathTransform2D} may or may not be serializable, at implementation choices.
* Most Apache SIS implementations are serializable, but the serialized objects are not guaranteed to be compatible
* with future SIS versions. Serialization should be used only for short term storage or RMI between applications
* running the same SIS version.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.5
* @since 0.5
* @module
*/
public abstract class AbstractMathTransform2D extends AbstractMathTransform implements MathTransform2D {
/**
* Constructor for subclasses.
*/
protected AbstractMathTransform2D() {
}
/**
* Returns the dimension of input points, which is always 2.
*/
@Override
public final int getSourceDimensions() {
return 2;
}
/**
* Returns the dimension of output points, which is always 2.
*/
@Override
public final int getTargetDimensions() {
return 2;
}
/**
* Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}.
* The default implementation invokes {@link #transform(double[], int, double[], int, boolean)}
* using a temporary array of doubles.
*
* @param ptSrc the coordinate point to be transformed.
* @param ptDst the coordinate point that stores the result of transforming {@code ptSrc},
* or {@code null} if a new point shall be created.
* @return the coordinate point after transforming {@code ptSrc} and storing the result in {@code ptDst},
* or in a new point if {@code ptDst} was null.
* @throws TransformException if the point can not be transformed.
*
* @see MathTransform2D#transform(Point2D, Point2D)
*/
@Override
public Point2D transform(final Point2D ptSrc, final Point2D ptDst) throws TransformException {
return transform(this, ptSrc, ptDst);
}
/**
* Implementation of {@link #transform(Point2D, Point2D)} shared by the inverse transform.
*/
static Point2D transform(final AbstractMathTransform tr, final Point2D ptSrc, final Point2D ptDst) throws TransformException {
final double[] ord = new double[] {ptSrc.getX(), ptSrc.getY()};
tr.transform(ord, 0, ord, 0, false);
if (ptDst != null) {
ptDst.setLocation(ord[0], ord[1]);
return ptDst;
} else {
return new Point2D.Double(ord[0], ord[1]);
}
}
/**
* Transforms the specified shape. The default implementation computes quadratic curves
* using three points for each line segment in the shape. The returned object is often
* a {@link Path2D}, but may also be a {@link Line2D} or a {@link QuadCurve2D} if such
* simplification is possible.
*
* @param shape shape to transform.
* @return transformed shape, or {@code shape} if this transform is the identity transform.
* @throws TransformException if a transform failed.
*/
@Override
public Shape createTransformedShape(final Shape shape) throws TransformException {
return isIdentity() ? shape : createTransformedShape(this, shape, null, null, false);
}
/**
* Transforms a geometric shape. This method always copy transformed coordinates in a new object.
* The new object is often a {@link Path2D}, but may also be a {@link Line2D} or a {@link QuadCurve2D}
* if such simplification is possible.
*
* @param mt the math transform to use.
* @param shape the geometric shape to transform.
* @param preTransform an optional affine transform to apply <em>before</em> the
* transformation using {@code this}, or {@code null} if none.
* @param postTransform an optional affine transform to apply <em>after</em> the transformation
* using {@code this}, or {@code null} if none.
* @param horizontal {@code true} for forcing parabolic equation.
* @return the transformed geometric shape.
* @throws TransformException if a transformation failed.
*/
static Shape createTransformedShape(final MathTransform2D mt,
final Shape shape,
final AffineTransform preTransform,
final AffineTransform postTransform,
final boolean horizontal)
throws TransformException
{
final PathIterator it = shape.getPathIterator(preTransform);
final Path2D.Double path = new Path2D.Double(it.getWindingRule());
final double[] buffer = new double[6];
double ax=0, ay=0; // Coordinate of the last point before transform.
double px=0, py=0; // Coordinate of the last point after transform.
for (; !it.isDone(); it.next()) {
switch (it.currentSegment(buffer)) {
default: {
throw new IllegalPathStateException();
}
case PathIterator.SEG_CLOSE: {
/*
* Close the geometric shape and continues the loop. We use the 'continue' instruction
* here instead of 'break' because we do not want to execute the code after the switch
* (addition of transformed points into the path - there is no such point in a SEG_CLOSE).
*/
path.closePath();
continue;
}
case PathIterator.SEG_MOVETO: {
/*
* Transform the single point and adds it to the path. We use the 'continue' instruction
* here instead of 'break' because we do not want to execute the code after the switch
* (addition of a line or a curve - there is no such curve to add here; we are just moving
* the cursor).
*/
ax = buffer[0];
ay = buffer[1];
mt.transform(buffer, 0, buffer, 0, 1);
px = buffer[0];
py = buffer[1];
if (Double.isFinite(px) && Double.isFinite(py)) {
path.moveTo(px, py);
continue;
} else {
throw new TransformException(Resources.format(Resources.Keys.CanNotTransformCoordinates_2, ax, ay));
}
}
case PathIterator.SEG_LINETO: {
/*
* Insert a new control point at 'buffer[0,1]'. This control point will
* be initialised with coordinates in the middle of the straight line:
*
* x = 0.5 * (x1+x2)
* y = 0.5 * (y1+y2)
*
* This point will be transformed after the 'switch', which is why we use
* the 'break' statement here instead of 'continue' as in previous case.
*/
buffer[0] = 0.5 * (ax + (ax = buffer[0]));
buffer[1] = 0.5 * (ay + (ay = buffer[1]));
buffer[2] = ax;
buffer[3] = ay;
break;
}
case PathIterator.SEG_QUADTO: {
/*
* Replace the control point in 'buffer[0,1]' by a new control point lying on the quadratic curve.
* Coordinates for a point in the middle of the curve can be computed with:
*
* x = 0.5 * (ctrlx + 0.5 * (x1+x2))
* y = 0.5 * (ctrly + 0.5 * (y1+y2))
*
* There is no need to keep the old control point because it was not lying on the curve.
*/
buffer[0] = 0.5 * (buffer[0] + 0.5*(ax + (ax = buffer[2])));
buffer[1] = 0.5 * (buffer[1] + 0.5*(ay + (ay = buffer[3])));
break;
}
case PathIterator.SEG_CUBICTO: {
/*
* Replace the control point in 'buffer[0,1]' by a new control point lying on the cubic curve.
* Coordinates for a point in the middle of the curve can be computed with:
*
* x = 0.25 * (1.5 * (ctrlx1 + ctrlx2) + 0.5 * (x1 + x2));
* y = 0.25 * (1.5 * (ctrly1 + ctrly2) + 0.5 * (y1 + y2));
*
* There is no need to keep the old control point because it was not lying on the curve.
*
* NOTE: The computed point is on the curve, but may not be representative of the shape.
* This algorithm replaces two control points by a single one, because we did not
* venture into a more sophisticated algorithm producing a CubicCurve2D. For now,
* we presume that the current algorithm is okay if the curve is smooth enough.
*/
buffer[0] = 0.25 * (1.5 * (buffer[0] + buffer[2]) + 0.5 * (ax + (ax = buffer[4])));
buffer[1] = 0.25 * (1.5 * (buffer[1] + buffer[3]) + 0.5 * (ay + (ay = buffer[5])));
buffer[2] = ax;
buffer[3] = ay;
break;
}
}
/*
* Apply the transform on the point in the buffer, and append the transformed points
* to the general path. Try to add them as a quadratic line, or as a straight line if
* the computed control point is colinear with the starting and ending points.
*/
mt.transform(buffer, 0, buffer, 0, 2);
final Point2D ctrlPoint = ShapeUtilities.parabolicControlPoint(px, py,
buffer[0], buffer[1],
buffer[2], buffer[3],
horizontal);
px = buffer[2];
py = buffer[3];
if (Double.isFinite(px) && Double.isFinite(py)) {
if (ctrlPoint == null) {
path.lineTo(px, py);
} else {
final double cx = ctrlPoint.getX();
final double cy = ctrlPoint.getY();
if (Double.isFinite(cx) && Double.isFinite(cy)) {
path.quadTo(cx, cy, px, py);
} else {
throw new TransformException(Resources.format(Resources.Keys.CanNotTransformGeometry));
}
}
} else {
throw new TransformException(Resources.format(Resources.Keys.CanNotTransformCoordinates_2, ax, ay));
}
}
/*
* Shape transformation is done. Apply an affine transform if it was requested,
* then simplify the geometric object (not the coordinate values) if possible.
*/
if (postTransform != null) {
path.transform(postTransform);
}
return ShapeUtilities.toPrimitive(path);
}
/**
* Gets the derivative of this transform at a point.
* The default implementation performs the following steps:
*
* <ul>
* <li>Copy the coordinate in a temporary array and pass that array to the
* {@link #transform(double[], int, double[], int, boolean)} method,
* with the {@code derivate} boolean argument set to {@code true}.</li>
* <li>If the later method returned a non-null matrix, returns that matrix.
* Otherwise throws {@link TransformException}.</li>
* </ul>
*
* @param point the coordinate point where to evaluate the derivative.
* @return the derivative at the specified point as a 2×2 matrix.
* @throws TransformException if the derivative can not be evaluated at the specified point.
*/
@Override
public Matrix derivative(final Point2D point) throws TransformException {
return derivative(this, point);
}
/**
* Implementation of {@link #derivative(DirectPosition)} shared by the inverse transform.
*/
static Matrix derivative(final AbstractMathTransform tr, final Point2D point) throws TransformException {
final double[] coordinate = new double[] {point.getX(), point.getY()};
final Matrix derivative = tr.transform(coordinate, 0, null, 0, true);
if (derivative == null) {
throw new TransformException(Resources.format(Resources.Keys.CanNotComputeDerivative));
}
return derivative;
}
/**
* Returns the inverse transform of this object.
* The default implementation returns {@code this} if this transform is an identity transform,
* or throws an exception otherwise. Subclasses should override this method.
*/
@Override
public MathTransform2D inverse() throws NoninvertibleTransformException {
return (MathTransform2D) super.inverse();
}
/**
* Base class for implementation of inverse math transforms.
* This inner class is the inverse of the enclosing {@link AbstractMathTransform2D}.
*
* <div class="section">Serialization</div>
* This object may or may not be serializable, at implementation choices.
* Most Apache SIS implementations are serializable, but the serialized objects are not guaranteed to be compatible
* with future SIS versions. Serialization should be used only for short term storage or RMI between applications
* running the same SIS version.
*
* @author Martin Desruisseaux (Geomatys)
* @version 1.0
* @since 0.5
* @module
*/
protected abstract static class Inverse extends AbstractMathTransform.Inverse implements MathTransform2D {
/**
* Constructs an inverse math transform.
*/
protected Inverse() {
}
/**
* Returns the inverse of this math transform.
* The returned transform should be the enclosing math transform.
*/
@Override
public abstract MathTransform2D inverse();
/**
* Transforms the specified {@code ptSrc} and stores the result in {@code ptDst}.
* The default implementation invokes {@link #transform(double[], int, double[], int, boolean)}
* using a temporary array of doubles.
*
* @param ptSrc the coordinate point to be transformed.
* @param ptDst the coordinate point that stores the result of transforming {@code ptSrc},
* or {@code null} if a new point shall be created.
* @return the coordinate point after transforming {@code ptSrc} and storing the result in {@code ptDst},
* or in a new point if {@code ptDst} was null.
* @throws TransformException if the point can not be transformed.
*
* @see MathTransform2D#transform(Point2D, Point2D)
*/
@Override
public Point2D transform(final Point2D ptSrc, final Point2D ptDst) throws TransformException {
return AbstractMathTransform2D.transform(this, ptSrc, ptDst);
}
/**
* Transforms the specified shape. The default implementation computes quadratic curves
* using three points for each line segment in the shape. The returned object is often
* a {@link Path2D}, but may also be a {@link Line2D} or a {@link QuadCurve2D} if such
* simplification is possible.
*
* @param shape shape to transform.
* @return transformed shape, or {@code shape} if this transform is the identity transform.
* @throws TransformException if a transform failed.
*/
@Override
public Shape createTransformedShape(final Shape shape) throws TransformException {
return isIdentity() ? shape : AbstractMathTransform2D.createTransformedShape(this, shape, null, null, false);
}
/**
* Gets the derivative of this transform at a point.
* The default implementation performs the following steps:
*
* <ul>
* <li>Copy the coordinate in a temporary array and pass that array to the
* {@link #transform(double[], int, double[], int, boolean)} method,
* with the {@code derivate} boolean argument set to {@code true}.</li>
* <li>If the later method returned a non-null matrix, returns that matrix.
* Otherwise throws {@link TransformException}.</li>
* </ul>
*
* @param point the coordinate point where to evaluate the derivative.
* @return the derivative at the specified point as a 2×2 matrix.
* @throws TransformException if the derivative can not be evaluated at the specified point.
*/
@Override
public Matrix derivative(final Point2D point) throws TransformException {
return AbstractMathTransform2D.derivative(this, point);
}
}
}