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: * This package contains documentation from OpenGIS specifications.
018: * OpenGIS consortium's work is fully acknowledged here.
019: */
020: package org.geotools.referencing.operation;
021:
022: // J2SE dependencies
023: import java.util.Collections;
024: import java.util.HashMap;
025: import java.util.Map;
026:
027: // OpenGIS dependencies
028: import org.opengis.parameter.GeneralParameterDescriptor;
029: import org.opengis.parameter.ParameterDescriptorGroup;
030: import org.opengis.referencing.operation.Matrix;
031: import org.opengis.referencing.operation.Projection;
032: import org.opengis.referencing.operation.MathTransform;
033: import org.opengis.referencing.operation.OperationMethod;
034: import org.opengis.geometry.MismatchedDimensionException;
035: import org.opengis.util.InternationalString;
036:
037: // Geotools dependencies
038: import org.geotools.parameter.Parameters;
039: import org.geotools.parameter.DefaultParameterDescriptorGroup;
040: import org.geotools.referencing.AbstractIdentifiedObject;
041: import org.geotools.referencing.operation.LinearTransform;
042: import org.geotools.referencing.operation.transform.AbstractMathTransform;
043: import org.geotools.referencing.operation.transform.ConcatenatedTransform;
044: import org.geotools.referencing.operation.transform.PassThroughTransform;
045: import org.geotools.referencing.wkt.Formatter;
046: import org.geotools.resources.Utilities;
047: import org.geotools.resources.i18n.Errors;
048: import org.geotools.resources.i18n.ErrorKeys;
049: import org.geotools.resources.i18n.Vocabulary;
050: import org.geotools.resources.i18n.VocabularyKeys;
051:
052: /**
053: * Definition of an algorithm used to perform a coordinate operation. Most operation
054: * methods use a number of operation parameters, although some coordinate conversions
055: * use none. Each coordinate operation using the method assigns values to these parameters.
056: *
057: * @since 2.1
058: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/operation/DefaultOperationMethod.java $
059: * @version $Id: DefaultOperationMethod.java 24925 2007-03-27 20:12:08Z jgarnett $
060: * @author Martin Desruisseaux
061: *
062: * @see DefaultOperation
063: */
064: public class DefaultOperationMethod extends AbstractIdentifiedObject
065: implements OperationMethod {
066: /**
067: * Serial number for interoperability with different versions.
068: */
069: private static final long serialVersionUID = -98032729598205972L;
070:
071: /**
072: * List of localizable properties. To be given to {@link AbstractIdentifiedObject} constructor.
073: */
074: private static final String[] LOCALIZABLES = { FORMULA_KEY };
075:
076: /**
077: * Formula(s) or procedure used by this operation method. This may be a reference to a
078: * publication. Note that the operation method may not be analytic, in which case this
079: * attribute references or contains the procedure, not an analytic formula.
080: */
081: private final InternationalString formula;
082:
083: /**
084: * Number of dimensions in the source CRS of this operation method.
085: */
086: protected final int sourceDimensions;
087:
088: /**
089: * Number of dimensions in the target CRS of this operation method.
090: */
091: protected final int targetDimensions;
092:
093: /**
094: * The set of parameters, or {@code null} if none.
095: */
096: private final ParameterDescriptorGroup parameters;
097:
098: /**
099: * Convenience constructor that creates an operation method from a math transform.
100: * The information provided in the newly created object are approximative, and
101: * usually acceptable only as a fallback when no other information are available.
102: *
103: * @param transform The math transform to describe.
104: */
105: public DefaultOperationMethod(final MathTransform transform) {
106: this (getProperties(transform), transform.getSourceDimensions(),
107: transform.getTargetDimensions(),
108: getDescriptor(transform));
109: }
110:
111: /**
112: * Work around for RFE #4093999 in Sun's bug database
113: * ("Relax constraint on placement of this()/super() call in constructors").
114: */
115: private static Map getProperties(final MathTransform transform) {
116: ensureNonNull("transform", transform);
117: final Map properties;
118: if (transform instanceof AbstractMathTransform) {
119: final AbstractMathTransform mt = (AbstractMathTransform) transform;
120: properties = getProperties(mt.getParameterDescriptors(),
121: null);
122: } else {
123: properties = Collections.singletonMap(NAME_KEY, Vocabulary
124: .format(VocabularyKeys.UNKNOW));
125: }
126: return properties;
127: }
128:
129: /**
130: * Work around for RFE #4093999 in Sun's bug database
131: * ("Relax constraint on placement of this()/super() call in constructors").
132: * This code should have been merged with {@code getProperties} above.
133: */
134: private static ParameterDescriptorGroup getDescriptor(
135: final MathTransform transform) {
136: ParameterDescriptorGroup descriptor = null;
137: if (transform instanceof AbstractMathTransform) {
138: descriptor = ((AbstractMathTransform) transform)
139: .getParameterDescriptors();
140: }
141: return descriptor;
142: }
143:
144: /**
145: * Constructs a new operation method with the same values than the specified one.
146: * This copy constructor provides a way to wrap an arbitrary implementation into a
147: * Geotools one or a user-defined one (as a subclass), usually in order to leverage
148: * some implementation-specific API. This constructor performs a shallow copy,
149: * i.e. the properties are not cloned.
150: */
151: public DefaultOperationMethod(final OperationMethod method) {
152: super (method);
153: formula = method.getFormula();
154: parameters = method.getParameters();
155: sourceDimensions = method.getSourceDimensions();
156: targetDimensions = method.getTargetDimensions();
157: }
158:
159: /**
160: * Constructs a new operation method with the same values than the specified one
161: * except the dimensions.
162: *
163: * @param method The operation method to copy.
164: * @param sourceDimensions Number of dimensions in the source CRS of this operation method.
165: * @param targetDimensions Number of dimensions in the target CRS of this operation method.
166: */
167: public DefaultOperationMethod(final OperationMethod method,
168: final int sourceDimensions, final int targetDimensions) {
169: super (method);
170: this .formula = method.getFormula();
171: this .parameters = method.getParameters();
172: this .sourceDimensions = sourceDimensions;
173: this .targetDimensions = targetDimensions;
174: ensurePositive("sourceDimensions", sourceDimensions);
175: ensurePositive("targetDimensions", targetDimensions);
176: }
177:
178: /**
179: * Utility method used to kludge {@code GeneralParameterDescriptor[]}
180: * into a {@code ParameterDescriptorGroup}.
181: * This is a work around for RFE #4093999 in Sun's bug database
182: * ("Relax constraint on placement of this()/super() call in constructors").
183: */
184: private static ParameterDescriptorGroup toGroup(
185: final Map properties,
186: final GeneralParameterDescriptor[] parameters) {
187: return (parameters == null || parameters.length == 0) ? null
188: : new DefaultParameterDescriptorGroup(properties,
189: parameters);
190: }
191:
192: /**
193: * Constructs an operation method from a set of properties and a descriptor group.
194: * The properties given in argument follow the same rules than for the
195: * {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}.
196: * Additionally, the following properties are understood by this construtor:
197: * <br><br>
198: * <table border='1'>
199: * <tr bgcolor="#CCCCFF" class="TableHeadingColor">
200: * <th nowrap>Property name</th>
201: * <th nowrap>Value type</th>
202: * <th nowrap>Value given to</th>
203: * </tr>
204: * <tr>
205: * <td nowrap> {@link #FORMULA_KEY "formula"} </td>
206: * <td nowrap> {@link String} or {@link InternationalString} </td>
207: * <td nowrap> {@link #getFormula}</td>
208: * </tr>
209: * </table>
210: *
211: * @param properties Set of properties. Should contains at least <code>"name"</code>.
212: * @param sourceDimensions Number of dimensions in the source CRS of this operation method.
213: * @param targetDimensions Number of dimensions in the target CRS of this operation method.
214: * @param parameters The set of parameters, or {@code null} if none.
215: */
216: public DefaultOperationMethod(final Map properties,
217: final int sourceDimensions, final int targetDimensions,
218: final ParameterDescriptorGroup parameters) {
219: this (properties, new HashMap(), sourceDimensions,
220: targetDimensions, parameters);
221: }
222:
223: /**
224: * Work around for RFE #4093999 in Sun's bug database
225: * ("Relax constraint on placement of this()/super() call in constructors").
226: */
227: private DefaultOperationMethod(final Map properties,
228: final Map subProperties, final int sourceDimensions,
229: final int targetDimensions,
230: ParameterDescriptorGroup parameters) {
231: super (properties, subProperties, LOCALIZABLES);
232: formula = (InternationalString) subProperties.get(FORMULA_KEY);
233: // 'parameters' may be null, which is okay. A null value will
234: // make serialization smaller and faster than an empty object.
235: this .parameters = parameters;
236: this .sourceDimensions = sourceDimensions;
237: this .targetDimensions = targetDimensions;
238: ensurePositive("sourceDimensions", sourceDimensions);
239: ensurePositive("targetDimensions", targetDimensions);
240: }
241:
242: /**
243: * Ensure that the specified value is positive.
244: * An {@link IllegalArgumentException} is throws if it is not.
245: *
246: * @param name The parameter name.
247: * @param value The parameter value.
248: * @throws IllegalArgumentException if the specified value is not positive.
249: */
250: private static void ensurePositive(final String name,
251: final int value) throws IllegalArgumentException {
252: if (value < 0) {
253: throw new IllegalArgumentException(Errors.format(
254: ErrorKeys.ILLEGAL_ARGUMENT_$2, name, new Integer(
255: value)));
256: }
257: }
258:
259: /**
260: * Formula(s) or procedure used by this operation method. This may be a reference to a
261: * publication. Note that the operation method may not be analytic, in which case this
262: * attribute references or contains the procedure, not an analytic formula.
263: */
264: public InternationalString getFormula() {
265: return formula;
266: }
267:
268: /**
269: * Number of dimensions in the source CRS of this operation method.
270: *
271: * @return The dimension of source CRS.
272: */
273: public int getSourceDimensions() {
274: return sourceDimensions;
275: }
276:
277: /**
278: * Number of dimensions in the target CRS of this operation method.
279: */
280: public int getTargetDimensions() {
281: return targetDimensions;
282: }
283:
284: /**
285: * Returns the set of parameters.
286: */
287: public ParameterDescriptorGroup getParameters() {
288: return (parameters != null) ? parameters
289: : Parameters.EMPTY_GROUP;
290: }
291:
292: /**
293: * Returns the operation type. Current implementation returns {@code Projection.class} for
294: * proper WKT formatting using an unknow implementation. But the {@link MathTransformProvider}
295: * subclass (with protected access) will overrides this method with a more conservative default
296: * value.
297: */
298: Class getOperationType() {
299: return Projection.class;
300: }
301:
302: /**
303: * Compare this operation method with the specified object for equality.
304: * If {@code compareMetadata} is {@code true}, then all available
305: * properties are compared including {@linkplain #getFormula formula}.
306: *
307: * @param object The object to compare to {@code this}.
308: * @param compareMetadata {@code true} for performing a strict comparaison, or
309: * {@code false} for comparing only properties relevant to transformations.
310: * @return {@code true} if both objects are equal.
311: */
312: public boolean equals(final AbstractIdentifiedObject object,
313: final boolean compareMetadata) {
314: if (object == this ) {
315: return true; // Slight optimization.
316: }
317: if (super .equals(object, compareMetadata)) {
318: final DefaultOperationMethod that = (DefaultOperationMethod) object;
319: if (this .sourceDimensions == that.sourceDimensions
320: && this .targetDimensions == that.targetDimensions
321: && equals(this .parameters, that.parameters,
322: compareMetadata)) {
323: return !compareMetadata
324: || Utilities.equals(this .formula, that.formula);
325: }
326: }
327: return false;
328: }
329:
330: /**
331: * Returns a hash code value for this operation method.
332: */
333: public int hashCode() {
334: int code = (int) serialVersionUID + sourceDimensions + 37
335: * targetDimensions;
336: if (parameters != null) {
337: code = code * 37 + parameters.hashCode();
338: }
339: return code;
340: }
341:
342: /**
343: * Format the inner part of a
344: * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
345: * Known Text</cite> (WKT)</A> element.
346: *
347: * @param formatter The formatter to use.
348: * @return The WKT element name.
349: */
350: protected String formatWKT(final Formatter formatter) {
351: if (Projection.class.isAssignableFrom(getOperationType())) {
352: return "PROJECTION";
353: }
354: return super .formatWKT(formatter);
355: }
356:
357: /**
358: * Returns {@code true} if the specified transform is likely to exists only for axis switch
359: * and/or unit conversions. The heuristic rule checks if the transform is backed by a square
360: * matrix with exactly one non-null value in each row and each column. This method is used
361: * for implementation of the {@link #checkDimensions} method only.
362: */
363: private static boolean isTrivial(final MathTransform transform) {
364: if (transform instanceof LinearTransform) {
365: final Matrix matrix = ((LinearTransform) transform)
366: .getMatrix();
367: final int size = matrix.getNumRow();
368: if (matrix.getNumCol() == size) {
369: for (int j = 0; j < size; j++) {
370: int n1 = 0, n2 = 0;
371: for (int i = 0; i < size; i++) {
372: if (matrix.getElement(j, i) != 0)
373: n1++;
374: if (matrix.getElement(i, j) != 0)
375: n2++;
376: }
377: if (n1 != 1 || n2 != 1) {
378: return false;
379: }
380: }
381: return true;
382: }
383: }
384: return false;
385: }
386:
387: /**
388: * Checks if an operation method and a math transform have a compatible number of source
389: * and target dimensions. In the special case of a {@linkplain PassThroughTransform pass
390: * through transform}, the method's dimensions may be checked against the
391: * {@linkplain PassThroughTransform#getSubTransform sub transform}'s dimensions.
392: * This convenience method is provided for argument checking.
393: *
394: * @param method The operation method to compare to the math transform, or {@code null}.
395: * @param transform The math transform to compare to the operation method, or {@code null}.
396: * @throws MismatchedDimensionException if the number of dimensions are incompatibles.
397: *
398: * @todo The check for {@link ConcatenatedTransform} and {@link PassThroughTransform} works
399: * only for Geotools implementation.
400: */
401: public static void checkDimensions(final OperationMethod method,
402: MathTransform transform)
403: throws MismatchedDimensionException {
404: if (method != null && transform != null) {
405: int actual, expected = method.getSourceDimensions();
406: while ((actual = transform.getSourceDimensions()) > expected) {
407: if (transform instanceof ConcatenatedTransform) {
408: // Ignore axis switch and unit conversions.
409: final ConcatenatedTransform c = (ConcatenatedTransform) transform;
410: if (isTrivial(c.transform1)) {
411: transform = c.transform2;
412: } else if (isTrivial(c.transform2)) {
413: transform = c.transform1;
414: } else {
415: break;
416: }
417: } else if (transform instanceof PassThroughTransform) {
418: transform = ((PassThroughTransform) transform)
419: .getSubTransform();
420: } else {
421: break;
422: }
423: }
424: final String name;
425: if (actual != expected) {
426: name = "sourceDimensions";
427: } else {
428: actual = transform.getTargetDimensions();
429: expected = method.getTargetDimensions();
430: if (actual != expected) {
431: name = "targetDimensions";
432: } else {
433: return;
434: }
435: }
436: throw new IllegalArgumentException(Errors.format(
437: ErrorKeys.MISMATCHED_DIMENSION_$3, name,
438: new Integer(actual), new Integer(expected)));
439: }
440: }
441: }
|