blob: a941896b67de423e2d74cb8cef0bdef87b2b4d25 [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.operation.projection;
import java.util.EnumMap;
import org.opengis.parameter.ParameterDescriptor;
import org.opengis.referencing.operation.Matrix;
import org.opengis.referencing.operation.OperationMethod;
import org.opengis.parameter.InvalidParameterValueException;
import org.apache.sis.internal.referencing.Formulas;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.referencing.operation.matrix.Matrix2;
import org.apache.sis.referencing.operation.matrix.MatrixSIS;
import org.apache.sis.referencing.operation.transform.ContextualParameters;
import org.apache.sis.parameter.Parameters;
import org.apache.sis.measure.Latitude;
import org.apache.sis.util.resources.Errors;
import org.apache.sis.util.Workaround;
import static java.lang.Math.*;
import static org.apache.sis.internal.referencing.provider.SatelliteTracking.*;
/**
* <cite>Satellite-Tracking</cite> projection.
* This projection has been developed in 1977 by Snyder and has no associated EPSG code.
* This projection is neither conformal or equal-area, but has the property that ground tracks
* for satellites orbiting the Earth with the same orbital parameters are shown as straight lines
* on the map. Other properties are (Snyder 1987):
*
* <ul>
* <li>All meridians are equally spaced straight lines.
* They are parallel on cylindrical form and converging to a common point on conical form.</li>
* <li>All parallels are straight but unequally spaced.
* They are parallel on cylindrical form and are concentric circular arcs on conical form.</li>
* <li>Conformality occurs along two chosen parallels. Scale is correct along one of these parameters
* on the conical form and along both on the cylindrical form.</li>
* </ul>
*
* <h2>Limitations</h2>
* This map projection supports only circular orbits. The Earth is assumed spherical.
* Areas close to poles can not be mapped.
*
* <h2>References</h2>
* John P. Snyder., 1987. <u>Map Projections - A Working Manual</u>
* chapter 28: <cite>Satellite-tracking projections</cite>.
*
* @author Matthieu Bastianelli (Geomatys)
* @author Martin Desruisseaux (Geomatys)
* @version 1.1
* @since 1.1
* @module
*/
public class SatelliteTracking extends NormalizedProjection {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = 859940667477896653L;
/**
* Sines and cosines of inclination between the plane of the Earth's Equator and the plane
* of the satellite orbit. The angle variable name is <var>i</var> in Snyder's book.
*
* @see org.apache.sis.internal.referencing.provider.SatelliteTracking#SATELLITE_ORBIT_INCLINATION
*/
private final double cos_i, sin_i, cos2_i;
/**
* Ratio of satellite orbital period (P₂) over ascending node period (P₁).
*
* @see org.apache.sis.internal.referencing.provider.SatelliteTracking#SATELLITE_ORBITAL_PERIOD
* @see org.apache.sis.internal.referencing.provider.SatelliteTracking#ASCENDING_NODE_PERIOD
*/
private final double p2_on_p1;
/**
* Coefficients for the Conic Satellite-Tracking Projection.
* Those values are {@link Double#NaN} in the cylindrical case.
*/
private final double n, s0;
/**
* {@code true} if this projection is conic, or {@code false} if cylindrical or unknown.
*/
private final boolean isConic;
/**
* Work around for RFE #4093999 in Sun's bug database ("Relax constraint on
* placement of this()/super() call in constructors").
*/
@Workaround(library = "JDK", version = "1.8")
static Initializer initializer(final OperationMethod method, final Parameters parameters) {
final EnumMap<NormalizedProjection.ParameterRole, ParameterDescriptor<Double>> roles = new EnumMap<>(NormalizedProjection.ParameterRole.class);
roles.put(NormalizedProjection.ParameterRole.CENTRAL_MERIDIAN, CENTRAL_MERIDIAN);
roles.put(ParameterRole.LATITUDE_OF_CONFORMAL_SPHERE_RADIUS, LATITUDE_OF_ORIGIN);
return new Initializer(method, parameters, roles, (byte) 0);
}
/**
* Creates a Satellite Tracking projection from the given parameters.
*
* @param method description of the projection parameters.
* @param parameters the parameter values of the projection to create.
* @throws InvalidParameterValueException if some parameters have incompatible values.
*/
public SatelliteTracking(final OperationMethod method, final Parameters parameters) {
this(initializer(method, parameters));
}
/**
* Work around for RFE #4093999 in Sun's bug database
* ("Relax constraint on placement of this()/super() call in constructors").
*/
private SatelliteTracking(final Initializer initializer) {
super(initializer);
final double φ0 = toRadians(initializer.getAndStore(LATITUDE_OF_ORIGIN));
final double φ1 = toRadians(initializer.getAndStore(STANDARD_PARALLEL_1));
final double φ2 = toRadians(initializer.getAndStore(STANDARD_PARALLEL_2));
final double i = toRadians(initializer.getAndStore(SATELLITE_ORBIT_INCLINATION));
p2_on_p1 = initializer.getAndStore(SATELLITE_ORBITAL_PERIOD) /
initializer.getAndStore(ASCENDING_NODE_PERIOD);
/*
* Symbols from Snyder used in the constructor:
*
* i = angle of inclination between the plane of Earth Equator and the plane of satellite orbit.
* P₁ = length of Earth's rotation with respect to precessed ascending node.
* P₂ = time required for revolution of the satellite.
* φ₁ = standard parallel.
* φ₂ = second standard parallel. Equals to -φ₁ in the cylindrical case.
* F₁ = angle on the globe between the ground-track and the meridian at latitude φ₁.
* n : used for conic projection only.
* s₀ : user for conic projection only.
*
* Next terms below are common to both cylindrical and conic sattelite tracking projections.
*/
sin_i = sin(i);
cos_i = cos(i);
cos2_i = cos_i * cos_i;
isConic = Math.abs2 + φ1) > ANGULAR_TOLERANCE;
final double cosφ1 = cos1);
final double cos2_φ1 = cosφ1 * cosφ1;
/*
* Some terms will be stored as scale factor and offset applied by matrix before projection
* (normalization) and after projection (denormalization). Those factors will be different
* for cylindric and conic projections.
*/
final MatrixSIS denormalize = context.getMatrix(ContextualParameters.MatrixRole.DENORMALIZATION);
if (isConic) {
final double sinφ1 = sin1);
final double L0 = L(sin0), LATITUDE_OF_ORIGIN);
final double L1 = L(sinφ1, STANDARD_PARALLEL_1);
final double F1 = F(cos2_φ1, STANDARD_PARALLEL_1);
/*
* For conic projection with one standard parallel (φ₁ = φ₂), the general case implemented by
* equation 28-10 become indeterminate. Equation 28-17 below resolves that indetermination.
* If furthermore φ₁ is equal to the upper tracking limit π-i, that equation 28-17 could be
* simplified by equation 28-18:
*
* f = p2_on_p1 * cos_i - 1;
* n = sin_i / (f*f); // Snyder equation 28-18.
*
* However since equation 28-17 still work, we keep it for avoiding discontinuity.
*/
if (abs2 - φ1) < ANGULAR_TOLERANCE) {
n = sinφ1 * (p2_on_p1 * (2*cos2_i - cos2_φ1) - cos_i)
/ (p2_on_p1 * ( cos2_φ1) - cos_i); // Equation 28-17.
} else {
final double cosφ2 = cos2);
final double F2 = F(cosφ2*cosφ2, STANDARD_PARALLEL_2);
final double L2 = L(sin2), STANDARD_PARALLEL_2);
n = (F2 - F1) / (L2 - L1); // Snyder equation 28-10.
}
s0 = F1 - n*L1; // Snyder equation 28-11.
final double ρf = cosφ1 * sin(F1) / n; // Part of Snyder 28-12 fraction.
final double ρ0 = ρf / sin(n*L0 + s0); // Remaining of Snyder 28-12 without R.
if (!Double.isFinitef) || ρf == 0) {
throw invalid(STANDARD_PARALLEL_1);
}
final MatrixSIS normalize = context.getMatrix(ContextualParameters.MatrixRole.NORMALIZATION);
normalize .convertAfter (0, n, null);
denormalize.convertBefore(0, f, null);
denormalize.convertBefore(1, f, ρ0);
} else {
/*
* Cylindrical projection case. The equations are (ignoring R and λ₀):
*
* x = λ⋅cosφ₁
* y = L⋅cosφ₁/F₁′ where L depends on φ but F₁′ is constant.
* F₁′ = tan(F₁)
*
* The cosφ₁ (for x at dimension 0) and cosφ₁/F₁′ (for y at dimension 1) factors are computed
* in advance and stored below. The remaining factor to compute in transform(…) method is L.
*/
n = s0 = Double.NaN;
final double cotF = sqrt(cos2_φ1 - cos2_i) / (p2_on_p1*cos2_φ1 - cos_i); // Cotangente of F₁.
denormalize.convertBefore(0, cosφ1, null);
denormalize.convertBefore(1, cosφ1*cotF, null);
if (!Double.isFinite(cotF) || cotF == 0) {
throw invalid(STANDARD_PARALLEL_1);
}
}
}
/**
* Computes the F₁ or F₂ coefficient using Snyder equation 28-9. Note that this is the same equation
* than F₁′ in above cylindrical case, but with the addition of arc-tangent. This value is constant
* after construction time.
*
* @param cos2_φ square of cosine of φ₁ or φ₂.
* @param source description of φ argument. Used for error message only.
* @return F coefficient for the given φ latitude.
*/
private double F(final double cos2_φ, final ParameterDescriptor<Double> source) {
final double F = atan((p2_on_p1*cos2_φ - cos_i) / sqrt(cos2_φ - cos2_i));
if (Double.isFinite(F)) {
return F;
}
throw invalid(source);
}
/**
* Computes the L₀, L₁ or L₂ coefficient using Snyder equation 28-2a to 28-4a.
* This value is constant after construction time.
*
* @param sinφ sine of φ₀, φ₁ or φ₂.
* @param source description of φ argument. Used for error message only.
* @return L coefficient for the given φ latitude.
*/
private double L(final double sinφ, final ParameterDescriptor<Double> source) {
final double λp = -asin(sinφ / sin_i); // λ′ in Snyder equation 28-2a.
final double L = atan(tanp) * cos_i) - p2_on_p1 * λp; // Snyder equation 28-3a and 28-4a.
if (Double.isFinite(L)) {
return L;
}
throw invalid(source);
}
/**
* Returns an exception for a latitude parameter out of range.
* The range is assumed given by satellite orbit inclination.
*
* @param source description of invalid φ argument.
*/
private InvalidParameterValueException invalid(final ParameterDescriptor<Double> source) {
final String name = source.getName().getCode();
final double value = context.doubleValue(source);
final double limit = abs(context.doubleValue(SATELLITE_ORBIT_INCLINATION));
return new InvalidParameterValueException(Errors.format(Errors.Keys.ValueOutOfRange_4,
name, -limit, limit, new Latitude(value)), name, value);
}
/**
* Converts the specified (λ,φ) coordinate (units in radians) and stores the result in {@code dstPts}.
* In addition, opportunistically computes the projection derivative if {@code derivate} is {@code true}.
* The results must be multiplied by the denormalization matrix before to get linear distances.
*
* <p>The <var>y</var> axis lies along the central meridian λ₀, <var>y</var> increasing northerly, and
* <var>x</var> axis intersects perpendicularly at latitude of origin φ₀, <var>x</var> increasing easterly.</p>
*
* @return the matrix of the projection derivative at the given source position,
* or {@code null} if the {@code derivate} argument is {@code false}.
* @throws ProjectionException if the coordinates can not be converted.
*/
@Override
public Matrix transform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff,
final boolean derivate) throws ProjectionException
{
final double φ = srcPts[srcOff + 1];
final double sinφ_sini = sin(φ) / sin_i;
double λpm = -asin(sinφ_sini); // Initialized to λ′ (Snyder equation 28-2), to be modified later.
final double tanλp = tanpm); // tan(λ′), saved because also used in derivative computation.
final double λt = atan(tanλp * cos_i); // λₜ in Snyder equation 28-3.
/*
* The (x,y) coordinates below are all is needed for the cylindrical case.
* With R and cosφ₁ omitted because multiplied outside this method:
*
* x = λ
* y = L with L = λₜ − (P₂/P₁)λ′ (Snyder equation 28-6)
*
* Only in conic case, we need to continue with some additional computation.
* Note that λpm is NaN if latitude φ is closer to pole than tracking limit.
*/
double x = srcPts[srcOff];
double y = λt - p2_on_p1 * λpm;
if (isConic) {
λpm = n*y + s0; // Use this variable for a new purpose. Needed for derivative.
if ((Double.doubleToRawLongBitspm) ^ Double.doubleToRawLongBits(n)) < 0) {
/*
* if λpm does not have the sign than n, the (x,y) values computed below would suddenly
* change their sign. The y values lower or greater (depending of n sign) than -s0/n can
* not be plotted. Snyder suggests to use another projection if cosmetic output is wanted.
* For now, we just set the results to NaN (meaning "no result", which is not the same than
* TransformException which means that a result exists but can not be computed).
*/
λpm = Double.NaN;
}
final double iρ = sinpm); // Inverse of ρ and without the ρf = cos(φ₁)⋅sin(F₁)/n part.
y = cos(x) / iρ; // x already multiplied by n (was done by normalization matrix).
x = sin(x) / iρ; // Must be last.
}
if (dstPts != null) {
dstPts[dstOff ] = x;
dstPts[dstOff + 1] = y;
}
if (!derivate) {
return null;
}
/*
* Create a derivative matrix initializez with ∂L/∂φ. For cylindrical case where y = L (in this method),
* this is all we need to do. For the conic case, we need to multiply the last column ∂x/∂L and ∂y/∂L.
*/
final Matrix2 d = new Matrix2();
d.m11 = ((cos(φ) / sin_i) / sqrt(1 - sinφ_sini*sinφ_sini))
* (p2_on_p1 - ((1 + tanλp*tanλp)*cos_i/(1 + λtt)));
if (isConic) {
final double dρ_dφ = -n / tanpm);
d.m00 = +y; // ∂x/∂λ
d.m10 = -x; // ∂y/∂λ
d.m01 = x * dρ_dφ * d.m11; // ∂x/∂φ = ∂x/∂L ⋅ ∂L/∂φ
d.m11 *= y * dρ_dφ; // ∂y/∂φ = ∂y/∂L ⋅ ∂L/∂φ
}
return d;
}
/**
* Transforms the specified (<var>x</var>,<var>y</var>) coordinates
* and stores the result in {@code dstPts} (angles in radians).
*
* @throws ProjectionException if the coordinates can not be converted.
*/
@Override
protected void inverseTransform(final double[] srcPts, final int srcOff,
final double[] dstPts, final int dstOff) throws ProjectionException
{
double x = srcPts[srcOff];
double L = srcPts[srcOff + 1]; // Multiplication e.g. by F₁′/(R⋅cosφ₁) already done.
if (isConic) {
final double ρ = copySign(hypot(x,L), n);
x = atan(x / L); // Undefined if x = y = 0.
L = (asin(1 / ρ) - s0) / n; // Equation 28-26 in Snyder with R=1.
}
/*
* Approximation of the latitude associated with L coefficient by applying Newton-Raphson method.
* Equations 28-24, 28-25 and then 28-22 in Snyder's book.
*/
final double ic = 1 / cos2_i;
final double pc = p2_on_p1 * cos_i;
double λp = -PI/2, Δλp;
int iter = Formulas.MAXIMUM_ITERATIONS;
do {
if (--iter < 0) {
throw new ProjectionException(Resources.format(Resources.Keys.NoConvergence));
}
final double A = tan(L + p2_on_p1 * λp) / cos_i; // Snyder equation 28-24.
final double A2 = A*A;
Δλp = (atan(A) - λp) / (1 - pc*((A2 + ic) / (A2 + 1))); // Snyder equation 28-25.
λp += Δλp;
} while (abs(Δλp) >= ANGULAR_TOLERANCE);
dstPts[dstOff ] = x;
dstPts[dstOff+1] = -asin(sinp) * sin_i); // Snyder equation 28-22.
}
}