/*---------------- FILE HEADER ------------------------------------------ This file is part of deegree. Copyright (C) 2001 by: EXSE, Department of Geography, University of Bonn http://www.giub.uni-bonn.de/exse/ lat/lon GmbH http://www.lat-lon.de It has been implemented within SEAGIS - An OpenSource implementation of OpenGIS specification (C) 2001, Institut de Recherche pour le D�veloppement (http://sourceforge.net/projects/seagis/) SEAGIS Contacts: Surveillance de l'Environnement Assist�e par Satellite Institut de Recherche pour le D�veloppement / US-Espace mailto:seasnet@teledetection.fr 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 Contact: Andreas Poth lat/lon GmbH Aennchenstr. 19 53115 Bonn Germany E-Mail: poth@lat-lon.de Klaus Greve Department of Geography University of Bonn Meckenheimer Allee 166 53115 Bonn Germany E-Mail: klaus.greve@uni-bonn.de ---------------------------------------------------------------------------*/ package org.deegree.model.csct.ct; // OpenGIS dependencies import java.awt.geom.AffineTransform; import java.util.Locale; import java.util.NoSuchElementException; import javax.media.jai.ParameterList; import org.deegree.model.csct.cs.Projection; import org.deegree.model.csct.pt.Matrix; import org.deegree.model.csct.resources.Naming; import org.deegree.model.csct.resources.WeakHashSet; import org.deegree.model.csct.resources.css.ResourceKeys; import org.deegree.model.csct.resources.css.Resources; /** * Creates math transforms. <code>MathTransformFactory</code> is a low level * factory that is used to create {@link MathTransform} objects. Many high * level GIS applications will never need to use a <code>MathTransformFactory</code> * directly; they can use a {@link CoordinateTransformationFactory} instead. * However, the <code>MathTransformFactory</code> class is specified here, * since it can be used directly by applications that wish to transform other * types of coordinates (e.g. color coordinates, or image pixel coordinates). * <br><br> * A math transform is an object that actually does the work of applying * formulae to coordinate values. The math transform does not know or * care how the coordinates relate to positions in the real world. This * lack of semantics makes implementing <code>MathTransformFactory</code> * significantly easier than it would be otherwise. * * For example <code>MathTransformFactory</code> can create affine math * transforms. The affine transform applies a matrix to the coordinates * without knowing how what it is doing relates to the real world. So if * the matrix scales <var>Z</var> values by a factor of 1000, then it could * be converting meters into millimeters, or it could be converting kilometers * into meters. * <br><br> * Because math transforms have low semantic value (but high mathematical * value), programmers who do not have much knowledge of how GIS applications * use coordinate systems, or how those coordinate systems relate to the real * world can implement <code>MathTransformFactory</code>. * * The low semantic content of math transforms also means that they will be * useful in applications that have nothing to do with GIS coordinates. For * example, a math transform could be used to map color coordinates between * different color spaces, such as converting (red, green, blue) colors into * (hue, light, saturation) colors. * <br><br> * Since a math transform does not know what its source and target coordinate * systems mean, it is not necessary or desirable for a math transform object * to keep information on its source and target coordinate systems. * * @version 1.00 * @author OpenGIS (www.opengis.org) * @author Martin Desruisseaux * * @see org.opengis.ct.CT_MathTransformFactory */ public class MathTransformFactory { /** * The default math transform factory. This factory * will be constructed only when first needed. */ private static MathTransformFactory DEFAULT; /** * A pool of math transform. This pool is used in order to * returns instance of existing math transforms when possible. */ static final WeakHashSet pool = new WeakHashSet(); /** * List of registered math transforms. */ private final MathTransformProvider[] providers; /** * Construct a factory using the specified providers. */ public MathTransformFactory( final MathTransformProvider[] providers ) { this.providers = providers.clone(); } /** * Returns the default math transform factory. */ public static synchronized MathTransformFactory getDefault() { if ( DEFAULT == null ) { DEFAULT = new MathTransformFactory( new MathTransformProvider[] { new MercatorProjection.Provider(), new LambertConformalProjection.Provider(), new StereographicProjection.Provider(), // Automatic new StereographicProjection.Provider( true ), // Polar new StereographicProjection.Provider( false ), // Oblique new TransverseMercatorProjection.Provider( false ), // Universal new TransverseMercatorProjection.Provider( true ), // Modified new GeocentricTransform.Provider( false ), // Geographic to Geocentric new GeocentricTransform.Provider( true ) // Geocentric to Geographic } ); for ( int i = DEFAULT.providers.length; --i >= 0; ) { final MathTransformProvider provider = DEFAULT.providers[i]; if ( provider instanceof MapProjection.Provider ) { // Register only projections. Naming.PROJECTIONS.bind( provider.getClassName(), provider.getParameterListDescriptor() ); } } } return DEFAULT; } /** * Creates an identity transform of the specified dimension. * * @param dimension The source and target dimension. * @return The identity transform. */ public MathTransform createIdentityTransform( final int dimension ) { // Affine transform has one more row/column than dimension. return createAffineTransform( new Matrix( dimension + 1 ) ); } /** * Creates an affine transform from a matrix. * * @param matrix The matrix used to define the affine transform. * @return The affine transform. */ public MathTransform2D createAffineTransform( final AffineTransform matrix ) { return (MathTransform2D) pool.intern( new AffineTransform2D( matrix ) ); } /** * Creates an affine transform from a matrix. * * @param matrix The matrix used to define the affine transform. * @return The affine transform. * */ public MathTransform createAffineTransform( final Matrix matrix ) { /* * If the user is requesting a 2D transform, delegate to the * highly optimized java.awt.geom.AffineTransform class. */ if ( matrix.getNumRow() == 3 && matrix.isAffine() ) // Affine transform are square. { return createAffineTransform( matrix.toAffineTransform2D() ); } /* * General case (slower). May not be a real * affine transform. We accept it anyway... */ return (MathTransform) pool.intern( new MatrixTransform( matrix ) ); } /** * Returns the underlying matrix for the specified transform, * or <code>null</code> if the matrix is unavailable. */ private static Matrix getMatrix( final MathTransform transform ) { if ( transform instanceof AffineTransform ) return new Matrix( (AffineTransform) transform ); if ( transform instanceof MatrixTransform ) return ( (MatrixTransform) transform ).getMatrix(); return null; } /** * Tests if one math transform is the inverse of the other. This implementation * can't detect every case. It just detect the case when <code>tr2</code> is an * instance of {@link AbstractMathTransform.Inverse}. */ private static boolean areInverse( final MathTransform tr1, final MathTransform tr2 ) { if ( tr2 instanceof AbstractMathTransform.Inverse ) { return tr1.equals( ( (AbstractMathTransform.Inverse) tr2 ).inverse() ); // TODO: we could make this test more general (just compare with tr2.inverse(), // no matter if it is an instance of AbstractMathTransform.Inverse or not, // and catch the exception if one is thrown). Would it be too expensive to // create inconditionnaly the inverse transform? } return false; } /** * Creates a transform by concatenating two existing transforms. * A concatenated transform acts in the same way as applying two * transforms, one after the other. The dimension of the output * space of the first transform must match the dimension of the * input space in the second transform. If you wish to concatenate * more than two transforms, then you can repeatedly use this method. * * @param tr1 The first transform to apply to points. * @param tr2 The second transform to apply to points. * @return The concatenated transform. * */ public MathTransform createConcatenatedTransform( MathTransform tr1, MathTransform tr2 ) { if ( tr1.isIdentity() ) return tr2; if ( tr2.isIdentity() ) return tr1; /* * If both transforms use matrix, then we can create * a single transform using the concatened matrix. */ final Matrix matrix1 = getMatrix( tr1 ); if ( matrix1 != null ) { final Matrix matrix2 = getMatrix( tr2 ); if ( matrix2 != null ) { // May not be really affine, but work anyway... // This call will detect and optimize the special // case where an 'AffineTransform' can be used. matrix2.mul( matrix1 ); return createAffineTransform( matrix2 ); } } /* * If one transform is the inverse of the * other, returns the identity transform. */ if ( areInverse( tr1, tr2 ) || areInverse( tr2, tr1 ) ) { return createIdentityTransform( tr1.getDimSource() ); } /* * If one or both math transform are instance of {@link ConcatenedTransform}, * then maybe it is possible to efficiently concatenate <code>tr1</code> or * <code>tr2</code> with one of step transforms. Try that... */ if ( tr1 instanceof ConcatenedTransform ) { final ConcatenedTransform ctr = (ConcatenedTransform) tr1; tr1 = ctr.transform1; tr2 = createConcatenatedTransform( ctr.transform2, tr2 ); } if ( tr2 instanceof ConcatenedTransform ) { final ConcatenedTransform ctr = (ConcatenedTransform) tr2; tr1 = createConcatenatedTransform( tr1, ctr.transform1 ); tr2 = ctr.transform2; } /* * The returned transform will implements {@link MathTransform2D} if source and * target dimensions are equal to 2. {@link MathTransform} implementations are * available in two version: direct and non-direct. The "non-direct" version use * an intermediate buffer when performing transformations; they are slower and * consume more memory. They are used only as a fallback when a "direct" version * can't be created. */ final MathTransform transform; final int dimSource = tr1.getDimSource(); final int dimTarget = tr2.getDimTarget(); if ( dimSource == 2 && dimTarget == 2 ) { if ( tr1 instanceof MathTransform2D && tr2 instanceof MathTransform2D ) { transform = new ConcatenedTransformDirect2D( this, (MathTransform2D) tr1, (MathTransform2D) tr2 ); } else transform = new ConcatenedTransform2D( this, tr1, tr2 ); } else if ( dimSource == tr1.getDimTarget() && tr2.getDimSource() == dimTarget ) { transform = new ConcatenedTransformDirect( this, tr1, tr2 ); } else transform = new ConcatenedTransform( this, tr1, tr2 ); return (MathTransform) pool.intern( transform ); } /** * Creates a transform which passes through a subset of ordinates to another transform. * This allows transforms to operate on a subset of ordinates. For example, if you have * (<var>latitidue</var>,<var>longitude</var>,<var>height</var>) coordinates, then you * may wish to convert the height values from feet to meters without affecting the * latitude and longitude values. * * @param firstAffectedOrdinate Index of the first affected ordinate. * @param subTransform The sub transform. * @param numTrailingOrdinates Number of trailing ordinates to pass through. * Affected ordinates will range from <code>firstAffectedOrdinate</code> * inclusive to <code>dimTarget-numTrailingOrdinates</code> exclusive. * @return A pass through transform with the following dimensions:<br> * <pre> * Source: firstAffectedOrdinate + subTransform.getDimSource() + numTrailingOrdinates * Target: firstAffectedOrdinate + subTransform.getDimTarget() + numTrailingOrdinates</pre> * */ public MathTransform createPassThroughTransform( final int firstAffectedOrdinate, final MathTransform subTransform, final int numTrailingOrdinates ) { if ( firstAffectedOrdinate < 0 ) { throw new IllegalArgumentException( Resources.format( ResourceKeys.ERROR_ILLEGAL_ARGUMENT_$2, "firstAffectedOrdinate", new Integer( firstAffectedOrdinate ) ) ); } if ( numTrailingOrdinates < 0 ) { throw new IllegalArgumentException( Resources.format( ResourceKeys.ERROR_ILLEGAL_ARGUMENT_$2, "numTrailingOrdinates", new Integer( numTrailingOrdinates ) ) ); } if ( firstAffectedOrdinate == 0 && numTrailingOrdinates == 0 ) { return subTransform; } // // Optimize the "Identity transform" case. // if ( subTransform.isIdentity() ) { final int dimension = subTransform.getDimSource(); if ( dimension == subTransform.getDimTarget() ) { // The AffineTransform is easier to concatenate with other transforms. return createIdentityTransform( firstAffectedOrdinate + dimension + numTrailingOrdinates ); } } // // Optimize the "Pass through case": this is done // right into PassThroughTransform's constructor. // return (MathTransform) pool.intern( new PassThroughTransform( firstAffectedOrdinate, subTransform, numTrailingOrdinates ) ); } /** * Creates a transform which retains only a portion of an other transform. For example * if the source coordinate system has (<var>longitude</var>, <var>latitude</var>, * <var>height</var>) values, then a sub-transform may be used to keep only the * (<var>longitude</var>, <var>latitude</var>) part. In most cases, the created * sub-transform is non-invertible since it loose informations. * <br><br> * This transform is a special case of a non-square matrix transform with less * rows than columns. However, using a <code>createSubMathTransfom(...)</code> * method makes it easier to optimize some common cases. * * @param transform The transform. * @param lower Index of the first ordinate to keep. * @param upper Index of the first ordinate. Must be greater than <code>lower</code>. */ public MathTransform createSubMathTransform( final int lower, final int upper, final MathTransform transform ) { if ( lower < 0 || lower >= upper ) { throw new IllegalArgumentException( Resources.format( ResourceKeys.ERROR_ILLEGAL_ARGUMENT_$2, "lower", new Integer( lower ) ) ); } final int dimTarget = transform.getDimTarget(); if ( upper > dimTarget ) { throw new IllegalArgumentException( Resources.format( ResourceKeys.ERROR_ILLEGAL_ARGUMENT_$2, "upper", new Integer( upper ) ) ); } if ( lower == 0 && upper == dimTarget ) { return transform; } if ( transform instanceof PassThroughTransform ) { // Special case for pass through transform: // Compute lower and upper values relatives // to the underlying sub-transform. final PassThroughTransform passThrough = (PassThroughTransform) transform; final int lowerTr = lower - passThrough.firstAffectedOrdinate; final int upperTr = upper - passThrough.firstAffectedOrdinate; final int passDim = passThrough.transform.getDimTarget(); if ( lowerTr >= 0 && upperTr <= passDim ) { return createSubMathTransform( lowerTr, upperTr, passThrough.transform ); } if ( lowerTr <= 0 && upperTr >= passDim ) { return createPassThroughTransform( -lowerTr, passThrough.transform, upperTr - passDim ); } } // General case: use a matrix. final int dimOutput = upper - lower; final Matrix matrix = new Matrix( dimOutput + 1, dimTarget + 1 ); matrix.setZero(); for ( int i = lower; i < upper; i++ ) { matrix.setElement( i - lower, i, 1 ); } matrix.setElement( dimOutput, dimTarget, 1 ); // Affine transform has one more row/column than dimension. return createConcatenatedTransform( transform, createAffineTransform( matrix ) ); } /** * Creates a transform from a classification name and parameters. * The client must ensure that all the linear parameters are expressed * in meters, and all the angular parameters are expressed in degrees. * Also, they must supply "semi_major" and "semi_minor" parameters * for cartographic projection transforms. * * @param classification The classification name of the transform * (e.g. "Transverse_Mercator"). Leading and trailing spaces * are ignored, and comparaison is case-insensitive. * @param parameters The parameter values in standard units. * @return The parameterized transform. * @throws NoSuchElementException if there is no transform for the specified classification. * @throws MissingParameterException if a parameter was required but not found. * */ public MathTransform createParameterizedTransform( String classification, final ParameterList parameters ) throws NoSuchElementException, MissingParameterException { final MathTransform transform; classification = classification.trim(); if ( classification.equalsIgnoreCase( "Affine" ) ) { // Special case for "Affine", since the ParameterListDescriptor // depends of the matrix size. transform = MatrixTransform.Provider.staticCreate( parameters ); } else { transform = getMathTransformProvider( classification ).create( parameters ); } return (MathTransform) pool.intern( transform ); } /** * Convenience method for creating a transform from a projection. * * @param projection The projection. * @return The parameterized transform. * @throws NoSuchElementException if there is no transform for the specified projection. * @throws MissingParameterException if a parameter was required but not found. */ public MathTransform createParameterizedTransform( final Projection projection ) throws NoSuchElementException, MissingParameterException { return createParameterizedTransform( projection.getClassName(), projection.getParameters() ); } /** * Returns the classification names of every available transforms. * The returned array may have a zero length, but will never be null. * */ public String[] getAvailableTransforms() { final String[] names = new String[providers.length + 1]; int i; for ( i = 0; i < names.length; i++ ) { names[i] = providers[i].getClassName(); } // Special case for "Affine", since the ParameterListDescriptor // depends of the matrix size. names[i] = "Affine"; return names; } /** * Returns the provider for the specified classification. This provider * may be used to query parameter list for a classification name (e.g. * <code>getMathTransformProvider("Transverse_Mercator").getParameterList()</code>), * or the transform name in a given locale (e.g. * <code>getMathTransformProvider("Transverse_Mercator").getName({@link Locale#FRENCH})</code>) * * @param classification The classification name of the transform * (e.g. "Transverse_Mercator"). It should be one of the name * returned by {@link #getAvailableTransforms}. Leading and * trailing spaces are ignored. Comparisons are case-insensitive. * @return The provider for a math transform. * @throws NoSuchElementException if there is no provider registered * with the specified classification name. */ public MathTransformProvider getMathTransformProvider( String classification ) throws NoSuchElementException { classification = classification.trim(); for ( int i = 0; i < providers.length; i++ ) { if ( classification.equalsIgnoreCase( providers[i].getClassName().trim() ) ) return providers[i]; } throw new NoSuchElementException( Resources.format( ResourceKeys.ERROR_NO_TRANSFORM_FOR_CLASSIFICATION_$1, classification ) ); } /** * Create a provider for affine transforms of the specified * dimension. Created affine transforms will have a size of * <code>numRow × numCol</code>. * <br><br> * <table align="center" border='1' cellpadding='3' bgcolor="F4F8FF"> * <tr bgcolor="#B9DCFF"><th>Parameter</th> <th>Description</th></tr> * <tr><td><code>Num_row</code></td> <td>Number of rows in matrix</td></tr> * <tr><td><code>Num_col</code></td> <td>Number of columns in matrix</td></tr> * <tr><td><code>elt_<r>_<c></code></td> <td>Element of matrix</td></tr> * </table> * <br> * For the element parameters, <code><r></code> and <code><c></code> * should be substituted by printed decimal numbers. The values of <var>r</var> * should be from 0 to <code>(num_row-1)</code>, and the values of <var>c</var> * should be from 0 to <code>(num_col-1)</code>. Any undefined matrix elements * are assumed to be zero for <code>(r!=c)</code>, and one for <code>(r==c)</code>. * This corresponds to the identity transformation when the number of rows and columns * are the same. The number of columns corresponds to one more than the dimension of * the source coordinates and the number of rows corresponds to one more than the * dimension of target coordinates. The extra dimension in the matrix is used to * let the affine map do a translation. * * @param numRow The number of matrix's rows. * @param numCol The number of matrix's columns. * @return The provider for an affine transform. * @throws IllegalArgumentException if <code>numRow</code> * or <code>numCol</code> is not a positive number. */ public MathTransformProvider getAffineTransformProvider( final int numRow, final int numCol ) throws IllegalArgumentException { return new MatrixTransform.Provider( numRow, numCol ); } }