Skip to content

Commit

Permalink
Merge pull request #285 from ibi-group/combine-departure-arrival-delays
Browse files Browse the repository at this point in the history
Combine departure+arrival delays
  • Loading branch information
binh-dam-ibigroup authored Jan 10, 2025
2 parents 742c40d + 98e4879 commit 8f981d7
Show file tree
Hide file tree
Showing 10 changed files with 325 additions and 230 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public enum Message {
TRIP_ALERT_ALL_RESOLVED,
TRIP_ALERT_ALL_RESOLVED_WITH_LIST,
TRIP_DELAY_NOTIFICATION,
TRIP_DELAY_NOTIFICATION_LONG,
TRIP_DELAY_ARRIVE,
TRIP_DELAY_DEPART,
TRIP_DELAY_ON_TIME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
import java.util.Locale;
import java.util.Objects;

import static org.opentripplanner.middleware.i18n.Message.TRIP_DELAY_ARRIVE;
import static org.opentripplanner.middleware.i18n.Message.TRIP_DELAY_DEPART;
import static org.opentripplanner.middleware.i18n.Message.TRIP_DELAY_EARLY;
import static org.opentripplanner.middleware.i18n.Message.TRIP_DELAY_LATE;

/**
* Contains information about the type and details of messages to be sent to users about their {@link MonitoredTrip}s.
*/
Expand Down Expand Up @@ -44,50 +49,69 @@ public TripMonitorNotification(NotificationType type, String body) {
* Create a new notification about a change in the trip's arrival or departure time exceeding a threshold.
*
* @param delayInMinutes The delay in minutes (negative values indicate early times).
* @param targetDatetime The actual arrival or departure of the trip
* @param startTime The actual departure time of the trip
* @param arrivalTime The actual arrival time of the trip
* @param delayType Whether the notification is for an arrival or departure delay
* @param locale The locale in which to display the message
*/
public static TripMonitorNotification createDelayNotification(
long delayInMinutes,
Date targetDatetime,
Date startTime,
Date arrivalTime,
NotificationType delayType,
Locale locale
) {
if (delayType != NotificationType.ARRIVAL_DELAY && delayType != NotificationType.DEPARTURE_DELAY) {
if (!delayType.isDelayNotification()) {
LOG.error("Delay notification not permitted for type {}", delayType);
return null;
}
String delayHumanTime;
long absoluteMinutes = Math.abs(delayInMinutes);
if (absoluteMinutes <= 1) {
delayHumanTime = Message.TRIP_DELAY_ON_TIME.get(locale);
} else {
// Delays start at two minutes (plural form).
String minutesString = String.format(
Message.TRIP_DELAY_MINUTES.get(locale),
absoluteMinutes

String delayHumanTime = getTimeAdherenceText(delayInMinutes, locale);

if (delayType == NotificationType.DEPARTURE_AND_ARRIVAL_DELAY) {
return new TripMonitorNotification(
delayType,
String.format(
Message.TRIP_DELAY_NOTIFICATION_LONG.get(locale),
STOPWATCH_ICON,
TRIP_DELAY_DEPART.get(locale),
delayHumanTime,
DateTimeUtils.formatShortDate(startTime, locale),
DateTimeUtils.formatShortDate(arrivalTime, locale)
)
);
if (delayInMinutes > 0) {
delayHumanTime = String.format(Message.TRIP_DELAY_LATE.get(locale), minutesString);
} else {
delayHumanTime = String.format(Message.TRIP_DELAY_EARLY.get(locale), minutesString);
}
}

boolean isArrivalDelay = delayType == NotificationType.ARRIVAL_DELAY;
return new TripMonitorNotification(
delayType,
String.format(
Message.TRIP_DELAY_NOTIFICATION.get(locale),
STOPWATCH_ICON,
delayType == NotificationType.ARRIVAL_DELAY
? Message.TRIP_DELAY_ARRIVE.get(locale)
: Message.TRIP_DELAY_DEPART.get(locale),
(isArrivalDelay ? TRIP_DELAY_ARRIVE : TRIP_DELAY_DEPART).get(locale),
delayHumanTime,
DateTimeUtils.formatShortDate(targetDatetime, locale)
DateTimeUtils.formatShortDate(isArrivalDelay ? arrivalTime : startTime, locale)
)
);
}

/**
* @return A string describing time adherence (e.g. "about on time", "2 minutes early", "5 minutes late").
*/
private static String getTimeAdherenceText(long delayInMinutes, Locale locale) {
long absoluteMinutes = Math.abs(delayInMinutes);
if (absoluteMinutes <= 1) {
return Message.TRIP_DELAY_ON_TIME.get(locale);
} else {
// Delays start at two minutes (plural form).
String minutesString = String.format(Message.TRIP_DELAY_MINUTES.get(locale), absoluteMinutes);
return String.format(
(delayInMinutes > 0 ? TRIP_DELAY_LATE : TRIP_DELAY_EARLY).get(locale),
minutesString
);
}
}

/**
* Creates a notification that the itinerary was not found on either the current day or any day of the week.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,24 @@ public void offsetTimes(long offsetMillis) {
leg.endTime = new Date(leg.endTime.getTime() + offsetMillis);
}
}

/**
* Offsets the start time and all start times of each leg by the given value in milliseconds.
*/
public void offsetStart(long offsetMillis) {
startTime = new Date(startTime.getTime() + offsetMillis);
for (Leg leg : legs) {
leg.startTime = new Date(leg.startTime.getTime() + offsetMillis);
}
}

/**
* Offsets the end time and all end times of each leg by the given value in milliseconds.
*/
public void offsetEnd(long offsetMillis) {
endTime = new Date(endTime.getTime() + offsetMillis);
for (Leg leg : legs) {
leg.endTime = new Date(leg.endTime.getTime() + offsetMillis);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import java.util.function.Supplier;

import static org.opentripplanner.middleware.models.LegTransitionNotification.getLegTransitionNotifyUsers;
import static org.opentripplanner.middleware.utils.DateTimeUtils.diffInMinutes;

/**
* This job handles the primary functions for checking a {@link MonitoredTrip}, including:
Expand Down Expand Up @@ -221,8 +222,7 @@ private void runCheckLogic() {
// Check for notifications related to service alerts.
checkTripForNewAlerts(),
// Check for notifications related to delays.
checkTripForDelay(NotificationType.DEPARTURE_DELAY),
checkTripForDelay(NotificationType.ARRIVAL_DELAY)
checkTripForDelays()
);
}

Expand Down Expand Up @@ -464,60 +464,52 @@ public TripMonitorNotification checkTripForNewAlerts() {
* - Current realtime departure time: 5:58pm
* - Result: The threshold is met, so a notification is sent.
*/
public TripMonitorNotification checkTripForDelay(NotificationType delayType) {
// the target time for trip depending on notification type (the matching itinerary's start time if checking for
// departure delay, or the matching itinerary's end time if checking for arrival delay)
Date matchingItineraryTargetTime;

// the current baseline epoch millis to check if a new threshold has been met (the baseline departure time if
// checking for departure delay, or the baseline arrival time if checking for arrival delay)
long baselineItineraryTargetEpochMillis;

// the scheduled target epoch millis that the trip would've started or ended at (the scheduled departure time if
// checking for departure delay, or the scheduled arrival time if checking for arrival delay)
long scheduledTargetTimeEpochMillis;

// the threshold of deviation to check (this can be different for arrival or departure thresholds).
int deviationThreshold;

if (delayType == NotificationType.DEPARTURE_DELAY) {
matchingItineraryTargetTime = matchingItinerary.startTime;
baselineItineraryTargetEpochMillis = journeyState.baselineDepartureTimeEpochMillis;
scheduledTargetTimeEpochMillis = journeyState.scheduledDepartureTimeEpochMillis;
deviationThreshold = trip.departureVarianceMinutesThreshold;
} else {
matchingItineraryTargetTime = matchingItinerary.endTime;
baselineItineraryTargetEpochMillis = journeyState.baselineArrivalTimeEpochMillis;
scheduledTargetTimeEpochMillis = journeyState.scheduledArrivalTimeEpochMillis;
deviationThreshold = trip.arrivalVarianceMinutesThreshold;
}

// calculate absolute deviation of current itinerary target time from the baseline target time in minutes
long deviationAbsoluteMinutes = Math.abs(
TimeUnit.MINUTES.convert(
baselineItineraryTargetEpochMillis - matchingItineraryTargetTime.getTime(),
TimeUnit.MILLISECONDS
)
);

// check if threshold met
if (deviationAbsoluteMinutes >= deviationThreshold) {
// threshold met, set new baseline time
if (delayType == NotificationType.DEPARTURE_DELAY) {
journeyState.baselineDepartureTimeEpochMillis = matchingItineraryTargetTime.getTime();
} else {
journeyState.baselineArrivalTimeEpochMillis = matchingItineraryTargetTime.getTime();
}

// create and return notification
long delayMinutes = TimeUnit.MINUTES.convert(
matchingItineraryTargetTime.getTime() - scheduledTargetTimeEpochMillis,
TimeUnit.MILLISECONDS
public TripMonitorNotification checkTripForDelays() {
Date newStartDate = matchingItinerary.startTime;
Date newEndDate = matchingItinerary.endTime;
long newStartTime = newStartDate.getTime();
long newEndTime = newEndDate.getTime();

long departureDelay = Math.abs(diffInMinutes(journeyState.baselineDepartureTimeEpochMillis, newStartTime));
long arrivalDelay = Math.abs(diffInMinutes(journeyState.baselineArrivalTimeEpochMillis, newEndTime));

// For each of the cases below, use the scheduled departure/arrival epoch millis of the trip
// (the scheduled departure/arrival time if checking for departure/arrival delay, respectively).

boolean isDepartureDelay = departureDelay >= trip.departureVarianceMinutesThreshold;
boolean isArrivalDelay = arrivalDelay >= trip.arrivalVarianceMinutesThreshold;
if (departureDelay == arrivalDelay && (isDepartureDelay || isArrivalDelay)) {
// Do a combined departure/arrival delay notification.
long delayMinutes = diffInMinutes(newStartTime, journeyState.scheduledDepartureTimeEpochMillis);
journeyState.baselineDepartureTimeEpochMillis = newStartTime;
journeyState.baselineArrivalTimeEpochMillis = newEndTime;
return TripMonitorNotification.createDelayNotification(
delayMinutes,
newStartDate,
newEndDate,
NotificationType.DEPARTURE_AND_ARRIVAL_DELAY,
getOtpUserLocale()
);
} else if (isDepartureDelay) {
// Do a departure delay notification.
long delayMinutes = diffInMinutes(newStartTime, journeyState.scheduledDepartureTimeEpochMillis);
journeyState.baselineDepartureTimeEpochMillis = newStartTime;
return TripMonitorNotification.createDelayNotification(
delayMinutes,
newStartDate,
newEndDate,
NotificationType.DEPARTURE_DELAY,
getOtpUserLocale()
);
} else if (isArrivalDelay) {
// Do an arrival delay notification.
long delayMinutes = diffInMinutes(newEndTime, journeyState.scheduledArrivalTimeEpochMillis);
journeyState.baselineArrivalTimeEpochMillis = newEndTime;
return TripMonitorNotification.createDelayNotification(
delayMinutes,
matchingItineraryTargetTime,
delayType,
newStartDate,
newEndDate,
NotificationType.ARRIVAL_DELAY,
getOtpUserLocale()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,15 @@ public enum NotificationType {
INITIAL_REMINDER,
MODE_CHANGE_NOTIFICATION,
DEPARTED_NOTIFICATION,
ARRIVED_NOTIFICATION
ARRIVED_NOTIFICATION,
DEPARTURE_AND_ARRIVAL_DELAY;

/**
* @return true if notification type corresponds to a delay notification, false otherwise.
*/
public boolean isDelayNotification() {
return this == DEPARTURE_AND_ARRIVAL_DELAY ||
this == ARRIVAL_DELAY ||
this == DEPARTURE_DELAY;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -332,4 +333,12 @@ public static LocalDateTime getPreviousDayFrom(LocalDateTime dateTime) {
public static Date convertDateFromSecondsToMillis(Date date) {
return Date.from(Instant.ofEpochSecond(date.getTime()));
}

/**
* Compute the diff, in minutes, between two timestamps.
*/
public static long diffInMinutes(long millis1, long millis2) {
// Calculate the deviation of current itinerary target time from the baseline target time in minutes
return TimeUnit.MINUTES.convert(millis1 - millis2, TimeUnit.MILLISECONDS);
}
}
1 change: 1 addition & 0 deletions src/main/resources/Message.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ TRIP_ALERT_NOTIFICATION = %s Your trip has %s.
TRIP_ALERT_ALL_RESOLVED = %s All clear! All alerts on your itinerary were all resolved.
TRIP_ALERT_ALL_RESOLVED_WITH_LIST = All clear! The following alerts on your itinerary were all resolved:
TRIP_DELAY_NOTIFICATION = %s Your trip is now predicted to %s %s (at %s).
TRIP_DELAY_NOTIFICATION_LONG = %s Your trip is now predicted to %s %s at %s (Now arriving at %s).
TRIP_DELAY_ARRIVE = arrive
TRIP_DELAY_DEPART = depart
TRIP_DELAY_ON_TIME = about on time
Expand Down
Loading

0 comments on commit 8f981d7

Please sign in to comment.