001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2003-2006, Geotools Project Managment Committee (PMC)
005: * (C) 2001, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation; either
010: * version 2.1 of the License, or (at your option) any later version.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.referencing.operation.builder;
018:
019: // J2SE dependencies
020: import java.util.Arrays;
021: import java.awt.image.BufferedImage; // For javadoc
022: import java.awt.geom.AffineTransform;
023:
024: // OpenGIS dependencies
025: import org.opengis.coverage.grid.GridRange;
026: import org.opengis.geometry.Envelope;
027: import org.opengis.geometry.MismatchedDimensionException;
028: import org.opengis.referencing.crs.CoordinateReferenceSystem;
029: import org.opengis.referencing.cs.AxisDirection;
030: import org.opengis.referencing.cs.CoordinateSystem;
031: import org.opengis.referencing.datum.PixelInCell;
032: import org.opengis.referencing.operation.MathTransform;
033: import org.opengis.referencing.operation.Matrix;
034:
035: // Geotools dependencies
036: import org.geotools.referencing.operation.matrix.MatrixFactory;
037: import org.geotools.referencing.operation.transform.ProjectiveTransform;
038: import org.geotools.resources.Utilities;
039: import org.geotools.resources.i18n.ErrorKeys;
040: import org.geotools.resources.i18n.Errors;
041:
042: /**
043: * A helper class for building <var>n</var>-dimensional {@linkplain AffineTransform
044: * affine transform} mapping {@linkplain GridRange grid ranges} to {@linkplain Envelope
045: * envelopes}. The affine transform will be computed automatically from the information
046: * specified by the {@link #setGridRange setGridRange} and {@link #setEnvelope setEnvelope}
047: * methods, which are mandatory. All other setter methods are optional hints about the
048: * affine transform to be created. This builder is convenient when the following conditions
049: * are meet:
050: * <p>
051: * <ul>
052: * <li><p>Pixels coordinates (usually (<var>x</var>,<var>y</var>) integer values inside
053: * the rectangle specified by the grid range) are expressed in some
054: * {@linkplain CoordinateReferenceSystem coordinate reference system} known at compile
055: * time. This is often the case. For example the CRS attached to {@link BufferedImage}
056: * has always ({@linkplain AxisDirection#COLUMN_POSITIVE column},
057: * {@linkplain AxisDirection#ROW_POSITIVE row}) axis, with the origin (0,0) in the upper
058: * left corner, and row values increasing down.</p></li>
059: *
060: * <li><p>"Real world" coordinates (inside the envelope) are expressed in arbitrary
061: * <em>horizontal</em> coordinate reference system. Axis directions may be
062: * ({@linkplain AxisDirection#NORTH North}, {@linkplain AxisDirection#WEST West}),
063: * or ({@linkplain AxisDirection#EAST East}, {@linkplain AxisDirection#NORTH North}),
064: * <cite>etc.</cite>.</p></li>
065: * </ul>
066: * <p>
067: * In such case (and assuming that the image's CRS has the same characteristics than the
068: * {@link BufferedImage}'s CRS described above):
069: * <p>
070: * <ul>
071: * <li><p>{@link #setSwapXY swapXY} shall be set to {@code true} if the "real world" axis
072: * order is ({@linkplain AxisDirection#NORTH North}, {@linkplain AxisDirection#EAST East})
073: * instead of ({@linkplain AxisDirection#EAST East}, {@linkplain AxisDirection#NORTH North}).
074: * This axis swapping is necessary for mapping the ({@linkplain AxisDirection#COLUMN_POSITIVE
075: * column}, {@linkplain AxisDirection#ROW_POSITIVE row}) axis order associated to the
076: * image CRS.</p></li>
077: *
078: * <li><p>In addition, the "real world" axis directions shall be reversed (by invoking
079: * <code>{@linkplain #reverseAxis reverseAxis}(dimension)</code>) if their direction is
080: * {@link AxisDirection#WEST WEST} (<var>x</var> axis) or {@link AxisDirection#NORTH NORTH}
081: * (<var>y</var> axis), in order to get them oriented toward the {@link AxisDirection#EAST
082: * EAST} or {@link AxisDirection#SOUTH SOUTH} direction respectively. The later may seems
083: * unatural, but it reflects the fact that row values are increasing down in an
084: * {@link BufferedImage}'s CRS.</p></li>
085: * </ul>
086: *
087: * @since 2.3
088: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/operation/builder/GridToEnvelopeMapper.java $
089: * @version $Id: GridToEnvelopeMapper.java 25778 2007-06-08 08:46:34Z desruisseaux $
090: * @author Martin Desruisseaux
091: */
092: public class GridToEnvelopeMapper {
093: /**
094: * A bit mask for the {@link #setSwapXY swapXY} property.
095: *
096: * @see #isAutomatic
097: * @see #setAutomatic
098: */
099: public static final int SWAP_XY = 1;
100:
101: /**
102: * A bit mask for the {@link #setReverseAxis reverseAxis} property.
103: *
104: * @see #isAutomatic
105: * @see #setAutomatic
106: */
107: public static final int REVERSE_AXIS = 2;
108:
109: /**
110: * A combinaison of bit masks telling which property were user-defined.
111: *
112: * @see #isAutomatic
113: * @see #setAutomatic
114: */
115: private int defined;
116:
117: /**
118: * The grid range, or {@code null} if not yet specified.
119: */
120: private GridRange gridRange;
121:
122: /**
123: * The envelope, or {@code null} if not yet specified.
124: */
125: private Envelope envelope;
126:
127: /**
128: * The grid type. The default value is {@link PixelInCell#CELL_CENTER}.
129: */
130: private PixelInCell gridType = PixelInCell.CELL_CENTER;
131:
132: /**
133: * {@code true} if we should swap the two first axis, {@code false} if we should
134: * not swap and {@code null} if this state is not yet determined.
135: */
136: private Boolean swapXY;
137:
138: /**
139: * The axis to reverse, or {@code null} if none or not yet determined.
140: */
141: private boolean[] reverseAxis;
142:
143: /**
144: * The math transform, or {@code null} if not yet computed.
145: */
146: private MathTransform transform;
147:
148: /**
149: * Creates a new instance of {@code GridToEnvelopeMapper}.
150: */
151: public GridToEnvelopeMapper() {
152: }
153:
154: /**
155: * Creates a new instance for the specified grid range and envelope.
156: *
157: * @param gridRange The valid coordinate range of a grid coverage.
158: * @param userRange The corresponding coordinate range in user coordinate. This envelope must
159: * contains entirely all pixels, i.e. the envelope's upper left corner must
160: * coincide with the upper left corner of the first pixel and the envelope's
161: * lower right corner must coincide with the lower right corner of the last
162: * pixel.
163: *
164: * @throws MismatchedDimensionException if the grid range and the envelope doesn't have
165: * consistent dimensions.
166: */
167: public GridToEnvelopeMapper(final GridRange gridRange,
168: final Envelope userRange)
169: throws MismatchedDimensionException {
170: ensureNonNull("gridRange", gridRange);
171: ensureNonNull("userRange", userRange);
172: final int gridDim = gridRange.getDimension();
173: final int userDim = userRange.getDimension();
174: if (userDim != gridDim) {
175: throw new MismatchedDimensionException(Errors.format(
176: ErrorKeys.MISMATCHED_DIMENSION_$2, new Integer(
177: gridDim), new Integer(userDim)));
178: }
179: this .gridRange = gridRange;
180: this .envelope = userRange;
181: }
182:
183: /**
184: * Makes sure that an argument is non-null.
185: */
186: private static void ensureNonNull(final String name,
187: final Object object) throws IllegalArgumentException {
188: if (object == null) {
189: throw new IllegalArgumentException(Errors.format(
190: ErrorKeys.NULL_ARGUMENT_$1, name));
191: }
192: }
193:
194: /**
195: * Makes sure that the specified objects have the same dimension.
196: */
197: private static void ensureDimensionMatch(final GridRange gridRange,
198: final Envelope envelope, final boolean checkingRange) {
199: if (gridRange != null && envelope != null) {
200: final String label;
201: final int dim1, dim2;
202: if (checkingRange) {
203: label = "gridRange";
204: dim1 = gridRange.getDimension();
205: dim2 = envelope.getDimension();
206: } else {
207: label = "envelope";
208: dim1 = envelope.getDimension();
209: dim2 = gridRange.getDimension();
210: }
211: if (dim1 != dim2) {
212: throw new MismatchedDimensionException(Errors.format(
213: ErrorKeys.MISMATCHED_DIMENSION_$3, label,
214: new Integer(dim1), new Integer(dim2)));
215: }
216: }
217: }
218:
219: /**
220: * Flush any information cached in this object.
221: */
222: private void reset() {
223: transform = null;
224: if (isAutomatic(REVERSE_AXIS)) {
225: reverseAxis = null;
226: }
227: if (isAutomatic(SWAP_XY)) {
228: swapXY = null;
229: }
230: }
231:
232: /**
233: * Returns whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
234: * or {@linkplain PixelInCell#CELL_CORNER pixel corner}.
235: */
236: public PixelInCell getGridType() {
237: return gridType;
238: }
239:
240: /**
241: * Set whatever the grid range maps {@linkplain PixelInCell#CELL_CENTER pixel center}
242: * or {@linkplain PixelInCell#CELL_CORNER pixel corner}.
243: */
244: public void setGridType(final PixelInCell gridType) {
245: ensureNonNull("gridType", gridType);
246: if (!Utilities.equals(this .gridType, gridType)) {
247: this .gridType = gridType;
248: reset();
249: }
250: }
251:
252: /**
253: * Returns the grid range.
254: *
255: * @throws IllegalStateException if the grid range has not yet been defined.
256: */
257: public GridRange getGridRange() throws IllegalStateException {
258: if (gridRange == null) {
259: throw new IllegalStateException(Errors.format(
260: ErrorKeys.MISSING_PARAMETER_VALUE_$1, "gridRange"));
261: }
262: return gridRange;
263: }
264:
265: /**
266: * Set the grid range.
267: */
268: public void setGridRange(final GridRange gridRange) {
269: ensureNonNull("gridRange", gridRange);
270: ensureDimensionMatch(gridRange, envelope, true);
271: if (!Utilities.equals(this .gridRange, gridRange)) {
272: this .gridRange = gridRange;
273: reset();
274: }
275: }
276:
277: /**
278: * Returns the envelope. For performance reason, this method do not
279: * clone the envelope. So the returned object should not be modified.
280: *
281: * @throws IllegalStateException if the envelope has not yet been defined.
282: */
283: public Envelope getEnvelope() throws IllegalStateException {
284: if (envelope == null) {
285: throw new IllegalStateException(Errors.format(
286: ErrorKeys.MISSING_PARAMETER_VALUE_$1, "envelope"));
287: }
288: return envelope;
289: }
290:
291: /**
292: * Set the envelope. This method do not clone the specified envelope,
293: * so it should not be modified after this method has been invoked.
294: */
295: public void setEnvelope(final Envelope envelope) {
296: ensureNonNull("envelope", envelope);
297: ensureDimensionMatch(gridRange, envelope, false);
298: if (!Utilities.equals(this .envelope, envelope)) {
299: this .envelope = envelope;
300: reset();
301: }
302: }
303:
304: /**
305: * Applies heuristic rules in order to determine if the two first axis should be interchanged.
306: *
307: * @deprecated Avoid this method as much as possible. Experience shows that this method is
308: * often used in a context where it should not, for example in order to select
309: * the coefficients to read in an affine transform.
310: */
311: public static boolean swapXY(final CoordinateSystem cs) {
312: if (cs != null && cs.getDimension() >= 2) {
313: return AxisDirection.NORTH.equals(cs.getAxis(0)
314: .getDirection().absolute())
315: && AxisDirection.EAST.equals(cs.getAxis(1)
316: .getDirection().absolute());
317: }
318: return false;
319: }
320:
321: /**
322: * Returns {@code true} if the two first axis should be interchanged. If
323: * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #SWAP_XY})</code>
324: * returns {@code true} (which is the default), then this method make the
325: * following assumptions:
326: *
327: * <ul>
328: * <li><p>Axis order in the grid range matches exactly axis order in the envelope, except
329: * for the special case described in the next point. In other words, if axis order in
330: * the underlying image is (<var>column</var>, <var>row</var>) (which is the case for
331: * a majority of images), then the envelope should probably have a (<var>longitude</var>,
332: * <var>latitude</var>) or (<var>easting</var>, <var>northing</var>) axis order.</p></li>
333: *
334: * <li><p>An exception to the above rule applies for CRS using exactly the following axis
335: * order: ({@link AxisDirection#NORTH NORTH}|{@link AxisDirection#SOUTH SOUTH},
336: * {@link AxisDirection#EAST EAST}|{@link AxisDirection#WEST WEST}). An example
337: * of such CRS is {@code EPSG:4326}. In this particular case, this method will
338: * returns {@code true}, thus suggesting to interchange the
339: * (<var>y</var>,<var>x</var>) axis for such CRS.</p></li>
340: * </ul>
341: */
342: public boolean getSwapXY() {
343: if (swapXY == null) {
344: boolean value = false;
345: if (isAutomatic(SWAP_XY)) {
346: value = swapXY(getCoordinateSystem());
347: }
348: swapXY = Boolean.valueOf(value);
349: }
350: return swapXY.booleanValue();
351: }
352:
353: /**
354: * Tells if the two first axis should be interchanged. Invoking this method force
355: * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #SWAP_XY})</code> to
356: * {@code false}.
357: */
358: public void setSwapXY(final boolean swapXY) {
359: final Boolean newValue = Boolean.valueOf(swapXY);
360: if (!newValue.equals(this .swapXY)) {
361: reset();
362: }
363: this .swapXY = newValue;
364: defined |= SWAP_XY;
365: }
366:
367: /**
368: * Returns which (if any) axis in <cite>user</cite> space
369: * (not grid space) should have their direction reversed. If
370: * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #REVERSE_AXIS})</code>
371: * returns {@code true} (which is the default), then this method make the
372: * following assumptions:
373: * <p>
374: * <ul>
375: * <li>Axis should be reverted if needed in order to point toward their
376: * "{@linkplain AxisDirection#absolute absolute}" direction.</li>
377: * <li>An exception to the above rule is the second axis in grid space,
378: * which is assumed to be the <var>y</var> axis on output device (usually
379: * the screen). This axis is reversed again in order to match the bottom
380: * direction often used with such devices.</li>
381: * </ul>
382: *
383: * @return The reversal state of each axis, or {@code null} if unspecified.
384: * For performance reason, this method do not clone the returned array.
385: */
386: public boolean[] getReverseAxis() {
387: if (reverseAxis == null) {
388: final CoordinateSystem cs = getCoordinateSystem();
389: if (cs != null) {
390: final int dimension = cs.getDimension();
391: reverseAxis = new boolean[dimension];
392: if (isAutomatic(REVERSE_AXIS)) {
393: for (int i = 0; i < dimension; i++) {
394: final AxisDirection direction = cs.getAxis(i)
395: .getDirection();
396: final AxisDirection absolute = direction
397: .absolute();
398: reverseAxis[i] = direction.equals(absolute
399: .opposite());
400: }
401: if (dimension >= 2) {
402: final int i = getSwapXY() ? 0 : 1;
403: reverseAxis[i] = !reverseAxis[i];
404: }
405: }
406: }
407: }
408: return reverseAxis;
409: }
410:
411: /**
412: * Set which (if any) axis in <cite>user</cite> space (not grid space)
413: * should have their direction reversed. Invoking this method force
414: * <code>{@linkplain #isAutomatic isAutomatic}({@linkplain #REVERSE_AXIS})</code>
415: * to {@code false}.
416: *
417: * @param reverse The reversal state of each axis. A {@code null} value means to
418: * reverse no axis. For performance reason, this method do not clone the
419: * supplied array.
420: */
421: public void setReverseAxis(final boolean[] reverse) {
422: if (!Arrays.equals(reverseAxis, reverse)) {
423: reset();
424: }
425: this .reverseAxis = reverse;
426: defined |= REVERSE_AXIS;
427: }
428:
429: /**
430: * Reverses a single axis in user space. Invoking this methods <var>n</var> time
431: * is equivalent to creating a boolean {@code reverse} array of the appropriate length,
432: * setting {@code reverse[dimension] = true} for the <var>n</var> axis to be reversed,
433: * and invoke <code>{@linkplain #setReverseAxis setReverseAxis}(reverse)</code>.
434: */
435: public void reverseAxis(final int dimension) {
436: if (reverseAxis == null) {
437: final int length;
438: if (gridRange != null) {
439: length = gridRange.getDimension();
440: } else {
441: ensureNonNull("envelope", envelope);
442: length = envelope.getDimension();
443: }
444: reverseAxis = new boolean[length];
445: }
446: if (!reverseAxis[dimension]) {
447: reset();
448: }
449: reverseAxis[dimension] = true;
450: defined |= REVERSE_AXIS;
451: }
452:
453: /**
454: * Returns {@code true} if all properties designed by the specified bit mask
455: * will be computed automatically.
456: *
457: * @param mask Any combinaison of {@link #REVERSE_AXIS} or {@link #SWAP_XY}.
458: */
459: public boolean isAutomatic(final int mask) {
460: return (defined & mask) == 0;
461: }
462:
463: /**
464: * Set all properties designed by the specified bit mask as automatic. Their
465: * value will be computed automatically by the corresponding methods (e.g.
466: * {@link #getReverseAxis}, {@link #getSwapXY}). By default, all properties
467: * are automatic.
468: *
469: * @param mask Any combinaison of {@link #REVERSE_AXIS} or {@link #SWAP_XY}.
470: */
471: public void setAutomatic(final int mask) {
472: defined &= ~mask;
473: }
474:
475: /**
476: * Returns the coordinate system in use with the envelope.
477: */
478: private CoordinateSystem getCoordinateSystem() {
479: if (envelope != null) {
480: final CoordinateReferenceSystem crs;
481: crs = envelope.getCoordinateReferenceSystem();
482: if (crs != null) {
483: return crs.getCoordinateSystem();
484: }
485: }
486: return null;
487: }
488:
489: /**
490: * Creates a math transform using the information provided by setter methods.
491: *
492: * @throws IllegalStateException if the grid range or the envelope were not set.
493: */
494: public MathTransform createTransform() throws IllegalStateException {
495: if (transform == null) {
496: final GridRange gridRange = getGridRange();
497: final Envelope userRange = getEnvelope();
498: final boolean swapXY = getSwapXY();
499: final boolean[] reverse = getReverseAxis();
500: final PixelInCell gridType = getGridType();
501: final int dimension = gridRange.getDimension();
502: /*
503: * Setup the multi-dimensional affine transform for use with OpenGIS.
504: * According OpenGIS specification, transforms must map pixel center.
505: * This is done by adding 0.5 to grid coordinates.
506: */
507: final double translate;
508: if (PixelInCell.CELL_CENTER.equals(gridType)) {
509: translate = 0.5;
510: } else if (PixelInCell.CELL_CORNER.equals(gridType)) {
511: translate = 0.0;
512: } else {
513: throw new IllegalStateException(Errors.format(
514: ErrorKeys.ILLEGAL_ARGUMENT_$2, "gridType",
515: gridType));
516: }
517: final Matrix matrix = MatrixFactory.create(dimension + 1);
518: for (int i = 0; i < dimension; i++) {
519: // NOTE: i is a dimension in the 'gridRange' space (source coordinates).
520: // j is a dimension in the 'userRange' space (target coordinates).
521: int j = i;
522: if (swapXY && j <= 1) {
523: j = 1 - j;
524: }
525: double scale = userRange.getLength(j)
526: / gridRange.getLength(i);
527: double offset;
528: if (reverse == null || j >= reverse.length
529: || !reverse[j]) {
530: offset = userRange.getMinimum(j);
531: } else {
532: scale = -scale;
533: offset = userRange.getMaximum(j);
534: }
535: offset -= scale * (gridRange.getLower(i) - translate);
536: matrix.setElement(j, j, 0.0);
537: matrix.setElement(j, i, scale);
538: matrix.setElement(j, dimension, offset);
539: }
540: transform = ProjectiveTransform.create(matrix);
541: }
542: return transform;
543: }
544:
545: /**
546: * Returns the math transform as a two-dimensional affine transform.
547: *
548: * @throws IllegalStateException if the math transform is not of the appropriate type.
549: */
550: public AffineTransform createAffineTransform()
551: throws IllegalStateException {
552: final MathTransform transform = createTransform();
553: if (transform instanceof AffineTransform) {
554: return (AffineTransform) transform;
555: }
556: throw new IllegalStateException(Errors
557: .format(ErrorKeys.NOT_AN_AFFINE_TRANSFORM));
558: }
559: }
|