blob: 4f5fe292456dba21612382314b4b068fae5b0861 [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.Arrays;
import java.io.Serializable;
import org.opengis.metadata.extent.Extent;
import org.opengis.referencing.datum.GeodeticDatum;
import org.opengis.referencing.datum.PrimeMeridian;
import org.opengis.referencing.operation.Matrix;
import org.apache.sis.referencing.operation.matrix.Matrix4;
import org.apache.sis.referencing.operation.matrix.Matrices;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.IdentifiedObjects;
import org.apache.sis.io.wkt.FormattableObject;
import org.apache.sis.io.wkt.Formatter;
import org.apache.sis.util.Utilities;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.internal.util.DoubleDouble;
import org.apache.sis.internal.referencing.WKTKeywords;
import org.apache.sis.internal.referencing.Resources;
import static java.lang.Math.abs;
import static org.apache.sis.util.ArgumentChecks.*;
import static org.apache.sis.referencing.operation.matrix.Matrix4.SIZE;
// Branch-dependent imports
import java.util.Objects;
/**
* Parameters for a geographic transformation between two datum having the same prime meridian.
* Bursa-Wolf parameters are also known as <cite>Helmert transformation parameters</cite>.
* For an explanation of their purpose, see the <cite>Bursa-Wolf parameters</cite> section
* of {@link DefaultGeodeticDatum} class javadoc.
*
* <p>The Bursa-Wolf parameters shall be applied to geocentric coordinates,
* where the <var>X</var> axis points towards the Prime Meridian (usually Greenwich),
* the <var>Y</var> axis points East, and the <var>Z</var> axis points North.</p>
*
* <div class="note"><b>Note:</b>
* The upper case letters are intentional. By convention, (<var>X</var>, <var>Y</var>, <var>Z</var>)
* stand for <cite>geocentric</cite> coordinates while (<var>x</var>, <var>y</var>, <var>z</var>)
* stand for <cite>projected</cite> coordinates.</div>
*
* The "Bursa-Wolf" formula is expressed with 7 parameters, listed in the table below.
* The <cite>code</cite>, <cite>name</cite> and <cite>abbreviation</cite> columns list EPSG identifiers,
* while the <cite>legacy</cite> column lists the identifiers used in the legacy OGC 01-009 specification
* (still used in some <cite>Well Known Texts</cite>).
*
* <div class="horizontal-flow">
* <div><table class="sis">
* <caption>Parameters defined by EPSG</caption>
* <tr><th>Code</th> <th>Name</th> <th>Abbr.</th> <th>Legacy</th></tr>
* <tr><td>8605</td> <td>X-axis translation</td> <td>{@link #tX}</td> <td>{@code dx}</td></tr>
* <tr><td>8606</td> <td>Y-axis translation</td> <td>{@link #tY}</td> <td>{@code dy}</td></tr>
* <tr><td>8607</td> <td>Z-axis translation</td> <td>{@link #tZ}</td> <td>{@code dz}</td></tr>
* <tr><td>8608</td> <td>X-axis rotation</td> <td>{@link #rX}</td> <td>{@code ex}</td></tr>
* <tr><td>8609</td> <td>Y-axis rotation</td> <td>{@link #rY}</td> <td>{@code ey}</td></tr>
* <tr><td>8610</td> <td>Z-axis rotation</td> <td>{@link #rZ}</td> <td>{@code ez}</td></tr>
* <tr><td>8611</td> <td>Scale difference</td> <td>{@link #dS}</td> <td>{@code ppm}</td></tr>
* </table></div>
* <div><p><b>Geocentric coordinates transformation</b></p>
* <p>from (<var>X</var><sub>s</sub>, <var>Y</var><sub>s</sub>, <var>Z</var><sub>s</sub>)
* to (<var>X</var><sub>t</sub>, <var>Y</var><sub>t</sub>, <var>Z</var><sub>t</sub>)
* <br><span style="font-size:small">(ignoring unit conversions)</span></p>
*
* <p>{@include formulas.html#Bursa-Wolf}</p>
* </div></div>
*
* The numerical fields in this {@code BursaWolfParameters} class use the EPSG abbreviations
* with 4 additional constraints compared to the EPSG definitions:
*
* <ul>
* <li>Unit of scale difference ({@link #dS}) is fixed to <em>parts per million</em>.</li>
* <li>Unit of translation terms ({@link #tX}, {@link #tY}, {@link #tZ}) is fixed to <em>metres</em>.</li>
* <li>Unit of rotation terms ({@link #rX}, {@link #rY}, {@link #rZ}) is fixed to <em>arc-seconds</em>.</li>
* <li>Sign of rotation terms is fixed to the <em>Position Vector</em> convention (EPSG operation method 9606).
* This is the opposite sign than the <cite>Coordinate Frame Rotation</cite> (EPSG operation method 9607).
* The Position Vector convention is used by IAG and recommended by ISO 19111.</li>
* </ul>
*
* <h2>Source and target geodetic datum</h2>
* The <var>source datum</var> in above coordinates transformation is the {@link DefaultGeodeticDatum} instance
* that contain this {@code BursaWolfParameters}. It can be any datum, including datum that are valid only locally.
* The <var>{@linkplain #getTargetDatum() target datum}</var> is specified at construction time and is often,
* but not necessarily, the <cite>World Geodetic System 1984</cite> (WGS 84) datum.
*
* <p>If the source and target datum does not have the same {@linkplain DefaultGeodeticDatum#getPrimeMeridian()
* prime meridian}, then it is user's responsibility to apply longitude rotation before to use the Bursa-Wolf
* parameters.</p>
*
* <h2>When Bursa-Wolf parameters are used</h2>
* {@code BursaWolfParameters} are used in three contexts:
* <ol>
* <li>Created as a step while creating a {@linkplain org.apache.sis.referencing.operation.AbstractCoordinateOperation
* coordinate operation} from the EPSG database.</li>
* <li>Associated to a {@link DefaultGeodeticDatum} with the WGS 84 {@linkplain #getTargetDatum() target datum} for
* providing the parameter values to display in the {@code TOWGS84[…]} element of <cite>Well Known Text</cite>
* (WKT) version 1. Note that WKT version 2 does not have {@code TOWGS84[…]} element anymore.</li>
* <li>Specified at {@code DefaultGeodeticDatum} construction time for arbitrary target datum.
* Apache SIS will ignore those Bursa-Wolf parameters, except as a fallback if no parameters
* can been found in the EPSG database for a given pair of source and target CRS.</li>
* </ol>
*
* <div class="note"><b>Note:</b>
* In EPSG terminology, Apache SIS gives precedence to the <cite>late-binding</cite> approach
* (case 1 above) over the <cite>early-binding</cite> approach (case 3 above).</div>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 0.7
*
* @see DefaultGeodeticDatum#getBursaWolfParameters()
* @see <a href="http://en.wikipedia.org/wiki/Helmert_transformation">Wikipedia: Helmert transformation</a>
*
* @since 0.4
* @module
*/
public class BursaWolfParameters extends FormattableObject implements Cloneable, Serializable {
/**
* Serial number for inter-operability with different versions.
*/
private static final long serialVersionUID = 754825592343010900L;
/**
* The conversion factor from <cite>parts per million</cite> to scale minus one.
*/
static final double PPM = 1E+6;
/**
* X-axis translation in metres (EPSG:8605).
* The legacy OGC parameter name is {@code "dx"}.
*/
public double tX;
/**
* Y-axis translation in metres (EPSG:8606).
* The legacy OGC parameter name is {@code "dy"}.
*/
public double tY;
/**
* Z-axis translation in metres (EPSG:8607).
* The legacy OGC parameter name is {@code "dz"}.
*/
public double tZ;
/**
* X-axis rotation in arc-seconds (EPSG:8608), sign following the <cite>Position Vector</cite> convention.
* The legacy OGC parameter name is {@code "ex"}.
*/
public double rX;
/**
* Y-axis rotation in arc-seconds (EPSG:8609), sign following the <cite>Position Vector</cite> convention.
* The legacy OGC parameter name is {@code "ey"}.
*/
public double rY;
/**
* Z-axis rotation in arc-seconds (EPSG:8610), sign following the <cite>Position Vector</cite> convention.
* The legacy OGC parameter name is {@code "ez"}.
*/
public double rZ;
/**
* The scale difference in parts per million (EPSG:8611).
* The legacy OGC parameter name is {@code "ppm"}.
*
* <div class="note"><b>Example:</b>
* If a distance of 100 km in the source coordinate reference system translates into a distance of 100.001 km
* in the target coordinate reference system, the scale difference is 1 ppm (the ratio being 1.000001).</div>
*/
public double dS;
/**
* The target datum for this set of parameters, or {@code null} if unknown.
* This is usually the WGS 84 datum, but other targets are allowed.
*
* <p>The source datum is the {@link DefaultGeodeticDatum} that contain this {@code BursaWolfParameters}
* instance.</p>
*
* @see #getTargetDatum()
*/
private final GeodeticDatum targetDatum;
/**
* Region or timeframe in which a coordinate transformation based on those Bursa-Wolf parameters is valid,
* or {@code null} if unspecified.
*
* @see #getDomainOfValidity()
*/
private final Extent domainOfValidity;
/**
* Creates a new instance for the given target datum and domain of validity.
* All numerical parameters are initialized to 0, which correspond to an identity transform.
* Callers can assign numerical values to the public fields of interest after construction.
* For example, many coordinate transformations will provide values only for the translation
* terms ({@link #tX}, {@link #tY}, {@link #tZ}).
*
* <p>Alternatively, numerical fields can also be initialized by a call to
* {@link #setPositionVectorTransformation(Matrix, double)}.</p>
*
* @param targetDatum the target datum (usually WGS 84) for this set of parameters, or {@code null} if unknown.
* @param domainOfValidity area or region in which a coordinate transformation based on those Bursa-Wolf parameters
* is valid, or {@code null} is unspecified.
*/
public BursaWolfParameters(final GeodeticDatum targetDatum, final Extent domainOfValidity) {
this.targetDatum = targetDatum;
this.domainOfValidity = domainOfValidity;
}
/**
* Verifies parameters validity after initialization of {@link DefaultGeodeticDatum}.
* This method requires that the prime meridian of the target datum is either the same
* than the enclosing {@code GeodeticDatum}, or Greenwich. We put this restriction for
* avoiding ambiguity about whether the longitude rotation should be applied before or
* after the datum shift.
*
* <p>If the target prime meridian is Greenwich, then SIS will assume that the datum shift
* needs to be applied in a coordinate system having Greenwich as the prime meridian.</p>
*
* <p><b>Maintenance note:</b>
* if the above policy regarding prime meridians is modified, then some {@code createOperationStep(…)} method
* implementations in {@link org.apache.sis.referencing.operation.CoordinateOperationFinder} may need to be
* revisited. See especially the methods creating a transformation between a pair of {@code GeocentricCRS} or
* between a pair of {@code GeographicCRS} (tip: search for {@code DefaultGeodeticDatum}).</p>
*
* @param pm the prime meridian of the enclosing {@code GeodeticDatum}.
*/
void verify(final PrimeMeridian pm) throws IllegalArgumentException {
if (targetDatum != null) {
final PrimeMeridian actual = targetDatum.getPrimeMeridian();
if (actual.getGreenwichLongitude() != 0 && !Utilities.equalsIgnoreMetadata(pm, actual)) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.MismatchedPrimeMeridian_2,
IdentifiedObjects.getName(pm, null), IdentifiedObjects.getName(actual, null)));
}
}
ensureFinite("tX", tX);
ensureFinite("tY", tY);
ensureFinite("tZ", tZ);
ensureFinite("rX", rX);
ensureFinite("rY", rY);
ensureFinite("rZ", rZ);
ensureBetween("dS", -PPM, PPM, dS); // For preventing zero or negative value on the matrix diagonal.
}
/**
* Returns the target datum for this set of parameters, or {@code null} if unknown.
* This is usually the WGS 84 datum, but other targets are allowed.
*
* <p>The source datum is the {@link DefaultGeodeticDatum} that contain this {@code BursaWolfParameters}
* instance.</p>
*
* @return the target datum for this set of parameters, or {@code null} if unknown.
*/
public GeodeticDatum getTargetDatum() {
return targetDatum;
}
/**
* Returns the parameter values. The length of the returned array depends on the values:
*
* <ul>
* <li>If this instance is an {@link TimeDependentBWP}, then the array length will be 14.</li>
* <li>Otherwise if this instance contains a non-zero {@link #dS} value, then the array length will be 7 with
* {@link #tX}, {@link #tY}, {@link #tZ}, {@link #rX}, {@link #rY}, {@link #rZ} and {@link #dS} values
* in that order.</li>
* <li>Otherwise if this instance contains non-zero rotation terms,
* then this method returns the first 6 of the above-cited values.</li>
* <li>Otherwise (i.e. this instance {@linkplain #isTranslation() is a translation}),
* this method returns only the first 3 of the above-cited values.</li>
* </ul>
*
* <div class="note"><b>Note:</b>
* the rules about the arrays of length 3, 6 or 7 are derived from the <cite>Well Known Text</cite> (WKT)
* version 1 specification. The rule about the array of length 14 is an extension.</div>
*
* @return the parameter values as an array of length 3, 6, 7 or 14.
*
* @since 0.6
*/
@SuppressWarnings("fallthrough")
public double[] getValues() {
final double[] elements = new double[(dS != 0) ? 7 : (rZ != 0 || rY != 0 || rX != 0) ? 6 : 3];
switch (elements.length) {
default: elements[6] = dS; // Fallthrough everywhere.
case 6: elements[5] = rZ;
elements[4] = rY;
elements[3] = rX;
case 3: elements[2] = tZ;
elements[1] = tY;
elements[0] = tX;
}
return elements;
}
/**
* Sets the parameters to the given values. The given array can have any length. The first array elements will be
* assigned to the {@link #tX}, {@link #tY}, {@link #tZ}, {@link #rX}, {@link #rY}, {@link #rZ} and {@link #dS}
* fields in that order.
*
* <ul>
* <li>If the length of the given array is not sufficient for assigning a value to every fields,
* then the remaining fields are left unchanged (they are <strong>not</strong> reset to zero,
* but this is not a problem if this {@code BursaWolfParameters} is a new instance).</li>
* <li>If the length of the given array is greater than necessary, then extra elements are ignored by this base
* class. Note however that those extra elements may be used by subclasses like {@link TimeDependentBWP}.</li>
* </ul>
*
* @param elements the new parameter values, as an array of any length.
*
* @since 0.6
*/
@SuppressWarnings("fallthrough")
public void setValues(final double... elements) {
switch (elements.length) {
default: dS = elements[6]; // Fallthrough everywhere.
case 6: rZ = elements[5];
case 5: rY = elements[4];
case 4: rX = elements[3];
case 3: tZ = elements[2];
case 2: tY = elements[1];
case 1: tX = elements[0];
case 0: break;
}
}
/**
* Returns {@code true} if the {@linkplain #targetDatum target datum} is equals (at least on computation purpose)
* to the WGS84 datum. If the datum is unspecified, then this method returns {@code true} since WGS84 is the only
* datum supported by the WKT 1 format, and is what users often mean.
*
* @return {@code true} if the given datum is equal to WGS84 for computational purpose.
*/
final boolean isToWGS84() {
return (targetDatum != null) && IdentifiedObjects.isHeuristicMatchForName(targetDatum, "WGS84");
}
/**
* Returns {@code true} if a transformation built from this set of parameters would perform no operation.
* This is true when the value of all parameters is zero.
*
* @return {@code true} if the parameters describe no operation.
*/
public boolean isIdentity() {
return tX == 0 && tY == 0 && tZ == 0 && isTranslation();
}
/**
* Returns {@code true} if a transformation built from this set of parameters would perform only a translation.
*
* @return {@code true} if the parameters describe a translation only.
*/
public boolean isTranslation() {
return rX == 0 && rY == 0 && rZ == 0 && dS == 0;
}
/**
* Inverts in-place the sign of rotation terms ({@link #rX}, {@link #rY}, {@link #rZ}).
* This method can be invoked for converting a <cite>Coordinate Frame Rotation</cite> transformation
* (EPSG operation method 9607) to a <em>Position Vector</em> transformation (EPSG operation method 9606).
* The later convention is used by IAG and recommended by ISO 19111.
*/
public void reverseRotation() {
rX = -rX;
rY = -rY;
rZ = -rZ;
}
/**
* Inverts in-place the transformation by inverting the sign of all numerical parameters.
* The {@linkplain #getPositionVectorTransformation(Date) position vector transformation} matrix
* created from inverted Bursa-Wolf parameters will be <strong>approximately</strong> equals
* to the {@linkplain org.apache.sis.referencing.operation.matrix.MatrixSIS#inverse() inverse}
* of the matrix created from the original parameters. The equality holds approximately only
* because the parameter values are very small (parts per millions and arc-seconds).
*/
public void invert() {
final double[] values = getValues();
for (int i=0; i<values.length; i++) {
values[i] = -values[i];
}
setValues(values);
}
/**
* Returns the elapsed time from the {@linkplain TimeDependentBWP#getTimeReference() reference time}
* to the given date, in millennium. If this {@code BursaWolfParameters} is not time-dependent, then
* returns {@code null}.
*/
DoubleDouble period(final Date time) {
return null;
}
/**
* Returns the parameter at the given index. If this {@code BursaWolfParameters} is time-dependent,
* then the returned value shall be corrected for the given period.
*
* @param index 0 for {@code tX}, 1 for {@code tY}, <i>etc.</i> in {@code TOWGS84[…]} order.
* @param period the value computed by {@link #period(Date)}, or {@code null}.
*/
DoubleDouble param(final int index, final DoubleDouble period) {
final double p;
switch (index) {
case 0: p = tX; break;
case 1: p = tY; break;
case 2: p = tZ; break;
case 3: p = rX; break;
case 4: p = rY; break;
case 5: p = rZ; break;
case 6: p = dS; break;
default: throw new AssertionError(index);
}
return DoubleDouble.createAndGuessError(p);
}
/**
* Returns the position vector transformation (geocentric domain) as an affine transform.
* For transformations that do not depend on time, the formula is as below where {@code R}
* is a conversion factor from arc-seconds to radians:
*
* <blockquote><pre> R = toRadians(1″)
* S = 1 + {@linkplain #dS}/1000000
* ┌ ┐ ┌ ┐ ┌ ┐
* │ X' │ │ S -{@linkplain #rZ}*RS +{@linkplain #rY}*RS {@linkplain #tX} │ │ X │
* │ Y' │ = │ +{@linkplain #rZ}*RS S -{@linkplain #rX}*RS {@linkplain #tY} │ │ Y │
* │ Z' │ │ -{@linkplain #rY}*RS +{@linkplain #rX}*RS S {@linkplain #tZ} │ │ Z │
* │ 1 │ │ 0 0 0 1 │ │ 1 │
* └ ┘ └ ┘ └ ┘</pre></blockquote>
*
* This affine transform can be applied on <strong>geocentric</strong> coordinates.
* This is identified as operation method 1033 in the EPSG database.
* Those geocentric coordinates are typically converted from geographic coordinates
* in the region or timeframe given by {@link #getDomainOfValidity()}.
*
* <p>If the source datum and the {@linkplain #getTargetDatum() target datum} do not use the same
* {@linkplain DefaultGeodeticDatum#getPrimeMeridian() prime meridian}, then it is caller's responsibility
* to apply longitude rotation before to use the matrix returned by this method.</p>
*
* <h4>Time-dependent transformation</h4>
* Some transformations use parameters that vary with time (e.g. operation method EPSG:1053).
* Users can optionally specify a date for which the transformation is desired.
* For transformations that do not depends on time, this date is ignored and can be null.
* For time-dependent transformations, {@code null} values default to the transformation's
* {@linkplain TimeDependentBWP#getTimeReference() reference time}.
*
* <h4>Inverse transformation</h4>
* The inverse transformation can be approximated by reversing the sign of the 7 parameters before to use them
* in the above matrix. This is often considered sufficient since <cite>position vector transformations</cite>
* are themselves approximations. However Apache SIS will rather use
* {@link org.apache.sis.referencing.operation.matrix.MatrixSIS#inverse()} in order to increase the chances
* that concatenation of transformations <var>A</var> → <var>B</var> followed by <var>B</var> → <var>A</var>
* gives back the identity transform.
*
* @param time date for which the transformation is desired, or {@code null} for the transformation's reference time.
* @return an affine transform in geocentric space created from this Bursa-Wolf parameters and the given time.
*
* @see DefaultGeodeticDatum#getPositionVectorTransformation(GeodeticDatum, Extent)
*/
public Matrix getPositionVectorTransformation(final Date time) {
final DoubleDouble period = period(time);
if (period == null && isTranslation()) {
final Matrix4 matrix = new Matrix4();
matrix.m03 = tX;
matrix.m13 = tY;
matrix.m23 = tZ;
return matrix;
}
/*
* Above was an optimization for the common case where the Bursa-Wolf parameters contain only
* translation terms. If we have rotation or scale terms, then use double-double arithmetic.
*/
final DoubleDouble RS = DoubleDouble.createSecondsToRadians();
final DoubleDouble S = param(6, period);
S.divide(PPM);
S.add(1); // S = 1 + dS / PPM;
RS.multiply(S); // RS = toRadians(1″) * S;
final DoubleDouble X = param(3, period); X.multiply(RS);
final DoubleDouble Y = param(4, period); Y.multiply(RS);
final DoubleDouble Z = param(5, period); Z.multiply(RS);
final DoubleDouble mX = new DoubleDouble(X); mX.negate();
final DoubleDouble mY = new DoubleDouble(Y); mY.negate();
final DoubleDouble mZ = new DoubleDouble(Z); mZ.negate();
final Integer O = 0; // Fetch Integer instance only once.
return Matrices.create(4, 4, new Number[] {
S, mZ, Y, param(0, period),
Z, S, mX, param(1, period),
mY, X, S, param(2, period),
O, O, O, 1});
}
/**
* Sets all Bursa-Wolf parameters from the given <cite>Position Vector transformation</cite> matrix.
* The matrix shall comply to the following constraints:
*
* <ul>
* <li>The matrix shall be {@linkplain org.apache.sis.referencing.operation.matrix.MatrixSIS#isAffine() affine}.</li>
* <li>The sub-matrix defined by {@code matrix} without the last row and last column shall be
* <a href="http://en.wikipedia.org/wiki/Skew-symmetric_matrix">skew-symmetric</a> (a.k.a. antisymmetric).</li>
* </ul>
*
* @param matrix the matrix from which to get Bursa-Wolf parameters.
* @param tolerance the tolerance error for the skew-symmetric matrix test, in units of PPM or arc-seconds (e.g. 1E-8).
* @throws IllegalArgumentException if the specified matrix does not met the conditions.
*
* @see #getPositionVectorTransformation(Date)
*/
public void setPositionVectorTransformation(final Matrix matrix, final double tolerance) throws IllegalArgumentException {
final int numRow = matrix.getNumRow();
final int numCol = matrix.getNumCol();
if (numRow != SIZE || numCol != SIZE) {
final Integer n = SIZE;
throw new IllegalArgumentException(Errors.format(Errors.Keys.MismatchedMatrixSize_4, n, n, numRow, numCol));
}
if (!Matrices.isAffine(matrix)) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.NotAnAffineTransform));
}
/*
* Translation terms, taken "as-is".
* If the matrix contains only translation terms (which is often the case), we are done.
*/
tX = matrix.getElement(0,3);
tY = matrix.getElement(1,3);
tZ = matrix.getElement(2,3);
if (Matrices.isTranslation(matrix)) { // Optimization for a common case.
return;
}
/*
* Scale factor: take the average of elements on the diagonal. All those
* elements should have the same value, but we tolerate slight deviation
* (this will be verified later).
*/
final DoubleDouble S = DoubleDouble.castOrCopy(getNumber(matrix, 0,0));
S.addGuessError(getNumber(matrix, 1,1));
S.addGuessError(getNumber(matrix, 2,2));
S.divide(3);
/*
* Computes: RS = S * toRadians(1″)
* dS = (S-1) * PPM
*/
final DoubleDouble RS = DoubleDouble.createSecondsToRadians();
RS.multiply(S);
S.add(-1);
S.multiply(PPM);
dS = S.doubleValue();
/*
* Rotation terms. Each rotation terms appear twice, with one value being the negative of the other value.
* We verify this skew symmetric aspect in the loop. We also opportunistically verify that the scale terms
* are uniform.
*/
for (int j=0; j < SIZE-1; j++) {
if (!(abs((matrix.getElement(j,j) - 1)*PPM - dS) <= tolerance)) {
throw new IllegalArgumentException(Resources.format(Resources.Keys.NonUniformScale));
}
for (int i = j+1; i < SIZE-1; i++) {
S.setFrom(RS);
S.inverseDivideGuessError(getNumber(matrix, j,i)); // Negative rotation term.
double value = S.value;
double error = S.error;
S.setFrom(RS);
S.inverseDivideGuessError(getNumber(matrix, i,j)); // Positive rotation term.
if (!(abs(value + S.value) <= tolerance)) { // We expect r1 ≈ -r2
throw new IllegalArgumentException(Resources.format(Resources.Keys.NotASkewSymmetricMatrix));
}
S.subtract(value, error);
S.multiply(0.5);
value = S.doubleValue(); // Average of the two rotation terms.
switch (j*SIZE + i) {
case 1: rZ = value; break;
case 2: rY = -value; break;
case 6: rX = value; break;
}
}
}
}
/**
* Retrieves the value at the specified row and column of the given matrix, wrapped in a {@code Number}.
* The {@code Number} type depends on the matrix accuracy.
*
* @param matrix the matrix from which to get the number.
* @param row the row index, from 0 inclusive to {@link Matrix#getNumRow()} exclusive.
* @param column the column index, from 0 inclusive to {@link Matrix#getNumCol()} exclusive.
* @return the current value at the given row and column.
*/
private static Number getNumber(final Matrix matrix, final int row, final int column) {
if (matrix instanceof MatrixSIS) {
return ((MatrixSIS) matrix).getNumber(row, column);
} else {
return matrix.getElement(row, column);
}
}
/**
* Returns the region or timeframe in which a coordinate transformation based on those Bursa-Wolf parameters is
* valid, or {@code null} if unspecified. If an extent was specified at construction time, then that extent is
* returned. Otherwise the datum domain of validity (which may be {@code null}) is returned.
*
* @return area or region or timeframe in which the coordinate transformation is valid, or {@code null}.
*
* @see org.apache.sis.metadata.iso.extent.DefaultExtent
*/
public Extent getDomainOfValidity() {
if (domainOfValidity == null && targetDatum != null) {
return targetDatum.getDomainOfValidity();
}
return domainOfValidity;
}
/**
* Returns a copy of this object.
*
* @return a copy of all parameters.
*/
@Override
public BursaWolfParameters clone() {
try {
return (BursaWolfParameters) super.clone();
} catch (CloneNotSupportedException exception) {
// Should not happen, since we are cloneable.
throw new AssertionError(exception);
}
}
/**
* Compares the specified object with this object for equality.
*
* @param object the object to compare with the parameters.
* @return {@code true} if the given object is equal to this {@code BursaWolfParameters}.
*/
@Override
public boolean equals(final Object object) {
if (object != null && object.getClass() == getClass()) {
final BursaWolfParameters that = (BursaWolfParameters) object;
return Arrays.equals(this.getValues(), that.getValues()) &&
Objects.equals(this.targetDatum, that.targetDatum) &&
Objects.equals(this.domainOfValidity, that.domainOfValidity);
}
return false;
}
/**
* Returns a hash value for this object.
*
* @return the hash code value. This value does not need to be the same in past or future versions of this class.
*/
@Override
public int hashCode() {
return Arrays.hashCode(getValues()) ^ (int) serialVersionUID;
}
/**
* Formats this object as a <cite>Well Known Text</cite> {@code ToWGS84[…]} element.
* The WKT contains the parameters in <var>translation</var>, <var>rotation</var>, <var>scale</var> order,
* like below:
*
* <blockquote><code>TOWGS84[{@linkplain #tX}, {@linkplain #tY}, {@linkplain #tZ}, {@linkplain #rX},
* {@linkplain #rY}, {@linkplain #rZ}, {@linkplain #dS}]</code></blockquote>
*
* <div class="note"><b>Compatibility note:</b>
* {@code TOWGS84} is defined in the WKT 1 specification only.</div>
*
* The element name is {@code "ToWGS84"} in the common case where the {@linkplain #getTargetDatum() target datum}
* is WGS 84. For other targets, the element name will be derived from the datum name.
*
* @param formatter The formatter where to format the inner content of this WKT element.
* @return Usually {@code "ToWGS84"}.
*/
@Override
protected String formatTo(final Formatter formatter) {
final double[] values = getValues();
for (final double value : values) {
formatter.append(value);
}
if (isToWGS84()) {
if (values.length > 7) {
formatter.setInvalidWKT(BursaWolfParameters.class, null);
}
return WKTKeywords.ToWGS84;
}
formatter.setInvalidWKT(BursaWolfParameters.class, null);
String name = IdentifiedObjects.getSimpleNameOrIdentifier(getTargetDatum());
if (name == null) {
name = "Unknown";
}
// We may try to build something better here in future SIS versions, if there is a need for that.
return "To" + name;
}
}