diff --git a/pom.xml b/pom.xml index d0131965f..26fce5f3e 100644 --- a/pom.xml +++ b/pom.xml @@ -125,6 +125,13 @@ 2.7 + + + org.apache.commons + commons-lang3 + 3.17.0 + + org.slf4j diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index e444fbe62..ce2fa62fe 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -4,6 +4,7 @@ import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.otp.response.Place; import org.opentripplanner.middleware.otp.response.Step; +import org.opentripplanner.middleware.triptracker.instruction.ContinueInstruction; import org.opentripplanner.middleware.triptracker.instruction.DeviatedInstruction; import org.opentripplanner.middleware.triptracker.instruction.GetOffHereTransitInstruction; import org.opentripplanner.middleware.triptracker.instruction.GetOffNextStopTransitInstruction; @@ -27,7 +28,9 @@ import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.IntStream; +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION; import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_IMMEDIATE_RADIUS; import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS; @@ -59,7 +62,7 @@ public static String getInstruction( ) { if (hasRequiredWalkLeg(travelerPosition)) { if (hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); + TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip, false); if (tripInstruction != null) { return tripInstruction.build(); } @@ -124,7 +127,7 @@ private static TripInstruction getBackOnTrack( TravelerPosition travelerPosition, boolean isStartOfTrip ) { - TripInstruction instruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); + TripInstruction instruction = alignTravelerToTrip(travelerPosition, isStartOfTrip, true); if (instruction != null && instruction.hasInstruction()) { return instruction; } @@ -169,7 +172,8 @@ private static String getBusStopName(Leg busLeg) { @Nullable public static TripInstruction alignTravelerToTrip( TravelerPosition travelerPosition, - boolean isStartOfTrip + boolean isStartOfTrip, + boolean travelerHasDeviated ) { Locale locale = travelerPosition.locale; @@ -182,16 +186,55 @@ public static TripInstruction alignTravelerToTrip( } Step nextStep = snapToWaypoint(travelerPosition, travelerPosition.expectedLeg.steps); + TripInstruction tripInstruction = null; if (nextStep != null && (!isPositionPastStep(travelerPosition, nextStep) || isStartOfTrip)) { - return new OnTrackInstruction( + tripInstruction = new OnTrackInstruction( getDistance(travelerPosition.currentPosition, new Coordinates(nextStep)), nextStep, locale ); } + return (travelerHasDeviated || (isNotEmpty(tripInstruction) && tripInstruction.hasInstruction())) + ? tripInstruction + : getContinueInstruction(travelerPosition, nextStep, locale); + } + + /** + * Traveler is on track, but no immediate instruction is available. Provide a "continue on street" reassurance + * instruction providing they are on a walk leg. This will be based on the current or previous step depending on the + * traveler's relative position to the next leg. + */ + private static ContinueInstruction getContinueInstruction( + TravelerPosition travelerPosition, + Step nextStep, + Locale locale + ) { + if (!travelerPosition.expectedLeg.transitLeg && nextStep != null) { + Step currentStep = isPositionPastStep(travelerPosition, nextStep) + ? nextStep : + getPreviousStep(travelerPosition.expectedLeg.steps, nextStep); + if (currentStep != null) { + return new ContinueInstruction(currentStep, locale); + } + } return null; } + /** + * Get the step prior to the next step provided. + */ + private static Step getPreviousStep(List steps, Step nextStep) { + if (steps.get(0).equals(nextStep)) { + return null; + } + Optional previousStep = IntStream + .range(0, steps.size()) + .filter(i -> steps.get(i).equals(nextStep)) + .mapToObj(i -> steps.get(i - 1)) + .findFirst(); + return previousStep.orElse(null); + } + /** * Send bus notification if the first leg is a bus leg or approaching a bus leg and within the notify window. */ diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/ContinueInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/ContinueInstruction.java new file mode 100644 index 000000000..c0f6f6915 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/ContinueInstruction.java @@ -0,0 +1,23 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Step; + +import java.util.Locale; + +import static org.apache.commons.lang3.ObjectUtils.isNotEmpty; + +public class ContinueInstruction extends SelfLegInstruction { + public ContinueInstruction(Step legStep, Locale locale) { + this.legStep = legStep; + this.locale = locale; + } + + @Override + public String build() { + if (isNotEmpty(legStep) && isNotEmpty(legStep.streetName)) { + // TODO: i18n + return String.format("Continue on %s", legStep.streetName); + } + return NO_INSTRUCTION; + } +} diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java index 0d6d95bcc..0ec06df67 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java @@ -16,6 +16,7 @@ import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.otp.response.Itinerary; import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.testutils.ApiTestUtils; import org.opentripplanner.middleware.testutils.CommonTestUtils; @@ -27,6 +28,10 @@ import org.opentripplanner.middleware.triptracker.TrackingLocation; import org.opentripplanner.middleware.triptracker.TripStatus; import org.opentripplanner.middleware.triptracker.TripTrackingData; +import org.opentripplanner.middleware.triptracker.instruction.ContinueInstruction; +import org.opentripplanner.middleware.triptracker.instruction.DeviatedInstruction; +import org.opentripplanner.middleware.triptracker.instruction.OnTrackInstruction; +import org.opentripplanner.middleware.triptracker.instruction.WaitForTransitInstruction; import org.opentripplanner.middleware.triptracker.payload.EndTrackingPayload; import org.opentripplanner.middleware.triptracker.payload.ForceEndTrackingPayload; import org.opentripplanner.middleware.triptracker.payload.StartTrackingPayload; @@ -39,10 +44,12 @@ import org.opentripplanner.middleware.utils.HttpResponseValues; import org.opentripplanner.middleware.utils.JsonUtils; +import java.time.Duration; import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -288,88 +295,105 @@ private static Stream createInstructionAndStatusCases() { final int NORTH_WEST_BEARING = 315; final int NORTH_EAST_BEARING = 45; final int WEST_BEARING = 270; + final Locale locale = Locale.US; Leg firstLeg = itinerary.legs.get(0); - Coordinates firstStepCoords = new Coordinates(firstLeg.steps.get(0)); - Coordinates thirdStepCoords = new Coordinates(firstLeg.steps.get(2)); + Step adairAvenueNortheastStep = firstLeg.steps.get(0); + Step virginiaCircleNortheastStep = firstLeg.steps.get(1); + Step ponceDeLeonPlaceNortheastStep = firstLeg.steps.get(2); + Coordinates firstStepCoords = new Coordinates(adairAvenueNortheastStep); + Coordinates thirdStepCoords = new Coordinates(ponceDeLeonPlaceNortheastStep); Coordinates destinationCoords = new Coordinates(firstLeg.to); + String monroeDrDestinationName = firstLeg.to.name; Leg multiItinFirstLeg = multiLegItinerary.legs.get(0); - Coordinates multiItinFirstLegDestCoords = new Coordinates(multiItinFirstLeg.to); Leg multiItinLastLeg = multiLegItinerary.legs.get(multiLegItinerary.legs.size() - 1); + Leg multiItinBusLeg = multiLegItinerary.legs.get(multiLegItinerary.legs.size() - 2); + Coordinates multiItinFirstLegDestCoords = new Coordinates(multiItinFirstLeg.to); Coordinates multiItinLastLegDestCoords = new Coordinates(multiItinLastLeg.to); + String ansleyMallPetShopDestinationName = multiItinLastLeg.to.name; return Stream.of( Arguments.of( monitoredTrip, createPoint(firstStepCoords, 1, NORTH_EAST_BEARING), - "IMMEDIATE: Head WEST on Adair Avenue Northeast", + new OnTrackInstruction(1, adairAvenueNortheastStep, locale).build(), TripStatus.ON_SCHEDULE, "Coords near first step should produce relevant instruction" ), Arguments.of( monitoredTrip, createPoint(firstStepCoords, 4, NORTH_EAST_BEARING), - "UPCOMING: Head WEST on Adair Avenue Northeast", + new OnTrackInstruction(4, adairAvenueNortheastStep, locale).build(), TripStatus.DEVIATED, "Coords deviated but near first step should produce relevant instruction" ), Arguments.of( monitoredTrip, createPoint(firstStepCoords, 30, NORTH_EAST_BEARING), - "Head to Adair Avenue Northeast", + new DeviatedInstruction(adairAvenueNortheastStep.streetName, locale).build(), TripStatus.DEVIATED, "Deviated coords near first step should produce instruction to head to first step #1" ), Arguments.of( monitoredTrip, createPoint(firstStepCoords, 15, NORTH_WEST_BEARING), - "Head to Adair Avenue Northeast", + new DeviatedInstruction(adairAvenueNortheastStep.streetName, locale).build(), TripStatus.DEVIATED, "Deviated coords near first step should produce instruction to head to first step #2" ), Arguments.of( monitoredTrip, createPoint(firstStepCoords, 20, WEST_BEARING), - NO_INSTRUCTION, + new ContinueInstruction(virginiaCircleNortheastStep, locale).build(), TripStatus.ON_SCHEDULE, - "Coords along a step should produce no instruction" + "Coords along a step should produce a continue on street instruction" ), Arguments.of( monitoredTrip, thirdStepCoords, - "IMMEDIATE: LEFT on Ponce de Leon Place Northeast", + new OnTrackInstruction(0, ponceDeLeonPlaceNortheastStep, locale).build(), TripStatus.AHEAD_OF_SCHEDULE, "Coords near a not-first step should produce relevant instruction" ), Arguments.of( monitoredTrip, createPoint(thirdStepCoords, 30, NORTH_WEST_BEARING), - "Head to Ponce de Leon Place Northeast", + new DeviatedInstruction(ponceDeLeonPlaceNortheastStep.streetName, locale).build(), TripStatus.DEVIATED, "Deviated coords near a not-first step should produce instruction to head to step" ), Arguments.of( monitoredTrip, createPoint(destinationCoords, 1, NORTH_WEST_BEARING), - "ARRIVED: Monroe Dr NE at Cooledge Ave NE", + new OnTrackInstruction(2, monroeDrDestinationName, locale).build(), TripStatus.COMPLETED, "Instructions for destination coordinate" ), Arguments.of( multiLegMonitoredTrip, createPoint(multiItinFirstLegDestCoords, 1.5, WEST_BEARING), - // Time is in US Pacific time zone (instead of US Eastern) by configuration for other E2E tests. - "Wait 6 minutes for your bus, route 27, scheduled at 9:18 AM, on time", + new WaitForTransitInstruction( + multiItinBusLeg, + multiItinBusLeg.getScheduledStartTime().toInstant().minus(Duration.ofMinutes(6)), + locale) + .build(), TripStatus.AHEAD_OF_SCHEDULE, "Arriving ahead of schedule to a bus stop at the end of first leg." ), Arguments.of( multiLegMonitoredTrip, createPoint(multiItinLastLegDestCoords, 1, NORTH_WEST_BEARING), - "ARRIVED: Ansley Mall Pet Shop", + new OnTrackInstruction(1, ansleyMallPetShopDestinationName, locale).build(), TripStatus.COMPLETED, "Instructions for destination coordinate of multi-leg trip" + ), + Arguments.of( + monitoredTrip, + createPoint(thirdStepCoords, 1000, NORTH_WEST_BEARING), + NO_INSTRUCTION, + TripStatus.DEVIATED, + "Deviated significantly from nearest step should produce no instruction" ) ); } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index ea6d6abc6..e3d919b4f 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -15,6 +15,7 @@ import org.opentripplanner.middleware.otp.response.Place; import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.testutils.CommonTestUtils; +import org.opentripplanner.middleware.triptracker.instruction.ContinueInstruction; import org.opentripplanner.middleware.triptracker.instruction.DeviatedInstruction; import org.opentripplanner.middleware.triptracker.instruction.OnTrackInstruction; import org.opentripplanner.middleware.utils.ConfigUtils; @@ -270,9 +271,9 @@ private static Stream createTurnByTurnTrace() { walkLeg, new TraceData( createPoint(virginiaCircleNortheastCoords, 12, SOUTH_WEST_BEARING), - NO_INSTRUCTION, + new ContinueInstruction(ponceDeLeonPlaceNortheastStep, locale).build(), false, - "On track approaching second step, but not close enough for instruction." + "On track approaching second step, provide continue instruction." ) ), Arguments.of( @@ -336,9 +337,9 @@ private static Stream createTurnByTurnTrace() { walkLeg, new TraceData( createPoint(pointAfterTurn, 0, calculateBearing(pointAfterTurn, virginiaAvenuePoint)), - NO_INSTRUCTION, + new ContinueInstruction(virginiaAvenueNortheastStep, locale).build(), false, - "After turn left on to Virginia Avenue should not produce turn instruction." + "After turn left on to Virginia Avenue should provide continue instruction." ) ), Arguments.of(