/*
* Geotools 2 - OpenSource mapping toolkit
* (C) 2003, Geotools Project Managment Committee (PMC)
* (C) 2001, Institut de Recherche pour le Développement
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
*
* This package contains documentation from OpenGIS specifications.
* OpenGIS consortium's work is fully acknowledged here.
*/
package org.geotools.ct;
// J2SE and vecmath dependencies
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.IllegalPathStateException;
import java.awt.geom.Line2D;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.QuadCurve2D;
import java.io.Serializable;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.rmi.RemoteException;
import java.util.Locale;
import javax.vecmath.SingularMatrixException;
import org.geotools.pt.CoordinatePoint;
import org.geotools.pt.Matrix;
import org.geotools.resources.Utilities;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.geometry.ShapeUtilities;
import org.geotools.util.UnsupportedImplementationException;
import org.opengis.ct.CT_MathTransform;
import org.opengis.referencing.operation.NoninvertibleTransformException;
import org.opengis.referencing.operation.TransformException;
import org.opengis.spatialschema.geometry.MismatchedDimensionException;
/**
* Provides a default implementations for most methods required by the
* {@link MathTransform} interface. AbstractMathTransform
* provides a convenient base class from which other transform classes
* can be easily derived. In addition, AbstractMathTransform
* implements methods required by the {@link MathTransform2D} interface,
* but does not implements MathTransform2D
.
* Subclasses must declare implements MathTransform2D
* themself if they know to maps two-dimensional coordinate systems.
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
*
* @deprecated Replaced by {@link org.geotools.referencing.operation.transform.AbstractMathTransform}
* in the org.geotools.referencing.operation.transform
package.
*/
public abstract class AbstractMathTransform implements MathTransform {
/**
* OpenGIS object returned by {@link #cachedOpenGIS}.
* It may be a hard or a weak reference.
*/
private transient Object proxy;
/**
* Construct a math transform which is an adapter for
* the specified {@link CT_MathTransform} interface.
*
* @param opengis The {@link CT_MathTransform} interface. We declare
* a generic {@link Object} in order to avoid too early class
* loading of OpenGIS's interfaces.
*/
AbstractMathTransform(final Object opengis) {
proxy = opengis;
}
/**
* Construct a math transform.
*/
public AbstractMathTransform() {
}
/**
* Returns a human readable name, if available. If no name is available in
* the specified locale, then this method returns a name in an arbitrary
* locale. If no name is available in any locale, then this method returns
* null
. The default implementation always returns null
.
*
* @param locale The desired locale, or null
for a default locale.
* @return The transform name localized in the specified locale if possible, or
* null
if no name is available in any locale.
*/
protected String getName(final Locale locale) {
return null;
}
/**
* Tests whether this transform does not move any points.
* The default implementation always returns false
.
*/
public boolean isIdentity() {
return false;
}
/**
* Transforms the specified ptSrc
and stores the result in ptDst
.
* The default implementation invokes {@link #transform(double[],int,double[],int,int)}
* using a temporary array of doubles.
*
* @param ptSrc the specified coordinate point to be transformed.
* @param ptDst the specified coordinate point that stores the
* result of transforming ptSrc
, or
* null
.
* @return the coordinate point after transforming ptSrc
* and stroring the result in ptDst
.
* @throws MismatchedDimensionException if this transform
* doesn't map two-dimensional coordinate systems.
* @throws TransformException if the point can't be transformed.
*
* @see MathTransform2D#transform(Point2D,Point2D)
*/
public Point2D transform(final Point2D ptSrc, final Point2D ptDst)
throws TransformException
{
if (getDimSource()!=2 || getDimTarget()!=2) {
throw new MismatchedDimensionException();
}
final double[] ord = new double[] {ptSrc.getX(), ptSrc.getY()};
this.transform(ord, 0, ord, 0, 1);
if (ptDst!=null) {
ptDst.setLocation(ord[0], ord[1]);
return ptDst;
} else {
return new Point2D.Double(ord[0], ord[1]);
}
}
/**
* Transforms the specified ptSrc
and stores the result
* in ptDst
. The default implementation invokes
* {@link #transform(double[],int,double[],int,int)}.
*/
public CoordinatePoint transform(final CoordinatePoint ptSrc, CoordinatePoint ptDst)
throws TransformException
{
final int dimPoint = ptSrc.getDimension();
final int dimSource = getDimSource();
final int dimTarget = getDimTarget();
if (dimPoint != dimSource) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$2,
new Integer(dimPoint), new Integer(dimSource)));
}
if (ptDst==null) {
ptDst = new CoordinatePoint(dimTarget);
} else if (ptDst.getDimension() != dimTarget) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$2,
new Integer(ptDst.getDimension()), new Integer(dimTarget)));
}
transform(ptSrc.ord, 0, ptDst.ord, 0, 1);
return ptDst;
}
/**
* Transforms a list of coordinate point ordinal values. The default implementation
* invokes {@link #transform(double[],int,double[],int,int)} using a temporary array
* of doubles.
*/
public void transform(final float[] srcPts, final int srcOff,
final float[] dstPts, final int dstOff, final int numPts)
throws TransformException
{
final int dimSource = getDimSource();
final int dimTarget = getDimTarget();
final double[] tmpPts = new double[numPts*Math.max(dimSource, dimTarget)];
for (int i=numPts*dimSource; --i>=0;) {
tmpPts[i] = srcPts[srcOff+i];
}
transform(tmpPts, 0, tmpPts, 0, numPts);
for (int i=numPts*dimTarget; --i>=0;) {
dstPts[dstOff+i] = (float)tmpPts[i];
}
}
/**
* Transform the specified shape. The default implementation compute
* quadratic curves using three points for each shape's segments.
*
* @param shape Shape to transform.
* @return Transformed shape, or shape
if
* this transform is the identity transform.
* @throws IllegalStateException if this transform doesn't map 2D coordinate systems.
* @throws TransformException if a transform failed.
*
* @see MathTransform2D#createTransformedShape(Shape)
*/
public Shape createTransformedShape(final Shape shape) throws TransformException {
return isIdentity() ? shape : createTransformedShape(shape, null, null, ShapeUtilities.PARALLEL);
}
/**
* Transforme une forme géométrique. Cette méthode copie toujours les coordonnées
* transformées dans un nouvel objet. La plupart du temps, elle produira un objet
* {@link GeneralPath}. Elle peut aussi retourner des objets {@link Line2D} ou
* {@link QuadCurve2D} si une telle simplification est possible.
*
* @param shape Forme géométrique à transformer.
* @param preTr Transformation affine à appliquer avant de transformer la forme
* shape
, ou null
pour ne pas en appliquer.
* Cet argument sera surtout utile lors des transformations inverses.
* @param postTr Transformation affine à appliquer après avoir transformée la
* forme shape
, ou null
pour ne pas en appliquer.
* Cet argument sera surtout utile lors des transformations directes.
* @param quadDir Direction des courbes quadratiques ({@link ShapeUtilities#HORIZONTAL}
* ou {@link ShapeUtilities#PARALLEL}).
*
* @return La forme géométrique transformée.
* @throws MismatchedDimensionException if this transform
* doesn't map two-dimensional coordinate systems.
* @throws TransformException Si une transformation a échoué.
*/
final Shape createTransformedShape(final Shape shape,
final AffineTransform preTr,
final AffineTransform postTr,
final int quadDir)
throws TransformException
{
if (getDimSource()!=2 || getDimTarget()!=2) {
throw new MismatchedDimensionException();
}
final PathIterator it = shape.getPathIterator(preTr);
final GeneralPath path = new GeneralPath(it.getWindingRule());
final Point2D.Float ctrl = new Point2D.Float();
final double[] buffer = new double[6];
double ax=0, ay=0; // Coordonnées du dernier point avant la projection.
double px=0, py=0; // Coordonnées du dernier point après la projection.
int indexCtrlPt=0; // Index du point de contrôle dans 'buffer'.
int indexLastPt=0; // Index du dernier point dans 'buffer'.
for (; !it.isDone(); it.next()) {
switch (it.currentSegment(buffer)) {
default: {
throw new IllegalPathStateException();
}
case PathIterator.SEG_CLOSE: {
/*
* Ferme la forme géométrique, puis continue la boucle. On utilise une
* instruction 'continue' plutôt que 'break' car il ne faut pas exécuter
* le code qui suit ce 'switch'.
*/
path.closePath();
continue;
}
case PathIterator.SEG_MOVETO: {
/*
* Mémorise les coordonnées spécifiées (avant et après les avoir
* projetées), puis continue la boucle. On utilise une instruction
* 'continue' plutôt que 'break' car il ne faut pas exécuter le
* code qui suit ce 'switch'.
*/
ax = buffer[0];
ay = buffer[1];
transform(buffer, 0, buffer, 0, 1);
path.moveTo((float) (px=buffer[0]),
(float) (py=buffer[1]));
continue;
}
case PathIterator.SEG_LINETO: {
/*
* Place dans 'buffer[2,3]' les coordonnées
* d'un point qui se trouve sur la droite:
*
* x = 0.5*(x1+x2)
* y = 0.5*(y1+y2)
*
* Ce point sera traité après le 'switch', d'où
* l'utilisation d'un 'break' plutôt que 'continue'.
*/
indexLastPt = 0;
indexCtrlPt = 2;
buffer[2] = 0.5*(ax + (ax=buffer[0]));
buffer[3] = 0.5*(ay + (ay=buffer[1]));
break;
}
case PathIterator.SEG_QUADTO: {
/*
* Place dans 'buffer[0,1]' les coordonnées
* d'un point qui se trouve sur la courbe:
*
* x = 0.5*(ctrlx + 0.5*(x1+x2))
* y = 0.5*(ctrly + 0.5*(y1+y2))
*
* Ce point sera traité après le 'switch', d'où
* l'utilisation d'un 'break' plutôt que 'continue'.
*/
indexLastPt = 2;
indexCtrlPt = 0;
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: {
/*
* Place dans 'buffer[0,1]' les coordonnées
* d'un point qui se trouve sur la courbe:
*
* x = 0.25*(1.5*(ctrlx1+ctrlx2) + 0.5*(x1+x2));
* y = 0.25*(1.5*(ctrly1+ctrly2) + 0.5*(y1+y2));
*
* Ce point sera traité après le 'switch', d'où
* l'utilisation d'un 'break' plutôt que 'continue'.
*
* NOTE: Le point calculé est bien sur la courbe, mais n'est pas
* nécessairement représentatif. Cet algorithme remplace les
* deux points de contrôles par un seul, ce qui se traduit par
* une perte de souplesse qui peut donner de mauvais résultats
* si la courbe cubique était bien tordue. Projeter une courbe
* cubique ne me semble pas être un problème simple, mais ce
* cas devrait être assez rare. Il se produira le plus souvent
* si on essaye de projeter un cercle ou une ellipse, auxquels
* cas l'algorithme actuel donnera quand même des résultats
* tolérables.
*/
indexLastPt = 4;
indexCtrlPt = 0;
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])));
break;
}
}
/*
* Applique la transformation sur les points qui se
* trouve dans le buffer, puis ajoute ces points à
* la forme géométrique projetée comme une courbe
* quadratique.
*/
transform(buffer, 0, buffer, 0, 2);
if (ShapeUtilities.parabolicControlPoint(px, py,
buffer[indexCtrlPt], buffer[indexCtrlPt+1],
buffer[indexLastPt], buffer[indexLastPt+1],
quadDir, ctrl)!=null)
{
path.quadTo(ctrl.x, ctrl.y, (float) (px=buffer[indexLastPt+0]),
(float) (py=buffer[indexLastPt+1]));
} else {
path.lineTo((float) (px=buffer[indexLastPt+0]),
(float) (py=buffer[indexLastPt+1]));
}
}
/*
* La projection de la forme géométrique est terminée. Applique
* une transformation affine si c'était demandée, puis retourne
* une version si possible simplifiée de la forme géométrique.
*/
if (postTr!=null) {
path.transform(postTr);
}
return ShapeUtilities.toPrimitive(path);
}
/**
* Gets the derivative of this transform at a point. The default implementation always
* throw an exception. Subclasses that implement the {@link MathTransform2D} interface
* should override this method. Other subclasses should override
* {@link #derivative(CoordinatePoint)} instead.
*
* @param point The coordinate point where to evaluate the derivative.
* @return The derivative at the specified point as a 2×2 matrix.
* @throws MismatchedDimensionException if the input dimension is not 2.
* @throws TransformException if the derivative can't be evaluated at the specified point.
*
* @see MathTransform2D#derivative(Point2D)
*/
public Matrix derivative(final Point2D point) throws TransformException {
final int dimSource = getDimSource();
if (dimSource != 2) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$2,
new Integer(2), new Integer(dimSource)));
}
throw new TransformException(Errors.format(ErrorKeys.CANT_COMPUTE_DERIVATIVE));
}
/**
* Gets the derivative of this transform at a point. The default implementation
* ensure that point
has a valid dimension. Next, it try to delegate
* the work to an other method:
*
*
null
).
* @throws NullPointerException if the derivative dependents on coordinate
* and point
is null
.
* @throws MismatchedDimensionException if point
doesn't have
* the expected dimension.
* @throws TransformException if the derivative can't be evaluated at the
* specified point.
*/
public Matrix derivative(final CoordinatePoint point) throws TransformException {
final int dimSource = getDimSource();
if (point != null) {
final int dimPoint = point.getDimension();
if (dimPoint != dimSource) {
throw new MismatchedDimensionException(Errors.format(
ErrorKeys.MISMATCHED_DIMENSION_$2,
new Integer(dimPoint), new Integer(dimSource)));
}
if (dimSource == 2) {
return derivative(point.toPoint2D());
}
} else if (dimSource == 2) {
return derivative((Point2D)null);
}
if (this instanceof MathTransform1D) {
return new Matrix(1, 1, new double[] {
((MathTransform1D) this).derivative(point.ord[0])
});
}
throw new TransformException(Errors.format(ErrorKeys.CANT_COMPUTE_DERIVATIVE));
}
/**
* Creates the inverse transform of this object.
* The default implementation returns this
if this transform is an identity
* transform, and throws a {@link NoninvertibleTransformException} otherwise. Subclasses
* should override this method.
*/
public MathTransform inverse() throws NoninvertibleTransformException {
if (isIdentity()) {
return this;
}
throw new NoninvertibleTransformException(
Errors.format(ErrorKeys.NONINVERTIBLE_TRANSFORM));
}
/**
* Concatenates in an optimized way a {@link MathTransform} other
to this
* MathTransform
. A new math transform is created to perform the combined
* transformation. The applyOtherFirst
value determine the transformation
* order as bellow:
*
* applyOtherFirst
is true
, then transforming a point
* p by the combined transform is equivalent to first transforming
* p by other
and then transforming the result by the
* original transform this
.applyOtherFirst
is false
, then transforming a point
* p by the combined transform is equivalent to first transforming
* p by the original transform this
and then transforming
* the result by other
.null
. In the later case, the concatenation will be prepared by
* {@link MathTransformFactory} using a generic {@link ConcatenatedTransform}.
*
* The default implementation always returns null
. This method is ought to be
* overrided by subclasses capable of concatenating some combinaison of transforms in a
* special way. Examples are {@link ExponentialTransform1D} and {@link LogarithmicTransform1D}.
*
* @param other The math transform to apply.
* @param applyOtherFirst true
if the transformation order is other
* followed by this
, or false
if the transformation order is
* this
followed by other
.
* @return The combined math transform, or null
if no optimized combined
* transform is available.
*/
MathTransform concatenate(final MathTransform other, final boolean applyOtherFirst) {
return null;
}
/**
* Returns a hash value for this transform.
*/
public int hashCode() {
return getDimSource() + 37*getDimTarget();
}
/**
* Compares the specified object with this math transform for equality.
* The default implementation checks if object
is an instance
* of the same class than this
. Subclasses should override
* this method in order to compare internal fields.
*/
public boolean equals(final Object object) {
// Do not check 'object==this' here, since this
// optimization is usually done in subclasses.
return (object!=null && getClass().equals(object.getClass()));
}
/**
* Returns a string représentation of this transform.
* Subclasses should override this method in order to
* returns Well Know Text (WKT) instead.
*/
public String toString() {
final StringBuffer buffer=new StringBuffer(Utilities.getShortClassName(this));
buffer.append('[');
final String name = getName(null);
if (name != null) {
buffer.append('"');
buffer.append(name);
buffer.append("\": ");
}
buffer.append(getDimSource());
buffer.append("D \u2192 "); // Arrow -->
buffer.append(getDimTarget());
buffer.append("D]");
return buffer.toString();
}
/**
* Returns a string buffer initialized with "PARAM_MT"
* and a classification name. This is a convenience
* method for WKT formatting.
*/
static StringBuffer paramMT(final String classification) {
final StringBuffer buffer=new StringBuffer("PARAM_MT[\"");
buffer.append(classification);
buffer.append('"');
return buffer;
}
/**
* Add the ", PARAMETER["", ]"
string
* to the specified string buffer. This is a convenience method
* for constructing WKT for "PARAM_MT".
*/
static void addParameter(final StringBuffer buffer, final String key, final double value) {
buffer.append(", PARAMETER[\"");
buffer.append(key);
buffer.append("\",");
buffer.append(value);
buffer.append(']');
}
/**
* Add the ", PARAMETER["", ]"
string
* to the specified string buffer. This is a convenience method
* for constructing WKT for "PARAM_MT".
*/
static void addParameter(final StringBuffer buffer, final String key, final int value) {
buffer.append(", PARAMETER[\"");
buffer.append(key);
buffer.append("\",");
buffer.append(value);
buffer.append(']');
}
/**
* Returns an OpenGIS interface for this math transform.
* The returned object is suitable for RMI use. This method
* look in the cache. If no interface was previously cached,
* then this method create a new adapter and cache the result.
*
* @param adapters The originating {@link Adapters}.
* @return A {@link CT_MathTransform} object. The returned type
* is a generic {@link Object} in order to avoid too early
* class loading of OpenGIS interface.
* @throws RemoteException if the object can't be exported.
*/
final Object cachedOpenGIS(final Object adapters) throws RemoteException {
if (proxy!=null) {
if (proxy instanceof Reference) {
final Object ref = ((Reference) proxy).get();
if (ref!=null) {
return ref;
}
} else {
return proxy;
}
}
throw new UnsupportedOperationException();
}
/**
* Invert the specified matrix in place. If the matrix can't be inverted
* because of a {@link SingularMatrixException}, then the exception is
* wrapped into a {@link NoninvertibleTransformException}.
*/
private static Matrix invert(final Matrix matrix) throws NoninvertibleTransformException {
try {
matrix.invert();
return matrix;
} catch (SingularMatrixException exception) {
NoninvertibleTransformException e = new NoninvertibleTransformException(Errors.format(ErrorKeys.NONINVERTIBLE_TRANSFORM));
e.initCause(exception);
throw e;
}
}
/**
* Default implementation for inverse math transform. This inner class is the inverse
* of the enclosing {@link MathTransform}. It is serializable only if the enclosing
* math transform is also serializable.
*
* @version $Id$
* @author Martin Desruisseaux
*/
protected abstract class Inverse extends AbstractMathTransform implements Serializable {
/**
* Serial number for interoperability with different versions. This serial number is
* especilly important for inner classes, since the default serialVersionUID
* computation will not produce consistent results across implementations of different
* Java compiler. This is because different compilers may generate different names for
* synthetic members used in the implementation of inner classes. See:
*
* http://developer.java.sun.com/developer/bugParade/bugs/4211550.html
*/
private static final long serialVersionUID = -864892964444937416L;
/**
* Construct an inverse math transform.
*/
public Inverse() {
}
/**
* Gets the dimension of input points. The default
* implementation returns the dimension of output
* points of the enclosing math transform.
*/
public int getDimSource() {
return AbstractMathTransform.this.getDimTarget();
}
/**
* Gets the dimension of output points. The default
* implementation returns the dimension of input
* points of the enclosing math transform.
*/
public int getDimTarget() {
return AbstractMathTransform.this.getDimSource();
}
/**
* Gets the derivative of this transform at a point. The default
* implementation compute the inverse of the matrix returned by
* the enclosing math transform.
*/
public Matrix derivative(final Point2D point) throws TransformException {
return invert(AbstractMathTransform.this.derivative(this.transform(point, null)));
}
/**
* Gets the derivative of this transform at a point. The default
* implementation compute the inverse of the matrix returned by
* the enclosing math transform.
*/
public Matrix derivative(final CoordinatePoint point) throws TransformException {
return invert(AbstractMathTransform.this.derivative(this.transform(point, null)));
}
/**
* Returns the inverse of this math transform, which is the enclosing math transform.
* This method is declared final because some implementation assume that the inverse
* of this
is always AbstractMathTransform.this
.
*/
public final MathTransform inverse() {
return AbstractMathTransform.this;
}
/**
* Tests whether this transform does not move any points.
* The default implementation delegate this tests to the
* enclosing math transform.
*/
public boolean isIdentity() {
return AbstractMathTransform.this.isIdentity();
}
/**
* Returns a hash code value for this math transform.
*/
public int hashCode() {
return ~AbstractMathTransform.this.hashCode();
}
/**
* Compares the specified object with this inverse math
* transform for equality. The default implementation tests
* if object
in an instance of the same class
* than this
, and then test their enclosing
* math transforms.
*/
public boolean equals(final Object object) {
if (object==this) {
// Slight optimization
return true;
}
if (object instanceof Inverse) {
final Inverse that = (Inverse) object;
return Utilities.equals(this.inverse(), that.inverse());
} else {
return false;
}
}
/**
* Returns the Well Know Text (WKT)
* for this inverse math transform.
*/
public String toString() {
return "INVERSE_MT["+AbstractMathTransform.this+']';
}
}
/**
* Mimic a GeoAPI interface as a legacy implementation. This method is provided
* as a temporary bridge for using new CRS object with J2D-Renderer for example.
*/
public static MathTransform fromGeoAPI(final org.opengis.referencing.operation.MathTransform mt)
throws UnsupportedImplementationException
{
try {
return MathTransformFactory.getDefault().createFromWKT(mt.toWKT());
} catch (org.opengis.referencing.FactoryException e) {
throw new UnsupportedImplementationException(mt.getClass(), e);
}
}
}