View Javadoc
1   package au.gov.amsa.util.navigation;
2   
3   import static java.lang.Math.PI;
4   import static java.lang.Math.abs;
5   import static java.lang.Math.acos;
6   import static java.lang.Math.asin;
7   import static java.lang.Math.atan;
8   import static java.lang.Math.atan2;
9   import static java.lang.Math.cos;
10  import static java.lang.Math.floor;
11  import static java.lang.Math.round;
12  import static java.lang.Math.signum;
13  import static java.lang.Math.sin;
14  import static java.lang.Math.sqrt;
15  import static java.lang.Math.toRadians;
16  
17  import java.awt.Polygon;
18  import java.text.DecimalFormat;
19  import java.util.ArrayList;
20  import java.util.List;
21  
22  import org.apache.commons.math3.util.FastMath;
23  
24  /**
25   * Can use commons-math3 FastMath for most trig functions exception for
26   * atan,atan2 (see <a
27   * href="https://issues.apache.org/jira/browse/MATH-740">here</a>).
28   * 
29   * @author dxm
30   * 
31   */
32  public class Position {
33  	private final double lat;
34  	private final double lon;
35  	private final double alt;
36  	private static final double radiusEarthKm = 6371.01;
37  	private static final double circumferenceEarthKm = 2.0 * PI * radiusEarthKm;
38  
39  	/**
40  	 * @param lat
41  	 *            in degrees
42  	 * @param lon
43  	 *            in degrees
44  	 */
45  	public Position(double lat, double lon) {
46  		this.lat = lat;
47  		this.lon = lon;
48  		this.alt = 0.0;
49  	}
50  
51  	/**
52  	 * @param lat
53  	 *            in degrees
54  	 * @param lon
55  	 *            in degrees
56  	 * @param alt
57  	 *            in metres
58  	 */
59  	public Position(double lat, double lon, double alt) {
60  		this.lat = lat;
61  		this.lon = lon;
62  		this.alt = alt;
63  	}
64  
65  	public static Position create(double lat, double lon) {
66  		return new Position(lat, lon);
67  	}
68  
69  	public static Position create(double lat, double lon, double alt) {
70  		return new Position(lat, lon, alt);
71  	}
72  
73  	public final double getLat() {
74  		return lat;
75  	}
76  
77  	public final double getLon() {
78  		return lon;
79  	}
80  
81  	public final double getAlt() {
82  		return alt;
83  	}
84  
85  	@Override
86  	public final String toString() {
87  		return "[" + lat + "," + lon + "]";
88  	}
89  
90  	/**
91  	 * Predicts position travelling along a great circle arc based on the
92  	 * Haversine formula.
93  	 * 
94  	 * From http://www.movable-type.co.uk/scripts/latlong.html
95  	 * 
96  	 * @param distanceKm
97  	 * @param courseDegrees
98  	 * @return
99  	 */
100 	public final Position predict(double distanceKm, double courseDegrees) {
101 		assertWithMsg(alt == 0.0, "Predictions only valid for Earth's surface");
102 		double dr = distanceKm / radiusEarthKm;
103 		double latR = toRadians(lat);
104 		double lonR = toRadians(lon);
105 		double courseR = toRadians(courseDegrees);
106 		double lat2Radians = asin(sin(latR) * cos(dr) + cos(latR) * sin(dr)
107 				* cos(courseR));
108 		double lon2Radians = atan2(sin(courseR) * sin(dr) * cos(latR), cos(dr)
109 				- sin(latR) * sin(lat2Radians));
110 		double lon3Radians = mod(lonR + lon2Radians + PI, 2 * PI) - PI;
111 		return new Position(FastMath.toDegrees(lat2Radians),
112 				FastMath.toDegrees(lon3Radians));
113 	}
114 
115 	public static double toDegrees(double degrees, double minutes,
116 			double seconds) {
117 		return degrees + minutes / 60.0 + seconds / 3600.0;
118 	}
119 
120 	/**
121 	 * From http://williams.best.vwh.net/avform.htm (Latitude of point on GC).
122 	 * 
123 	 * @param position
124 	 * @param longitudeDegrees
125 	 * @return
126 	 */
127 	public Double getLatitudeOnGreatCircle(Position position,
128 			double longitudeDegrees) {
129 		double lonR = toRadians(longitudeDegrees);
130 		double lat1R = toRadians(lat);
131 		double lon1R = toRadians(lon);
132 		double lat2R = toRadians(position.getLat());
133 		double lon2R = toRadians(position.getLon());
134 
135 		double sinDiffLon1RLon2R = sin(lon1R - lon2R);
136 		if (abs(sinDiffLon1RLon2R) < 0.00000001) {
137 			return null;
138 		} else {
139 			double cosLat1R = cos(lat1R);
140 			double cosLat2R = cos(lat2R);
141 			double numerator = sin(lat1R) * cosLat2R * sin(lonR - lon2R)
142 					- sin(lat2R) * cosLat1R * sin(lonR - lon1R);
143 			double denominator = cosLat1R * cosLat2R * sinDiffLon1RLon2R;
144 			double radians = atan(numerator / denominator);
145 			return FastMath.toDegrees(radians);
146 		}
147 	}
148 
149 	public static class LongitudePair {
150 		private final double lon1, lon2;
151 
152 		public LongitudePair(double lon1, double lon2) {
153 			this.lon1 = lon1;
154 			this.lon2 = lon2;
155 		}
156 
157 		public double getLon1() {
158 			return lon1;
159 		}
160 
161 		public double getLon2() {
162 			return lon2;
163 		}
164 
165 		@Override
166 		public String toString() {
167 			return "LongitudePair [lon1=" + lon1 + ", lon2=" + lon2 + "]";
168 		}
169 
170 	}
171 
172 	/**
173 	 * Returns null if no crossing of latitude otherwise return two longitude
174 	 * candidates. From http://williams.best.vwh.net/avform.htm (Crossing
175 	 * parallels).
176 	 * 
177 	 * @param position
178 	 * @param latitudeDegrees
179 	 * @return
180 	 */
181 	// TODO add unit test
182 	public LongitudePair getLongitudeOnGreatCircle(Position position,
183 			double latitudeDegrees) {
184 		double lat3 = toRadians(latitudeDegrees);
185 		double lat1 = toRadians(lat);
186 		double lon1 = toRadians(lon);
187 		double lat2 = toRadians(position.getLat());
188 		double lon2 = toRadians(position.getLon());
189 		double l12 = lon1 - lon2;
190 		double sinLat1 = sin(lat1);
191 		double cosLat2 = cos(lat2);
192 		double cosLat3 = cos(lat3);
193 		double cosLat1 = cos(lat1);
194 		double sinL12 = sin(l12);
195 		double A = sinLat1 * cosLat2 * cosLat3 * sinL12;
196 		double B = sinLat1 * cosLat2 * cosLat3 * cos(l12) - cosLat1 * sin(lat2)
197 				* cosLat3;
198 		double C = cosLat1 * cosLat2 * sin(lat3) * sinL12;
199 		double longitude = atan2(B, A);
200 		double v = sqrt(sqr(A) + sqr(B));
201 		if (abs(C) >= v) {
202 			// not found!
203 			return null;
204 		} else {
205 			double dlon = acos(C / v);
206 			double lonCandidate1Degrees = to180(FastMath.toDegrees(lon1 + dlon
207 					+ longitude));
208 			double lonCandidate2Degrees = to180(FastMath.toDegrees(lon1 - dlon
209 					+ longitude));
210 			return new LongitudePair(lonCandidate1Degrees, lonCandidate2Degrees);
211 		}
212 	}
213 
214 	private double sqr(double d) {
215 		return d * d;
216 	}
217 
218 	/**
219 	 * Return an array of Positions representing the earths limb (aka: horizon)
220 	 * as viewed from this Position in space. This position must have altitude >
221 	 * 0
222 	 * 
223 	 * The array returned will have the specified number of elements (radials).
224 	 * 
225 	 * 
226 	 * This method is useful for the calculation of satellite footprints or the
227 	 * position of the Earth's day/night terminator.
228 	 * 
229 	 * 
230 	 * This formula from Aviation Formula by Ed Williams
231 	 * (http://williams.best.vwh.net/avform.htm)
232 	 * 
233 	 * @param radials
234 	 *            the number of radials to calculated (evenly spaced around the
235 	 *            circumference of the circle
236 	 * 
237 	 * @return An array of radial points a fixed distance from this point
238 	 *         representing the Earth's limb as viewed from this point in space.
239 	 * 
240 	 */
241 	public final Position[] getEarthLimb(int radials) {
242 
243 		Position[] result = new Position[radials];
244 
245 		double radialDegrees = 0.0;
246 		double incDegrees = 360.0 / radials;
247 		double quarterEarthKm = circumferenceEarthKm / 4.0;
248 		Position surfacePosition = new Position(this.lat, this.lon, 0.0);
249 
250 		// Assert( this.alt>0.0, "getEarthLimb() requires Position a positive
251 		// altitude");
252 		for (int i = 0; i < radials; i++) {
253 
254 			// TODO: base the distance on the altitude above the Earth
255 
256 			result[i] = surfacePosition.predict(quarterEarthKm, radialDegrees);
257 			radialDegrees += incDegrees;
258 		}
259 
260 		return result;
261 	}
262 
263 	/**
264 	 * returns distance between two WGS84 positions according to Vincenty's
265 	 * formula from Wikipedia
266 	 * 
267 	 * @param position
268 	 * @return
269 	 */
270 	public final double getDistanceToKm(Position position) {
271 		double lat1 = toRadians(lat);
272 		double lat2 = toRadians(position.lat);
273 		double lon1 = toRadians(lon);
274 		double lon2 = toRadians(position.lon);
275 		double deltaLon = lon2 - lon1;
276 		double cosLat2 = cos(lat2);
277 		double cosLat1 = cos(lat1);
278 		double sinLat1 = sin(lat1);
279 		double sinLat2 = sin(lat2);
280 		double cosDeltaLon = cos(deltaLon);
281 		double top = sqrt(sqr(cosLat2 * sin(deltaLon))
282 				+ sqr(cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosDeltaLon));
283 		double bottom = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosDeltaLon;
284 		double distance = radiusEarthKm * atan2(top, bottom);
285 		return abs(distance);
286 	}
287 
288 	/**
289 	 * Returns a great circle bearing in degrees in the range 0 to 360.
290 	 * 
291 	 * @param position
292 	 * @return
293 	 */
294 	public final double getBearingDegrees(Position position) {
295 		double lat1 = toRadians(lat);
296 		double lat2 = toRadians(position.lat);
297 		double lon1 = toRadians(lon);
298 		double lon2 = toRadians(position.lon);
299 		double dLon = lon2 - lon1;
300 		double sinDLon = sin(dLon);
301 		double cosLat2 = cos(lat2);
302 		double y = sinDLon * cosLat2;
303 		double x = cos(lat1) * sin(lat2) - sin(lat1) * cosLat2 * cos(dLon);
304 		double course = FastMath.toDegrees(atan2(y, x));
305 		if (course < 0)
306 			course += 360;
307 		return course;
308 	}
309 
310 	/**
311 	 * returns difference in degrees in the range -180 to 180
312 	 * 
313 	 * @param bearing1
314 	 *            degrees between -360 and 360
315 	 * @param bearing2
316 	 *            degrees between -360 and 360
317 	 * @return
318 	 */
319 	public static double getBearingDifferenceDegrees(double bearing1,
320 			double bearing2) {
321 		if (bearing1 < 0)
322 			bearing1 += 360;
323 		if (bearing2 > 180)
324 			bearing2 -= 360;
325 		double result = bearing1 - bearing2;
326 		if (result > 180)
327 			result -= 360;
328 		return result;
329 	}
330 
331 	/**
332 	 * calculates the distance of a point to the great circle path between p1
333 	 * and p2.
334 	 * 
335 	 * Formula from: http://www.movable-type.co.uk/scripts/latlong.html
336 	 * 
337 	 * @param p1
338 	 * @param p2
339 	 * @return
340 	 */
341 	public final double getDistanceKmToPath(Position p1, Position p2) {
342 		double d = radiusEarthKm
343 				* asin(sin(getDistanceToKm(p1) / radiusEarthKm)
344 						* sin(toRadians(getBearingDegrees(p1)
345 								- p1.getBearingDegrees(p2))));
346 		return abs(d);
347 	}
348 
349 	public static String toDegreesMinutesDecimalMinutesLatitude(double lat) {
350 		long degrees = round(signum(lat) * floor(abs(lat)));
351 		double remaining = abs(lat - degrees);
352 		remaining *= 60;
353 		String result = abs(degrees) + "" + (char) 0x00B0
354 				+ new DecimalFormat("00.00").format(remaining) + "'"
355 				+ (lat < 0 ? "S" : "N");
356 		return result;
357 	}
358 
359 	public static String toDegreesMinutesDecimalMinutesLongitude(double lon) {
360 		long degrees = round(signum(lon) * floor(abs(lon)));
361 		double remaining = abs(lon - degrees);
362 		remaining *= 60;
363 		String result = abs(degrees) + "" + (char) 0x00B0
364 				+ new DecimalFormat("00.00").format(remaining) + "'"
365 				+ (lon < 0 ? "W" : "E");
366 		return result;
367 	}
368 
369 	private static double mod(double y, double x) {
370 
371 		x = abs(x);
372 		int n = (int) (y / x);
373 		double mod = y - x * n;
374 		if (mod < 0) {
375 			mod += x;
376 		}
377 		return mod;
378 	}
379 
380 	public static void assertWithMsg(boolean assertion, String msg) {
381 		if (!assertion)
382 			throw new RuntimeException("Assertion failed: " + msg);
383 
384 	}
385 
386 	/**
387 	 * Returns a position along a path according to the proportion value
388 	 * 
389 	 * @param position
390 	 * @param proportion
391 	 *            is between 0 and 1 inclusive
392 	 * @return
393 	 */
394 
395 	public final Position getPositionAlongPath(Position position,
396 			double proportion) {
397 
398 		if (proportion >= 0 && proportion <= 1) {
399 
400 			// Get bearing degrees for course
401 			double courseDegrees = this.getBearingDegrees(position);
402 
403 			// Get distance from position arg and this objects location
404 			double distanceKm = this.getDistanceToKm(position);
405 
406 			// Predict the position for a proportion of the course
407 			// where this object is the start position and the arg
408 			// is the destination position.
409 			Position retPosition = this.predict(proportion * distanceKm,
410 					courseDegrees);
411 
412 			return retPosition;
413 		} else
414 			throw new RuntimeException(
415 					"Proportion must be between 0 and 1 inclusive");
416 	}
417 
418 	public final List<Position> getPositionsAlongPath(Position position,
419 			double maxSegmentLengthKm) {
420 
421 		// Get distance from this to position
422 		double distanceKm = this.getDistanceToKm(position);
423 
424 		List<Position> positions = new ArrayList<Position>();
425 
426 		long numSegments = round(floor(distanceKm / maxSegmentLengthKm)) + 1;
427 		positions.add(this);
428 		for (int i = 1; i < numSegments; i++)
429 			positions.add(getPositionAlongPath(position, i
430 					/ (double) numSegments));
431 		positions.add(position);
432 		return positions;
433 	}
434 
435 	public final Position to360() {
436 		double lat = this.lat;
437 		double lon = this.lon;
438 		if (lon < 0)
439 			lon += 360;
440 		return new Position(lat, lon);
441 	}
442 
443 	/**
444 	 * normalize the lat lon values of this to ensure that no large longitude
445 	 * jumps are made from lastPosition (e.g. 179 to -180)
446 	 * 
447 	 * @param lastPosition
448 	 */
449 	public final Position ensureContinuous(Position lastPosition) {
450 		double lon = this.lon;
451 		if (abs(lon - lastPosition.lon) > 180) {
452 			if (lastPosition.lon < 0)
453 				lon -= 360;
454 			else
455 				lon += 360;
456 			return new Position(lat, lon);
457 		} else
458 			return this;
459 
460 	}
461 
462 	public final boolean isWithin(List<Position> positions) {
463 		Polygon polygon = new Polygon();
464 		for (Position p : positions) {
465 			polygon.addPoint(degreesToArbitraryInteger(p.lon),
466 					degreesToArbitraryInteger(p.lat));
467 		}
468 		int x = degreesToArbitraryInteger(this.lon);
469 		int y = degreesToArbitraryInteger(this.lat);
470 		return polygon.contains(x, y);
471 	}
472 
473 	private int degreesToArbitraryInteger(double d) {
474 		return (int) round(d * 3600);
475 	}
476 
477 	@Override
478 	public final boolean equals(Object o) {
479 		if (o == null)
480 			return false;
481 		else if (o instanceof Position) {
482 			Position p = (Position) o;
483 			return p.lat == lat && p.lon == lon;
484 		} else
485 			return false;
486 	}
487 
488 	@Override
489 	public final int hashCode() {
490 		return (int) (lat + lon);
491 	}
492 
493 	public final double getDistanceToPathKm(List<Position> positions) {
494 		if (positions.size() == 0)
495 			throw new RuntimeException("positions must not be empty");
496 		else if (positions.size() == 1)
497 			return this.getDistanceToKm(positions.get(0));
498 		else {
499 			Double distance = null;
500 			for (int i = 0; i < positions.size() - 1; i++) {
501 				double d = getDistanceToSegmentKm(positions.get(i),
502 						positions.get(i + 1));
503 				if (distance == null || d < distance)
504 					distance = d;
505 			}
506 			return distance;
507 		}
508 	}
509 
510 	public final double getDistanceToSegmentKm(Position p1, Position p2) {
511 		return getDistanceToKm(getClosestIntersectionWithSegment(p1, p2));
512 	}
513 
514 	public final Position getClosestIntersectionWithSegment(Position p1,
515 			Position p2) {
516 		if (p1.equals(p2))
517 			return p1;
518 		double d = getDistanceToKm(p1);
519 		double bearing1 = p1.getBearingDegrees(this);
520 		double bearing2 = p1.getBearingDegrees(p2);
521 		double bearingDiff = bearing1 - bearing2;
522 		double proportion = d * cos(toRadians(bearingDiff))
523 				/ p1.getDistanceToKm(p2);
524 		if (proportion < 0 || proportion > 1) {
525 			if (d < getDistanceToKm(p2))
526 				return p1;
527 			else
528 				return p2;
529 		} else
530 			return p1.getPositionAlongPath(p2, proportion);
531 	}
532 
533 	/**
534 	 * @param path
535 	 * @param minDistanceKm
536 	 * @return
537 	 */
538 	public boolean isOutside(List<Position> path, double minDistanceKm) {
539 		if (isWithin(path))
540 			return false;
541 		else {
542 			double distance = getDistanceToPathKm(path);
543 			return distance >= minDistanceKm;
544 		}
545 	}
546 
547 	/**
548 	 * Returns the difference between two longitude values. The returned value
549 	 * is always >=0.
550 	 * 
551 	 * @param a
552 	 * @param b
553 	 * @return
554 	 */
555 	public static double longitudeDiff(double a, double b) {
556 		a = to180(a);
557 		b = to180(b);
558 		if (a < b)
559 			return a - b + 360;
560 		else
561 			return a - b;
562 	}
563 
564 	/**
565 	 * Converts an angle in degrees to range -180< x <= 180.
566 	 * 
567 	 * @param d
568 	 * @return
569 	 */
570 	public static double to180(double d) {
571 		if (d < 0)
572 			return -to180(abs(d));
573 		else {
574 			if (d > 180) {
575 				long n = round(floor((d + 180) / 360.0));
576 				return d - n * 360;
577 			} else
578 				return d;
579 		}
580 	}
581 
582 	public Position normalizeLongitude() {
583 		return new Position(lat, to180(lon), alt);
584 	}
585 }