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–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 ρ = <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> = ρ sin <i>azi1</i>; <i>y</i> = ρ
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, ρ 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> − <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°, when endpoints are a distance <i>r</i> away, and when
52 * their azimuths from the center are ± 45° or ± 135°. 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–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 + " " + 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 + " " + 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 [−90°, 90°] and <i>lon0</i>
143 * and <i>lon</i> should be in the range [−540°, 540°). 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> ≤ 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 [−90°, 90°] and <i>lon</i> will be in the
177 * range (−180°, 180°]. 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 [−90°, 90°]
187 * @param lon0 longitude of center point of projection (degrees). <i>lon0</i> should be in the
188 * range [−540°, 540°).
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 }