001: /*
002: * Geotools2 - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2006, Geotools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.referencing.operation.builder;
017:
018: // J2SE and extensions
019: import java.util.Map;
020: import java.util.HashMap;
021: import java.util.List;
022: import java.util.ArrayList;
023: import java.util.Collections;
024: import java.util.Iterator;
025: import java.util.Locale;
026: import java.io.Writer;
027: import java.io.IOException;
028: import java.io.StringWriter;
029: import java.text.NumberFormat;
030: import javax.vecmath.MismatchedSizeException;
031:
032: // OpenGIS dependencies
033: import org.opengis.util.InternationalString;
034: import org.opengis.referencing.FactoryException;
035: import org.opengis.referencing.cs.CartesianCS; // For javadoc only
036: import org.opengis.referencing.cs.CoordinateSystem;
037: import org.opengis.referencing.crs.*; // Includes imports used only for javadoc.
038: import org.opengis.referencing.operation.*; // Includes imports used only for javadoc.
039: import org.opengis.referencing.datum.DatumFactory;
040: import org.opengis.metadata.extent.GeographicExtent;
041: import org.opengis.metadata.extent.GeographicBoundingBox;
042: import org.opengis.metadata.quality.EvaluationMethodType;
043: import org.opengis.geometry.DirectPosition;
044: import org.opengis.geometry.MismatchedDimensionException;
045: import org.opengis.geometry.MismatchedReferenceSystemException;
046:
047: // Geotools dependencies
048: import org.geotools.factory.Hints;
049: import org.geotools.io.TableWriter;
050: import org.geotools.math.Statistics;
051: import org.geotools.resources.XMath;
052: import org.geotools.referencing.CRS;
053: import org.geotools.referencing.ReferencingFactoryFinder;
054: import org.geotools.referencing.cs.DefaultCartesianCS;
055: import org.geotools.referencing.operation.DefaultOperationMethod;
056: import org.geotools.referencing.operation.DefaultTransformation;
057: import org.geotools.geometry.GeneralEnvelope;
058: import org.geotools.geometry.GeneralDirectPosition;
059: import org.geotools.metadata.iso.extent.ExtentImpl;
060: import org.geotools.metadata.iso.extent.GeographicBoundingBoxImpl;
061: import org.geotools.metadata.iso.quality.PositionalAccuracyImpl;
062: import org.geotools.metadata.iso.quality.QuantitativeResultImpl;
063: import org.geotools.resources.i18n.VocabularyKeys;
064: import org.geotools.resources.i18n.Vocabulary;
065: import org.geotools.resources.i18n.ErrorKeys;
066: import org.geotools.resources.i18n.Errors;
067: import org.geotools.resources.CRSUtilities;
068: import org.geotools.resources.Utilities;
069:
070: /**
071: * Provides a basic implementation for {@linkplain MathTransform math transform}
072: * builders.
073: *
074: * Math transform builders create {@link MathTransform} objects for transforming
075: * coordinates from a source CRS
076: * ({@linkplain CoordinateReferenceSystem Coordinate Reference System}) to
077: * a target CRS using empirical parameters. Usually, one of those CRS is a
078: * {@linkplain GeographicCRS geographic} or {@linkplain ProjectedCRS projected}
079: * one with a well known relationship to the earth. The other CRS is often an
080: * {@linkplain EngineeringCRS engineering} or {@linkplain ImageCRS image} one
081: * tied to some ship. For example a remote sensing image <em>before</em>
082: * georectification may be referenced by an {@linkplain ImageCRS image CRS}.
083: *
084: * <blockquote><p><font size=-1><strong>Design note:</strong>
085: * It is technically possible to reference such remote sensing images with a
086: * {@linkplain DerivedCRS CRS derived} from the geographic or projected CRS,
087: * where the {@linkplain DerivedCRS#getConversionFromBase conversion from base}
088: * is the math transform {@linkplain #getMathTransform computed by this builder}.
089: * Such approach is advantageous for {@linkplain CoordinateOperationFactory
090: * coordinate operation factory} implementations, since they can determine the
091: * operation just by inspection of the {@link DerivedCRS} instance. However this
092: * is conceptually incorrect since {@link DerivedCRS} can be related to an other
093: * CRS only through {@linkplain Conversion conversions}, which by definition are
094: * accurate up to rounding errors. The operations created by math transform
095: * builders are rather {@linkplain Transformation transformations}, which can't
096: * be used for {@link DerivedCRS} creation.
097: * </font></p></blockquote>
098: *
099: * The math transform from {@linkplain #getSourceCRS source CRS} to {@linkplain
100: * #getTargetCRS target CRS} is calculated by {@code MathTransformBuilder} from
101: * a set of {@linkplain #getMappedPositions mapped positions} in both CRS.
102: * <p>
103: * Subclasses must implement at least the {@link #getMinimumPointCount()} and
104: * {@link #computeMathTransform()} methods.
105: *
106: * @since 2.4
107: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/operation/builder/MathTransformBuilder.java $
108: * @version $Id: MathTransformBuilder.java 28982 2008-01-28 16:27:33Z acuster $
109: * @author Jan Jezek
110: * @author Martin Desruisseaux
111: */
112: public abstract class MathTransformBuilder {
113: /**
114: * The list of mapped positions.
115: */
116: private final List/*<MappedPosition>*/positions = new ArrayList();
117:
118: /**
119: * An unmodifiable view of mapped positions to be returned by {@link #getMappedPositions}.
120: */
121: private final List/*<MappedPosition>*/unmodifiablePositions = Collections
122: .unmodifiableList(positions);
123:
124: /**
125: * Coordinate Reference System of the source and target points,
126: * or {@code null} if unknown.
127: */
128: private CoordinateReferenceSystem sourceCRS, targetCRS;
129:
130: /**
131: * The math transform. Will be computed only when first needed.
132: */
133: private transient MathTransform transform;
134:
135: /**
136: * The transformation. Will be computed only when first needed.
137: */
138: private transient Transformation transformation;
139:
140: /**
141: * The factory to use for creating {@link MathTransform math transform} instances.
142: */
143: protected final MathTransformFactory mtFactory;
144:
145: /**
146: * The CRS factory to use for creating {@link EngineeringCRS} instances.
147: */
148: private final CRSFactory crsFactory;
149:
150: /**
151: * The datum factory to use for creating {@link EngineeringCRS} instances.
152: */
153: private final DatumFactory datumFactory;
154:
155: /**
156: * Creates a builder with the default factories.
157: */
158: public MathTransformBuilder() {
159: this (null);
160: }
161:
162: /**
163: * Creates a builder from the specified hints.
164: */
165: public MathTransformBuilder(final Hints hints) {
166: mtFactory = ReferencingFactoryFinder
167: .getMathTransformFactory(hints);
168: crsFactory = ReferencingFactoryFinder.getCRSFactory(hints);
169: datumFactory = ReferencingFactoryFinder.getDatumFactory(hints);
170: }
171:
172: /**
173: * Returns the name for the {@linkplain #getTransformation transformation} to
174: * be created by this builder.
175: */
176: public String getName() {
177: return Utilities.getShortClassName(this ) + " fit";
178: }
179:
180: /**
181: * Returns the minimum number of points required by this builder. This minimum depends on the
182: * algorithm used. For example {@linkplain AffineTransformBuilder affine transform builders}
183: * require at least 3 points, while {@linkplain SimilarTransformBuilder similar transform
184: * builders} requires only 2 points.
185: */
186: public abstract int getMinimumPointCount();
187:
188: /**
189: * Returns the dimension for {@linkplain #getSourceCRS source} and
190: * {@link #getTargetCRS target} CRS. The default value is 2.
191: */
192: public int getDimension() {
193: return 2;
194: }
195:
196: /**
197: * Returns the list of mapped positions.
198: */
199: public List/*<MappedPosition>*/getMappedPositions() {
200: return unmodifiablePositions;
201: }
202:
203: /**
204: * Set the list of mapped positions.
205: *
206: * @throws MismatchedSizeException if the list doesn't have the expected number of points.
207: * @throws MismatchedDimensionException if some points doesn't have the
208: * {@linkplain #getDimension expected number of dimensions}.
209: * @throws MismatchedReferenceSystemException if CRS is not the same for all points.
210: */
211: public void setMappedPositions(
212: final List/*<MappedPosition>*/positions)
213: throws MismatchedSizeException,
214: MismatchedDimensionException,
215: MismatchedReferenceSystemException {
216: final CoordinateReferenceSystem source, target;
217: source = ensureValid(getPoints(positions, false),
218: "sourcePoints");
219: target = ensureValid(getPoints(positions, true), "targetPoints");
220: /*
221: * Now stores the informations. Note that we set the source and target CRS
222: * only after 'ensureValid' succeed for both CRS.
223: */
224: this .positions.clear();
225: this .positions.addAll(positions);
226: this .sourceCRS = source;
227: this .targetCRS = target;
228: this .transform = null;
229: }
230:
231: /**
232: * Extracts the source or target points from the specified list.
233: *
234: * @param positions The array where to take points from.
235: * @param target {@code false} for extracting source points,
236: * or {@code true} for extracting target points.
237: */
238: private static DirectPosition[] getPoints(
239: List/*<MappedPosition>*/positions, boolean target) {
240: final DirectPosition[] points = new DirectPosition[positions
241: .size()];
242: for (int i = 0; i < points.length; i++) {
243: final MappedPosition mp = (MappedPosition) positions.get(i);
244: points[i] = target ? mp.getTarget() : mp.getSource();
245: }
246: return points;
247: }
248:
249: /**
250: * Set the source or target points. Note: {@link #sourceCRS} or {@link #targetCRS} must be
251: * setup appropriately before this method is invoked.
252: *
253: * @param points The new points to use.
254: * @param target {@code false} for setting the source points,
255: * or {@code true} for setting the target points.
256: *
257: * @throws MismatchedSizeException if the array doesn't have the expected number of points.
258: */
259: private void setPoints(final DirectPosition[] points,
260: final boolean target) throws MismatchedSizeException {
261: transform = null;
262: final boolean add = positions.isEmpty();
263: if (!add && points.length != positions.size()) {
264: throw new MismatchedSizeException(Errors
265: .format(ErrorKeys.MISMATCHED_ARRAY_LENGTH));
266: }
267: final int dimension = getDimension();
268: for (int i = 0; i < points.length; i++) {
269: final MappedPosition mp;
270: if (add) {
271: mp = new MappedPosition(dimension);
272: positions.add(mp);
273: } else {
274: mp = (MappedPosition) positions.get(i);
275: }
276: final DirectPosition point = points[i];
277: if (target) {
278: mp.setTarget(point);
279: } else {
280: mp.setSource(point);
281: }
282: }
283: }
284:
285: /**
286: * Returns the source points. This convenience method extracts those points from
287: * the {@linkplain #getMappedPositions mapped positions}.
288: */
289: public DirectPosition[] getSourcePoints() {
290: final DirectPosition[] points = getPoints(getMappedPositions(),
291: false);
292: assert ensureValid(points, "sourcePoints", sourceCRS);
293: return points;
294: }
295:
296: /**
297: * Convenience method setting the {@linkplain MappedPosition#getSource source points}
298: * in mapped positions.
299: *
300: * @param points The source points.
301: * @throws MismatchedSizeException if the list doesn't have the expected number of points.
302: * @throws MismatchedDimensionException if some points doesn't have the
303: * {@linkplain #getDimension expected number of dimensions}.
304: * @throws MismatchedReferenceSystemException if CRS is not the same for all points.
305: */
306: public void setSourcePoints(final DirectPosition[] points)
307: throws MismatchedSizeException,
308: MismatchedDimensionException,
309: MismatchedReferenceSystemException {
310: // Set the points only after we checked them.
311: sourceCRS = ensureValid(points, "sourcePoints");
312: setPoints(points, false);
313: }
314:
315: /**
316: * Returns the target points. This convenience method extracts those points from
317: * the {@linkplain #getMappedPositions mapped positions}.
318: */
319: public DirectPosition[] getTargetPoints() {
320: final DirectPosition[] points = getPoints(getMappedPositions(),
321: true);
322: assert ensureValid(points, "targetPoints", targetCRS);
323: return points;
324: }
325:
326: /**
327: * Convenience method setting the {@linkplain MappedPosition#getTarget target points}
328: * in mapped positions.
329: *
330: * @param points The target points.
331: * @throws MismatchedSizeException if the list doesn't have the expected number of points.
332: * @throws MismatchedDimensionException if some points doesn't have the
333: * {@linkplain #getDimension expected number of dimensions}.
334: * @throws MismatchedReferenceSystemException if CRS is not the same for all points.
335: */
336: public void setTargetPoints(final DirectPosition[] points)
337: throws MismatchedSizeException,
338: MismatchedDimensionException,
339: MismatchedReferenceSystemException {
340: // Set the points only after we checked them.
341: targetCRS = ensureValid(points, "targetPoints");
342: setPoints(points, true);
343: }
344:
345: /**
346: * Prints a table of all source and target points stored in this builder.
347: *
348: * @param out The output device where to print all points.
349: * @param locale The locale, or {@code null} for the default.
350: * @throws IOException if an error occured while printing.
351: *
352: * @todo Insert a double-line column separator between the source and target points.
353: */
354: public void printPoints(final Writer out, Locale locale)
355: throws IOException {
356: if (locale == null) {
357: locale = Locale.getDefault();
358: }
359: final NumberFormat source = getNumberFormat(locale, false);
360: final NumberFormat target = getNumberFormat(locale, true);
361: final TableWriter table = new TableWriter(out, " \u2502 ");
362: table.setAlignment(TableWriter.ALIGN_CENTER);
363: table.writeHorizontalSeparator();
364: try {
365: final CoordinateSystem sourceCS = getSourceCRS()
366: .getCoordinateSystem();
367: final CoordinateSystem targetCS = getTargetCRS()
368: .getCoordinateSystem();
369: int dimension = sourceCS.getDimension();
370: for (int i = 0; i < dimension; i++) {
371: table.write(sourceCS.getAxis(i).getName().getCode());
372: table.nextColumn();
373: }
374: dimension = targetCS.getDimension();
375: for (int i = 0; i < dimension; i++) {
376: table.write(targetCS.getAxis(i).getName().getCode());
377: table.nextColumn();
378: }
379: table.writeHorizontalSeparator();
380: } catch (FactoryException e) {
381: /*
382: * Ignore. The only consequences is that the table will not
383: * contains a title line.
384: */
385: }
386: table.setAlignment(TableWriter.ALIGN_RIGHT);
387: for (final Iterator it = getMappedPositions().iterator(); it
388: .hasNext();) {
389: final MappedPosition mp = (MappedPosition) it.next();
390: DirectPosition point = mp.getSource();
391: int dimension = point.getDimension();
392: for (int i = 0; i < dimension; i++) {
393: table.write(source.format(point.getOrdinate(i)));
394: table.nextColumn();
395: }
396: point = mp.getTarget();
397: dimension = point.getDimension();
398: for (int i = 0; i < dimension; i++) {
399: table.write(target.format(point.getOrdinate(i)));
400: table.nextColumn();
401: }
402: table.nextLine();
403: }
404: table.writeHorizontalSeparator();
405: table.flush();
406: }
407:
408: /**
409: * Returns the coordinate reference system for the {@link #getSourcePoints source points}.
410: * This method determines the CRS as below:
411: * <p>
412: * <ul>
413: * <li>If at least one source points has a CRS, then this CRS is selected
414: * as the source one and returned.</li>
415: * <li>If no source point has a CRS, then this method creates an
416: * {@linkplain EngineeringCRS engineering CRS} using the same
417: * {@linkplain CoordinateSystem coordinate system} than the one used
418: * by the {@linkplain #getTargetCRS target CRS}.</li>
419: * </ul>
420: *
421: * @throws FactoryException if the CRS can't be created.
422: */
423: public CoordinateReferenceSystem getSourceCRS()
424: throws FactoryException {
425: if (sourceCRS == null) {
426: sourceCRS = createEngineeringCRS(false);
427: }
428: assert sourceCRS.getCoordinateSystem().getDimension() == getDimension();
429: return sourceCRS;
430: }
431:
432: /**
433: * Returns the coordinate reference system for the {@link #getTargetPoints target points}.
434: * This method determines the CRS as below:
435: * <p>
436: * <ul>
437: * <li>If at least one target points has a CRS, then this CRS is selected
438: * as the target one and returned.</li>
439: * <li>If no target point has a CRS, then this method creates an
440: * {@linkplain EngineeringCRS engineering CRS} using the same
441: * {@linkplain CoordinateSystem coordinate system} than the one used
442: * by the {@linkplain #getSourceCRS source CRS}.</li>
443: * </ul>
444: *
445: * @throws FactoryException if the CRS can't be created.
446: */
447: public CoordinateReferenceSystem getTargetCRS()
448: throws FactoryException {
449: if (targetCRS == null) {
450: targetCRS = createEngineeringCRS(true);
451: }
452: assert targetCRS.getCoordinateSystem().getDimension() == getDimension();
453: return targetCRS;
454: }
455:
456: /**
457: * Creates an engineering CRS using the same {@linkplain CoordinateSystem
458: * coordinate system} than the existing CRS, and an area of validity
459: * determined from the specified points. This method is used for creating
460: * a {@linkplain #getTargetCRS target CRS} from the
461: * {@linkplain #getSourceCRS source CRS}, or conversely.
462: *
463: * @param target {@code false} for creating the source CRS, or
464: * or {@code true} for creating the target CRS.
465: * @throws FactoryException if the CRS can't be created.
466: */
467: private EngineeringCRS createEngineeringCRS(final boolean target)
468: throws FactoryException {
469: final Map properties = new HashMap(4);
470: properties.put(CoordinateReferenceSystem.NAME_KEY, Vocabulary
471: .format(VocabularyKeys.UNKNOW));
472: final GeographicExtent validArea = getValidArea(target);
473: if (validArea != null) {
474: final ExtentImpl extent = new ExtentImpl();
475: extent.getGeographicElements().add(validArea);
476: properties.put(
477: CoordinateReferenceSystem.DOMAIN_OF_VALIDITY_KEY,
478: extent.unmodifiable());
479: }
480: final CoordinateReferenceSystem oppositeCRS = target ? sourceCRS
481: : targetCRS;
482: final CoordinateSystem cs;
483: if (oppositeCRS != null) {
484: cs = oppositeCRS.getCoordinateSystem();
485: } else {
486: switch (getDimension()) {
487: case 2:
488: cs = DefaultCartesianCS.GENERIC_2D;
489: break;
490: case 3:
491: cs = DefaultCartesianCS.GENERIC_3D;
492: break;
493: default:
494: throw new FactoryException(Errors
495: .format(ErrorKeys.UNSPECIFIED_CRS));
496: }
497: }
498: return crsFactory.createEngineeringCRS(properties, datumFactory
499: .createEngineeringDatum(properties), cs);
500: }
501:
502: /**
503: * Returns a default format for source or target points.
504: * The precision is computed from the envelope.
505: */
506: private NumberFormat getNumberFormat(final Locale locale,
507: final boolean target) {
508: final NumberFormat format = NumberFormat
509: .getNumberInstance(locale);
510: final GeneralEnvelope envelope = getEnvelope(target);
511: double length = 0;
512: for (int i = envelope.getDimension(); --i >= 0;) {
513: final double candidate = envelope.getLength(i);
514: if (candidate > length) {
515: length = candidate;
516: }
517: }
518: if (length > 0) {
519: final int digits = Math.max(0, 3 - (int) Math.ceil(XMath
520: .log10(length)));
521: if (digits < 16) {
522: format.setMinimumFractionDigits(digits);
523: format.setMaximumFractionDigits(digits);
524: }
525: }
526: return format;
527: }
528:
529: /**
530: * Returns an envelope that contains fully all the specified points.
531: * If the envelope can't be calculated, then this method returns {@code null}.
532: *
533: * @param target {@code false} for the envelope of source points,
534: * or {@code true} for the envelope of target points.
535: */
536: private GeneralEnvelope getEnvelope(final boolean target) {
537: GeneralEnvelope envelope = null;
538: CoordinateReferenceSystem crs = null;
539: for (final Iterator it = getMappedPositions().iterator(); it
540: .hasNext();) {
541: final MappedPosition mp = (MappedPosition) it.next();
542: final DirectPosition point = target ? mp.getTarget() : mp
543: .getSource();
544: if (point != null) {
545: if (envelope == null) {
546: final double[] coordinates = point.getCoordinates();
547: envelope = new GeneralEnvelope(coordinates,
548: coordinates);
549: } else {
550: envelope.add(point);
551: }
552: crs = getCoordinateReferenceSystem(point, crs);
553: }
554: }
555: if (envelope != null) {
556: envelope.setCoordinateReferenceSystem(crs);
557: }
558: return envelope;
559: }
560:
561: /**
562: * Returns a geographic extent that contains fully all the specified points.
563: * If the envelope can't be calculated, then this method returns {@code null}.
564: *
565: * @param target {@code false} for the valid area of source points,
566: * or {@code true} for the valid area of target points.
567: */
568: private GeographicBoundingBox getValidArea(final boolean target) {
569: GeneralEnvelope envelope = getEnvelope(target);
570: if (envelope != null)
571: try {
572: return new GeographicBoundingBoxImpl(envelope);
573: } catch (TransformException exception) {
574: /*
575: * Can't transform the envelope. Do not rethrown this exception. We don't
576: * log it neither (at least not at the warning level) because this method
577: * is optional.
578: */
579: }
580: return null;
581: }
582:
583: /**
584: * Returns the CRS of the specified point. If the CRS of the previous point is known,
585: * it can be specified. This method will then ensure that the two CRS are compatibles.
586: */
587: private static CoordinateReferenceSystem getCoordinateReferenceSystem(
588: final DirectPosition point,
589: CoordinateReferenceSystem previousCRS)
590: throws MismatchedReferenceSystemException {
591: final CoordinateReferenceSystem candidate = point
592: .getCoordinateReferenceSystem();
593: if (candidate != null) {
594: if (previousCRS == null) {
595: return candidate;
596: }
597: /*
598: * We use strict 'equals' instead of 'equalsIgnoreCase' because if the metadata
599: * are not identical, we have no easy way to choose which CRS is the "main" one.
600: */
601: if (!previousCRS.equals(candidate)) {
602: throw new MismatchedReferenceSystemException(
603: Errors
604: .format(ErrorKeys.MISMATCHED_COORDINATE_REFERENCE_SYSTEM));
605: }
606: }
607: return previousCRS;
608: }
609:
610: /**
611: * Returns the required coordinate system type. The default implementation returns
612: * {@code CoordinateSystem.class}, which means that every kind of coordinate system
613: * is legal. Some subclasses will restrict to {@linkplain CartesianCS cartesian CS}.
614: */
615: public Class/*<? extends CoordinateSystem>*/getCoordinateSystemType() {
616: return CoordinateSystem.class;
617: }
618:
619: /**
620: * Ensures that the specified list of points is valid, and returns their CRS.
621: *
622: * @param points The points to check.
623: * @param label The argument name, used for formatting error message only.
624: *
625: * @throws MismatchedSizeException if the list doesn't have the expected number of points.
626: * @throws MismatchedDimensionException if some points doesn't have the
627: * {@linkplain #getDimension expected number of dimensions}.
628: * @throws MismatchedReferenceSystemException if CRS is not the same for all points.
629: * @return The CRS used for the specified points, or {@code null} if unknown.
630: */
631: private CoordinateReferenceSystem ensureValid(
632: final DirectPosition[] points, final String label)
633: throws MismatchedSizeException,
634: MismatchedDimensionException,
635: MismatchedReferenceSystemException {
636: final int necessaryNumber = getMinimumPointCount();
637: if (points.length < necessaryNumber) {
638: throw new MismatchedSizeException(Errors.format(
639: ErrorKeys.INSUFFICIENT_POINTS_$2, new Integer(
640: points.length),
641: new Integer(necessaryNumber)));
642: }
643: CoordinateReferenceSystem crs = null;
644: final int dimension = getDimension();
645: for (int i = 0; i < points.length; i++) {
646: final DirectPosition point = points[i];
647: final int pointDim = point.getDimension();
648: if (pointDim != dimension) {
649: throw new MismatchedDimensionException(Errors.format(
650: ErrorKeys.MISMATCHED_DIMENSION_$3, label + '['
651: + i + ']', new Integer(pointDim),
652: new Integer(dimension)));
653: }
654: crs = getCoordinateReferenceSystem(point, crs);
655: }
656: if (crs != null) {
657: final CoordinateSystem cs = crs.getCoordinateSystem();
658: if (!getCoordinateSystemType().isAssignableFrom(
659: cs.getClass())) {
660: throw new MismatchedReferenceSystemException(
661: Errors
662: .format(
663: ErrorKeys.UNSUPPORTED_COORDINATE_SYSTEM_$1,
664: cs.getName()));
665: }
666: }
667: return crs;
668: }
669:
670: /**
671: * Used for assertions only.
672: */
673: private boolean ensureValid(final DirectPosition[] points,
674: final String label, final CoordinateReferenceSystem expected) {
675: final CoordinateReferenceSystem actual = ensureValid(points,
676: label);
677: return actual == null || actual == expected;
678: }
679:
680: /**
681: * Returns statistics about the errors. The errors are computed as the distance between
682: * {@linkplain #getSourcePoints source points} transformed by the math transform computed
683: * by this {@code MathTransformBuilder}, and the {@linkplain #getTargetPoints target points}.
684: * Use {@link Statistics#rms} for the <cite>Root Mean Squared error</cite>.
685: *
686: * @throws FactoryException If the math transform can't be created or used.
687: */
688: public Statistics getErrorStatistics() throws FactoryException {
689: final MathTransform mt = getMathTransform();
690: final Statistics stats = new Statistics();
691: final DirectPosition buffer = new GeneralDirectPosition(
692: getDimension());
693: for (final Iterator it = getMappedPositions().iterator(); it
694: .hasNext();) {
695: final MappedPosition mp = (MappedPosition) it.next();
696: /*
697: * Transforms the source point using the math transform calculated by this class.
698: * If the transform can't be applied, then we consider this failure as if it was
699: * a factory error rather than a transformation error. This simplify the exception
700: * declaration, but also has some sense on a conceptual point of view. We are
701: * transforming the exact same points than the one used for creating the math
702: * transform. If one of those points can't be transformed, then there is probably
703: * something wrong with the transform we just created.
704: */
705: final double error;
706: try {
707: error = mp.getError(mt, buffer);
708: } catch (TransformException e) {
709: throw new FactoryException(Errors
710: .format(ErrorKeys.CANT_TRANSFORM_VALID_POINTS),
711: e);
712: }
713: stats.add(error);
714: }
715: return stats;
716: }
717:
718: /**
719: * Calculates the math transform immediately.
720: *
721: * @return Math transform from {@link #setMappedPositions MappedPosition}.
722: * @throws FactoryException if the math transform can't be created.
723: */
724: protected abstract MathTransform computeMathTransform()
725: throws FactoryException;
726:
727: /**
728: * Returns the calculated math transform. This method {@linkplain #computeMathTransform the math
729: * transform} the first time it is requested.
730: *
731: * @return Math transform from {@link #setMappedPositions MappedPosition}.
732: * @throws FactoryException if the math transform can't be created.
733: */
734: public final MathTransform getMathTransform()
735: throws FactoryException {
736: if (transform == null) {
737: transform = computeMathTransform();
738: }
739: return transform;
740: }
741:
742: /**
743: * Returns the coordinate operation wrapping the {@linkplain #getMathTransform() calculated
744: * math transform}. The {@linkplain Transformation#getPositionalAccuracy positional
745: * accuracy} will be set to the Root Mean Square (RMS) of the differences between the
746: * source points transformed to the target CRS, and the expected target points.
747: */
748: public Transformation getTransformation() throws FactoryException {
749: if (transformation == null) {
750: final Map properties = new HashMap();
751: properties.put(Transformation.NAME_KEY, getName());
752: /*
753: * Set the valid area as the intersection of source CRS and target CRS valid area.
754: */
755: final CoordinateReferenceSystem sourceCRS = getSourceCRS();
756: final CoordinateReferenceSystem targetCRS = getTargetCRS();
757: final GeographicBoundingBox sourceBox = CRS
758: .getGeographicBoundingBox(sourceCRS);
759: final GeographicBoundingBox targetBox = CRS
760: .getGeographicBoundingBox(targetCRS);
761: final GeographicBoundingBox validArea;
762: if (sourceBox == null) {
763: validArea = targetBox;
764: } else if (targetBox == null) {
765: validArea = sourceBox;
766: } else {
767: final GeneralEnvelope area = new GeneralEnvelope(
768: sourceBox);
769: area.intersect(new GeneralEnvelope(sourceBox));
770: try {
771: validArea = new GeographicBoundingBoxImpl(area);
772: } catch (TransformException e) {
773: // Should never happen, because we know that 'area' CRS is WGS84.
774: throw new AssertionError(e);
775: }
776: }
777: if (validArea != null) {
778: final ExtentImpl extent = new ExtentImpl();
779: extent.getGeographicElements().add(validArea);
780: properties.put(Transformation.DOMAIN_OF_VALIDITY_KEY,
781: extent.unmodifiable());
782: }
783: /*
784: * Computes the positional accuracy as the RMS value of differences
785: * between the computed target points and the supplied target points.
786: */
787: final double error = getErrorStatistics().rms();
788: if (!Double.isNaN(error)) {
789: final InternationalString description = Vocabulary
790: .formatInternational(VocabularyKeys.ROOT_MEAN_SQUARED_ERROR);
791: final QuantitativeResultImpl result = new QuantitativeResultImpl();
792: result.setValues(new double[] { error });
793: //result.setValueType(Double.TYPE);
794: result.setValueUnit(CRSUtilities.getUnit(targetCRS
795: .getCoordinateSystem()));
796: result.setErrorStatistic(description);
797: final PositionalAccuracyImpl accuracy = new PositionalAccuracyImpl(
798: result);
799: accuracy
800: .setEvaluationMethodType(EvaluationMethodType.DIRECT_INTERNAL);
801: accuracy.setEvaluationMethodDescription(description);
802: properties
803: .put(
804: Transformation.COORDINATE_OPERATION_ACCURACY_KEY,
805: accuracy.unmodifiable());
806: }
807: /*
808: * Now creates the transformation.
809: */
810: final MathTransform transform = getMathTransform();
811: transformation = new DefaultTransformation(properties,
812: sourceCRS, targetCRS, transform,
813: new DefaultOperationMethod(transform));
814: }
815: return transformation;
816: }
817:
818: /**
819: * Returns a string representation of this builder. The default implementation
820: * returns a table containing all source and target points.
821: */
822: public String toString() {
823: final StringWriter out = new StringWriter();
824: try {
825: printPoints(out, null);
826: } catch (IOException e) {
827: // Should never happen, since we are printing to a StringWriter.
828: throw new AssertionError(e);
829: }
830: return out.toString();
831: }
832: }
|