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

Dynamic Rerouting #286

Open
wants to merge 11 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.opentripplanner.middleware.triptracker.payload.TrackPayload;
import org.opentripplanner.middleware.triptracker.payload.UpdatedTrackingPayload;
import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse;
import org.opentripplanner.middleware.triptracker.response.RerouteResponse;
import org.opentripplanner.middleware.triptracker.response.TrackingResponse;
import org.opentripplanner.middleware.utils.HttpUtils;
import org.opentripplanner.middleware.utils.JsonUtils;
Expand Down Expand Up @@ -72,6 +73,12 @@ public void bind(final SparkSwagger restApi) {
.withProduces(JSON_ONLY)
.withRequestType(ForceEndTrackingPayload.class)
.withResponseType(EndTrackingResponse.class),
(request, response) -> ManageTripTracking.forciblyEndTracking(request), JsonUtils::toJson);
(request, response) -> ManageTripTracking.forciblyEndTracking(request), JsonUtils::toJson)
.post(path("/reroute")
.withDescription("Reroute from the traveler's current location to the original trip destination.")
.withProduces(JSON_ONLY)
.withRequestType(TrackPayload.class)
.withResponseType(RerouteResponse.class),
(request, response) -> ManageTripTracking.rerouteTracking(request), JsonUtils::toJson);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public class TrackedJourney extends Model {

public transient MonitoredTrip trip;

/** Holds the location and time a trip was rerouted. */
public Map<String, String> reroutings = new HashMap<>();

public static final String TRIP_ID_FIELD_NAME = "tripId";

public static final String LOCATIONS_FIELD_NAME = "locations";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -277,6 +278,13 @@ public void offsetTimes(long offsetMillis) {
}
}

/**
* Select the itinerary which has the shortest duration.
*/
public static Itinerary getShortestDuration(List<Itinerary> itineraries) {
return itineraries.stream().min(Comparator.comparingLong(itinerary -> itinerary.duration)).orElse(null);
}

/**
* Offsets the start time and all start times of each leg by the given value in milliseconds.
*/
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

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

If the traveler takes the trip, it will likely still within the active trip monitoring window, and trip monitoring will attempt to replace journeyState.matchingItinerary with an itinerary matching the original one. To prevent that, if rerouting has occured, getQueryParamsForTargetZonedDateTime should also plug the most recent rerouting location that is saved in TrackedJourney.

Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,9 @@ private boolean isTrackingOngoing() {
* to that returned from OTP, so the existing trip itinerary is used and therefore preserved.
*/
public void processLegTransition(NotificationType notificationType, TravelerPosition travelerPosition) throws CloneNotSupportedException {
matchingItinerary = trip.itinerary.clone();
if (trip.journeyState.matchingItinerary != null) {
matchingItinerary = trip.journeyState.matchingItinerary.clone();
}
OtpUser tripOwner = getOtpUser();
Set<OtpUser> notifyUsers = getLegTransitionNotifyUsers(trip);
notifyUsers.forEach(observer -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,45 @@
package org.opentripplanner.middleware.triptracker;

import org.eclipse.jetty.http.HttpStatus;
import org.opentripplanner.middleware.OtpMiddlewareMain;
import org.opentripplanner.middleware.models.LegTransitionNotification;
import org.opentripplanner.middleware.models.TrackedJourney;
import org.opentripplanner.middleware.otp.OtpDispatcher;
import org.opentripplanner.middleware.otp.OtpGraphQLVariables;
import org.opentripplanner.middleware.otp.response.Itinerary;
import org.opentripplanner.middleware.otp.response.Leg;
import org.opentripplanner.middleware.otp.response.OtpResponse;
import org.opentripplanner.middleware.otp.response.TripPlan;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.triptracker.instruction.SelfLegInstruction;
import org.opentripplanner.middleware.triptracker.interactions.TripActions;
import org.opentripplanner.middleware.triptracker.instruction.TripInstruction;
import org.opentripplanner.middleware.triptracker.interactions.TripActions;
import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions;
import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse;
import org.opentripplanner.middleware.triptracker.response.RerouteResponse;
import org.opentripplanner.middleware.triptracker.response.TrackingResponse;
import org.opentripplanner.middleware.utils.Coordinates;
import spark.Request;

import java.time.LocalDateTime;
import java.util.function.Supplier;

import static org.opentripplanner.middleware.otp.response.Itinerary.getShortestDuration;
import static org.opentripplanner.middleware.triptracker.TravelerLocator.isAtStartOfLeg;
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.triptracker.TravelerLocator.isAtStartOfLeg;
import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt;
import static org.opentripplanner.middleware.utils.DateTimeUtils.getTimeNowAsString;
import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteGtfsIdFromLeg;
import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg;
import static org.opentripplanner.middleware.utils.ItineraryUtils.legsMatch;
import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt;

public class ManageTripTracking {

public static Supplier<OtpResponse> otpResponseProviderOverride = null;
private static OtpGraphQLVariables rerouteVariables = null;

private ManageTripTracking() {
}

Expand Down Expand Up @@ -52,7 +68,20 @@ public static TrackingResponse startTracking(Request request) {
return null;
}

private static TrackingResponse doUpdateTracking(Request request, TripTrackingData tripData, boolean create) {
private static TrackingResponse doUpdateTracking(
Request request,
TripTrackingData tripData,
boolean create
) {
return doUpdateTracking(request, tripData, create, false);
}

private static TrackingResponse doUpdateTracking(
Request request,
TripTrackingData tripData,
boolean create,
boolean rerouted
) {
try {
TrackedJourney trackedJourney;
if (create) {
Expand Down Expand Up @@ -84,7 +113,11 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa
LegTransitionNotification.checkForLegTransition(tripStatus, travelerPosition, tripData.trip);

// Provide response.
TripInstruction instruction = TravelerLocator.getInstruction(tripStatus, travelerPosition, create);
TripInstruction instruction = TravelerLocator.getInstruction(
tripStatus,
travelerPosition,
create || rerouted
);

// Perform interactions such as triggering traffic signals when approaching segments so configured.
// It is assumed to be ok to repeatedly perform the interaction.
Expand Down Expand Up @@ -159,6 +192,115 @@ public static EndTrackingResponse forciblyEndTracking(Request request) {
return null;
}

/**
* Attempt to reroute trip and provide appropriate response.
*/
public static RerouteResponse rerouteTracking(Request request) {
TripTrackingData tripData = TripTrackingData.fromRequestTripId(request);
if (tripData != null) {
var trackedJourney = tripData.journey;
if (trackedJourney != null) {
var reroutedItinerary = rerouteTrip(tripData);
if (reroutedItinerary != null) {
return new RerouteResponse(
doUpdateTracking(request, tripData, false, true),
reroutedItinerary
);
} else {
return new RerouteResponse(
TRIP_TRACKING_UPDATE_FREQUENCY_SECONDS,
"No itinerary found!",
trackedJourney.id,
TripStatus.DEVIATED.name(),
null
);
}
} else {
logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Journey for provided trip id does not exist!");
return null;
}
}
return null;
}

/**
* Attempt to reroute from the traveler's current location to the trip's original destination.
*/
public static Itinerary rerouteTrip(TripTrackingData tripData) {
if (tripData != null) {
var trackedJourney = tripData.journey;
if (trackedJourney != null) {
return updateTripWithNewItinerary(trackedJourney);
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should probably notify companions/observers of that change.

}
}
return null;
}

/**
* Record the location and time (as Strings to appease Mongo) a trip was rerouted and reset deviations.
*/
private static void registerRerouting(TrackedJourney trackedJourney) {
trackedJourney.reroutings.put(
new Coordinates(trackedJourney.lastLocation()).getCoordinates(),
LocalDateTime.now().toString()
);
Persistence.trackedJourneys.replace(trackedJourney.id, trackedJourney);
}

/**
* Retrieve a new itinerary from OTP and update the associated trip's matching itinerary.
*/
private static Itinerary updateTripWithNewItinerary(TrackedJourney trackedJourney) {
Itinerary itinerary = getItineraryFromOtpResponse(trackedJourney);
if (itinerary != null) {
trackedJourney.trip.journeyState.matchingItinerary = itinerary;
Persistence.monitoredTrips.replace(trackedJourney.trip.id, trackedJourney.trip);
registerRerouting(trackedJourney);
return itinerary;
}
return null;
}

/**
* Get the itinerary with the shortest duration returned from OTP using the new start location and current time.
*/
private static Itinerary getItineraryFromOtpResponse(TrackedJourney trackedJourney) {
try {
Supplier<OtpResponse> otpResponseProvider = getOtpResponseProvider();
rerouteVariables = setOtpGraphQLVariables(
trackedJourney.trip.otp2QueryParams, new Coordinates(trackedJourney.lastLocation())
);
TripPlan plan = otpResponseProvider.get().plan;
return plan == null ? null : getShortestDuration(plan.itineraries);
} catch (Exception e) {
return null;
}
}

/**
* Define the new reroute variables related to the traveler's current location to be passed to OTP.
*/
public static OtpGraphQLVariables setOtpGraphQLVariables(OtpGraphQLVariables originalTripVariables, Coordinates from) {
OtpGraphQLVariables query = originalTripVariables.clone();
query.fromPlace = from.getCoordinates();
query.time = getTimeNowAsString();
return query;
}

/**
* Define the OTP response source. This will either be an OTP server response or a mocked response for testing.
*/
private static Supplier<OtpResponse> getOtpResponseProvider() {
return OtpMiddlewareMain.inTestEnvironment && otpResponseProviderOverride != null
? otpResponseProviderOverride
: ManageTripTracking::getOtpResponse;
}

/** Default implementation for OtpResponse provider that actually invokes the OTP server. */
private static OtpResponse getOtpResponse() {
return OtpDispatcher.sendOtpRequestWithErrorHandling(rerouteVariables);
}

/**
* Complete a journey by defining the ending type, time and condition. Also cancel possible upcoming bus
* notification.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package org.opentripplanner.middleware.triptracker.response;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.opentripplanner.middleware.otp.response.Itinerary;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class RerouteResponse extends TrackingResponse {

public final Itinerary itinerary;

public RerouteResponse(int frequencySeconds, String instruction, String journeyId, String tripStatus, Itinerary itinerary) {
super(frequencySeconds, instruction, journeyId, tripStatus);
this.itinerary = itinerary;
}

public RerouteResponse(TrackingResponse trackingResponse, Itinerary itinerary) {
super(trackingResponse);
this.itinerary = itinerary;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ public TrackingResponse() {
// do nothing.
}

public TrackingResponse(TrackingResponse trackingResponse) {
this.frequencySeconds = trackingResponse.frequencySeconds;
this.instruction = trackingResponse.instruction;
this.journeyId = trackingResponse.journeyId;
this.tripStatus = trackingResponse.tripStatus;
}

public TrackingResponse(int frequencySeconds, String instruction, String journeyId, String tripStatus) {
this.frequencySeconds = frequencySeconds;
this.instruction = instruction;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ public Coordinates(Place place) {
this.lon = place.lon;
}

public String getCoordinates() {
return String.format("%s,%s", lat, lon);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ public static LocalDate nowAsLocalDate() {
return LocalDate.now(clock);
}

public static String getTimeNowAsString() {
LocalTime currentTime = LocalTime.now(clock);
return currentTime.format(DateTimeFormatter.ofPattern("HH:mm"));
}

/**
* Returns the current LocalDateTime according to the currently set Clock.
*/
Expand All @@ -131,7 +136,7 @@ public static ZonedDateTime nowAsZonedDateTime(ZoneId zoneId) {
}

/**
* Returns the current time in milliseconds according the the current set Clock.
* Returns the current time in milliseconds according to the current set Clock.
*/
public static long currentTimeMillis() {
return clock.millis();
Expand Down
Loading
Loading