| /* |
| * 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.abs(φ2 + φ1) > ANGULAR_TOLERANCE; |
| final double cosφ1 = cos(φ1); |
| 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 = sin(φ1); |
| final double L0 = L(sin(φ0), 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 (abs(φ2 - φ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 = cos(φ2); |
| final double F2 = F(cosφ2*cosφ2, STANDARD_PARALLEL_2); |
| final double L2 = L(sin(φ2), 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.isFinite(ρf) || ρ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(tan(λp) * 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 = tan(λpm); // 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.doubleToRawLongBits(λpm) ^ 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ρ = sin(λpm); // 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 + λt*λt))); |
| if (isConic) { |
| final double dρ_dφ = -n / tan(λpm); |
| 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(sin(λp) * sin_i); // Snyder equation 28-22. |
| } |
| } |