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 }