| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.referencing.datum; |
| |
| import java.util.Map; |
| import java.util.Arrays; |
| import java.util.Date; |
| import java.util.Objects; |
| import javax.xml.bind.annotation.XmlType; |
| import javax.xml.bind.annotation.XmlElement; |
| import javax.xml.bind.annotation.XmlRootElement; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.InternationalString; |
| import org.opengis.metadata.extent.Extent; |
| import org.opengis.referencing.ReferenceIdentifier; |
| import org.opengis.metadata.extent.GeographicBoundingBox; |
| import org.opengis.referencing.crs.GeodeticCRS; |
| 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.apache.sis.referencing.operation.matrix.Matrices; |
| import org.apache.sis.referencing.operation.matrix.MatrixSIS; |
| import org.apache.sis.referencing.operation.matrix.NoninvertibleMatrixException; |
| import org.apache.sis.metadata.iso.extent.Extents; |
| import org.apache.sis.internal.referencing.WKTKeywords; |
| import org.apache.sis.internal.metadata.NameToIdentifier; |
| import org.apache.sis.internal.metadata.MetadataUtilities; |
| import org.apache.sis.internal.referencing.AnnotatedMatrix; |
| import org.apache.sis.internal.referencing.CoordinateOperations; |
| import org.apache.sis.internal.referencing.ExtentSelector; |
| import org.apache.sis.internal.util.CollectionsExt; |
| import org.apache.sis.internal.system.Loggers; |
| import org.apache.sis.util.logging.Logging; |
| import org.apache.sis.util.ComparisonMode; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.io.wkt.Formatter; |
| |
| import static org.apache.sis.util.Utilities.deepEquals; |
| import static org.apache.sis.util.ArgumentChecks.ensureNonNull; |
| import static org.apache.sis.util.ArgumentChecks.ensureNonNullElement; |
| import static org.apache.sis.internal.referencing.WKTUtilities.toFormattable; |
| |
| |
| /** |
| * Defines the location and orientation of an ellipsoid that approximates the shape of the earth. |
| * Geodetic datum are used together with ellipsoidal coordinate system, and also with Cartesian |
| * coordinate system centered in the ellipsoid (or sphere). |
| * |
| * <h2>Bursa-Wolf parameters</h2> |
| * One or many {@link BursaWolfParameters} can optionally be associated to each {@code DefaultGeodeticDatum} instance. |
| * This association is not part of the ISO 19111 model, but still a common practice (especially in older standards). |
| * Associating Bursa-Wolf parameters to geodetic datum is known as the <cite>early-binding</cite> approach. |
| * A recommended alternative, discussed below, is the <cite>late-binding</cite> approach. |
| * |
| * <p>The Bursa-Wolf parameters serve two purposes:</p> |
| * <ol class="verbose"> |
| * <li><b>Fallback for datum shifts</b><br> |
| * There is different methods for transforming coordinates from one geodetic datum to an other datum, |
| * and Bursa-Wolf parameters are used with some of them. However different set of parameters may exist |
| * for the same pair of (<var>source</var>, <var>target</var>) datum, so it is often not sufficient to |
| * know those datum. The (<var>source</var>, <var>target</var>) pair of CRS are often necessary, |
| * sometime together with the geographic extent of the coordinates to transform. |
| * |
| * <p>Apache SIS searches for datum shift methods (including Bursa-Wolf parameters) in the EPSG database when a |
| * {@link org.opengis.referencing.operation.CoordinateOperation} or a |
| * {@link org.opengis.referencing.operation.MathTransform} is requested for a pair of CRS. |
| * This is known as the <cite>late-binding</cite> approach. |
| * If a datum shift method is found in the database, it will have precedence over any {@code BursaWolfParameters} |
| * instance associated to this {@code DefaultGeodeticDatum}. Only if no datum shift method is found in the database, |
| * then the {@code BursaWolfParameters} associated to the datum may be used as a fallback.</p> |
| * </li> |
| * |
| * <li><b>WKT version 1 formatting</b><br> |
| * The Bursa-Wolf parameters association serves an other purpose: when a CRS is formatted in the older |
| * <cite>Well Known Text</cite> (WKT 1) format, the formatted string may contain a {@code TOWGS84[…]} element |
| * with the parameter values of the transformation to the WGS 84 datum. This element is provided as a help |
| * for other Geographic Information Systems that support only the <cite>early-binding</cite> approach. |
| * Apache SIS usually does not need the {@code TOWGS84} element, except as a fallback for datum that |
| * do not exist in the EPSG database. |
| * </li> |
| * </ol> |
| * |
| * <h2>Creating new geodetic datum instances</h2> |
| * New instances can be created either directly by specifying all information to a factory method (choices 3 |
| * and 4 below), or indirectly by specifying the identifier of an entry in a database (choices 1 and 2 below). |
| * Choice 1 in the following list is the easiest but most restrictive way to get a geodetic datum. |
| * The other choices provide more freedom. |
| * |
| * <ol> |
| * <li>Create a {@code GeodeticDatum} from one of the static convenience shortcuts listed in |
| * {@link org.apache.sis.referencing.CommonCRS#datum()}.</li> |
| * <li>Create a {@code GeodeticDatum} from an identifier in a database by invoking |
| * {@link org.opengis.referencing.datum.DatumAuthorityFactory#createGeodeticDatum(String)}.</li> |
| * <li>Create a {@code GeodeticDatum} by invoking the {@code DatumFactory.createGeodeticDatum(…)} method |
| * (implemented for example by {@link org.apache.sis.referencing.factory.GeodeticObjectFactory}).</li> |
| * <li>Create a {@code DefaultGeodeticDatum} by invoking the |
| * {@linkplain #DefaultGeodeticDatum(Map, Ellipsoid, PrimeMeridian) constructor}.</li> |
| * </ol> |
| * |
| * <b>Example:</b> the following code gets a <cite>World Geodetic System 1984</cite> datum: |
| * |
| * {@preformat java |
| * GeodeticDatum datum = CommonCRS.WGS84.datum(); |
| * } |
| * |
| * <h2>Immutability and thread safety</h2> |
| * This class is immutable and thus thread-safe if the property <em>values</em> (not necessarily the map itself), |
| * the {@link Ellipsoid} and the {@link PrimeMeridian} given to the constructor are also immutable. Unless otherwise |
| * noted in the javadoc, this condition holds if all components were created using only SIS factories and static |
| * constants. |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @version 1.1 |
| * |
| * @see DefaultEllipsoid |
| * @see DefaultPrimeMeridian |
| * @see org.apache.sis.referencing.CommonCRS#datum() |
| * @see org.apache.sis.referencing.factory.GeodeticAuthorityFactory#createGeodeticDatum(String) |
| * |
| * @since 0.4 |
| * @module |
| */ |
| @XmlType(name = "GeodeticDatumType", propOrder = { |
| "primeMeridian", |
| "ellipsoid" |
| }) |
| @XmlRootElement(name = "GeodeticDatum") |
| public class DefaultGeodeticDatum extends AbstractDatum implements GeodeticDatum { |
| /** |
| * Serial number for inter-operability with different versions. |
| */ |
| private static final long serialVersionUID = 8832100095648302943L; |
| |
| /** |
| * The <code>{@value #BURSA_WOLF_KEY}</code> property for |
| * {@linkplain #getBursaWolfParameters() Bursa-Wolf parameters}. |
| */ |
| public static final String BURSA_WOLF_KEY = CoordinateOperations.BURSA_WOLF_KEY; |
| |
| /** |
| * The array to be returned by {@link #getBursaWolfParameters()} when there is no Bursa-Wolf parameters. |
| */ |
| private static final BursaWolfParameters[] EMPTY_ARRAY = new BursaWolfParameters[0]; |
| |
| /** |
| * The ellipsoid. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setEllipsoid(Ellipsoid)}</p> |
| * |
| * @see #getEllipsoid() |
| */ |
| private Ellipsoid ellipsoid; |
| |
| /** |
| * The prime meridian. |
| * |
| * <p><b>Consider this field as final!</b> |
| * This field is modified only at unmarshalling time by {@link #setPrimeMeridian(PrimeMeridian)}</p> |
| * |
| * @see #getPrimeMeridian() |
| */ |
| private PrimeMeridian primeMeridian; |
| |
| /** |
| * Bursa-Wolf parameters for datum shifts, or {@code null} if none. |
| */ |
| private final BursaWolfParameters[] bursaWolf; |
| |
| /** |
| * Creates a geodetic datum from the given properties. The properties map is given |
| * unchanged to the {@linkplain AbstractDatum#AbstractDatum(Map) super-class constructor}. |
| * In addition to the properties documented in the parent constructor, |
| * the following properties are understood by this constructor: |
| * |
| * <table class="sis"> |
| * <caption>Recognized properties (non exhaustive list)</caption> |
| * <tr> |
| * <th>Property name</th> |
| * <th>Value type</th> |
| * <th>Returned by</th> |
| * </tr> |
| * <tr> |
| * <td>{@value #BURSA_WOLF_KEY}</td> |
| * <td>{@link BursaWolfParameters} (optionally as array)</td> |
| * <td>{@link #getBursaWolfParameters()}</td> |
| * </tr> |
| * <tr> |
| * <th colspan="3" class="hsep">Defined in parent classes (reminder)</th> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td> |
| * <td>{@link ReferenceIdentifier} or {@link String}</td> |
| * <td>{@link #getName()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#ALIAS_KEY}</td> |
| * <td>{@link GenericName} or {@link CharSequence} (optionally as array)</td> |
| * <td>{@link #getAlias()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#IDENTIFIERS_KEY}</td> |
| * <td>{@link ReferenceIdentifier} (optionally as array)</td> |
| * <td>{@link #getIdentifiers()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.IdentifiedObject#REMARKS_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getRemarks()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.datum.Datum#ANCHOR_POINT_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getAnchorPoint()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.datum.Datum#REALIZATION_EPOCH_KEY}</td> |
| * <td>{@link Date}</td> |
| * <td>{@link #getRealizationEpoch()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.datum.Datum#DOMAIN_OF_VALIDITY_KEY}</td> |
| * <td>{@link Extent}</td> |
| * <td>{@link #getDomainOfValidity()}</td> |
| * </tr> |
| * <tr> |
| * <td>{@value org.opengis.referencing.datum.Datum#SCOPE_KEY}</td> |
| * <td>{@link InternationalString} or {@link String}</td> |
| * <td>{@link #getScope()}</td> |
| * </tr> |
| * </table> |
| * |
| * If Bursa-Wolf parameters are specified, then the prime meridian of their |
| * {@linkplain BursaWolfParameters#getTargetDatum() target datum} shall be either the same than the |
| * {@code primeMeridian} given to this constructor, or Greenwich. This restriction is for avoiding |
| * ambiguity about whether the longitude rotation shall be applied before or after the datum shift. |
| * If the target prime meridian is Greenwich, then the datum shift will be applied in a coordinate |
| * system having Greenwich as the prime meridian. |
| * |
| * @param properties the properties to be given to the identified object. |
| * @param ellipsoid the ellipsoid. |
| * @param primeMeridian the prime meridian. |
| * |
| * @see org.apache.sis.referencing.factory.GeodeticObjectFactory#createGeodeticDatum(Map, Ellipsoid, PrimeMeridian) |
| */ |
| public DefaultGeodeticDatum(final Map<String,?> properties, |
| final Ellipsoid ellipsoid, |
| final PrimeMeridian primeMeridian) |
| { |
| super(properties); |
| ensureNonNull("ellipsoid", ellipsoid); |
| ensureNonNull("primeMeridian", primeMeridian); |
| this.ellipsoid = ellipsoid; |
| this.primeMeridian = primeMeridian; |
| bursaWolf = CollectionsExt.nonEmpty(CollectionsExt.nonNullArraySet( |
| BURSA_WOLF_KEY, properties.get(BURSA_WOLF_KEY), EMPTY_ARRAY)); |
| if (bursaWolf != null) { |
| for (int i=0; i<bursaWolf.length; i++) { |
| BursaWolfParameters param = bursaWolf[i]; |
| ensureNonNullElement("bursaWolf", i, param); |
| param = param.clone(); |
| param.verify(primeMeridian); |
| bursaWolf[i] = param; |
| } |
| } |
| } |
| |
| /** |
| * Creates a new datum with the same values than the specified one. |
| * This copy constructor provides a way to convert an arbitrary implementation into a SIS one |
| * or a user-defined one (as a subclass), usually in order to leverage some implementation-specific API. |
| * |
| * <p>This constructor performs a shallow copy, i.e. the properties are not cloned.</p> |
| * |
| * @param datum the datum to copy. |
| * |
| * @see #castOrCopy(GeodeticDatum) |
| */ |
| protected DefaultGeodeticDatum(final GeodeticDatum datum) { |
| super(datum); |
| ellipsoid = datum.getEllipsoid(); |
| primeMeridian = datum.getPrimeMeridian(); |
| bursaWolf = (datum instanceof DefaultGeodeticDatum) ? ((DefaultGeodeticDatum) datum).bursaWolf : null; |
| // No need to clone the 'bursaWolf' array since it is read only. |
| } |
| |
| /** |
| * Returns a SIS datum implementation with the same values than the given arbitrary implementation. |
| * If the given object is {@code null}, then this method returns {@code null}. |
| * Otherwise if the given object is already a SIS implementation, then the given object is returned unchanged. |
| * Otherwise a new SIS implementation is created and initialized to the attribute values of the given object. |
| * |
| * @param object the object to get as a SIS implementation, or {@code null} if none. |
| * @return a SIS implementation containing the values of the given object (may be the |
| * given object itself), or {@code null} if the argument was null. |
| */ |
| public static DefaultGeodeticDatum castOrCopy(final GeodeticDatum object) { |
| return (object == null) || (object instanceof DefaultGeodeticDatum) |
| ? (DefaultGeodeticDatum) object : new DefaultGeodeticDatum(object); |
| } |
| |
| /** |
| * Returns the GeoAPI interface implemented by this class. |
| * The SIS implementation returns {@code GeodeticDatum.class}. |
| * |
| * <div class="note"><b>Note for implementers:</b> |
| * Subclasses usually do not need to override this method since GeoAPI does not define {@code GeodeticDatum} |
| * sub-interface. Overriding possibility is left mostly for implementers who wish to extend GeoAPI with their |
| * own set of interfaces.</div> |
| * |
| * @return {@code GeodeticDatum.class} or a user-defined sub-interface. |
| */ |
| @Override |
| public Class<? extends GeodeticDatum> getInterface() { |
| return GeodeticDatum.class; |
| } |
| |
| /** |
| * Returns the ellipsoid given at construction time. |
| * |
| * @return the ellipsoid. |
| */ |
| @Override |
| @XmlElement(name = "ellipsoid", required = true) |
| public Ellipsoid getEllipsoid() { |
| return ellipsoid; |
| } |
| |
| /** |
| * Returns the prime meridian given at construction time. |
| * |
| * @return the prime meridian. |
| */ |
| @Override |
| @XmlElement(name = "primeMeridian", required = true) |
| public PrimeMeridian getPrimeMeridian() { |
| return primeMeridian; |
| } |
| |
| /** |
| * Returns all Bursa-Wolf parameters specified in the {@code properties} map at construction time. |
| * See class javadoc for a discussion about Bursa-Wolf parameters. |
| * |
| * @return the Bursa-Wolf parameters, or an empty array if none. |
| */ |
| @SuppressWarnings("ReturnOfCollectionOrArrayField") |
| public BursaWolfParameters[] getBursaWolfParameters() { |
| if (bursaWolf == null) { |
| return EMPTY_ARRAY; |
| } |
| final BursaWolfParameters[] copy = bursaWolf.clone(); |
| for (int i=0; i<copy.length; i++) { |
| copy[i] = copy[i].clone(); |
| } |
| return copy; |
| } |
| |
| /** |
| * Returns the position vector transformation (geocentric domain) to the specified datum. |
| * If the returned matrix is non-null, then the transformation is represented by an affine transform which can be |
| * applied on <strong>geocentric</strong> coordinates. This is identified in the EPSG database as operation method |
| * 1033 – <cite>Position Vector transformation (geocentric domain)</cite>, or |
| * 1053 – <cite>Time-dependent Position Vector transformation</cite>. |
| * |
| * <p>If this datum and the given {@code targetDatum} do not use the same {@linkplain #getPrimeMeridian() prime meridian}, |
| * then it is caller's responsibility to apply longitude rotation before to use the matrix returned by this method. |
| * The target prime meridian should be Greenwich (see {@linkplain #DefaultGeodeticDatum(Map, Ellipsoid, PrimeMeridian) |
| * constructor javadoc}), in which case the datum shift should be applied in a geocentric coordinate system having |
| * Greenwich as the prime meridian.</p> |
| * |
| * <div class="note"><b>Note:</b> |
| * in EPSG dataset version 8.9, all datum shifts that can be represented by this method use Greenwich as the |
| * prime meridian, both in source and target datum.</div> |
| * |
| * <h4>Search criterion</h4> |
| * If the given {@code areaOfInterest} is non-null and contains at least one geographic bounding box, then this |
| * method ignores any Bursa-Wolf parameters having a {@linkplain BursaWolfParameters#getDomainOfValidity() domain |
| * of validity} that does not intersect the given geographic extent. |
| * This method performs the search among the remaining parameters in the following order: |
| * <ol> |
| * <li>If this {@code GeodeticDatum} contains {@code BursaWolfParameters} having the given |
| * {@linkplain BursaWolfParameters#getTargetDatum() target datum} (ignoring metadata), |
| * then the matrix will be built from those parameters.</li> |
| * <li>Otherwise if the other datum contains {@code BursaWolfParameters} having this datum |
| * as their target (ignoring metadata), then the matrix will be built from those parameters |
| * and {@linkplain org.apache.sis.referencing.operation.matrix.MatrixSIS#inverse() inverted}.</li> |
| * </ol> |
| * |
| * <h4>Multi-occurrences resolution</h4> |
| * If more than one {@code BursaWolfParameters} instance is found in any of the above steps, then the one having |
| * the largest intersection between its {@linkplain BursaWolfParameters#getDomainOfValidity() domain of validity} |
| * and the given extent will be selected. If more than one instance have the same intersection, then the first |
| * occurrence is selected. |
| * |
| * <h4>Time-dependent parameters</h4> |
| * If the given extent contains a {@linkplain org.opengis.metadata.extent.TemporalExtent temporal extent}, |
| * then the instant located midway between start and end time will be taken as the date where to evaluate the |
| * Bursa-Wolf parameters. This is relevant only to {@linkplain TimeDependentBWP time-dependent parameters}. |
| * |
| * @param targetDatum the target datum. |
| * @param areaOfInterest the geographic and temporal extent where the transformation should be valid, or {@code null}. |
| * @return an affine transform from {@code this} to {@code target} in geocentric space, or {@code null} if none. |
| * |
| * @see BursaWolfParameters#getPositionVectorTransformation(Date) |
| */ |
| public Matrix getPositionVectorTransformation(final GeodeticDatum targetDatum, final Extent areaOfInterest) { |
| ensureNonNull("targetDatum", targetDatum); |
| final ExtentSelector<BursaWolfParameters> selector = new ExtentSelector<>(areaOfInterest); |
| BursaWolfParameters candidate = select(targetDatum, selector); |
| if (candidate != null) { |
| return createTransformation(candidate, areaOfInterest); |
| } |
| /* |
| * Found no suitable BursaWolfParameters associated to this instance. |
| * Search in the BursaWolfParameters associated to the other instance. |
| */ |
| if (targetDatum instanceof DefaultGeodeticDatum) { |
| candidate = ((DefaultGeodeticDatum) targetDatum).select(this, selector); |
| if (candidate != null) try { |
| return Matrices.inverse(createTransformation(candidate, areaOfInterest)); |
| } catch (NoninvertibleMatrixException e) { |
| /* |
| * Should never happen because BursaWolfParameters.getPositionVectorTransformation(Date) |
| * is defined in such a way that matrix should always be invertible. If it happen anyway, |
| * returning 'null' is allowed by this method's contract. |
| */ |
| Logging.unexpectedException(Logging.getLogger(Loggers.COORDINATE_OPERATION), |
| DefaultGeodeticDatum.class, "getPositionVectorTransformation", e); |
| } |
| /* |
| * No direct tranformation found. Search for a path through an 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 path: |
| * |
| * source → [common datum] → target |
| * |
| * A consequence of such indirect path is that it may connect unrelated datums |
| * if [common datum] is a world datum such as WGS84. We do not have a solution |
| * for preventing that. |
| */ |
| if (bursaWolf != null) { |
| final GeographicBoundingBox bbox = selector.getAreaOfInterest(); |
| for (final BursaWolfParameters toPivot : bursaWolf) { |
| selector.setAreaOfInterest(bbox, toPivot.getDomainOfValidity()); |
| candidate = ((DefaultGeodeticDatum) targetDatum).select(toPivot.getTargetDatum(), selector); |
| if (candidate != null) { |
| final Matrix step1 = createTransformation(toPivot, areaOfInterest); |
| final Matrix step2 = createTransformation(candidate, areaOfInterest); |
| /* |
| * MatrixSIS.multiply(MatrixSIS) is equivalent to AffineTransform.concatenate(…): |
| * First transform by the supplied transform and then transform the result by the |
| * original transform. |
| */ |
| try { |
| Matrix m = MatrixSIS.castOrCopy(step2).inverse().multiply(step1); |
| return AnnotatedMatrix.indirect(m, selector.hasIntersection()); |
| } catch (NoninvertibleMatrixException e) { |
| Logging.unexpectedException(Logging.getLogger(Loggers.COORDINATE_OPERATION), |
| DefaultGeodeticDatum.class, "getPositionVectorTransformation", e); |
| } |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Invokes {@link BursaWolfParameters#getPositionVectorTransformation(Date)} for a date calculated from |
| * the temporal elements on the given extent. This method chooses an instant located midway between the |
| * start and end time. |
| */ |
| private static Matrix createTransformation(final BursaWolfParameters bursaWolf, final Extent areaOfInterest) { |
| /* |
| * Implementation note: we know that we do not need to compute an instant if the parameters is |
| * not a subclass of BursaWolfParameters. This optimisation covers the vast majority of cases. |
| */ |
| return bursaWolf.getPositionVectorTransformation(bursaWolf.getClass() != BursaWolfParameters.class ? |
| Extents.getDate(areaOfInterest, 0.5) : null); // 0.5 is for choosing midway instant. |
| } |
| |
| /** |
| * Returns the best parameters matching the given criteria, or {@code null} if none. |
| */ |
| private BursaWolfParameters select(final GeodeticDatum targetDatum, final ExtentSelector<BursaWolfParameters> selector) { |
| if (bursaWolf == null) { |
| return null; |
| } |
| for (final BursaWolfParameters candidate : bursaWolf) { |
| if (deepEquals(targetDatum, candidate.getTargetDatum(), ComparisonMode.IGNORE_METADATA)) { |
| selector.evaluate(candidate.getDomainOfValidity(), candidate); |
| } |
| } |
| return selector.best(); |
| } |
| |
| /** |
| * Returns {@code true} if either the {@linkplain #getName() primary name} or at least |
| * one {@linkplain #getAlias() alias} matches the given string according heuristic rules. |
| * This method implements the flexibility documented in the |
| * {@linkplain AbstractDatum#isHeuristicMatchForName(String) super-class}. In particular, |
| * this method ignores the prime meridian name if that name is found between parenthesis in the datum name. |
| * The meridian can be safely ignored in the datum name because the {@link PrimeMeridian} object is already |
| * compared by the {@link #equals(Object)} method. |
| * |
| * <div class="note"><b>Example:</b> |
| * if the datum name is <cite>"Nouvelle Triangulation Française (Paris)"</cite> and the prime meridian name is |
| * <cite>"Paris"</cite>, then this method compares only the <cite>"Nouvelle Triangulation Française"</cite> part. |
| * </div> |
| * |
| * <h4>Future evolutions</h4> |
| * This method implements heuristic rules learned from experience while trying to provide inter-operability |
| * with different data producers. Those rules may be adjusted in any future SIS version according experience |
| * gained while working with more data producers. |
| * |
| * @param name the name to compare. |
| * @return {@code true} if the primary name or at least one alias matches the specified {@code name}. |
| * |
| * @since 0.7 |
| */ |
| @Override |
| public boolean isHeuristicMatchForName(final String name) { |
| final String meridian = primeMeridian.getName().getCode(); |
| return NameToIdentifier.isHeuristicMatchForName(super.getName(), super.getAlias(), name, new Simplifier() { |
| @Override protected CharSequence apply(CharSequence name) { |
| name = super.apply(name); |
| int lower = CharSequences.indexOf(name, meridian, 0, name.length()) - 1; |
| if (lower >= 0 && name.charAt(lower) == '(') { |
| int upper = lower + meridian.length() + 1; |
| if (upper < name.length() && name.charAt(upper) == ')') { |
| lower = CharSequences.skipTrailingWhitespaces(name, 0, lower); |
| while (lower > 0) { |
| final int c = Character.codePointBefore(name, lower); |
| if (Character.isLetterOrDigit(c)) { |
| // Remove the meridian name only if it is not at the beginning of the name. |
| name = new StringBuilder(name).delete(lower, upper+1).toString(); |
| break; |
| } |
| lower -= Character.charCount(c); |
| } |
| } |
| } |
| return name; |
| } |
| }); |
| } |
| |
| /** |
| * Compare this datum with the specified object for equality. |
| * |
| * @param object the object to compare to {@code this}. |
| * @param mode {@link ComparisonMode#STRICT STRICT} for performing a strict comparison, or |
| * {@link ComparisonMode#IGNORE_METADATA IGNORE_METADATA} for comparing only |
| * properties relevant to coordinate transformations. |
| * @return {@code true} if both objects are equal. |
| */ |
| @Override |
| public boolean equals(final Object object, final ComparisonMode mode) { |
| if (object == this) { |
| return true; // Slight optimization. |
| } |
| if (!super.equals(object, mode)) { |
| return false; |
| } |
| switch (mode) { |
| case STRICT: { |
| final DefaultGeodeticDatum that = (DefaultGeodeticDatum) object; |
| return Objects.equals(this.ellipsoid, that.ellipsoid) && |
| Objects.equals(this.primeMeridian, that.primeMeridian) && |
| Arrays.equals(this.bursaWolf, that.bursaWolf); |
| } |
| default: { |
| final GeodeticDatum that = (GeodeticDatum) object; |
| return deepEquals(getEllipsoid(), that.getEllipsoid(), mode) && |
| deepEquals(getPrimeMeridian(), that.getPrimeMeridian(), mode); |
| /* |
| * Bursa-Wolf parameters are considered ignorable metadata. This is needed in order to get |
| * equalsIgnoreMetadata(…) to return true when comparing WGS84 datums with and without the |
| * WKT 1 "TOWGS84[0,0,0,0,0,0,0]" element. Furthermore those Bursa-Wolf parameters are not |
| * part of ISO 19111 specification. |
| */ |
| } |
| } |
| } |
| |
| /** |
| * Invoked by {@code hashCode()} for computing the hash code when first needed. |
| * See {@link org.apache.sis.referencing.AbstractIdentifiedObject#computeHashCode()} |
| * for more information. |
| * |
| * @return the hash code value. This value may change in any future Apache SIS version. |
| */ |
| @Override |
| protected long computeHashCode() { |
| return super.computeHashCode() + Objects.hashCode(ellipsoid) + 31 * Objects.hashCode(primeMeridian); |
| } |
| |
| /** |
| * Formats this datum as a <cite>Well Known Text</cite> {@code Datum[…]} element. |
| * |
| * <div class="note"><b>Example:</b> Well-Known Text of a WGS 84 datum. |
| * |
| * {@preformat wkt |
| * Datum["World Geodetic System 1984", |
| * Ellipsoid["WGS84", 6378137.0, 298.257223563, LengthUnit["metre", 1]], |
| * Id["EPSG", 6326, Citation["IOGP"], URI["urn:ogc:def:datum:EPSG::6326"]]] |
| * } |
| * |
| * <p>Same datum using WKT 1.</p> |
| * |
| * {@preformat wkt |
| * DATUM["World Geodetic System 1984" |
| * SPHEROID["WGS84", 6378137.0, 298.257223563], |
| * AUTHORITY["EPSG", "6326"]] |
| * } |
| * </div> |
| * |
| * Note that the {@linkplain #getPrimeMeridian() prime meridian} shall be formatted by the caller |
| * as a separated element after the geodetic datum (for compatibility with WKT 1). |
| * |
| * @return {@code "Datum"} or {@code "GeodeticDatum"}. |
| * |
| * @see <a href="http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#51">WKT 2 specification §8.2</a> |
| */ |
| @Override |
| protected String formatTo(final Formatter formatter) { |
| super.formatTo(formatter); |
| formatter.newLine(); |
| formatter.append(toFormattable(getEllipsoid())); |
| final boolean isWKT1 = formatter.getConvention().majorVersion() == 1; |
| if (isWKT1) { |
| /* |
| * Note that at the different of other datum (in particular vertical datum), |
| * WKT of geodetic datum do not have a numerical code for the datum type. |
| */ |
| if (bursaWolf != null) { |
| for (final BursaWolfParameters candidate : bursaWolf) { |
| if (candidate.isToWGS84()) { |
| formatter.newLine(); |
| formatter.append(candidate); |
| break; |
| } |
| } |
| } |
| } |
| /* |
| * For the WKT 2 case, the ANCHOR[…] element is added by Formatter itself. |
| */ |
| formatter.newLine(); // For writing the ID[…] element on its own line. |
| if (!isWKT1) { |
| /* |
| * In WKT 2, both "Datum" and "GeodeticDatum" keywords are permitted. The standard recommends |
| * to use "Datum" for simplicity. We will follow this advice when the Datum element is inside |
| * a GeodeticCRS element since the "Geodetic" aspect is more obvious in such case. But if the |
| * Datum appears in another context, then we will use "GeodeticDatum" for clarity. |
| */ |
| if (!(formatter.getEnclosingElement(1) instanceof GeodeticCRS)) { |
| return formatter.shortOrLong(WKTKeywords.Datum, WKTKeywords.GeodeticDatum); |
| } |
| } |
| return WKTKeywords.Datum; |
| } |
| |
| |
| |
| |
| ////////////////////////////////////////////////////////////////////////////////////////////////// |
| //////// //////// |
| //////// XML support with JAXB //////// |
| //////// //////// |
| //////// The following methods are invoked by JAXB using reflection (even if //////// |
| //////// they are private) or are helpers for other methods invoked by JAXB. //////// |
| //////// Those methods can be safely removed if Geographic Markup Language //////// |
| //////// (GML) support is not needed. //////// |
| //////// //////// |
| ////////////////////////////////////////////////////////////////////////////////////////////////// |
| |
| /** |
| * Constructs a new datum in which every attributes are set to a null value. |
| * <strong>This is not a valid object.</strong> This constructor is strictly |
| * reserved to JAXB, which will assign values to the fields using reflexion. |
| */ |
| private DefaultGeodeticDatum() { |
| bursaWolf = null; |
| /* |
| * Ellipsoid and PrimeMeridian are mandatory for SIS working. We do not verify their presence here |
| * (because the verification would have to be done in an 'afterMarshal(…)' method and throwing an |
| * exception in that method causes the whole unmarshalling to fail). But the CD_GeodeticDatum |
| * adapter does some verifications. |
| */ |
| } |
| |
| /** |
| * Invoked by JAXB only at unmarshalling time. |
| * |
| * @see #getEllipsoid() |
| */ |
| private void setEllipsoid(final Ellipsoid value) { |
| if (ellipsoid == null) { |
| ellipsoid = value; |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultGeodeticDatum.class, "setEllipsoid", "ellipsoid"); |
| } |
| } |
| |
| /** |
| * Invoked by JAXB only at unmarshalling time. |
| * |
| * @see #getPrimeMeridian() |
| */ |
| private void setPrimeMeridian(final PrimeMeridian value) { |
| if (primeMeridian == null) { |
| primeMeridian = value; |
| } else { |
| MetadataUtilities.propertyAlreadySet(DefaultGeodeticDatum.class, "setPrimeMeridian", "primeMeridian"); |
| } |
| } |
| } |