View Javadoc
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          // order of these should reflect numerical order in nav status int
22          // returned from ITU standard ais position report A
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         // leave these null so if not set get an error in VesselPosition
171         // constructor
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     // private Area createArea(VesselPosition relativeTo) {
278     // Vector v = position(relativeTo);
279     // Rectangle2D.Double r = baseRectangle();
280     // Area a = new Area(r);
281     // AffineTransform af = new AffineTransform();
282     // af.rotate(toRadians(headingDegrees), v.x(), v.y());
283     // return a.createTransformedArea(af);
284     // }
285 
286     public Vector position(VesselPosition relativeTo) {
287         // TODO longitude wrapping check
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     // public boolean intersects(VesselPosition p) {
294     // Area area = createArea(this);
295     // area.intersect(p.createArea(this));
296     // return !area.isEmpty();
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      * Returns absent if no intersection occurs else return the one or two times
332      * of intersection of circles around the vessel relative to this.time().
333      * 
334      * @param vp
335      * @return
336      */
337     public Optional<Times> intersectionTimes(VesselPosition vp) {
338 
339         // TODO handle vp doesn't have speed or cog but is within collision
340         // distance given any cog and max speed
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         // imagine a ring around the vessel centroid with maxDimensionMetres/2
350         // radius. This is the ring we are going to test for collision.
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         // Now solve the quadratic equation with coefficients a,b,c
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 }