Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

OTP-1382 GMAP Push notifications for leg transition #275

Merged
merged 24 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7ba414d
refactor(Initial work to check for major trip changes):
br648 Dec 3, 2024
7c466fc
refactor(Updated swagger docs and included the current leg on major c…
br648 Dec 3, 2024
21d9407
Merge branch 'dev' into feature/OTP-1382-notify-major-changes
br648 Dec 3, 2024
ee451f6
Merge branch 'dev' into feature/OTP-1382-notify-major-changes
br648 Dec 4, 2024
f9126c4
refactor(Added unit tests and made changes to wording.):
br648 Dec 4, 2024
ad42160
Merge branch 'dev' into feature/OTP-1382-notify-major-changes
br648 Dec 6, 2024
75f44c8
refactor(Various updates to monitor leg transition):
br648 Dec 9, 2024
926f994
refactor(Addressed unused params and methods. Updated Swagger docs.):
br648 Dec 9, 2024
128fb32
refactor(Fixed merge conflicts):
br648 Dec 10, 2024
91d6619
refactor(Refactor to create and process leg tranistion based notifica…
br648 Dec 10, 2024
c5582e1
refactor(Addressed PR feedback.):
br648 Dec 13, 2024
2638f04
refactor(Fixed merge conflicts and moved leg trans notify test into o…
br648 Dec 13, 2024
17363ac
refactor(Refactored otp user to provide display name):
br648 Dec 13, 2024
bc20b84
refactor(Moved various methods into the LegTransitionNotification cla…
br648 Dec 13, 2024
4e788c5
refactor(Created builder to handle basic traveler position needs):
br648 Dec 13, 2024
22ed611
refactor(Addressed PR feedback):
br648 Dec 16, 2024
5159804
refactor(Update to guard against missing target zoned date time when …
br648 Dec 16, 2024
c05e011
refactor(CheckMonitoredTrip): Update to preserve matching itinerary
br648 Dec 16, 2024
8340c43
refactor(Fixed merge conflicts):
br648 Dec 18, 2024
07ad8d8
improvement(hidden getDisplayName from Mongo and JSON serialisation):
br648 Dec 18, 2024
36b26af
chore(i18n): Add French text for new strings.
binh-dam-ibigroup Dec 18, 2024
4366df5
Merge branch 'dev' into feature/OTP-1382-notify-major-changes
br648 Dec 19, 2024
b3ab1fd
Merge branch 'feature/OTP-1382-notify-major-changes' of https://githu…
br648 Dec 19, 2024
eb2e49a
Merge branch 'dev' into feature/OTP-1382-notify-major-changes
br648 Dec 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public enum Message {
ACCEPT_DEPENDENT_EMAIL_SUBJECT,
ACCEPT_DEPENDENT_EMAIL_MANAGE,
ACCEPT_DEPENDENT_ERROR,
DESTINATION_CHANGE_NOTIFICATION,
br648 marked this conversation as resolved.
Show resolved Hide resolved
LABEL_AND_CONTENT,
MODE_CHANGE_NOTIFICATION,
ORIGIN_CHANGE_NOTIFICATION,
br648 marked this conversation as resolved.
Show resolved Hide resolved
SMS_STOP_NOTIFICATIONS,
TRIP_EMAIL_SUBJECT,
TRIP_EMAIL_SUBJECT_FOR_USER,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opentripplanner.middleware.models;

import org.opentripplanner.middleware.i18n.Message;
import org.opentripplanner.middleware.otp.response.Leg;
import org.opentripplanner.middleware.tripmonitor.jobs.NotificationType;
import org.opentripplanner.middleware.utils.DateTimeUtils;
import org.slf4j.Logger;
Expand Down Expand Up @@ -110,4 +111,46 @@ public static TripMonitorNotification createInitialReminderNotification(
)
);
}

/**
* Creates a notification that the leg has changed mode.
*/
public static TripMonitorNotification createModeChangeNotification(Leg expectedLeg, Leg changedLeg, Locale locale) {
return new TripMonitorNotification(
NotificationType.MODE_CHANGE,
String.format(
Message.MODE_CHANGE_NOTIFICATION.get(locale),
expectedLeg.mode,
changedLeg.mode,
expectedLeg.from.name
)
);
}

/**
* Creates a notification that the origin or destination has changed.
*/
public static TripMonitorNotification createStopChangeNotification(
br648 marked this conversation as resolved.
Show resolved Hide resolved
NotificationType delayType,
Leg expectedLeg,
Leg changedLeg,
Locale locale
) {
String message = null;
if (delayType == NotificationType.ORIGIN_CHANGE) {
message = String.format(
Message.ORIGIN_CHANGE_NOTIFICATION.get(locale),
expectedLeg.from.name,
changedLeg.from.name
);
} else if (delayType == NotificationType.DESTINATION_CHANGE) {
message = String.format(
Message.DESTINATION_CHANGE_NOTIFICATION.get(locale),
expectedLeg.from.name,
expectedLeg.to.name,
changedLeg.to.name
);
}
return (message != null) ? new TripMonitorNotification(delayType, message) : null;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package org.opentripplanner.middleware.tripmonitor.jobs;

import com.mongodb.lang.Nullable;
import org.opentripplanner.middleware.i18n.Message;
import org.opentripplanner.middleware.models.ItineraryExistence;
import org.opentripplanner.middleware.models.MonitoredTrip;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.models.TripMonitorAlertNotification;
import org.opentripplanner.middleware.models.TripMonitorNotification;
import org.opentripplanner.middleware.otp.OtpGraphQLVariables;
import org.opentripplanner.middleware.otp.response.Leg;
import org.opentripplanner.middleware.tripmonitor.TripStatus;
import org.opentripplanner.middleware.otp.OtpDispatcher;
import org.opentripplanner.middleware.otp.response.Itinerary;
Expand All @@ -33,11 +35,13 @@
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.IntStream;

import static org.opentripplanner.middleware.utils.I18nUtils.label;

Expand Down Expand Up @@ -248,6 +252,53 @@ private boolean isTrackingOngoing() {
return trip.journeyState.tripStatus == TripStatus.TRIP_ACTIVE && TripTrackingData.getOngoingTrackedJourney(trip.id) != null;
}

/**
* Whilst a trip is ongoing check for upcoming major changes and notify the traveler. These checks are specifically
* looking for individual differences between the trip itinerary and the latest OTP response itinerary.
*/
public void checkForMajorTripChanges(Leg nextLeg) {
var otpResponse = getOtpResponse();
int nextLegIndex = getLegIndex(nextLeg);
if (
otpResponse == null ||
nextLegIndex == -1 ||
(!trip.isOneTime() && !isTrackingOngoing() && trip.journeyState.matchingItinerary.hasEnded())
) {
// Required criteria to check for major changes not met.
return;
}

if (!hasMatchingItinerary(otpResponse)) {
// There is a change, workout if it is one of the major changes.
enqueueNotification(
checkTripForMajorChange(NotificationType.MODE_CHANGE, otpResponse.plan.itineraries, nextLegIndex),
checkTripForMajorChange(NotificationType.ORIGIN_CHANGE, otpResponse.plan.itineraries, nextLegIndex),
checkTripForMajorChange(NotificationType.DESTINATION_CHANGE, otpResponse.plan.itineraries, nextLegIndex)
);
}
sendNotifications();
}

/**
* Get the position of the next leg within the itinerary.
*/
private int getLegIndex(Leg nextLeg) {
return IntStream
.range(0, trip.itinerary.legs.size())
.filter(i -> ItineraryUtils.legsMatch(nextLeg, trip.itinerary.legs.get(i)))
.findFirst()
.orElse(-1);
}

/**
* Confirm that the OTP response contains an itinerary that matches the trip itinerary.
*/
private boolean hasMatchingItinerary(OtpResponse otpResponse) {
return otpResponse.plan.itineraries
.stream()
.anyMatch(i -> ItineraryUtils.itinerariesMatch(trip.itinerary, i));
}

/**
* Find and set the matching itinerary from the OTP response that matches the monitored trip's stored itinerary if a
* match exists.
Expand Down Expand Up @@ -498,6 +549,95 @@ public TripMonitorNotification checkTripForDelay(NotificationType delayType) {
return null;
}

/**
* Check for major upcoming change and notify the traveler.
*/
@Nullable
public TripMonitorNotification checkTripForMajorChange(
NotificationType delayType,
br648 marked this conversation as resolved.
Show resolved Hide resolved
List<Itinerary> itineraries,
int nextLegIndex
) {
var itineraryToCheck = getMatchingItinerary(delayType, itineraries);
if (itineraryToCheck != null) {
int majorChangeIndex = IntStream
.range(0, itineraryToCheck.legs.size())
.filter(i -> hasMajorChange(i, delayType, itineraryToCheck))
.findFirst()
.orElse(-1);

if (majorChangeIndex == -1 || majorChangeIndex <= (nextLegIndex - 1)) {
// No major change or the traveler has already passed it.
return null;
}

// Found a major change.
switch (delayType) {
case MODE_CHANGE:
return TripMonitorNotification.createModeChangeNotification(
trip.itinerary.legs.get(majorChangeIndex),
itineraryToCheck.legs.get(majorChangeIndex),
getOtpUserLocale()
);
case ORIGIN_CHANGE:
case DESTINATION_CHANGE:
return TripMonitorNotification.createStopChangeNotification(
delayType,
trip.itinerary.legs.get(majorChangeIndex),
itineraryToCheck.legs.get(majorChangeIndex),
getOtpUserLocale()
);
default:
return null;
}
}
return null;
}

/**
* Check the appropriate leg for a major change based on the provided exception.
*/
private boolean hasMajorChange(int index, NotificationType exception, Itinerary itineraryToCheck) {
if (exception == NotificationType.MODE_CHANGE) {
return !ItineraryUtils.legsModeMatches(trip.itinerary.legs.get(index), itineraryToCheck.legs.get(index));
} else if (exception == NotificationType.ORIGIN_CHANGE){
return !ItineraryUtils.stopsMatch(trip.itinerary.legs.get(index).from, itineraryToCheck.legs.get(index).from);
} else if (exception == NotificationType.DESTINATION_CHANGE){
return !ItineraryUtils.stopsMatch(trip.itinerary.legs.get(index).to, itineraryToCheck.legs.get(index).to);
}
return false;
}

/**
* A list of all notification types that are classed as major changes to an itinerary.
*/
private List<NotificationType> getAllMajorChangeNotificationTypes() {
return List.of(NotificationType.MODE_CHANGE, NotificationType.ORIGIN_CHANGE, NotificationType.DESTINATION_CHANGE);
br648 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Attempt to match on just a single exception, else use the wider exception criteria.
*/
@Nullable
private Itinerary getMatchingItinerary(NotificationType delayType, List<Itinerary> itineraries) {
br648 marked this conversation as resolved.
Show resolved Hide resolved
Itinerary matchingWithSingleException = getMatchingItineraryWithExceptions(itineraries, List.of(delayType));
if (matchingWithSingleException != null) {
return matchingWithSingleException;
}
return getMatchingItineraryWithExceptions(itineraries, getAllMajorChangeNotificationTypes());
}

/**
* Get the first matching itinerary ignoring exceptions.
*/
private Itinerary getMatchingItineraryWithExceptions(List<Itinerary> itineraries, List<NotificationType> exceptions) {
return itineraries
.stream()
.filter(i -> ItineraryUtils.itinerariesMatch(trip.itinerary, i, exceptions))
.findFirst()
.orElse(null);
}

/**
* Compose a message for any enqueued notifications and send to {@link OtpUser} based on their notification
* preferences.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ public enum NotificationType {
ITINERARY_CHANGED, // TODO
ALERT_FOUND,
ITINERARY_NOT_FOUND,
INITIAL_REMINDER
INITIAL_REMINDER,
MODE_CHANGE,
ORIGIN_CHANGE,
DESTINATION_CHANGE
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa
TravelerPosition travelerPosition = new TravelerPosition(
trackedJourney,
tripData.trip.journeyState.matchingItinerary,
Persistence.otpUsers.getById(tripData.trip.userId)
Persistence.otpUsers.getById(tripData.trip.userId),
tripData.trip
);
TripStatus tripStatus = TripStatus.getTripStatus(travelerPosition);
trackedJourney.lastLocation().tripStatus = tripStatus;
Expand Down Expand Up @@ -148,7 +149,8 @@ private static EndTrackingResponse completeJourney(TripTrackingData tripData, bo
TravelerPosition travelerPosition = new TravelerPosition(
tripData.journey,
tripData.trip.journeyState.matchingItinerary,
Persistence.otpUsers.getById(tripData.trip.userId)
Persistence.otpUsers.getById(tripData.trip.userId),
tripData.trip
);
cancelBusNotification(travelerPosition);
TrackedJourney trackedJourney = travelerPosition.trackedJourney;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.opentripplanner.middleware.otp.response.Leg;
import org.opentripplanner.middleware.otp.response.Place;
import org.opentripplanner.middleware.otp.response.Step;
import org.opentripplanner.middleware.tripmonitor.jobs.CheckMonitoredTrip;
import org.opentripplanner.middleware.triptracker.instruction.DeviatedInstruction;
import org.opentripplanner.middleware.triptracker.instruction.GetOffHereTransitInstruction;
import org.opentripplanner.middleware.triptracker.instruction.GetOffNextStopTransitInstruction;
Expand All @@ -16,6 +17,8 @@
import org.opentripplanner.middleware.utils.Coordinates;
import org.opentripplanner.middleware.utils.ConvertsToCoordinates;
import org.opentripplanner.middleware.utils.DateTimeUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import java.time.Duration;
Expand All @@ -41,6 +44,8 @@
*/
public class TravelerLocator {

private static final Logger LOG = LoggerFactory.getLogger(TravelerLocator.class);

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.
Expand Down Expand Up @@ -86,6 +91,12 @@ public static String getInstruction(
}
}
}

try {
new CheckMonitoredTrip(travelerPosition.trip).checkForMajorTripChanges(travelerPosition.nextLeg);
} catch (Exception e) {
LOG.error("Error encountered while checking for major trip changes.", e);
}
br648 marked this conversation as resolved.
Show resolved Hide resolved
return NO_INSTRUCTION;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opentripplanner.middleware.triptracker;

import org.opentripplanner.middleware.models.MonitoredTrip;
import org.opentripplanner.middleware.models.OtpUser;
import org.opentripplanner.middleware.models.TrackedJourney;
import org.opentripplanner.middleware.otp.response.Itinerary;
Expand Down Expand Up @@ -48,6 +49,14 @@ public class TravelerPosition {
/** The first leg of the trip. **/
public Leg firstLegOfTrip;

/** The trip related to the traveler's position */
public MonitoredTrip trip;

public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpUser otpUser, MonitoredTrip trip) {
this(trackedJourney, itinerary, otpUser);
this.trip = trip;
}

public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpUser otpUser) {
TrackingLocation lastLocation = trackedJourney.locations.get(trackedJourney.locations.size() - 1);
currentTime = lastLocation.timestamp.toInstant();
Expand Down
Loading
Loading