All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.geotoolkit.referencing.operation.DefaultCoordinateOperationFactory Maven / Gradle / Ivy

/*
 *    Geotoolkit.org - An Open Source Java GIS Toolkit
 *    http://www.geotoolkit.org
 *
 *    (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
 *    (C) 2009-2012, Geomatys
 *
 *    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.
 *
 *    This package contains documentation from OpenGIS specifications.
 *    OpenGIS consortium's work is fully acknowledged here.
 */
package org.geotoolkit.referencing.operation;

import java.util.Map;
import java.util.List;
import javax.measure.unit.Unit;
import javax.measure.quantity.Angle;
import javax.measure.quantity.Duration;
import javax.measure.quantity.Length;
import javax.vecmath.SingularMatrixException;
import net.jcip.annotations.ThreadSafe;

import org.opengis.util.FactoryException;
import org.opengis.parameter.ParameterValueGroup;
import org.opengis.parameter.ParameterNotFoundException;
import org.opengis.referencing.ReferenceIdentifier;
import org.opengis.referencing.cs.*;
import org.opengis.referencing.crs.*;
import org.opengis.referencing.datum.*;
import org.opengis.referencing.operation.*;

import org.geotoolkit.factory.Hints;
import org.geotoolkit.resources.Errors;
import org.geotoolkit.util.ComparisonMode;
import org.geotoolkit.util.logging.Logging;
import org.geotoolkit.util.converter.Classes;
import org.geotoolkit.referencing.IdentifiedObjects;
import org.geotoolkit.referencing.crs.DefaultCompoundCRS;
import org.geotoolkit.referencing.crs.DefaultEngineeringCRS;
import org.geotoolkit.referencing.cs.DefaultCartesianCS;
import org.geotoolkit.referencing.cs.DefaultEllipsoidalCS;
import org.geotoolkit.referencing.datum.BursaWolfParameters;
import org.geotoolkit.referencing.datum.DefaultGeodeticDatum;
import org.geotoolkit.referencing.operation.matrix.XMatrix;
import org.geotoolkit.referencing.operation.matrix.Matrix4;
import org.geotoolkit.referencing.operation.matrix.Matrices;
import org.geotoolkit.internal.referencing.AxisDirections;
import org.geotoolkit.internal.referencing.VerticalDatumTypes;

import static java.util.Collections.singletonList;
import static org.geotoolkit.measure.Units.MILLISECOND;
import static org.geotoolkit.referencing.CRS.equalsIgnoreMetadata;
import static org.geotoolkit.referencing.CRS.equalsApproximatively;
import static org.geotoolkit.referencing.IdentifiedObjects.nameMatches;
import static org.geotoolkit.internal.referencing.CRSUtilities.getGreenwichLongitude;


/**
 * Creates {@linkplain CoordinateOperation coordinate operations}. This factory is capable to find
 * coordinate {@linkplain Transformation transformations} or {@linkplain Conversion conversions}
 * between two {@linkplain CoordinateReferenceSystem coordinate reference systems}. It delegates
 * most of its work to one or many of {@code createOperationStep} methods. Subclasses can
 * override those methods in order to extend the factory capability to some more CRS.
 *
 * @author Martin Desruisseaux (IRD, Geomatys)
 * @version 3.18
 *
 * @since 1.2
 * @module
 */
@ThreadSafe
public class DefaultCoordinateOperationFactory extends AbstractCoordinateOperationFactory {
    /**
     * The operation to use by {@link #createTransformationStep(GeographicCRS,GeographicCRS)} for
     * datum shift. This string can have one of the following values:
     * 

*

    *
  • {@code "Abridged Molodensky"} for the abridged Molodensky transformation.
  • *
  • {@code "Molodensky"} for the Molodensky transformation.
  • *
  • {@code null} for performing datum shifts is geocentric coordinates.
  • *
*/ private final String molodenskyMethod; /** * {@code true} if datum shift are allowed even if no Bursa Wolf parameters is available. */ private final boolean lenientDatumShift; /** * Constructs a coordinate operation factory using the default factories. */ public DefaultCoordinateOperationFactory() { this(EMPTY_HINTS); } /** * Constructs a coordinate operation factory using the specified hints. * This constructor recognizes the {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS}, * {@link Hints#DATUM_FACTORY DATUM} and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM} * {@code FACTORY} hints. * * @param userHints The hints, or {@code null} if none. */ public DefaultCoordinateOperationFactory(final Hints userHints) { super(userHints); // // Default hints values // String molodenskyMethod = "Molodensky"; // Alternative: "Abridged Molodensky" boolean lenientDatumShift = false; // // Fetches the user-supplied hints // if (userHints != null) { Object candidate = userHints.get(Hints.DATUM_SHIFT_METHOD); if (candidate instanceof String) { molodenskyMethod = (String) candidate; if (molodenskyMethod.trim().equalsIgnoreCase("Geocentric")) { molodenskyMethod = null; } } candidate = userHints.get(Hints.LENIENT_DATUM_SHIFT); if (candidate instanceof Boolean) { lenientDatumShift = ((Boolean) candidate).booleanValue(); } } // // Stores the retained hints // this.molodenskyMethod = molodenskyMethod; this.lenientDatumShift = lenientDatumShift; this.hints.put(Hints.DATUM_SHIFT_METHOD, (molodenskyMethod != null) ? molodenskyMethod : "Geocentric"); this.hints.put(Hints.LENIENT_DATUM_SHIFT, Boolean.valueOf(lenientDatumShift)); } /** * Invoked by {@link org.geotoolkit.factory.FactoryRegistry} in order to set the ordering relative * to other factories. The current implementation specifies that this factory should defer to * {@link CachingCoordinateOperationFactory}. * * @since 3.00 */ @Override protected void setOrdering(final Organizer organizer) { super.setOrdering(organizer); organizer.after(CachingCoordinateOperationFactory.class, false); } /** * Returns an operation for conversion or transformation between two coordinate reference * systems. If an operation exists, it is returned. If more than one operation exists, the * default is returned. If no operation exists, then the exception is thrown. *

* The default implementation inspects the CRS and delegates the work to one or * many {@code createOperationStep(...)} methods. This method fails if no path * between the CRS is found. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws OperationNotFoundException if no operation path was found from {@code sourceCRS} * to {@code targetCRS}. * @throws FactoryException if the operation creation failed for some other reason. */ @Override public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS) throws OperationNotFoundException, FactoryException { ensureNonNull("sourceCRS", sourceCRS); ensureNonNull("targetCRS", targetCRS); if (equalsIgnoreMetadata(sourceCRS, targetCRS)) { final int dim = getDimension(sourceCRS); assert dim == getDimension(targetCRS) : dim; return createFromAffineTransform(IDENTITY, sourceCRS, targetCRS, Matrices.create(dim+1)); } else { // Query the database (if any) before to try to find the operation by ourself. final CoordinateOperation candidate = createFromDatabase(sourceCRS, targetCRS); if (candidate != null) { return candidate; } } /* * Now perform "instanceof" checks for all supported types. We check CompoundCRS first * because experience shows that it produces simpler transformation chains than if we * check them last. */ //////////////////////////////////////////// //// //// //// Compound --> various CRS //// //// //// //////////////////////////////////////////// if (sourceCRS instanceof CompoundCRS) { final CompoundCRS source = (CompoundCRS) sourceCRS; if (targetCRS instanceof CompoundCRS) { return createOperationStep(source, (CompoundCRS) targetCRS); } if (targetCRS instanceof SingleCRS) { return createOperationStep(source, (SingleCRS) targetCRS); } } if (targetCRS instanceof CompoundCRS) { final CompoundCRS target = (CompoundCRS) targetCRS; if (sourceCRS instanceof SingleCRS) { return createOperationStep((SingleCRS) sourceCRS, target); } } ///////////////////////////////////////////////////////////////////// //// //// //// Geographic --> Geographic, Projected or Geocentric //// //// //// ///////////////////////////////////////////////////////////////////// if (sourceCRS instanceof GeographicCRS) { final GeographicCRS source = (GeographicCRS) sourceCRS; if (targetCRS instanceof GeographicCRS) { return createOperationStep(source, (GeographicCRS) targetCRS); } if (targetCRS instanceof ProjectedCRS) { return createOperationStep(source, (ProjectedCRS) targetCRS); } if (targetCRS instanceof GeocentricCRS) { return createOperationStep(source, (GeocentricCRS) targetCRS); } if (targetCRS instanceof VerticalCRS) { return createOperationStep(source, (VerticalCRS) targetCRS); } } ///////////////////////////////////////////////////////// //// //// //// Projected --> Projected or Geographic //// //// //// ///////////////////////////////////////////////////////// if (sourceCRS instanceof ProjectedCRS) { final ProjectedCRS source = (ProjectedCRS) sourceCRS; if (targetCRS instanceof ProjectedCRS) { return createOperationStep(source, (ProjectedCRS) targetCRS); } if (targetCRS instanceof GeographicCRS) { return createOperationStep(source, (GeographicCRS) targetCRS); } } ////////////////////////////////////////////////////////// //// //// //// Geocentric --> Geocentric or Geographic //// //// //// ////////////////////////////////////////////////////////// if (sourceCRS instanceof GeocentricCRS) { final GeocentricCRS source = (GeocentricCRS) sourceCRS; if (targetCRS instanceof GeocentricCRS) { return createOperationStep(source, (GeocentricCRS) targetCRS); } if (targetCRS instanceof GeographicCRS) { return createOperationStep(source, (GeographicCRS) targetCRS); } } ///////////////////////////////////////// //// //// //// Vertical --> Vertical //// //// //// ///////////////////////////////////////// if (sourceCRS instanceof VerticalCRS) { final VerticalCRS source = (VerticalCRS) sourceCRS; if (targetCRS instanceof VerticalCRS) { return createOperationStep(source, (VerticalCRS) targetCRS); } } ///////////////////////////////////////// //// //// //// Temporal --> Temporal //// //// //// ///////////////////////////////////////// if (sourceCRS instanceof TemporalCRS) { final TemporalCRS source = (TemporalCRS) sourceCRS; if (targetCRS instanceof TemporalCRS) { return createOperationStep(source, (TemporalCRS) targetCRS); } } ////////////////////////////////////////////////////////////////// //// //// //// Any coordinate reference system --> Derived CRS //// //// //// ////////////////////////////////////////////////////////////////// if (targetCRS instanceof GeneralDerivedCRS) { // Note: this code is identical to 'createOperationStep(GeographicCRS, ProjectedCRS)' // except that the later invokes directly the right method for 'step1' instead // of invoking 'createOperation' recursively. final GeneralDerivedCRS target = (GeneralDerivedCRS) targetCRS; return concatenate(createOperation(sourceCRS, target.getBaseCRS()), target.getConversionFromBase()); } ////////////////////////////////////////////////////////////////// //// //// //// Derived CRS --> Any coordinate reference system //// //// //// ////////////////////////////////////////////////////////////////// if (sourceCRS instanceof GeneralDerivedCRS) { // Note: this code is identical to 'createOperationStep(ProjectedCRS, GeographicCRS)' // except that the later invokes directly the right method for 'step2' instead // of invoking 'createOperation' recursively. final GeneralDerivedCRS source = (GeneralDerivedCRS) sourceCRS; final CoordinateReferenceSystem base = source.getBaseCRS(); CoordinateOperation step1 = source.getConversionFromBase(); MathTransform transform = step1.getMathTransform(); try { transform = transform.inverse(); } catch (NoninvertibleTransformException exception) { throw new OperationNotFoundException(getErrorMessage(sourceCRS, base), exception); } step1 = createFromMathTransform(INVERSE_OPERATION, sourceCRS, base, transform); return concatenate(step1, createOperation(base, targetCRS)); } ///////////////////////////////////////// //// //// //// Generic --> various CS //// //// Various CS --> Generic //// //// //// ///////////////////////////////////////// if (sourceCRS == DefaultEngineeringCRS.GENERIC_2D || targetCRS == DefaultEngineeringCRS.GENERIC_2D || sourceCRS == DefaultEngineeringCRS.GENERIC_3D || targetCRS == DefaultEngineeringCRS.GENERIC_3D) { final int dimSource = getDimension(sourceCRS); final int dimTarget = getDimension(targetCRS); if (dimTarget == dimSource) { final Matrix matrix = Matrices.create(dimTarget+1, dimSource+1); return createFromAffineTransform(IDENTITY, sourceCRS, targetCRS, matrix); } } throw new OperationNotFoundException(getErrorMessage(sourceCRS, targetCRS)); } /** * Returns an operation using a particular method for conversion or transformation between * two coordinate reference systems. If the operation exists on the implementation, then it * is returned. If the operation does not exist on the implementation, then the implementation * has the option of inferring the operation from the argument objects. If for whatever reason * the specified operation will not be returned, then the exception is thrown. *

* Current implementation ignores the {@code method} argument. * This behavior may change in a future Geotk version. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @param method the algorithmic method for conversion or transformation. * @throws OperationNotFoundException if no operation path was found from {@code sourceCRS} * to {@code targetCRS}. * @throws FactoryException if the operation creation failed for some other reason. */ @Override public CoordinateOperation createOperation(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS, final OperationMethod method) throws OperationNotFoundException, FactoryException { ensureNonNull("method", method); // As a matter of principle. return createOperation(sourceCRS, targetCRS); } ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// //////////// //////////// //////////// N O R M A L I Z A T I O N S //////////// //////////// //////////// ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// /** * Makes sure that the specified geocentric CRS uses standard axis, prime meridian and * the specified datum. If {@code crs} already meets all those conditions, then it is * returned unchanged. Otherwise, a new normalized geocentric CRS is created and returned. * * @param crs The geocentric coordinate reference system to normalize. * @param datum The expected datum. * @return The normalized coordinate reference system. * @throws FactoryException if the construction of a new CRS was needed but failed. */ private GeocentricCRS normalize(final GeocentricCRS crs, final GeodeticDatum datum) throws FactoryException { final CartesianCS STANDARD = DefaultCartesianCS.GEOCENTRIC; final GeodeticDatum candidate = crs.getDatum(); if (equalsIgnorePrimeMeridian(candidate, datum)) { if (getGreenwichLongitude(candidate.getPrimeMeridian()) == getGreenwichLongitude(datum .getPrimeMeridian())) { if (hasStandardAxis(crs.getCoordinateSystem(), STANDARD)) { return crs; } } } final CRSFactory crsFactory = factories.getCRSFactory(); return crsFactory.createGeocentricCRS(getTemporaryName(crs), datum, STANDARD); } /** * Makes sure that the specified geographic CRS uses standard axis (longitude and latitude in * decimal degrees). Optionally, this method can also make sure that the CRS use the Greenwich * prime meridian. Other datum properties are left unchanged. If {@code crs} already meets all * those conditions, then it is returned unchanged. Otherwise, a new normalized geographic CRS * is created and returned. * * @param crs The geographic coordinate reference system to normalize. * @param forceGreenwich {@code true} for forcing the Greenwich prime meridian. * @return The normalized coordinate reference system. * @throws FactoryException if the construction of a new CRS was needed but failed. */ private GeographicCRS normalize(final GeographicCRS crs, final boolean forceGreenwich) throws FactoryException { GeodeticDatum datum = crs.getDatum(); final EllipsoidalCS cs = crs.getCoordinateSystem(); final EllipsoidalCS STANDARD = (cs.getDimension() <= 2) ? DefaultEllipsoidalCS.GEODETIC_2D : DefaultEllipsoidalCS.GEODETIC_3D; if (forceGreenwich && getGreenwichLongitude(datum.getPrimeMeridian()) != 0) { datum = new TemporaryDatum(datum); } else if (hasStandardAxis(cs, STANDARD)) { return crs; } /* * The specified geographic coordinate system doesn't use standard axis * (EAST, NORTH) or the greenwich meridian. Create a new one meeting those criterions. */ final CRSFactory crsFactory = factories.getCRSFactory(); return crsFactory.createGeographicCRS(getTemporaryName(crs), datum, STANDARD); } /** * A datum identical to the specified datum except for the prime meridian, which is replaced * by Greenwich. This datum is processed in a special way by {@link #equalsIgnorePrimeMeridian}. */ private static final class TemporaryDatum extends DefaultGeodeticDatum { /** For cross-version compatibility. */ private static final long serialVersionUID = -8964199103509187219L; /** The wrapped datum. */ private final GeodeticDatum datum; /** Wrap the specified datum. */ public TemporaryDatum(final GeodeticDatum datum) { super(getTemporaryName(datum), datum.getEllipsoid()); this.datum = datum; } /** Unwrap the datum. */ public static GeodeticDatum unwrap(GeodeticDatum datum) { while (datum instanceof TemporaryDatum) { datum = ((TemporaryDatum) datum).datum; } return datum; } /** Compares this datum with the specified object for equality. */ @Override public boolean equals(final Object object, final ComparisonMode mode) { if (object instanceof TemporaryDatum && super.equals(object, mode)) { final GeodeticDatum other = ((TemporaryDatum) object).datum; switch (mode) { case STRICT: return datum.equals(other); default: return equalsIgnoreMetadata(datum, other); } } return false; } } /** * Returns {@code true} if the specified coordinate system uses standard axis and units. * * @param crs The coordinate system to test. * @param standard The coordinate system that defines the standard. Usually * {@link DefaultEllipsoidalCS#GEODETIC_2D} or {@link DefaultCartesianCS#PROJECTED}. */ private static boolean hasStandardAxis(final CoordinateSystem cs, final CoordinateSystem standard) { final int dimension = standard.getDimension(); if (cs.getDimension() != dimension) { return false; } for (int i=0; i * If a conversion from 2D to 3D is requested, this method will set the elevation * ordinate to 0 in the target 3D coordinates. In other words, the coordinates are * assumed on the ellipsoid surface. This is done that way for consistency with * {@link org.geotoolkit.referencing.operation.transform.MolodenskyTransform}, which * also assumes that the points are on the ellipsoid when the z values are * missing (while in the Molodenski case, the result is more complicated than just 0). * * @param sourceCS The source coordinate system. * @param targetCS The target coordinate system. * @param sourcePM The source prime meridian. * @param targetPM The target prime meridian. * @return The transformation from {@code sourceCS} to {@code targetCS} as * an affine transform. Only axis orientation, units and prime meridian are * taken in account. * @throws OperationNotFoundException If the affine transform can't be constructed. */ private Matrix swapAndScaleAxis(final EllipsoidalCS sourceCS, final EllipsoidalCS targetCS, final PrimeMeridian sourcePM, final PrimeMeridian targetPM) throws OperationNotFoundException { /* * Compute swapAndScaleAxis(sourceCS, targetCS) with a special case for the conversion * from 2D to 3D Geographic CRS: we expect that the new dimension is the ellipsoidal * height, so we create a Matrix which will "generate" the new ordinates with NaN value. */ Matrix matrix = null; if (sourceCS.getDimension() == 2 && targetCS.getDimension() == 3) { for (int k=3; --k>=0;) { if (AxisDirection.UP.equals(AxisDirections.absolute(targetCS.getAxis(k).getDirection()))) { final CoordinateSystemAxis axis0 = targetCS.getAxis(k != 0 ? 0 : 1); final CoordinateSystemAxis axis1 = targetCS.getAxis(k != 2 ? 2 : 1); final EllipsoidalCS step = new DefaultEllipsoidalCS("Step", axis0, axis1); final Matrix reduced = swapAndScaleAxis(sourceCS, step); assert reduced.getNumRow() == 3 && reduced.getNumCol() == 3 : reduced; matrix = Matrices.create(4, 3); matrix.setElement(3, 2, 1); for (int jm=0,j=0; j<3; j++) { if (j == k) { matrix.setElement(j, j, 0); // Translation term intentionally left to 0 - see method javadoc. } else { for (int i=3; --i>=0;) { matrix.setElement(jm, i, reduced.getElement(j, i)); } jm++; } } break; } } } if (matrix == null) { matrix = swapAndScaleAxis(sourceCS, targetCS); } for (int i=targetCS.getDimension(); --i>=0;) { final CoordinateSystemAxis axis = targetCS.getAxis(i); final AxisDirection direction = axis.getDirection(); if (AxisDirection.EAST.equals(AxisDirections.absolute(direction))) { /* * A longitude ordinate has been found (i.e. the axis is oriented toward EAST or * WEST). Compute the amount of angle to add to the source longitude in order to * get the destination longitude. This amount is measured in units of the target * axis. The affine transform is then updated in order to take this rotation in * account. Note that the resulting longitude may be outside the usual [-180..180°] * range. */ final Unit unit = axis.getUnit().asType(Angle.class); final double sourceLongitude = getGreenwichLongitude(sourcePM, unit); final double targetLongitude = getGreenwichLongitude(targetPM, unit); final int lastMatrixColumn = matrix.getNumCol()-1; double rotate = sourceLongitude - targetLongitude; if (AxisDirection.WEST.equals(direction)) { rotate = -rotate; } rotate += matrix.getElement(i, lastMatrixColumn); matrix.setElement(i, lastMatrixColumn, rotate); } } return matrix; } ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// //////////// //////////// //////////// T R A N S F O R M A T I O N S S T E P S //////////// //////////// //////////// ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// /** * Creates an operation between two temporal coordinate reference systems. * The default implementation checks if both CRS use the same datum, and * then adjusts for axis direction, units and epoch. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final TemporalCRS sourceCRS, final TemporalCRS targetCRS) throws FactoryException { final TemporalDatum sourceDatum = sourceCRS.getDatum(); final TemporalDatum targetDatum = targetCRS.getDatum(); /* * Compute the epoch shift. The epoch is the time "0" in a particular coordinate * reference system. For example, the epoch for java.util.Date object is january 1, * 1970 at 00:00 UTC. We compute how much to add to a time in 'sourceCRS' in order * to get a time in 'targetCRS'. This "epoch shift" is in units of 'targetCRS'. */ final TimeCS sourceCS = sourceCRS.getCoordinateSystem(); final TimeCS targetCS = targetCRS.getCoordinateSystem(); final Unit targetUnit = targetCS.getAxis(0).getUnit().asType(Duration.class); double epochShift = sourceDatum.getOrigin().getTime() - targetDatum.getOrigin().getTime(); epochShift = MILLISECOND.getConverterTo(targetUnit).convert(epochShift); /* * Check axis orientation. The method 'swapAndScaleAxis' should returns a matrix * of size 2x2. The element at index (0,0) may be 1 if sourceCRS and targetCRS axis * are in the same direction, or -1 if there are in opposite direction (e.g. * "PAST" vs "FUTURE"). This number may be something else than -1 or +1 if a unit * conversion was applied too, for example 60 if time in 'sourceCRS' was in hours * while time in 'targetCRS' was in minutes. * * The "epoch shift" previously computed is a translation. * Consequently, it is added to element (0,1). */ final Matrix matrix = swapAndScaleAxis(sourceCS, targetCS); final int translationColumn = matrix.getNumCol() - 1; if (translationColumn >= 0) { // Paranoiac check: should always be 1. final double translation = matrix.getElement(0, translationColumn); matrix.setElement(0, translationColumn, translation + epochShift); } return createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix); } /** * Creates an operation between two vertical coordinate reference systems. * The default implementation checks if both CRS use the same datum, and * then adjusts for axis direction and units. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final VerticalCRS sourceCRS, final VerticalCRS targetCRS) throws FactoryException { final VerticalDatum sourceDatum = sourceCRS.getDatum(); final VerticalDatum targetDatum = targetCRS.getDatum(); if (!equalsIgnoreMetadata(sourceDatum, targetDatum)) { throw new OperationNotFoundException(getErrorMessage(sourceDatum, targetDatum)); } final VerticalCS sourceCS = sourceCRS.getCoordinateSystem(); final VerticalCS targetCS = targetCRS.getCoordinateSystem(); final Matrix matrix = swapAndScaleAxis(sourceCS, targetCS); return createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix); } /** * Creates an operation between a geographic and a vertical coordinate reference systems. * The default implementation accepts the conversion only if the geographic CRS is a * tridimensional one and the vertical CRS is for height above the ellipsoid. * More elaborated operation, like transformation from ellipsoidal to geoidal height, * should be implemented here. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. * * @todo Implement GEOT-352 here. */ protected CoordinateOperation createOperationStep(final GeographicCRS sourceCRS, final VerticalCRS targetCRS) throws FactoryException { if (VerticalDatumTypes.ELLIPSOIDAL.equals(targetCRS.getDatum().getVerticalDatumType())) { final Matrix matrix = swapAndScaleAxis(sourceCRS.getCoordinateSystem(), targetCRS.getCoordinateSystem()); return createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix); } throw new OperationNotFoundException(getErrorMessage(sourceCRS, targetCRS)); } /** * Creates an operation between two geographic coordinate reference systems. The default * implementation can adjust axis order and orientation (e.g. transforming from * {@code (NORTH,WEST)} to {@code (EAST,NORTH)}), performs units conversion * and apply datum shifts if needed. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. * * @todo When rotating the prime meridian, we should ensure that * transformed longitudes stay in the range [-180..+180°]. */ protected CoordinateOperation createOperationStep(final GeographicCRS sourceCRS, final GeographicCRS targetCRS) throws FactoryException { final EllipsoidalCS sourceCS = sourceCRS.getCoordinateSystem(); final EllipsoidalCS targetCS = targetCRS.getCoordinateSystem(); final GeodeticDatum sourceDatum = sourceCRS.getDatum(); final GeodeticDatum targetDatum = targetCRS.getDatum(); final PrimeMeridian sourcePM = sourceDatum.getPrimeMeridian(); final PrimeMeridian targetPM = targetDatum.getPrimeMeridian(); if (equalsIgnorePrimeMeridian(sourceDatum, targetDatum)) { /* * If both geographic CRS use the same datum, then there is no need for a datum shift. * Just swap axis order, and rotate the longitude coordinate if prime meridians are * different. Note: this special block is mandatory for avoiding never-ending loop, * since it is invoked by 'createOperationStep(GeocentricCRS...)'. * * TODO: We should ensure that longitude is in range [-180..+180°]. */ final Matrix matrix = swapAndScaleAxis(sourceCS, targetCS, sourcePM, targetPM); return createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix); } /* * The two geographic CRS use different datum. If Molodensky transformations * are allowed, try them first. Note that in some cases if the datum shift can't * be performed in a single Molodensky transformation step (i.e. if we need to * go through at least one intermediate datum), then we will use the geocentric * transform below instead: it allows to concatenate many Bursa Wolf parameters * in a single affine transform. */ if (molodenskyMethod != null) { ReferenceIdentifier identifier = DATUM_SHIFT; BursaWolfParameters bursaWolf = null; if (sourceDatum instanceof DefaultGeodeticDatum) { bursaWolf = ((DefaultGeodeticDatum) sourceDatum).getBursaWolfParameters(targetDatum); } if (bursaWolf == null) { /* * No direct path found. Try the more expensive matrix calculation, and * see if we can retrofit the result in a BursaWolfParameters object. */ final Matrix shift = DefaultGeodeticDatum.getAffineTransform(sourceDatum, targetDatum); if (shift != null) try { bursaWolf = new BursaWolfParameters(targetDatum); bursaWolf.setAffineTransform(shift, 1E-4); } catch (IllegalArgumentException ignore) { /* * A matrix exists, but we are unable to retrofit it as a set of Bursa-Wolf * parameters. Do NOT set the 'bursaWolf' variable: it must stay null, which * means to perform the datum shift using geocentric coordinates. */ } else if (lenientDatumShift) { /* * No BursaWolf parameters available. No affine transform to be applied in * geocentric coordinates are available neither (the "shift" matrix above), * so performing a geocentric transformation will not help. But the user wants * us to perform the datum shift anyway. We will notify the user through * positional accuracy, which is set indirectly through ELLIPSOID_SHIFT. */ bursaWolf = new BursaWolfParameters(targetDatum); identifier = ELLIPSOID_SHIFT; } } /* * Applies the Molodensky transformation now. Note: in current parameters, we can't * specify a different input and output dimension. However, our Molodensky transform * allows that. We should expand the parameters block for this case (TODO). */ if (bursaWolf != null && bursaWolf.isTranslation()) { final Ellipsoid sourceEllipsoid = sourceDatum.getEllipsoid(); final Ellipsoid targetEllipsoid = targetDatum.getEllipsoid(); if (bursaWolf.isIdentity() && equalsIgnoreMetadata(sourceEllipsoid, targetEllipsoid)) { final Matrix matrix = swapAndScaleAxis(sourceCS, targetCS, sourcePM, targetPM); return createFromAffineTransform(identifier, sourceCRS, targetCRS, matrix); } final int sourceDim = getDimension(sourceCRS); final int targetDim = getDimension(targetCRS); final ParameterValueGroup parameters; parameters = getMathTransformFactory().getDefaultParameters(molodenskyMethod); parameters.parameter("src_semi_major").setValue(sourceEllipsoid.getSemiMajorAxis()); parameters.parameter("src_semi_minor").setValue(sourceEllipsoid.getSemiMinorAxis()); parameters.parameter("tgt_semi_major").setValue(targetEllipsoid.getSemiMajorAxis()); parameters.parameter("tgt_semi_minor").setValue(targetEllipsoid.getSemiMinorAxis()); parameters.parameter("dx").setValue(bursaWolf.dx); parameters.parameter("dy").setValue(bursaWolf.dy); parameters.parameter("dz").setValue(bursaWolf.dz); parameters.parameter("dim").setValue(sourceDim); boolean ready = true; if (sourceDim != targetDim) try { // Following is Geotk-specific, so it may not be supported // if the math transform provider come from an other library. parameters.parameter("src_dim").setValue(sourceDim); parameters.parameter("tgt_dim").setValue(targetDim); } catch (ParameterNotFoundException e) { ready = false; // Will fallback on geocentric transformation. Logging.recoverableException(LOGGER, DefaultCoordinateOperationFactory.class, "createOperationStep", e); } if (ready) { final GeographicCRS normSourceCRS = normalize(sourceCRS, true); final GeographicCRS normTargetCRS = normalize(targetCRS, true); return concatenate( createOperationStep(sourceCRS, normSourceCRS), createFromParameters(identifier, normSourceCRS, normTargetCRS, parameters), createOperationStep(normTargetCRS, targetCRS)); } } } /* * If the two geographic CRS use different datum, transform from the * source to target datum through the geocentric coordinate system. * The transformation chain is: * * source geographic CRS --> * geocentric CRS with a preference for datum using Greenwich meridian --> * target geographic CRS */ final CartesianCS STANDARD = DefaultCartesianCS.GEOCENTRIC; final GeocentricCRS stepCRS; final CRSFactory crsFactory = factories.getCRSFactory(); if (getGreenwichLongitude(targetPM) == 0) { stepCRS = crsFactory.createGeocentricCRS( getTemporaryName(targetCRS), targetDatum, STANDARD); } else { stepCRS = crsFactory.createGeocentricCRS( getTemporaryName(sourceCRS), sourceDatum, STANDARD); } return concatenate(createOperationStep(sourceCRS, stepCRS), createOperationStep(stepCRS, targetCRS)); } /** * Creates an operation between two projected coordinate reference systems. * The default implementation can adjust axis order and orientation. It also * performs units conversion if it is the only extra change needed. Otherwise, * it performs three steps: * *

    *
  • Unproject from {@code sourceCRS} to its base * {@linkplain GeographicCRS geographic CRS}.
  • *
  • Convert the source to target base geographic CRS.
  • *
  • Project from the base {@linkplain GeographicCRS geographic CRS} * to the {@code targetCRS}.
  • *
* * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final ProjectedCRS sourceCRS, final ProjectedCRS targetCRS) throws FactoryException { /* * Apply the transformation in 3 steps (the 3 arrows below): * * source projected CRS --(unproject)--> * source geographic CRS ---------------> * target geographic CRS ---(project)---> * target projected CRS */ final GeographicCRS sourceGeo = sourceCRS.getBaseCRS(); final GeographicCRS targetGeo = targetCRS.getBaseCRS(); return concatenate(tryDB(sourceCRS, sourceGeo), tryDB(sourceGeo, targetGeo), tryDB(targetGeo, targetCRS)); } /** * Creates an operation from a geographic to a projected coordinate reference system. * The default implementation constructs the following operation chain: * *
* sourceCRS → {@linkplain ProjectedCRS#getBaseCRS baseCRS} → targetCRS *
* * where the conversion from {@code baseCRS} to {@code targetCRS} is obtained from * targetCRS.{@linkplain ProjectedCRS#getConversionFromBase getConversionFromBase()}. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final GeographicCRS sourceCRS, final ProjectedCRS targetCRS) throws FactoryException { return concatenate(tryDB(sourceCRS, targetCRS.getBaseCRS()), targetCRS.getConversionFromBase()); } /** * Creates an operation from a projected to a geographic coordinate reference system. * The default implementation constructs the following operation chain: * *
* sourceCRS → {@linkplain ProjectedCRS#getBaseCRS baseCRS} → targetCRS *
* * where the conversion from {@code sourceCRS} to {@code baseCRS} is obtained from the inverse of * sourceCRS.{@linkplain ProjectedCRS#getConversionFromBase getConversionFromBase()}. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. * * @todo Provides a non-null method. */ protected CoordinateOperation createOperationStep(final ProjectedCRS sourceCRS, final GeographicCRS targetCRS) throws FactoryException { final GeographicCRS base = sourceCRS.getBaseCRS(); CoordinateOperation step1 = sourceCRS.getConversionFromBase(); MathTransform transform = step1.getMathTransform(); try { transform = transform.inverse(); } catch (NoninvertibleTransformException exception) { throw new OperationNotFoundException(getErrorMessage(sourceCRS, base), exception); } step1 = createFromMathTransform(INVERSE_OPERATION, sourceCRS, base, transform); return concatenate(step1, tryDB(base, targetCRS)); } /** * Creates an operation between two geocentric coordinate reference systems. * The default implementation can adjust for axis order and orientation, * performs units conversion and apply Bursa Wolf transformation if needed. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. * * @todo Rotation of prime meridian not yet implemented. * @todo Transformation version set to "(unknown)". We should search this information somewhere. */ protected CoordinateOperation createOperationStep(final GeocentricCRS sourceCRS, final GeocentricCRS targetCRS) throws FactoryException { final GeodeticDatum sourceDatum = sourceCRS.getDatum(); final GeodeticDatum targetDatum = targetCRS.getDatum(); final CoordinateSystem sourceCS = sourceCRS.getCoordinateSystem(); final CoordinateSystem targetCS = targetCRS.getCoordinateSystem(); final double sourcePM, targetPM; sourcePM = getGreenwichLongitude(sourceDatum.getPrimeMeridian()); targetPM = getGreenwichLongitude(targetDatum.getPrimeMeridian()); if (equalsIgnorePrimeMeridian(sourceDatum, targetDatum)) { if (sourcePM == targetPM) { /* * If both CRS use the same datum and the same prime meridian, * then the transformation is probably just axis swap or unit * conversions. */ final Matrix matrix = swapAndScaleAxis(sourceCS, targetCS); return createFromAffineTransform(AXIS_CHANGES, sourceCRS, targetCRS, matrix); } // Prime meridians are differents. Performs the full transformation. } if (sourcePM != targetPM) { throw new OperationNotFoundException("Rotation of prime meridian not yet implemented"); } /* * Transform between differents ellipsoids using Bursa Wolf parameters. * The Bursa Wolf parameters are used with "standard" geocentric CS, i.e. * with x axis towards the prime meridian, y axis towards East and z axis * toward North. The following steps are applied: * * source CRS --> * standard CRS with source datum --> * standard CRS with target datum --> * target CRS */ final CartesianCS STANDARD = DefaultCartesianCS.GEOCENTRIC; final XMatrix matrix; ReferenceIdentifier identifier = DATUM_SHIFT; try { Matrix datumShift = DefaultGeodeticDatum.getAffineTransform( TemporaryDatum.unwrap(sourceDatum), TemporaryDatum.unwrap(targetDatum)); if (datumShift == null) { if (lenientDatumShift) { datumShift = new Matrix4(); // Identity transform. identifier = ELLIPSOID_SHIFT; } else { throw new OperationNotFoundException(Errors.format( Errors.Keys.BURSA_WOLF_PARAMETERS_REQUIRED)); } } final Matrix normalizeSource = swapAndScaleAxis(sourceCS, STANDARD); final Matrix normalizeTarget = swapAndScaleAxis(STANDARD, targetCS); /* * Since all steps are matrix, we can multiply them into a single matrix operation. * Note: XMatrix.multiply(XMatrix) is equivalents to AffineTransform.concatenate(...): * First transform by the supplied transform and then transform the result * by the original transform. * * We compute: matrix = normalizeTarget * datumShift * normalizeSource */ matrix = new Matrix4(normalizeTarget); matrix.multiply(datumShift); matrix.multiply(normalizeSource); } catch (SingularMatrixException cause) { throw new OperationNotFoundException(getErrorMessage(sourceDatum, targetDatum), cause); } return createFromAffineTransform(identifier, sourceCRS, targetCRS, matrix); } /** * Creates an operation from a geographic to a geocentric coordinate reference systems. * If the source CRS doesn't have a vertical axis, height above the ellipsoid will be * assumed equal to zero everywhere. The default implementation uses the * {@code "Ellipsoid_To_Geocentric"} math transform. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final GeographicCRS sourceCRS, final GeocentricCRS targetCRS) throws FactoryException { /* * This transformation is a 3 steps process: * * source geographic CRS --> * normalized geographic CRS --> * normalized geocentric CRS --> * target geocentric CRS * * "Normalized" means that axis point toward standards direction (East, North, etc.), * units are metres or decimal degrees, prime meridian is Greenwich and height is measured * above the ellipsoid. However, the horizontal datum is preserved. */ final GeographicCRS normSourceCRS = normalize(sourceCRS, true); final GeodeticDatum datum = normSourceCRS.getDatum(); final GeocentricCRS normTargetCRS = normalize(targetCRS, datum); final Ellipsoid ellipsoid = datum.getEllipsoid(); final Unit unit = ellipsoid.getAxisUnit(); final ParameterValueGroup param; param = getMathTransformFactory().getDefaultParameters("Ellipsoid_To_Geocentric"); param.parameter("semi_major").setValue(ellipsoid.getSemiMajorAxis(), unit); param.parameter("semi_minor").setValue(ellipsoid.getSemiMinorAxis(), unit); param.parameter("dim") .setValue(getDimension(normSourceCRS)); return concatenate( createOperationStep (sourceCRS, normSourceCRS), createFromParameters(GEOCENTRIC_CONVERSION, normSourceCRS, normTargetCRS, param), createOperationStep (normTargetCRS, targetCRS)); } /** * Creates an operation from a geocentric to a geographic coordinate reference systems. * The default implementation use the "Geocentric_To_Ellipsoid" math transform. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final GeocentricCRS sourceCRS, final GeographicCRS targetCRS) throws FactoryException { final GeographicCRS normTargetCRS = normalize(targetCRS, true); final GeodeticDatum datum = normTargetCRS.getDatum(); final GeocentricCRS normSourceCRS = normalize(sourceCRS, datum); final Ellipsoid ellipsoid = datum.getEllipsoid(); final Unit unit = ellipsoid.getAxisUnit(); final ParameterValueGroup param; param = getMathTransformFactory().getDefaultParameters("Geocentric_To_Ellipsoid"); param.parameter("semi_major").setValue(ellipsoid.getSemiMajorAxis(), unit); param.parameter("semi_minor").setValue(ellipsoid.getSemiMinorAxis(), unit); param.parameter("dim") .setValue(getDimension(normTargetCRS)); final CoordinateOperation step1, step2, step3; step1 = createOperationStep (sourceCRS, normSourceCRS); step2 = createFromParameters(GEOCENTRIC_CONVERSION, normSourceCRS, normTargetCRS, param); step3 = createOperationStep (normTargetCRS, targetCRS); return concatenate(step1, step2, step3); } /** * Creates an operation from a compound to a single coordinate reference systems. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. * * @todo (GEOTK-83) * This method works for some simple cases (e.g. no datum change), and give up * otherwise. Before to give up at the end of this method, we should try the following: *
    *
  • Maybe {@code sourceCRS} uses a non-ellipsoidal height. We should replace * the non-ellipsoidal height by an ellipsoidal one, create a transformation step * for that (to be concatenated), and then try again this operation step.
  • * *
  • Maybe {@code sourceCRS} contains some extra axis, like a temporal CRS. * We should revisit this code in other to lets supplemental ordinates to be * pass through or removed.
  • *
*/ protected CoordinateOperation createOperationStep(final CompoundCRS sourceCRS, final SingleCRS targetCRS) throws FactoryException { final List sources = DefaultCompoundCRS.getSingleCRS(sourceCRS); if (sources.size() == 1) { return createOperation(sources.get(0), targetCRS); } if (needsGeodetic3D(sources, targetCRS, true)) { /* * There is a change of datum. It may be a vertical datum change (for example from * ellipsoidal to geoidal height), in which case geographic coordinates are usually * needed. It may also be a geodetic datum change, in which case the height is part * of computation. Try to convert the source CRS into a 3D-geodetic CRS. */ final CoordinateReferenceSystem source3D = factories.toGeodetic3D(sourceCRS); if (source3D != sourceCRS) { return createOperation(source3D, targetCRS); } /* * TODO: Search for non-ellipsoidal height, and lets supplemental axis (e.g. time) * pass through. See javadoc comments above. */ if (!lenientDatumShift && needsGeodetic3D(sources, targetCRS, false)) { throw new OperationNotFoundException(getErrorMessage(sourceCRS, targetCRS)); } } // No need for a datum change (see 'needGeodetic3D' javadoc). final List targets = singletonList(targetCRS); return createOperationStep(sourceCRS, sources, targetCRS, targets); } /** * Creates an operation from a single to a compound coordinate reference system. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final SingleCRS sourceCRS, final CompoundCRS targetCRS) throws FactoryException { final List targets = DefaultCompoundCRS.getSingleCRS(targetCRS); if (targets.size() == 1) { return createOperation(sourceCRS, targets.get(0)); } /* * This method has almost no chance to succeed (we can't invent ordinate values!) unless * 'sourceCRS' is a 3D-geodetic CRS and 'targetCRS' is a 2D + 1D one. Test for this case. * Otherwise, the 'createOperationStep' invocation will throws the appropriate exception. */ final CoordinateReferenceSystem target3D = factories.toGeodetic3D(targetCRS); if (target3D != targetCRS) { return createOperation(sourceCRS, target3D); } final List sources = singletonList(sourceCRS); return createOperationStep(sourceCRS, sources, targetCRS, targets); } /** * Creates an operation between two compound coordinate reference systems. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ protected CoordinateOperation createOperationStep(final CompoundCRS sourceCRS, final CompoundCRS targetCRS) throws FactoryException { final List sources = DefaultCompoundCRS.getSingleCRS(sourceCRS); final List targets = DefaultCompoundCRS.getSingleCRS(targetCRS); if (targets.size() == 1) { return createOperation(sourceCRS, targets.get(0)); } if (sources.size() == 1) { // After 'targets' because more likely to fails to transform. return createOperation(sources.get(0), targetCRS); } /* * If the source CRS contains both a geodetic and a vertical CRS, then we can process * only if there is no datum change. If at least one of those CRS appears in the target * CRS with a different datum, then the datum shift must be applied on the horizontal and * vertical components together. */ for (final SingleCRS target : targets) { if (needsGeodetic3D(sources, target, true)) { final CoordinateReferenceSystem source3D = factories.toGeodetic3D(sourceCRS); final CoordinateReferenceSystem target3D = factories.toGeodetic3D(targetCRS); if (source3D != sourceCRS || target3D != targetCRS) { return createOperation(source3D, target3D); } /* * TODO: Search for non-ellipsoidal height, and lets supplemental axis pass through. * See javadoc comments for createOperation(CompoundCRS, SingleCRS). */ if (!lenientDatumShift && needsGeodetic3D(sources, target, false)) { throw new OperationNotFoundException(getErrorMessage(sourceCRS, targetCRS)); } } } // No need for a datum change (see 'needGeodetic3D' javadoc). return createOperationStep(sourceCRS, sources, targetCRS, targets); } /** * Implementation of transformation step on compound CRS. * * {@note * If there is a horizontal (geographic or projected) CRS together with a vertical CRS, * then we can't performs the transformation since the vertical value has an impact on * the horizontal value, and this impact is not taken in account if the horizontal and * vertical components are not together in a 3D geographic CRS. This case occurs when * the vertical CRS is not a height above the ellipsoid. It must be checked by the * caller before this method is invoked.} * * @param sourceCRS Input coordinate reference system. * @param sources The source CRS components. * @param targetCRS Output coordinate reference system. * @param targets The target CRS components. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS}. * @throws FactoryException If the operation can't be constructed. */ private CoordinateOperation createOperationStep( final CoordinateReferenceSystem sourceCRS, final List sources, final CoordinateReferenceSystem targetCRS, final List targets) throws FactoryException { /* * Try to find operations from source CRSs to target CRSs. All pairwise combinations are * tried, but the preference is given to CRS in the same order (source[0] with target[0], * source[1] with target[1], etc.). Operations found are stored in 'steps', but are not * yet given to pass through transforms. We need to know first if some ordinate values * need reordering (for matching the order of target CRS) if any ordinates reordering and * source ordinates drops are required. */ final int sourceDim = getDimension(sourceCRS); final CoordinateOperation[] subOps = new CoordinateOperation[targets.size()]; final boolean[] sourceIsUsed = new boolean [sources.size()]; final SingleCRS[] orderedSources = new SingleCRS [subOps.length]; final int[] srcToOrderedSrc = new int [sourceDim]; int subOpCount=0, orderedSourceDim = 0; search: for (int j=0; j= subOpCount) { // If all remaining parts are identity transforms, // then we have reached the final target CRS. stepTargetCRS = targetCRS; } else if (subTransform.isIdentity()) { // In any identity transform, the source and target CRS are // equal. So we don't need to create a new stepTargetCRS. stepTargetCRS = stepSourceCRS; } else if (orderedSources.length == 1) { // Slight optimization of the next block, keeping in mind // that orderedSources[0] has been set to 'target' above. stepTargetCRS = target; } else { stepTargetCRS = factories.getCRSFactory().createCompoundCRS( getTemporaryName(target), orderedSources); } int delta = getDimension(source); final int lower = upper; upper += delta; /* * Constructs the pass through transform only if there is at least one ordinate to * pass. Actually the code below would work inconditionally, but we perform this * check anyway for avoiding the creation of intermediate objects. */ if (!(lower == 0 && upper == orderedSourceDim)) { final MathTransform step = getMathTransformFactory() .createPassThroughTransform(lower, subTransform, orderedSourceDim - upper); final Map properties = IdentifiedObjects.getProperties(subOperation); /* * The DefaultPassThroughOperation constuctor expect a SingleOperation. * In most case, the 'subOperation' is already of this kind. However if * it is not, try to copy it in such object. */ final SingleOperation op; if (subOperation instanceof SingleOperation) { op = (SingleOperation) subOperation; } else { op = (SingleOperation) DefaultSingleOperation.create(properties, subOperation.getSourceCRS(), subOperation.getTargetCRS(), subTransform, new DefaultOperationMethod(subTransform), subOperation.getClass()); } subOperation = new DefaultPassThroughOperation(properties, stepSourceCRS, stepTargetCRS, op, step); } /* * Concatenate the operation with the ones we have found so far, and use the * current 'stepTargetCRS' as the source CRS for the next operation step. We * also need to adjust the dimension indices, since the preivous operations * may have removed some dimensions. Note that the delta may also be negative * in a few occasions. */ operation = (operation == null) ? subOperation : concatenate(operation, subOperation); stepSourceCRS = stepTargetCRS; delta -= getDimension(target); upper -= delta; orderedSourceDim -= delta; } assert upper == orderedSourceDim : upper; return operation; } /** * Returns {@code true} if a transformation path from {@code sourceCRS} to {@code targetCRS} * is likely to require a tri-dimensional geodetic CRS as an intermediate step. This method * is used for enforcing the following rule: ellipsoidal height shall never be separated * from the geographic coordinates, because some transformation steps (the datum shift) * require the full 3D coordinates even if the result is two-dimensional. *

* We relax the above rule by returning {@code false} if using the (2D + 1D) components * rather than a single 3D CRS is not expected to change the numerical result. We do that * because replacing a (2D + 1D) pair by a 3D singleton cause a lost of information (like * the authority codes). More specifically, this method returns {@code false} if at least * one of the following conditions is meet: * *

    *
  • The target datum is neither a vertical or geodetic datum. Consequently, * eventual datum change (typically a temporal datum change) is not the caller * business. It will be handled by the generic method above.

  • * *
  • There is geodetic datum change. While a 3D SingleCRS is conceptually required, * it is not numerically required if the the target CRS is not geodetic 3D.

  • * *
  • A datum change is required, but source CRS doesn't have both a geodetic * and a vertical CRS, so we can't apply a 3D datum shift anyway.

  • *
* * @param strict {@code false} if we tolerate some error (typically up to 3 metres). * To be set to {@code false} only in last resort before throwing an exception. * Must be {@code true} in all other cases. */ private static boolean needsGeodetic3D(final List sourceCRS, final SingleCRS targetCRS, final boolean strict) { final Datum targetDatum = targetCRS.getDatum(); final boolean targetIsGeodetic = (targetDatum instanceof GeodeticDatum); if (!targetIsGeodetic && !(targetDatum instanceof VerticalDatum)) { return false; } boolean hasHorizontal = false; // Whatever at least one source component is horizontal. boolean hasVertical = false; // Whatever at least one source component is vertical. boolean needDatumShift = false; for (final SingleCRS crs : sourceCRS) { if (crs.getCoordinateSystem().getDimension() >= 3) { /* * If the CRS is already three-dimensional, we don't have to rebuild it. * This method shall return 'true' only if there is (2D + 1D) to combine. */ continue; } final Datum sourceDatum = crs.getDatum(); final boolean sourceIsGeodetic = (sourceDatum instanceof GeodeticDatum); if (sourceIsGeodetic) { hasHorizontal = true; } else if (sourceDatum instanceof VerticalDatum) { hasVertical = true; } else { continue; } /* * If we have found the source component of the same kind than the target element * (either a GeodeticDatum or a VerticalDatum), check if there is a need for a datum * shift. The other source components are ignored, which is okay if the target CRS is * only 1D or 2D since this means that the extra source components will be discarded * anyway. The case where the target CRS is 3D is handled at the end of this method. */ if (!needDatumShift && sourceIsGeodetic == targetIsGeodetic) { assert Classes.implementSameInterfaces(sourceDatum.getClass(), targetDatum.getClass(), Datum.class) : targetDatum; if (sourceIsGeodetic && targetIsGeodetic) { final GeodeticDatum sd = (GeodeticDatum) sourceDatum; final GeodeticDatum td = (GeodeticDatum) targetDatum; if (strict) { needDatumShift = !equalsIgnorePrimeMeridian(sd, td); } else { needDatumShift = isDatumShiftRequired(sd, td); } } else { needDatumShift = !equalsIgnoreMetadata(sourceDatum, targetDatum); } } } return hasHorizontal && hasVertical && (needDatumShift || targetCRS.getCoordinateSystem().getDimension() >= 3); } ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// //////////// //////////// //////////// M I S C E L L A N E O U S //////////// //////////// //////////// ///////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////// /** * Returns {@code true} if conversions between CRS using the given datum are likely to require * a datum shift. This method is not provided in the public API because it is not correct. For * example NAD83 and WGS84 are close enough for having Bursa-Wolf parameters set to 0, but are * nevertheless not identical. They don't have the same ellipsoid for instance. Considering * them as equal is okay only if we don't need an accuracy better than 3 metres. *

* Use this method only in last resort, before throwing an exception if we can't consider * that there is no datum shift. In current implementation, this method is used only in * the context of {@link CompoundCRS}. * * @param source The datum of the source CRS. * @param target The datum of the target CRS * @return {@code true} If conversions between CRS using the given datum are likely to require * a datum shift. Note that a return value of {@code false} does not mean that there is * no rotation of prime meridian. * * @since 3.07 */ private static boolean isDatumShiftRequired(final GeodeticDatum source, final GeodeticDatum target) { if (equalsApproximatively(source, target)) { return false; } if (source instanceof DefaultGeodeticDatum) { final BursaWolfParameters param = ((DefaultGeodeticDatum) source).getBursaWolfParameters(target); if (param != null && param.isIdentity()) { return false; } } if (target instanceof DefaultGeodeticDatum) { final BursaWolfParameters param = ((DefaultGeodeticDatum) target).getBursaWolfParameters(source); if (param != null && param.isIdentity()) { return false; } } return true; } /** * Compares the specified datum for equality, except the prime meridian. In principle, * comparing the datum names should be sufficient. However we also compare ellipsoids * for safety, with tolerance for rounding errors since the datum names were supposed * to be the main criterion. * * @param object1 The first object to compare (may be null). * @param object2 The second object to compare (may be null). * @return {@code true} if both objects are equals. */ private static boolean equalsIgnorePrimeMeridian(GeodeticDatum object1, GeodeticDatum object2) { object1 = TemporaryDatum.unwrap(object1); object2 = TemporaryDatum.unwrap(object2); if (equalsApproximatively(object1.getEllipsoid(), object2.getEllipsoid())) { return nameMatches(object1, object2.getName().getCode()) || nameMatches(object2, object1.getName().getCode()); } return false; } /** * Tries to get a coordinate operation from a database (typically EPSG). The exact behavior * depends on the {@link AuthorityBackedFactory} implementation (the most typical subclass), * but usually the database query is delegated to some instance of * {@link org.opengis.referencing.operation.CoordinateOperationAuthorityFactory}. *

* If no coordinate operation was found in the database, then this method delegates * to {@link #createOperation(CoordinateReferenceSystem, CoordinateReferenceSystem)}. *

* If {@code sourceCRS} and {@code targetCRS} are the same, then this method returns * {@code null} as a shortcut for identity transform. * * @throws FactoryException If an exception occurred while invoking {@code createOperation}. */ private CoordinateOperation tryDB(final SingleCRS sourceCRS, final SingleCRS targetCRS) throws FactoryException { if (sourceCRS == targetCRS) { return null; } final CoordinateOperation operation = createFromDatabase(sourceCRS, targetCRS); if (operation != null) { return operation; } return createOperation(sourceCRS, targetCRS); } /** * If the coordinate operation is explicitly defined in some database (typically EPSG), * returns it. Otherwise (if there is no database, or if the database doesn't contains * an explicit operation from {@code sourceCRS} to {@code targetCRS}, or if this method * failed to create an operation from the database), returns {@code null}. *

* The default implementation always returns {@code null}, since there is no database * connected to a {@code DefaultCoordinateOperationFactory} instance. In other words, * the default implementation is "standalone": it tries to figure out transformation * paths by itself. Subclasses should override this method if they can fetch a more * accurate operation from some database. The mean subclass doing so is * {@link AuthorityBackedFactory}. *

* This method is invoked by {@linkplain #createOperation createOperation}(sourceCRS, * targetCRS) before to try to figure out a transformation path by itself. It is also * invoked by various {@code createOperationStep(...)} methods when an intermediate CRS was * obtained by {@link GeneralDerivedCRS#getBaseCRS()} (this case occurs especially during * {@linkplain GeographicCRS geographic} from/to {@linkplain ProjectedCRS projected} CRS * operations). This method is not invoked for synthetic CRS generated by * {@code createOperationStep(...)}, since those temporary CRS are not expected to exist * in a database. * * @param sourceCRS Input coordinate reference system. * @param targetCRS Output coordinate reference system. * @return A coordinate operation from {@code sourceCRS} to {@code targetCRS} if and only if * one is explicitly defined in some underlying database, or {@code null} otherwise. * * @since 2.3 */ protected CoordinateOperation createFromDatabase(final CoordinateReferenceSystem sourceCRS, final CoordinateReferenceSystem targetCRS) { return null; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy