1 // Copyright 2005 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS-IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 // Original author: ericv@google.com (Eric Veach) 16 // Converted to D: madric@gmail.com (Vijay Nayar) 17 18 module s2.s2latlng; 19 20 import s2.logger; 21 import s2.r2point; 22 import s2.s2point; 23 import s2.s1angle; 24 import s2.util.math.vector; 25 import algorithm = std.algorithm; 26 import conv = std.conv; 27 import format = std.format; 28 import math = std.math; 29 30 // This class represents a point on the unit sphere as a pair 31 // of latitude-longitude coordinates. Like the rest of the "geometry" 32 // package, the intent is to represent spherical geometry as a mathematical 33 // abstraction, so functions that are specifically related to the Earth's 34 // geometry (e.g. easting/northing conversions) should be put elsewhere. 35 // 36 // This class is intended to be copied by value as desired. It uses 37 // the default copy constructor and assignment operator. 38 struct S2LatLng { 39 private: 40 // The default constructor sets the latitude and longitude to zero. This is 41 // mainly useful when declaring arrays, STL containers, etc. 42 R2Point _coords; 43 44 // Internal constructor. 45 this(in R2Point coords) { 46 _coords = coords; 47 } 48 49 // This is internal to avoid ambiguity about which units are expected. 50 this(double latRadians, double lngRadians) { 51 _coords = R2Point([latRadians, lngRadians]); 52 } 53 54 55 public: 56 // Constructor. The latitude and longitude are allowed to be outside 57 // the is_valid() range. However, note that most methods that accept 58 // S2LatLngs expect them to be normalized (see normalized() below). 59 this(in S1Angle lat, in S1Angle lng) { 60 _coords = R2Point([lat.radians(), lng.radians()]); 61 } 62 63 // Convert a direction vector (not necessarily unit length) to an S2LatLng. 64 this(in S2Point p) 65 out { 66 // The latitude and longitude are already normalized. 67 assert(isValid(), "Invalid S2LatLng in constructor: " ~ this.toString()); 68 } do { 69 _coords = R2Point([latitude(p).radians(), longitude(p).radians]); 70 } 71 72 // Returns an S2LatLng for which isValid() will return false. 73 static S2LatLng invalid() { 74 // These coordinates are outside the bounds allowed by is_valid(). 75 return S2LatLng(M_PI, 2 * M_PI); 76 } 77 78 // Convenience functions -- shorter than calling S1Angle::Radians(), etc. 79 static S2LatLng fromRadians(double latRadians, double lngRadians) { 80 return S2LatLng(latRadians, lngRadians); 81 } 82 83 static S2LatLng fromDegrees(double latDegrees, double lngDegrees) { 84 return S2LatLng(S1Angle.fromDegrees(latDegrees), S1Angle.fromDegrees(lngDegrees)); 85 } 86 87 static S2LatLng fromE5(int latE5, int lngE5) { 88 return S2LatLng(S1Angle.fromE5(latE5), S1Angle.fromE5(lngE5)); 89 } 90 91 static S2LatLng fromE6(int latE6, int lngE6) { 92 return S2LatLng(S1Angle.fromE6(latE6), S1Angle.fromE6(lngE6)); 93 } 94 95 static S2LatLng fromE7(int latE7, int lngE7) { 96 return S2LatLng(S1Angle.fromE7(latE7), S1Angle.fromE7(lngE7)); 97 } 98 99 // Convenience functions -- to use when args have been fixed32s in protos. 100 // 101 // The arguments are cast into int, so very large unsigned values 102 // are treated as negative numbers. 103 static S2LatLng fromUnsignedE6(uint latE6, uint lngE6) { 104 return S2LatLng(S1Angle.fromUnsignedE6(latE6), S1Angle.fromUnsignedE6(lngE6)); 105 } 106 107 static S2LatLng fromUnsignedE7(uint latE7, uint lngE7) { 108 return S2LatLng(S1Angle.fromUnsignedE7(latE7), S1Angle.fromUnsignedE7(lngE7)); 109 } 110 111 // Methods to compute the latitude and longitude of a point separately. 112 static S1Angle latitude(in S2Point p) { 113 // We use atan2 rather than asin because the input vector is not necessarily 114 // unit length, and atan2 is much more accurate than asin near the poles. 115 return S1Angle.fromRadians(math.atan2(p[2], math.sqrt(p[0]*p[0] + p[1]*p[1]))); 116 } 117 118 static S1Angle longitude(in S2Point p) { 119 // Note that atan2(0, 0) is defined to be zero. 120 return S1Angle.fromRadians(math.atan2(p[1], p[0])); 121 } 122 123 // Accessor methods. 124 S1Angle lat() const { 125 return S1Angle.fromRadians(_coords[0]); 126 } 127 128 S1Angle lng() const { 129 return S1Angle.fromRadians(_coords[1]); 130 } 131 132 R2Point coords() const { 133 return _coords; 134 } 135 136 // Return true if the latitude is between -90 and 90 degrees inclusive 137 // and the longitude is between -180 and 180 degrees inclusive. 138 bool isValid() const { 139 return math.fabs(lat().radians()) <= M_PI_2 && math.fabs(lng().radians()) <= M_PI; 140 } 141 142 // Clamps the latitude to the range [-90, 90] degrees, and adds or subtracts 143 // a multiple of 360 degrees to the longitude if necessary to reduce it to 144 // the range [-180, 180]. 145 S2LatLng normalized() const { 146 // remainder(x, 2 * M_PI) reduces its argument to the range [-M_PI, M_PI] 147 // inclusive, which is what we want here. 148 return S2LatLng( 149 algorithm.max(-M_PI_2, algorithm.min(M_PI_2, lat().radians())), 150 math.remainder(lng().radians(), 2 * M_PI)); 151 } 152 153 // Converts a normalized S2LatLng to the equivalent unit-length vector. 154 // The maximum error in the result is 1.5 * DBL_EPSILON. (This does not 155 // include the error of converting degrees, E5, E6, or E7 to radians.) 156 S2Point toS2Point() const { 157 if (!isValid()) logger.logError("Invalid S2LatLng in S2LatLng.toPoint() : ", this); 158 double phi = lat().radians(); 159 double theta = lng().radians(); 160 double cosphi = math.cos(phi); 161 return S2Point([math.cos(theta) * cosphi, math.sin(theta) * cosphi, math.sin(phi)]); 162 } 163 164 // Returns the distance (measured along the surface of the sphere) to the 165 // given S2LatLng, implemented using the Haversine formula. This is 166 // equivalent to 167 // 168 // S1Angle(ToPoint(), o.ToPoint()) 169 // 170 // except that this function is slightly faster, and is also somewhat less 171 // accurate for distances approaching 180 degrees (see s1angle.h for 172 // details). Both S2LatLngs must be normalized. 173 S1Angle getDistance(in S2LatLng o) const 174 in { 175 assert(isValid(), "Invalid S2LatLng in S2LatLng.getDistance: " ~ toString()); 176 assert(o.isValid(), "Invalid S2LatLng in S2LatLng.getDistance: " ~ o.toString()); 177 } do { 178 // This implements the Haversine formula, which is numerically stable for 179 // small distances but only gets about 8 digits of precision for very large 180 // distances (e.g. antipodal points). Note that 8 digits is still accurate 181 // to within about 10cm for a sphere the size of the Earth. 182 // 183 // This could be fixed with another sin() and cos() below, but at that point 184 // you might as well just convert both arguments to S2Points and compute the 185 // distance that way (which gives about 15 digits of accuracy for all 186 // distances). 187 188 double lat1 = lat().radians(); 189 double lat2 = o.lat().radians(); 190 double lng1 = lng().radians(); 191 double lng2 = o.lng().radians(); 192 double dlat = math.sin(0.5 * (lat2 - lat1)); 193 double dlng = math.sin(0.5 * (lng2 - lng1)); 194 double x = dlat * dlat + dlng * dlng * math.cos(lat1) * math.cos(lat2); 195 return S1Angle.fromRadians(2 * math.asin(math.sqrt(algorithm.min(1.0, x)))); 196 } 197 198 // Simple arithmetic operations for manipulating latitude-longitude pairs. 199 // The results are not normalized (see Normalized()). 200 S2LatLng opBinary(string op)(in S2LatLng v) { 201 static if (op == "+" || op == "-") { 202 return mixin("S2LatLng(coords() " ~ op ~ " v.coords())"); 203 } else { 204 static assert(false, "Unsupporeted operator: " ~ op); 205 } 206 } 207 208 S2LatLng opBinary(string op)(double m) const 209 if (op == "*" || op == "/") { 210 return mixin("S2LatLng(_coords " ~ op ~ " m)"); 211 } 212 213 S2LatLng opBinaryRight(string op)(double m) const 214 if (op == "*" || op == "/") { 215 return mixin("S2LatLng(m " ~ op ~ " _coords)"); 216 } 217 218 bool opEquals(in S2LatLng o) const { 219 return _coords == o._coords; 220 } 221 222 bool opCmp(in S2LatLng o) const { 223 if (_coords == o._coords) { 224 return 0; 225 } 226 return _coords > o._coords; 227 } 228 229 bool approxEquals(in S2LatLng o, S1Angle maxError = S1Angle.fromRadians(1e-15)) const { 230 return _coords.aequal(o._coords, maxError.radians()); 231 } 232 233 string toString() const { 234 return "[" ~ conv.to!string(lat()) ~ ", " ~ conv.to!string(lng()) ~ "]"; 235 } 236 237 // Exports the latitude and longitude in degrees, separated by a comma. 238 // e.g. "94.518000,150.300000" 239 string toStringInDegrees() const { 240 auto pt = normalized(); 241 return format.format("%f,%f", pt.lat().degrees(), pt.lng().degrees()); 242 } 243 }