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 }