| /* |
| * 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; |
| } |
| } |