View Javadoc
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.utils;
17  
18  import com.irurueta.navigation.geodesic.Geodesic;
19  import com.irurueta.units.Distance;
20  import com.irurueta.units.DistanceUnit;
21  
22  import java.text.DecimalFormat;
23  import java.util.StringTokenizer;
24  
25  /**
26   * Location utility class based on Android's SDK Location class.
27   */
28  public class LocationUtils {
29      /**
30       * Constant used to specify formatting of a latitude or longitude
31       * in the form "[+-]DDD.DDDDD where D indicates degrees.
32       */
33      public static final int FORMAT_DEGREES = 0;
34  
35      /**
36       * Constant used to specify formatting of a latitude or longitude
37       * in the form "[+-]DDD:MM.MMMMM" where D indicates degrees and
38       * M indicates minutes of arc (1 minute = 1/60th of a degree).
39       */
40      public static final int FORMAT_MINUTES = 1;
41  
42      /**
43       * Constant used to specify formatting of a latitude or longitude
44       * in the form "DDD:MM:SS.SSSSS" where D indicates degrees, M
45       * indicates minutes of arc, and S indicates seconds of arc (1
46       * minute = 1/60th of a degree, 1 second = 1/3600th of a degree).
47       */
48      public static final int FORMAT_SECONDS = 2;
49  
50      /**
51       * Constructor.
52       * Prevents public instantiation.
53       */
54      private LocationUtils() {
55      }
56  
57      /**
58       * Converts a coordinate to a String representation. The outputType
59       * may be one of FORMAT_DEGREES, FORMAT_MINUTES, or FORMAT_SECONDS.
60       * The coordinate must be a valid double between -180.0 and 180.0.
61       * This conversion is performed in a method that is dependent on the
62       * default locale, and so is not guaranteed to round-trip with
63       * {@link #convert(String)}.
64       *
65       * @param coordinate coordinate to be converted.
66       * @param outputType output format.
67       * @return converted coordinate.
68       * @throws IllegalArgumentException if coordinate is less than
69       *                                  -180.0, greater than 180.0, or is not a number.
70       * @throws IllegalArgumentException if outputType is not one of
71       *                                  FORMAT_DEGREES, FORMAT_MINUTES, or FORMAT_SECONDS.
72       */
73      public static String convert(double coordinate, final int outputType) {
74          if (coordinate < -180.0 || coordinate > 180.0 || Double.isNaN(coordinate)) {
75              throw new IllegalArgumentException();
76          }
77          if ((outputType != FORMAT_DEGREES) && (outputType != FORMAT_MINUTES) && (outputType != FORMAT_SECONDS)) {
78              throw new IllegalArgumentException();
79          }
80  
81          final var sb = new StringBuilder();
82  
83          // Handle negative values
84          if (coordinate < 0) {
85              sb.append('-');
86              coordinate = -coordinate;
87          }
88  
89          final var df = new DecimalFormat("###.#####");
90          if (outputType == FORMAT_MINUTES || outputType == FORMAT_SECONDS) {
91              final var degrees = (int) Math.floor(coordinate);
92              sb.append(degrees);
93              sb.append(':');
94              coordinate -= degrees;
95              coordinate *= 60.0;
96              if (outputType == FORMAT_SECONDS) {
97                  final var minutes = (int) Math.floor(coordinate);
98                  sb.append(minutes);
99                  sb.append(':');
100                 coordinate -= minutes;
101                 coordinate *= 60.0;
102             }
103         }
104         sb.append(df.format(coordinate));
105         return sb.toString();
106     }
107 
108     /**
109      * Converts a String in one of the formats described by
110      * FORMAT_DEGREES, FORMAT_MINUTES, or FORMAT_SECONDS into a
111      * double. This conversion is performed in a locale agnostic
112      * method, and so is not guaranteed to round-trip with
113      * {@link #convert(double, int)}.
114      *
115      * @param coordinate coordinate to be parsed.
116      * @return parsed value.
117      * @throws NullPointerException     if coordinate is null
118      * @throws IllegalArgumentException if the coordinate is not
119      *                                  in one of the valid formats.
120      */
121     public static double convert(String coordinate) {
122         // IllegalArgumentException if bad syntax
123         if (coordinate == null) {
124             throw new NullPointerException();
125         }
126 
127         var negative = false;
128         if (coordinate.charAt(0) == '-') {
129             coordinate = coordinate.substring(1);
130             negative = true;
131         }
132 
133         final var st = new StringTokenizer(coordinate, ":");
134         final var tokens = st.countTokens();
135         if (tokens < 1) {
136             throw new IllegalArgumentException();
137         }
138         try {
139             final var degrees = st.nextToken();
140             double val;
141             if (tokens == 1) {
142                 val = Double.parseDouble(degrees);
143                 return negative ? -val : val;
144             }
145 
146             final var minutes = st.nextToken();
147             final var deg = Integer.parseInt(degrees);
148             double min;
149             var sec = 0.0;
150             var secPresent = false;
151 
152             if (st.hasMoreTokens()) {
153                 min = Integer.parseInt(minutes);
154                 final var seconds = st.nextToken();
155                 sec = Double.parseDouble(seconds);
156                 secPresent = true;
157             } else {
158                 min = Double.parseDouble(minutes);
159             }
160 
161             final var isNegative180 = negative && (deg == 180) && (min == 0) && (sec == 0);
162 
163             // deg must be in [0, 179] except for the case of -180 degrees
164             if ((deg < 0.0) || (deg > 179 && !isNegative180)) {
165                 throw new IllegalArgumentException();
166             }
167 
168             // min must be in [0, 59] if seconds are present, otherwise [0.0, 60.0)
169             if (min < 0 || min >= 60 || (secPresent && (min > 59))) {
170                 throw new IllegalArgumentException();
171             }
172 
173             // sec must be in [0.0, 60.0)
174             if (sec < 0 || sec >= 60) {
175                 throw new IllegalArgumentException();
176             }
177 
178             val = deg * 3600.0 + min * 60.0 + sec;
179             val /= 3600.0;
180             return negative ? -val : val;
181         } catch (final NumberFormatException nfe) {
182             throw new IllegalArgumentException();
183         }
184     }
185 
186     /**
187      * Computes the approximate distance in meters between two locations, and the initial and final bearings of the
188      * shortest path between them. Distance and bearing are defined using the WGS84 ellipsoid.
189      *
190      * @param startLatitude  the starting latitude.
191      * @param startLongitude the starting longitude.
192      * @param endLatitude    the ending latitude.
193      * @param endLongitude   the ending longitude.
194      * @param results        instance containing results.
195      */
196     public static void distanceAndBearing(
197             final double startLatitude, final double startLongitude, final double endLatitude,
198             final double endLongitude, final BearingDistance results) {
199         //noinspection all
200         final var data = Geodesic.WGS84.inverse(startLatitude, startLongitude, endLatitude, endLongitude);
201         results.startLatitude = data.getLat1();
202         results.startLongitude = data.getLon1();
203         results.endLatitude = data.getLat2();
204         results.endLongitude = data.getLon2();
205         results.distance = data.getS12();
206         results.initialBearing = data.getAzi1();
207         results.finalBearing = data.getAzi2();
208     }
209 
210     /**
211      * Computes the approximate distance in meters between two locations, and the initial and final bearings of the
212      * shortest path between them. Distance and bearing are defined using the WGS84 ellipsoid.
213      *
214      * @param startLatitude  the starting latitude.
215      * @param startLongitude the starting longitude.
216      * @param endLatitude    the ending latitude.
217      * @param endLongitude   the ending longitude.
218      * @return bearing and distance results.
219      */
220     public static BearingDistance distanceAndBearing(
221             final double startLatitude, final double startLongitude, final double endLatitude,
222             final double endLongitude) {
223         final var results = new BearingDistance();
224         distanceAndBearing(startLatitude, startLongitude, endLatitude, endLongitude, results);
225         return results;
226     }
227 
228     /**
229      * Computes the approximate distance in meters between two locations, and the initial and final bearings of the
230      * shortest path between them. Distance and bearing are defined using the WGS84 ellipsoid.
231      *
232      * @param startLatitude  the starting latitude.
233      * @param startLongitude the starting longitude.
234      * @param endLatitude    the ending latitude.
235      * @param endLongitude   the ending longitude.
236      * @param results        array containing results. First element will contain distance. Second element will contain
237      *                       initial bearing (optional). Third element will contain ending bearing (optional).
238      * @throws IllegalArgumentException if results does not have at least 1 element.
239      */
240     public static void distanceAndBearing(
241             final double startLatitude, final double startLongitude, final double endLatitude,
242             final double endLongitude, final double[] results) {
243         if (results.length == 0) {
244             throw new IllegalArgumentException();
245         }
246         //noinspection all
247         final var data = Geodesic.WGS84.inverse(startLatitude, startLongitude, endLatitude, endLongitude);
248         results[0] = data.getS12();
249         if (results.length > 1) {
250             results[1] = data.getAzi1();
251             if (results.length > 2) {
252                 results[2] = data.getAzi2();
253             }
254         }
255     }
256 
257     /**
258      * Computes the approximate distance in meters between two locations.
259      *
260      * @param startLatitude  the starting latitude.
261      * @param startLongitude the starting longitude.
262      * @param endLatitude    the ending latitude.
263      * @param endLongitude   the ending longitude.
264      * @return distance in meters between two locations.
265      */
266     public static double distanceBetweenMeters(
267             final double startLatitude, final double startLongitude, final double endLatitude,
268             final double endLongitude) {
269         //noinspection all
270         return Geodesic.WGS84.inverse(startLatitude, startLongitude, endLatitude, endLongitude).getS12();
271     }
272 
273     /**
274      * Computes the approximate distance between two locations.
275      *
276      * @param startLatitude  the starting latitude.
277      * @param startLongitude the starting longitude.
278      * @param endLatitude    the ending latitude.
279      * @param endLongitude   the ending longitude.
280      * @return distance between two locations.
281      */
282     public static Distance distanceBetween(
283             final double startLatitude, final double startLongitude, final double endLatitude,
284             final double endLongitude) {
285         return new Distance(distanceBetweenMeters(startLatitude, startLongitude, endLatitude, endLongitude),
286                 DistanceUnit.METER);
287     }
288 
289     /**
290      * Computes the approximate distance between two locations.
291      *
292      * @param startLatitude  the starting latitude.
293      * @param startLongitude the starting longitude.
294      * @param endLatitude    the ending latitude.
295      * @param endLongitude   the ending longitude.
296      * @param result         instance where distance between two locations is stored.
297      * @return provided result instance.
298      */
299     public static Distance distanceBetween(
300             final double startLatitude, final double startLongitude, final double endLatitude,
301             final double endLongitude, final Distance result) {
302         result.setValue(distanceBetweenMeters(startLatitude, startLongitude, endLatitude, endLongitude));
303         result.setUnit(DistanceUnit.METER);
304         return result;
305     }
306 
307 
308     /**
309      * Contains distance and bearing.
310      */
311     public static class BearingDistance {
312         /**
313          * Starting latitude (degrees).
314          */
315         private double startLatitude;
316 
317         /**
318          * Starting longitude (degrees).
319          */
320         private double startLongitude;
321 
322         /**
323          * Ending latitude (degrees).
324          */
325         private double endLatitude;
326 
327         /**
328          * Ending longitude (degrees).
329          */
330         private double endLongitude;
331 
332         /**
333          * Distance (meters).
334          */
335         private double distance = 0.0f;
336 
337         /**
338          * Initial bearing (degrees).
339          */
340         private double initialBearing = 0.0f;
341 
342         /**
343          * Final bearing (degrees).
344          */
345         private double finalBearing = 0.0f;
346 
347         /**
348          * Gets starting latitude expressed in degrees.
349          *
350          * @return starting latitude (degrees).
351          */
352         public double getStartLatitude() {
353             return startLatitude;
354         }
355 
356         /**
357          * Gets starting longitude expressed in degrees.
358          *
359          * @return starting longitude (degrees).
360          */
361         public double getStartLongitude() {
362             return startLongitude;
363         }
364 
365         /**
366          * Gets ending latitude expressed in degrees.
367          *
368          * @return ending latitude (degrees).
369          */
370         public double getEndLatitude() {
371             return endLatitude;
372         }
373 
374         /**
375          * Gets ending longitude expressed in degrees.
376          *
377          * @return ending longitude (degrees).
378          */
379         public double getEndLongitude() {
380             return endLongitude;
381         }
382 
383         /**
384          * Gets distance expressed in meters.
385          *
386          * @return distance (meters).
387          */
388         public double getDistanceMeters() {
389             return distance;
390         }
391 
392         /**
393          * Gets distance.
394          *
395          * @return distance.
396          */
397         public Distance getDistance() {
398             return new Distance(distance, DistanceUnit.METER);
399         }
400 
401         /**
402          * Gets distance.
403          *
404          * @param result instance where result value is stored in meters.
405          * @return provided distance instance.
406          */
407         public Distance getDistance(final Distance result) {
408             result.setValue(distance);
409             result.setUnit(DistanceUnit.METER);
410             return result;
411         }
412 
413         /**
414          * Gets initial bearing/azimuth expressed in degrees.
415          *
416          * @return initial bearing/azimuth (degrees).
417          */
418         public double getInitialBearing() {
419             return initialBearing;
420         }
421 
422         /**
423          * Gets final bearing/azimuth expressed in degrees.
424          *
425          * @return final bearing/azimuth (degrees).
426          */
427         public double getFinalBearing() {
428             return finalBearing;
429         }
430     }
431 }