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.crs;
021:
022: // J2SE dependencies
023: import java.util.Collection;
024: import java.util.Collections;
025: import java.util.Iterator;
026: import java.util.Map;
027: import javax.units.Unit;
028:
029: // OpenGIS dependencies
030: import org.opengis.parameter.GeneralParameterDescriptor;
031: import org.opengis.parameter.GeneralParameterValue;
032: import org.opengis.parameter.ParameterDescriptor;
033: import org.opengis.parameter.ParameterValue;
034: import org.opengis.parameter.ParameterValueGroup;
035: import org.opengis.referencing.crs.ProjectedCRS;
036: import org.opengis.referencing.crs.GeographicCRS;
037: import org.opengis.referencing.crs.CoordinateReferenceSystem;
038: import org.opengis.referencing.cs.AxisDirection;
039: import org.opengis.referencing.cs.CartesianCS;
040: import org.opengis.referencing.cs.CoordinateSystem;
041: import org.opengis.referencing.datum.Ellipsoid;
042: import org.opengis.referencing.datum.GeodeticDatum;
043: import org.opengis.referencing.operation.Matrix;
044: import org.opengis.referencing.operation.Conversion;
045: import org.opengis.referencing.operation.MathTransform;
046: import org.opengis.referencing.operation.OperationMethod;
047: import org.opengis.geometry.MismatchedDimensionException;
048:
049: // Geotools dependencies
050: import org.geotools.referencing.wkt.Formatter;
051: import org.geotools.referencing.AbstractReferenceSystem;
052: import org.geotools.referencing.cs.AbstractCS;
053: import org.geotools.referencing.operation.DefiningConversion; // For javadoc
054: import org.geotools.resources.Utilities;
055:
056: /**
057: * A 2D coordinate reference system used to approximate the shape of the earth on a planar surface.
058: * It is done in such a way that the distortion that is inherent to the approximation is carefully
059: * controlled and known. Distortion correction is commonly applied to calculated bearings and
060: * distances to produce values that are a close match to actual field values.
061: *
062: * <TABLE CELLPADDING='6' BORDER='1'>
063: * <TR BGCOLOR="#EEEEFF"><TH NOWRAP>Used with CS type(s)</TH></TR>
064: * <TR><TD>
065: * {@link CartesianCS Cartesian}
066: * </TD></TR></TABLE>
067: *
068: * @since 2.1
069: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/crs/DefaultProjectedCRS.java $
070: * @version $Id: DefaultProjectedCRS.java 29128 2008-02-07 10:56:41Z desruisseaux $
071: * @author Martin Desruisseaux
072: */
073: public class DefaultProjectedCRS extends AbstractDerivedCRS implements
074: ProjectedCRS {
075: /**
076: * Serial number for interoperability with different versions.
077: */
078: private static final long serialVersionUID = -4502680112031773028L;
079:
080: /**
081: * Name of the {@value} projection parameter, which is handled specially during WKT formatting.
082: */
083: private static final String SEMI_MAJOR = "semi_major",
084: SEMI_MINOR = "semi_minor";
085:
086: /**
087: * Constructs a new projected CRS with the same values than the specified one.
088: * This copy constructor provides a way to wrap an arbitrary implementation into a
089: * Geotools one or a user-defined one (as a subclass), usually in order to leverage
090: * some implementation-specific API. This constructor performs a shallow copy,
091: * i.e. the properties are not cloned.
092: *
093: * @since 2.2
094: */
095: public DefaultProjectedCRS(final ProjectedCRS crs) {
096: super (crs);
097: }
098:
099: /**
100: * Constructs a projected CRS from a name.
101: *
102: * @param name The name.
103: * @param method A description of the {@linkplain Conversion#getMethod method for the
104: * conversion}.
105: * @param base Coordinate reference system to base the derived CRS on.
106: * @param baseToDerived The transform from the base CRS to returned CRS.
107: * @param derivedCS The coordinate system for the derived CRS. The number
108: * of axes must match the target dimension of the transform
109: * {@code baseToDerived}.
110: * @throws MismatchedDimensionException if the source and target dimension of
111: * {@code baseToDeviced} don't match the dimension of {@code base}
112: * and {@code derivedCS} respectively.
113: *
114: * @deprecated Create explicitly a {@link DefiningConversion} instead.
115: */
116: public DefaultProjectedCRS(final String name,
117: final OperationMethod method, final GeographicCRS base,
118: final MathTransform baseToDerived,
119: final CartesianCS derivedCS)
120: throws MismatchedDimensionException {
121: this (Collections.singletonMap(NAME_KEY, name), method, base,
122: baseToDerived, derivedCS);
123: }
124:
125: /**
126: * Constructs a projected CRS from a set of properties. The properties are given unchanged
127: * to the {@linkplain AbstractDerivedCRS#AbstractDerivedCRS(Map, OperationMethod,
128: * CoordinateReferenceSystem, MathTransform, CoordinateSystem) super-class constructor}.
129: *
130: * @param properties Name and other properties to give to the new derived CRS object and to
131: * the underlying {@linkplain org.geotools.referencing.operation.DefaultProjection
132: * projection}.
133: * @param method A description of the {@linkplain Conversion#getMethod method for the
134: * conversion}.
135: * @param base Coordinate reference system to base the derived CRS on.
136: * @param baseToDerived The transform from the base CRS to returned CRS.
137: * @param derivedCS The coordinate system for the derived CRS. The number
138: * of axes must match the target dimension of the transform
139: * {@code baseToDerived}.
140: * @throws MismatchedDimensionException if the source and target dimension of
141: * {@code baseToDeviced} don't match the dimension of {@code base}
142: * and {@code derivedCS} respectively.
143: *
144: * @deprecated Create explicitly a {@link DefiningConversion} instead.
145: */
146: public DefaultProjectedCRS(final Map properties,
147: final OperationMethod method, final GeographicCRS base,
148: final MathTransform baseToDerived,
149: final CartesianCS derivedCS)
150: throws MismatchedDimensionException {
151: super (properties, method, base, baseToDerived, derivedCS);
152: }
153:
154: /**
155: * Constructs a projected CRS from a {@linkplain DefiningConversion defining conversion}.
156: * The properties are given unchanged to the
157: * {@linkplain AbstractReferenceSystem#AbstractReferenceSystem(Map) super-class constructor}.
158: *
159: * @param properties Name and other properties to give to the new projected CRS object.
160: * @param conversionFromBase The {@linkplain DefiningConversion defining conversion}.
161: * @param base Coordinate reference system to base the projected CRS on.
162: * @param baseToDerived The transform from the base CRS to returned CRS.
163: * @param derivedCS The coordinate system for the projected CRS. The number
164: * of axes must match the target dimension of the transform
165: * {@code baseToDerived}.
166: * @throws MismatchedDimensionException if the source and target dimension of
167: * {@code baseToDerived} don't match the dimension of {@code base}
168: * and {@code derivedCS} respectively.
169: */
170: public DefaultProjectedCRS(final Map properties,
171: final Conversion conversionFromBase,
172: final GeographicCRS base,
173: final MathTransform baseToDerived,
174: final CartesianCS derivedCS)
175: throws MismatchedDimensionException {
176: super (properties, conversionFromBase, base, baseToDerived,
177: derivedCS);
178: }
179:
180: /**
181: * Returns a conversion from a source to target projected CRS, if this conversion
182: * is representable as an affine transform. More specifically, if all projection
183: * parameters are identical except the following ones:
184: * <P>
185: * <UL>
186: * <LI>{@link org.geotools.referencing.operation.projection.MapProjection.AbstractProvider#SCALE_FACTOR scale_factor}</LI>
187: * <LI>{@link org.geotools.referencing.operation.projection.MapProjection.AbstractProvider#SEMI_MAJOR semi_major}</LI>
188: * <LI>{@link org.geotools.referencing.operation.projection.MapProjection.AbstractProvider#SEMI_MINOR semi_minor}</LI>
189: * <LI>{@link org.geotools.referencing.operation.projection.MapProjection.AbstractProvider#FALSE_EASTING false_easting}</LI>
190: * <LI>{@link org.geotools.referencing.operation.projection.MapProjection.AbstractProvider#FALSE_NORTHING false_northing}</LI>
191: * </UL>
192: * <P>
193: * Then the conversion between two projected CRS can sometime be represented as a linear
194: * conversion. For example if only false easting/northing differ, then the coordinate conversion
195: * is simply a translation. If no linear conversion has been found between the two CRS, then
196: * this method returns {@code null}.
197: *
198: * @param sourceCRS The source coordinate reference system.
199: * @param targetCRS The target coordinate reference system.
200: * @param errorTolerance Relative error tolerance for considering two parameter values as
201: * equal. This is usually a small number like {@code 1E-10}.
202: * @return The conversion from {@code sourceCRS} to {@code targetCRS} as an
203: * affine transform, or {@code null} if no linear transform has been found.
204: *
205: * @deprecated This method was for {@code DefaultCoordinateOperationFactory} internal
206: * use only, and contains some shortcomming. Avoid direct use.
207: */
208: public static Matrix createLinearConversion(
209: final ProjectedCRS sourceCRS, final ProjectedCRS targetCRS,
210: final double errorTolerance) {
211: /*
212: * Checks if the datum are the same. To be stricter, we could compare the 'baseCRS'
213: * instead. But this is not always needed. For example we don't really care if the
214: * underlying geographic CRS use different axis order or units. What matter are the
215: * axis order and units of the projected CRS.
216: *
217: * Actually, checking for 'baseCRS' causes an infinite loop (until StackOverflowError)
218: * in CoordinateOperationFactory, because it prevents this method to recognize that the
219: * transform between two projected CRS is the identity transform even if their underlying
220: * geographic CRS use different axis order.
221: */
222: if (!equals(sourceCRS.getDatum(), targetCRS.getDatum(), false)) {
223: return null;
224: }
225: // TODO: remove the cast once we will be allowed to compile for J2SE 1.5.
226: final Conversion sourceOp = (Conversion) sourceCRS
227: .getConversionFromBase();
228: final Conversion targetOp = (Conversion) targetCRS
229: .getConversionFromBase();
230: if (!equals(sourceOp.getMethod(), targetOp.getMethod(), false)) {
231: return null;
232: }
233: final ParameterValueGroup sourceGroup = sourceOp
234: .getParameterValues();
235: final ParameterValueGroup targetGroup = targetOp
236: .getParameterValues();
237: if (sourceGroup == null || targetGroup == null) {
238: return null;
239: }
240: final Collection sourceParams = sourceGroup.values();
241: final Collection targetParams = targetGroup.values();
242: final GeneralParameterValue[] sourceArray = (GeneralParameterValue[]) sourceParams
243: .toArray(new GeneralParameterValue[sourceParams.size()]);
244: double scaleX = 1;
245: double scaleY = 1;
246: double oldTX = 0;
247: double oldTY = 0;
248: double newTX = 0;
249: double newTY = 0;
250: search: for (final Iterator it = targetParams.iterator(); it
251: .hasNext();) {
252: final GeneralParameterValue targetParam = (GeneralParameterValue) it
253: .next();
254: final GeneralParameterDescriptor descriptor = targetParam
255: .getDescriptor();
256: final String name = descriptor.getName().getCode();
257: for (int j = 0; j < sourceArray.length; j++) {
258: final GeneralParameterValue sourceParam = sourceArray[j];
259: if (sourceParam == null) {
260: continue;
261: }
262: if (nameMatches(sourceParam.getDescriptor(), name)) {
263: if (sourceParam instanceof ParameterValue
264: && targetParam instanceof ParameterValue) {
265: /*
266: * A pair of parameter values has been found (i.e. parameter with the
267: * same name in the source and destination arrays). Now, search for
268: * map projection parameters that can been factored out in an affine
269: * transform. All other parameters (including non-numeric ones) must
270: * be identical.
271: */
272: final ParameterValue parameter = (ParameterValue) targetParam;
273: final ParameterValue candidate = (ParameterValue) sourceParam;
274: if (Number.class
275: .isAssignableFrom(((ParameterDescriptor) descriptor)
276: .getValueClass())) {
277: final double targetValue;
278: final double sourceValue;
279: final Unit unit = parameter.getUnit();
280: if (unit != null) {
281: targetValue = parameter
282: .doubleValue(unit);
283: sourceValue = candidate
284: .doubleValue(unit);
285: } else {
286: targetValue = parameter.doubleValue();
287: sourceValue = candidate.doubleValue();
288: }
289: if (nameMatches(descriptor, "scale_factor")) {
290: final double scale = targetValue
291: / sourceValue;
292: scaleX *= scale;
293: scaleY *= scale;
294: } else if (nameMatches(descriptor,
295: "semi_major")) {
296: scaleX *= (targetValue / sourceValue);
297: } else if (nameMatches(descriptor,
298: "semi_minor")) {
299: scaleY *= (targetValue / sourceValue);
300: } else if (nameMatches(descriptor,
301: "false_easting")) {
302: oldTX += sourceValue;
303: newTX += targetValue;
304: } else if (nameMatches(descriptor,
305: "false_northing")) {
306: oldTY += sourceValue;
307: newTY += targetValue;
308: } else {
309: double error = (targetValue - sourceValue);
310: if (targetValue != 0)
311: error /= targetValue;
312: if (!(Math.abs(error) <= errorTolerance)) { // '!' for trapping NaN
313: return null;
314: }
315: }
316: } else if (!Utilities.equals(parameter
317: .getValue(), candidate.getValue())) {
318: return null;
319: }
320: } else if (!Utilities.equals(targetParam,
321: sourceParam)) {
322: return null;
323: }
324: /*
325: * End of processing of the pair of matching parameters.
326: * Search for a new pair.
327: */
328: sourceArray[j] = null;
329: continue search;
330: }
331: }
332: /*
333: * End of search in the array of source parameter.
334: * A parameter in the target has no matching parameter in source.
335: */
336: return null;
337: }
338: /*
339: * End of parameter comparaison. Check if there is any parameter in
340: * the source array without a matching parameter in the destination
341: * array.
342: */
343: for (int i = 0; i < sourceArray.length; i++) {
344: if (sourceArray[i] != null) {
345: return null;
346: }
347: }
348: /*
349: * At this stage, we have found exact matching pairs for all parameters,
350: * and the only parameters to differ are the special one representables
351: * in an affine transform. 'scaleX' and 'scaleY' must be identical since
352: * they are actually about semi-major and semi-minor axis length, which
353: * are involved in non-linear calculations.
354: */
355: if (!(Math.abs(scaleX - scaleY) <= errorTolerance)) { // '!' for trapping NaN
356: return null;
357: }
358: /*
359: * Creates the matrix (including axis order changes and unit conversions),
360: * and apply the scale and translation inferred from the "false_easting"
361: * parameter and its friends. We perform the conversion in three conceptual
362: * steps (in the end, everything is bundle in a single matrix):
363: *
364: * 1) remove the old false northing/easting
365: * 2) apply the scale
366: * 3) add the new false northing/easting
367: *
368: * Note that those operation are performed in units of the target CRS.
369: */
370: final double scale = 0.5 * (scaleX + scaleY);
371: final boolean applyScale = (Math.abs(scale - 1) > errorTolerance);
372: final CoordinateSystem sourceCS = sourceCRS
373: .getCoordinateSystem();
374: final CoordinateSystem targetCS = targetCRS
375: .getCoordinateSystem();
376: final Matrix matrix = AbstractCS.swapAndScaleAxis(sourceCS,
377: targetCS);
378: final int sourceDim = sourceCS.getDimension();
379: final int targetDim = targetCS.getDimension();
380: for (int j = 0; j < targetDim; j++) {
381: final AxisDirection axis = targetCS.getAxis(j)
382: .getDirection();
383: final double oldT, newT;
384: if (AxisDirection.EAST.equals(axis)) {
385: oldT = +oldTX;
386: newT = +newTX;
387: } else if (AxisDirection.WEST.equals(axis)) {
388: oldT = -oldTX;
389: newT = -newTX;
390: } else if (AxisDirection.NORTH.equals(axis)) {
391: oldT = +oldTY;
392: newT = +newTY;
393: } else if (AxisDirection.SOUTH.equals(axis)) {
394: oldT = -oldTY;
395: newT = -newTY;
396: } else {
397: continue;
398: }
399: /*
400: * Applies the scale. Usually all elements on the same row are equal to zero,
401: * except one element in a column which depends on the source axis position.
402: * Note that we must multiply the last column (unit offset) as well.
403: */
404: if (applyScale) {
405: for (int i = 0; i <= sourceDim; i++) {
406: matrix.setElement(j, i, matrix.getElement(j, i)
407: * scale);
408: }
409: }
410: /*
411: * Applies the translation. The old value in the matrix is usually 0,
412: * but could be non-zero for some unit conversion, which we keep.
413: */
414: final double delta = newT
415: - (applyScale ? oldT * scale : oldT);
416: if (!(Math.abs(delta) <= errorTolerance)) {
417: matrix.setElement(j, sourceDim, matrix.getElement(j,
418: sourceDim)
419: + delta);
420: }
421: }
422: return matrix;
423: }
424:
425: /**
426: * Returns a hash value for this projected CRS.
427: *
428: * @return The hash code value. This value doesn't need to be the same
429: * in past or future versions of this class.
430: */
431: public int hashCode() {
432: return (int) serialVersionUID ^ super .hashCode();
433: }
434:
435: /**
436: * Format the inner part of a
437: * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well
438: * Known Text</cite> (WKT)</A> element.
439: *
440: * @param formatter The formatter to use.
441: * @return The name of the WKT element type, which is {@code "PROJCS"}.
442: */
443: protected String formatWKT(final Formatter formatter) {
444: final Ellipsoid ellipsoid = ((GeodeticDatum) datum)
445: .getEllipsoid();
446: final Unit unit = getUnit();
447: final Unit linearUnit = formatter.getLinearUnit();
448: final Unit angularUnit = formatter.getAngularUnit();
449: final Unit axisUnit = ellipsoid.getAxisUnit();
450: formatter.setLinearUnit(unit);
451: formatter.setAngularUnit(DefaultGeographicCRS
452: .getAngularUnit(baseCRS.getCoordinateSystem()));
453: formatter.append(baseCRS);
454: formatter.append(conversionFromBase.getMethod());
455: final Collection parameters = conversionFromBase
456: .getParameterValues().values();
457: for (final Iterator it = parameters.iterator(); it.hasNext();) {
458: final GeneralParameterValue param = (GeneralParameterValue) it
459: .next();
460: final GeneralParameterDescriptor desc = param
461: .getDescriptor();
462: String name;
463: if (nameMatches(desc, name = SEMI_MAJOR)
464: || nameMatches(desc, name = SEMI_MINOR)) {
465: /*
466: * Do not format semi-major and semi-minor axis length in most cases, since those
467: * informations are provided in the ellipsoid. An exception occurs if the lengths
468: * are different from the ones declared in the datum.
469: */
470: if (param instanceof ParameterValue) {
471: final double value = ((ParameterValue) param)
472: .doubleValue(axisUnit);
473: final double expected = (name == SEMI_MINOR) ? // using '==' is okay here.
474: ellipsoid.getSemiMinorAxis()
475: : ellipsoid.getSemiMajorAxis();
476: if (value == expected) {
477: continue;
478: }
479: }
480: }
481: formatter.append(param);
482: }
483: formatter.append(unit);
484: final int dimension = coordinateSystem.getDimension();
485: for (int i = 0; i < dimension; i++) {
486: formatter.append(coordinateSystem.getAxis(i));
487: }
488: if (unit == null) {
489: formatter.setInvalidWKT(ProjectedCRS.class);
490: }
491: formatter.setAngularUnit(angularUnit);
492: formatter.setLinearUnit(linearUnit);
493: return "PROJCS";
494: }
495: }
|