Package org.geotools.referencing.operation.builder

Source Code of org.geotools.referencing.operation.builder.GridToEnvelopeMapper

/*
*    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.builder;

import java.util.Arrays;
import java.awt.image.BufferedImage;
import java.awt.geom.AffineTransform;

import org.opengis.coverage.grid.GridEnvelope;
import org.opengis.geometry.Envelope;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.cs.AxisDirection;
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.datum.PixelInCell;
import org.opengis.referencing.operation.MathTransform;
import org.opengis.referencing.operation.Matrix;

import org.geotools.referencing.operation.matrix.MatrixFactory;
import org.geotools.referencing.operation.transform.ProjectiveTransform;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.util.Utilities;


/**
* A helper class for building <var>n</var>-dimensional {@linkplain AffineTransform
* affine transform} mapping {@linkplain GridEnvelope grid ranges} to {@linkplain Envelope
* envelopes}. The affine transform will be computed automatically from the information
* specified by the {@link #setGridRange setGridRange} and {@link #setEnvelope setEnvelope}
* methods, which are mandatory. All other setter methods are optional hints about the
* affine transform to be created. This builder is convenient when the following conditions
* are meet:
* <p>
* <ul>
*   <li><p>Pixels coordinates (usually (<var>x</var>,<var>y</var>) integer values inside
*       the rectangle specified by the grid range) are expressed in some
*       {@linkplain CoordinateReferenceSystem coordinate reference system} known at compile
*       time. This is often the case. For example the CRS attached to {@link BufferedImage}
*       has always ({@linkplain AxisDirection#COLUMN_POSITIVE column},
*       {@linkplain AxisDirection#ROW_POSITIVE row}) axis, with the origin (0,0) in the upper
*       left corner, and row values increasing down.</p></li>
*
*   <li><p>"Real world" coordinates (inside the envelope) are expressed in arbitrary
*       <em>horizontal</em> coordinate reference system. Axis directions may be
*       ({@linkplain AxisDirection#NORTH North}, {@linkplain AxisDirection#WEST West}),
*       or ({@linkplain AxisDirection#EAST East}, {@linkplain AxisDirection#NORTH North}),
*       <cite>etc.</cite>.</p></li>
* </ul>
* <p>
* In such case (and assuming that the image's CRS has the same characteristics than the
* {@link BufferedImage}'s CRS described above):
* <p>
* <ul>
*   <li><p>{@link #setSwapXY swapXY} shall be set to {@code true} if the "real world" axis
*       order is ({@linkplain AxisDirection#NORTH North}, {@linkplain AxisDirection#EAST East})
*       instead of ({@linkplain AxisDirection#EAST East}, {@linkplain AxisDirection#NORTH North}).
*       This axis swapping is necessary for mapping the ({@linkplain AxisDirection#COLUMN_POSITIVE
*       column}, {@linkplain AxisDirection#ROW_POSITIVE row}) axis order associated to the
*       image CRS.</p></li>
*
*   <li><p>In addition, the "real world" axis directions shall be reversed (by invoking
*       <code>{@linkplain #reverseAxis reverseAxis}(dimension)</code>) if their direction is
*       {@link AxisDirection#WEST WEST} (<var>x</var> axis) or {@link AxisDirection#NORTH NORTH}
*       (<var>y</var> axis), in order to get them oriented toward the {@link AxisDirection#EAST
*       EAST} or {@link AxisDirection#SOUTH SOUTH} direction respectively. The later may seems
*       unatural, but it reflects the fact that row values are increasing down in an
*       {@link BufferedImage}'s CRS.</p></li>
* </ul>
*
* @since 2.3
*
*
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public class GridToEnvelopeMapper {
    /**
     * A bit mask for the {@link #setSwapXY swapXY} property.
     *
     * @see #isAutomatic
     * @see #setAutomatic
     */
    public static final int SWAP_XY = 1;

    /**
     * A bit mask for the {@link #setReverseAxis reverseAxis} property.
     *
     * @see #isAutomatic
     * @see #setAutomatic
     */
    public static final int REVERSE_AXIS = 2;

    /**
     * A combinaison of bit masks telling which property were user-defined.
     *
     * @see #isAutomatic
     * @see #setAutomatic
     */
    private int defined;

    /**
     * The grid range, or {@code null} if not yet specified.
     */
    private GridEnvelope gridRange;

    /**
     * The envelope, or {@code null} if not yet specified.
     */
    private Envelope envelope;

    /**
     * Whatever the {@code gridToCRS} transform will maps pixel center or corner.
     * The default value is {@link PixelInCell#CELL_CENTER}.
     */
    private PixelInCell anchor = PixelInCell.CELL_CENTER;

    /**
     * {@code true} if we should swap the two first axis, {@code false} if we should
     * not swap and {@code null} if this state is not yet determined.
     */
    private Boolean swapXY;

    /**
     * The axis to reverse, or {@code null} if none or not yet determined.
     */
    private boolean[] reverseAxis;

    /**
     * The math transform, or {@code null} if not yet computed.
     */
    private MathTransform transform;

    /**
     * Creates a new instance of {@code GridToEnvelopeMapper}.
     */
    public GridToEnvelopeMapper() {
    }

    /**
     * Creates a new instance for the specified grid range and envelope.
     *
     * @param gridRange The valid coordinate range of a grid coverage.
     * @param userRange The corresponding coordinate range in user coordinate. This envelope must
     *                  contains entirely all pixels, i.e. the envelope's upper left corner must
     *                  coincide with the upper left corner of the first pixel and the envelope's
     *                  lower right corner must coincide with the lower right corner of the last
     *                  pixel.
     *
     * @throws MismatchedDimensionException if the grid range and the envelope doesn't have
     *         consistent dimensions.
     */
    public GridToEnvelopeMapper(final GridEnvelope gridRange, final Envelope userRange)
            throws MismatchedDimensionException
    {
        ensureNonNull("gridRange", gridRange);
        ensureNonNull("userRange", userRange);
        final int gridDim = gridRange.getDimension();
        final int userDim = userRange.getDimension();
        if (userDim != gridDim) {
            throw new MismatchedDimensionException(Errors.format(ErrorKeys.MISMATCHED_DIMENSION_$2,
                        gridDim, userDim));
        }
        this.gridRange = gridRange;
        this.envelope  = userRange;
    }

    /**
     * Makes sure that an argument is non-null.
     */
    private static void ensureNonNull(final String name, final Object object)
            throws IllegalArgumentException
    {
        if (object == null) {
            throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, name));
        }
    }

    /**
     * Makes sure that the specified objects have the same dimension.
     */
    private static void ensureDimensionMatch(final GridEnvelope gridRange,
                                             final Envelope envelope,
                                             final boolean checkingRange)
    {
        if (gridRange != null && envelope != null) {
            final String label;
            final int dim1, dim2;
            if (checkingRange) {
                label = "gridRange";
                dim1  = gridRange.getDimension();
                dim2  = envelope .getDimension();
            } else {
                label = "envelope";
                dim1  = envelope .getDimension();
                dim2  = gridRange.getDimension();
            }
            if (dim1 != dim2) {
                throw new MismatchedDimensionException(Errors.format(
                        ErrorKeys.MISMATCHED_DIMENSION_$3, label, dim1, dim2));
            }
        }
    }

    /**
     * Flush any information cached in this object.
     */
    private void reset() {
        transform = null;
        if (isAutomatic(REVERSE_AXIS)) {
            reverseAxis = null;
        }
        if (isAutomatic(SWAP_XY)) {
            swapXY = null;
        }
    }

    /**
     * Returns whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
     * or {@linkplain PixelInCell#CELL_CORNER pixel corner}.
     *
     * @deprecated Renamed {@link #getPixelAnchor}.
     */
    @Deprecated
    public PixelInCell getGridType() {
        return getPixelAnchor();
    }

    /**
     * Returns whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
     * or {@linkplain PixelInCell#CELL_CORNER pixel corner}. The former is OGC convention while
     * the later is Java2D/JAI convention. The default is cell center (OGC convention).
     *
     * @return Whatever the grid range maps pixel center or corner.
     *
     * @since 2.5
     */
    public PixelInCell getPixelAnchor() {
        return anchor;
    }

    /**
     * Set whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
     * or {@linkplain PixelInCell#CELL_CORNER pixel corner}.
     *
     * @deprecated Renamed {@link #setPixelAnchor}.
     */
    @Deprecated
    public void setGridType(final PixelInCell anchor) {
        setPixelAnchor(anchor);
    }

    /**
     * Sets whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
     * or {@linkplain PixelInCell#CELL_CORNER pixel corner}. The former is OGC convention
     * while the later is Java2D/JAI convention.
     *
     * @param anchor Whatever the grid range maps pixel center or corner.
     *
     * @since 2.5
     */
    public void setPixelAnchor(final PixelInCell anchor) {
        ensureNonNull("anchor", anchor);
        if (!Utilities.equals(this.anchor, anchor)) {
            this.anchor = anchor;
            reset();
        }
    }

    /**
     * Returns the grid range.
     *
     * @return The grid range.
     * @throws IllegalStateException if the grid range has not yet been defined.
     */
    public GridEnvelope getGridRange() throws IllegalStateException {
        if (gridRange == null) {
            throw new IllegalStateException(Errors.format(
                    ErrorKeys.MISSING_PARAMETER_VALUE_$1, "gridRange"));
        }
        return gridRange;
    }

    /**
     * Sets the grid range.
     *
     * @param gridRange The new grid range.
     */
    public void setGridRange(final GridEnvelope gridRange) {
        ensureNonNull("gridRange", gridRange);
        ensureDimensionMatch(gridRange, envelope, true);
        if (!Utilities.equals(this.gridRange, gridRange)) {
            this.gridRange = gridRange;
            reset();
        }
    }

    /**
     * Returns the envelope. For performance reason, this method do not
     * clone the envelope. So the returned object should not be modified.
     *
     * @return The envelope.
     * @throws IllegalStateException if the envelope has not yet been defined.
     */
    public Envelope getEnvelope() throws IllegalStateException {
        if (envelope == null) {
            throw new IllegalStateException(Errors.format(
                    ErrorKeys.MISSING_PARAMETER_VALUE_$1, "envelope"));
        }
        return envelope;
    }

    /**
     * Sets the envelope. This method do not clone the specified envelope,
     * so it should not be modified after this method has been invoked.
     *
     * @param envelope The new envelope.
     */
    public void setEnvelope(final Envelope envelope) {
        ensureNonNull("envelope", envelope);
        ensureDimensionMatch(gridRange, envelope, false);
        if (!Utilities.equals(this.envelope, envelope)) {
            this.envelope = envelope;
            reset();
        }
    }

    /**
     * Applies heuristic rules in order to determine if the two first axis should be interchanged.
     */
    private static boolean swapXY(final CoordinateSystem cs) {
        if (cs != null && cs.getDimension() >= 2) {
            return AxisDirection.NORTH.equals(cs.getAxis(0).getDirection().absolute()) &&
                   AxisDirection.EAST .equals(cs.getAxis(1).getDirection().absolute());
        }
        return false;
    }

    /**
     * Returns {@code true} if the two first axis should be interchanged. If
     * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #SWAP_XY})</code>
     * returns {@code true} (which is the default), then this method make the
     * following assumptions:
     *
     * <ul>
     *   <li><p>Axis order in the grid range matches exactly axis order in the envelope, except
     *       for the special case described in the next point. In other words, if axis order in
     *       the underlying image is (<var>column</var>, <var>row</var>) (which is the case for
     *       a majority of images), then the envelope should probably have a (<var>longitude</var>,
     *       <var>latitude</var>) or (<var>easting</var>, <var>northing</var>) axis order.</p></li>
     *
     *   <li><p>An exception to the above rule applies for CRS using exactly the following axis
     *       order: ({@link AxisDirection#NORTH NORTH}|{@link AxisDirection#SOUTH SOUTH},
     *       {@link AxisDirection#EAST EAST}|{@link AxisDirection#WEST WEST}). An example
     *       of such CRS is {@code EPSG:4326}. In this particular case, this method will
     *       returns {@code true}, thus suggesting to interchange the
     *       (<var>y</var>,<var>x</var>) axis for such CRS.</p></li>
     * </ul>
     *
     * @return {@code true} if the two first axis should be interchanged.
     */
    public boolean getSwapXY() {
        if (swapXY == null) {
            boolean value = false;
            if (isAutomatic(SWAP_XY)) {
                value = swapXY(getCoordinateSystem());
            }
            swapXY = Boolean.valueOf(value);
        }
        return swapXY.booleanValue();
    }

    /**
     * Tells if the two first axis should be interchanged. Invoking this method force
     * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #SWAP_XY})</code> to
     * {@code false}.
     *
     * @param swapXY {@code true} if the two first axis should be interchanged.
     */
    public void setSwapXY(final boolean swapXY) {
        final Boolean newValue = Boolean.valueOf(swapXY);
        if (!newValue.equals(this.swapXY)) {
            reset();
        }
        this.swapXY = newValue;
        defined |= SWAP_XY;
    }

    /**
     * Returns which (if any) axis in <cite>user</cite> space
     * (not grid space) should have their direction reversed. If
     * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #REVERSE_AXIS})</code>
     * returns {@code true} (which is the default), then this method make the
     * following assumptions:
     * <p>
     * <ul>
     *   <li>Axis should be reverted if needed in order to point toward their
     *       "{@linkplain AxisDirection#absolute absolute}" direction.</li>
     *   <li>An exception to the above rule is the second axis in grid space,
     *       which is assumed to be the <var>y</var> axis on output device (usually
     *       the screen). This axis is reversed again in order to match the bottom
     *       direction often used with such devices.</li>
     * </ul>
     *
     * @return The reversal state of each axis, or {@code null} if unspecified.
     */
    public boolean[] getReverseAxis() {
        if (reverseAxis == null) {
            final CoordinateSystem cs = getCoordinateSystem();
            if (cs != null) {
                final int dimension = cs.getDimension();
                reverseAxis = new boolean[dimension];
                if (isAutomatic(REVERSE_AXIS)) {
                    for (int i=0; i<dimension; i++) {
                        final AxisDirection direction = cs.getAxis(i).getDirection();
                        final AxisDirection absolute  = direction.absolute();
                        reverseAxis[i] = direction.equals(absolute.opposite());
                    }
                    if (dimension >= 2) {
                        final int i = getSwapXY() ? 0 : 1;
                        reverseAxis[i] = !reverseAxis[i];
                    }
                }
            } else {
                // No coordinate system. Reverse the second axis inconditionnaly
                // (except if there is not enough dimensions).
                int length = 0;
                if (gridRange != null) {
                    length = gridRange.getDimension();
                } else if (envelope != null) {
                    length = envelope.getDimension();
                }
                if (length >= 2) {
                    reverseAxis = new boolean[length];
                    reverseAxis[1] = true;
                }
            }
        }
        return reverseAxis;
    }

    /**
     * Set which (if any) axis in <cite>user</cite> space (not grid space)
     * should have their direction reversed. Invoking this method force
     * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #REVERSE_AXIS})</code>
     * to {@code false}.
     *
     * @param reverse The reversal state of each axis. A {@code null} value means to
     *        reverse no axis.
     */
    public void setReverseAxis(final boolean[] reverse) {
        if (!Arrays.equals(reverseAxis, reverse)) {
            reset();
        }
        this.reverseAxis = reverse;
        defined |= REVERSE_AXIS;
    }

    /**
     * Reverses a single axis in user space. Invoking this methods <var>n</var> time
     * is equivalent to creating a boolean {@code reverse} array of the appropriate length,
     * setting {@code reverse[dimension] = true} for the <var>n</var> axis to be reversed,
     * and invoke <code>{@linkplain #setReverseAxis setReverseAxis}(reverse)</code>.
     *
     * @param dimension The index of the axis to reverse.
     */
    public void reverseAxis(final int dimension) {
        if (reverseAxis == null) {
            final int length;
            if (gridRange != null) {
                length = gridRange.getDimension();
            } else {
                ensureNonNull("envelope", envelope);
                length = envelope.getDimension();
            }
            reverseAxis = new boolean[length];
        }
        if (!reverseAxis[dimension]) {
            reset();
        }
        reverseAxis[dimension] = true;
        defined |= REVERSE_AXIS;
    }

    /**
     * Returns {@code true} if all properties designed by the specified bit mask
     * will be computed automatically.
     *
     * @param mask Any combinaison of {@link #REVERSE_AXIS} or {@link #SWAP_XY}.
     * @return {@code true} if all properties given by the mask will be computed automatically.
     */
    public boolean isAutomatic(final int mask) {
        return (defined & mask) == 0;
    }

    /**
     * Set all properties designed by the specified bit mask as automatic. Their
     * value will be computed automatically by the corresponding methods (e.g.
     * {@link #getReverseAxis}, {@link #getSwapXY}). By default, all properties
     * are automatic.
     *
     * @param mask Any combinaison of {@link #REVERSE_AXIS} or {@link #SWAP_XY}.
     */
    public void setAutomatic(final int mask) {
        defined &= ~mask;
    }

    /**
     * Returns the coordinate system in use with the envelope.
     */
    private CoordinateSystem getCoordinateSystem() {
        if (envelope != null) {
            final CoordinateReferenceSystem crs;
            crs = envelope.getCoordinateReferenceSystem();
            if (crs != null) {
                return crs.getCoordinateSystem();
            }
        }
        return null;
    }

    /**
     * Creates a math transform using the information provided by setter methods.
     *
     * @return The math transform.
     * @throws IllegalStateException if the grid range or the envelope were not set.
     */
    public MathTransform createTransform() throws IllegalStateException {
        if (transform == null) {
            final GridEnvelope gridRange = getGridRange();
            final Envelope     userRange = getEnvelope();
            final boolean      swapXY    = getSwapXY();
            final boolean[]    reverse   = getReverseAxis();
            final PixelInCell  gridType  = getPixelAnchor();
            final int          dimension = gridRange.getDimension();
            /*
             * Setup the multi-dimensional affine transform for use with OpenGIS.
             * According OpenGIS specification, transforms must map pixel center.
             * This is done by adding 0.5 to grid coordinates.
             */
            final double translate;
            if (PixelInCell.CELL_CENTER.equals(gridType)) {
                translate = 0.5;
            } else if (PixelInCell.CELL_CORNER.equals(gridType)) {
                translate = 0.0;
            } else {
                throw new IllegalStateException(Errors.format(ErrorKeys.ILLEGAL_ARGUMENT_$2,
                            "gridType", gridType));
            }
            final Matrix matrix = MatrixFactory.create(dimension + 1);
            for (int i=0; i<dimension; i++) {
                // NOTE: i is a dimension in the 'gridRange' space (source coordinates).
                //       j is a dimension in the 'userRange' space (target coordinates).
                int j = i;
                if (swapXY && j<=1) {
                    j = 1-j;
                }
                double scale = userRange.getSpan(j) / gridRange.getSpan(i);
                double offset;
                if (reverse==null || j>=reverse.length || !reverse[j]) {
                    offset = userRange.getMinimum(j);
                } else {
                    scale  = -scale;
                    offset = userRange.getMaximum(j);
                }
                offset -= scale * (gridRange.getLow(i) - translate);
                matrix.setElement(j, j,         0.0   );
                matrix.setElement(j, i,         scale );
                matrix.setElement(j, dimension, offset);
            }
            transform = ProjectiveTransform.create(matrix);
        }
        return transform;
    }

    /**
     * Returns the math transform as a two-dimensional affine transform.
     *
     * @return The math transform as a two-dimensional affine transform.
     * @throws IllegalStateException if the math transform is not of the appropriate type.
     */
    public AffineTransform createAffineTransform() throws IllegalStateException {
        final MathTransform transform = createTransform();
        if (transform instanceof AffineTransform) {
            return (AffineTransform) transform;
        }
        throw new IllegalStateException(Errors.format(ErrorKeys.NOT_AN_AFFINE_TRANSFORM));
    }
}
TOP

Related Classes of org.geotools.referencing.operation.builder.GridToEnvelopeMapper

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.