/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.referencing.operation.builder;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.Locale;
import java.io.Writer;
import java.io.IOException;
import java.io.StringWriter;
import java.text.NumberFormat;
import javax.vecmath.MismatchedSizeException;
import org.opengis.util.InternationalString;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.cs.CartesianCS; // For javadoc only
import org.opengis.referencing.cs.CoordinateSystem;
import org.opengis.referencing.crs.*; // Includes imports used only for javadoc.
import org.opengis.referencing.operation.*; // Includes imports used only for javadoc.
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.metadata.extent.GeographicExtent;
import org.opengis.metadata.extent.GeographicBoundingBox;
import org.opengis.metadata.quality.EvaluationMethodType;
import org.opengis.geometry.DirectPosition;
import org.opengis.geometry.MismatchedDimensionException;
import org.opengis.geometry.MismatchedReferenceSystemException;
import org.geotools.factory.Hints;
import org.geotools.io.TableWriter;
import org.geotools.math.Statistics;
import org.geotools.referencing.CRS;
import org.geotools.referencing.ReferencingFactoryFinder;
import org.geotools.referencing.cs.DefaultCartesianCS;
import org.geotools.referencing.operation.DefaultOperationMethod;
import org.geotools.referencing.operation.DefaultTransformation;
import org.geotools.geometry.GeneralEnvelope;
import org.geotools.geometry.GeneralDirectPosition;
import org.geotools.metadata.iso.extent.ExtentImpl;
import org.geotools.metadata.iso.extent.GeographicBoundingBoxImpl;
import org.geotools.metadata.iso.quality.PositionalAccuracyImpl;
import org.geotools.metadata.iso.quality.QuantitativeResultImpl;
import org.geotools.resources.i18n.VocabularyKeys;
import org.geotools.resources.i18n.Vocabulary;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.CRSUtilities;
import org.geotools.resources.Classes;
/**
* Provides a basic implementation for {@linkplain MathTransform math transform}
* builders.
*
* Math transform builders create {@link MathTransform} objects for transforming
* coordinates from a source CRS
* ({@linkplain CoordinateReferenceSystem Coordinate Reference System}) to
* a target CRS using empirical parameters. Usually, one of those CRS is a
* {@linkplain GeographicCRS geographic} or {@linkplain ProjectedCRS projected}
* one with a well known relationship to the earth. The other CRS is often an
* {@linkplain EngineeringCRS engineering} or {@linkplain ImageCRS image} one
* tied to some ship. For example a remote sensing image before
* georectification may be referenced by an {@linkplain ImageCRS image CRS}.
*
*
Design note:
* It is technically possible to reference such remote sensing images with a
* {@linkplain DerivedCRS CRS derived} from the geographic or projected CRS,
* where the {@linkplain DerivedCRS#getConversionFromBase conversion from base}
* is the math transform {@linkplain #getMathTransform computed by this builder}.
* Such approach is advantageous for {@linkplain CoordinateOperationFactory
* coordinate operation factory} implementations, since they can determine the
* operation just by inspection of the {@link DerivedCRS} instance. However this
* is conceptually incorrect since {@link DerivedCRS} can be related to an other
* CRS only through {@linkplain Conversion conversions}, which by definition are
* accurate up to rounding errors. The operations created by math transform
* builders are rather {@linkplain Transformation transformations}, which can't
* be used for {@link DerivedCRS} creation.
*
*
* The math transform from {@linkplain #getSourceCRS source CRS} to {@linkplain
* #getTargetCRS target CRS} is calculated by {@code MathTransformBuilder} from
* a set of {@linkplain #getMappedPositions mapped positions} in both CRS.
*
* Subclasses must implement at least the {@link #getMinimumPointCount()} and
* {@link #computeMathTransform()} methods.
*
* @since 2.4
*
* @source $URL$
* @version $Id$
* @author Jan Jezek
* @author Martin Desruisseaux
*/
public abstract class MathTransformBuilder {
/**
* The list of mapped positions.
*/
private final List positions = new ArrayList();
/**
* An unmodifiable view of mapped positions to be returned by {@link #getMappedPositions}.
*/
private final List unmodifiablePositions =
Collections.unmodifiableList(positions);
/**
* Coordinate Reference System of the source and target points,
* or {@code null} if unknown.
*/
private CoordinateReferenceSystem sourceCRS, targetCRS;
/**
* The math transform. Will be computed only when first needed.
*/
private transient MathTransform transform;
/**
* The transformation. Will be computed only when first needed.
*/
private transient Transformation transformation;
/**
* The factory to use for creating {@link MathTransform math transform} instances.
*/
protected final MathTransformFactory mtFactory;
/**
* The CRS factory to use for creating {@link EngineeringCRS} instances.
*/
private final CRSFactory crsFactory;
/**
* The datum factory to use for creating {@link EngineeringCRS} instances.
*/
private final DatumFactory datumFactory;
/**
* Creates a builder with the default factories.
*/
public MathTransformBuilder() {
this(null);
}
/**
* Creates a builder from the specified hints.
*/
public MathTransformBuilder(final Hints hints) {
mtFactory = ReferencingFactoryFinder.getMathTransformFactory(hints);
crsFactory = ReferencingFactoryFinder.getCRSFactory (hints);
datumFactory = ReferencingFactoryFinder.getDatumFactory (hints);
}
/**
* Returns the name for the {@linkplain #getTransformation transformation} to
* be created by this builder.
*/
public String getName() {
return Classes.getShortClassName(this) + " fit";
}
/**
* Returns the minimum number of points required by this builder. This minimum depends on the
* algorithm used. For example {@linkplain AffineTransformBuilder affine transform builders}
* require at least 3 points, while {@linkplain SimilarTransformBuilder similar transform
* builders} requires only 2 points.
*/
public abstract int getMinimumPointCount();
/**
* Returns the dimension for {@linkplain #getSourceCRS source} and
* {@link #getTargetCRS target} CRS. The default value is 2.
*/
public int getDimension() {
return 2;
}
/**
* Returns the list of mapped positions.
*/
public List getMappedPositions() {
return unmodifiablePositions;
}
/**
* Set the list of mapped positions.
*
* @throws MismatchedSizeException if the list doesn't have the expected number of points.
* @throws MismatchedDimensionException if some points doesn't have the
* {@linkplain #getDimension expected number of dimensions}.
* @throws MismatchedReferenceSystemException if CRS is not the same for all points.
*/
public void setMappedPositions(final List positions)
throws MismatchedSizeException, MismatchedDimensionException,
MismatchedReferenceSystemException
{
final CoordinateReferenceSystem source, target;
source = ensureValid(getPoints(positions, false), "sourcePoints");
target = ensureValid(getPoints(positions, true ), "targetPoints");
/*
* Now stores the informations. Note that we set the source and target CRS
* only after 'ensureValid' succeed for both CRS.
*/
this.positions.clear();
this.positions.addAll(positions);
this.sourceCRS = source;
this.targetCRS = target;
this.transform = null;
}
/**
* Extracts the source or target points from the specified list.
*
* @param positions The array where to take points from.
* @param target {@code false} for extracting source points,
* or {@code true} for extracting target points.
*/
private static DirectPosition[] getPoints(List positions, boolean target) {
final DirectPosition[] points = new DirectPosition[positions.size()];
for (int i=0; i it=getMappedPositions().iterator(); it.hasNext();) {
final MappedPosition mp = (MappedPosition) it.next();
DirectPosition point = mp.getSource();
int dimension = point.getDimension();
for (int i=0; i
*
* - If at least one source points has a CRS, then this CRS is selected
* as the source one and returned.
* - If no source point has a CRS, then this method creates an
* {@linkplain EngineeringCRS engineering CRS} using the same
* {@linkplain CoordinateSystem coordinate system} than the one used
* by the {@linkplain #getTargetCRS target CRS}.
*
*
* @throws FactoryException if the CRS can't be created.
*/
public CoordinateReferenceSystem getSourceCRS() throws FactoryException {
if (sourceCRS == null) {
sourceCRS = createEngineeringCRS(false);
}
assert sourceCRS.getCoordinateSystem().getDimension() == getDimension();
return sourceCRS;
}
/**
* Returns the coordinate reference system for the {@link #getTargetPoints target points}.
* This method determines the CRS as below:
*
*
* - If at least one target points has a CRS, then this CRS is selected
* as the target one and returned.
* - If no target point has a CRS, then this method creates an
* {@linkplain EngineeringCRS engineering CRS} using the same
* {@linkplain CoordinateSystem coordinate system} than the one used
* by the {@linkplain #getSourceCRS source CRS}.
*
*
* @throws FactoryException if the CRS can't be created.
*/
public CoordinateReferenceSystem getTargetCRS() throws FactoryException {
if (targetCRS == null) {
targetCRS = createEngineeringCRS(true);
}
assert targetCRS.getCoordinateSystem().getDimension() == getDimension();
return targetCRS;
}
/**
* Creates an engineering CRS using the same {@linkplain CoordinateSystem
* coordinate system} than the existing CRS, and an area of validity
* determined from the specified points. This method is used for creating
* a {@linkplain #getTargetCRS target CRS} from the
* {@linkplain #getSourceCRS source CRS}, or conversely.
*
* @param target {@code false} for creating the source CRS, or
* or {@code true} for creating the target CRS.
* @throws FactoryException if the CRS can't be created.
*/
private EngineeringCRS createEngineeringCRS(final boolean target) throws FactoryException {
final Map properties = new HashMap(4);
properties.put(CoordinateReferenceSystem.NAME_KEY, Vocabulary.format(VocabularyKeys.UNKNOW));
final GeographicExtent validArea = getValidArea(target);
if (validArea != null) {
final ExtentImpl extent = new ExtentImpl();
extent.getGeographicElements().add(validArea);
properties.put(CoordinateReferenceSystem.DOMAIN_OF_VALIDITY_KEY, extent.unmodifiable());
}
final CoordinateReferenceSystem oppositeCRS = target ? sourceCRS : targetCRS;
final CoordinateSystem cs;
if (oppositeCRS != null) {
cs = oppositeCRS.getCoordinateSystem();
} else {
switch (getDimension()) {
case 2: cs = DefaultCartesianCS.GENERIC_2D; break;
case 3: cs = DefaultCartesianCS.GENERIC_3D; break;
default: throw new FactoryException(Errors.format(ErrorKeys.UNSPECIFIED_CRS));
}
}
return crsFactory.createEngineeringCRS(properties,
datumFactory.createEngineeringDatum(properties), cs);
}
/**
* Returns a default format for source or target points.
* The precision is computed from the envelope.
*/
private NumberFormat getNumberFormat(final Locale locale, final boolean target) {
final NumberFormat format = NumberFormat.getNumberInstance(locale);
final GeneralEnvelope envelope = getEnvelope(target);
double length = 0;
for (int i=envelope.getDimension(); --i>=0;) {
final double candidate = envelope.getLength(i);
if (candidate > length) {
length = candidate;
}
}
if (length > 0) {
final int digits = Math.max(0, 3 - (int) Math.ceil(Math.log10(length)));
if (digits < 16) {
format.setMinimumFractionDigits(digits);
format.setMaximumFractionDigits(digits);
}
}
return format;
}
/**
* Returns an envelope that contains fully all the specified points.
* If the envelope can't be calculated, then this method returns {@code null}.
*
* @param target {@code false} for the envelope of source points,
* or {@code true} for the envelope of target points.
*/
private GeneralEnvelope getEnvelope(final boolean target) {
GeneralEnvelope envelope = null;
CoordinateReferenceSystem crs = null;
for (final Iterator it=getMappedPositions().iterator(); it.hasNext();) {
final MappedPosition mp = (MappedPosition) it.next();
final DirectPosition point = target ? mp.getTarget() : mp.getSource();
if (point != null) {
if (envelope == null) {
final double[] coordinates = point.getCoordinates();
envelope = new GeneralEnvelope(coordinates, coordinates);
} else {
envelope.add(point);
}
crs = getCoordinateReferenceSystem(point, crs);
}
}
if (envelope != null) {
envelope.setCoordinateReferenceSystem(crs);
}
return envelope;
}
/**
* Returns a geographic extent that contains fully all the specified points.
* If the envelope can't be calculated, then this method returns {@code null}.
*
* @param target {@code false} for the valid area of source points,
* or {@code true} for the valid area of target points.
*/
private GeographicBoundingBox getValidArea(final boolean target) {
GeneralEnvelope envelope = getEnvelope(target);
if (envelope != null) try {
return new GeographicBoundingBoxImpl(envelope);
} catch (TransformException exception) {
/*
* Can't transform the envelope. Do not rethrown this exception. We don't
* log it neither (at least not at the warning level) because this method
* is optional.
*/
}
return null;
}
/**
* Returns the CRS of the specified point. If the CRS of the previous point is known,
* it can be specified. This method will then ensure that the two CRS are compatibles.
*/
private static CoordinateReferenceSystem getCoordinateReferenceSystem(
final DirectPosition point, CoordinateReferenceSystem previousCRS)
throws MismatchedReferenceSystemException
{
final CoordinateReferenceSystem candidate = point.getCoordinateReferenceSystem();
if (candidate != null) {
if (previousCRS == null) {
return candidate;
}
/*
* We use strict 'equals' instead of 'equalsIgnoreCase' because if the metadata
* are not identical, we have no easy way to choose which CRS is the "main" one.
*/
if (!previousCRS.equals(candidate)) {
throw new MismatchedReferenceSystemException(
Errors.format(ErrorKeys.MISMATCHED_COORDINATE_REFERENCE_SYSTEM));
}
}
return previousCRS;
}
/**
* Returns the required coordinate system type. The default implementation returns
* {@code CoordinateSystem.class}, which means that every kind of coordinate system
* is legal. Some subclasses will restrict to {@linkplain CartesianCS cartesian CS}.
*/
public Class extends CoordinateSystem> getCoordinateSystemType() {
return CoordinateSystem.class;
}
/**
* Ensures that the specified list of points is valid, and returns their CRS.
*
* @param points The points to check.
* @param label The argument name, used for formatting error message only.
*
* @throws MismatchedSizeException if the list doesn't have the expected number of points.
* @throws MismatchedDimensionException if some points doesn't have the
* {@linkplain #getDimension expected number of dimensions}.
* @throws MismatchedReferenceSystemException if CRS is not the same for all points.
* @return The CRS used for the specified points, or {@code null} if unknown.
*/
private CoordinateReferenceSystem ensureValid(final DirectPosition[] points,
final String label)
throws MismatchedSizeException, MismatchedDimensionException,
MismatchedReferenceSystemException
{
final int necessaryNumber = getMinimumPointCount();
if (points.length < necessaryNumber) {
throw new MismatchedSizeException(Errors.format(ErrorKeys.INSUFFICIENT_POINTS_$2,
points.length, necessaryNumber));
}
CoordinateReferenceSystem crs = null;
final int dimension = getDimension();
for (int i=0; iRoot Mean Squared error.
*
* @throws FactoryException If the math transform can't be created or used.
*/
public Statistics getErrorStatistics() throws FactoryException {
final MathTransform mt = getMathTransform();
final Statistics stats = new Statistics();
final DirectPosition buffer = new GeneralDirectPosition(getDimension());
for (final Iterator it=getMappedPositions().iterator(); it.hasNext();) {
final MappedPosition mp = (MappedPosition) it.next();
/*
* Transforms the source point using the math transform calculated by this class.
* If the transform can't be applied, then we consider this failure as if it was
* a factory error rather than a transformation error. This simplify the exception
* declaration, but also has some sense on a conceptual point of view. We are
* transforming the exact same points than the one used for creating the math
* transform. If one of those points can't be transformed, then there is probably
* something wrong with the transform we just created.
*/
final double error;
try {
error = mp.getError(mt, buffer);
} catch (TransformException e) {
throw new FactoryException(Errors.format(ErrorKeys.CANT_TRANSFORM_VALID_POINTS), e);
}
stats.add(error);
}
return stats;
}
/**
* Calculates the math transform immediately.
*
* @return Math transform from {@link #setMappedPositions MappedPosition}.
* @throws FactoryException if the math transform can't be created.
*/
protected abstract MathTransform computeMathTransform() throws FactoryException;
/**
* Returns the calculated math transform. This method {@linkplain #computeMathTransform the math
* transform} the first time it is requested.
*
* @return Math transform from {@link #setMappedPositions MappedPosition}.
* @throws FactoryException if the math transform can't be created.
*/
public final MathTransform getMathTransform() throws FactoryException {
if (transform == null) {
transform = computeMathTransform();
}
return transform;
}
/**
* Returns the coordinate operation wrapping the {@linkplain #getMathTransform() calculated
* math transform}. The {@linkplain Transformation#getPositionalAccuracy positional
* accuracy} will be set to the Root Mean Square (RMS) of the differences between the
* source points transformed to the target CRS, and the expected target points.
*/
public Transformation getTransformation() throws FactoryException {
if (transformation == null) {
final Map properties = new HashMap();
properties.put(Transformation.NAME_KEY, getName());
/*
* Set the valid area as the intersection of source CRS and target CRS valid area.
*/
final CoordinateReferenceSystem sourceCRS = getSourceCRS();
final CoordinateReferenceSystem targetCRS = getTargetCRS();
final GeographicBoundingBox sourceBox = CRS.getGeographicBoundingBox(sourceCRS);
final GeographicBoundingBox targetBox = CRS.getGeographicBoundingBox(targetCRS);
final GeographicBoundingBox validArea;
if (sourceBox == null) {
validArea = targetBox;
} else if (targetBox == null) {
validArea = sourceBox;
} else {
final GeneralEnvelope area = new GeneralEnvelope(sourceBox);
area.intersect(new GeneralEnvelope(sourceBox));
try {
validArea = new GeographicBoundingBoxImpl(area);
} catch (TransformException e) {
// Should never happen, because we know that 'area' CRS is WGS84.
throw new AssertionError(e);
}
}
if (validArea != null) {
final ExtentImpl extent = new ExtentImpl();
extent.getGeographicElements().add(validArea);
properties.put(Transformation.DOMAIN_OF_VALIDITY_KEY, extent.unmodifiable());
}
/*
* Computes the positional accuracy as the RMS value of differences
* between the computed target points and the supplied target points.
*/
final double error = getErrorStatistics().rms();
if (!Double.isNaN(error)) {
final InternationalString description =
Vocabulary.formatInternational(VocabularyKeys.ROOT_MEAN_SQUARED_ERROR);
final QuantitativeResultImpl result = new QuantitativeResultImpl();
result.setValues(new double[] {error});
//result.setValueType(Double.TYPE);
result.setValueUnit(CRSUtilities.getUnit(targetCRS.getCoordinateSystem()));
result.setErrorStatistic(description);
final PositionalAccuracyImpl accuracy = new PositionalAccuracyImpl(result);
accuracy.setEvaluationMethodType(EvaluationMethodType.DIRECT_INTERNAL);
accuracy.setEvaluationMethodDescription(description);
properties.put(Transformation.COORDINATE_OPERATION_ACCURACY_KEY, accuracy.unmodifiable());
}
/*
* Now creates the transformation.
*/
final MathTransform transform = getMathTransform();
transformation = new DefaultTransformation(properties, sourceCRS, targetCRS, transform,
new DefaultOperationMethod(transform));
}
return transformation;
}
/**
* Returns a string representation of this builder. The default implementation
* returns a table containing all source and target points.
*/
@Override
public String toString() {
final StringWriter out = new StringWriter();
try {
printPoints(out, null);
} catch (IOException e) {
// Should never happen, since we are printing to a StringWriter.
throw new AssertionError(e);
}
return out.toString();
}
}