/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-2008, Open Source Geospatial Foundation (OSGeo) * * 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; * version 2.1 of the License. * * 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. */ package org.geotools.referencing.operation.transform; import java.util.Arrays; import org.opengis.referencing.FactoryException; import org.opengis.referencing.operation.Matrix; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.MathTransformFactory; import org.geotools.factory.Hints; import org.geotools.referencing.ReferencingFactoryFinder; import org.geotools.referencing.operation.LinearTransform; import org.geotools.referencing.operation.matrix.XMatrix; import org.geotools.referencing.operation.matrix.MatrixFactory; import org.geotools.referencing.operation.matrix.GeneralMatrix; import org.geotools.resources.XArray; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * An utility class for the separation of {@linkplain ConcatenatedTransform concatenation} of * {@linkplain PassThroughTransform pass through transforms}. Given an arbitrary * {@linkplain MathTransform math transform}, this utility class will returns a new math transform * that operates only of a given set of source dimensions. For example if the supplied * {@code transform} has (x, y, z) inputs and * (longitude, latitude, height) outputs, then * the following code: * *
 * {@linkplain #addSourceDimensionRange addSourceDimensionRange}(0, 2);
 * MathTransform mt = {@linkplain #separate separate}(transform);
 * 
* *

will returns a transform with (x, y) inputs and (probably) * (longitude, latitude) outputs. The later can be verified with * a call to {@link #getTargetDimensions}.

* * @since 2.1 * * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * @author Simone Giannecchini * * @todo This class is specific to Geotools implementation; it is better to avoid it if * you can. It could be generalized a bit if we perform the same operations on * {@link org.opengis.referencing.operation.CoordinateOperation} interfaces instead * of math transforms. We should revisit this issue after grid coverage API has been * revisited (since grid coverage is a user of this class). * * @todo This class contains a set of static methods that could be factored out in * some kind of {@code org.geotools.util.SortedIntegerSet} implementation. */ public class DimensionFilter { /** * Hint key for specifying a particular instance of {@code DimensionFilter} to use. * * @see #getInstance * * @since 2.5 */ public static final Hints.Key INSTANCE = new Hints.Key(DimensionFilter.class); /** * The input dimensions to keep, in strictly increasing order. * This sequence can contains any integers in the range 0 inclusive to * transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()} * exclusive. */ private int[] sourceDimensions; /** * The output dimensions to keep, in strictly increasing order. * This sequence can contains any integers in the range 0 inclusive to * transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()} * exclusive. */ private int[] targetDimensions; /** * The factory for the creation of new math transforms. */ private final MathTransformFactory factory; /** * Constructs a dimension filter with the * {@linkplain ReferencingFactoryFinder#getMathTransformFactory default math transform factory}. */ public DimensionFilter() { this(ReferencingFactoryFinder.getMathTransformFactory(null)); } /** * Constructs a dimension filter with a * {@linkplain ReferencingFactoryFinder#getMathTransformFactory math transform factory * built using the provided hints}. * * @param hints Hints to control the creation of the {@link MathTransformFactory}. */ public DimensionFilter(final Hints hints) { this(ReferencingFactoryFinder.getMathTransformFactory(hints)); } /** * Constructs a dimension filter with the specified factory. * * @param factory The factory for the creation of new math transforms. */ public DimensionFilter(final MathTransformFactory factory) { this.factory = factory; } /** * Creates or returns an existing instance from the given set of hints. If the hints contain * a value for the {@link #INSTANCE} key, this value is returned. Otherwise a new instance is * {@linkplain #DimensionFilter(Hints) created} with the given hints. * * @param hints The hints, or {@code null} if none. * @return An existing or a new {@code DimensionFilter} instance (never {@code null}). * * @see #INSTANCE * * @since 2.5 */ public static DimensionFilter getInstance(final Hints hints) { if (hints != null) { final DimensionFilter candidate = (DimensionFilter) hints.get(INSTANCE); if (candidate != null) { candidate.clear(); return candidate; } } return new DimensionFilter(hints); } /** * Clears any {@linkplain #getSourceDimensions source} and * {@linkplain #getTargetDimensions target dimension} setting. */ public void clear() { sourceDimensions = null; targetDimensions = null; } /** * Add an input dimension to keep. The {@code dimension} applies to the * source dimensions of the transform to be given to * {@linkplain #separate separate}(transform). * The number must be in the range 0 inclusive to * transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()} * exclusive. * * @param dimension The dimension to add. * @throws IllegalArgumentException if {@code dimension} is negative. */ public void addSourceDimension(final int dimension) throws IllegalArgumentException { sourceDimensions = add(sourceDimensions, dimension); } /** * Add input dimensions to keep. The {@code dimensions} apply to the * source dimensions of the transform to be given to * {@linkplain #separate separate}(transform). * All numbers must be in the range 0 inclusive to * transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()} * exclusive. The {@code dimensions} values must be in strictly increasing order. * * @param dimensions The new sequence of dimensions. * @throws IllegalArgumentException if {@code dimensions} contains negative values or * is not a strictly increasing sequence. */ public void addSourceDimensions(final int[] dimensions) throws IllegalArgumentException { sourceDimensions = add(sourceDimensions, dimensions); } /** * Add a range of input dimensions to keep. The {@code lower} and {@code upper} values * apply to the source dimensions of the transform to be given to * {@linkplain #separate separate}(transform). * * @param lower The lower dimension, inclusive. Must not be smaller than 0. * @param upper The upper dimension, exclusive. Must not be greater than * transform.{@linkplain MathTransform#getSourceDimensions getSourceDimensions()}. */ public void addSourceDimensionRange(final int lower, final int upper) throws IllegalArgumentException { sourceDimensions = add(sourceDimensions, lower, upper); } /** * Returns the input dimensions. This information is available only if at least one * setter method has been explicitly invoked for source dimensions. * * @return The input dimension as a sequence of strictly increasing values. * @throws IllegalStateException if input dimensions have not been set. */ public int[] getSourceDimensions() throws IllegalStateException { if (sourceDimensions != null) { return sourceDimensions.clone(); } throw new IllegalStateException(); } /** * Add an output dimension to keep. The {@code dimension} applies to the * target dimensions of the transform to be given to * {@linkplain #separate separate}(transform). * The number must be in the range 0 inclusive to * transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()} * exclusive. * * @param dimension The dimension to add. * @throws IllegalArgumentException if {@code dimension} is negative. */ public void addTargetDimension(final int dimension) throws IllegalArgumentException { targetDimensions = add(targetDimensions, dimension); } /** * Add output dimensions to keep. The {@code dimensions} apply to the * target dimensions of the transform to be given to * {@linkplain #separate separate}(transform). * All numbers must be in the range 0 inclusive to * transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()} * exclusive. The {@code dimensions} values must be in strictly increasing order. * * @param dimensions The new sequence of dimensions. * @throws IllegalArgumentException if {@code dimensions} contains negative values or * is not a strictly increasing sequence. */ public void addTargetDimensions(int[] dimensions) throws IllegalArgumentException { targetDimensions = add(targetDimensions, dimensions); } /** * Add a range of output dimensions to keep. The {@code lower} and {@code upper} values * apply to the target dimensions of the transform to be given to * {@linkplain #separate separate}(transform). * * @param lower The lower dimension, inclusive. Must not be smaller than 0. * @param upper The upper dimension, exclusive. Must not be greater than * transform.{@linkplain MathTransform#getTargetDimensions getTargetDimensions()}. */ public void addTargetDimensionRange(final int lower, final int upper) throws IllegalArgumentException { targetDimensions = add(targetDimensions, lower, upper); } /** * Returns the output dimensions. This information is available only if one of the following * conditions is meet: *

*

* * @return The output dimension as a sequence of strictly increasing values. * @throws IllegalStateException if this information is not available. */ public int[] getTargetDimensions() throws IllegalStateException { if (targetDimensions != null) { return targetDimensions.clone(); } throw new IllegalStateException(); } /** * Separates the specified math transform. This method returns a math transform that uses * only the specified {@linkplain #getSourceDimensions source dimensions} and returns only * the specified {@linkplain #getTargetDimensions target dimensions}. Special case: *

*

* * @param transform The transform to separate. * @return The separated math transform. * @throws FactoryException if the transform can't be separated. */ public MathTransform separate(MathTransform transform) throws FactoryException { if (sourceDimensions == null) { sourceDimensions = series(0, transform.getSourceDimensions()); if (targetDimensions == null) { targetDimensions = series(0, transform.getTargetDimensions()); return transform; } return separateOutput(transform); } final int[] target = targetDimensions; transform = separateInput(transform); assert XArray.isStrictlySorted(targetDimensions); if (target != null) { final int[] step = targetDimensions; targetDimensions = new int[target.length]; for (int i=0; i dimSource) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "sourceDimensions", upper-1)); } /* * Check for easiest cases: same transform, identity transform or concatenated transforms. */ if (dimInput == dimSource) { assert lower==0 && upper==dimSource; targetDimensions = series(0, dimTarget); return transform; } if (transform.isIdentity()) { targetDimensions = sourceDimensions; return factory.createAffineTransform(MatrixFactory.create(dimInput+1)); } if (transform instanceof ConcatenatedTransform) { final ConcatenatedTransform ctr = (ConcatenatedTransform) transform; final int[] original = sourceDimensions; final MathTransform step1, step2; step1 = separateInput(ctr.transform1); sourceDimensions = targetDimensions; step2 = separateInput(ctr.transform2); sourceDimensions = original; return factory.createConcatenatedTransform(step1, step2); } /* * Special case for the pass through transform: if at least one input dimension * belong to the passthrough's sub-transform, then delegates part of the work to * {@code subTransform(passThrough.transform, ...)} */ if (transform instanceof PassThroughTransform) { final PassThroughTransform passThrough = (PassThroughTransform) transform; final int dimPass = passThrough.subTransform.getSourceDimensions(); final int dimDiff = passThrough.subTransform.getTargetDimensions() - dimPass; final int subLower = passThrough.firstAffectedOrdinate; final int subUpper = subLower + dimPass; final DimensionFilter subFilter = new DimensionFilter(factory); for (int i=0; i=subLower && n= subUpper) { n += dimDiff; } targetDimensions = add(targetDimensions, n); } } if (subFilter.sourceDimensions == null) { /* * No source dimensions belong to the sub-transform. The only remaining * sources are heading and trailing dimensions. A passthrough transform * without its sub-transform is an identity transform... */ return factory.createAffineTransform(MatrixFactory.create(dimInput+1)); } /* * There is at least one dimension to separate in the sub-transform. Performs this * separation and gets the list of output dimensions. We need to offset the output * dimensions by the amount of leading dimensions once the separation is done, in * order to translate from the sub-transform's dimension numbering to the transform's * numbering. */ final MathTransform subTransform = subFilter.separateInput(passThrough.subTransform); for (int i=0; ilongitude, latitude, height) outputs, then a sub-transform * may be used to keep only the (longitude, latitude) part. In most cases, * the created sub-transform is non-invertible since it loose informations. *

* This transform may be see as a non-square matrix transform with less rows * than columns, concatenated with {@code transform}. However, invoking * {@code createFilterTransfom(...)} allows the optimization of some common cases. * * @param transform The transform to reduces. * @return The {@code transform} keeping only the output dimensions. * @throws FactoryException if the transform can't be created. */ private MathTransform separateOutput(MathTransform transform) throws FactoryException { final int dimSource = transform.getSourceDimensions(); final int dimTarget = transform.getTargetDimensions(); final int dimOutput = targetDimensions.length; final int lower = targetDimensions[0]; final int upper = targetDimensions[dimOutput-1]; assert XArray.isStrictlySorted(targetDimensions); if (upper > dimTarget) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "targetDimensions", upper)); } if (dimOutput == dimTarget) { assert lower==0 && upper==dimTarget; return transform; } /* * If the transform is an instance of "pass through" transform but no dimension from its * subtransform is requested, then ignore the subtransform (i.e. treat the whole transform * as identity, except for the number of output dimension which may be different from the * number of input dimension). */ int dimPass = 0; int dimDiff = 0; int dimStep = dimTarget; if (transform instanceof PassThroughTransform) { final PassThroughTransform passThrough = (PassThroughTransform) transform; final int subLower = passThrough.firstAffectedOrdinate; final int subUpper = subLower + passThrough.subTransform.getTargetDimensions(); if (!containsAny(targetDimensions, subLower, subUpper)) { transform = null; dimStep = dimSource; dimPass = subLower; dimDiff = (subLower + passThrough.subTransform.getSourceDimensions()) - subUpper; } } /* * Creates the matrix to be used as a filter, [x'] [1 0 0 0] [x] * and concatenates it to the transform. The [z'] = [0 0 1 0] [y] * matrix will contains only a 1 for the output [1 ] [0 0 0 1] [z] * dimension to keep, as in the following example: [1] */ final XMatrix matrix = MatrixFactory.create(dimOutput+1, dimStep+1); matrix.setZero(); for (int j=0; j= dimPass) { i += dimDiff; } matrix.setElement(j, i, 1); } // Affine transform has one more row/column than dimension. matrix.setElement(dimOutput, dimStep, 1); MathTransform filtered = factory.createAffineTransform(matrix); if (transform != null) { filtered = factory.createConcatenatedTransform(transform, filtered); } return filtered; } /** * Returns {@code true} if the given sequence contains all index in the range {@code lower} * inclusive to {@code upper} exclusive. * * @param sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to test. * @param lower The lower value, inclusive. * @param upper The upper value, exclusive. * @return {@code true} if the full range was found in the sequence. */ private static boolean containsAll(final int[] sequence, final int lower, int upper) { if (lower == upper) { return true; } if (sequence != null) { assert XArray.isStrictlySorted(sequence); int index = Arrays.binarySearch(sequence, lower); if (index >= 0) { index += --upper - lower; if (index>=0 && index= 0) { return true; } index = ~index; // Tild, not minus sign. return index < sequence.length && sequence[index] < upper; } return false; } /** * Adds the specified {@code dimension} to the specified sequence. Values are added * in increasing order. Duplicated values are not added. * * @param sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to update. */ private static int[] add(int[] sequence, int dimension) throws IllegalArgumentException { if (dimension < 0) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "dimension", dimension)); } if (sequence == null) { return new int[] {dimension}; } assert XArray.isStrictlySorted(sequence); int i = Arrays.binarySearch(sequence, dimension); if (i < 0) { i = ~i; // Tild, not the minus sign. sequence = XArray.insert(sequence, i, 1); sequence[i] = dimension; } assert Arrays.binarySearch(sequence, dimension) == i; return sequence; } /** * Adds the specified {@code dimensions} to the specified sequence. Values are added * in increasing order. Duplicated values are not added. * * @param sequence The {@link #sourceDimensions} or {@link #targetDimensions} sequence to update. */ private static int[] add(int[] sequence, final int[] dimensions) throws IllegalArgumentException { if (dimensions.length != 0) { ensureValidSeries(dimensions); if (sequence == null) { sequence = dimensions.clone(); } else { // Note: the following loop is unefficient, but should suffise since this // case should not occurs often and arrays should be small anyway. for (int i=0; i=upper) { throw new IllegalArgumentException(Errors.format( ErrorKeys.ILLEGAL_ARGUMENT_$2, "lower", lower)); } if (sequence == null) { sequence = series(lower, upper); } else { // Note: the following loop is unefficient, but should suffise since this // case should not occurs often and arrays should be small anyway. while (lower < upper) { sequence = add(sequence, lower++); } } assert containsAll(sequence, lower, upper); return sequence; } /** * Returns a series of increasing values starting at {@code lower}. */ private static int[] series(final int lower, final int upper) throws IllegalArgumentException { final int[] sequence = new int[upper-lower]; for (int i=0; i