/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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. * * This package contains documentation from OpenGIS specifications. * OpenGIS consortium's work is fully acknowledged here. */ package org.geotools.referencing.datum; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import org.opengis.referencing.ReferenceIdentifier; import org.opengis.referencing.datum.Datum; import org.opengis.referencing.datum.Ellipsoid; import org.opengis.referencing.datum.PrimeMeridian; import org.opengis.referencing.datum.GeodeticDatum; import org.opengis.referencing.operation.Matrix; import org.geotools.metadata.iso.citation.Citations; import org.geotools.referencing.operation.matrix.XMatrix; import org.geotools.referencing.AbstractIdentifiedObject; import org.geotools.referencing.NamedIdentifier; import org.geotools.referencing.wkt.Formatter; /** * Defines the location and precise orientation in 3-dimensional space of a defined ellipsoid * (or sphere) that approximates the shape of the earth. Used also for Cartesian coordinate * system centered in this ellipsoid (or sphere). * * @since 2.1 * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) * * @see Ellipsoid * @see PrimeMeridian */ public class DefaultGeodeticDatum extends AbstractDatum implements GeodeticDatum { /** * Serial number for interoperability with different versions. */ private static final long serialVersionUID = 8832100095648302943L; /** * The default WGS 1984 datum. */ public static final DefaultGeodeticDatum WGS84; static { final ReferenceIdentifier[] identifiers = { new NamedIdentifier(Citations.OGC, "WGS84"), new NamedIdentifier(Citations.ORACLE, "WGS 84"), new NamedIdentifier(null, "WGS_84"), new NamedIdentifier(null, "WGS 1984"), new NamedIdentifier(Citations.EPSG, "WGS_1984"), new NamedIdentifier(Citations.ESRI, "D_WGS_1984"), new NamedIdentifier(Citations.EPSG, "World Geodetic System 1984") }; final Map<String,Object> properties = new HashMap<String,Object>(4); properties.put(NAME_KEY, identifiers[0]); properties.put(ALIAS_KEY, identifiers); WGS84 = new DefaultGeodeticDatum(properties, DefaultEllipsoid.WGS84, DefaultPrimeMeridian.GREENWICH); } /** * The <code>{@value #BURSA_WOLF_KEY}</code> property for * {@linkplain #getAffineTransform datum shifts}. */ public static final String BURSA_WOLF_KEY = "bursaWolf"; /** * The ellipsoid. */ private final Ellipsoid ellipsoid; /** * The prime meridian. */ private final PrimeMeridian primeMeridian; /** * Bursa Wolf parameters for datum shifts, or {@code null} if none. */ private final BursaWolfParameters[] bursaWolf; /** * Constructs a new datum with the same values than the specified one. * This copy constructor provides a way to wrap an arbitrary implementation into a * Geotools one or a user-defined one (as a subclass), usually in order to leverage * some implementation-specific API. This constructor performs a shallow copy, * i.e. the properties are not cloned. * * @since 2.2 */ public DefaultGeodeticDatum(final GeodeticDatum datum) { super(datum); ellipsoid = datum.getEllipsoid(); primeMeridian = datum.getPrimeMeridian(); bursaWolf = (datum instanceof DefaultGeodeticDatum) ? ((DefaultGeodeticDatum) datum).bursaWolf : null; } /** * Constructs a geodetic datum from a name. * * @param name The datum name. * @param ellipsoid The ellipsoid. * @param primeMeridian The prime meridian. */ public DefaultGeodeticDatum(final String name, final Ellipsoid ellipsoid, final PrimeMeridian primeMeridian) { this(Collections.singletonMap(NAME_KEY, name), ellipsoid, primeMeridian); } /** * Constructs a geodetic datum from a set of properties. The properties map is given * unchanged to the {@linkplain AbstractDatum#AbstractDatum(Map) super-class constructor}. * Additionally, the following properties are understood by this construtor: * <p> * <table border='1'> * <tr bgcolor="#CCCCFF" class="TableHeadingColor"> * <th nowrap>Property name</th> * <th nowrap>Value type</th> * <th nowrap>Value given to</th> * </tr> * <tr> * <td nowrap> {@link #BURSA_WOLF_KEY "bursaWolf"} </td> * <td nowrap> {@link BursaWolfParameters} or an array of those </td> * <td nowrap> {@link #getBursaWolfParameters}</td> * </tr> * </table> * * @param properties Set of properties. Should contains at least {@code "name"}. * @param ellipsoid The ellipsoid. * @param primeMeridian The prime meridian. */ public DefaultGeodeticDatum(final Map<String,?> properties, final Ellipsoid ellipsoid, final PrimeMeridian primeMeridian) { super(properties); this.ellipsoid = ellipsoid; this.primeMeridian = primeMeridian; ensureNonNull("ellipsoid", ellipsoid); ensureNonNull("primeMeridian", primeMeridian); BursaWolfParameters[] bursaWolf; final Object object = properties.get(BURSA_WOLF_KEY); if (object instanceof BursaWolfParameters) { bursaWolf = new BursaWolfParameters[] { ((BursaWolfParameters) object).clone() }; } else { bursaWolf = (BursaWolfParameters[]) object; if (bursaWolf != null) { if (bursaWolf.length == 0) { bursaWolf = null; } else { final Set<BursaWolfParameters> s = new LinkedHashSet<BursaWolfParameters>(); for (int i=0; i<bursaWolf.length; i++) { s.add(bursaWolf[i].clone()); } bursaWolf = s.toArray(new BursaWolfParameters[s.size()]); } } } this.bursaWolf = bursaWolf; } /** * Returns the ellipsoid. */ public Ellipsoid getEllipsoid() { return ellipsoid; } /** * Returns the prime meridian. */ public PrimeMeridian getPrimeMeridian() { return primeMeridian; } /** * Returns all Bursa Wolf parameters specified in the {@code properties} map at * construction time. * * @since 2.4 */ public BursaWolfParameters[] getBursaWolfParameters() { if (bursaWolf != null) { return bursaWolf.clone(); } return new BursaWolfParameters[0]; } /** * Returns Bursa Wolf parameters for a datum shift toward the specified target, or {@code null} * if none. This method search only for Bursa-Wolf parameters explicitly specified in the * {@code properties} map at construction time. This method doesn't try to infer a set of * parameters from indirect informations. For example it doesn't try to inverse the parameters * specified in the {@code target} datum if none were found in this datum. If such an elaborated * search is wanted, use {@link #getAffineTransform} instead. */ public BursaWolfParameters getBursaWolfParameters(final GeodeticDatum target) { if (bursaWolf != null) { for (int i=0; i<bursaWolf.length; i++) { final BursaWolfParameters candidate = bursaWolf[i]; if (equals(target, candidate.targetDatum, false)) { return candidate.clone(); } } } return null; } /** * Returns a matrix that can be used to define a transformation to the specified datum. * If no transformation path is found, then this method returns {@code null}. * * @param source The source datum. * @param target The target datum. * @return An affine transform from {@code source} to {@code target}, or {@code null} if none. * * @see BursaWolfParameters#getAffineTransform */ public static Matrix getAffineTransform(final GeodeticDatum source, final GeodeticDatum target) { return getAffineTransform(source, target, null); } /** * Returns a matrix that can be used to define a transformation to the specified datum. * If no transformation path is found, then this method returns {@code null}. * * @param source The source datum. * @param target The target datum. * @param exclusion The set of datum to exclude from the search, or {@code null}. * This is used in order to avoid never-ending recursivity. * @return An affine transform from {@code source} to {@code target}, or {@code null} if none. * * @see BursaWolfParameters#getAffineTransform */ private static XMatrix getAffineTransform(final GeodeticDatum source, final GeodeticDatum target, Set<GeodeticDatum> exclusion) { ensureNonNull("source", source); ensureNonNull("target", target); if (source instanceof DefaultGeodeticDatum) { final BursaWolfParameters[] bursaWolf = ((DefaultGeodeticDatum) source).bursaWolf; if (bursaWolf != null) { for (int i=0; i<bursaWolf.length; i++) { final BursaWolfParameters transformation = bursaWolf[i]; if (equals(target, transformation.targetDatum, false)) { return transformation.getAffineTransform(); } } } } /* * No transformation found to the specified target datum. * Search if a transform exists in the opposite direction. */ if (target instanceof DefaultGeodeticDatum) { final BursaWolfParameters[] bursaWolf = ((DefaultGeodeticDatum) target).bursaWolf; if (bursaWolf != null) { for (int i=0; i<bursaWolf.length; i++) { final BursaWolfParameters transformation = bursaWolf[i]; if (equals(source, transformation.targetDatum, false)) { final XMatrix matrix = transformation.getAffineTransform(); matrix.invert(); return matrix; } } } } /* * No direct tranformation found. Search for a path through some intermediate datum. * First, search if there is some BursaWolfParameters for the same target in both * 'source' and 'target' datum. If such an intermediate is found, ask for a path * as below: * * source --> [common datum] --> target */ if (source instanceof DefaultGeodeticDatum && target instanceof DefaultGeodeticDatum) { final BursaWolfParameters[] sourceParam = ((DefaultGeodeticDatum) source).bursaWolf; final BursaWolfParameters[] targetParam = ((DefaultGeodeticDatum) target).bursaWolf; if (sourceParam!=null && targetParam!=null) { GeodeticDatum sourceStep; GeodeticDatum targetStep; for (int i=0; i<sourceParam.length; i++) { sourceStep = sourceParam[i].targetDatum; for (int j=0; j<targetParam.length; j++) { targetStep = targetParam[j].targetDatum; if (equals(sourceStep, targetStep, false)) { final XMatrix step1, step2; if (exclusion == null) { exclusion = new HashSet<GeodeticDatum>(); } if (exclusion.add(source)) { if (exclusion.add(target)) { step1 = getAffineTransform(source, sourceStep, exclusion); if (step1 != null) { step2 = getAffineTransform(targetStep, target, exclusion); if (step2 != null) { /* * Note: XMatrix.multiply(XMatrix) is equivalent to * AffineTransform.concatenate(...): First * transform by the supplied transform and * then transform the result by the original * transform. */ step2.multiply(step1); return step2; } } exclusion.remove(target); } exclusion.remove(source); } } } } } } return null; } /** * Returns {@code true} if the specified object is equals (at least on * computation purpose) to the {@link #WGS84} datum. This method may conservatively * returns {@code false} if the specified datum is uncertain (for example * because it come from an other implementation). */ public static boolean isWGS84(final Datum datum) { if (datum instanceof AbstractIdentifiedObject) { return WGS84.equals((AbstractIdentifiedObject) datum, false); } // Maybe the specified object has its own test... return datum!=null && datum.equals(WGS84); } /** * Compare this datum with the specified object for equality. * * @param object The object to compare to {@code this}. * @param compareMetadata {@code true} for performing a strict comparaison, or * {@code false} for comparing only properties relevant to transformations. * @return {@code true} if both objects are equal. */ @Override public boolean equals(final AbstractIdentifiedObject object, final boolean compareMetadata) { if (object == this) { return true; // Slight optimization. } if (super.equals(object, compareMetadata)) { final DefaultGeodeticDatum that = (DefaultGeodeticDatum) object; if (equals(this.ellipsoid, that.ellipsoid, compareMetadata) && equals(this.primeMeridian, that.primeMeridian, compareMetadata)) { /* * HACK: We do not consider Bursa Wolf parameters as a non-metadata field. * This is needed in order to get equalsIgnoreMetadata(...) to returns * 'true' when comparing the WGS84 constant in this class with a WKT * DATUM element with a TOWGS84[0,0,0,0,0,0,0] element. Furthermore, * the Bursa Wolf parameters are not part of ISO 19111 specification. * We don't want two CRS to be considered as different because one has * more of those transformation informations (which is nice, but doesn't * change the CRS itself). */ return !compareMetadata || Arrays.equals(this.bursaWolf, that.bursaWolf); } } return false; } /** * Returns a hash value for this geodetic datum. {@linkplain #getName Name}, * {@linkplain #getRemarks remarks} and the like are not taken in account. In * other words, two geodetic datums will return the same hash value if they * are equal in the sense of * <code>{@link #equals equals}(AbstractIdentifiedObject, <strong>false</strong>)</code>. * * @return The hash code value. This value doesn't need to be the same * in past or future versions of this class. */ @Override public int hashCode() { int code = (int)serialVersionUID ^ 37*(super .hashCode() ^ 37*(ellipsoid .hashCode() ^ 37*(primeMeridian.hashCode()))); return code; } /** * Format the inner part of a * <A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/referencing/doc-files/WKT.html"><cite>Well * Known Text</cite> (WKT)</A> element. * * @param formatter The formatter to use. * @return The WKT element name, which is "DATUM" */ @Override protected String formatWKT(final Formatter formatter) { // Do NOT invokes the super-class method, because // horizontal datum do not write the datum type. formatter.append(ellipsoid); if (bursaWolf != null) { for (int i=0; i<bursaWolf.length; i++) { final BursaWolfParameters transformation = bursaWolf[i]; if (isWGS84(transformation.targetDatum)) { formatter.append(transformation); break; } } } return "DATUM"; } }