1 /**
2  * Implementation of the net.sf.geographiclib.Gnomonic class
3  *
4  * Copyright (c) BMW Car IT GmbH (2014-2019) <sebastian.mattheis@bmw-carit.de>
5  * and Charles Karney (2020) <charles@karney.com>∑ and licensed and licensed
6  * under the MIT/X11 License. For more information, see
7  * https://geographiclib.sourceforge.io/
8  **********************************************************************/
9 package net.sf.geographiclib;
10 
11 /**
12  * Gnomonic projection.
13  * <p>
14  * <i>Note: Gnomonic.java has been ported to Java from its C++ equivalent
15  * Gnomonic.cpp, authored by C. F. F. Karney and licensed under MIT/X11
16  * license.  The following documentation is mostly the same as for its C++
17  * equivalent, but has been adopted to apply to this Java implementation.</i>
18  * <p>
19  * Gnomonic projection centered at an arbitrary position <i>C</i> on the
20  * ellipsoid. This projection is derived in Section 8 of
21  * <ul>
22  * <li>
23  * C. F. F. Karney, <a href="https://doi.org/10.1007/s00190-012-0578-z">
24  * Algorithms for geodesics</a>, J. Geodesy <b>87</b>, 43&ndash;55 (2013);
25  * DOI: <a href="https://doi.org/10.1007/s00190-012-0578-z">
26  * 10.1007/s00190-012-0578-z</a>; addenda:
27  * <a href="https://geographiclib.sourceforge.io/geod-addenda.html">
28  * geod-addenda.html</a>.
29  * </li>
30  * </ul>
31  * <p>
32  * The gnomonic projection of a point <i>P</i> on the ellipsoid is defined as
33  * follows: compute the geodesic line from <i>C</i> to <i>P</i>; compute the
34  * reduced length <i>m12</i>, geodesic scale <i>M12</i>, and &rho; =
35  * <i>m12</i>/<i>M12</i>; finally, this gives the coordinates <i>x</i> and
36  * <i>y</i> of <i>P</i> in gnomonic projection with <i>x</i> = &rho; sin
37  * <i>azi1</i>; <i>y</i> = &rho; cos <i>azi1</i>, where <i>azi1</i> is the
38  * azimuth of the geodesic at <i>C</i>. The method
39  * {@link Gnomonic#Forward(double, double, double, double)} performs the
40  * forward projection and
41  * {@link Gnomonic#Reverse(double, double, double, double)} is the
42  * inverse of the projection. The methods also return the azimuth
43  * <i>azi</i> of the geodesic at <i>P</i> and reciprocal scale
44  * <i>rk</i> in the azimuthal direction. The scale in the radial
45  * direction is 1/<i>rk</i><sup>2</sup>.
46  * <p>
47  * For a sphere, &rho; reduces to <i>a</i> tan(<i>s12</i>/<i>a</i>), where
48  * <i>s12</i> is the length of the geodesic from <i>C</i> to <i>P</i>, and the
49  * gnomonic projection has the property that all geodesics appear as straight
50  * lines. For an ellipsoid, this property holds only for geodesics interesting
51  * the centers. However geodesic segments close to the center are approximately
52  * straight.
53  * <p>
54  * Consider a geodesic segment of length <i>l</i>. Let <i>T</i> be the point on
55  * the geodesic (extended if necessary) closest to <i>C</i>, the center of the
56  * projection, and <i>t</i>, be the distance <i>CT</i>. To lowest order, the
57  * maximum deviation (as a true distance) of the corresponding gnomonic line
58  * segment (i.e., with the same end points) from the geodesic is<br>
59  * <br>
60  * (<i>K</i>(<i>T</i>) &minus; <i>K</i>(<i>C</i>))
61  * <i>l</i><sup>2</sup> <i>t</i> / 32.
62  * <br>
63  * <br>
64  * where <i>K</i> is the Gaussian curvature.
65  * <p>
66  * This result applies for any surface. For an ellipsoid of revolution,
67  * consider all geodesics whose end points are within a distance <i>r</i> of
68  * <i>C</i>.  For a given <i>r</i>, the deviation is maximum when the latitude
69  * of <i>C</i> is 45&deg;, when endpoints are a distance <i>r</i> away, and
70  * when their azimuths from the center are &plusmn; 45&deg; or &plusmn;
71  * 135&deg;. To lowest order in <i>r</i> and the flattening <i>f</i>, the
72  * deviation is <i>f</i> (<i>r</i>/2<i>a</i>)<sup>3</sup> <i>r</i>.
73  * <p>
74  * <b>CAUTION:</b> The definition of this projection for a sphere is standard.
75  * However, there is no standard for how it should be extended to an ellipsoid.
76  * The choices are:
77  * <ul>
78  * <li>
79  * Declare that the projection is undefined for an ellipsoid.
80  * </li>
81  * <li>
82  * Project to a tangent plane from the center of the ellipsoid. This causes
83  * great ellipses to appear as straight lines in the projection; i.e., it
84  * generalizes the spherical great circle to a great ellipse. This was proposed
85  * by independently by Bowring and Williams in 1997.
86  * </li>
87  * <li>
88  * Project to the conformal sphere with the constant of integration chosen so
89  * that the values of the latitude match for the center point and perform a
90  * central projection onto the plane tangent to the conformal sphere at the
91  * center point. This causes normal sections through the center point to appear
92  * as straight lines in the projection; i.e., it generalizes the spherical
93  * great circle to a normal section. This was proposed by I. G. Letoval'tsev,
94  * Generalization of the gnomonic projection for a spheroid and the principal
95  * geodetic problems involved in the alignment of surface routes, Geodesy and
96  * Aerophotography (5), 271&ndash;274 (1963).
97  * </li>
98  * <li>
99  * The projection given here. This causes geodesics close to the center point
100  * to appear as straight lines in the projection; i.e., it generalizes the
101  * spherical great circle to a geodesic.
102  * </li>
103  * </ul>
104  * <p>
105  * Example of use:
106  *
107  * <pre>
108  * // Example of using the Gnomonic.java class
109  * import net.sf.geographiclib.Geodesic;
110  * import net.sf.geographiclib.Gnomonic;
111  * import net.sf.geographiclib.GnomonicData;
112  * public class ExampleGnomonic {
113  *   public static void main(String[] args) {
114  *     Geodesic geod = Geodesic.WGS84;
115  *     double lat0 = 48 + 50 / 60.0, lon0 = 2 + 20 / 60.0; // Paris
116  *     Gnomonic gnom = new Gnomonic(geod);
117  *     {
118  *       // Sample forward calculation
119  *       double lat = 50.9, lon = 1.8; // Calais
120  *       GnomonicData proj = gnom.Forward(lat0, lon0, lat, lon);
121  *       System.out.println(proj.x + &quot; &quot; + proj.y);
122  *     }
123  *     {
124  *       // Sample reverse calculation
125  *       double x = -38e3, y = 230e3;
126  *       GnomonicData proj = gnom.Reverse(lat0, lon0, x, y);
127  *       System.out.println(proj.lat + &quot; &quot; + proj.lon);
128  *     }
129  *   }
130  * }
131  * </pre>
132  */
133 
134 public class Gnomonic {
135   private static final double eps_ = 0.01 * Math.sqrt(Math.ulp(1.0));
136   private static final int numit_ = 10;
137   private Geodesic _earth;
138   private double _a, _f;
139 
140   /**
141    * Constructor for Gnomonic.
142    * <p>
143    * @param earth the {@link Geodesic} object to use for geodesic
144    *   calculations.
145    */
Gnomonic(Geodesic earth)146   public Gnomonic(Geodesic earth) {
147     _earth = earth;
148     _a = _earth.EquatorialRadius();
149     _f = _earth.Flattening();
150   }
151 
152   /**
153    * Forward projection, from geographic to gnomonic.
154    * <p>
155    * @param lat0 latitude of center point of projection (degrees).
156    * @param lon0 longitude of center point of projection (degrees).
157    * @param lat latitude of point (degrees).
158    * @param lon longitude of point (degrees).
159    * @return {@link GnomonicData} object with the following fields:
160    *   <i>lat0</i>, <i>lon0</i>, <i>lat</i>, <i>lon</i>, <i>x</i>, <i>y</i>,
161    *   <i>azi</i>, <i>rk</i>.
162    * <p>
163    * <i>lat0</i> and <i>lat</i> should be in the range [&minus;90&deg;,
164    * 90&deg;] and <i>lon0</i> and <i>lon</i> should be in the range
165    * [&minus;540&deg;, 540&deg;). The scale of the projection is
166    * 1/<i>rk<sup>2</sup></i> in the "radial" direction, <i>azi</i> clockwise
167    * from true north, and is 1/<i>rk</i> in the direction perpendicular to
168    * this. If the point lies "over the horizon", i.e., if <i>rk</i> &le; 0,
169    * then NaNs are returned for <i>x</i> and <i>y</i> (the correct values are
170    * returned for <i>azi</i> and <i>rk</i>). A call to Forward followed by a
171    * call to Reverse will return the original (<i>lat</i>, <i>lon</i>) (to
172    * within roundoff) provided the point in not over the horizon.
173    */
Forward(double lat0, double lon0, double lat, double lon)174   public GnomonicData Forward(double lat0, double lon0, double lat, double lon)
175   {
176     GeodesicData inv =
177       _earth.Inverse(lat0, lon0, lat, lon,
178                      GeodesicMask.AZIMUTH | GeodesicMask.GEODESICSCALE |
179                      GeodesicMask.REDUCEDLENGTH);
180     GnomonicData fwd =
181       new GnomonicData(lat0, lon0, lat, lon, Double.NaN, Double.NaN,
182                        inv.azi2, inv.M12);
183 
184     if (inv.M12 > 0) {
185       double rho = inv.m12 / inv.M12;
186       Pair p = new Pair();
187       GeoMath.sincosd(p, inv.azi1);
188       fwd.x = rho * p.first;
189       fwd.y = rho * p.second;
190     }
191 
192     return fwd;
193   }
194 
195   /**
196    * Reverse projection, from gnomonic to geographic.
197    * <p>
198    * @param lat0 latitude of center point of projection (degrees).
199    * @param lon0 longitude of center point of projection (degrees).
200    * @param x easting of point (meters).
201    * @param y northing of point (meters).
202    * @return {@link GnomonicData} object with the following fields:
203    *   <i>lat0</i>, <i>lon0</i>, <i>lat</i>, <i>lon</i>, <i>x</i>, <i>y</i>,
204    *   <i>azi</i>, <i>rk</i>.
205    * <p>
206    * <i>lat0</i> should be in the range [&minus;90&deg;, 90&deg;] and
207    * <i>lon0</i> should be in the range [&minus;540&deg;, 540&deg;).
208    * <i>lat</i> will be in the range [&minus;90&deg;, 90&deg;] and <i>lon</i>
209    * will be in the range [&minus;180&deg;, 180&deg;]. The scale of the
210    * projection is 1/<i>rk<sup>2</sup></i> in the "radial" direction,
211    * <i>azi</i> clockwise from true north, and is 1/<i>rk</i> in the direction
212    * perpendicular to this. Even though all inputs should return a valid
213    * <i>lat</i> and <i>lon</i>, it's possible that the procedure fails to
214    * converge for very large <i>x</i> or <i>y</i>; in this case NaNs are
215    * returned for all the output arguments. A call to Reverse followed by a
216    * call to Forward will return the original (<i>x</i>, <i>y</i>) (to
217    * roundoff).
218    */
Reverse(double lat0, double lon0, double x, double y)219   public GnomonicData Reverse(double lat0, double lon0, double x, double y) {
220     GnomonicData rev =
221       new GnomonicData(lat0, lon0, Double.NaN, Double.NaN, x, y, Double.NaN,
222                        Double.NaN);
223 
224     double azi0 = GeoMath.atan2d(x, y);
225     double rho = Math.hypot(x, y);
226     double s = _a * Math.atan(rho / _a);
227     boolean little = rho <= _a;
228 
229     if (!little)
230       rho = 1 / rho;
231 
232     GeodesicLine line =
233       _earth.Line(lat0, lon0, azi0, GeodesicMask.LATITUDE
234                   | GeodesicMask.LONGITUDE | GeodesicMask.AZIMUTH
235                   | GeodesicMask.DISTANCE_IN | GeodesicMask.REDUCEDLENGTH
236                   | GeodesicMask.GEODESICSCALE);
237 
238     int count = numit_, trip = 0;
239     GeodesicData pos = null;
240 
241     while (count-- > 0) {
242       pos =
243         line.Position(s, GeodesicMask.LONGITUDE | GeodesicMask.LATITUDE
244                       | GeodesicMask.AZIMUTH | GeodesicMask.DISTANCE_IN
245                       | GeodesicMask.REDUCEDLENGTH
246                       | GeodesicMask.GEODESICSCALE);
247 
248       if (trip > 0)
249         break;
250 
251       double ds =
252         little ? ((pos.m12 / pos.M12) - rho) * pos.M12 * pos.M12
253         : (rho - (pos.M12 / pos.m12)) * pos.m12 * pos.m12;
254       s -= ds;
255 
256       if (Math.abs(ds) <= eps_ * _a)
257         trip++;
258     }
259 
260     if (trip == 0)
261       return rev;
262 
263     rev.lat = pos.lat2;
264     rev.lon = pos.lon2;
265     rev.azi = pos.azi2;
266     rev.rk = pos.M12;
267 
268     return rev;
269   }
270 
271   /**
272    * @return <i>a</i> the equatorial radius of the ellipsoid (meters).  This is
273    *   the value inherited from the Geodesic object used in the constructor.
274    **********************************************************************/
EquatorialRadius()275   public double EquatorialRadius() { return _a; }
276 
277   /**
278    * @return <i>f</i> the  flattening of the ellipsoid.  This is
279    *   the value inherited from the Geodesic object used in the constructor.
280    **********************************************************************/
Flattening()281   public double Flattening() { return _f; }
282 
283   /**
284    * @deprecated An old name for {@link #EquatorialRadius()}.
285    * @return <i>a</i> the equatorial radius of the ellipsoid (meters).
286    **********************************************************************/
287   @Deprecated
MajorRadius()288   public double MajorRadius() { return EquatorialRadius(); }
289 }
290