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(