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;
010: * version 2.1 of the License.
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.matrix;
018:
019: // J2SE dependencies and extensions
020: import java.awt.geom.AffineTransform;
021: import java.text.ParseException;
022: import java.text.FieldPosition;
023: import java.text.NumberFormat;
024: import java.util.Locale;
025: import java.io.File;
026: import java.io.FileReader;
027: import java.io.BufferedReader;
028: import java.io.IOException;
029: import javax.vecmath.GMatrix;
030:
031: // OpenGIS dependencies
032: import org.opengis.referencing.cs.AxisDirection;
033: import org.opengis.referencing.operation.Matrix;
034: import org.opengis.geometry.Envelope;
035: import org.opengis.geometry.MismatchedDimensionException;
036:
037: // Geotools dependencies
038: import org.geotools.io.LineFormat;
039: import org.geotools.io.ContentFormatException;
040: import org.geotools.resources.XArray;
041: import org.geotools.resources.Utilities;
042: import org.geotools.resources.i18n.Errors;
043: import org.geotools.resources.i18n.ErrorKeys;
044:
045: /**
046: * A two dimensional array of numbers. Row and column numbering begins with zero.
047: *
048: * @since 2.2
049: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/operation/matrix/GeneralMatrix.java $
050: * @version $Id: GeneralMatrix.java 24925 2007-03-27 20:12:08Z jgarnett $
051: * @author Martin Desruisseaux
052: * @author Simone Giannecchini
053: *
054: * @see javax.vecmath.GMatrix
055: * @see java.awt.geom.AffineTransform
056: * @see javax.media.jai.PerspectiveTransform
057: * @see javax.media.j3d.Transform3D
058: * @see <A HREF="http://math.nist.gov/javanumerics/jama/">Jama matrix</A>
059: * @see <A HREF="http://jcp.org/jsr/detail/83.jsp">JSR-83 Multiarray package</A>
060: */
061: public class GeneralMatrix extends GMatrix implements XMatrix {
062: /**
063: * Serial number for interoperability with different versions.
064: */
065: private static final long serialVersionUID = 8447482612423035360L;
066:
067: /**
068: * Defaul tolerance value for floating point comparisons.
069: *
070: * @since 2.4
071: *
072: * @deprecated Doesn't seem to be used...
073: */
074: public static final double EPS = 1E-6;
075:
076: /**
077: * Constructs a square identity matrix of size {@code size} × {@code size}.
078: */
079: public GeneralMatrix(final int size) {
080: super (size, size);
081: }
082:
083: /**
084: * Creates a matrix of size {@code numRow} × {@code numCol}.
085: * Elements on the diagonal <var>j==i</var> are set to 1.
086: */
087: public GeneralMatrix(final int numRow, final int numCol) {
088: super (numRow, numCol);
089: }
090:
091: /**
092: * Constructs a {@code numRow} × {@code numCol} matrix
093: * initialized to the values in the {@code matrix} array. The array values
094: * are copied in one row at a time in row major fashion. The array should be
095: * exactly <code>numRow*numCol</code> in length. Note that because row and column
096: * numbering begins with zero, {@code numRow} and {@code numCol} will be
097: * one larger than the maximum possible matrix index values.
098: */
099: public GeneralMatrix(final int numRow, final int numCol,
100: final double[] matrix) {
101: super (numRow, numCol, matrix);
102: if (numRow * numCol != matrix.length) {
103: throw new IllegalArgumentException(String
104: .valueOf(matrix.length));
105: }
106: }
107:
108: /**
109: * Constructs a new matrix from a two-dimensional array of doubles.
110: *
111: * @param matrix Array of rows. Each row must have the same length.
112: * @throws IllegalArgumentException if the specified matrix is not regular
113: * (i.e. if all rows doesn't have the same length).
114: */
115: public GeneralMatrix(final double[][] matrix)
116: throws IllegalArgumentException {
117: super (matrix.length, (matrix.length != 0) ? matrix[0].length
118: : 0);
119: final int numRow = getNumRow();
120: final int numCol = getNumCol();
121: for (int j = 0; j < numRow; j++) {
122: if (matrix[j].length != numCol) {
123: throw new IllegalArgumentException(Errors
124: .format(ErrorKeys.MATRIX_NOT_REGULAR));
125: }
126: setRow(j, matrix[j]);
127: }
128: }
129:
130: /**
131: * Constructs a new matrix and copies the initial values from the parameter matrix.
132: */
133: public GeneralMatrix(final Matrix matrix) {
134: this (matrix.getNumRow(), matrix.getNumCol());
135: final int height = getNumRow();
136: final int width = getNumCol();
137: for (int j = 0; j < height; j++) {
138: for (int i = 0; i < width; i++) {
139: setElement(j, i, matrix.getElement(j, i));
140: }
141: }
142: }
143:
144: /**
145: * Constructs a new matrix and copies the initial values from the parameter matrix.
146: */
147: public GeneralMatrix(final GMatrix matrix) {
148: super (matrix);
149: }
150:
151: /**
152: * Constructs a 3×3 matrix from the specified affine transform.
153: */
154: public GeneralMatrix(final AffineTransform transform) {
155: super (3, 3, new double[] { transform.getScaleX(),
156: transform.getShearX(), transform.getTranslateX(),
157: transform.getShearY(), transform.getScaleY(),
158: transform.getTranslateY(), 0, 0, 1 });
159: assert isAffine() : this ;
160: }
161:
162: /**
163: * Constructs a transform that maps a source region to a destination region.
164: * Axis order and direction are left unchanged.
165: *
166: * <P>If the source dimension is equals to the destination dimension,
167: * then the transform is affine. However, the following special cases
168: * are also handled:</P>
169: *
170: * <UL>
171: * <LI>If the target dimension is smaller than the source dimension,
172: * then extra dimensions are dropped.</LI>
173: * <LI>If the target dimension is greater than the source dimension,
174: * then the coordinates in the new dimensions are set to 0.</LI>
175: * </UL>
176: *
177: * @param srcRegion The source region.
178: * @param dstRegion The destination region.
179: */
180: public GeneralMatrix(final Envelope srcRegion,
181: final Envelope dstRegion) {
182: super (dstRegion.getDimension() + 1,
183: srcRegion.getDimension() + 1);
184: // Next lines should be first if only Sun could fix RFE #4093999 (sigh...)
185: final int srcDim = srcRegion.getDimension();
186: final int dstDim = dstRegion.getDimension();
187: for (int i = Math.min(srcDim, dstDim); --i >= 0;) {
188: double scale = dstRegion.getLength(i)
189: / srcRegion.getLength(i);
190: double translate = dstRegion.getMinimum(i)
191: - srcRegion.getMinimum(i) * scale;
192: setElement(i, i, scale);
193: setElement(i, srcDim, translate);
194: }
195: setElement(dstDim, srcDim, 1);
196: assert (srcDim != dstDim) || isAffine() : this ;
197: }
198:
199: /**
200: * Constructs a transform changing axis order and/or direction.
201: * For example, the transform may converts (NORTH,WEST) coordinates
202: * into (EAST,NORTH). Axis direction can be inversed only. For example,
203: * it is illegal to transform (NORTH,WEST) coordinates into (NORTH,DOWN).
204: *
205: * <P>If the source dimension is equals to the destination dimension,
206: * then the transform is affine. However, the following special cases
207: * are also handled:</P>
208: * <BR>
209: * <UL>
210: * <LI>If the target dimension is smaller than the source dimension,
211: * extra axis are dropped. An exception is thrown if the target
212: * contains some axis not found in the source.</LI>
213: * </UL>
214: *
215: * @param srcAxis The set of axis direction for source coordinate system.
216: * @param dstAxis The set of axis direction for destination coordinate system.
217: * @throws IllegalArgumentException If {@code dstAxis} contains some axis
218: * not found in {@code srcAxis}, or if some colinear axis were found.
219: */
220: public GeneralMatrix(final AxisDirection[] srcAxis,
221: final AxisDirection[] dstAxis) {
222: this (null, srcAxis, null, dstAxis, false);
223: }
224:
225: /**
226: * Constructs a transform mapping a source region to a destination region.
227: * Axis order and/or direction can be changed during the process.
228: * For example, the transform may convert (NORTH,WEST) coordinates
229: * into (EAST,NORTH). Axis direction can be inversed only. For example,
230: * it is illegal to transform (NORTH,WEST) coordinates into (NORTH,DOWN).
231: *
232: * <P>If the source dimension is equals to the destination dimension,
233: * then the transform is affine. However, the following special cases
234: * are also handled:</P>
235: * <BR>
236: * <UL>
237: * <LI>If the target dimension is smaller than the source dimension,
238: * extra axis are dropped. An exception is thrown if the target
239: * contains some axis not found in the source.</LI>
240: * </UL>
241: *
242: * @param srcRegion The source region.
243: * @param srcAxis Axis direction for each dimension of the source region.
244: * @param dstRegion The destination region.
245: * @param dstAxis Axis direction for each dimension of the destination region.
246: * @throws MismatchedDimensionException if the envelope dimension doesn't
247: * matches the axis direction array length.
248: * @throws IllegalArgumentException If {@code dstAxis} contains some axis
249: * not found in {@code srcAxis}, or if some colinear axis were found.
250: */
251: public GeneralMatrix(final Envelope srcRegion,
252: final AxisDirection[] srcAxis, final Envelope dstRegion,
253: final AxisDirection[] dstAxis) {
254: this (srcRegion, srcAxis, dstRegion, dstAxis, true);
255: }
256:
257: /**
258: * Implementation of constructors expecting envelope and/or axis directions.
259: *
260: * @param validRegions {@code true} if source and destination regions must
261: * be taken in account. If {@code false}, then source and destination
262: * regions will be ignored and may be null.
263: */
264: private GeneralMatrix(final Envelope srcRegion,
265: final AxisDirection[] srcAxis, final Envelope dstRegion,
266: final AxisDirection[] dstAxis, final boolean validRegions) {
267: super (dstAxis.length + 1, srcAxis.length + 1);
268: if (validRegions) {
269: ensureDimensionMatch("srcRegion", srcRegion, srcAxis.length);
270: ensureDimensionMatch("dstRegion", dstRegion, dstAxis.length);
271: }
272: /*
273: * Map source axis to destination axis. If no axis is moved (for example if the user
274: * want to transform (NORTH,EAST) to (SOUTH,EAST)), then source and destination index
275: * will be equal. If some axis are moved (for example if the user want to transform
276: * (NORTH,EAST) to (EAST,NORTH)), then ordinates at index {@code srcIndex} will
277: * have to be moved at index {@code dstIndex}.
278: */
279: setZero();
280: for (int dstIndex = 0; dstIndex < dstAxis.length; dstIndex++) {
281: boolean hasFound = false;
282: final AxisDirection dstAxe = dstAxis[dstIndex];
283: final AxisDirection search = dstAxe.absolute();
284: for (int srcIndex = 0; srcIndex < srcAxis.length; srcIndex++) {
285: final AxisDirection srcAxe = srcAxis[srcIndex];
286: if (search.equals(srcAxe.absolute())) {
287: if (hasFound) {
288: // TODO: Use the localized version of 'getName' in GeoAPI 2.1
289: throw new IllegalArgumentException(Errors
290: .format(ErrorKeys.COLINEAR_AXIS_$2,
291: srcAxe.name(), dstAxe.name()));
292: }
293: hasFound = true;
294: /*
295: * Set the matrix elements. Some matrix elements will never
296: * be set. They will be left to zero, which is their wanted
297: * value.
298: */
299: final boolean normal = srcAxe.equals(dstAxe);
300: double scale = (normal) ? +1 : -1;
301: double translate = 0;
302: if (validRegions) {
303: translate = (normal) ? dstRegion
304: .getMinimum(dstIndex) : dstRegion
305: .getMaximum(dstIndex);
306: scale *= dstRegion.getLength(dstIndex)
307: / srcRegion.getLength(srcIndex);
308: translate -= srcRegion.getMinimum(srcIndex)
309: * scale;
310: }
311: setElement(dstIndex, srcIndex, scale);
312: setElement(dstIndex, srcAxis.length, translate);
313: }
314: }
315: if (!hasFound) {
316: // TODO: Use the localized version of 'getName' in GeoAPI 2.1
317: throw new IllegalArgumentException(Errors.format(
318: ErrorKeys.NO_SOURCE_AXIS_$1, dstAxis[dstIndex]
319: .name()));
320: }
321: }
322: setElement(dstAxis.length, srcAxis.length, 1);
323: assert (srcAxis.length != dstAxis.length) || isAffine() : this ;
324: }
325:
326: /**
327: * Convenience method for checking object dimension validity.
328: * This method is usually invoked for argument checking.
329: *
330: * @param name The name of the argument to check.
331: * @param envelope The envelope to check.
332: * @param dimension The expected dimension for the object.
333: * @throws MismatchedDimensionException if the envelope doesn't have the expected dimension.
334: */
335: private static void ensureDimensionMatch(final String name,
336: final Envelope envelope, final int dimension)
337: throws MismatchedDimensionException {
338: final int dim = envelope.getDimension();
339: if (dimension != dim) {
340: throw new MismatchedDimensionException(Errors.format(
341: ErrorKeys.MISMATCHED_DIMENSION_$3, name,
342: new Integer(dim), new Integer(dimension)));
343: }
344: }
345:
346: /**
347: * Retrieves the specifiable values in the transformation matrix into a
348: * 2-dimensional array of double precision values. The values are stored
349: * into the 2-dimensional array using the row index as the first subscript
350: * and the column index as the second. Values are copied; changes to the
351: * returned array will not change this matrix.
352: */
353: public static double[][] getElements(final Matrix matrix) {
354: if (matrix instanceof GeneralMatrix) {
355: return ((GeneralMatrix) matrix).getElements();
356: }
357: final int numCol = matrix.getNumCol();
358: final double[][] rows = new double[matrix.getNumRow()][];
359: for (int j = 0; j < rows.length; j++) {
360: final double[] row;
361: rows[j] = row = new double[numCol];
362: for (int i = 0; i < row.length; i++) {
363: row[i] = matrix.getElement(j, i);
364: }
365: }
366: return rows;
367: }
368:
369: /**
370: * Retrieves the specifiable values in the transformation matrix into a
371: * 2-dimensional array of double precision values. The values are stored
372: * into the 2-dimensional array using the row index as the first subscript
373: * and the column index as the second. Values are copied; changes to the
374: * returned array will not change this matrix.
375: */
376: public final double[][] getElements() {
377: final int numCol = getNumCol();
378: final double[][] rows = new double[getNumRow()][];
379: for (int j = 0; j < rows.length; j++) {
380: getRow(j, rows[j] = new double[numCol]);
381: }
382: return rows;
383: }
384:
385: /**
386: * {@inheritDoc}
387: */
388: public final boolean isAffine() {
389: int dimension = getNumRow();
390: if (dimension != getNumCol()) {
391: return false;
392: }
393: dimension--;
394: for (int i = 0; i <= dimension; i++) {
395: if (getElement(dimension, i) != (i == dimension ? 1 : 0)) {
396: return false;
397: }
398: }
399: return true;
400: }
401:
402: /**
403: * Returns {@code true} if this matrix is an identity matrix.
404: */
405: public final boolean isIdentity() {
406: final int numRow = getNumRow();
407: final int numCol = getNumCol();
408: if (numRow != numCol) {
409: return false;
410: }
411: for (int j = 0; j < numRow; j++) {
412: for (int i = 0; i < numCol; i++) {
413: if (getElement(j, i) != (i == j ? 1 : 0)) {
414: return false;
415: }
416: }
417: }
418: assert isAffine() : this ;
419: assert isIdentity(0) : this ;
420: return true;
421: }
422:
423: /**
424: * {@inheritDoc}
425: *
426: * @since 2.3.1
427: */
428: public final boolean isIdentity(double tolerance) {
429: return isIdentity(this , tolerance);
430: }
431:
432: /**
433: * Returns {@code true} if the matrix is an identity matrix using the provided tolerance.
434: */
435: static boolean isIdentity(final Matrix matrix, double tolerance) {
436: tolerance = Math.abs(tolerance);
437: final int numRow = matrix.getNumRow();
438: final int numCol = matrix.getNumCol();
439: if (numRow != numCol) {
440: return false;
441: }
442: for (int j = 0; j < numRow; j++) {
443: for (int i = 0; i < numCol; i++) {
444: double e = matrix.getElement(j, i);
445: if (i == j) {
446: e--;
447: }
448: if (!(Math.abs(e) <= tolerance)) { // Uses '!' in order to catch NaN values.
449: return false;
450: }
451: }
452: }
453: // Note: we can't assert matrix.isAffine().
454: return true;
455: }
456:
457: /**
458: * {@inheritDoc}
459: */
460: public final void multiply(final Matrix matrix) {
461: final GMatrix m;
462: if (matrix instanceof GMatrix) {
463: m = (GMatrix) matrix;
464: } else {
465: m = new GeneralMatrix(matrix);
466: }
467: mul(m);
468: }
469:
470: /**
471: * Returns an affine transform for this matrix.
472: * This is a convenience method for interoperability with Java2D.
473: *
474: * @throws IllegalStateException if this matrix is not 3×3,
475: * or if the last row is not {@code [0 0 1]}.
476: */
477: public final AffineTransform toAffineTransform2D()
478: throws IllegalStateException {
479: int check;
480: if ((check = getNumRow()) != 3 || (check = getNumCol()) != 3) {
481: throw new IllegalStateException(Errors.format(
482: ErrorKeys.NOT_TWO_DIMENSIONAL_$1, new Integer(
483: check - 1)));
484: }
485: if (isAffine()) {
486: return new AffineTransform(getElement(0, 0), getElement(1,
487: 0), getElement(0, 1), getElement(1, 1), getElement(
488: 0, 2), getElement(1, 2));
489: }
490: throw new IllegalStateException(Errors
491: .format(ErrorKeys.NOT_AN_AFFINE_TRANSFORM));
492: }
493:
494: /**
495: * Loads data from the specified file until the first blank line or end of file.
496: *
497: * @param file The file to read.
498: * @return The matrix parsed from the file.
499: * @throws IOException if an error occured while reading the file.
500: *
501: * @since 2.2
502: */
503: public static GeneralMatrix load(final File file)
504: throws IOException {
505: final BufferedReader in = new BufferedReader(new FileReader(
506: file));
507: try {
508: return load(in, Locale.US);
509: } finally {
510: in.close();
511: }
512: }
513:
514: /**
515: * Loads data from the specified streal until the first blank line or end of stream.
516: *
517: * @param in The stream to read.
518: * @param locale The locale for the numbers to be parsed.
519: * @return The matrix parsed from the stream.
520: * @throws IOException if an error occured while reading the stream.
521: *
522: * @since 2.2
523: */
524: public static GeneralMatrix load(final BufferedReader in,
525: final Locale locale) throws IOException {
526: final LineFormat parser = new LineFormat(locale);
527: double[] data = null;
528: double[] row = null;
529: int numRow = 0;
530: int numData = 0;
531: String line;
532: while ((line = in.readLine()) != null) {
533: if ((line = line.trim()).length() == 0) {
534: if (numRow == 0) {
535: continue;
536: } else {
537: break;
538: }
539: }
540: try {
541: parser.setLine(line);
542: row = parser.getValues(row);
543: } catch (ParseException exception) {
544: throw new ContentFormatException(exception
545: .getLocalizedMessage(), exception);
546: }
547: final int upper = numData + row.length;
548: if (data == null) {
549: // Assumes a square matrix.
550: data = new double[numData * numData];
551: }
552: if (upper > data.length) {
553: data = XArray.resize(data, upper * 2);
554: }
555: System.arraycopy(row, 0, data, numData, row.length);
556: numData = upper;
557: numRow++;
558: assert numData % numRow == 0 : numData;
559: }
560: data = (data != null) ? XArray.resize(data, numData)
561: : new double[0];
562: return new GeneralMatrix(numRow, numData / numRow, data);
563: }
564:
565: /**
566: * Returns a string representation of this matrix. The returned string is implementation
567: * dependent. It is usually provided for debugging purposes only.
568: */
569: public String toString() {
570: return toString(this );
571: }
572:
573: /**
574: * Returns a string representation of the specified matrix. The returned string is
575: * implementation dependent. It is usually provided for debugging purposes only.
576: */
577: static String toString(final Matrix matrix) {
578: final int numRow = matrix.getNumRow();
579: final int numCol = matrix.getNumCol();
580: StringBuffer buffer = new StringBuffer();
581: final int columnWidth = 12;
582: final String lineSeparator = System.getProperty(
583: "line.separator", "\n");
584: final FieldPosition dummy = new FieldPosition(0);
585: final NumberFormat format = NumberFormat.getNumberInstance();
586: format.setGroupingUsed(false);
587: format.setMinimumFractionDigits(6);
588: format.setMaximumFractionDigits(6);
589: for (int j = 0; j < numRow; j++) {
590: for (int i = 0; i < numCol; i++) {
591: final int position = buffer.length();
592: buffer = format.format(matrix.getElement(j, i), buffer,
593: dummy);
594: final int spaces = Math.max(columnWidth
595: - (buffer.length() - position), 1);
596: buffer.insert(position, Utilities.spaces(spaces));
597: }
598: buffer.append(lineSeparator);
599: }
600: return buffer.toString();
601: }
602: }
|