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 }