diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Place.java b/src/main/java/org/opentripplanner/middleware/otp/response/Place.java index d6b89e314..b58a672b3 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Place.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Place.java @@ -2,9 +2,10 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.ConvertsToCoordinates; import java.util.Date; -import java.util.Objects; import java.util.Set; /** @@ -12,7 +13,7 @@ */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class Place implements Cloneable { +public class Place implements ConvertsToCoordinates, Cloneable { public String name; public Double lon; @@ -39,4 +40,8 @@ public class Place implements Cloneable { protected Place clone() throws CloneNotSupportedException { return (Place) super.clone(); } + + public Coordinates toCoordinates() { + return new Coordinates(lat, lon); + } } diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Step.java b/src/main/java/org/opentripplanner/middleware/otp/response/Step.java index d39a35045..508e9fa9f 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Step.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Step.java @@ -2,15 +2,15 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; - -import java.util.Objects; +import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.ConvertsToCoordinates; /** * Plan response, step information. Produced using http://www.jsonschema2pojo.org/ */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) -public class Step implements Cloneable { +public class Step implements ConvertsToCoordinates, Cloneable { public Double distance; public String relativeDirection; @@ -30,4 +30,8 @@ public class Step implements Cloneable { protected Step clone() throws CloneNotSupportedException { return (Step) super.clone(); } + + public Coordinates toCoordinates() { + return new Coordinates(lat, lon); + } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 30ab3131f..0e131ce75 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -3,14 +3,13 @@ import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.triptracker.instruction.TripInstruction; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; -import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator; import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse; import org.opentripplanner.middleware.triptracker.response.TrackingResponse; import spark.Request; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; -import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; public class ManageTripTracking { diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index c512c15ce..2cfa5d694 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -2,9 +2,19 @@ import io.leonard.PolylineUtils; 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.DeviatedInstruction; +import org.opentripplanner.middleware.triptracker.instruction.GetOffHereTransitInstruction; +import org.opentripplanner.middleware.triptracker.instruction.GetOffNextStopTransitInstruction; +import org.opentripplanner.middleware.triptracker.instruction.GetOffSoonTransitInstruction; +import org.opentripplanner.middleware.triptracker.instruction.OnTrackInstruction; +import org.opentripplanner.middleware.triptracker.instruction.TransitLegSummaryInstruction; +import org.opentripplanner.middleware.triptracker.instruction.TripInstruction; +import org.opentripplanner.middleware.triptracker.instruction.WaitForTransitInstruction; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.ConvertsToCoordinates; import org.opentripplanner.middleware.utils.DateTimeUtils; import javax.annotation.Nullable; @@ -14,10 +24,11 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.stream.Collectors; -import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; -import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS; +import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION; +import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS; import static org.opentripplanner.middleware.utils.GeometryUtils.getDistance; import static org.opentripplanner.middleware.utils.GeometryUtils.isPointBetween; import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; @@ -29,6 +40,8 @@ public class TravelerLocator { public static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; + private static final int MIN_TRANSIT_VEHICLE_SPEED = 5; // meters per second. 11.1 mph or 18 km/h. + private TravelerLocator() { } @@ -55,6 +68,11 @@ public static String getInstruction( return tripInstruction.build(); } } + } else if (hasRequiredTransitLeg(travelerPosition) && hasRequiredTripStatus(tripStatus)) { + TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition); + if (tripInstruction != null) { + return tripInstruction.build(); + } } return NO_INSTRUCTION; } @@ -68,6 +86,15 @@ private static boolean hasRequiredWalkLeg(TravelerPosition travelerPosition) { travelerPosition.expectedLeg.mode.equalsIgnoreCase("walk"); } + /** + * Has required transit leg. + */ + private static boolean hasRequiredTransitLeg(TravelerPosition travelerPosition) { + return + travelerPosition.expectedLeg != null && + travelerPosition.expectedLeg.transitLeg; + } + /** * The trip instruction can only be provided if the traveler is close to the indicated route. */ @@ -76,8 +103,9 @@ private static boolean hasRequiredTripStatus(TripStatus tripStatus) { } /** - * Attempt to align the deviated traveler to the trip. If the traveler happens to be within an upcoming instruction - * provider this, else suggest the closest street to head towards. + * Attempt to align the deviated traveler to the trip when on access legs (e.g. walk legs). + * If the traveler happens to be within an upcoming instruction, the instruction will be issued, + * else suggest the closest street to head towards. */ @Nullable private static TripInstruction getBackOnTrack( @@ -89,9 +117,9 @@ private static TripInstruction getBackOnTrack( if (instruction != null && instruction.hasInstruction()) { return instruction; } - Step nearestStep = snapToStep(travelerPosition); + Step nearestStep = snapToWaypoint(travelerPosition, travelerPosition.expectedLeg.steps); return (nearestStep != null) - ? new TripInstruction(nearestStep.streetName, travelerPosition.locale) + ? new DeviatedInstruction(nearestStep.streetName, travelerPosition.locale) : null; } @@ -112,14 +140,14 @@ public static TripInstruction alignTravelerToTrip( .getDefault() .handleSendNotificationAction(tripStatus, travelerPosition); // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. - return new TripInstruction(travelerPosition.nextLeg, travelerPosition.currentTime, locale); + return new WaitForTransitInstruction(travelerPosition.nextLeg, travelerPosition.currentTime, locale); } - return new TripInstruction(getDistanceToEndOfLeg(travelerPosition), travelerPosition.expectedLeg.to.name, locale); + return new OnTrackInstruction(getDistanceToEndOfLeg(travelerPosition), travelerPosition.expectedLeg.to.name, locale); } - Step nextStep = snapToStep(travelerPosition); + Step nextStep = snapToWaypoint(travelerPosition, travelerPosition.expectedLeg.steps); if (nextStep != null && (!isPositionPastStep(travelerPosition, nextStep) || isStartOfTrip)) { - return new TripInstruction( + return new OnTrackInstruction( getDistance(travelerPosition.currentPosition, new Coordinates(nextStep)), nextStep, locale @@ -128,18 +156,49 @@ public static TripInstruction alignTravelerToTrip( return null; } + /** + * Align the traveler's position to the nearest transit stop or destination. + */ + @Nullable + public static TripInstruction alignTravelerToTransitTrip(TravelerPosition travelerPosition) { + Locale locale = travelerPosition.locale; + Leg expectedLeg = travelerPosition.expectedLeg; + String finalStop = expectedLeg.to.name; + + if (isApproachingEndOfLeg(travelerPosition)) { + return new GetOffHereTransitInstruction(finalStop, locale); + } + + Place nextStop = snapToWaypoint(travelerPosition, getIntermediateAndLastStop(expectedLeg), true); + if (nextStop != null) { + int stopsRemaining = stopsUntilEndOfLeg(nextStop, expectedLeg); + double distance = getDistance(travelerPosition.currentPosition, new Coordinates(nextStop)); + if (stopsRemaining == 1 && distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS && !isPositionPastStep(travelerPosition, nextStop) || stopsRemaining == 0) { + return new GetOffNextStopTransitInstruction(finalStop, locale); + } else if (stopsRemaining <= 3) { + return new GetOffSoonTransitInstruction(finalStop, locale); + } else if ( + stopsRemaining == expectedLeg.intermediateStops.size() && + travelerPosition.speed >= MIN_TRANSIT_VEHICLE_SPEED + ) { + return new TransitLegSummaryInstruction(expectedLeg, locale); + } + } + return null; + } + /** * Check that the current position is not past the "next step". This is to prevent an instruction being provided * for a step which is behind the traveler, but is within radius. */ - private static boolean isPositionPastStep(TravelerPosition travelerPosition, Step nextStep) { + private static boolean isPositionPastStep(TravelerPosition travelerPosition, ConvertsToCoordinates nextStep) { double distanceFromPositionToEndOfLegSegment = getDistance( travelerPosition.legSegmentFromPosition.end, travelerPosition.currentPosition ); double distanceFromStepToEndOfLegSegment = getDistance( travelerPosition.legSegmentFromPosition.end, - new Coordinates(nextStep) + nextStep.toCoordinates() ); return distanceFromPositionToEndOfLegSegment < distanceFromStepToEndOfLegSegment; } @@ -188,24 +247,18 @@ private static double getDistanceToEndOfLeg(TravelerPosition travelerPosition) { } /** - * Align the traveler to the leg and provide the next step from this point forward. + * From the starting index, find the next waypoint along a leg. */ - private static Step snapToStep(TravelerPosition travelerPosition) { - List legPositions = injectStepsIntoLegPositions(travelerPosition.expectedLeg); - int pointIndex = getNearestPointIndex(legPositions, travelerPosition.currentPosition); - return (pointIndex != -1) - ? getNextStep(travelerPosition.expectedLeg, legPositions, pointIndex) - : null; - } + public static T getNextWayPoint(List positions, List steps, int startIndex) { + Map waypoints = steps + .stream() + .collect(Collectors.toMap(s -> s, ConvertsToCoordinates::toCoordinates)); - /** - * From the starting index, find the next step along the leg. - */ - public static Step getNextStep(Leg leg, List positions, int startIndex) { for (int i = startIndex; i < positions.size(); i++) { - for (Step step : leg.steps) { - if (positions.get(i).equals(new Coordinates(step))) { - return step; + Coordinates pos = positions.get(i); + for (var entry : waypoints.entrySet()) { + if (pos.equals(entry.getValue())) { + return entry.getKey(); } } } @@ -228,25 +281,36 @@ private static int getNearestPointIndex(List positions, Coordinates return pointIndex; } + private static List getIntermediateAndLastStop(Leg leg) { + ArrayList stops = new ArrayList<>(leg.intermediateStops); + stops.add(leg.to); + return stops; + } + /** - * Inject the step positions into the leg positions. It is assumed that both sets of points are on the same route - * and are in between the start and end positions. If b = beginning, p = point on leg, S = step and e = end, create - * a list of coordinates which can be traversed to get the next step. + * Inject waypoints (could be steps on a walk leg, or intermediate stops on a transit leg) + * into the leg positions. It is assumed that both sets of points are on the same route + * and are in between the start and end positions. If b = beginning, p = point on leg, W = waypoint and e = end, create + * a list of coordinates which can be traversed to get the next waypoint. *

- * b|p|S|p|p|p|p|p|p|S|p|p|S|p|p|p|p|p|S|e + * b|p|W|p|p|p|p|p|p|W|p|p|W|p|p|p|p|p|W|e */ - public static List injectStepsIntoLegPositions(Leg leg) { + public static List injectWaypointsIntoLegPositions(Leg leg, List steps) { List allPositions = getAllLegPositions(leg); - List injectedSteps = new ArrayList<>(); + List waypoints = steps + .stream() + .map(ConvertsToCoordinates::toCoordinates) + .collect(Collectors.toList()); + List injectedPoints = new ArrayList<>(); List finalPositions = new ArrayList<>(); for (int i = 0; i < allPositions.size() - 1; i++) { Coordinates p1 = allPositions.get(i); finalPositions.add(p1); Coordinates p2 = allPositions.get(i + 1); - for (Step step : leg.steps) { - if (isPointBetween(p1, p2, new Coordinates(step)) && !injectedSteps.contains(step)) { - finalPositions.add(new Coordinates(step)); - injectedSteps.add(step); + for (Coordinates waypoint : waypoints) { + if (isPointBetween(p1, p2, waypoint) && !injectedPoints.contains(waypoint)) { + finalPositions.add(waypoint); + injectedPoints.add(waypoint); } } } @@ -254,23 +318,39 @@ public static List injectStepsIntoLegPositions(Leg leg) { // Add the destination coords which are missed because of the -1 condition above. finalPositions.add(allPositions.get(allPositions.size() - 1)); - if (injectedSteps.size() != leg.steps.size()) { - // One or more steps have not been injected because they are not between two geometry points. Inject these - // based on proximity. - List missedSteps = leg.steps + if (injectedPoints.size() != waypoints.size()) { + // One or more waypoints have not been injected because they are not between two geometry points. + // Inject these based on proximity. + waypoints .stream() - .filter(step -> !injectedSteps.contains(step)) - .collect(Collectors.toList()); - for (Step missedStep : missedSteps) { - int pointIndex = getNearestPointIndex(finalPositions, new Coordinates(missedStep)); - if (pointIndex != -1) { - finalPositions.add(pointIndex, new Coordinates(missedStep)); - } - } + .filter(pt -> !injectedPoints.contains(pt)) + .forEach(missedPoint -> { + int pointIndex = getNearestPointIndex(finalPositions, missedPoint); + if (pointIndex != -1) { + finalPositions.add(pointIndex, missedPoint); + } + }); } return createExclusionZone(finalPositions, leg); } + /** + * Align the traveler to the transit leg and provide the next waypoint from this point forward. + */ + private static T snapToWaypoint(TravelerPosition pos, List waypoints, boolean excludeCurrent) { + List legPositions = injectWaypointsIntoLegPositions(pos.expectedLeg, waypoints); + int pointIndex = getNearestPointIndex(legPositions, pos.currentPosition); + int startingIndex = excludeCurrent ? Math.min(pointIndex + 1, legPositions.size() - 1) : pointIndex; + return pointIndex != -1 ? getNextWayPoint(legPositions, waypoints, startingIndex) : null; + } + + /** + * Align the traveler to the transit leg and provide the next waypoint forward, excluding the current position. + */ + private static T snapToWaypoint(TravelerPosition pos, List waypoints) { + return snapToWaypoint(pos, waypoints, false); + } + /** * Get a list containing all positions on a leg. */ @@ -333,4 +413,11 @@ public static boolean isWithinExclusionZone(Coordinates position, List ste } return false; } + + public static int stopsUntilEndOfLeg(Place stop, Leg leg) { + if (stop == leg.to) return 0; + + List stops = leg.intermediateStops; + return stops.size() - stops.indexOf(stop); + } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 43fdd988c..40bd3ae8e 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -25,6 +25,9 @@ public class TravelerPosition { /** Traveler current coordinates. */ public Coordinates currentPosition; + /** Speed reported at the position, in meters per second. */ + public int speed; + /** Traveler current time. */ public Instant currentTime; @@ -44,6 +47,7 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU TrackingLocation lastLocation = trackedJourney.locations.get(trackedJourney.locations.size() - 1); currentTime = lastLocation.timestamp.toInstant(); currentPosition = new Coordinates(lastLocation); + speed = lastLocation.speed; expectedLeg = getExpectedLeg(currentPosition, itinerary); if (expectedLeg != null) { nextLeg = getNextLeg(expectedLeg, itinerary); @@ -59,12 +63,19 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU } /** Used for unit testing. */ - public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { + public TravelerPosition(Leg expectedLeg, Coordinates currentPosition, int speed) { this.expectedLeg = expectedLeg; this.currentPosition = currentPosition; + this.speed = speed; legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); } + /** Used for unit testing. */ + public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { + // Anywhere the speed is zero means that speed is not considered for a specific logic. + this(expectedLeg, currentPosition, 0); + } + /** Used for unit testing. */ public TravelerPosition(Leg nextLeg, Instant currentTime) { this.nextLeg = nextLeg; diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java deleted file mode 100644 index 64d7254af..000000000 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ /dev/null @@ -1,197 +0,0 @@ -package org.opentripplanner.middleware.triptracker; - -import org.opentripplanner.middleware.otp.response.Leg; -import org.opentripplanner.middleware.otp.response.Step; -import org.opentripplanner.middleware.utils.DateTimeUtils; - -import java.util.Date; -import java.time.Duration; -import java.time.Instant; -import java.util.Locale; - -import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; -import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteShortNameFromLeg; - -public class TripInstruction { - - public enum TripInstructionType { ON_TRACK, DEVIATED, WAIT_FOR_BUS } - - /** The radius in meters under which an immediate instruction is given. */ - public static final int TRIP_INSTRUCTION_IMMEDIATE_RADIUS - = getConfigPropertyAsInt("TRIP_INSTRUCTION_IMMEDIATE_RADIUS", 2); - - /** The radius in meters under which an upcoming instruction is given. */ - public static final int TRIP_INSTRUCTION_UPCOMING_RADIUS - = getConfigPropertyAsInt("TRIP_INSTRUCTION_UPCOMING_RADIUS", 10); - - /** The prefix to use when at a street location with an instruction. */ - public static final String TRIP_INSTRUCTION_IMMEDIATE_PREFIX = "IMMEDIATE: "; - - /** The prefix to use when nearing a street location with an instruction. */ - public static final String TRIP_INSTRUCTION_UPCOMING_PREFIX = "UPCOMING: "; - - /** The prefix to use when arrived at the destination. */ - public static final String TRIP_INSTRUCTION_ARRIVED_PREFIX = "ARRIVED: "; - - public static final String NO_INSTRUCTION = "NO_INSTRUCTION"; - - /** Distance in meters to step instruction or destination. */ - public double distance; - - /** Step aligned with traveler's position. */ - public Step legStep; - - /** Instruction prefix. */ - public String prefix; - - /** Name of final destination or street. */ - public String locationName; - - /** Provided if the next leg for the traveler will be a bus transit leg. */ - public Leg busLeg; - - /** The time provided by the traveler */ - public Instant currentTime; - - /** The type of instruction to be provided to the traveler. */ - private final TripInstructionType tripInstructionType; - - /** The traveler's locale. */ - private final Locale locale; - - public TripInstruction(boolean isDestination, double distance, Locale locale) { - this.distance = distance; - this.tripInstructionType = TripInstructionType.ON_TRACK; - this.locale = locale; - setPrefix(isDestination); - } - - /** - * If the traveler is within the upcoming radius an instruction will be provided. - */ - public boolean hasInstruction() { - return distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS; - } - - /** - * On track instruction to step. - */ - public TripInstruction(double distance, Step legStep, Locale locale) { - this(false, distance, locale); - this.legStep = legStep; - } - - /** - * On track instruction to destination. - */ - public TripInstruction(double distance, String locationName, Locale locale) { - this(true, distance, locale); - this.locationName = locationName; - } - - /** - * Deviated instruction. - */ - public TripInstruction(String locationName, Locale locale) { - this.tripInstructionType = TripInstructionType.DEVIATED; - this.locationName = locationName; - this.locale = locale; - } - - /** - * Provide bus related trip instruction. - */ - public TripInstruction(Leg busLeg, Instant currentTime, Locale locale) { - this.tripInstructionType = TripInstructionType.WAIT_FOR_BUS; - this.busLeg = busLeg; - this.currentTime = currentTime; - this.locale = locale; - } - - /** - * The prefix is defined depending on the traveler either approaching a step or destination and the predefined - * distances from these points. - */ - private void setPrefix(boolean isDestination) { - if (distance <= TRIP_INSTRUCTION_IMMEDIATE_RADIUS) { - prefix = (isDestination) ? TRIP_INSTRUCTION_ARRIVED_PREFIX : TRIP_INSTRUCTION_IMMEDIATE_PREFIX; - } else if (distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) { - prefix = TRIP_INSTRUCTION_UPCOMING_PREFIX; - } - } - - /** - * Build instruction based on the traveler's location. - */ - public String build() { - switch (tripInstructionType) { - case ON_TRACK: - return buildOnTrackInstruction(); - case DEVIATED: - return String.format("Head to %s", locationName); - case WAIT_FOR_BUS: - return buildWaitForBusInstruction(); - default: - return NO_INSTRUCTION; - } - } - - /** - * Build on track instruction based on step instructions and location. e.g. - *

- * "UPCOMING: CONTINUE on Langley Drive" - * "IMMEDIATE: RIGHT on service road" - * "ARRIVED: Gwinnett Justice Center (Central)" - *

- * TODO: Internationalization and refinements to these generated instructions with input from the mobile app team. - */ - - private String buildOnTrackInstruction() { - if (hasInstruction()) { - if (legStep != null) { - String relativeDirection = (legStep.relativeDirection.equals("DEPART")) - ? "Head " + legStep.absoluteDirection - : legStep.relativeDirection; - return String.format("%s%s on %s", prefix, relativeDirection, legStep.streetName); - } else if (locationName != null) { - return String.format("%s%s", prefix, locationName); - } - } - return NO_INSTRUCTION; - } - - /** - * Build wait for bus instruction. - */ - private String buildWaitForBusInstruction() { - String routeShortName = getRouteShortNameFromLeg(busLeg); - long delayInMinutes = busLeg.departureDelay; - long absoluteMinutes = Math.abs(delayInMinutes); - long waitInMinutes = Duration - .between(currentTime.atZone(DateTimeUtils.getOtpZoneId()), busLeg.getScheduledStartTime()) - .toMinutes(); - String delayInfo = (delayInMinutes > 0) ? "late" : "early"; - String arrivalInfo = (absoluteMinutes <= 1) - ? ", on time" - : String.format(" now%s %s", getReadableMinutes(delayInMinutes), delayInfo); - return String.format( - "Wait%s for your bus, route %s, scheduled at %s%s", - getReadableMinutes(waitInMinutes), - routeShortName, - DateTimeUtils.formatShortDate(Date.from(busLeg.getScheduledStartTime().toInstant()), locale), - arrivalInfo - ); - } - - /** - * Get the number of minutes to wait for a bus. If the wait is zero (or less than zero!) return empty string. - */ - private String getReadableMinutes(long waitInMinutes) { - if (waitInMinutes == 1) { - return String.format(" %s minute", waitInMinutes); - } else if (waitInMinutes > 1) { - return String.format(" %s minutes", waitInMinutes); - } - return ""; - } -} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/DeviatedInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/DeviatedInstruction.java new file mode 100644 index 000000000..8e096e640 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/DeviatedInstruction.java @@ -0,0 +1,17 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import java.util.Locale; + +/** Instruction when someone is deviated from their route */ +public class DeviatedInstruction extends SelfLegInstruction { + public DeviatedInstruction(String referenceLocation, Locale locale) { + this.locationName = referenceLocation; + this.locale = locale; + } + + @Override + public String build() { + // TODO: i18n + return String.format("Head to %s", locationName); + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffHereTransitInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffHereTransitInstruction.java new file mode 100644 index 000000000..14f56f7d1 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffHereTransitInstruction.java @@ -0,0 +1,20 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import java.util.Locale; + +/** + * Instruction to get off a transit vehicle at the current stop or imminently. + */ +public class GetOffHereTransitInstruction extends TripInstruction { + + public GetOffHereTransitInstruction(String stopName, Locale locale) { + this.locationName = stopName; + this.locale = locale; + } + + @Override + public String build() { + // TODO: i18n + return String.format("Get off here (%s)", locationName); + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffNextStopTransitInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffNextStopTransitInstruction.java new file mode 100644 index 000000000..fdbdfc7c8 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffNextStopTransitInstruction.java @@ -0,0 +1,20 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import java.util.Locale; + +/** + * Instruction to get off a transit vehicle at the next stop. + */ +public class GetOffNextStopTransitInstruction extends TripInstruction { + + public GetOffNextStopTransitInstruction(String stopName, Locale locale) { + this.locationName = stopName; + this.locale = locale; + } + + @Override + public String build() { + // TODO: i18n + return String.format("Get off at next stop (%s)", locationName); + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffSoonTransitInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffSoonTransitInstruction.java new file mode 100644 index 000000000..4693b9a20 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/GetOffSoonTransitInstruction.java @@ -0,0 +1,20 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import java.util.Locale; + +/** + * Instruction to prepare to get off a transit vehicle. + */ +public class GetOffSoonTransitInstruction extends TripInstruction { + + public GetOffSoonTransitInstruction(String stopName, Locale locale) { + this.locationName = stopName; + this.locale = locale; + } + + @Override + public String build() { + // TODO: i18n + return String.format("Your stop is coming up (%s)", locationName); + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/OnTrackInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/OnTrackInstruction.java new file mode 100644 index 000000000..87a9b6214 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/OnTrackInstruction.java @@ -0,0 +1,75 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Step; + +import java.util.Locale; + +/** Instruction for cases someone is on track in their itinerary */ +public class OnTrackInstruction extends SelfLegInstruction { + /** The prefix to use when at a street location with an instruction. */ + public static final String TRIP_INSTRUCTION_IMMEDIATE_PREFIX = "IMMEDIATE: "; + + /** The prefix to use when nearing a street location with an instruction. */ + public static final String TRIP_INSTRUCTION_UPCOMING_PREFIX = "UPCOMING: "; + + /** The prefix to use when arrived at the destination. */ + public static final String TRIP_INSTRUCTION_ARRIVED_PREFIX = "ARRIVED: "; + + public OnTrackInstruction(boolean isDestination, double distance, Locale locale) { + this.distance = distance; + this.locale = locale; + setPrefix(isDestination); + } + + /** + * On track instruction to step. + */ + public OnTrackInstruction(double distance, Step legStep, Locale locale) { + this(false, distance, locale); + this.legStep = legStep; + } + + /** + * On track instruction to destination. + */ + public OnTrackInstruction(double distance, String locationName, Locale locale) { + this(true, distance, locale); + this.locationName = locationName; + } + + /** + * The prefix is defined depending on the traveler either approaching a step or destination and the predefined + * distances from these points. + */ + private void setPrefix(boolean isDestination) { + if (distance <= TRIP_INSTRUCTION_IMMEDIATE_RADIUS) { + prefix = (isDestination) ? TRIP_INSTRUCTION_ARRIVED_PREFIX : TRIP_INSTRUCTION_IMMEDIATE_PREFIX; + } else if (distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) { + prefix = TRIP_INSTRUCTION_UPCOMING_PREFIX; + } + } + + /** + * Build on track instruction based on step instructions and location. e.g. + *

+ * "UPCOMING: CONTINUE on Langley Drive" + * "IMMEDIATE: RIGHT on service road" + * "ARRIVED: Gwinnett Justice Center (Central)" + *

+ * TODO: Internationalization and refinements to these generated instructions with input from the mobile app team. + */ + @Override + public String build() { + if (hasInstruction()) { + if (legStep != null) { + String relativeDirection = (legStep.relativeDirection.equals("DEPART")) + ? "Head " + legStep.absoluteDirection + : legStep.relativeDirection; + return String.format("%s%s on %s", prefix, relativeDirection, legStep.streetName); + } else if (locationName != null) { + return String.format("%s%s", prefix, locationName); + } + } + return NO_INSTRUCTION; + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/SelfLegInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/SelfLegInstruction.java new file mode 100644 index 000000000..ab39c21ba --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/SelfLegInstruction.java @@ -0,0 +1,13 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Step; + +/** + * Parent class for instructions on legs where the user is in charge of where they are going (walk, bike, scooter), + * as opposed to a transit or taxi leg where user has no control of where the vehicle goes. + */ +public class SelfLegInstruction extends TripInstruction { + /** Step aligned with traveler's position. */ + protected Step legStep; + +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TransitLegInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TransitLegInstruction.java new file mode 100644 index 000000000..081c3d40a --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TransitLegInstruction.java @@ -0,0 +1,7 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Leg; + +public class TransitLegInstruction extends TripInstruction { + protected Leg transitLeg; +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TransitLegSummaryInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TransitLegSummaryInstruction.java new file mode 100644 index 000000000..e8829360a --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TransitLegSummaryInstruction.java @@ -0,0 +1,28 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Leg; + +import java.util.Locale; + +/** + * Instruction that summarizes a transit leg, emitted typically after getting onboard a transit vehicle. + */ +public class TransitLegSummaryInstruction extends TransitLegInstruction { + public TransitLegSummaryInstruction(Leg leg, Locale locale) { + this.transitLeg = leg; + this.locale = locale; + } + + @Override + public String build() { + // TODO: i18n + return String.format( + "Ride %d min / %d stops to %s", + // Use Math.floor to be consistent with UI for transit leg durations. + (int)(Math.floor(transitLeg.duration / 60)), + // OTP returns an empty list if there are no intermediate stops. + transitLeg.intermediateStops.size() + 1, + transitLeg.to.name + ); + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TripInstruction.java new file mode 100644 index 000000000..f1c7abae7 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/TripInstruction.java @@ -0,0 +1,66 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Place; + +import java.time.Instant; +import java.util.Locale; + +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; + +public class TripInstruction { + + /** The radius in meters under which an immediate instruction is given. */ + public static final int TRIP_INSTRUCTION_IMMEDIATE_RADIUS + = getConfigPropertyAsInt("TRIP_INSTRUCTION_IMMEDIATE_RADIUS", 2); + + /** The radius in meters under which an upcoming instruction is given. */ + public static final int TRIP_INSTRUCTION_UPCOMING_RADIUS + = getConfigPropertyAsInt("TRIP_INSTRUCTION_UPCOMING_RADIUS", 10); + + public static final String NO_INSTRUCTION = "NO_INSTRUCTION"; + + /** Distance in meters to step instruction or destination. */ + public double distance; + + /** Stop/place aligned with traveler's position. */ + public Place place; + + /** Instruction prefix. */ + public String prefix; + + /** Name of final destination or street. */ + public String locationName; + + /** The time provided by the traveler */ + public Instant currentTime; + + /** The traveler's locale. */ + protected Locale locale; + + protected TripInstruction() { + // For use by subclasses. + } + + /** + * If the traveler is within the upcoming radius an instruction will be provided. + */ + public boolean hasInstruction() { + return distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS; + } + + /** + * Get the number of minutes to wait for a bus. If the wait is zero (or less than zero!) return empty string. + */ + protected String getReadableMinutes(long waitInMinutes) { + if (waitInMinutes == 1) { + return String.format(" %s minute", waitInMinutes); + } else if (waitInMinutes > 1) { + return String.format(" %s minutes", waitInMinutes); + } + return ""; + } + + public String build() { + return NO_INSTRUCTION; + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/instruction/WaitForTransitInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/WaitForTransitInstruction.java new file mode 100644 index 000000000..085ea06d7 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/instruction/WaitForTransitInstruction.java @@ -0,0 +1,44 @@ +package org.opentripplanner.middleware.triptracker.instruction; + +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.utils.DateTimeUtils; + +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.Locale; + +import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteShortNameFromLeg; + +/** + * Instruction to wait for a transit vehicle, typically emitted when someone is arriving at a transit stop. + */ +public class WaitForTransitInstruction extends TransitLegInstruction { + public WaitForTransitInstruction(Leg transitLeg, Instant currentTime, Locale locale) { + this.transitLeg = transitLeg; + this.currentTime = currentTime; + this.locale = locale; + } + + @Override + public String build() { + // TODO: i18n + String routeShortName = getRouteShortNameFromLeg(transitLeg); + long delayInMinutes = transitLeg.departureDelay; + long absoluteMinutes = Math.abs(delayInMinutes); + long waitInMinutes = Duration + .between(currentTime.atZone(DateTimeUtils.getOtpZoneId()), transitLeg.getScheduledStartTime()) + .toMinutes(); + String delayInfo = (delayInMinutes > 0) ? "late" : "early"; + String arrivalInfo = (absoluteMinutes <= 1) + ? ", on time" + : String.format(" now%s %s", getReadableMinutes(delayInMinutes), delayInfo); + return String.format( + "Wait%s for your bus, route %s, scheduled at %s%s", + getReadableMinutes(waitInMinutes), + routeShortName, + DateTimeUtils.formatShortDate(Date.from(transitLeg.getScheduledStartTime().toInstant()), locale), + arrivalInfo + ); + } +} diff --git a/src/main/java/org/opentripplanner/middleware/utils/ConvertsToCoordinates.java b/src/main/java/org/opentripplanner/middleware/utils/ConvertsToCoordinates.java new file mode 100644 index 000000000..d2511588b --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/utils/ConvertsToCoordinates.java @@ -0,0 +1,8 @@ +package org.opentripplanner.middleware.utils; + +public interface ConvertsToCoordinates { + + /** Extracts coordinates of an object. */ + Coordinates toCoordinates(); + +} 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 2cd0c8303..27b3dc84b 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java @@ -29,7 +29,6 @@ import org.opentripplanner.middleware.triptracker.TripTrackingData; import org.opentripplanner.middleware.triptracker.payload.EndTrackingPayload; import org.opentripplanner.middleware.triptracker.payload.ForceEndTrackingPayload; -import org.opentripplanner.middleware.triptracker.payload.GeneralPayload; import org.opentripplanner.middleware.triptracker.payload.StartTrackingPayload; import org.opentripplanner.middleware.triptracker.payload.TrackPayload; import org.opentripplanner.middleware.triptracker.payload.UpdatedTrackingPayload; @@ -40,7 +39,6 @@ import org.opentripplanner.middleware.utils.HttpResponseValues; import org.opentripplanner.middleware.utils.JsonUtils; -import java.time.Instant; import java.util.Date; import java.util.HashMap; import java.util.List; @@ -55,7 +53,7 @@ import static org.opentripplanner.middleware.testutils.ApiTestUtils.TEMP_AUTH0_USER_PASSWORD; import static org.opentripplanner.middleware.testutils.ApiTestUtils.getMockHeaders; import static org.opentripplanner.middleware.testutils.ApiTestUtils.makeRequest; -import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; +import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION; import static org.opentripplanner.middleware.utils.GeometryUtils.createPoint; public class TrackedTripControllerTest extends OtpMiddlewareTestEnvironment { diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index f17ac54cf..8cb9027f0 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -7,19 +7,14 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.OtpUser; 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.testutils.CommonTestUtils; -import org.opentripplanner.middleware.triptracker.TripInstruction; -import org.opentripplanner.middleware.triptracker.LegSegment; -import org.opentripplanner.middleware.triptracker.TrackingLocation; -import org.opentripplanner.middleware.triptracker.TravelerPosition; -import org.opentripplanner.middleware.triptracker.TravelerLocator; -import org.opentripplanner.middleware.triptracker.TripStatus; +import org.opentripplanner.middleware.triptracker.instruction.DeviatedInstruction; +import org.opentripplanner.middleware.triptracker.instruction.OnTrackInstruction; import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.Coordinates; import org.opentripplanner.middleware.utils.DateTimeUtils; @@ -38,10 +33,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getSecondsToMilliseconds; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.interpolatePoints; -import static org.opentripplanner.middleware.triptracker.TravelerLocator.getNextStep; -import static org.opentripplanner.middleware.triptracker.TravelerLocator.injectStepsIntoLegPositions; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.getNextWayPoint; import static org.opentripplanner.middleware.triptracker.TravelerLocator.isWithinExclusionZone; -import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; +import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION; import static org.opentripplanner.middleware.utils.GeometryUtils.calculateBearing; import static org.opentripplanner.middleware.utils.GeometryUtils.createPoint; @@ -51,6 +45,7 @@ public class ManageLegTraversalTest { private static Itinerary edmundParkDriveToRockSpringsItinerary; private static Itinerary adairAvenueToMonroeDriveItinerary; + private static Itinerary midtownToAnsleyItinerary; private static final Locale locale = Locale.US; @@ -71,6 +66,10 @@ public static void setUp() throws IOException { CommonTestUtils.getTestResourceAsString("controllers/api/adair-avenue-to-monroe-drive.json"), Itinerary.class ); + midtownToAnsleyItinerary = JsonUtils.getPOJOFromJSON( + CommonTestUtils.getTestResourceAsString("controllers/api/27nb-midtown-to-ansley.json"), + Itinerary.class + ); } @ParameterizedTest @@ -157,10 +156,12 @@ private static Stream createTrace() { @ParameterizedTest @MethodSource("createTurnByTurnTrace") - void canTrackTurnByTurn(TurnTrace turnTrace) { - TravelerPosition travelerPosition = new TravelerPosition(turnTrace.itinerary.legs.get(0), turnTrace.position); - String tripInstruction = TravelerLocator.getInstruction(turnTrace.tripStatus, travelerPosition, turnTrace.isStartOfTrip); - assertEquals(turnTrace.expectedInstruction, Objects.requireNonNullElse(tripInstruction, NO_INSTRUCTION), turnTrace.message); + void canTrackTurnByTurn(TraceData traceData) { + Itinerary itinerary = adairAvenueToMonroeDriveItinerary; + Leg walkLeg = itinerary.legs.get(0); + TravelerPosition travelerPosition = new TravelerPosition(walkLeg, traceData.position); + String tripInstruction = TravelerLocator.getInstruction(traceData.tripStatus, travelerPosition, traceData.isStartOfTrip); + assertEquals(traceData.expectedInstruction, Objects.requireNonNullElse(tripInstruction, NO_INSTRUCTION), traceData.message); } private static Stream createTurnByTurnTrace() { @@ -190,49 +191,49 @@ private static Stream createTurnByTurnTrace() { return Stream.of( Arguments.of( - new TurnTrace( + new TraceData( originCoords, - new TripInstruction(10, adairAvenueNortheastStep, locale).build(), + new OnTrackInstruction(10, adairAvenueNortheastStep, locale).build(), true, "Just started the trip and near to the instruction for the first step. " ) ), Arguments.of( - new TurnTrace( + new TraceData( originCoords, - new TripInstruction(10, adairAvenueNortheastStep, locale).build(), + new OnTrackInstruction(10, adairAvenueNortheastStep, locale).build(), false, "Coming up on first instruction." ) ), Arguments.of( - new TurnTrace( + new TraceData( adairAvenueNortheastCoords, - new TripInstruction(2, adairAvenueNortheastStep, locale).build(), + new OnTrackInstruction(2, adairAvenueNortheastStep, locale).build(), false, "On first instruction." ) ), Arguments.of( - new TurnTrace( + new TraceData( TripStatus.DEVIATED, createPoint(adairAvenueNortheastCoords, 12, NORTH_WEST_BEARING), - new TripInstruction(adairAvenueNortheastStep.streetName, locale).build(), + new DeviatedInstruction(adairAvenueNortheastStep.streetName, locale).build(), false, "Deviated to the north of east to west path. Suggest path to head towards." ) ), Arguments.of( - new TurnTrace( + new TraceData( TripStatus.DEVIATED, createPoint(adairAvenueNortheastCoords, 12, SOUTH_WEST_BEARING), - new TripInstruction(adairAvenueNortheastStep.streetName, locale).build(), + new DeviatedInstruction(adairAvenueNortheastStep.streetName, locale).build(), false, "Deviated to the south of east to west path. Suggest path to head towards." ) ), Arguments.of( - new TurnTrace( + new TraceData( createPoint(virginiaCircleNortheastCoords, 12, SOUTH_WEST_BEARING), NO_INSTRUCTION, false, @@ -240,58 +241,58 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( - new TurnTrace( + new TraceData( TripStatus.DEVIATED, createPoint(virginiaCircleNortheastCoords, 8, NORTH_BEARING), - new TripInstruction(9, virginiaCircleNortheastStep, locale).build(), + new OnTrackInstruction(9, virginiaCircleNortheastStep, locale).build(), false, "Deviated from path, but within the upcoming radius of second instruction." ) ), Arguments.of( - new TurnTrace( + new TraceData( virginiaCircleNortheastCoords, - new TripInstruction(0, virginiaCircleNortheastStep, locale).build(), + new OnTrackInstruction(0, virginiaCircleNortheastStep, locale).build(), false, "On second instruction." ) ), Arguments.of( - new TurnTrace( + new TraceData( TripStatus.DEVIATED, - createPoint(ponceDeLeonPlaceNortheastCoords, 8, NORTH_WEST_BEARING), - new TripInstruction(10, ponceDeLeonPlaceNortheastStep, locale).build(), + createPoint(ponceDeLeonPlaceNortheastCoords, 10, NORTH_WEST_BEARING), + new DeviatedInstruction(ponceDeLeonPlaceNortheastStep.streetName, locale).build(), false, "Deviated to the west of south to north path. Suggest path to head towards." ) ), Arguments.of( - new TurnTrace( + new TraceData( TripStatus.DEVIATED, - createPoint(ponceDeLeonPlaceNortheastCoords, 8, NORTH_EAST_BEARING), - new TripInstruction(10, ponceDeLeonPlaceNortheastStep, locale).build(), + createPoint(ponceDeLeonPlaceNortheastCoords, 10, NORTH_EAST_BEARING), + new DeviatedInstruction(ponceDeLeonPlaceNortheastStep.streetName, locale).build(), false, "Deviated to the east of south to north path. Suggest path to head towards." ) ), Arguments.of( - new TurnTrace( + new TraceData( createPoint(pointBeforeTurn, 8, calculateBearing(pointBeforeTurn, virginiaAvenuePoint)), - new TripInstruction(10, virginiaAvenueNortheastStep, locale).build(), + new OnTrackInstruction(10, virginiaAvenueNortheastStep, locale).build(), false, "Approaching left turn on Virginia Avenue (Test to make sure turn is not missed)." ) ), Arguments.of( - new TurnTrace( + new TraceData( createPoint(pointBeforeTurn, 17, calculateBearing(pointBeforeTurn, virginiaAvenuePoint)), - new TripInstruction(2, virginiaAvenueNortheastStep, locale).build(), + new OnTrackInstruction(2, virginiaAvenueNortheastStep, locale).build(), false, "Turn left on to Virginia Avenue (Test to make sure turn is not missed)." ) ), Arguments.of( - new TurnTrace( + new TraceData( createPoint(pointAfterTurn, 0, calculateBearing(pointAfterTurn, virginiaAvenuePoint)), NO_INSTRUCTION, false, @@ -299,17 +300,17 @@ private static Stream createTurnByTurnTrace() { ) ), Arguments.of( - new TurnTrace( + new TraceData( createPoint(destinationCoords, 8, SOUTH_BEARING), - new TripInstruction(10, destinationName, locale).build(), + new OnTrackInstruction(10, destinationName, locale).build(), false, "Coming up on destination instruction." ) ), Arguments.of( - new TurnTrace( + new TraceData( destinationCoords, - new TripInstruction(2, destinationName, locale).build(), + new OnTrackInstruction(2, destinationName, locale).build(), false, "On destination instruction." ) @@ -318,14 +319,110 @@ private static Stream createTurnByTurnTrace() { } @ParameterizedTest - @MethodSource("createGetNearestStepTrace") - void canGetNearestStep(Step expectedStep, int startIndex, String message) { + @MethodSource("createTransitRideTrace") + void canTrackTransitRide(TraceData traceData) { + Itinerary itinerary = midtownToAnsleyItinerary; + Leg transitLeg = itinerary.legs.get(1); + TravelerPosition travelerPosition = new TravelerPosition(transitLeg, traceData.position, traceData.speed); + String tripInstruction = TravelerLocator.getInstruction(traceData.tripStatus, travelerPosition, false); + assertEquals(traceData.expectedInstruction, Objects.requireNonNullElse(tripInstruction, NO_INSTRUCTION), traceData.message); + } + + private static Stream createTransitRideTrace() { + final int SOUTH_WEST_BEARING = 225; + Leg transitLeg = midtownToAnsleyItinerary.legs.get(1); + String destinationName = transitLeg.to.name; + + Coordinates originCoords = new Coordinates(transitLeg.from); + Coordinates destinationCoords = new Coordinates(transitLeg.to); + + return Stream.of( + Arguments.of( + new TraceData( + originCoords, + NO_INSTRUCTION, + "Just boarded the transit vehicle leg, there should not be an instruction." + ) + ), + // This instruction can be missed if the transit vehicle is in a slow/congested area + // with speeds less than 5 meters/second (11.1 mph, 18 km/h). + Arguments.of( + new TraceData( + new Coordinates(33.78647, -84.38041), + 6, // meters per second, ~13.4 mph or 21.6 km/h. The threshold is 5 meters per second. + String.format("Ride 4 min / 8 stops to %s", destinationName), + "Summarize the transit trip as vehicle departs." + ) + ), + Arguments.of( + new TraceData( + new Coordinates(33.78792, -84.37776), + NO_INSTRUCTION, + "On the transit segment, but far from the arrival stop, so no instruction is given." + ) + ), + Arguments.of( + new TraceData( + new Coordinates(33.79139, -84.37441), + String.format("Your stop is coming up (%s)", destinationName), + "Upcoming arrival stop instruction." + ) + ), + Arguments.of( + new TraceData( + new Coordinates(33.79362, -84.37235), + String.format("Your stop is coming up (%s)", destinationName), + "Between the third and second to last stop." + ) + ), + Arguments.of( + new TraceData( + new Coordinates(33.79445, -84.37156), + String.format("Get off at next stop (%s)", destinationName), + "One-stop warning (only within 'upcoming' distance of that stop) before the stop to get off" + ) + ), + Arguments.of( + new TraceData( + new Coordinates(33.79478, -84.37127), + String.format("Get off at next stop (%s)", destinationName), + "Past the one-stop warning from the stop where you should get off." + ) + ), + Arguments.of( + new TraceData( + new Coordinates(33.79489, -84.37115), + String.format("Get off at next stop (%s)", destinationName), + "Past the one-stop warning from the stop where you should get off (#2)." + ) + ), + Arguments.of( + new TraceData( + createPoint(destinationCoords, 8, SOUTH_WEST_BEARING), + String.format("Get off here (%s)", destinationName), + "Instruction approaching or at the stop where you should get off." + ) + ), + Arguments.of( + new TraceData( + TripStatus.DEVIATED, + new Coordinates(33.79371, -84.37711), + NO_INSTRUCTION, + "No instruction provided besides trip status if bus is deviated or user missed their stop." + ) + ) + ); + } + + @ParameterizedTest + @MethodSource("createGetNearestWaypointTrace") + void canGetNearestWaypoint(Step expectedStep, int startIndex, String message) { Leg leg = edmundParkDriveToRockSpringsItinerary.legs.get(0); - List allPositions = injectStepsIntoLegPositions(edmundParkDriveToRockSpringsItinerary.legs.get(0)); - assertEquals(expectedStep, getNextStep(leg, allPositions, startIndex), message); + List allPositions = TravelerLocator.injectWaypointsIntoLegPositions(leg, leg.steps); + assertEquals(expectedStep, getNextWayPoint(allPositions, leg.steps, startIndex), message); } - private static Stream createGetNearestStepTrace() { + private static Stream createGetNearestWaypointTrace() { Leg leg = edmundParkDriveToRockSpringsItinerary.legs.get(0); return Stream.of( Arguments.of(leg.steps.get(0), 0, "At the beginning, expecting the first step."), @@ -341,12 +438,12 @@ private static Stream createGetNearestStepTrace() { } @Test - void canInjectSteps() { + void canInjectWaypoints() { Leg leg = edmundParkDriveToRockSpringsItinerary.legs.get(0); List legPositions = PolylineUtils.decode(leg.legGeometry.points, 5); int excluded = getNumberOfExcludedPoints(legPositions, leg); int expectedNumberOfPositions = (legPositions.size() - excluded) + leg.steps.size() + 2; // from and to points. - List allPositions = injectStepsIntoLegPositions(leg); + List allPositions = TravelerLocator.injectWaypointsIntoLegPositions(leg, leg.steps); assertEquals(expectedNumberOfPositions, allPositions.size()); } @@ -402,28 +499,41 @@ void cumulativeSegmentTimeMatchesWalkLegDuration() { assertEquals(busStopToJusticeCenterItinerary.legs.get(0).duration, cumulative, 0.01f); } - private static class TurnTrace { - Itinerary itinerary = adairAvenueToMonroeDriveItinerary; + private static class TraceData { TripStatus tripStatus = TripStatus.ON_SCHEDULE; Coordinates position; + int speed; String expectedInstruction; boolean isStartOfTrip; String message; - public TurnTrace(Coordinates position, String expectedInstruction, boolean isStartOfTrip, String message) { + public TraceData(Coordinates position, String expectedInstruction, boolean isStartOfTrip, String message) { this.position = position; this.expectedInstruction = expectedInstruction; this.isStartOfTrip = isStartOfTrip; this.message = message; } - public TurnTrace(TripStatus tripStatus, Coordinates position, String expectedInstruction, boolean isStartOfTrip, String message) { + public TraceData(Coordinates position, String expectedInstruction, String message) { + this(position, expectedInstruction, false, message); + } + + public TraceData(Coordinates position, int speed, String expectedInstruction, String message) { + this(position, expectedInstruction, false, message); + this.speed = speed; + } + + public TraceData(TripStatus tripStatus, Coordinates position, String expectedInstruction, boolean isStartOfTrip, String message) { this.tripStatus = tripStatus; this.position = position; this.expectedInstruction = expectedInstruction; this.isStartOfTrip = isStartOfTrip; this.message = message; } + + public TraceData(TripStatus tripStatus, Coordinates position, String expectedInstruction, String message) { + this(tripStatus, position, expectedInstruction, false, message); + } } private static List createSegmentsForLeg() { diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index 87456f799..e84b52407 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -15,6 +15,8 @@ import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.testutils.CommonTestUtils; import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment; +import org.opentripplanner.middleware.triptracker.instruction.TripInstruction; +import org.opentripplanner.middleware.triptracker.instruction.WaitForTransitInstruction; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.AgencyAction; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettBusOpNotificationMessage; @@ -75,7 +77,7 @@ void canNotifyBusOperatorForScheduledDeparture() { trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates(), busDepartureTime); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); - TripInstruction expectInstruction = new TripInstruction(busLeg, busDepartureTime, locale); + TripInstruction expectInstruction = new WaitForTransitInstruction(busLeg, busDepartureTime, locale); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); assertEquals(expectInstruction.build(), tripInstruction); @@ -96,7 +98,7 @@ void canNotifyBusOperatorForDelayedDeparture() throws CloneNotSupportedException String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); Leg busLeg = itinerary.legs.get(1); - TripInstruction expectInstruction = new TripInstruction(busLeg, timeAtEndOfWalkLeg, locale); + TripInstruction expectInstruction = new WaitForTransitInstruction(busLeg, timeAtEndOfWalkLeg, locale); assertEquals(expectInstruction.build(), tripInstruction); } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/TravelerLocatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/TravelerLocatorTest.java new file mode 100644 index 000000000..bdecd599c --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/triptracker/TravelerLocatorTest.java @@ -0,0 +1,39 @@ +package org.opentripplanner.middleware.triptracker; + +import org.junit.jupiter.api.Test; +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.otp.response.Place; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.stopsUntilEndOfLeg; + +class TravelerLocatorTest { + @Test + void testStopsUntilEndOfLeg() { + Leg leg = new Leg(); + leg.to = createPlace("FinalStop"); + leg.intermediateStops = List.of( + createPlace("Stop0"), + createPlace("Stop1"), + createPlace("Stop2"), + createPlace("Stop3"), + createPlace("Stop4"), + createPlace("Stop5"), + createPlace("Stop6") + ); + + for (int i = 0; i < leg.intermediateStops.size(); i++) { + Place stop = leg.intermediateStops.get(i); + assertEquals(7 - i, stopsUntilEndOfLeg(stop, leg), stop.stopId); + } + assertEquals(0, stopsUntilEndOfLeg(leg.to, leg), leg.to.stopId); + } + + Place createPlace(String id) { + Place place = new Place(); + place.stopId = id; + return place; + } +} diff --git a/src/test/resources/org/opentripplanner/middleware/controllers/api/27nb-midtown-to-ansley.json b/src/test/resources/org/opentripplanner/middleware/controllers/api/27nb-midtown-to-ansley.json new file mode 100644 index 000000000..c577b6e03 --- /dev/null +++ b/src/test/resources/org/opentripplanner/middleware/controllers/api/27nb-midtown-to-ansley.json @@ -0,0 +1,290 @@ +{ + "duration": 1242, + "startTime": 1718295118000, + "endTime": 1718296360000, + "walkTime": 645, + "transitTime": 297, + "waitingTime": 300, + "walkDistance": 696.05, + "walkLimitExceeded": false, + "elevationLost": 0, + "elevationGained": 0, + "transfers": 0, + "fare": { + "fare": {}, + "details": {} + }, + "legs": [ + { + "startTime": 1718295118000, + "endTime": 1718295183000, + "departureDelay": 0, + "arrivalDelay": 0, + "realTime": false, + "distance": 69.22, + "pathway": false, + "mode": "WALK", + "interlineWithPreviousLeg": false, + "from": { + "name": "Sheraton Midtown Atlanta at Colony Square, Atlanta, GA, USA", + "lon": -84.3809644, + "lat": 33.7863653, + "departure": 1718295118000, + "vertexType": "NORMAL" + }, + "to": { + "name": "14th St at Juniper St", + "lon": -84.381713, + "lat": 33.78645, + "departure": 1718295483000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69356", + "arrival": 1718295183000, + "stopCode": "902581" + }, + "legGeometry": { + "points": "a|emE`t_bO?B@l@?`A?`@I?", + "length": 6 + }, + "rentedBike": false, + "transitLeg": false, + "duration": 65, + "steps": [ + { + "distance": 69.22, + "relativeDirection": "DEPART", + "streetName": "sidewalk 635800755 (635800755, 9535944810→5996570686)", + "absoluteDirection": "WEST", + "stayOn": false, + "area": false, + "bogusName": true, + "lon": -84.3809643, + "lat": 33.7864111 + } + ] + }, + { + "startTime": 1718295483000, + "endTime": 1718295780000, + "departureDelay": 0, + "arrivalDelay": 0, + "realTime": false, + "distance": 1593.54, + "pathway": false, + "mode": "BUS", + "interlineWithPreviousLeg": false, + "from": { + "name": "14th St at Juniper St", + "lon": -84.381713, + "lat": 33.78645, + "departure": 1718295483000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69356", + "arrival": 1718295183000, + "stopIndex": 3, + "stopSequence": 4, + "stopCode": "902581" + }, + "to": { + "name": "Piedmont Ave NE at Monroe Dr", + "lon": -84.370392, + "lat": 33.795571, + "departure": 1718295780000, + "vertexType": "TRANSIT", + "stopId": "MARTA:99973628", + "arrival": 1718295780000, + "stopIndex": 11, + "stopSequence": 12, + "stopCode": "213258" + }, + "legGeometry": { + "points": "q|emEvx_bO?o@?a@DqJ??@eA?e@C_DS?gBA??C?_AA_@Iq@c@e@g@OQSQEG??SWeBoB_@c@k@o@??IMm@q@{@_AiCsBeFuD??MKWWm@c@_C{BoAkA??QQ_C{Bk@m@{AuAA???cC}BqAsA", + "length": 45 + }, + "transitLeg": true, + "duration": 297, + "intermediateStops": [ + { + "name": "14th St NE at 14th Pl", + "lon": -84.379461, + "lat": 33.786415, + "departure": 1718295521000, + "vertexType": "TRANSIT", + "stopId": "MARTA:99972303", + "arrival": 1718295521000, + "stopIndex": 4, + "stopSequence": 5, + "stopCode": "211955" + }, + { + "name": "Piedmont Ave NE at 14th St NE", + "lon": -84.37807, + "lat": 33.78709, + "departure": 1718295556000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69325", + "arrival": 1718295556000, + "stopIndex": 5, + "stopSequence": 6, + "stopCode": "902521" + }, + { + "name": "Piedmont Ave NE at 15th St NE", + "lon": -84.377418, + "lat": 33.788224, + "departure": 1718295583000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69326", + "arrival": 1718295583000, + "stopIndex": 6, + "stopSequence": 7, + "stopCode": "902522" + }, + { + "name": "Piedmont Ave NE at Prado", + "lon": -84.376309, + "lat": 33.7892, + "departure": 1718295610000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69328", + "arrival": 1718295610000, + "stopIndex": 7, + "stopSequence": 8, + "stopCode": "902523" + }, + { + "name": "Piedmont Ave NE at The Prado", + "lon": -84.374179, + "lat": 33.791636, + "departure": 1718295672000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69330", + "arrival": 1718295672000, + "stopIndex": 8, + "stopSequence": 9, + "stopCode": "902524" + }, + { + "name": "Piedmont Ave NE at Westminster Dr NE", + "lon": -84.37282, + "lat": 33.793095, + "departure": 1718295710000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69332", + "arrival": 1718295710000, + "stopIndex": 9, + "stopSequence": 10, + "stopCode": "902528" + }, + { + "name": "Piedmont Ave NE at Avery Dr NE", + "lon": -84.371451, + "lat": 33.79451, + "departure": 1718295747000, + "vertexType": "TRANSIT", + "stopId": "MARTA:69334", + "arrival": 1718295747000, + "stopIndex": 10, + "stopSequence": 11, + "stopCode": "902529" + } + ], + "steps": [], + "agencyName": "Metropolitan Atlanta Rapid Transit Authority", + "agencyUrl": "https://www.itsmarta.com", + "routeType": 3, + "routeId": "MARTA:21634", + "agencyId": "MARTA:MARTA", + "tripBlockId": "1153735", + "tripId": "MARTA:9101693", + "serviceDate": "2024-06-13", + "routeShortName": "27", + "routeLongName": "Cheshire Bridge Road", + "routeColor": "FF0080", + "routeTextColor": "000000", + "headsign": "Lenox Station" + }, + { + "startTime": 1718295780000, + "endTime": 1718296360000, + "departureDelay": 0, + "arrivalDelay": 0, + "realTime": false, + "distance": 626.83, + "pathway": false, + "mode": "WALK", + "interlineWithPreviousLeg": false, + "from": { + "name": "Piedmont Ave NE at Monroe Dr", + "lon": -84.370392, + "lat": 33.795571, + "departure": 1718295780000, + "vertexType": "TRANSIT", + "stopId": "MARTA:99973628", + "arrival": 1718295780000, + "stopCode": "213258" + }, + "to": { + "name": "33.79868, -84.37128", + "lon": -84.371285, + "lat": 33.798675, + "vertexType": "NORMAL", + "arrival": 1718296360000 + }, + "legGeometry": { + "points": "iugmE~q}aO`CcCAAIKKWKc@[gAAY?I?EBIKIUOQW[k@Wa@GKEEOL]Ve@^kCzBKJA@QPOHc@Ze@\\c@ZuFvDOJ", + "length": 31 + }, + "rentedBike": false, + "transitLeg": false, + "duration": 580, + "steps": [ + { + "distance": 101.06, + "relativeDirection": "DEPART", + "streetName": "path 226324516 (226324516, 2351728397→2351728398)", + "absoluteDirection": "NORTHEAST", + "stayOn": false, + "area": false, + "bogusName": true, + "lon": -84.369738, + "lat": 33.7949285 + }, + { + "distance": 95, + "relativeDirection": "LEFT", + "streetName": "path 226324515 (226324515, 2351728380→2938286004)", + "absoluteDirection": "NORTHEAST", + "stayOn": true, + "area": false, + "bogusName": true, + "lon": -84.3687447, + "lat": 33.7952379 + }, + { + "distance": 31.03, + "relativeDirection": "LEFT", + "streetName": "Monroe Drive Northeast (569578148, 2938286005→2351731996)", + "absoluteDirection": "NORTHWEST", + "stayOn": false, + "area": false, + "bogusName": false, + "lon": -84.3680125, + "lat": 33.7958264 + }, + { + "distance": 399.75, + "relativeDirection": "CONTINUE", + "streetName": "crossing over Worcester Drive NE (636623277, 6003263567→6003263568)", + "absoluteDirection": "NORTHWEST", + "stayOn": false, + "area": false, + "bogusName": false, + "lon": -84.3682076, + "lat": 33.7960535 + } + ] + } + ], + "alerts": [] +} \ No newline at end of file