/* Copyright (c) 2011 Danish Maritime Authority * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this library. If not, see <http://www.gnu.org/licenses/>. */ package dk.dma.ais.tracker.eventEmittingTracker; import com.google.common.eventbus.Subscribe; import dk.dma.ais.binary.SixbitException; import dk.dma.ais.message.AisMessage; import dk.dma.ais.message.AisMessage24; import dk.dma.ais.message.AisMessage3; import dk.dma.ais.message.AisMessage5; import dk.dma.ais.message.AisMessageException; import dk.dma.ais.message.AisPosition; import dk.dma.ais.message.AisPositionMessage; import dk.dma.ais.message.IPositionMessage; import dk.dma.ais.packet.AisPacket; import dk.dma.ais.sentence.SentenceException; import dk.dma.ais.sentence.Vdm; import dk.dma.ais.tracker.eventEmittingTracker.events.CellChangedEvent; import dk.dma.ais.tracker.eventEmittingTracker.events.PositionChangedEvent; import dk.dma.ais.tracker.eventEmittingTracker.events.TimeEvent; import dk.dma.enav.model.geometry.Position; import dk.dma.enav.model.geometry.PositionTime; import dk.dma.enav.model.geometry.grid.Cell; import dk.dma.enav.model.geometry.grid.Grid; import net.maritimecloud.util.SpeedUnit; import net.maritimecloud.util.geometry.PositionReader; import net.maritimecloud.util.geometry.PositionReaderSimulator; import org.junit.Test; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; public class EventEmittingTrackerImplTest { final static int MMSI = 12345678; final String[] NMEA_TEST_STRINGS = { // GatehouseSourceTag [baseMmsi=2190067, country=DK, region=, timestamp=Thu Apr 10 15:30:28 CEST 2014] // [msgId=1, repeat=0, userId=219000606, cog=2010, navStatus=0, pos=(33024811,6011092) = (33024811,6011092), posAcc=1, raim=0, specialManIndicator=0, rot=0, sog=108, spare=0, syncState=1, trueHeading=200, utcSec=60, slotTimeout=6, subMessage=1063] "$PGHP,1,2014,4,10,13,30,28,385,219,,2190067,1,12*26\r\n" + "!BSVDM,1,1,,A,13@ng7P01dPeo6`OOc:onVAp0p@W,0*12", // GatehouseSourceTag [baseMmsi=2190067, country=DK, region=, timestamp=Thu Apr 10 15:30:29 CEST 2014] // [msgId=5, repeat=0, userId=219000606, callsign=OWNM@@@, dest=BOEJDEN-FYNSHAV@@@@@, dimBow=12, dimPort=8, dimStarboard=4, dimStern=58, draught=30, dte=0, eta=67584, imo=8222824, name=FRIGG SYDFYEN@@@@@@@, posType=1, shipType=61, spare=0, version=0] "$PGHP,1,2014,4,10,13,30,29,165,219,,2190067,1,28*22\r\n" + "!BSVDM,2,1,1,A,53@ng7P1uN6PuLpl000I8TLN1=T@ITDp0000000u1Pr844@P07PSiBQ1,0*7B\r\n" + "!BSVDM,2,2,1,A,CcAVCTj0EP00000,2*53", // GatehouseSourceTag [baseMmsi=2190067, country=DK, region=, timestamp=Thu Apr 10 15:30:38 CEST 2014] // [msgId=1, repeat=0, userId=219000606, cog=2010, navStatus=0, pos=(33024530,6010902) = (33024530,6010902), posAcc=1, raim=0, specialManIndicator=0, rot=0, sog=108, spare=0, syncState=1, trueHeading=200, utcSec=60, slotTimeout=2, subMessage=1427] "$PGHP,1,2014,4,10,13,30,38,88,219,,2190067,1,26*1E\r\n" + "!BSVDM,1,1,,B,13@ng7P01dPeo0dOOb4WnVAp0`FC,0*26", // GatehouseSourceTag [baseMmsi=2190075, country=DK, region=, timestamp=Thu Apr 10 15:30:49 CEST 2014] // [msgId=1, repeat=0, userId=219000606, cog=2010, navStatus=0, pos=(33024198,6010667) = (33024198,6010667), posAcc=1, raim=0, specialManIndicator=0, rot=0, sog=108, spare=0, syncState=1, trueHeading=200, utcSec=60, slotTimeout=4, subMessage=1852] "$PGHP,1,2014,4,10,13,30,49,429,219,,2190075,1,2D*56\r\n" + "!BSVDM,1,1,,A,13@ng7P01dPenqFOO`iWnVAp0hLt,0*2D", // GatehouseSourceTag [baseMmsi=2190067, country=DK, region=, timestamp=Thu Apr 10 15:30:59 CEST 2014] // [msgId=1, repeat=0, userId=219000606, cog=2010, navStatus=0, pos=(33023918,6010471) = (33023918,6010471), posAcc=1, raim=0, specialManIndicator=0, rot=0, sog=108, spare=0, syncState=1, trueHeading=200, utcSec=60, slotTimeout=1, subMessage=12528] "$PGHP,1,2014,4,10,13,30,59,428,219,,2190067,1,2D*55\r\n" + "!BSVDM,1,1,,B,13@ng7P01dPenk>OOWcWnVAp0W3h,0*2D", // GatehouseSourceTag [baseMmsi=2190067, country=DK, region=, timestamp=Thu Apr 10 15:31:09 CEST 2014] // [msgId=1, repeat=0, userId=219000606, cog=2020, navStatus=0, pos=(33023639,6010274) = (33023639,6010274), posAcc=1, raim=0, specialManIndicator=0, rot=0, sog=108, spare=0, syncState=1, trueHeading=200, utcSec=60, slotTimeout=2, subMessage=348] "$PGHP,1,2014,4,10,13,31,9,318,219,,2190067,1,4F*61\r\n" + "!BSVDM,1,1,,A,13@ng7P01dPene4OOVUoq6Ap0`5L,0*4F", // GatehouseSourceTag [baseMmsi=2190067, country=DK, region=, timestamp=Thu Apr 10 15:31:18 CEST 2014] // [msgId=1, repeat=0, userId=219000606, cog=2010, navStatus=0, pos=(33023417,6010117) = (33023417,6010117), posAcc=1, raim=0, specialManIndicator=0, rot=0, sog=108, spare=0, syncState=1, trueHeading=199, utcSec=60, slotTimeout=0, subMessage=2263] "$PGHP,1,2014,4,10,13,31,18,678,219,,2190067,1,03*23\r\n" + "!BSVDM,1,1,,B,13@ng7P01dPen`:OOUfGnV?p0PSG,0*03" }; final Grid grid = Grid.createSize(100); /** * Test that grid cell change events are fired by the tracker when a simulated track is moving * north under the Great Belt bridge. * * Assumes grid size 100m. * Track starts in cell id 24686212289 (55°20'13.7"N,11°02'21.8"E) - (55°20'10.5"N,11°02'25.1"E) */ @Test public void testGridChangeEventsFired() { // Starting position in the center of cell 24686212289 Queue<AisMessage> messageQueue = initQueueOfAisMessages(MMSI); final int expectedNumberOfCellChangeEvents = messageQueue.size() - 1 /* minus the static msg */; // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Set up our expectations final long[] expectedSequenceOfCells = { 24686212289L, 24686613039L, 24687013789L, 24687414539L, 24687815289L, 24688216039L, 24688616789L, 24689017539L, 24689418289L, 24689819039L, 24690219789L, 24690620539L }; int nextExpectedCellId = 0; // Play scenario through tracker long firstTimestamp = System.currentTimeMillis(); int timeStep = 0; // Run test scenario and assert results assertEquals(0, tracker.getNumberOfTracks()); while (!messageQueue.isEmpty()) { AisMessage message = messageQueue.remove(); Date messageTimestamp = new Date(firstTimestamp + (timeStep++ * 10000)); // 10 secs between msgs System.out.println(messageTimestamp + ": " + message); tracker.update(messageTimestamp.getTime(), message); if (message instanceof IPositionMessage) { assertEquals(expectedSequenceOfCells[nextExpectedCellId++], testSubscriber.getCurrentCellId()); } } assertEquals(expectedNumberOfCellChangeEvents, testSubscriber.getNumberOfCellIdChangedEventsReceived()); assertEquals(1, tracker.getNumberOfTracks()); } @Test public void testGridChangeEventsAreNotFiredForBlackListedMMSIs() { Queue<AisMessage> messageQueue = initQueueOfAisMessages(MMSI); final int expectedNumberOfCellChangeEvents = messageQueue.size() - 1 /* minus the static msg */; // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid, 1, MMSI, 3); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Play scenario through tracker long firstTimestamp = System.currentTimeMillis(); int timeStep = 0; // Run test scenario and assert results assertEquals(0, tracker.getNumberOfTracks()); while (!messageQueue.isEmpty()) { AisMessage message = messageQueue.remove(); Date messageTimestamp = new Date(firstTimestamp + (timeStep++ * 10000)); // 10 secs between msgs System.out.println(messageTimestamp + ": " + message); tracker.update(messageTimestamp.getTime(), message); assertEquals(0, tracker.getNumberOfTracks()); assertFalse(testSubscriber.isPositionChangedEventFired()); assertFalse(testSubscriber.isCellIdChangedEventFired()); assertEquals(0, testSubscriber.getNumberOfCellIdChangedEventsReceived()); assertEquals(0, testSubscriber.getNumberOfPositionChangedEventsReceived()); assertEquals(null, testSubscriber.currentPosition); } } private Queue<AisMessage> initQueueOfAisMessages(int mmsi) { // Starting position in the center of cell 24686212289 Position startingPosition = Position.create((55.33714285714286 + 55.33624454148472)/2, (11.039401122894573 + 11.040299438552713)/2); System.out.println("Starting position: " + startingPosition); Cell startingCell = grid.getCell(startingPosition); assertEquals(24686212289L, startingCell.getCellId()); AisPosition aisStartingPosition = new AisPosition(startingPosition); // Create initial static and voyage data message Queue<AisMessage> messageQueue = new LinkedList<>(); AisMessage5 message5 = createAisMessage5(mmsi); messageQueue.add(message5); // Create series of position reports for passing under the bridge (north-going) AisMessage3 firstPositionMessage = createAisMessage3(MMSI, aisStartingPosition); messageQueue.add(firstPositionMessage); Position prevGeoLocation = firstPositionMessage.getPos().getGeoLocation(); final double step = grid.getResolution(); for (int i = 0; i < 10; i++) { AisMessage3 positionMessage = cloneAisMessage3(mmsi, firstPositionMessage); Position nextGeoLocation = Position.create(prevGeoLocation.getLatitude() + step, prevGeoLocation.getLongitude()); AisPosition nextPosition = new AisPosition(nextGeoLocation); positionMessage.setPos(nextPosition); System.out.println("Next position: " + nextGeoLocation); messageQueue.add(positionMessage); prevGeoLocation = positionMessage.getPos().getGeoLocation(); } return messageQueue; } /** * Test that events are fired by the tracker at the correct times, when a * when track movement is simulated by PositionReaderSimulator. * * The test scenario includes that no AIS message of type 5 (with static content * is received as the first message). Only position reports are received for the * track. * * Assumes grid size 200m. * Track starts in cell id 6169552395L (55 19.161N, 011 02.457E) */ @Test public void testEventsFiredForKnownSimulatedPatternWhenOnlyPositionReportsAreReceivedForTheTrack() throws SentenceException, AisMessageException, SixbitException { testEventsFiredForKnownSimulatedPattern(false); } /** * Test that events are fired by the tracker at the correct times, when a * when track movement is simulated by PositionReaderSimulator. * * The test scenario includes that an AIS message of type 5 (with static content) * is received as the first message. * * Assumes grid size 200m. * Track starts in cell id 6169552395L (55 19.161N, 011 02.457E) */ @Test public void testEventsFiredForKnownSimulatedPatternWhenStaticMessageIsReceivedFirst() throws SentenceException, AisMessageException, SixbitException { testEventsFiredForKnownSimulatedPattern(true); } private void testEventsFiredForKnownSimulatedPattern(final boolean initTrackerWithAisMessage5) throws SentenceException, AisMessageException, SixbitException { // Setup test data final int speedKnots = 20; PositionReaderSimulator simulator = new PositionReaderSimulator(); simulator.setSpeedFixed(speedKnots, SpeedUnit.KNOTS); simulator.setTimeSourceFixedSlice(5000); /* Interpolation never kicks in */ net.maritimecloud.util.geometry.Position southOfGreatBeltBridge = net.maritimecloud.util.geometry.Position.create(55.31889216347121, 11.04098609182333); // Cell 6169552395 net.maritimecloud.util.geometry.Position northOfGreatBeltBridge = net.maritimecloud.util.geometry.Position.create(55.37667465016155, 11.03926947805367); // Cell ? PositionReader reader = simulator.forRoute(southOfGreatBeltBridge, northOfGreatBeltBridge); Date timestamp = new Date(5000L); // Create or mock dependencies final Grid grid = Grid.createSize(200); // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Init tracker with static ship info if (initTrackerWithAisMessage5) { AisMessage5 aisMessage5 = new AisMessage5(); aisMessage5.setRepeat(0); aisMessage5.setUserId(211223120); aisMessage5.setImo(1234567); aisMessage5.setDest("HORSENS"); aisMessage5.setDraught(60); aisMessage5.setDte(0); aisMessage5.setPosType(1); aisMessage5.setCallsign("OY1234"); aisMessage5.setName("NO NAME"); aisMessage5.setEta(31012013); aisMessage5.setDimBow(18); aisMessage5.setDimPort(3); aisMessage5.setDimStarboard(3); aisMessage5.setDimStern(12); aisMessage5.setShipType(5); tracker.update(timestamp.getTime(), aisMessage5); } // Prepare position message Vdm vdm = new Vdm(); vdm.parse("!BSVDM,1,1,,A,339L2D0OhC0fW:lO5Sa:>8=J00s1,0*7E"); AisPositionMessage aisMessage3 = new AisMessage3(vdm); aisMessage3.setCog(0); aisMessage3.setSog(speedKnots); // Run test scenario and assert results long[] expectedCellIds = { 6169552395L, // #0 6169552395L, 6169752770L, 6169752770L, 6169752770L, 6169752770L, // #5 6169953145L, 6169953145L, 6169953145L, 6169953145L, 6170153520L, // #10 6170153520L, 6170153520L, 6170153520L, 6170353895L, 6170353895L, // #15 6170353895L, 6170353895L, 6170554270L, 6170554270L, 6170554270L, // #20 6170754645L, 6170754645L, 6170754645L, 6170754645L, 6170955020L, // #25 6170955020L, 6170955020L, 6170955020L, 6171155395L, 6171155395L, // #30 6171155395L, 6171155395L, 6171355770L, 6171355770L, 6171355770L, // #35 6171355770L, 6171556145L, 6171556145L, 6171556145L, 6171556145L, // #40 6171756520L, 6171756520L, 6171756520L, 6171756520L, 6171956895L, // #45 6171956895L, 6171956895L, 6171956895L, 6172157270L, 6172157269L, /* under the bridge */ 6172157269L, /* under the bridge */ 6172157269L, /* under the bridge */ 6172357644L, 6172357644L, 6172357644L, // #55 6172558019L, 6172558019L, 6172558019L, 6172558019L, 6172758394L, // #60 6172758394L, 6172758394L, 6172758394L, 6172958769L, 6172958769L, // #65 6172958769L, 6172958769L, 6173159144L, 6173159144L, // #70 6173159144L, 6173159144L, 6173359519L, 6173359519L, 6173359519L, // #75 6173359519L, 6173559894L, 6173559894L, 6173559894L, 6173559894L, // #80 6173760269L, 6173760269L, 6173760269L, 6173760269L, 6173960644L, 6173960644L, 6173960644L, 6173960644L, 6174161019L, 6174161019L, 6174161019L, 6174361394L, 6174361394L, 6174361394L, 6174361394L, 6174561769L, 6174561769L, 6174561769L, 6174561769L, 6174762144L, 6174762144L, 6174762144L, 6174762144L, 6174962519L, 6174962519L, 6174962519L, 6174962519L, 6175162894L, 6175162894L, 6175162894L, 6175162894L, 6175363269L, 6175363269L, 6175363269L, 6175363269L, 6175563644L, 6175563644L, 6175563644L, 6175563644L, 6175764019L, 6175764019L, 6175764019L, 6175964394L, 6175964394L, 6175964394L }; boolean[] expectedCellIdChangedEventFired = { true, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, true, false, false, true, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, false, true, false, false, true, false, false, }; assertEquals(expectedCellIds.length, expectedCellIdChangedEventFired.length); int i = 0; dk.dma.enav.model.geometry.Position position; do { System.out.println("i: " + i); net.maritimecloud.util.geometry.PositionTime currentPosition = reader.getCurrentPosition(); long oldTimestamp = timestamp.getTime(); long newTimestamp = currentPosition.getTime(); assertEquals(oldTimestamp + 5000, newTimestamp); timestamp = new Date(newTimestamp); position = dk.dma.enav.model.geometry.Position.create(currentPosition.getLatitude(), currentPosition.getLongitude()); AisPosition aisPosition = new AisPosition(position); aisMessage3.setPos(aisPosition); testSubscriber.resetFiredStatus(); tracker.update(newTimestamp, aisMessage3); System.out.println("---"); assertTrue(testSubscriber.isPositionChangedEventFired()); assertEquals(expectedCellIdChangedEventFired[i], testSubscriber.isCellIdChangedEventFired()); assertEquals(expectedCellIds[i], testSubscriber.getCurrentCellId()); i++; } while (position.getLatitude() < 55.3766); } /** * Test that grid cell change events are not fired by the tracker when a simulated track is moving * inside the same cell. * * Assumes grid size 100m. * Track starts in cell id 24686212289 (55°20'13.7"N,11°02'21.8"E) - (55°20'10.5"N,11°02'25.1"E) */ @Test public void testGridChangeEventsNotFiredForMovementsInsideSameCell() { // Starting position in the center of cell 24686212289 Position startingPosition = Position.create((55.33714285714286 + 55.33624454148472)/2, (11.039401122894573 + 11.040299438552713)/2); System.out.println("Starting position: " + startingPosition); Cell startingCell = grid.getCell(startingPosition); assertEquals(24686212289L, startingCell.getCellId()); dk.dma.ais.message.AisPosition aisStartingPosition = new AisPosition(startingPosition); // Create initial static and voyage data message Queue<AisMessage> messageQueue = new LinkedList<>(); AisMessage5 message5 = createAisMessage5(MMSI); messageQueue.add(message5); // Create series of position reports for passing under the bridge (north-going) AisMessage3 firstPositionMessage = createAisMessage3(MMSI, aisStartingPosition); messageQueue.add(firstPositionMessage); Position prevGeoLocation = firstPositionMessage.getPos().getGeoLocation(); final double step = grid.getResolution() / 25; for (int i = 0; i < 10; i++) { AisMessage3 positionMessage = cloneAisMessage3(MMSI, firstPositionMessage); Position nextGeoLocation = Position.create(prevGeoLocation.getLatitude() + step, prevGeoLocation.getLongitude()); AisPosition nextPosition = new AisPosition(nextGeoLocation); positionMessage.setPos(nextPosition); System.out.println("Next position: " + nextGeoLocation); messageQueue.add(positionMessage); prevGeoLocation = positionMessage.getPos().getGeoLocation(); } // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Play scenario through tracker long firstTimestamp = System.currentTimeMillis(); int timeStep = 0; // Run test scenario and assert results assertEquals(0, tracker.getNumberOfTracks()); while (!messageQueue.isEmpty()) { AisMessage message = messageQueue.remove(); Date messageTimestamp = new Date(firstTimestamp + (timeStep++ * 10000)); // 10 secs between msgs System.out.println(messageTimestamp + ": " + message); tracker.update(messageTimestamp.getTime(), message); if (message instanceof IPositionMessage) { assertEquals(startingCell.getCellId(), testSubscriber.getCurrentCellId()); } } assertEquals(1, testSubscriber.getNumberOfCellIdChangedEventsReceived()); assertEquals(1, tracker.getNumberOfTracks()); } /** * Test that TimeEvents are fired by the tracker at periodic intervals in the data stream. */ @Test public void testTimeEventsFired() { // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Play events through tracker long firstTimestamp = System.currentTimeMillis(); int timeStep = EventEmittingTrackerImpl.TIME_EVENT_PERIOD_MILLIS / 2 - 1; assertEquals(0, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp, new AisMessage24()); assertEquals(1, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp + 1 * timeStep, new AisMessage24()); assertEquals(1, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp + 2 * timeStep, new AisMessage24()); assertEquals(1, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp + 3 * timeStep, new AisMessage24()); assertEquals(2, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp + 4 * timeStep, new AisMessage24()); assertEquals(2, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp + 5 * timeStep, new AisMessage24()); assertEquals(2, testSubscriber.getNumberOfTimeEventsReceived()); tracker.update(firstTimestamp + 6 * timeStep, new AisMessage24()); assertEquals(3, testSubscriber.getNumberOfTimeEventsReceived()); } /** * Test that Track.getTimeOfLastUpdate is updated on every update */ @Test public void testTrackTimestampIsUpdatedOnUpdates() { AisPacket[] packets = new AisPacket[NMEA_TEST_STRINGS.length]; for (int i = 0; i < NMEA_TEST_STRINGS.length; i++) { packets[i] = AisPacket.from(NMEA_TEST_STRINGS[i]); } // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); for (AisPacket packet : packets) { tracker.update(packet); Track track = tracker.tracks.get(219000606); assertEquals(packet.getBestTimestamp(), track.getTimeOfLastUpdate()); } } @Test public void testCalculateInterpolatedPositions() { Date now = new Date(System.currentTimeMillis()); long t1 = now.getTime(); PositionTime pt1 = PositionTime.create(55, 10, t1); long t2 = t1 + 50*1000; PositionTime pt2 = PositionTime.create(60, 15, t2); Map<Long,Position> interpolatedPositions = EventEmittingTrackerImpl.calculateInterpolatedPositions(pt1, pt2); assertEquals(4, interpolatedPositions.size()); Set<Long> interpolationTimestamps = interpolatedPositions.keySet(); // Assert timestamps assertEquals((Long) (t1 + (long) EventEmittingTrackerImpl.INTERPOLATION_TIME_STEP_MILLIS), interpolationTimestamps.toArray()[0]); assertEquals((Long) (t1 + (long) EventEmittingTrackerImpl.INTERPOLATION_TIME_STEP_MILLIS * 2), interpolationTimestamps.toArray()[1]); assertEquals((Long) (t1 + (long) EventEmittingTrackerImpl.INTERPOLATION_TIME_STEP_MILLIS*3), interpolationTimestamps.toArray()[2]); assertEquals((Long) (t1 + (long) EventEmittingTrackerImpl.INTERPOLATION_TIME_STEP_MILLIS * 4), interpolationTimestamps.toArray()[3]); // Assert positions assertEquals(Position.create(56, 11), interpolatedPositions.get(interpolationTimestamps.toArray()[0])); assertEquals(Position.create(57, 12), interpolatedPositions.get(interpolationTimestamps.toArray()[1])); assertEquals(Position.create(58, 13), interpolatedPositions.get(interpolationTimestamps.toArray()[2])); assertEquals(Position.create(59, 14), interpolatedPositions.get(interpolationTimestamps.toArray()[3])); // assertEquals(p2, interpolatedPositions.get(interpolationTimestamps.toArray()[4])); } /** * Test that interpolation is not used if less than 10 secs between messages */ @Test public void testTrackIsNotInterpolated() { testInterpolation(EventEmittingTrackerImpl.TRACK_INTERPOLATION_REQUIRED_MILLIS - 1000, 2 /* initial + second */ ); } /** * Test that interpolation is used if more than 10 secs between messages */ @Test public void testTrackIsInterpolated() { testInterpolation(EventEmittingTrackerImpl.TRACK_INTERPOLATION_REQUIRED_MILLIS + 1000, 5 /* initial + second + 3 interpolated */); } @Test public void testAisPacketsAreStored() { AisPacket[] packets = new AisPacket[NMEA_TEST_STRINGS.length]; for (int i = 0; i < NMEA_TEST_STRINGS.length; i++) { packets[i] = AisPacket.from(NMEA_TEST_STRINGS[i]); } final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestPositionEventSubscriber testSubscriber = new TestPositionEventSubscriber(); tracker.registerSubscriber(testSubscriber); for (AisPacket packet : packets) { tracker.update(packet); } // Test that exactly one track is created assertEquals(1, tracker.tracks.size()); Track track = tracker.tracks.get(219000606); assertNotNull(track); // Test tracking reports of this track assertEquals(6, testSubscriber.getNumberOfPositionChangedEventsReceived()); // Test newest tracking report TrackingReport newestTrackingReport = track.getNewestTrackingReport(); assertTrue(newestTrackingReport instanceof AisTrackingReport); AisTrackingReport aisTrackingReport = (AisTrackingReport) newestTrackingReport; assertEquals(1397136678678L, aisTrackingReport.getTimestamp()); assertEquals(Position.create(55.039028333333334, 10.016861666666667), aisTrackingReport.getPosition()); assertEquals(10.8, aisTrackingReport.getSpeedOverGround(), 1e-6); assertEquals(201.0, aisTrackingReport.getCourseOverGround(), 1e-6); // Test all raw AisPacket are stored with the track List<TrackingReport> trackingReports = track.getTrackingReports(); assertEquals(6, trackingReports.size()); trackingReports.forEach( r -> assertTrue(r instanceof AisTrackingReport)); assertEquals(NMEA_TEST_STRINGS[0], ((AisTrackingReport) trackingReports.get(0)).getPacket().getStringMessage()); assertEquals(NMEA_TEST_STRINGS[2], ((AisTrackingReport) trackingReports.get(1)).getPacket().getStringMessage()); assertEquals(NMEA_TEST_STRINGS[3], ((AisTrackingReport) trackingReports.get(2)).getPacket().getStringMessage()); assertEquals(NMEA_TEST_STRINGS[4], ((AisTrackingReport) trackingReports.get(3)).getPacket().getStringMessage()); assertEquals(NMEA_TEST_STRINGS[5], ((AisTrackingReport) trackingReports.get(4)).getPacket().getStringMessage()); assertEquals(NMEA_TEST_STRINGS[6], ((AisTrackingReport) trackingReports.get(5)).getPacket().getStringMessage()); // Test the static assertEquals(NMEA_TEST_STRINGS[1], track.getLastStaticReport().getStringMessage()); } /** * Test that all position between two time-distant AIS position messages are marked as interpolated. */ @Test public void testTrackingPositionsAreMarkedAsInterpolated() throws SentenceException, AisMessageException, SixbitException { AisPacket[] packets = new AisPacket[NMEA_TEST_STRINGS.length]; for (int i = 0; i < NMEA_TEST_STRINGS.length; i++) { packets[i] = AisPacket.from(NMEA_TEST_STRINGS[i]); } final int n1 = 0; final int n2 = 6; assertTrue(packets[n2].getBestTimestamp() - packets[n1].getBestTimestamp() > EventEmittingTrackerImpl.TRACK_INTERPOLATION_REQUIRED_MILLIS); // Create or mock dependencies final Grid grid = Grid.createSize(200); final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestPositionEventSubscriber testSubscriber = new TestPositionEventSubscriber(); tracker.registerSubscriber(testSubscriber); testSubscriber.resetFiredStatus(); // Insert first position update tracker.update(packets[n1]); assertTrue(testSubscriber.isPositionChangedEventFired()); assertEquals(1, testSubscriber.getNumberOfPositionChangedEventsReceived()); // Insert second position update tracker.update(packets[n2]); assertTrue(testSubscriber.isPositionChangedEventFired()); assertEquals(7, testSubscriber.getNumberOfPositionChangedEventsReceived()); // Assert results List<Boolean> positionTypes = testSubscriber.getPositionChangedEvents(); Boolean[] pos = positionTypes.toArray(new Boolean[0]); assertEquals(false, pos[0]); assertEquals(true, pos[1]); assertEquals(true, pos[2]); assertEquals(true, pos[3]); assertEquals(true, pos[4]); assertEquals(true, pos[5]); assertEquals(false, pos[6]); } /** * Test that interpolation is used between 2 position messages 50 seconds apart - even though there * is a static message half way between */ @Test public void testTrackIsInterpolatedEvenThoughStaticMessageIsBetweenToPositionUpdates() { // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Scenario AisPosition p1 = new AisPosition(Position.create(55, 10)); AisPosition p3 = new AisPosition(Position.create(56, 11)); long currentTimeMillis = System.currentTimeMillis(); Date t1 = new Date(currentTimeMillis); Date t2 = new Date(currentTimeMillis + (EventEmittingTrackerImpl.TRACK_INTERPOLATION_REQUIRED_MILLIS-1000)*1); Date t3 = new Date(currentTimeMillis + (EventEmittingTrackerImpl.TRACK_INTERPOLATION_REQUIRED_MILLIS-1000)*2); AisMessage messageSeq1 = createAisMessage3(MMSI, p1); AisMessage messageSeq2 = createAisMessage5(MMSI); AisMessage messageSeq3 = createAisMessage3(MMSI, p3); tracker.update(t1.getTime(), messageSeq1); tracker.update(t2.getTime(), messageSeq2); tracker.update(t3.getTime(), messageSeq3); // Assert result int expectedNumberOfPositionChangeEvents = 2 /* p1, p3 */+ /* interpolated */(int) ((t3.getTime() - t1.getTime())/ EventEmittingTrackerImpl.INTERPOLATION_TIME_STEP_MILLIS); assertEquals(expectedNumberOfPositionChangeEvents, testSubscriber.getNumberOfPositionChangedEventsReceived()); assertEquals(Position.create(56, 11), testSubscriber.getCurrentPosition()); } @Test public void testTrackIsStale() { assertFalse(EventEmittingTrackerImpl.isTrackStale(0, 0, EventEmittingTrackerImpl.TRACK_STALE_MILLIS - 1)); assertFalse(EventEmittingTrackerImpl.isTrackStale(0, 0, EventEmittingTrackerImpl.TRACK_STALE_MILLIS + 1)); long now = new Date(System.currentTimeMillis()).getTime(); assertFalse(EventEmittingTrackerImpl.isTrackStale(now, now, now + EventEmittingTrackerImpl.TRACK_STALE_MILLIS - 1)); assertTrue(EventEmittingTrackerImpl.isTrackStale(now, now, now + EventEmittingTrackerImpl.TRACK_STALE_MILLIS + 1)); assertFalse(EventEmittingTrackerImpl.isTrackStale(1, now, now + EventEmittingTrackerImpl.TRACK_STALE_MILLIS - 1)); assertTrue(EventEmittingTrackerImpl.isTrackStale(1, now, now + EventEmittingTrackerImpl.TRACK_STALE_MILLIS + 1)); assertFalse(EventEmittingTrackerImpl.isTrackStale(now, 1, now + EventEmittingTrackerImpl.TRACK_STALE_MILLIS - 1)); assertTrue(EventEmittingTrackerImpl.isTrackStale(now, 1, now + EventEmittingTrackerImpl.TRACK_STALE_MILLIS + 1)); } @Test public void testCanProcessStaleTracks() { // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Prepare test data Position startingPosition = Position.create((55.33714285714286 + 55.33624454148472)/2, (11.039401122894573 + 11.040299438552713)/2); dk.dma.ais.message.AisPosition aisPosition = new AisPosition(startingPosition); AisMessage3 positionMessage = createAisMessage3(MMSI, aisPosition); Date t1 = new Date(System.currentTimeMillis()); Date t2 = new Date(t1.getTime() + EventEmittingTrackerImpl.TRACK_STALE_MILLIS + 60000); // Execute test where track goes stale tracker.update(t1.getTime(), positionMessage); tracker.update(t2.getTime(), positionMessage); // No exceptions are expected } private void testInterpolation(long millisBetweenMessages, int expectedNumberOfPositionChangeEvents) { // Create object under test final EventEmittingTrackerImpl tracker = new EventEmittingTrackerImpl(grid); // Prepare test data Position startingPosition = Position.create((55.33714285714286 + 55.33624454148472)/2, (11.039401122894573 + 11.040299438552713)/2); dk.dma.ais.message.AisPosition aisStartingPosition = new AisPosition(startingPosition); AisMessage3 firstPositionMessage = createAisMessage3(MMSI, aisStartingPosition); Position secondPosition = Position.create((55.33714285714286 + 55.35624454148472)/2, (11.038401122894573 + 10.890299438552713)/2); dk.dma.ais.message.AisPosition aisSecondPosition = new AisPosition(secondPosition); AisMessage3 secondPositionMessage = createAisMessage3(MMSI, aisSecondPosition); Date firstTimestamp = new Date(System.currentTimeMillis()); Date secondTimestamp = new Date(System.currentTimeMillis() + millisBetweenMessages); System.out.println("Starting at " + firstTimestamp.getTime() + " in " + startingPosition); System.out.println("Ending at " + secondTimestamp.getTime() + " in " + secondPosition); // Wire up test subscriber // (discussion: https://code.google.com/p/guava-libraries/issues/detail?id=875) TestSubscriber testSubscriber = new TestSubscriber(); tracker.registerSubscriber(testSubscriber); // Execute test tracker.update(firstTimestamp.getTime(), firstPositionMessage); tracker.update(secondTimestamp.getTime(), secondPositionMessage); // Assert result assertEquals(expectedNumberOfPositionChangeEvents, testSubscriber.getNumberOfPositionChangedEventsReceived()); assertEquals(secondPosition.getLatitude(), testSubscriber.getCurrentPosition().getLatitude(), 1e-6); assertEquals(secondPosition.getLongitude(), testSubscriber.getCurrentPosition().getLongitude(), 1e-6); } private static AisMessage3 cloneAisMessage3(int mmsi, AisMessage3 msgToClone) { AisMessage3 message = new AisMessage3(); message.setUserId(mmsi); message.setCog(msgToClone.getCog()); message.setNavStatus(msgToClone.getNavStatus()); message.setRot(msgToClone.getRot()); message.setSog(msgToClone.getSog()); return message; } private static AisMessage3 createAisMessage3(int mmsi, AisPosition aisStartingPosition) { AisMessage3 positionMessage = new AisMessage3(); positionMessage.setUserId(mmsi); positionMessage.setPos(aisStartingPosition); positionMessage.setCog(1); positionMessage.setNavStatus(0); positionMessage.setRot(0); positionMessage.setSog(10); return positionMessage; } private static AisMessage5 createAisMessage5(int mmsi) { AisMessage5 message5 = new AisMessage5(); message5.setUserId(mmsi); message5.setDest("SKAGEN"); message5.setCallsign("OY1234"); message5.setImo(1234567); return message5; } public class TestSubscriber { private long currentCellId; private Position currentPosition; private int numberOfTimeEventsReceived; private int numberOfCellIdChangedEventsReceived; private int numberOfPositionChangedEventsReceived; private boolean timeEventFired; private boolean cellIdChangedEventFired; private boolean positionChangedEventFired; public void resetFiredStatus() { cellIdChangedEventFired = false; positionChangedEventFired = false; } @Subscribe public void onTimeMark(TimeEvent event) { System.out.println("TimeEvent"); timeEventFired = true; numberOfTimeEventsReceived++; } @Subscribe public void onCellIdChanged(CellChangedEvent event) { System.out.println("CellChangedEvent"); cellIdChangedEventFired = true; numberOfCellIdChangedEventsReceived++; currentCellId = (long) event.getTrack().getProperty(Track.CELL_ID); System.out.println("We are now in cell: " + currentCellId); assertTrackTimestamps(event.getTrack()); } @Subscribe public void onPositionChanged(PositionChangedEvent event) { System.out.println("PositionChangedEvent"); positionChangedEventFired = true; numberOfPositionChangedEventsReceived++; currentPosition = event.getTrack().getNewestTrackingReport().getPosition(); System.out.println("We are now in position: " + currentPosition); assertTrackTimestamps(event.getTrack()); } private void assertTrackTimestamps(Track track) { // TIMESTAMP_ANY_UPDATE is never allowed to fall behind TIMESTAMP_POSITION_UPDATE long anyUpdate = track.getTimeOfLastUpdate(); long posUpdate = track.getNewestTrackingReport().getTimestamp(); assertTrue(anyUpdate >= posUpdate); } private long getTimestampFromTrack(Track track, String timestampType) { Long timestamp = (Long) track.getProperty(timestampType); if (timestamp == null) { timestamp = 0L; } return timestamp; } public long getCurrentCellId() { return currentCellId; } public Position getCurrentPosition() { return currentPosition; } public int getNumberOfCellIdChangedEventsReceived() { return numberOfCellIdChangedEventsReceived; } public int getNumberOfTimeEventsReceived() { return numberOfTimeEventsReceived; } public int getNumberOfPositionChangedEventsReceived() { return numberOfPositionChangedEventsReceived; } public boolean isCellIdChangedEventFired() { return cellIdChangedEventFired; } public boolean isPositionChangedEventFired() { return positionChangedEventFired; } } public class TestPositionEventSubscriber { private List<Boolean> positionIsInterpolated = new LinkedList<>(); public void resetFiredStatus() { positionIsInterpolated = new LinkedList<>(); } @Subscribe public void onPositionChanged(PositionChangedEvent event) { System.out.println("PositionChangedEvent"); positionIsInterpolated.add(new Boolean(event.getTrack().getNewestTrackingReport() instanceof InterpolatedTrackingReport)); } public int getNumberOfPositionChangedEventsReceived() { return positionIsInterpolated.size(); } public boolean isPositionChangedEventFired() { return positionIsInterpolated.size() > 0; } public List<Boolean> getPositionChangedEvents() { return positionIsInterpolated; } } }