blob: 95368262d2b3d892b782cf0e4bb565a2f7d215aa [file] [log] [blame]
/*
* 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.Date;
import java.util.Map;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSeeAlso;
import org.opengis.util.GenericName;
import org.opengis.util.InternationalString;
import org.opengis.metadata.Identifier;
import org.opengis.metadata.extent.Extent;
import org.opengis.metadata.citation.Citation;
import org.opengis.referencing.datum.Datum;
import org.apache.sis.referencing.AbstractIdentifiedObject;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.util.iso.Types;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.ComparisonMode;
import org.apache.sis.internal.metadata.Identifiers;
import org.apache.sis.internal.metadata.NameToIdentifier;
import org.apache.sis.internal.metadata.MetadataUtilities;
import org.apache.sis.metadata.iso.citation.Citations;
import org.apache.sis.io.wkt.ElementKind;
import org.apache.sis.io.wkt.Formatter;
import static org.apache.sis.util.Utilities.deepEquals;
import static org.apache.sis.util.collection.Containers.property;
// Branch-dependent imports
import java.util.Objects;
/**
* Specifies the relationship of a {@linkplain org.apache.sis.referencing.cs.AbstractCS Coordinate System} to the earth.
* A datum can be defined as a set of real points on the earth that have coordinates.
* Each datum subtype can be associated with only specific types of
* {@linkplain org.apache.sis.referencing.cs.AbstractCS coordinate systems}, thus creating specific types of
* {@linkplain org.apache.sis.referencing.crs.AbstractCRS coordinate reference system}.
*
* <h2>Instantiation</h2>
* This class is conceptually <cite>abstract</cite>, even if it is technically possible to instantiate it.
* Typical applications should create instances of the most specific subclass prefixed by {@code Default} instead.
*
* <h2>Immutability and thread safety</h2>
* This base class is immutable if the property <em>values</em> (not necessarily the map itself) given to the
* constructor are also immutable. Most SIS subclasses and related classes are immutable under similar conditions.
* This means that unless otherwise noted in the javadoc, {@code Datum} instances created using only SIS factories
* and static constants can be shared by many objects and passed between threads without synchronization.
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.7
*
* @see org.apache.sis.referencing.cs.AbstractCS
* @see org.apache.sis.referencing.crs.AbstractCRS
*
* @since 0.4
* @module
*/
@XmlType(name = "AbstractDatumType", propOrder = {
"domainOfValidity",
"scope",
"anchorPoint",
"realizationEpoch"
})
@XmlRootElement(name = "AbstractDatum")
@XmlSeeAlso({
DefaultGeodeticDatum.class,
DefaultVerticalDatum.class,
DefaultTemporalDatum.class,
DefaultParametricDatum.class,
DefaultEngineeringDatum.class,
DefaultImageDatum.class
})
public class AbstractDatum extends AbstractIdentifiedObject implements Datum {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = -4894180465652474930L;
/**
* Description, possibly including coordinates, of the point or points used to anchor the datum
* to the Earth. Also known as the "origin", especially for Engineering and Image Datums.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setAnchorPoint(InternationalString)}</p>
*
* @see #getAnchorPoint()
*/
private InternationalString anchorDefinition;
/**
* The time after which this datum definition is valid. This time may be precise
* (e.g. 1997 for IRTF97) or merely a year (e.g. 1983 for NAD83). If the time is
* not defined, then the value is {@link Long#MIN_VALUE}.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setRealizationEpoch(Date)}</p>
*/
private long realizationEpoch;
/**
* Area or region in which this datum object is valid.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setDomainOfValidity(Extent)}</p>
*
* @see #getDomainOfValidity()
*/
private Extent domainOfValidity;
/**
* Description of domain of usage, or limitations of usage, for which this datum object is valid.
*
* <p><b>Consider this field as final!</b>
* This field is modified only at unmarshalling time by {@link #setScope(InternationalString)}</p>
*
* @see #getScope()
*/
private InternationalString scope;
/**
* Creates a datum from the given properties.
* The properties given in argument follow the same rules than for the
* {@linkplain AbstractIdentifiedObject#AbstractIdentifiedObject(Map) super-class constructor}.
* Additionally, 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 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>
* <tr>
* <th colspan="3" class="hsep">Defined in parent class (reminder)</th>
* </tr>
* <tr>
* <td>{@value org.opengis.referencing.IdentifiedObject#NAME_KEY}</td>
* <td>{@link Identifier} 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 Identifier} (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>
* </table>
*
* @param properties the properties to be given to the identified object.
*/
public AbstractDatum(final Map<String,?> properties) {
super(properties);
realizationEpoch = MetadataUtilities.toMilliseconds(property(properties, REALIZATION_EPOCH_KEY, Date.class));
domainOfValidity = property(properties, DOMAIN_OF_VALIDITY_KEY, Extent.class);
anchorDefinition = Types.toInternationalString(properties, ANCHOR_POINT_KEY);
scope = Types.toInternationalString(properties, SCOPE_KEY);
}
/**
* 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.
*/
protected AbstractDatum(final Datum datum) {
super(datum);
realizationEpoch = MetadataUtilities.toMilliseconds(datum.getRealizationEpoch());
domainOfValidity = datum.getDomainOfValidity();
scope = datum.getScope();
anchorDefinition = datum.getAnchorPoint();
}
/**
* Returns a SIS datum implementation with the values of the given arbitrary implementation.
* This method performs the first applicable action in the following choices:
*
* <ul>
* <li>If the given object is {@code null}, then this method returns {@code null}.</li>
* <li>Otherwise if the given object is an instance of
* {@link org.opengis.referencing.datum.GeodeticDatum},
* {@link org.opengis.referencing.datum.VerticalDatum},
* {@link org.opengis.referencing.datum.TemporalDatum},
* {@link org.opengis.referencing.datum.EngineeringDatum} or
* {@link org.opengis.referencing.datum.ImageDatum},
* then this method delegates to the {@code castOrCopy(…)} method of the corresponding SIS subclass.
* Note that if the given object implements more than one of the above-cited interfaces,
* then the {@code castOrCopy(…)} method to be used is unspecified.</li>
* <li>Otherwise if the given object is already an instance of
* {@code AbstractDatum}, then it is returned unchanged.</li>
* <li>Otherwise a new {@code AbstractDatum} instance is created using the
* {@linkplain #AbstractDatum(Datum) copy constructor}
* and returned. Note that this is a <cite>shallow</cite> copy operation, since the other
* properties contained in the given object are not recursively copied.</li>
* </ul>
*
* @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 AbstractDatum castOrCopy(final Datum object) {
return SubTypes.castOrCopy(object);
}
/**
* Returns the GeoAPI interface implemented by this class.
* The default implementation returns {@code Datum.class}.
* Subclasses implementing a more specific GeoAPI interface shall override this method.
*
* @return the datum interface implemented by this class.
*/
@Override
public Class<? extends Datum> getInterface() {
return Datum.class;
}
/**
* Returns a description of the point(s) used to anchor the datum to the Earth.
* Also known as the "origin", especially for Engineering and Image Datums.
*
* <ul>
* <li>For a {@linkplain DefaultGeodeticDatum geodetic datum}, the anchor may be the point(s) where the
* relationship between geoid and ellipsoid is defined.</li>
*
* <li>For an {@linkplain DefaultEngineeringDatum engineering datum}, the anchor may be an identified
* physical point with the orientation defined relative to the object.</li>
*
* <li>For an {@linkplain DefaultImageDatum image datum}, the anchor point may be the centre or the corner
* of the image.</li>
*
* <li>For a {@linkplain DefaultTemporalDatum temporal datum}, see their
* {@linkplain DefaultTemporalDatum#getOrigin() origin} instead.</li>
* </ul>
*
* @return description, possibly including coordinates, of the point or points used to anchor the datum to the Earth.
*/
@Override
@XmlElement(name = "anchorDefinition")
public InternationalString getAnchorPoint() {
return anchorDefinition;
}
/**
* The time after which this datum definition is valid.
* This time may be precise or merely a year.
*
* <p>If an old datum is superseded by a new datum, then the realization epoch for the new datum
* defines the upper limit for the validity of the old datum.</p>
*
* @return the time after which this datum definition is valid, or {@code null} if none.
*/
@Override
@XmlSchemaType(name = "date")
@XmlElement(name = "realizationEpoch")
public Date getRealizationEpoch() {
return MetadataUtilities.toDate(realizationEpoch);
}
/**
* Returns the region or timeframe in which this datum is valid, or {@code null} if unspecified.
*
* @return area or region or timeframe in which this datum is valid, or {@code null}.
*
* @see org.apache.sis.metadata.iso.extent.DefaultExtent
*/
@Override
@XmlElement(name = "domainOfValidity")
public Extent getDomainOfValidity() {
return domainOfValidity;
}
/**
* Returns the domain or limitations of usage, or {@code null} if unspecified.
*
* @return description of domain of usage, or limitations of usage, for which this datum object is valid.
*/
@Override
@XmlElement(name = "scope", required = true)
public InternationalString getScope() {
return scope;
}
/**
* 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 performs the comparison documented in the
* {@linkplain AbstractIdentifiedObject#isHeuristicMatchForName(String) super-class},
* with the following additional flexibility:
*
* <ul>
* <li>The {@code "D_"} prefix (used in ESRI datum names), if presents in the given name or in this datum name,
* is ignored.</li>
* <li>If this datum is an instance of {@link DefaultGeodeticDatum}, then the prime meridian name may also
* be ignored.</li>
* </ul>
*
* <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}.
*/
@Override
public boolean isHeuristicMatchForName(final String name) {
return NameToIdentifier.isHeuristicMatchForName(super.getName(), super.getAlias(), name, Simplifier.INSTANCE);
}
/**
* A function for simplifying a {@link Datum} name before comparison.
*
* <p>Note: if heuristic rules are modified, consider updating {@code EPSGDataAccess} accordingly.</p>
*
* @see org.apache.sis.referencing.factory.sql.EPSGCodeFinder#toDatumPattern(String, StringBuilder)
*
* @since 0.7
*/
static class Simplifier extends NameToIdentifier.Simplifier {
/** The singleton simplifier for non-geodetic datum. */
static final Simplifier INSTANCE = new Simplifier();
/** For subclasses and default instance only. */
Simplifier() {}
/** Simplify the given datum name. */
@Override protected CharSequence apply(CharSequence name) {
name = super.apply(name);
if (CharSequences.startsWith(name, ESRI_DATUM_PREFIX, false)) {
name = name.subSequence(ESRI_DATUM_PREFIX.length(), name.length());
}
return name;
}
}
/**
* Compares the specified object with this datum for equality.
* If the {@code mode} argument value is {@link ComparisonMode#STRICT STRICT} or
* {@link ComparisonMode#BY_CONTRACT BY_CONTRACT}, then all available properties are compared including the
* {@linkplain #getAnchorPoint() anchor point}, {@linkplain #getRealizationEpoch() realization epoch},
* {@linkplain #getDomainOfValidity() domain of validity} and the {@linkplain #getScope() scope}.
*
* @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 (!super.equals(object, mode)) {
return false;
}
switch (mode) {
case STRICT: {
final AbstractDatum that = (AbstractDatum) object;
return this.realizationEpoch == that.realizationEpoch &&
Objects.equals(this.domainOfValidity, that.domainOfValidity) &&
Objects.equals(this.anchorDefinition, that.anchorDefinition) &&
Objects.equals(this.scope, that.scope);
}
case BY_CONTRACT: {
final Datum that = (Datum) object;
return deepEquals(getRealizationEpoch(), that.getRealizationEpoch(), mode) &&
deepEquals(getDomainOfValidity(), that.getDomainOfValidity(), mode) &&
deepEquals(getAnchorPoint(), that.getAnchorPoint(), mode) &&
deepEquals(getScope(), that.getScope(), mode);
}
default: {
/*
* Tests for identifiers or name since datum with different identification may have completely
* different meaning. We do not perform this check if the user asked for metadata comparison,
* because in such case the name and identifiers have already been compared by the subclass.
*
* According ISO 19162 (Well Known Text representation of Coordinate Reference Systems),
* identifiers shall have precedence over name at least in the case of operation methods
* and parameters. We extend this rule to datum as well.
*/
final Datum that = (Datum) object;
final Boolean match = Identifiers.hasCommonIdentifier(getIdentifiers(), that.getIdentifiers());
if (match != null) {
return match;
}
return isHeuristicMatchForName(that.getName().getCode())
|| IdentifiedObjects.isHeuristicMatchForName(that, getName().getCode());
}
}
}
/**
* 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.hash(anchorDefinition, realizationEpoch, domainOfValidity, scope);
}
/**
* Formats the inner part of the <cite>Well Known Text</cite> (WKT) representation for this datum.
* See {@link AbstractIdentifiedObject#formatTo(Formatter)} for more information.
*
* @param formatter the formatter where to format the inner content of this WKT element.
* @return the {@linkplain org.apache.sis.io.wkt.KeywordCase#CAMEL_CASE CamelCase} keyword
* for the WKT element, or {@code null} if unknown.
*/
@Override
protected String formatTo(final Formatter formatter) {
final Citation authority = formatter.getNameAuthority();
String name = IdentifiedObjects.getName(this, authority);
if (name == null) {
name = IdentifiedObjects.getName(this, null);
if (name == null) { // Should never happen, but be safe.
return super.formatTo(formatter);
}
if ("ESRI".equalsIgnoreCase(Citations.toCodeSpace(authority)) && !name.startsWith(Simplifier.ESRI_DATUM_PREFIX)) {
name = Simplifier.ESRI_DATUM_PREFIX + name;
}
}
formatter.append(name, ElementKind.DATUM);
return null;
}
//////////////////////////////////////////////////////////////////////////////////////////////////
//////// ////////
//////// 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 object 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.
*/
AbstractDatum() {
super(org.apache.sis.internal.referencing.NilReferencingObject.INSTANCE);
realizationEpoch = Long.MIN_VALUE;
}
/**
* Invoked by JAXB only at unmarshalling time.
*
* @see #getAnchorPoint()
*/
private void setAnchorPoint(final InternationalString value) {
if (anchorDefinition == null) {
anchorDefinition = value;
} else {
MetadataUtilities.propertyAlreadySet(AbstractDatum.class, "setAnchorPoint", "anchorDefinition");
}
}
/**
* Invoked by JAXB only at unmarshalling time.
*
* @see #getRealizationEpoch()
*/
private void setRealizationEpoch(final Date value) {
if (realizationEpoch == Long.MIN_VALUE) {
realizationEpoch = value.getTime();
} else {
MetadataUtilities.propertyAlreadySet(AbstractDatum.class, "setRealizationEpoch", "realizationEpoch");
}
}
/**
* Invoked by JAXB only at unmarshalling time.
*
* @see #getDomainOfValidity()
*/
private void setDomainOfValidity(final Extent value) {
if (domainOfValidity == null) {
domainOfValidity = value;
} else {
MetadataUtilities.propertyAlreadySet(AbstractDatum.class, "setDomainOfValidity", "domainOfValidity");
}
}
/**
* Invoked by JAXB only at unmarshalling time.
*
* @see #getScope()
*/
private void setScope(final InternationalString value) {
if (scope == null) {
scope = value;
} else {
MetadataUtilities.propertyAlreadySet(AbstractDatum.class, "setScope", "scope");
}
}
}