use of org.opengis.metadata.Identifier in project sis by apache.
the class IdentifierCommand method create.
/**
* Creates an identifier row for the given CRS.
* This method gives precedence to {@code "urn:ogc:def:"} identifiers if possible.
*
* @return the row, or {@code null} if no identifier has been found.
*/
static Row create(ReferenceSystem rs) throws FactoryException {
String identifier = IdentifiedObjects.lookupURN(rs, null);
if (identifier == null) {
/*
* If we can not find an identifier matching the EPSG or WMS definitions,
* look at the identifiers declared in the CRS and verify their validity.
*/
for (final Identifier id : rs.getIdentifiers()) {
final String c = IdentifiedObjects.toURN(rs.getClass(), id);
if (c != null) {
identifier = c;
// Stop at the first "urn:ogc:def:…".
break;
}
if (identifier == null) {
// "AUTHORITY:CODE" as a fallback if no URN.
identifier = IdentifiedObjects.toString(id);
}
}
if (identifier == null) {
// No identifier found.
return null;
}
}
/*
* The CRS provided by the user contains identifier, but the 'lookupURN' operation above failed to
* find it. The most likely cause is that the user-provided CRS does not use the same axis order.
*/
State state;
try {
final ReferenceSystem def = CRS.forCode(identifier);
final ComparisonMode c = ComparisonMode.equalityLevel(def, rs);
if (c == null) {
state = State.MISMATCH;
} else
switch(c) {
case ALLOW_VARIANT:
{
state = State.AXIS_ORDER;
break;
}
case APPROXIMATIVE:
{
state = State.APPROXIMATIVE;
rs = def;
break;
}
default:
{
state = State.VALID;
rs = def;
break;
}
}
} catch (NoSuchAuthorityCodeException e) {
state = State.UNKNOWN;
}
return new Row(state, identifier, rs.getName().getCode());
}
use of org.opengis.metadata.Identifier in project sis by apache.
the class CoordinateOperationFinder method createOperationStep.
/**
* Creates an operation between two geodetic (geographic or geocentric) coordinate reference systems.
* The default implementation can:
*
* <ul>
* <li>adjust axis order and orientation, for example converting from (<cite>North</cite>, <cite>West</cite>)
* axes to (<cite>East</cite>, <cite>North</cite>) axes,</li>
* <li>apply units conversion if needed,</li>
* <li>perform longitude rotation if needed,</li>
* <li>perform datum shift if {@linkplain BursaWolfParameters Bursa-Wolf parameters} are available
* for the area of interest.</li>
* </ul>
*
* <p>This method returns only <em>one</em> step for a chain of concatenated operations (to be built by the caller).
* But a list is returned because the same step may be implemented by different operation methods. Only one element
* in the returned list should be selected (usually the first one).</p>
*
* @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 not be constructed.
*/
@SuppressWarnings("null")
protected List<CoordinateOperation> createOperationStep(final GeodeticCRS sourceCRS, final GeodeticCRS targetCRS) throws FactoryException {
final GeodeticDatum sourceDatum = sourceCRS.getDatum();
final GeodeticDatum targetDatum = targetCRS.getDatum();
Matrix datumShift = null;
/*
* If the prime meridian is not the same, we will concatenate a longitude rotation before or after datum shift
* (that concatenation will be performed by the customized DefaultMathTransformFactory.Context created below).
* Actually we do not know if the longitude rotation should be before or after datum shift. But this ambiguity
* can usually be ignored because Bursa-Wolf parameters are always used with source and target prime meridians
* set to Greenwich in EPSG dataset 8.9. For safety, the SIS's DefaultGeodeticDatum class ensures that if the
* prime meridians are not the same, then the target meridian must be Greenwich.
*/
final DefaultMathTransformFactory.Context context = ReferencingUtilities.createTransformContext(sourceCRS, targetCRS, new MathTransformContext(sourceDatum, targetDatum));
/*
* If both CRS use the same datum and the same prime meridian, then the coordinate operation is only axis
* swapping, unit conversion or change of coordinate system type (Ellipsoidal ↔ Cartesian ↔ Spherical).
* Otherwise (if the datum are not the same), we will need to perform a scale, translation and rotation
* in Cartesian space using the Bursa-Wolf parameters. If the user does not require the best accuracy,
* then the Molodensky approximation may be used for avoiding the conversion step to geocentric CRS.
*/
Identifier identifier;
boolean isGeographicToGeocentric = false;
final CoordinateSystem sourceCS = context.getSourceCS();
final CoordinateSystem targetCS = context.getTargetCS();
if (equalsIgnoreMetadata(sourceDatum, targetDatum)) {
final boolean isGeocentricToGeographic;
isGeographicToGeocentric = (sourceCS instanceof EllipsoidalCS && targetCS instanceof CartesianCS);
isGeocentricToGeographic = (sourceCS instanceof CartesianCS && targetCS instanceof EllipsoidalCS);
/*
* Above booleans should never be true in same time. If it nevertheless happen (we are paranoiac;
* maybe a lazy user implemented all interfaces in a single class), do not apply any geographic ↔
* geocentric conversion. Instead do as if the coordinate system types were the same.
*/
if (isGeocentricToGeographic ^ isGeographicToGeocentric) {
identifier = GEOCENTRIC_CONVERSION;
} else {
identifier = AXIS_CHANGES;
}
} else {
identifier = ELLIPSOID_CHANGE;
if (sourceDatum instanceof DefaultGeodeticDatum) {
datumShift = ((DefaultGeodeticDatum) sourceDatum).getPositionVectorTransformation(targetDatum, areaOfInterest);
if (datumShift != null) {
identifier = DATUM_SHIFT;
}
}
}
/*
* Conceptually, all transformations below could done by first converting from the source coordinate
* system to geocentric Cartesian coordinates (X,Y,Z), apply an affine transform represented by the
* datum shift matrix, then convert from the (X′,Y′,Z′) coordinates to the target coordinate system.
* However there is two exceptions to this path:
*
* 1) In the particular where both the source and target CS are ellipsoidal, we may use the
* Molodensky approximation as a shortcut (if the desired accuracy allows).
*
* 2) Even if we really go through the XYZ coordinates without Molodensky approximation, there is
* at least 9 different ways to name this operation depending on whether the source and target
* CRS are geocentric or geographic, 2- or 3-dimensional, whether there is a translation or not,
* the rotation sign, etc. We try to use the most specific name if we can find one, and fallback
* on an arbitrary name only in last resort.
*/
final DefaultMathTransformFactory mtFactory = factorySIS.getDefaultMathTransformFactory();
MathTransform before = null, after = null;
ParameterValueGroup parameters;
if (identifier == DATUM_SHIFT || identifier == ELLIPSOID_CHANGE) {
/*
* If the transform can be represented by a single coordinate operation, returns that operation.
* Possible operations are:
*
* - Position Vector transformation (in geocentric, geographic-2D or geographic-3D domains)
* - Geocentric translation (in geocentric, geographic-2D or geographic-3D domains)
* - [Abridged] Molodensky (as an approximation of geocentric translation)
* - Identity (if the desired accuracy is so large than we can skip datum shift)
*
* TODO: if both CS are ellipsoidal but with different number of dimensions, then we should use
* an intermediate 3D geographic CRS in order to enable the use of Molodensky method if desired.
*/
final DatumShiftMethod preferredMethod = DatumShiftMethod.forAccuracy(desiredAccuracy);
parameters = GeocentricAffine.createParameters(sourceCS, targetCS, datumShift, preferredMethod);
if (parameters == null) {
/*
* Failed to select a coordinate operation. Maybe because the coordinate system types are not the same.
* Convert unconditionally to XYZ geocentric coordinates and apply the datum shift in that CS space.
*
* TODO: operation name should not be "Affine" if 'before' or 'after' transforms are not identity.
* Reminder: the parameter group name here determines the OperationMethod later in this method.
*/
if (datumShift != null) {
parameters = TensorParameters.WKT1.createValueGroup(properties(Constants.AFFINE), datumShift);
} else {
// Dimension of geocentric CRS.
parameters = Affine.identity(3);
}
final CoordinateSystem normalized = CommonCRS.WGS84.geocentric().getCoordinateSystem();
before = mtFactory.createCoordinateSystemChange(sourceCS, normalized, sourceDatum.getEllipsoid());
after = mtFactory.createCoordinateSystemChange(normalized, targetCS, targetDatum.getEllipsoid());
context.setSource(normalized);
context.setTarget(normalized);
}
} else if (identifier == GEOCENTRIC_CONVERSION) {
parameters = (isGeographicToGeocentric ? GeographicToGeocentric.PARAMETERS : GeocentricToGeographic.PARAMETERS).createValue();
} else {
/*
* Coordinate system change (including change in the number of dimensions) without datum shift.
*/
final int sourceDim = sourceCS.getDimension();
final int targetDim = targetCS.getDimension();
if (// sourceDim == 2 or 3.
(sourceDim & ~1) == 2 && // abs(sourceDim - targetDim) == 1.
(sourceDim ^ targetDim) == 1 && (sourceCS instanceof EllipsoidalCS) && (targetCS instanceof EllipsoidalCS)) {
parameters = (sourceDim == 2 ? Geographic2Dto3D.PARAMETERS : Geographic3Dto2D.PARAMETERS).createValue();
} else {
/*
* TODO: instead than creating parameters for an identity operation, we should create the
* CoordinateOperation directly from the MathTransform created by mtFactory below.
* The intent if to get the correct OperationMethod, which should not be "Affine"
* if there is a CS type change.
*/
parameters = Affine.identity(targetDim);
/*
* createCoordinateSystemChange(…) needs the ellipsoid associated to the ellipsoidal coordinate system,
* if any. If none or both coordinate systems are ellipsoidal, then the ellipsoid will be ignored (see
* createCoordinateSystemChange(…) javadoc for the rational) so it does not matter which one we pick.
*/
before = mtFactory.createCoordinateSystemChange(sourceCS, targetCS, (sourceCS instanceof EllipsoidalCS ? sourceDatum : targetDatum).getEllipsoid());
context.setSource(targetCS);
}
}
/*
* Transform between differents datums 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, unless the Molodensky approximation is used. The following steps are applied:
*
* source CRS →
* normalized CRS with source datum →
* normalized CRS with target datum →
* target CRS
*
* Those steps may be either explicit with the 'before' and 'after' transform, or implicit with the
* Context parameter.
*/
MathTransform transform = mtFactory.createParameterizedTransform(parameters, context);
final OperationMethod method = mtFactory.getLastMethodUsed();
if (before != null) {
transform = mtFactory.createConcatenatedTransform(before, transform);
if (after != null) {
transform = mtFactory.createConcatenatedTransform(transform, after);
}
}
return asList(createFromMathTransform(properties(identifier), sourceCRS, targetCRS, transform, method, parameters, null));
}
use of org.opengis.metadata.Identifier in project sis by apache.
the class CoordinateOperationFinder method derivedFrom.
/**
* Returns a name for an object derived from the specified one.
* This method builds a name of the form "{@literal <original identifier>} (step 1)"
* where "(step 1)" may be replaced by "(step 2)", "(step 3)", <i>etc.</i> if this
* method has already been invoked for the same identifier (directly or indirectly).
*/
private Map<String, ?> derivedFrom(final IdentifiedObject object) {
Identifier oldID = object.getName();
Object p = identifierOfStepCRS.get(oldID);
if (p instanceof Identifier) {
oldID = (Identifier) p;
p = identifierOfStepCRS.get(oldID);
}
final int count = (p != null) ? (Integer) p + 1 : 1;
final Identifier newID = new NamedIdentifier(Citations.SIS, oldID.getCode() + " (step " + count + ')');
identifierOfStepCRS.put(newID, oldID);
identifierOfStepCRS.put(oldID, count);
final Map<String, Object> properties = new HashMap<>(4);
properties.put(IdentifiedObject.NAME_KEY, newID);
properties.put(IdentifiedObject.REMARKS_KEY, Vocabulary.formatInternational(Vocabulary.Keys.DerivedFrom_1, CRSPair.label(object)));
return properties;
}
use of org.opengis.metadata.Identifier in project sis by apache.
the class GeodeticAuthorityFactory method cast.
/**
* Casts the given object to the given type, or throws an exception if the object can not be casted.
* This convenience method is provided for implementation of {@code createXXX} methods.
*
* @param type the type to return (e.g. {@code CoordinateReferenceSystem.class}).
* @param object the object to cast.
* @param code the authority code, used only for formatting an error message.
* @return the object casted to the given type.
* @throws NoSuchAuthorityCodeException if the given object is not an instance of the given type.
*/
@SuppressWarnings("unchecked")
private <T> T cast(final Class<T> type, final IdentifiedObject object, final String code) throws NoSuchAuthorityCodeException {
if (type.isInstance(object)) {
return (T) object;
}
/*
* Get the actual type of the object. Returns the GeoAPI type if possible,
* or fallback on the implementation class otherwise.
*/
final Class<?> actual;
if (object instanceof AbstractIdentifiedObject) {
actual = ((AbstractIdentifiedObject) object).getInterface();
} else {
actual = object.getClass();
}
/*
* Get the authority from the object if possible, in order to avoid a call
* to the potentially costly (for EPSGDataAccess) getAuthority() method.
*/
final Identifier id = object.getName();
final Citation authority = (id != null) ? id.getAuthority() : getAuthority();
throw new NoSuchAuthorityCodeException(Errors.format(Errors.Keys.UnexpectedTypeForReference_3, code, type, actual), Citations.getIdentifier(authority, false), trimNamespace(code), code);
}
use of org.opengis.metadata.Identifier in project sis by apache.
the class DefaultCoordinateSystemAxis method equals.
/**
* Compares the specified object with this axis for equality.
* The strictness level is controlled by the second argument.
* This method compares the following properties in every cases:
*
* <ul>
* <li>{@link #getName()}</li>
* <li>{@link #getDirection()}</li>
* <li>{@link #getUnit()}</li>
* </ul>
*
* In the particular case where {@link #getRangeMeaning()} is {@code WRAPAROUND}, then {@link #getMinimumValue()}
* and {@link #getMaximumValue()} are considered non-ignorable metadata and will be compared for every modes.
* All other properties are compared only for modes stricter than {@link ComparisonMode#IGNORE_METADATA}.
*
* @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) {
// Slight optimization.
return true;
}
if (!super.equals(object, mode)) {
return false;
}
switch(mode) {
case STRICT:
{
final DefaultCoordinateSystemAxis that = (DefaultCoordinateSystemAxis) object;
return Objects.equals(unit, that.unit) && Objects.equals(direction, that.direction) && Objects.equals(abbreviation, that.abbreviation) && Objects.equals(rangeMeaning, that.rangeMeaning) && doubleToLongBits(minimumValue) == doubleToLongBits(that.minimumValue) && doubleToLongBits(maximumValue) == doubleToLongBits(that.maximumValue);
}
case BY_CONTRACT:
{
final CoordinateSystemAxis that = (CoordinateSystemAxis) object;
return equalsIgnoreMetadata(that, mode, true) && Objects.equals(getAbbreviation(), that.getAbbreviation()) && Objects.equals(getRangeMeaning(), that.getRangeMeaning());
}
}
/*
* At this point the comparison is in "ignore metadata" mode. We compare the axis range
* only if the range meaning is "wraparound" for both axes, because only in such case a
* coordinate operation may shift some ordinate values (typically ±360° on longitudes).
*/
final CoordinateSystemAxis that = (CoordinateSystemAxis) object;
if (!equalsIgnoreMetadata(that, mode, RangeMeaning.WRAPAROUND.equals(this.getRangeMeaning()) && RangeMeaning.WRAPAROUND.equals(that.getRangeMeaning()))) {
return false;
}
Identifier name = that.getName();
if (name != UNNAMED) {
/*
* Checking the abbreviation is not sufficient. For example the polar angle and the
* spherical latitude have the same abbreviation (θ). Legacy names like "Longitude"
* (in addition to ISO 19111 "Geodetic longitude") bring more potential confusion.
* Furthermore, not all implementors use the greek letters. For example most CRS in
* WKT format use the "Lat" abbreviation instead of the greek letter φ.
* For comparisons without metadata, we ignore the unreliable abbreviation and check
* the axis name instead. These names are constrained by ISO 19111 specification
* (see class javadoc), so they should be reliable enough.
*
* Note: there is no need to execute this block if metadata are not ignored,
* because in this case a stricter check has already been performed by
* the 'equals' method in the superclass.
*/
final String thatCode = name.getCode();
if (!isHeuristicMatchForName(thatCode)) {
name = getName();
if (name != UNNAMED) {
/*
* The above test checked for special cases ("Lat" / "Lon" aliases, etc.).
* The next line may repeat the same check, so we may have a partial waste
* of CPU. But we do it anyway for checking the 'that' aliases, and also
* because the user may have overridden 'that.isHeuristicMatchForName(…)'.
*/
final String thisCode = name.getCode();
if (!IdentifiedObjects.isHeuristicMatchForName(that, thisCode)) {
// Check for the special case of "x" and "y" axis names.
if (!isHeuristicMatchForNameXY(thatCode, thisCode) && !isHeuristicMatchForNameXY(thisCode, thatCode)) {
return false;
}
}
}
}
}
return true;
}
Aggregations