diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index c11d13a8..97f387a9 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -136,10 +136,7 @@ public static TripInstruction alignTravelerToTrip( Locale locale = travelerPosition.locale; if (isApproachingEndOfLeg(travelerPosition)) { - if (isBusLeg(travelerPosition.nextLeg) && isWithinOperationalNotifyWindow(travelerPosition)) { - BusOperatorActions - .getDefault() - .handleSendNotificationAction(tripStatus, travelerPosition); + if (sendBusNotification(travelerPosition, isStartOfTrip, tripStatus)) { // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. return new WaitForTransitInstruction(travelerPosition.nextLeg, travelerPosition.currentTime, locale); } @@ -157,6 +154,32 @@ public static TripInstruction alignTravelerToTrip( return null; } + /** + * Send bus notification if the first leg is a bus leg or approaching a bus leg and within the notify window. + */ + public static boolean sendBusNotification( + TravelerPosition travelerPosition, + boolean isStartOfTrip, + TripStatus tripStatus + ) { + if (shouldNotifyBusOperator(travelerPosition, isStartOfTrip)) { + BusOperatorActions + .getDefault() + .handleSendNotificationAction(tripStatus, travelerPosition); + return true; + } + return false; + } + + /** + * Given the traveler's position and leg type, check if bus notification should be sent. + */ + public static boolean shouldNotifyBusOperator(TravelerPosition travelerPosition, boolean isStartOfTrip) { + return (isStartOfTrip) + ? isBusLeg(travelerPosition.expectedLeg) && isWithinOperationalNotifyWindow(travelerPosition.currentTime, travelerPosition.expectedLeg) + : isBusLeg(travelerPosition.nextLeg) && isWithinOperationalNotifyWindow(travelerPosition); + } + /** * Align the traveler's position to the nearest transit stop or destination. */ @@ -218,15 +241,19 @@ public static boolean isAtEndOfLeg(TravelerPosition travelerPosition) { return getDistanceToEndOfLeg(travelerPosition) <= TRIP_INSTRUCTION_IMMEDIATE_RADIUS; } + public static boolean isWithinOperationalNotifyWindow(TravelerPosition travelerPosition) { + return isWithinOperationalNotifyWindow(travelerPosition.currentTime, travelerPosition.nextLeg); + } + /** * Make sure the traveler is on schedule or ahead of schedule (but not too far) to be within an operational window * for the bus service. */ - public static boolean isWithinOperationalNotifyWindow(TravelerPosition travelerPosition) { - var busDepartureTime = getBusDepartureTime(travelerPosition.nextLeg); + public static boolean isWithinOperationalNotifyWindow(Instant currentTime, Leg busLeg) { + var busDepartureTime = getBusDepartureTime(busLeg); return - (travelerPosition.currentTime.equals(busDepartureTime) || travelerPosition.currentTime.isBefore(busDepartureTime)) && - ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES >= getMinutesAheadOfDeparture(travelerPosition.currentTime, busDepartureTime); + (currentTime.equals(busDepartureTime) || currentTime.isBefore(busDepartureTime)) && + ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES >= getMinutesAheadOfDeparture(currentTime, busDepartureTime); } /** diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 104ee417..ae876224 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -79,6 +79,12 @@ public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { /** Used for unit testing. */ public TravelerPosition(Leg nextLeg, Instant currentTime) { + this (null, nextLeg, currentTime); + } + + /** Used for unit testing. */ + public TravelerPosition(Leg expectedLeg, Leg nextLeg, Instant currentTime) { + this.expectedLeg = expectedLeg; this.nextLeg = nextLeg; this.currentTime = currentTime; } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index 24b27125..804ed383 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -155,7 +155,7 @@ void canNotifyBusOperatorOnlyOnce() throws InterruptedException, JsonProcessingE @ParameterizedTest @MethodSource("createWithinOperationalNotifyWindowTrace") - void isWithinOperationalNotifyWindow(boolean expected, TravelerPosition travelerPosition,String message) { + void isWithinOperationalNotifyWindow(boolean expected, TravelerPosition travelerPosition, String message) { assertEquals(expected, TravelerLocator.isWithinOperationalNotifyWindow(travelerPosition), message); } @@ -188,6 +188,45 @@ private static Stream createWithinOperationalNotifyWindowTrace() { ); } + @ParameterizedTest + @MethodSource("createShouldNotifyBusOperatorTrace") + void shouldNotifyBusOperator(boolean expected, TravelerPosition travelerPosition, boolean isStartOfTrip, String message) { + assertEquals(expected, TravelerLocator.shouldNotifyBusOperator(travelerPosition, isStartOfTrip), message); + } + + private static Stream createShouldNotifyBusOperatorTrace() { + var walkLeg = walkToBusTransition.legs.get(0); + var busLeg = walkToBusTransition.legs.get(1); + var busDepartureTime = getBusDepartureTime(busLeg); + + return Stream.of( + Arguments.of( + true, + new TravelerPosition(busLeg, busDepartureTime), + false, + "Traveler approaching a bus leg, should notify." + ), + Arguments.of( + false, + new TravelerPosition(walkLeg, busDepartureTime), + false, + "Traveler approaching a walk leg, should not notify." + ), + Arguments.of( + true, + new TravelerPosition(busLeg, null, busDepartureTime), + true, + "Traveler at the start of a trip which starts with a bus leg, should notify." + ), + Arguments.of( + false, + new TravelerPosition(walkLeg, null, busDepartureTime), + true, + "Traveler at the start of a trip which starts with a walk leg, should not notify." + ) + ); + } + private static OtpUser createOtpUser() { MobilityProfile mobilityProfile = new MobilityProfile(); mobilityProfile.mobilityMode = "WChairE";