Skip to content

Commit

Permalink
Merge pull request #238 from ibi-group/bus-onboard-instructions
Browse files Browse the repository at this point in the history
Bus on-board instructions [OTP-1223]
  • Loading branch information
binh-dam-ibigroup authored Jul 16, 2024
2 parents adc630e + 765ff64 commit 4be49d0
Show file tree
Hide file tree
Showing 22 changed files with 980 additions and 314 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@

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;

/**
* Plan response, place information. Produced using http://www.jsonschema2pojo.org/
*/
@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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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() {
}

Expand All @@ -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;
}
Expand All @@ -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.
*/
Expand All @@ -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(
Expand All @@ -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;
}

Expand All @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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<Coordinates> legPositions = injectStepsIntoLegPositions(travelerPosition.expectedLeg);
int pointIndex = getNearestPointIndex(legPositions, travelerPosition.currentPosition);
return (pointIndex != -1)
? getNextStep(travelerPosition.expectedLeg, legPositions, pointIndex)
: null;
}
public static <T extends ConvertsToCoordinates> T getNextWayPoint(List<Coordinates> positions, List<T> steps, int startIndex) {
Map<T, Coordinates> 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<Coordinates> 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();
}
}
}
Expand All @@ -228,49 +281,76 @@ private static int getNearestPointIndex(List<Coordinates> positions, Coordinates
return pointIndex;
}

private static List<Place> getIntermediateAndLastStop(Leg leg) {
ArrayList<Place> 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.
* <p>
* 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<Coordinates> injectStepsIntoLegPositions(Leg leg) {
public static List<Coordinates> injectWaypointsIntoLegPositions(Leg leg, List<? extends ConvertsToCoordinates> steps) {
List<Coordinates> allPositions = getAllLegPositions(leg);
List<Step> injectedSteps = new ArrayList<>();
List<Coordinates> waypoints = steps
.stream()
.map(ConvertsToCoordinates::toCoordinates)
.collect(Collectors.toList());
List<Coordinates> injectedPoints = new ArrayList<>();
List<Coordinates> 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);
}
}
}

// 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<Step> 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 extends ConvertsToCoordinates> T snapToWaypoint(TravelerPosition pos, List<T> waypoints, boolean excludeCurrent) {
List<Coordinates> 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 extends ConvertsToCoordinates> T snapToWaypoint(TravelerPosition pos, List<T> waypoints) {
return snapToWaypoint(pos, waypoints, false);
}

/**
* Get a list containing all positions on a leg.
*/
Expand Down Expand Up @@ -333,4 +413,11 @@ public static boolean isWithinExclusionZone(Coordinates position, List<Step> ste
}
return false;
}

public static int stopsUntilEndOfLeg(Place stop, Leg leg) {
if (stop == leg.to) return 0;

List<Place> stops = leg.intermediateStops;
return stops.size() - stops.indexOf(stop);
}
}
Loading

0 comments on commit 4be49d0

Please sign in to comment.