From 89d50747cfd20fb69c46d4bb61c9ec29999dc41f Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 24 Oct 2024 16:22:29 +0100 Subject: [PATCH 1/7] refactor(Notify bus op if first leg is bus leg): --- .../triptracker/TravelerLocator.java | 43 +++++++++++++++---- .../triptracker/TravelerPosition.java | 6 +++ .../triptracker/NotifyBusOperatorTest.java | 41 +++++++++++++++++- 3 files changed, 81 insertions(+), 9 deletions(-) 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"; From 4df263b1c07dc2162a90d53682d148cffcf656a0 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 29 Oct 2024 15:05:47 +0000 Subject: [PATCH 2/7] refactor(Various changes to allow bus notification to be sent on first trip leg): --- .../triptracker/ManageTripTracking.java | 35 +- .../triptracker/TravelerLocator.java | 54 +- .../triptracker/TravelerPosition.java | 7 + .../busnotifiers/BusOperatorActions.java | 18 +- .../busnotifiers/BusOperatorInteraction.java | 5 +- .../UsRideGwinnettNotifyBusOperator.java | 14 +- .../middleware/utils/ItineraryUtils.java | 13 +- .../triptracker/NotifyBusOperatorTest.java | 94 +++- .../controllers/api/first-leg-transit.json | 464 ++++++++++++++++++ 9 files changed, 639 insertions(+), 65 deletions(-) create mode 100644 src/test/resources/org/opentripplanner/middleware/controllers/api/first-leg-transit.json diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 1332bdf1..b8acfaff 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -2,6 +2,8 @@ import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.otp.response.Itinerary; +import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.triptracker.instruction.TripInstruction; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; @@ -10,6 +12,10 @@ import spark.Request; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getFirstLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.legsMatch; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; public class ManageTripTracking { @@ -145,19 +151,40 @@ private static EndTrackingResponse completeJourney(TripTrackingData tripData, bo tripData.trip.journeyState.matchingItinerary, Persistence.otpUsers.getById(tripData.trip.userId) ); - BusOperatorActions - .getDefault() - .handleCancelNotificationAction(travelerPosition); + cancelBusNotification(travelerPosition, tripData.trip.journeyState.matchingItinerary); TrackedJourney trackedJourney = travelerPosition.trackedJourney; trackedJourney.end(isForciblyEnded); Persistence.trackedJourneys.updateField(trackedJourney.id, TrackedJourney.END_TIME_FIELD_NAME, trackedJourney.endTime); Persistence.trackedJourneys.updateField(trackedJourney.id, TrackedJourney.END_CONDITION_FIELD_NAME, trackedJourney.endCondition); - // Provide response. return new EndTrackingResponse( TripInstruction.NO_INSTRUCTION, TripStatus.ENDED.name() ); + } + /** + * Cancel bus notifications which will not be fulfilled. + */ + private static void cancelBusNotification(TravelerPosition travelerPosition, Itinerary itinerary) { + Leg firstLegOfTrip = getFirstLeg(itinerary); + Leg busLeg = getLegToCancel(travelerPosition, firstLegOfTrip); + BusOperatorActions + .getDefault() + .handleCancelNotificationAction(travelerPosition, busLeg); + } + + /** + * If the traveler is still on the first leg of their trip and bus notification has been sent, cancel notification + * related to this first leg. If the traveler is passed the first leg, cancel notification related to the next leg. + */ + public static Leg getLegToCancel(TravelerPosition travelerPosition, Leg firstLegOfTrip) { + if (legsMatch(travelerPosition.expectedLeg, firstLegOfTrip) && isBusLeg(travelerPosition.expectedLeg)) { + var routeId = getRouteIdFromLeg(travelerPosition.expectedLeg); + if (routeId != null && travelerPosition.trackedJourney.busNotificationMessages.containsKey(routeId)) { + return firstLegOfTrip; + } + } + return travelerPosition.nextLeg; } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 97f387a9..a4d511a2 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -57,20 +57,20 @@ public static String getInstruction( ) { if (hasRequiredWalkLeg(travelerPosition)) { if (hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip, tripStatus); + TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); if (tripInstruction != null) { return tripInstruction.build(); } } if (tripStatus.equals(TripStatus.DEVIATED)) { - TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip, tripStatus); + TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip); if (tripInstruction != null) { return tripInstruction.build(); } } } else if (hasRequiredTransitLeg(travelerPosition) && hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition); + TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition, isStartOfTrip); if (tripInstruction != null) { return tripInstruction.build(); } @@ -111,10 +111,9 @@ private static boolean hasRequiredTripStatus(TripStatus tripStatus) { @Nullable private static TripInstruction getBackOnTrack( TravelerPosition travelerPosition, - boolean isStartOfTrip, - TripStatus tripStatus + boolean isStartOfTrip ) { - TripInstruction instruction = alignTravelerToTrip(travelerPosition, isStartOfTrip, tripStatus); + TripInstruction instruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); if (instruction != null && instruction.hasInstruction()) { return instruction; } @@ -130,13 +129,12 @@ private static TripInstruction getBackOnTrack( @Nullable public static TripInstruction alignTravelerToTrip( TravelerPosition travelerPosition, - boolean isStartOfTrip, - TripStatus tripStatus + boolean isStartOfTrip ) { Locale locale = travelerPosition.locale; if (isApproachingEndOfLeg(travelerPosition)) { - if (sendBusNotification(travelerPosition, isStartOfTrip, tripStatus)) { + if (sendBusNotification(travelerPosition, isStartOfTrip)) { // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. return new WaitForTransitInstruction(travelerPosition.nextLeg, travelerPosition.currentTime, locale); } @@ -159,13 +157,13 @@ public static TripInstruction alignTravelerToTrip( */ public static boolean sendBusNotification( TravelerPosition travelerPosition, - boolean isStartOfTrip, - TripStatus tripStatus + boolean isStartOfTrip ) { - if (shouldNotifyBusOperator(travelerPosition, isStartOfTrip)) { + Leg busLeg = (isStartOfTrip) ? travelerPosition.expectedLeg : travelerPosition.nextLeg; + if (shouldNotifyBusOperator(travelerPosition, busLeg)) { BusOperatorActions .getDefault() - .handleSendNotificationAction(tripStatus, travelerPosition); + .handleSendNotificationAction(travelerPosition, busLeg); return true; } return false; @@ -174,21 +172,37 @@ public static boolean sendBusNotification( /** * 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); + public static boolean shouldNotifyBusOperator(TravelerPosition travelerPosition, Leg busLeg) { + return isBusLeg(busLeg) && isWithinOperationalNotifyWindow(travelerPosition.currentTime, busLeg); + } + + /** + * A trip which starts with a transit leg. + */ + private static boolean tripStartsWithTransitLeg(TravelerPosition travelerPosition, boolean isStartOfTrip) { + return isStartOfTrip && travelerPosition.expectedLeg.transitLeg; } /** * Align the traveler's position to the nearest transit stop or destination. */ @Nullable - public static TripInstruction alignTravelerToTransitTrip(TravelerPosition travelerPosition) { + public static TripInstruction alignTravelerToTransitTrip( + TravelerPosition travelerPosition, + boolean isStartOfTrip + ) { Locale locale = travelerPosition.locale; Leg expectedLeg = travelerPosition.expectedLeg; String finalStop = expectedLeg.to.name; + if ( + tripStartsWithTransitLeg(travelerPosition, isStartOfTrip) && + sendBusNotification(travelerPosition, isStartOfTrip) + ) { + // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. + return new WaitForTransitInstruction(expectedLeg, travelerPosition.currentTime, locale); + } + if (isApproachingEndOfLeg(travelerPosition)) { return new GetOffHereTransitInstruction(finalStop, locale); } @@ -241,10 +255,6 @@ 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. diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index ae876224..9230d31c 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -89,6 +89,13 @@ public TravelerPosition(Leg expectedLeg, Leg nextLeg, Instant currentTime) { this.currentTime = currentTime; } + /** Used for unit testing. */ + public TravelerPosition(Leg expectedLeg, Leg nextLeg, TrackedJourney trackedJourney) { + this.expectedLeg = expectedLeg; + this.nextLeg = nextLeg; + this.trackedJourney = trackedJourney; + } + /** Computes the current deviation in meters from the expected itinerary. */ public double getDeviationMeters() { return getDistanceFromLine(legSegmentFromPosition.start, legSegmentFromPosition.end, currentPosition); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java index 35502e54..23828563 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java @@ -1,8 +1,8 @@ package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; import com.fasterxml.jackson.databind.JsonNode; +import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.triptracker.TravelerPosition; -import org.opentripplanner.middleware.triptracker.TripStatus; import org.opentripplanner.middleware.utils.JsonUtils; import org.opentripplanner.middleware.utils.YamlUtils; import org.slf4j.Logger; @@ -48,8 +48,8 @@ public BusOperatorActions(List agencyActions) { /** * Get the action that matches the given agency id. */ - public AgencyAction getAgencyAction(TravelerPosition travelerPosition) { - String agencyId = removeAgencyPrefix(getAgencyIdFromLeg(travelerPosition.nextLeg)); + public AgencyAction getAgencyAction(Leg busLeg) { + String agencyId = removeAgencyPrefix(getAgencyIdFromLeg(busLeg)); if (agencyId != null) { for (AgencyAction agencyAction : agencyActions) { if (agencyAction.agencyId.equalsIgnoreCase(agencyId)) { @@ -63,12 +63,12 @@ public AgencyAction getAgencyAction(TravelerPosition travelerPosition) { /** * Get the correct action for agency and send notification. */ - public void handleSendNotificationAction(TripStatus tripStatus, TravelerPosition travelerPosition) { - AgencyAction action = getAgencyAction(travelerPosition); + public void handleSendNotificationAction(TravelerPosition travelerPosition, Leg busLeg) { + AgencyAction action = getAgencyAction(busLeg); if (action != null) { BusOperatorInteraction interaction = getBusOperatorInteraction(action); try { - interaction.sendNotification(tripStatus, travelerPosition); + interaction.sendNotification(travelerPosition, busLeg); } catch (Exception e) { LOG.error("Could not trigger class {} for agency {}", action.trigger, action.agencyId, e); throw new RuntimeException(e); @@ -79,12 +79,12 @@ public void handleSendNotificationAction(TripStatus tripStatus, TravelerPosition /** * Get the correct action for agency and cancel notification. */ - public void handleCancelNotificationAction(TravelerPosition travelerPosition) { - AgencyAction action = getAgencyAction(travelerPosition); + public void handleCancelNotificationAction(TravelerPosition travelerPosition, Leg busLeg) { + AgencyAction action = getAgencyAction(busLeg); if (action != null) { BusOperatorInteraction interaction = getBusOperatorInteraction(action); try { - interaction.cancelNotification(travelerPosition); + interaction.cancelNotification(travelerPosition, busLeg); } catch (Exception e) { LOG.error("Could not trigger class {} for agency {}", action.trigger, action.agencyId, e); throw new RuntimeException(e); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java index b73d4049..9d2e04d6 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java @@ -1,11 +1,12 @@ package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; +import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.triptracker.TravelerPosition; import org.opentripplanner.middleware.triptracker.TripStatus; public interface BusOperatorInteraction { - void sendNotification(TripStatus tripStatus, TravelerPosition travelerPosition); + void sendNotification(TravelerPosition travelerPosition, Leg busLeg); - void cancelNotification(TravelerPosition travelerPosition); + void cancelNotification(TravelerPosition travelerPosition, Leg busLeg); } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java index dd1f1936..cda86e01 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java @@ -4,8 +4,8 @@ import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.triptracker.TravelerPosition; -import org.opentripplanner.middleware.triptracker.TripStatus; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; @@ -79,8 +79,8 @@ private static List getBusOperatorNotifierQualifyingRoutes() { /** * Stage notification to bus operator by making sure all required conditions are met. */ - public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPosition) { - var routeId = getRouteIdFromLeg(travelerPosition.nextLeg); + public void sendNotification(TravelerPosition travelerPosition, Leg busLeg) { + var routeId = getRouteIdFromLeg(busLeg); try { if ( hasNotSentNotificationForRoute(travelerPosition.trackedJourney, routeId) && @@ -98,13 +98,13 @@ public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPos } /** - * Cancel a previously sent notification for the next bus leg. + * Cancel a previously sent notification for the expected or next leg. */ - public void cancelNotification(TravelerPosition travelerPosition) { - var routeId = getRouteIdFromLeg(travelerPosition.nextLeg); + public void cancelNotification(TravelerPosition travelerPosition, Leg busLeg) { + var routeId = getRouteIdFromLeg(busLeg); try { if ( - isBusLeg(travelerPosition.nextLeg) && routeId != null && + isBusLeg(busLeg) && routeId != null && hasNotCanceledNotificationForRoute(travelerPosition.trackedJourney, routeId) ) { Map busNotificationRequests = travelerPosition.trackedJourney.busNotificationMessages; diff --git a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java index 79a4a4a6..be3fbe4f 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java @@ -178,7 +178,7 @@ private static boolean isAfterServiceStart(ZonedDateTime time) { /** * Check whether a new leg of an itinerary matches the previous itinerary leg for the purposes of trip monitoring. */ - private static boolean legsMatch(Leg referenceItineraryLeg, Leg candidateItineraryLeg) { + public static boolean legsMatch(Leg referenceItineraryLeg, Leg candidateItineraryLeg) { // for now don't analyze non-transit legs if (!referenceItineraryLeg.transitLeg) return true; @@ -360,4 +360,15 @@ public static String getStopIdFromPlace(Place place) { public static String getRouteShortNameFromLeg(Leg leg) { return leg.routeShortName; } + + /** + * Get the first leg in an itinerary. + */ + public static Leg getFirstLeg(Itinerary itinerary) { + if (itinerary != null && itinerary.legs != null && !itinerary.legs.isEmpty()) { + return itinerary.legs.get(0); + } + return null; + } + } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index 804ed383..e1bd5eb5 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -37,11 +37,14 @@ import static org.opentripplanner.middleware.triptracker.TravelerLocator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; import static org.opentripplanner.middleware.triptracker.TravelerLocator.getBusDepartureTime; import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.getNotificationMessage; +import static org.opentripplanner.middleware.utils.ItineraryUtils.legsMatch; class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { private static Itinerary walkToBusTransition; + private static Itinerary firstLegBusTransit; + private static TrackedJourney trackedJourney; private static final String routeId = "GwinnettCountyTransit:40"; @@ -54,11 +57,15 @@ class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { @BeforeAll public static void setUp() throws IOException { - // This itinerary is from OTP2 and has been modified to work with OTP1 to avoid breaking changes. + // These itineraries are from OTP2 and have been modified to work with OTP1 to avoid breaking changes. walkToBusTransition = JsonUtils.getPOJOFromJSON( CommonTestUtils.getTestResourceAsString("controllers/api/walk-to-bus-transition.json"), Itinerary.class ); + firstLegBusTransit = JsonUtils.getPOJOFromJSON( + CommonTestUtils.getTestResourceAsString("controllers/api/first-leg-transit.json"), + Itinerary.class + ); UsRideGwinnettNotifyBusOperator.IS_TEST = true; UsRideGwinnettNotifyBusOperator.US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES = List.of(routeId); } @@ -70,17 +77,60 @@ public void tearDown() { } } - @Test - void canNotifyBusOperatorForScheduledDeparture() { - Leg busLeg = walkToBusTransition.legs.get(1); + @ParameterizedTest + @MethodSource("creatNotifyBusOperatorForScheduledDepartureTrace") + void canNotifyBusOperatorForScheduledDeparture(Leg busLeg, Itinerary itinerary, boolean isStartOfTrip, String message) { + Coordinates startOfTransitCoordinates = new Coordinates(busLeg.from); Instant busDepartureTime = getBusDepartureTime(busLeg); - trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates(), busDepartureTime); - TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); + trackedJourney = createAndPersistTrackedJourney(startOfTransitCoordinates, busDepartureTime); + TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, itinerary, createOtpUser()); + String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, isStartOfTrip); TripInstruction expectInstruction = new WaitForTransitInstruction(busLeg, busDepartureTime, locale); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); - assertEquals(expectInstruction.build(), tripInstruction); + assertEquals(expectInstruction.build(), tripInstruction, message); + } + + private static Stream creatNotifyBusOperatorForScheduledDepartureTrace() { + return Stream.of( + Arguments.of( + firstLegBusTransit.legs.get(0), + firstLegBusTransit, + true, + "Can notify bus operator when the first leg is transit." + ), + Arguments.of( + walkToBusTransition.legs.get(1), + walkToBusTransition, + false, + "Can notify bus operator when the next leg is transit." + ) + ); + } + + @ParameterizedTest + @MethodSource("creatGetCorrectLegToCancelNotificationTrace") + void canGetCorrectLegToCancelNotification(Leg expected, Leg next, String message) { + Leg first = firstLegBusTransit.legs.get(0); + TrackedJourney journey = new TrackedJourney(); + journey.busNotificationMessages.put("GwinnettCountyTransit:40", "{\"msg_type\": 1}"); + TravelerPosition travelerPosition = new TravelerPosition(expected, next, journey); + assertTrue(legsMatch(expected, ManageTripTracking.getLegToCancel(travelerPosition, first)), message); + } + + private static Stream creatGetCorrectLegToCancelNotificationTrace() { + return Stream.of( + Arguments.of( + firstLegBusTransit.legs.get(0), + firstLegBusTransit.legs.get(1), + "Should cancel notification for first leg." + ), + Arguments.of( + firstLegBusTransit.legs.get(1), + firstLegBusTransit.legs.get(1), + "Traveler is already passed the first leg, no need to cancel notification for first leg." + ) + ); } @Test @@ -107,12 +157,12 @@ void canCancelBusOperatorNotification() throws JsonProcessingException, Interrup trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates()); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); + busOperatorActions.handleSendNotificationAction(travelerPosition, travelerPosition.nextLeg); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); assertEquals(1, getMessage(updated).msg_type); - busOperatorActions.handleCancelNotificationAction(travelerPosition); + busOperatorActions.handleCancelNotificationAction(travelerPosition, travelerPosition.nextLeg); UsRideGwinnettBusOpNotificationMessage cancelMessage1 = getMessage( Persistence.trackedJourneys.getById(trackedJourney.id) ); @@ -121,7 +171,7 @@ void canCancelBusOperatorNotification() throws JsonProcessingException, Interrup // A second request to cancel should not touch the previous request. Thread.sleep(20); - busOperatorActions.handleCancelNotificationAction(travelerPosition); + busOperatorActions.handleCancelNotificationAction(travelerPosition, travelerPosition.nextLeg); UsRideGwinnettBusOpNotificationMessage cancelMessage2 = getMessage( Persistence.trackedJourneys.getById(trackedJourney.id) ); @@ -139,7 +189,7 @@ void canNotifyBusOperatorOnlyOnce() throws InterruptedException, JsonProcessingE trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates()); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); + busOperatorActions.handleSendNotificationAction(travelerPosition, travelerPosition.nextLeg); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); assertFalse(UsRideGwinnettNotifyBusOperator.hasNotSentNotificationForRoute(updated, routeId)); @@ -147,7 +197,7 @@ void canNotifyBusOperatorOnlyOnce() throws InterruptedException, JsonProcessingE // A second request to notify the operator should not touch the previous request. Thread.sleep(20); - busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); + busOperatorActions.handleSendNotificationAction(travelerPosition, travelerPosition.nextLeg); TrackedJourney updated2 = Persistence.trackedJourneys.getById(trackedJourney.id); assertFalse(UsRideGwinnettNotifyBusOperator.hasNotSentNotificationForRoute(updated2, routeId)); assertEquals(notifyMessage.timestamp, getMessage(updated2).timestamp); @@ -156,7 +206,11 @@ void canNotifyBusOperatorOnlyOnce() throws InterruptedException, JsonProcessingE @ParameterizedTest @MethodSource("createWithinOperationalNotifyWindowTrace") void isWithinOperationalNotifyWindow(boolean expected, TravelerPosition travelerPosition, String message) { - assertEquals(expected, TravelerLocator.isWithinOperationalNotifyWindow(travelerPosition), message); + assertEquals( + expected, + TravelerLocator.isWithinOperationalNotifyWindow(travelerPosition.currentTime, travelerPosition.nextLeg), + message + ); } private static Stream createWithinOperationalNotifyWindowTrace() { @@ -190,8 +244,8 @@ private static Stream createWithinOperationalNotifyWindowTrace() { @ParameterizedTest @MethodSource("createShouldNotifyBusOperatorTrace") - void shouldNotifyBusOperator(boolean expected, TravelerPosition travelerPosition, boolean isStartOfTrip, String message) { - assertEquals(expected, TravelerLocator.shouldNotifyBusOperator(travelerPosition, isStartOfTrip), message); + void shouldNotifyBusOperator(boolean expected, TravelerPosition travelerPosition, Leg currentLeg, String message) { + assertEquals(expected, TravelerLocator.shouldNotifyBusOperator(travelerPosition, currentLeg), message); } private static Stream createShouldNotifyBusOperatorTrace() { @@ -203,25 +257,25 @@ private static Stream createShouldNotifyBusOperatorTrace() { Arguments.of( true, new TravelerPosition(busLeg, busDepartureTime), - false, + busLeg, "Traveler approaching a bus leg, should notify." ), Arguments.of( false, new TravelerPosition(walkLeg, busDepartureTime), - false, + walkLeg, "Traveler approaching a walk leg, should not notify." ), Arguments.of( true, new TravelerPosition(busLeg, null, busDepartureTime), - true, + busLeg, "Traveler at the start of a trip which starts with a bus leg, should notify." ), Arguments.of( false, new TravelerPosition(walkLeg, null, busDepartureTime), - true, + walkLeg, "Traveler at the start of a trip which starts with a walk leg, should not notify." ) ); diff --git a/src/test/resources/org/opentripplanner/middleware/controllers/api/first-leg-transit.json b/src/test/resources/org/opentripplanner/middleware/controllers/api/first-leg-transit.json new file mode 100644 index 00000000..5e9366ad --- /dev/null +++ b/src/test/resources/org/opentripplanner/middleware/controllers/api/first-leg-transit.json @@ -0,0 +1,464 @@ +{ + "accessibilityScore": null, + "duration": 545, + "endTime": 1729868831000, + "legs": [ + { + "accessibilityScore": null, + "agencyId": "GwinnettCountyTransit:GCT", + "agency": { + "alerts": [ + { + "alertDescriptionText": "Attention Route 70 Riders - Effective October 7, 2024, Route 70 will relocate bus stop # 7000 from Wisteria Drive @ Main Street to Wisteria Drive and Highway 78. ", + "alertHeaderText": "Attention Route 70 Riders - Effective October 7, 2024, Route 70 will relocate bus stop # 7000 from Wisteria Drive @ Main Street to Wisteria Drive and Highway 78. ", + "alertUrl": null, + "effectiveStartDate": 1727222400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjMzMTQ" + }, + { + "alertDescriptionText": "AM Service Alert- Route 103 at 6:30am from Sugarloaf Mills Park and Ride will be a missed trip. We apologize for the inconvenience", + "alertHeaderText": "AM Service Alert- Route 103 at 6:30am from Sugarloaf Mills Park and Ride will be a missed trip. We apologize for the inconvenience", + "alertUrl": null, + "effectiveStartDate": 1729814400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjM0ODI" + }, + { + "alertDescriptionText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertHeaderText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertUrl": null, + "effectiveStartDate": 1707350400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1ODk" + }, + { + "alertDescriptionText": "AM Service Alert- Route 101 at 7:00am from I-985 Park and Ride will be a missed trip. We apologize for the inconvenience", + "alertHeaderText": "AM Service Alert- Route 101 at 7:00am from I-985 Park and Ride will be a missed trip. We apologize for the inconvenience", + "alertUrl": null, + "effectiveStartDate": 1729814400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjM0ODM" + }, + { + "alertDescriptionText": "AM Service Alert - Route 110 outbound from Sugarloaf Mills Park and Ride at 8:10am will be a missed trip. We apologize for the inconvenience.\n", + "alertHeaderText": "AM Service Alert - Route 110 outbound from Sugarloaf Mills Park and Ride at 8:10am will be a missed trip. We apologize for the inconvenience.\n", + "alertUrl": null, + "effectiveStartDate": 1729814400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjM0ODQ" + }, + { + "alertDescriptionText": "Attention Route 40 Riders – Effective October 9, 2024 Stop # 4058 on Stone Mountain St. & Piedmont Bank (OB) has temporarily relocated to a location about 250 feet away due to construction in the area. We apologize for any inconvenience.", + "alertHeaderText": "Attention Route 40 Riders – Effective October 9, 2024 Stop # 4058 on Stone Mountain St. & Piedmont Bank (OB) has temporarily relocated to a location about 250 feet away due to construction in the area. We apologize for any inconvenience.", + "alertUrl": null, + "effectiveStartDate": 1728432000, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjMzOTA" + } + ], + "gtfsId": "GwinnettCountyTransit:GCT", + "id": "GwinnettCountyTransit:GCT", + "name": "Gwinnett County Transit", + "timezone": "America/New_York", + "url": "https://www.ridegwinnett.com/" + }, + "alerts": [ + { + "alertDescriptionText": "Attention Route 70 Riders - Effective October 7, 2024, Route 70 will relocate bus stop # 7000 from Wisteria Drive @ Main Street to Wisteria Drive and Highway 78. ", + "alertHeaderText": "Attention Route 70 Riders - Effective October 7, 2024, Route 70 will relocate bus stop # 7000 from Wisteria Drive @ Main Street to Wisteria Drive and Highway 78. ", + "alertUrl": null, + "effectiveStartDate": 1727222400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjMzMTQ" + }, + { + "alertDescriptionText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertHeaderText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertUrl": null, + "effectiveStartDate": 1707350400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1ODk" + }, + { + "alertDescriptionText": "Attention Route 40 Riders – Effective October 9, 2024 Stop # 4058 on Stone Mountain St. & Piedmont Bank (OB) has temporarily relocated to a location about 250 feet away due to construction in the area. We apologize for any inconvenience.", + "alertHeaderText": "Attention Route 40 Riders – Effective October 9, 2024 Stop # 4058 on Stone Mountain St. & Piedmont Bank (OB) has temporarily relocated to a location about 250 feet away due to construction in the area. We apologize for any inconvenience.", + "alertUrl": null, + "effectiveStartDate": 1728432000, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjMzOTA" + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 4716.45, + "dropoffType": "SCHEDULED", + "duration": 522, + "endTime": 1729868808000, + "fareProducts": [ + { + "id": "55a84984-7687-39e8-9a41-ee6e6dbc23ed", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:electronicRegular", + "medium": null, + "name": "electronicRegular", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + }, + { + "id": "01689082-2707-31ee-8d91-203eda3a29be", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:electronicYouth", + "medium": null, + "name": "electronicYouth", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + }, + { + "id": "02bf6764-589e-38fe-8b89-e07d7203237d", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:regular", + "medium": null, + "name": "regular", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + }, + { + "id": "9002d499-a1f7-3696-88de-3854ed39b883", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:electronicSenior", + "medium": null, + "name": "electronicSenior", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + }, + { + "id": "c25f4197-3483-3364-b447-f5f9adf421da", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:youth", + "medium": null, + "name": "youth", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + }, + { + "id": "889ffb31-2e8f-3c4d-bda4-df7c6add1026", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:senior", + "medium": null, + "name": "senior", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + }, + { + "id": "bb9d929b-5ce6-3bcf-8b38-6eaf4eaa2c22", + "product": { + "__typename": "DefaultFareProduct", + "id": "atlanta:electronicSpecial", + "medium": null, + "name": "electronicSpecial", + "riderCategory": null, + "price": { + "amount": 0, + "currency": { + "code": "USD", + "digits": 2 + } + } + } + } + ], + "from": { + "lat": 33.916913, + "lon": -84.22609, + "name": "Best Friend Rd & Nancy Hanks Dr OB", + "rentalVehicle": null, + "stop": { + "alerts": [], + "code": "115", + "gtfsId": "GwinnettCountyTransit:115", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTE1", + "lat": 33.916913, + "lon": -84.22609 + }, + "vertexType": "TRANSIT" + }, + "headsign": "Doraville MARTA", + "interlineWithPreviousLeg": false, + "intermediateStops": [ + { + "lat": 33.916055, + "locationType": "STOP", + "lon": -84.229996, + "name": "Best Friend Rd & Skyland Ct OB", + "stopCode": "119", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTE5" + }, + { + "lat": 33.915618, + "locationType": "STOP", + "lon": -84.233462, + "name": "Best Friend Rd & Royal Palm Ct OB", + "stopCode": "117", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTE3" + }, + { + "lat": 33.913848, + "locationType": "STOP", + "lon": -84.236614, + "name": "Best Friend Rd & Button Gwinnett Dr OB", + "stopCode": "318", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzE4" + }, + { + "lat": 33.914705, + "locationType": "STOP", + "lon": -84.238103, + "name": "Button Gwinnett & TOPSA", + "stopCode": "880", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6ODgw" + }, + { + "lat": 33.917448, + "locationType": "STOP", + "lon": -84.240083, + "name": "Button Gwinnett Dr & Mimms Dr", + "stopCode": "122", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTIy" + }, + { + "lat": 33.919495, + "locationType": "STOP", + "lon": -84.241523, + "name": "Button Gwinnett Dr & RUSH Truck Ctr", + "stopCode": "278", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6Mjc4" + }, + { + "lat": 33.924089, + "locationType": "STOP", + "lon": -84.244794, + "name": "Button Gwinnett Rd & Ryder", + "stopCode": "280", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6Mjgw" + }, + { + "lat": 33.925893, + "locationType": "STOP", + "lon": -84.247838, + "name": "Buford Hwy & Jones Mills Rd", + "stopCode": "191", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTkx" + }, + { + "lat": 33.923945, + "locationType": "STOP", + "lon": -84.250991, + "name": "Buford Hwy & Amwiler OB", + "stopCode": "180", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTgw" + }, + { + "lat": 33.922996, + "locationType": "STOP", + "lon": -84.25242, + "name": "Buford Hwy & East Lake Dr (across) OB", + "stopCode": "186", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTg2" + }, + { + "lat": 33.920743, + "locationType": "STOP", + "lon": -84.255512, + "name": "Buford Hwy & Steve Dr OB", + "stopCode": "202", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MjAy" + }, + { + "lat": 33.919256, + "locationType": "STOP", + "lon": -84.257175, + "name": "Buford Hwy at Global Forum OB", + "stopCode": "215", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MjE1" + }, + { + "lat": 33.917962, + "locationType": "STOP", + "lon": -84.258413, + "name": "Buford Hwy & Reyes Auto OB", + "stopCode": "225", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MjI1" + } + ], + "legGeometry": { + "length": 54, + "points": "kk_nEzkaaO?@hDnW????pAnKDdH??DlBt@jChHlJ??@@FH|ArBwGbEA@??a@V_OnJC@??w@f@_JtFA@??uVjOwC~B?B??oDvGkFvDx@lB@B??fCrFxFbK??@@h@|@rCzE??LP`FhIpEhG??@@dHjI??@@zFxF????jTlS" + }, + "mode": "BUS", + "pickupBookingInfo": null, + "pickupType": "SCHEDULED", + "realTime": false, + "realtimeState": null, + "rentedBike": null, + "rideHailingEstimate": null, + "routeId": "GwinnettCountyTransit:40", + "route": { + "alerts": [], + "color": "00BCF2", + "gtfsId": "GwinnettCountyTransit:20", + "id": "GwinnettCountyTransit:20", + "longName": "Beaver Ruin - Doraville", + "shortName": "20", + "textColor": "000000", + "type": 3 + }, + "startTime": 1729868286000, + "steps": [], + "to": { + "lat": 33.914541, + "lon": -84.261696, + "name": "Buford Hwy & Andy Glass OB", + "rentalVehicle": null, + "stop": { + "alerts": [], + "code": "184", + "gtfsId": "GwinnettCountyTransit:184", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTg0", + "lat": 33.914541, + "lon": -84.261696 + }, + "vertexType": "TRANSIT" + }, + "transitLeg": true, + "trip": { + "arrivalStoptime": { + "stop": { + "gtfsId": "GwinnettCountyTransit:6", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6Ng" + }, + "stopPosition": 3060 + }, + "departureStoptime": { + "stop": { + "gtfsId": "GwinnettCountyTransit:7", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6Nw" + }, + "stopPosition": 0 + }, + "gtfsId": "GwinnettCountyTransit:t3FC-bCB-sl6", + "id": "VHJpcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6dDNGQy1iQ0Itc2w2" + } + }, + { + "accessibilityScore": null, + "agency": null, + "alerts": [], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 25.22, + "dropoffType": "SCHEDULED", + "duration": 23, + "endTime": 1729868831000, + "fareProducts": [], + "from": { + "lat": 33.914541, + "lon": -84.261696, + "name": "Buford Hwy & Andy Glass OB", + "rentalVehicle": null, + "stop": { + "alerts": [], + "code": "184", + "gtfsId": "GwinnettCountyTransit:184", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTg0", + "lat": 33.914541, + "lon": -84.261696 + }, + "vertexType": "TRANSIT" + }, + "headsign": null, + "interlineWithPreviousLeg": false, + "intermediateStops": null, + "legGeometry": { + "length": 3, + "points": "{|~mErjhaO@Ac@a@" + }, + "mode": "WALK", + "pickupBookingInfo": null, + "pickupType": "SCHEDULED", + "realTime": false, + "realtimeState": null, + "rentedBike": false, + "rideHailingEstimate": null, + "route": null, + "startTime": 1729868808000, + "steps": [ + { + "absoluteDirection": "NORTHEAST", + "alerts": [], + "area": false, + "distance": 25.22, + "elevationProfile": [], + "lat": 33.9145331, + "lon": -84.2616838, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "Buford Highway☆☆☆ tmp r0.384 l25.221" + } + ], + "to": { + "lat": 33.9150078, + "lon": -84.2619698, + "name": "6020 Buford Highway, Doraville, GA, USA", + "rentalVehicle": null, + "stop": null, + "vertexType": "NORMAL" + }, + "transitLeg": false, + "trip": null + } + ], + "startTime": 1729868286000, + "transfers": 0, + "waitingTime": 0, + "walkTime": 23 +} \ No newline at end of file From 188f531d7545dd4bc91f9bc07c00c81cca287117 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 30 Oct 2024 13:50:43 +0000 Subject: [PATCH 3/7] Update src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java Co-authored-by: Binh Dam <56846598+binh-dam-ibigroup@users.noreply.github.com> --- .../middleware/triptracker/ManageTripTracking.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 15aaf050..94845c4a 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -164,7 +164,7 @@ private static EndTrackingResponse completeJourney(TripTrackingData tripData, bo } /** - * Cancel bus notifications which will not be fulfilled. + * Cancel bus notifications which are no longer needed/relevant. */ private static void cancelBusNotification(TravelerPosition travelerPosition, Itinerary itinerary) { Leg firstLegOfTrip = getFirstLeg(itinerary); From 2b3e003e0d164c9862ba88180056d97d40f1058e Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 1 Nov 2024 14:24:08 +0000 Subject: [PATCH 4/7] refactor(Update to work with first leg of trip): --- .../triptracker/ManageTripTracking.java | 46 ++++++++++------ .../triptracker/TravelerLocator.java | 49 ++++++++--------- .../triptracker/TravelerPosition.java | 10 +++- .../triptracker/NotifyBusOperatorTest.java | 53 ++++++++----------- 4 files changed, 81 insertions(+), 77 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 94845c4a..f7259396 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -2,7 +2,6 @@ import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; -import org.opentripplanner.middleware.otp.response.Itinerary; import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.triptracker.instruction.TripInstruction; @@ -11,8 +10,8 @@ import org.opentripplanner.middleware.triptracker.response.TrackingResponse; import spark.Request; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.isAtStartOfLeg; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; -import static org.opentripplanner.middleware.utils.ItineraryUtils.getFirstLeg; import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteGtfsIdFromLeg; import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; import static org.opentripplanner.middleware.utils.ItineraryUtils.legsMatch; @@ -151,7 +150,7 @@ private static EndTrackingResponse completeJourney(TripTrackingData tripData, bo tripData.trip.journeyState.matchingItinerary, Persistence.otpUsers.getById(tripData.trip.userId) ); - cancelBusNotification(travelerPosition, tripData.trip.journeyState.matchingItinerary); + cancelBusNotification(travelerPosition); TrackedJourney trackedJourney = travelerPosition.trackedJourney; trackedJourney.end(isForciblyEnded); Persistence.trackedJourneys.updateField(trackedJourney.id, TrackedJourney.END_TIME_FIELD_NAME, trackedJourney.endTime); @@ -166,25 +165,38 @@ private static EndTrackingResponse completeJourney(TripTrackingData tripData, bo /** * Cancel bus notifications which are no longer needed/relevant. */ - private static void cancelBusNotification(TravelerPosition travelerPosition, Itinerary itinerary) { - Leg firstLegOfTrip = getFirstLeg(itinerary); - Leg busLeg = getLegToCancel(travelerPosition, firstLegOfTrip); + private static void cancelBusNotification(TravelerPosition travelerPosition) { + Leg busLeg = travelerPosition.nextLeg; + if (shouldCancelBusNotificationForStartOfTrip(travelerPosition)) { + busLeg = travelerPosition.expectedLeg; + } BusOperatorActions .getDefault() .handleCancelNotificationAction(travelerPosition, busLeg); } /** - * If the traveler is still on the first leg of their trip and bus notification has been sent, cancel notification - * related to this first leg. If the traveler is passed the first leg, cancel notification related to the next leg. + * Traveler is still waiting to board the bus at the start of a trip and notification has been sent. */ - public static Leg getLegToCancel(TravelerPosition travelerPosition, Leg firstLegOfTrip) { - if (legsMatch(travelerPosition.expectedLeg, firstLegOfTrip) && isBusLeg(travelerPosition.expectedLeg)) { - var routeId = getRouteGtfsIdFromLeg(travelerPosition.expectedLeg); - if (routeId != null && travelerPosition.trackedJourney.busNotificationMessages.containsKey(routeId)) { - return firstLegOfTrip; - } - } - return travelerPosition.nextLeg; + public static boolean shouldCancelBusNotificationForStartOfTrip(TravelerPosition travelerPosition) { + return hasSentBusNotificationForStartOfTrip(travelerPosition) && isWaitingForBusAtStartOfTrip(travelerPosition); + } + + /** + * Bus notification has been sent for the start of the trip. + */ + private static boolean hasSentBusNotificationForStartOfTrip(TravelerPosition travelerPosition) { + var routeId = getRouteGtfsIdFromLeg(travelerPosition.expectedLeg); + return routeId != null && travelerPosition.trackedJourney.busNotificationMessages.containsKey(routeId); + } + + /** + * Traveler is waiting for a bus at the start of a trip. + */ + private static boolean isWaitingForBusAtStartOfTrip(TravelerPosition travelerPosition) { + return + legsMatch(travelerPosition.expectedLeg, travelerPosition.firstLegOfTrip) && + isBusLeg(travelerPosition.expectedLeg) && + isAtStartOfLeg(travelerPosition); } -} +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index a4d511a2..46dfa7c1 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -33,6 +33,7 @@ import static org.opentripplanner.middleware.utils.GeometryUtils.getDistance; import static org.opentripplanner.middleware.utils.GeometryUtils.isPointBetween; import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.legsMatch; /** * Locate the traveler in relation to the nearest step or destination and provide the appropriate instructions. @@ -70,7 +71,7 @@ public static String getInstruction( } } } else if (hasRequiredTransitLeg(travelerPosition) && hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition, isStartOfTrip); + TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition); if (tripInstruction != null) { return tripInstruction.build(); } @@ -134,7 +135,7 @@ public static TripInstruction alignTravelerToTrip( Locale locale = travelerPosition.locale; if (isApproachingEndOfLeg(travelerPosition)) { - if (sendBusNotification(travelerPosition, isStartOfTrip)) { + if (sendBusNotification(travelerPosition)) { // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. return new WaitForTransitInstruction(travelerPosition.nextLeg, travelerPosition.currentTime, locale); } @@ -155,12 +156,9 @@ public static TripInstruction alignTravelerToTrip( /** * 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 - ) { - Leg busLeg = (isStartOfTrip) ? travelerPosition.expectedLeg : travelerPosition.nextLeg; - if (shouldNotifyBusOperator(travelerPosition, busLeg)) { + public static boolean sendBusNotification(TravelerPosition travelerPosition) { + Leg busLeg = atStartOfTransitTrip(travelerPosition) ? travelerPosition.expectedLeg : travelerPosition.nextLeg; + if (isBusLeg(busLeg) && isWithinOperationalNotifyWindow(travelerPosition.currentTime, busLeg)) { BusOperatorActions .getDefault() .handleSendNotificationAction(travelerPosition, busLeg); @@ -170,35 +168,26 @@ public static boolean sendBusNotification( } /** - * Given the traveler's position and leg type, check if bus notification should be sent. - */ - public static boolean shouldNotifyBusOperator(TravelerPosition travelerPosition, Leg busLeg) { - return isBusLeg(busLeg) && isWithinOperationalNotifyWindow(travelerPosition.currentTime, busLeg); - } - - /** - * A trip which starts with a transit leg. + * A trip which starts with a transit leg and the traveler is on that leg. */ - private static boolean tripStartsWithTransitLeg(TravelerPosition travelerPosition, boolean isStartOfTrip) { - return isStartOfTrip && travelerPosition.expectedLeg.transitLeg; + private static boolean atStartOfTransitTrip(TravelerPosition travelerPosition) { + return + travelerPosition.expectedLeg != null && + travelerPosition.firstLegOfTrip != null && + travelerPosition.firstLegOfTrip.transitLeg && + legsMatch(travelerPosition.expectedLeg, travelerPosition.firstLegOfTrip); } /** * Align the traveler's position to the nearest transit stop or destination. */ @Nullable - public static TripInstruction alignTravelerToTransitTrip( - TravelerPosition travelerPosition, - boolean isStartOfTrip - ) { + public static TripInstruction alignTravelerToTransitTrip(TravelerPosition travelerPosition) { Locale locale = travelerPosition.locale; Leg expectedLeg = travelerPosition.expectedLeg; String finalStop = expectedLeg.to.name; - if ( - tripStartsWithTransitLeg(travelerPosition, isStartOfTrip) && - sendBusNotification(travelerPosition, isStartOfTrip) - ) { + if (sendBusNotification(travelerPosition)) { // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. return new WaitForTransitInstruction(expectedLeg, travelerPosition.currentTime, locale); } @@ -248,6 +237,14 @@ private static boolean isApproachingEndOfLeg(TravelerPosition travelerPosition) return getDistanceToEndOfLeg(travelerPosition) <= TRIP_INSTRUCTION_UPCOMING_RADIUS; } + /** + * Is the traveler at the start of a leg. + */ + public static boolean isAtStartOfLeg(TravelerPosition travelerPosition) { + Coordinates legDestination = new Coordinates(travelerPosition.expectedLeg.from); + return getDistance(travelerPosition.currentPosition, legDestination) <= TRIP_INSTRUCTION_UPCOMING_RADIUS; + } + /** * Is the traveler at the leg destination. */ diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 9230d31c..0328ae84 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -14,6 +14,7 @@ import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getNextLeg; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getSegmentFromPosition; import static org.opentripplanner.middleware.utils.GeometryUtils.getDistanceFromLine; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getFirstLeg; public class TravelerPosition { @@ -44,6 +45,9 @@ public class TravelerPosition { /** The traveler's locale. */ public Locale locale; + /** The first leg of the trip. **/ + public Leg firstLegOfTrip; + public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpUser otpUser) { TrackingLocation lastLocation = trackedJourney.locations.get(trackedJourney.locations.size() - 1); currentTime = lastLocation.timestamp.toInstant(); @@ -61,6 +65,7 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU } this.locale = I18nUtils.getOtpUserLocale(otpUser); } + firstLegOfTrip = getFirstLeg(itinerary); } /** Used for unit testing. */ @@ -90,10 +95,11 @@ public TravelerPosition(Leg expectedLeg, Leg nextLeg, Instant currentTime) { } /** Used for unit testing. */ - public TravelerPosition(Leg expectedLeg, Leg nextLeg, TrackedJourney trackedJourney) { + public TravelerPosition(Leg expectedLeg, TrackedJourney trackedJourney, Leg first, Coordinates currentPosition) { this.expectedLeg = expectedLeg; - this.nextLeg = nextLeg; this.trackedJourney = trackedJourney; + this.firstLegOfTrip = first; + this.currentPosition = currentPosition; } /** Computes the current deviation in meters from the expected itinerary. */ diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index 311df819..ec81deb6 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -37,7 +37,6 @@ import static org.opentripplanner.middleware.triptracker.TravelerLocator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; import static org.opentripplanner.middleware.triptracker.TravelerLocator.getBusDepartureTime; import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.getNotificationMessage; -import static org.opentripplanner.middleware.utils.ItineraryUtils.legsMatch; class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { @@ -109,26 +108,31 @@ private static Stream creatNotifyBusOperatorForScheduledDepartureTrac } @ParameterizedTest - @MethodSource("creatGetCorrectLegToCancelNotificationTrace") - void canGetCorrectLegToCancelNotification(Leg expected, Leg next, String message) { + @MethodSource("shouldCancelBusNotificationForStartOfTripTrace") + void shouldCancelBusNotificationForStartOfTrip(boolean expected, Leg expectedLeg, Coordinates currentPosition, String message) { Leg first = firstLegBusTransit.legs.get(0); TrackedJourney journey = new TrackedJourney(); journey.busNotificationMessages.put(routeId, "{\"msg_type\": 1}"); - TravelerPosition travelerPosition = new TravelerPosition(expected, next, journey); - assertTrue(legsMatch(expected, ManageTripTracking.getLegToCancel(travelerPosition, first)), message); + TravelerPosition travelerPosition = new TravelerPosition(expectedLeg, journey, first, currentPosition); + assertEquals(expected, ManageTripTracking.shouldCancelBusNotificationForStartOfTrip(travelerPosition), message); } - private static Stream creatGetCorrectLegToCancelNotificationTrace() { + private static Stream shouldCancelBusNotificationForStartOfTripTrace() { + Leg first = firstLegBusTransit.legs.get(0); + Coordinates atStartOfBusJourney = new Coordinates(first.from); + Coordinates atEndOfBusJourney = new Coordinates(first.to); return Stream.of( Arguments.of( + true, firstLegBusTransit.legs.get(0), - firstLegBusTransit.legs.get(1), - "Should cancel notification for first leg." + atStartOfBusJourney, + "Still waiting for bus, should cancel notification." ), Arguments.of( + false, firstLegBusTransit.legs.get(1), - firstLegBusTransit.legs.get(1), - "Traveler has passed the first leg, no need to cancel notification for first leg." + atEndOfBusJourney, + "Already on the bus, no need to cancel notification." ) ); } @@ -243,39 +247,24 @@ private static Stream createWithinOperationalNotifyWindowTrace() { } @ParameterizedTest - @MethodSource("createShouldNotifyBusOperatorTrace") - void shouldNotifyBusOperator(boolean expected, TravelerPosition travelerPosition, Leg currentLeg, String message) { - assertEquals(expected, TravelerLocator.shouldNotifyBusOperator(travelerPosition, currentLeg), message); + @MethodSource("shouldSendBusNotificationAtStartOfTripTrace") + void shouldSendBusNotificationAtStartOfTrip(boolean expected, TravelerPosition travelerPosition, String message) { + assertEquals(expected, TravelerLocator.sendBusNotification(travelerPosition), message); } - private static Stream createShouldNotifyBusOperatorTrace() { + private static Stream shouldSendBusNotificationAtStartOfTripTrace() { + var busLeg = firstLegBusTransit.legs.get(0); 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), - busLeg, - "Traveler approaching a bus leg, should notify." - ), - Arguments.of( - false, - new TravelerPosition(walkLeg, busDepartureTime), - walkLeg, - "Traveler approaching a walk leg, should not notify." - ), - Arguments.of( - true, - new TravelerPosition(busLeg, null, busDepartureTime), - busLeg, + new TravelerPosition(busLeg, getBusDepartureTime(busLeg)), "Traveler at the start of a trip which starts with a bus leg, should notify." ), Arguments.of( false, - new TravelerPosition(walkLeg, null, busDepartureTime), - walkLeg, + new TravelerPosition(walkLeg, getBusDepartureTime(walkLeg)), "Traveler at the start of a trip which starts with a walk leg, should not notify." ) ); From 820584f5b733342026804f975bf40580e79e8d13 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 1 Nov 2024 14:32:13 +0000 Subject: [PATCH 5/7] refactor(TravelerPosition.java): Removed redundant constructor --- .../middleware/triptracker/TravelerPosition.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 0328ae84..c51966fa 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -84,12 +84,6 @@ 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; } From eeea56917235f4b7c431e1b58937d23c242c334f Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 4 Nov 2024 15:37:04 +0000 Subject: [PATCH 6/7] refactor(Update to provided deviated instruction for trip starting with bus leg): --- .../triptracker/TravelerLocator.java | 47 +++++++++++++++---- .../triptracker/TravelerPosition.java | 7 +++ .../triptracker/ManageLegTraversalTest.java | 42 +++++++++++++++-- 3 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 46dfa7c1..95a5fa2c 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -70,10 +70,19 @@ public static String getInstruction( return tripInstruction.build(); } } - } else if (hasRequiredTransitLeg(travelerPosition) && hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition); - if (tripInstruction != null) { - return tripInstruction.build(); + } else if (hasRequiredTransitLeg(travelerPosition)) { + if (hasRequiredTripStatus(tripStatus)) { + TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition); + if (tripInstruction != null) { + return tripInstruction.build(); + } + } + + if (tripStatus.equals(TripStatus.DEVIATED)) { + TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip); + if (tripInstruction != null) { + return tripInstruction.build(); + } } } return NO_INSTRUCTION; @@ -118,10 +127,32 @@ private static TripInstruction getBackOnTrack( if (instruction != null && instruction.hasInstruction()) { return instruction; } - Step nearestStep = snapToWaypoint(travelerPosition, travelerPosition.expectedLeg.steps); - return (nearestStep != null) - ? new DeviatedInstruction(nearestStep.streetName, travelerPosition.locale) - : null; + return getDeviatedInstruction(travelerPosition); + } + + /** + * If the traveler has deviated, attempt to provide instructions to get back on track. + */ + @Nullable + private static TripInstruction getDeviatedInstruction(TravelerPosition travelerPosition) { + if (!isBusLeg(travelerPosition.expectedLeg)) { + Step nearestStep = snapToWaypoint(travelerPosition, travelerPosition.expectedLeg.steps); + return (nearestStep != null) + ? new DeviatedInstruction(nearestStep.streetName, travelerPosition.locale) + : null; + } else if (atStartOfTransitTrip(travelerPosition)) { + // Only provide instruction if at the start of a trip. + String busStopName = getBusStopName(travelerPosition.expectedLeg); + return (busStopName != null) + ? new DeviatedInstruction(busStopName, travelerPosition.locale) + : null; + } + return null; + } + + @Nullable + private static String getBusStopName(Leg busLeg) { + return (busLeg.from != null && busLeg.from.name != null) ? busLeg.from.name : null; } /** diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index c51966fa..2756f7e3 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -82,6 +82,13 @@ public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { this(expectedLeg, currentPosition, 0); } + /** Used for unit testing. */ + public TravelerPosition(Leg expectedLeg, Coordinates currentPosition, Leg firstLegOfTrip) { + // Anywhere the speed is zero means that speed is not considered for a specific logic. + this(expectedLeg, currentPosition, 0); + this.firstLegOfTrip = firstLegOfTrip; + } + /** Used for unit testing. */ public TravelerPosition(Leg nextLeg, Instant currentTime) { this.nextLeg = nextLeg; diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index a643b24e..ea6d6abc 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -49,6 +49,7 @@ public class ManageLegTraversalTest { private static Itinerary adairAvenueToMonroeDriveItinerary; private static Itinerary midtownToAnsleyItinerary; private static List midtownToAnsleyIntermediateStops; + private static Itinerary firstLegBusTransit; private static final Locale locale = Locale.US; @@ -73,6 +74,10 @@ public static void setUp() throws IOException { CommonTestUtils.getTestResourceAsString("controllers/api/27nb-midtown-to-ansley.json"), Itinerary.class ); + firstLegBusTransit = JsonUtils.getPOJOFromJSON( + CommonTestUtils.getTestResourceAsString("controllers/api/first-leg-transit.json"), + Itinerary.class + ); // Hold on to the original list of intermediate stops (some tests will overwrite it) midtownToAnsleyIntermediateStops = midtownToAnsleyItinerary.legs.get(1).intermediateStops; } @@ -166,10 +171,8 @@ private static Stream createTrace() { @ParameterizedTest @MethodSource("createTurnByTurnTrace") - void canTrackTurnByTurn(TraceData traceData) { - Itinerary itinerary = adairAvenueToMonroeDriveItinerary; - Leg walkLeg = itinerary.legs.get(0); - TravelerPosition travelerPosition = new TravelerPosition(walkLeg, traceData.position); + void canTrackTurnByTurn(Leg firstLeg, TraceData traceData) { + TravelerPosition travelerPosition = new TravelerPosition(firstLeg, traceData.position, firstLeg); String tripInstruction = TravelerLocator.getInstruction(traceData.tripStatus, travelerPosition, traceData.isStartOfTrip); assertEquals(traceData.expectedInstruction, Objects.requireNonNullElse(tripInstruction, NO_INSTRUCTION), traceData.message); } @@ -185,6 +188,8 @@ private static Stream createTurnByTurnTrace() { List walkSteps = adairAvenueToMonroeDriveLeg.steps; String destinationName = adairAvenueToMonroeDriveLeg.to.name; + Leg walkLeg = adairAvenueToMonroeDriveItinerary.legs.get(0); + Step adairAvenueNortheastStep = walkSteps.get(0); Step virginiaCircleNortheastStep = walkSteps.get(1); Step ponceDeLeonPlaceNortheastStep = walkSteps.get(2); @@ -199,8 +204,23 @@ private static Stream createTurnByTurnTrace() { Coordinates pointBeforeTurn = new Coordinates(33.78151,-84.36481); Coordinates pointAfterTurn = new Coordinates(33.78165, -84.36484); + Leg firstBusLeg = firstLegBusTransit.legs.get(0); + Coordinates busStopCoords = new Coordinates(firstBusLeg.from); + String busStopName = firstBusLeg.from.name; + return Stream.of( Arguments.of( + firstBusLeg, + new TraceData( + TripStatus.DEVIATED, + createPoint(busStopCoords, 12, NORTH_WEST_BEARING), + new DeviatedInstruction(busStopName, locale).build(), + true, + "Deviated from the start of a trip which starts with a bus leg. Suggest path to head towards." + ) + ), + Arguments.of( + walkLeg, new TraceData( originCoords, new OnTrackInstruction(10, adairAvenueNortheastStep, locale).build(), @@ -209,6 +229,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( originCoords, new OnTrackInstruction(10, adairAvenueNortheastStep, locale).build(), @@ -217,6 +238,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( adairAvenueNortheastCoords, new OnTrackInstruction(2, adairAvenueNortheastStep, locale).build(), @@ -225,6 +247,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( TripStatus.DEVIATED, createPoint(adairAvenueNortheastCoords, 12, NORTH_WEST_BEARING), @@ -234,6 +257,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( TripStatus.DEVIATED, createPoint(adairAvenueNortheastCoords, 12, SOUTH_WEST_BEARING), @@ -243,6 +267,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( createPoint(virginiaCircleNortheastCoords, 12, SOUTH_WEST_BEARING), NO_INSTRUCTION, @@ -251,6 +276,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( TripStatus.DEVIATED, createPoint(virginiaCircleNortheastCoords, 8, NORTH_BEARING), @@ -260,6 +286,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( virginiaCircleNortheastCoords, new OnTrackInstruction(0, virginiaCircleNortheastStep, locale).build(), @@ -268,6 +295,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( TripStatus.DEVIATED, createPoint(ponceDeLeonPlaceNortheastCoords, 10, NORTH_WEST_BEARING), @@ -277,6 +305,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( TripStatus.DEVIATED, createPoint(ponceDeLeonPlaceNortheastCoords, 10, NORTH_EAST_BEARING), @@ -286,6 +315,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( createPoint(pointBeforeTurn, 8, calculateBearing(pointBeforeTurn, virginiaAvenuePoint)), new OnTrackInstruction(10, virginiaAvenueNortheastStep, locale).build(), @@ -294,6 +324,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( createPoint(pointBeforeTurn, 17, calculateBearing(pointBeforeTurn, virginiaAvenuePoint)), new OnTrackInstruction(2, virginiaAvenueNortheastStep, locale).build(), @@ -302,6 +333,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( createPoint(pointAfterTurn, 0, calculateBearing(pointAfterTurn, virginiaAvenuePoint)), NO_INSTRUCTION, @@ -310,6 +342,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( createPoint(destinationCoords, 8, SOUTH_BEARING), new OnTrackInstruction(10, destinationName, locale).build(), @@ -318,6 +351,7 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( + walkLeg, new TraceData( destinationCoords, new OnTrackInstruction(2, destinationName, locale).build(), From b6a853628ca90675140de1e5a62d7742ca4eb898 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 5 Nov 2024 11:00:20 +0000 Subject: [PATCH 7/7] refactor(Updated methods to use Optional): --- .../middleware/triptracker/TravelerLocator.java | 10 +++++++++- .../middleware/utils/ItineraryUtils.java | 10 +++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 95a5fa2c..e444fbe6 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION; @@ -150,9 +151,16 @@ private static TripInstruction getDeviatedInstruction(TravelerPosition travelerP return null; } + /** + * Get the bus stop name from the 'from' place name if available. + */ @Nullable private static String getBusStopName(Leg busLeg) { - return (busLeg.from != null && busLeg.from.name != null) ? busLeg.from.name : null; + return Optional + .ofNullable(busLeg) + .map(leg -> leg.from) + .map(place -> place.name) + .orElse(null); } /** diff --git a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java index ee86b608..9011c6dd 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java @@ -365,10 +365,10 @@ public static String getRouteShortNameFromLeg(Leg leg) { * Get the first leg in an itinerary. */ public static Leg getFirstLeg(Itinerary itinerary) { - if (itinerary != null && itinerary.legs != null && !itinerary.legs.isEmpty()) { - return itinerary.legs.get(0); - } - return null; + return Optional + .ofNullable(itinerary) + .map(itin -> itin.legs) + .map(legs -> legs.get(0)) + .orElse(null); } - }