Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OTP-1458 Provide traveler with continue instruction #268

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,6 +28,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_IMMEDIATE_RADIUS;
Expand Down Expand Up @@ -59,7 +61,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();
}
Expand Down Expand Up @@ -124,7 +126,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;
}
Expand Down Expand Up @@ -169,7 +171,8 @@ private static String getBusStopName(Leg busLeg) {
@Nullable
public static TripInstruction alignTravelerToTrip(
TravelerPosition travelerPosition,
boolean isStartOfTrip
boolean isStartOfTrip,
boolean travelerHasDeviated
) {
Locale locale = travelerPosition.locale;

Expand All @@ -182,16 +185,72 @@ 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 || (tripInstruction != null && tripInstruction.hasInstruction()))
? tripInstruction
: getContinueInstruction(travelerPosition, nextStep, locale);
}

/**
* Traveler is on track, but no immediate instruction is available. Provide a "continue on street" reassurance
* instruction if the traveler is on a walk leg. This will be based on the next or previous step depending on the
* traveler's relative position to both.
*/
private static ContinueInstruction getContinueInstruction(
TravelerPosition travelerPosition,
Step nextStep,
Locale locale
) {
if (
Boolean.TRUE.equals(!travelerPosition.expectedLeg.transitLeg) &&
travelerPosition.expectedLeg.steps != null &&
!travelerPosition.expectedLeg.steps.isEmpty()
) {
Step previousStep = getPreviousStep(travelerPosition.expectedLeg.steps, nextStep);
if (previousStep != null) {
boolean travelerBetweenSteps = isPointBetween(previousStep.toCoordinates(), nextStep.toCoordinates(), travelerPosition.currentPosition);
if (travelerBetweenSteps) {
return new ContinueInstruction(previousStep, locale);
} else if (isWithinStepRange(travelerPosition, previousStep)) {
return new ContinueInstruction(previousStep, locale);
} else if (isWithinStepRange(travelerPosition, nextStep)) {
return new ContinueInstruction(nextStep, locale);
}
}
}
return null;
}

/**
* The traveler is still with the provided step range.
*/
private static boolean isWithinStepRange(TravelerPosition travelerPosition, Step step) {
double distanceFromTravelerToStep = getDistance(travelerPosition.currentPosition, step.toCoordinates());
return distanceFromTravelerToStep < step.distance;
}

/**
* Get the step prior to the next step provided.
*/
private static Step getPreviousStep(List<Step> steps, Step nextStep) {
if (steps.get(0).equals(nextStep)) {
return null;
}
Optional<Step> 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.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.opentripplanner.middleware.triptracker.instruction;

import org.apache.logging.log4j.util.Strings;
import org.opentripplanner.middleware.otp.response.Step;

import java.util.Locale;

public class ContinueInstruction extends SelfLegInstruction {
public ContinueInstruction(Step legStep, Locale locale) {
this.legStep = legStep;
this.locale = locale;
}

@Override
public String build() {
if (legStep != null && !Strings.isBlank(legStep.streetName)) {
// TODO: i18n
return String.format("Continue on %s", legStep.streetName);
}
return NO_INSTRUCTION;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -294,88 +301,105 @@ private static Stream<Arguments> 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",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll need the plain text to keep track of the kinds of instructions expected. Or, we can further break down the instructions, this one could be a "directional" instruction for instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be cleaner to further break down the instruction. Also, this way eliminates time zone issues. But I think that is out of scope. Let me know.

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(adairAvenueNortheastStep, 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"
)
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -50,6 +51,7 @@ public class ManageLegTraversalTest {
private static Itinerary midtownToAnsleyItinerary;
private static List<Place> midtownToAnsleyIntermediateStops;
private static Itinerary firstLegBusTransit;
private static Itinerary baptistChurchToEastCroganStreetIntinerary;

private static final Locale locale = Locale.US;

Expand Down Expand Up @@ -78,6 +80,10 @@ public static void setUp() throws IOException {
CommonTestUtils.getTestResourceAsString("controllers/api/first-leg-transit.json"),
Itinerary.class
);
baptistChurchToEastCroganStreetIntinerary = JsonUtils.getPOJOFromJSON(
CommonTestUtils.getTestResourceAsString("controllers/api/baptist-church-to-east-crogan-street.json"),
Itinerary.class
);
// Hold on to the original list of intermediate stops (some tests will overwrite it)
midtownToAnsleyIntermediateStops = midtownToAnsleyItinerary.legs.get(1).intermediateStops;
}
Expand Down Expand Up @@ -208,6 +214,11 @@ private static Stream<Arguments> createTurnByTurnTrace() {
Coordinates busStopCoords = new Coordinates(firstBusLeg.from);
String busStopName = firstBusLeg.from.name;

Leg toEastCroganFirstLeg = baptistChurchToEastCroganStreetIntinerary.legs.get(0);
Step southClaytonSt = toEastCroganFirstLeg.steps.get(1);
Step eastCroganSt = toEastCroganFirstLeg.steps.get(2);
Coordinates pointOnSouthClaytonSt = new Coordinates(33.955561, -83.988204);

return Stream.of(
Arguments.of(
firstBusLeg,
Expand Down Expand Up @@ -270,9 +281,9 @@ private static Stream<Arguments> createTurnByTurnTrace() {
walkLeg,
new TraceData(
createPoint(virginiaCircleNortheastCoords, 12, SOUTH_WEST_BEARING),
NO_INSTRUCTION,
new ContinueInstruction(virginiaCircleNortheastStep, locale).build(),
false,
"On track approaching second step, but not close enough for instruction."
"On track approaching second step, provide continue instruction."
)
),
Arguments.of(
Expand Down Expand Up @@ -336,9 +347,9 @@ private static Stream<Arguments> 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(
Expand All @@ -358,6 +369,33 @@ private static Stream<Arguments> createTurnByTurnTrace() {
false,
"On destination instruction."
)
),
Arguments.of(
toEastCroganFirstLeg,
new TraceData(
pointOnSouthClaytonSt,
new ContinueInstruction(southClaytonSt, locale).build(),
false,
"On track passed second step and not near to next step, provide continue instruction for second step."
)
),
Arguments.of(
toEastCroganFirstLeg,
new TraceData(
createPoint(pointOnSouthClaytonSt, 12, NORTH_WEST_BEARING),
new ContinueInstruction(southClaytonSt, locale).build(),
false,
"On track a bit near to the next step, provide continue instruction for second step."
)
),
Arguments.of(
toEastCroganFirstLeg,
new TraceData(
createPoint(pointOnSouthClaytonSt, 72, NORTH_BEARING),
new ContinueInstruction(eastCroganSt, locale).build(),
false,
"On track passed next step, provide continue instruction for next step."
)
)
);
}
Expand Down
Loading
Loading