1 package au.gov.amsa.navigation;
2
3 import static com.google.common.base.Optional.of;
4 import static java.lang.Math.toRadians;
5
6 import java.util.concurrent.atomic.AtomicLong;
7
8 import com.github.davidmoten.grumpy.core.Position;
9 import com.google.common.base.Optional;
10 import com.google.common.base.Preconditions;
11
12 import au.gov.amsa.risky.format.AisClass;
13 import au.gov.amsa.risky.format.Fix;
14 import au.gov.amsa.risky.format.HasFix;
15
16 public class VesselPosition implements HasFix {
17
18 public static boolean validate = true;
19
20 public enum NavigationalStatus {
21
22
23 UNDER_WAY_USING_ENGINE, AT_ANCHOR, NOT_UNDER_COMMAND, RESTRICTED_MANOEUVRABILITY, CONSTRAINED_BY_HER_DRAUGHT, MOORED, AGROUND, ENGAGED_IN_FISHING, UNDER_WAY, RESERVED_1, RESERVED_2, FUTURE_1, FUTURE_2, FUTURE_3, AIS_SART, NOT_DEFINED;
24 }
25
26 private static final double EARTH_RADIUS_KM = 6378.1;
27 private static final int maxDimensionMetresWhenUnknown = 30;
28 private final double lat;
29 private final double lon;
30 private final Optional<Integer> lengthMetres;
31 private final Optional<Integer> widthMetres;
32 private final Optional<Double> cogDegrees;
33 private final Optional<Double> headingDegrees;
34 private final Optional<Double> speedMetresPerSecond;
35 private final Optional<String> positionAisNmea;
36 private final Optional<String> shipStaticAisNmea;
37 private final NavigationalStatus navigationalStatus;
38 private final long time;
39 private final Identifier id;
40 private final VesselClass cls;
41 private final Optional<Integer> shipType;
42
43 private static AtomicLong counter = new AtomicLong();
44 private final long messageId;
45 private final Optional<?> data;
46
47 private VesselPosition(long messageId, Identifier id, double lat, double lon,
48 Optional<Integer> lengthMetres, Optional<Integer> widthMetres, Optional<Double> cog,
49 Optional<Double> heading, Optional<Double> speedMetresPerSecond, VesselClass cls,
50 NavigationalStatus navigationalStatus, long time, Optional<Integer> shipType,
51 Optional<String> positionAisNmea, Optional<String> shipStaticAisNmea,
52 Optional<?> data) {
53
54 if (validate) {
55 Preconditions.checkArgument(lat >= -90 && lat <= 90);
56 Preconditions.checkArgument(lon >= -180 && lon <= 180);
57 Preconditions.checkNotNull(id);
58 Preconditions.checkNotNull(lengthMetres);
59 Preconditions.checkNotNull(widthMetres);
60 Preconditions.checkNotNull(shipType);
61 Preconditions.checkNotNull(positionAisNmea);
62 Preconditions.checkNotNull(shipStaticAisNmea);
63 Preconditions.checkNotNull(navigationalStatus);
64 }
65
66 this.messageId = messageId;
67 this.cls = cls;
68 this.id = id;
69 this.lat = lat;
70 this.lon = lon;
71 this.lengthMetres = lengthMetres;
72 this.widthMetres = widthMetres;
73 this.cogDegrees = cog;
74 this.headingDegrees = heading;
75 this.speedMetresPerSecond = speedMetresPerSecond;
76 this.time = time;
77 this.navigationalStatus = navigationalStatus;
78 this.shipType = shipType;
79 this.positionAisNmea = positionAisNmea;
80 this.shipStaticAisNmea = shipStaticAisNmea;
81 this.data = data;
82 }
83
84 public long messageId() {
85 return messageId;
86 }
87
88 public Identifier id() {
89 return id;
90 }
91
92 public double lat() {
93 return lat;
94 }
95
96 public double lon() {
97 return lon;
98 }
99
100 public Optional<?> data() {
101 return data;
102 }
103
104 public Optional<Integer> lengthMetres() {
105 return lengthMetres;
106 }
107
108 public Optional<Integer> widthMetres() {
109 return widthMetres;
110 }
111
112 public Optional<Integer> maxDimensionMetres() {
113 if (lengthMetres.isPresent() && widthMetres.isPresent())
114 return Optional.of(Math.max(lengthMetres.get(), widthMetres.get()));
115 else
116 return Optional.absent();
117 }
118
119 public Optional<Double> cogDegrees() {
120 return cogDegrees;
121 }
122
123 public Optional<Double> headingDegrees() {
124 return headingDegrees;
125 }
126
127 public Optional<Double> speedMetresPerSecond() {
128 return speedMetresPerSecond;
129 }
130
131 public Optional<Double> speedKnots() {
132 return speedMetresPerSecond.transform(x -> x / 0.5144444);
133 }
134
135 public VesselClass cls() {
136 return cls;
137 }
138
139 public long time() {
140 return time;
141 }
142
143 public NavigationalStatus navigationalStatus() {
144 return navigationalStatus;
145 }
146
147 public Optional<Integer> shipType() {
148 return shipType;
149 }
150
151 public Optional<String> positionAisNmea() {
152 return positionAisNmea;
153 }
154
155 public Optional<String> shipStaticAisNmea() {
156 return shipStaticAisNmea;
157 }
158
159 public static Builder builder() {
160 return new Builder();
161 }
162
163 public static class Builder {
164
165 private Identifier id;
166 private double lat;
167 private double lon;
168 private Optional<Integer> lengthMetres = Optional.absent();
169 private Optional<Integer> widthMetres = Optional.absent();
170
171
172 private Optional<Double> cogDegrees;
173 private Optional<Double> headingDegrees;
174 private Optional<Double> speedMetresPerSecond;
175 private Optional<String> positionAisNmea;
176 private Optional<String> shipStaticAisNmea;
177 private VesselClass cls;
178 private long time;
179 private Optional<Integer> shipType = Optional.absent();
180 private NavigationalStatus navigationalStatus;
181 private Optional<?> data;
182
183 private Builder() {
184 }
185
186 public Builder id(Identifier id) {
187 this.id = id;
188 return this;
189 }
190
191 public Builder lat(double lat) {
192 this.lat = lat;
193 return this;
194 }
195
196 public Builder lon(double lon) {
197 this.lon = lon;
198 return this;
199 }
200
201 public Builder lengthMetres(Optional<Integer> lengthMetres) {
202 this.lengthMetres = lengthMetres;
203 return this;
204 }
205
206 public Builder widthMetres(Optional<Integer> widthMetres) {
207 this.widthMetres = widthMetres;
208 return this;
209 }
210
211 public Builder cogDegrees(Optional<Double> cog) {
212 this.cogDegrees = cog;
213 return this;
214 }
215
216 public Builder headingDegrees(Optional<Double> heading) {
217 this.headingDegrees = heading;
218 return this;
219 }
220
221 public Builder speedMetresPerSecond(Optional<Double> speedMetresPerSecond) {
222 this.speedMetresPerSecond = speedMetresPerSecond;
223 return this;
224 }
225
226 public Builder time(long time) {
227 this.time = time;
228 return this;
229 }
230
231 public Builder cls(VesselClass cls) {
232 this.cls = cls;
233 return this;
234 }
235
236 public Builder shipType(Optional<Integer> shipType) {
237 this.shipType = shipType;
238 return this;
239 }
240
241 public Builder positionAisNmea(Optional<String> nmea) {
242 this.positionAisNmea = nmea;
243 return this;
244 }
245
246 public Builder shipStaticAisNmea(Optional<String> nmea) {
247 this.shipStaticAisNmea = nmea;
248 return this;
249 }
250
251 public Builder navigationalStatus(NavigationalStatus status) {
252 this.navigationalStatus = status;
253 return this;
254 }
255
256 public Builder data(Optional<?> data) {
257 this.data = data;
258 return this;
259 }
260
261 public VesselPosition build() {
262 return new VesselPosition(counter.incrementAndGet(), id, lat, lon, lengthMetres,
263 widthMetres, cogDegrees, headingDegrees, speedMetresPerSecond, cls,
264 navigationalStatus, time, shipType, positionAisNmea, shipStaticAisNmea, data);
265 }
266
267 }
268
269 private double metresPerDegreeLongitude() {
270 return Math.PI / 180 * EARTH_RADIUS_KM * Math.cos(toRadians(lat));
271 }
272
273 private double metresPerDegreeLatitude() {
274 return 111321.543;
275 }
276
277
278
279
280
281
282
283
284
285
286 public Vector position(VesselPosition relativeTo) {
287
288 double xMetres = (lon - relativeTo.lon()) * relativeTo.metresPerDegreeLongitude();
289 double yMetres = (lat - relativeTo.lat()) * relativeTo.metresPerDegreeLatitude();
290 return new Vector(xMetres, yMetres);
291 }
292
293
294
295
296
297
298
299 public Optional<VesselPosition> predict(long t) {
300 if (!speedMetresPerSecond.isPresent() || !cogDegrees.isPresent()
301 || navigationalStatus == NavigationalStatus.AT_ANCHOR
302 || navigationalStatus == NavigationalStatus.MOORED)
303 return Optional.absent();
304 else {
305 double lat = this.lat - speedMetresPerSecond.get() / metresPerDegreeLatitude()
306 * (t - time) / 1000.0 * Math.cos(Math.toRadians(cogDegrees.get()));
307 if (lat > 90)
308 lat = 90;
309 else if (lat < -90)
310 lat = -90;
311 double lon = Position
312 .to180(this.lon + speedMetresPerSecond.get() / metresPerDegreeLongitude()
313 * (t - time) / 1000.0 * Math.sin(Math.toRadians(cogDegrees.get())));
314
315 return Optional.of(new VesselPosition(messageId, id, lat, lon, lengthMetres,
316 widthMetres, cogDegrees, headingDegrees, speedMetresPerSecond, cls,
317 navigationalStatus, time, shipType, positionAisNmea, shipStaticAisNmea, data));
318 }
319 }
320
321 private Optional<Vector> velocity() {
322 if (speedMetresPerSecond.isPresent() && cogDegrees.isPresent())
323 return Optional.of(new Vector(
324 speedMetresPerSecond.get() * Math.sin(Math.toRadians(cogDegrees.get())),
325 speedMetresPerSecond.get() * Math.cos(Math.toRadians(cogDegrees.get()))));
326 else
327 return Optional.absent();
328 }
329
330
331
332
333
334
335
336
337 public Optional<Times> intersectionTimes(VesselPosition vp) {
338
339
340
341
342 Optional<VesselPosition> p = vp.predict(time);
343 if (!p.isPresent()) {
344 return Optional.absent();
345 }
346 Vector deltaV = velocity().get().minus(p.get().velocity().get());
347 Vector deltaP = position(this).minus(p.get().position(this));
348
349
350
351 double r = p.get().maxDimensionMetres().or(maxDimensionMetresWhenUnknown) / 2
352 + maxDimensionMetres().or(maxDimensionMetresWhenUnknown) / 2;
353
354 if (deltaP.dot(deltaP) <= r)
355 return of(new Times(p.get().time()));
356
357 double a = deltaV.dot(deltaV);
358 double b = 2 * deltaV.dot(deltaP);
359 double c = deltaP.dot(deltaP) - r * r;
360
361
362 double discriminant = b * b - 4 * a * c;
363
364 if (a == 0)
365 return Optional.absent();
366 else if (discriminant < 0)
367 return Optional.absent();
368 else {
369 if (discriminant == 0) {
370 return of(new Times(Math.round(-b / 2 / a)));
371 } else {
372 long alpha1 = Math.round((-b + Math.sqrt(discriminant)) / 2 / a);
373 long alpha2 = Math.round((-b - Math.sqrt(discriminant)) / 2 / a);
374 return of(new Times(alpha1, alpha2));
375 }
376 }
377 }
378
379 @Override
380 public String toString() {
381 StringBuilder b = new StringBuilder();
382 b.append("VesselPosition [lat=");
383 b.append(lat);
384 b.append(", lon=");
385 b.append(lon);
386 b.append(", lengthMetres=");
387 b.append(lengthMetres);
388 b.append(", widthMetres=");
389 b.append(widthMetres);
390 b.append(", cogDegrees=");
391 b.append(cogDegrees);
392 b.append(", headingDegrees=");
393 b.append(headingDegrees);
394 b.append(", speedMetresPerSecond=");
395 b.append(speedMetresPerSecond);
396 b.append(", positionAisNmea=");
397 b.append(positionAisNmea);
398 b.append(", shipStaticAisNmea=");
399 b.append(shipStaticAisNmea);
400 b.append(", navStatus=");
401 b.append(navigationalStatus);
402 b.append(", time=");
403 b.append(time);
404 b.append(", id=");
405 b.append(id);
406 b.append(", cls=");
407 b.append(cls);
408 b.append(", shipType=");
409 b.append(shipType);
410 b.append(", messageId=");
411 b.append(messageId);
412 b.append(", data=");
413 b.append(data);
414 b.append("]");
415 return b.toString();
416 }
417
418 @Override
419 public int hashCode() {
420 final int prime = 31;
421 int result = 1;
422 result = prime * result + ((id == null) ? 0 : id.hashCode());
423 result = prime * result + (int) (messageId ^ (messageId >>> 32));
424 result = prime * result + (int) (time ^ (time >>> 32));
425 return result;
426 }
427
428 @Override
429 public boolean equals(Object obj) {
430 if (this == obj)
431 return true;
432 if (obj == null)
433 return false;
434 if (getClass() != obj.getClass())
435 return false;
436 VesselPosition other = (VesselPosition) obj;
437 if (id == null) {
438 if (other.id != null)
439 return false;
440 } else if (!id.equals(other.id))
441 return false;
442 if (messageId != other.messageId)
443 return false;
444 if (time != other.time)
445 return false;
446 return true;
447 }
448
449 @Override
450 public Fix fix() {
451 return new Fix() {
452
453 @Override
454 public Fix fix() {
455 return this;
456 }
457
458 @Override
459 public int mmsi() {
460 return (int) ((Mmsi) id).uniqueId();
461 }
462
463 @Override
464 public long time() {
465 return time;
466 }
467
468 @Override
469 public float lat() {
470 return (float) lat;
471 }
472
473 @Override
474 public float lon() {
475 return (float) lon;
476 }
477
478 @Override
479 public Optional<au.gov.amsa.risky.format.NavigationalStatus> navigationalStatus() {
480 return Optional.of(au.gov.amsa.risky.format.NavigationalStatus
481 .values()[navigationalStatus.ordinal()]);
482 }
483
484 @Override
485 public Optional<Float> speedOverGroundKnots() {
486 if (speedMetresPerSecond.isPresent())
487 return Optional.of((float) metresPerSecondToKnots(speedMetresPerSecond.get()));
488 else
489 return Optional.absent();
490 }
491
492 @Override
493 public Optional<Float> courseOverGroundDegrees() {
494 return toFloat(cogDegrees);
495 }
496
497 @Override
498 public Optional<Float> headingDegrees() {
499 return toFloat(headingDegrees);
500 }
501
502 @Override
503 public AisClass aisClass() {
504 if (cls == VesselClass.A)
505 return AisClass.A;
506 else if (cls == VesselClass.B)
507 return AisClass.B;
508 else
509 throw new RuntimeException("unexpected");
510 }
511
512 @Override
513 public Optional<Integer> latencySeconds() {
514 return Optional.absent();
515 }
516
517 @Override
518 public Optional<Short> source() {
519 return Optional.absent();
520 }
521
522 @Override
523 public Optional<Byte> rateOfTurn() {
524 return Optional.absent();
525 }
526 };
527 }
528
529 static double metresPerSecondToKnots(double x) {
530 return x * 3600.0 / 1852.0;
531 }
532
533 private static Optional<Float> toFloat(Optional<Double> value) {
534 if (value.isPresent())
535 return Optional.of(value.get().floatValue());
536 else
537 return Optional.absent();
538 }
539
540 }