View Javadoc
1   /*
2    * Copyright (C) 2018 Alberto Irurueta Carro (alberto@irurueta.com)
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.irurueta.navigation.geodesic;
17  
18  /**
19   * Gnomonic projection.
20   * Gnomonic projection centered at an arbitrary position <i>C</i> on the ellipsoid. This projection
21   * is derived in Section 8 of
22   * <ul>
23   *     <li>
24   *         C. F. F. Karney, <a href="https://doi.org/10.1007/s00190-012-0578-z">Algorithms for
25   *         geodesics</a>, J. Geodesy <b>87</b>, 43&ndash;55 (2013);
26   *         DOI: <a href="https://doi.org/10.1007/s00190-012-0578-z">10.1007/s00190-012-0578-z</a>;
27   *     </li>
28   * </ul>
29   * The gnomonic projection of a point <i>P</i> on the ellipsoid is defined as follows: compute the
30   * geodesic line from <i>C</i> to <i>P</i>; compute the reduced length <i>m12</i>, geodesic scale
31   * <i>M12</i>, and &rho; = <i>m12</i>/<i>M12</i>; finally, this gives the coordinates <i>x</i> and
32   * <i>y</i> of <i>P</i> in gnomonic projection with <i>x</i> = &rho; sin <i>azi1</i>; <i>y</i> = &rho;
33   * cos <i>azi1</i>, where <i>azi1</i> is the azimuth of the geodesic at <i>C</i>. The method
34   * {@link Gnomonic#forward(double, double, double, double)} performs the forward projection and
35   * {@link Gnomonic#reverse(double, double, double, double)} is the inverse of the projection. The
36   * methods also return the azimuth <i>azi</i> of the geodesic at <i>P</i> and reciprocal scale
37   * <i>rk</i> in the azimuthal direction. The scale in the radial direction is 1/<i>rk</i><sup>2</sup>.
38   * For a sphere, &rho; reduces to <i>a</i> tan (<i>s12</i>/<i>a</i>), where <i>s12</i> is the length
39   * of the geodesic from <i>C</i> to <i>P</i>, and the gnomonic projection has the property that all
40   * geodesics appear as straight lines. For an ellipsoid, this property holds only for geodesics
41   * interesting the centers. However, geodesic segments close to the center are approximately straight.
42   * Consider a geodesic segment of length <i>1</i>. Let <i>T</i> be the point on the geodesic
43   * (extended if necessary) closest to <i>C</i>, the center of the projection, and <i>t</i>, be the
44   * distance <i>CT</i>. To the lowest order, the maximum deviation (as a true distance) of the corresponding
45   * gnomonic line segment (i.e., with the same end points) from the geodesic is
46   * (<i>K</i>(<i>T</i> &minus; <i>K</i>(<i>C</i>))
47   * <i>l</i><sup>2</sup> <i>t</i> / 32.
48   * where <i>K</i> is the Gaussian curvature.
49   * This result applies for any surface. For an ellipsoid of revolution, consider all geodesics whose end
50   * points are within a distance <i>r</i> of <i>C</i>. For a given <i>r</i>, the deviation is maximum
51   * when the latitude of <i>C</i> is 46&deg;, when endpoints are a distance <i>r</i> away, and when
52   * their azimuths from the center are &plusmn; 45&deg; or &plusmn; 135&deg;. To the lowest order in
53   * <i>r</i> and the flattening <i>f</i>, the deviation is <i>f</i> (<i>r</i>/2<i>a</i>)<sup>3</sup>
54   * <i>r</i>.
55   * <b>CAUTION:</b> The definition of this projection for a sphere is standard. However, there is no
56   * standard for how it should be extended to an ellipsoid.
57   * The choices are:
58   * <ul>
59   *     <li>
60   *         Declare that the projection is undefined for an ellipsoid.
61   *     </li>
62   *     <li>
63   *         Project to a tangent plane from the center of the ellipsoid. This causes great ellipses to
64   *         appear as straight lines in the projection; i.e., it generalizes the spherical great circle
65   *         to a great ellipse. This was proposed independently by Bowring and Williams in 1997.
66   *     </li>
67   *     <li>
68   *         Project to the conformal sphere with the constant of integration chosen so that the values
69   *         of the latitude match for the center point and perform a central projection onto the plane
70   *         tangent to the conformal sphere at the center point. This causes normal section through the
71   *         center point to appear as straight lines in the projection; i.e., it generalizes the
72   *         spherical great circle to a normal section. This was proposed by I. G. Letoval'tsev,
73   *         Generalization of the gnomonic projection for a spheroid and the principal geodetic
74   *         problems involved in the alignment of surface routes, Geodesy and Aerophotography(5),
75   *         271&ndash;275 (1963)
76   *     </li>
77   *     <li>
78   *         The projection given here. This causes geodesics close to the center point to appear as
79   *         straight lines in the projection; i.e., it generalizes the spherical great circle to a
80   *         geodesic.
81   *     </li>
82   * </ul>
83   * Example of use:
84   * <pre>
85   * // Example of using the Gnomonic.java class
86   * import com.irurueta.navigation.geodesic.Geodesic;
87   * import com.irurueta.navigation.geodesic.Gnomonic;
88   * import com.irurueta.navigation.geodesic.GnomonicData;
89   * public class ExampleGnomonic {
90   *   public static void main(String[] args) {
91   *     Geodesic geod = Geodesic.WGS84;
92   *     double lat0 = 48 + 50 / 60.0, lon0 = 2 + 20 / 60.0; // Paris
93   *     Gnomonic gnom = new Gnomonic(geod);
94   *     {
95   *       // Sample forward calculation
96   *       double lat = 50.9, lon = 1.8; // Calais
97   *       GnomonicData proj = gnom.Forward(lat0, lon0, lat, lon);
98   *       System.out.println(proj.x + &quot; &quot; + proj.y);
99   *     }
100  *     {
101  *       // Sample reverse calculation
102  *       double x = -38e3, y = 230e3;
103  *       GnomonicData proj = gnom.Reverse(lat0, lon0, x, y);
104  *       System.out.println(proj.lat + &quot; &quot; + proj.lon);
105  *     }
106  *   }
107  * }
108  * </pre>
109  */
110 public class Gnomonic {
111     private static final double EPS = 0.01 * Math.sqrt(GeoMath.EPSILON);
112     private static final int NUMIT = 10;
113 
114     /**
115      * Earth geodesic.
116      */
117     private final Geodesic earth;
118 
119     /**
120      * Major equatorial Earth radius.
121      */
122     private final double a;
123 
124     /**
125      * Earth flattening.
126      */
127     private final double f;
128 
129     /**
130      * Constructor for Gnomonic.
131      *
132      * @param earth the {@link Geodesic} object to use for geodesic calculations.
133      */
134     public Gnomonic(final Geodesic earth) {
135         this.earth = earth;
136         a = this.earth.getMajorRadius();
137         f = this.earth.getFlattening();
138     }
139 
140     /**
141      * Forward projection, from geographic to gnomonic.
142      * <i>lat0</i> and <i>lat</i> should be in the range [&minus;90&deg;, 90&deg;] and <i>lon0</i>
143      * and <i>lon</i> should be in the range [&minus;540&deg;, 540&deg;). The scale of the projection
144      * is 1/<i>rk<sup>2</sup></i> in the "radial" direction, <i>azi</i> clockwise from true north,
145      * and is 1/<i>rk</i> in the direction perpendicular to this. If the point lies "over the
146      * horizon", i.e., if <i>rk</i> &le; 0, then NaNs are returned for <i>x</i> and <i>y</i> (the
147      * correct values are returned for <i>azi</i> and <i>rk</i>). A call to forward followed by a
148      * call to reverse will return the original (<i>lat</i>, <i>lon</i>) (to within roundoff)
149      * provided the point in not over the horizon.
150      *
151      * @param lat0 latitude of center point of projection (degrees).
152      * @param lon0 longitude of center point of projection (degrees).
153      * @param lat  latitude of point (degrees).
154      * @param lon  longitude of point (degrees).
155      * @return {@link GnomonicData} object with the following fields:
156      * <i>lat0</i>, <i>lon0</i>, <i>lat</i>, <i>lon</i>, <i>x</i>, <i>y</i>, <i>azi</i>, <i>rk</i>.
157      */
158     public GnomonicData forward(final double lat0, final double lon0, final double lat, final double lon) {
159         final var inv = earth.inverse(lat0, lon0, lat, lon,
160                 GeodesicMask.AZIMUTH | GeodesicMask.GEODESIC_SCALE | GeodesicMask.REDUCED_LENGTH);
161         final var fwd = new GnomonicData(lat0, lon0, lat, lon, Double.NaN, Double.NaN, inv.getAzi2(),
162                 inv.getScaleM12());
163 
164         if (inv.getScaleM12() > 0) {
165             final var rho = inv.getM12() / inv.getScaleM12();
166             final var p = GeoMath.sincosd(inv.getAzi1());
167             fwd.setX(rho * p.getFirst());
168             fwd.setY(rho * p.getSecond());
169         }
170 
171         return fwd;
172     }
173 
174     /**
175      * Reverse projection, from gnomonic to geographic.
176      * <i>lat</i> will be in the range [&minus;90&deg;, 90&deg;] and <i>lon</i> will be in the
177      * range (&minus;180&deg;, 180&deg;]. The scale of the projection is 1/<i>rk<sup>2</sup></i>
178      * in the "radial" direction, <i>azi</i> clockwise from true north, and is 1/<i>rk</i> in the
179      * direction perpendicular to this. Even though all inputs should return a valid <i>lat</i>
180      * and <i>lon</i>, it's possible that the procedure fails to converge for very large <i>x</i>
181      * or <i>y</i>; in this case NaNs are returned for very large <i>x</i> or <i>y</i>; in this
182      * case NaNs are returned for all the output arguments. A call to reverse followed by a call
183      * to forward will return the original (<i>x</i>, <i>y</i>) (to round-off).
184      *
185      * @param lat0 latitude of center point of projection (degrees). <i>lat0</i> should be in the
186      *             range [&minus;90&deg;, 90&deg;]
187      * @param lon0 longitude of center point of projection (degrees). <i>lon0</i> should be in the
188      *             range [&minus;540&deg;, 540&deg;).
189      * @param x    easting of point (meters).
190      * @param y    northing of point (meters).
191      * @return {@link GnomonicData} object with the following fields:
192      * <i>lat0</i>, <i>lon0</i>, <i>lat</i>, <i>lon</i>, <i>x</i>, <i>y</i>,
193      * <i>azi</i>, <i>rk</i>.
194      */
195     public GnomonicData reverse(final double lat0, final double lon0, final double x, final double y) {
196         final var rev = new GnomonicData(lat0, lon0, Double.NaN, Double.NaN, x, y, Double.NaN, Double.NaN);
197 
198         //noinspection all
199         final var azi0 = GeoMath.atan2d(x, y);
200         var rho = Math.hypot(x, y);
201         var s = a * Math.atan(rho / a);
202         final var little = rho <= a;
203 
204         if (!little) {
205             rho = 1 / rho;
206         }
207 
208         final var line = earth.line(lat0, lon0, azi0, GeodesicMask.LATITUDE | GeodesicMask.LONGITUDE
209                 | GeodesicMask.AZIMUTH | GeodesicMask.DISTANCE_IN | GeodesicMask.REDUCED_LENGTH
210                 | GeodesicMask.GEODESIC_SCALE);
211 
212         var count = NUMIT;
213         var trip = 0;
214         GeodesicData pos = null;
215 
216         while (count-- > 0) {
217             pos = line.position(s, GeodesicMask.LONGITUDE | GeodesicMask.LATITUDE | GeodesicMask.AZIMUTH
218                     | GeodesicMask.DISTANCE_IN | GeodesicMask.REDUCED_LENGTH | GeodesicMask.GEODESIC_SCALE);
219 
220             if (trip > 0) {
221                 break;
222             }
223 
224             final var ds = little
225                     ? ((pos.getM12() / pos.getScaleM12()) - rho) * pos.getScaleM12() * pos.getScaleM12()
226                     : (rho - (pos.getScaleM12() / pos.getM12())) * pos.getM12() * pos.getM12();
227             s -= ds;
228 
229             if (Math.abs(ds) <= EPS * a) {
230                 trip++;
231             }
232         }
233 
234         if (trip == 0) {
235             return rev;
236         }
237 
238         rev.setLat(pos.getLat2());
239         rev.setLon(pos.getLon2());
240         rev.setAzi(pos.getAzi2());
241         rev.setRk(pos.getScaleM12());
242 
243         return rev;
244     }
245 
246     /**
247      * Gets the equatorial radius of the ellipsoid (meters). This is the value inherited from the
248      * Geodesic object used in the constructor.
249      *
250      * @return <i>a</i> the equatorial radius of the ellipsoid (meters).
251      */
252     public double getMajorRadius() {
253         return a;
254     }
255 
256     /**
257      * Gets the flattening of the ellipsoid.
258      *
259      * @return <i>f</i> the flattening of the ellipsoid. This is the value inherited from the
260      * Geodesic object used in the constructor.
261      */
262     public double getFlattening() {
263         return f;
264     }
265 }