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
26
27
28
29
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
41
42
43
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
53
54
55
56
57
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
92
93
94
95
96
97
98
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
122
123
124
125
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
174
175
176
177
178
179
180
181
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
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
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
251
252 for (int i = 0; i < radials; i++) {
253
254
255
256 result[i] = surfacePosition.predict(quarterEarthKm, radialDegrees);
257 radialDegrees += incDegrees;
258 }
259
260 return result;
261 }
262
263
264
265
266
267
268
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
290
291
292
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
312
313
314
315
316
317
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
333
334
335
336
337
338
339
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
388
389
390
391
392
393
394
395 public final Position getPositionAlongPath(Position position,
396 double proportion) {
397
398 if (proportion >= 0 && proportion <= 1) {
399
400
401 double courseDegrees = this.getBearingDegrees(position);
402
403
404 double distanceKm = this.getDistanceToKm(position);
405
406
407
408
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
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
445
446
447
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
535
536
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
549
550
551
552
553
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
566
567
568
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 }