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–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 ρ = 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> = ρ sin 37 * <i>azi1</i>; <i>y</i> = ρ 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, ρ 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>) − <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°, when endpoints are a distance <i>r</i> away, and 70 * when their azimuths from the center are ± 45° or ± 71 * 135°. 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–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 + " " + 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 + " " + 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 [−90°, 164 * 90°] and <i>lon0</i> and <i>lon</i> should be in the range 165 * [−540°, 540°). 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> ≤ 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 [−90°, 90°] and 207 * <i>lon0</i> should be in the range [−540°, 540°). 208 * <i>lat</i> will be in the range [−90°, 90°] and <i>lon</i> 209 * will be in the range [−180°, 180°]. 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