diff --git a/src/main/java/org/opentripplanner/middleware/i18n/Message.java b/src/main/java/org/opentripplanner/middleware/i18n/Message.java index 5449885b2..fa43c1532 100644 --- a/src/main/java/org/opentripplanner/middleware/i18n/Message.java +++ b/src/main/java/org/opentripplanner/middleware/i18n/Message.java @@ -19,7 +19,10 @@ public enum Message { ACCEPT_DEPENDENT_EMAIL_SUBJECT, ACCEPT_DEPENDENT_EMAIL_MANAGE, ACCEPT_DEPENDENT_ERROR, + ARRIVED_NOTIFICATION, + DEPARTED_NOTIFICATION, LABEL_AND_CONTENT, + MODE_CHANGE_NOTIFICATION, SMS_STOP_NOTIFICATIONS, TRIP_EMAIL_SUBJECT, TRIP_EMAIL_SUBJECT_FOR_USER, @@ -47,6 +50,7 @@ public enum Message { TRIP_INVITE_COMPANION, TRIP_INVITE_PRIMARY_TRAVELER, TRIP_INVITE_OBSERVER, + TRIP_NAME_UNDEFINED, TRIP_NOT_FOUND_NOTIFICATION, TRIP_NO_LONGER_POSSIBLE_NOTIFICATION, TRIP_REMINDER_NOTIFICATION, diff --git a/src/main/java/org/opentripplanner/middleware/models/LegTransitionNotification.java b/src/main/java/org/opentripplanner/middleware/models/LegTransitionNotification.java new file mode 100644 index 000000000..a3ce72350 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/models/LegTransitionNotification.java @@ -0,0 +1,154 @@ +package org.opentripplanner.middleware.models; + +import org.opentripplanner.middleware.i18n.Message; +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.tripmonitor.jobs.CheckMonitoredTrip; +import org.opentripplanner.middleware.tripmonitor.jobs.NotificationType; +import org.opentripplanner.middleware.triptracker.TravelerPosition; +import org.opentripplanner.middleware.triptracker.TripStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; +import java.util.HashSet; +import java.util.Locale; +import java.util.Set; + +import static com.mongodb.client.model.Filters.eq; +import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.MODE_CHANGE_NOTIFICATION; +import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.ARRIVED_NOTIFICATION; +import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.DEPARTED_NOTIFICATION; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.hasRequiredTransitLeg; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.hasRequiredTripStatus; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.hasRequiredWalkLeg; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.isApproachingEndOfLeg; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.isAtStartOfLeg; + +public class LegTransitionNotification { + private static final Logger LOG = LoggerFactory.getLogger(LegTransitionNotification.class); + + public String travelerName; + public NotificationType notificationType; + public TravelerPosition travelerPosition; + public Locale observerLocale; + public TripMonitorNotification tripMonitorNotification; + + public LegTransitionNotification( + String travelerName, + NotificationType notificationType, + TravelerPosition travelerPosition, + Locale observerLocale + ) { + this.travelerName = travelerName; + this.notificationType = notificationType; + this.travelerPosition = travelerPosition; + this.observerLocale = observerLocale; + this.tripMonitorNotification = createTripMonitorNotification(notificationType); + } + + /** + * Create {@link TripMonitorNotification} for leg transition based on notification type. + */ + @Nullable + private TripMonitorNotification createTripMonitorNotification(NotificationType notificationType) { + String body; + switch (notificationType) { + case MODE_CHANGE_NOTIFICATION: + body = String.format( + Message.MODE_CHANGE_NOTIFICATION.get(observerLocale), + travelerName, + travelerPosition.expectedLeg.to.name + ); + break; + case DEPARTED_NOTIFICATION: + body = String.format( + Message.DEPARTED_NOTIFICATION.get(observerLocale), + travelerName, + travelerPosition.expectedLeg.from.name + ); + break; + case ARRIVED_NOTIFICATION: + body = String.format( + Message.ARRIVED_NOTIFICATION.get(observerLocale), + travelerName, + travelerPosition.expectedLeg.to.name + ); + break; + default: + body = null; + } + return (body != null) ? new TripMonitorNotification(notificationType, body) : null; + } + + /** + * Get a list of users that should be notified of a traveler's leg transition. + */ + public static Set getLegTransitionNotifyUsers(MonitoredTrip trip) { + Set notifyUsers = new HashSet<>(); + + if (trip.ownedByPrimary() && trip.companion != null) { + notifyUsers.add(Persistence.otpUsers.getOneFiltered(eq("email", trip.companion.email))); + } else if (trip.ownedByCompanion() && trip.primary != null) { + notifyUsers.add(Persistence.otpUsers.getById(trip.primary.userId)); + } + + trip.observers.forEach(observer -> { + if (observer.isConfirmed()) { + notifyUsers.add(Persistence.otpUsers.getOneFiltered(eq("email", observer.email))); + } + }); + + return notifyUsers; + } + + /** + * If a traveler is on the route (not deviated), check for possible leg transition notification. + */ + public static void checkForLegTransition( + TripStatus tripStatus, + TravelerPosition travelerPosition, + MonitoredTrip trip + ) { + if ( + hasRequiredTripStatus(tripStatus) && + (hasRequiredWalkLeg(travelerPosition) || hasRequiredTransitLeg(travelerPosition)) + ) { + NotificationType notificationType = getLegTransitionNotificationType(travelerPosition); + if (notificationType != null) { + try { + new CheckMonitoredTrip(trip).processLegTransition(notificationType, travelerPosition); + } catch (CloneNotSupportedException e) { + LOG.error("Error encountered while checking leg transition.", e); + } + } + } + } + + /** + * Depending on the traveler's proximity to the start/end of a leg, return the appropriate notification type. + */ + private static NotificationType getLegTransitionNotificationType(TravelerPosition travelerPosition) { + if (isAtStartOfLeg(travelerPosition)) { + return DEPARTED_NOTIFICATION; + } else if (isApproachingEndOfLeg(travelerPosition)) { + if (hasModeChanged(travelerPosition)) { + return MODE_CHANGE_NOTIFICATION; + } + return ARRIVED_NOTIFICATION; + } + return null; + } + + /** + * The traveler is at the end of the current leg and the mode has changed between this and the next leg. + */ + private static boolean hasModeChanged(TravelerPosition travelerPosition) { + Leg nextLeg = travelerPosition.nextLeg; + Leg expectedLeg = travelerPosition.expectedLeg; + return + isApproachingEndOfLeg(travelerPosition) && + nextLeg != null && + !nextLeg.mode.equalsIgnoreCase(expectedLeg.mode); + } +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 2a845273e..375431603 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -32,6 +32,8 @@ import java.util.function.Function; import java.util.stream.Collectors; +import static com.mongodb.client.model.Filters.eq; + /** * A monitored trip represents a trip a user would like to receive notification on if affected by a delay and/or route * change. @@ -510,4 +512,19 @@ public TripUsers(MobilityProfileLite primary, RelatedUser companion, List findLastTripSurveyNotificationSent() { return tripSurveyNotifications.stream().max(Comparator.comparingLong(n -> n.timeSent.getTime())); } + /** + * Use name if available, if not fallback on email (which is a required field). + */ + @JsonIgnore + @BsonIgnore + public String getDisplayedName() { + return Strings.isBlank(name) ? email.replace("@", " at ") : name; + } + /** Obtains a notification with the given id, if available. */ public Optional findNotification(String id) { if (tripSurveyNotifications == null || Strings.isBlank(id)) return Optional.empty(); diff --git a/src/main/java/org/opentripplanner/middleware/models/TripMonitorNotification.java b/src/main/java/org/opentripplanner/middleware/models/TripMonitorNotification.java index 985970370..d854d9246 100644 --- a/src/main/java/org/opentripplanner/middleware/models/TripMonitorNotification.java +++ b/src/main/java/org/opentripplanner/middleware/models/TripMonitorNotification.java @@ -8,6 +8,7 @@ import java.util.Date; import java.util.Locale; +import java.util.Objects; /** * Contains information about the type and details of messages to be sent to users about their {@link MonitoredTrip}s. @@ -16,8 +17,8 @@ public class TripMonitorNotification extends Model { private static final Logger LOG = LoggerFactory.getLogger(TripMonitorNotification.class); public static final String STOPWATCH_ICON = "â±"; - public final NotificationType type; - public final String body; + public NotificationType type; + public String body; /** Getter functions are used by HTML template renderer */ public String getBody() { @@ -28,6 +29,12 @@ public NotificationType getType() { return type; } + /** + * This no-arg constructor exists to make MongoDB happy. + */ + public TripMonitorNotification() { + } + public TripMonitorNotification(NotificationType type, String body) { this.type = type; this.body = body; @@ -110,4 +117,22 @@ public static TripMonitorNotification createInitialReminderNotification( ) ); } -} + + /** + * Checks for equality excluding the parent {@link Model} class. + */ + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + TripMonitorNotification that = (TripMonitorNotification) o; + return type == that.type && Objects.equals(body, that.body); + } + + /** + * Creates a hash code from fields in this class only excluding fields within the parent {@link Model} class. + */ + @Override + public int hashCode() { + return Objects.hash(type, body); + } +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/tripmonitor/JourneyState.java b/src/main/java/org/opentripplanner/middleware/tripmonitor/JourneyState.java index e1b54b0ce..86ccaea48 100644 --- a/src/main/java/org/opentripplanner/middleware/tripmonitor/JourneyState.java +++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/JourneyState.java @@ -40,7 +40,6 @@ public class JourneyState implements Cloneable { /** * The notifications already sent. - * FIXME this is never set, so it has no effect. */ public Set lastNotifications = new HashSet<>(); diff --git a/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java b/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java index 59c59b1a7..b977fef81 100644 --- a/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java +++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/TrustedCompanion.java @@ -186,7 +186,6 @@ private static boolean sendAcceptDependentEmail(OtpUser dependentUser, OtpUser r String acceptDependentLinkLabel = Message.ACCEPT_DEPENDENT_EMAIL_LINK_TEXT.get(locale); String acceptDependentUrl = getAcceptDependentUrl(acceptKey, locale); - String addressee = (Strings.isBlank(dependentUser.name)) ? dependentUser.email : dependentUser.name; // A HashMap is needed instead of a Map for template data to be serialized to the template renderer. Map templateData = new HashMap<>( @@ -195,7 +194,7 @@ private static boolean sendAcceptDependentEmail(OtpUser dependentUser, OtpUser r "acceptDependentLinkLabelAndUrl", label(acceptDependentLinkLabel, acceptDependentUrl, locale), "acceptDependentUrl", acceptDependentUrl, "emailFooter", Message.ACCEPT_DEPENDENT_EMAIL_FOOTER.get(locale), - "emailGreeting", String.format(Message.ACCEPT_DEPENDENT_EMAIL_GREETING.get(locale), addressee), + "emailGreeting", String.format(Message.ACCEPT_DEPENDENT_EMAIL_GREETING.get(locale), dependentUser.getDisplayedName()), "manageLinkUrl", String.format("%s%s", OTP_UI_URL, SETTINGS_PATH), "manageLinkText", Message.ACCEPT_DEPENDENT_EMAIL_MANAGE.get(locale) ) diff --git a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java index d8fbb962c..9bff9e545 100644 --- a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java @@ -2,6 +2,7 @@ import org.opentripplanner.middleware.i18n.Message; import org.opentripplanner.middleware.models.ItineraryExistence; +import org.opentripplanner.middleware.models.LegTransitionNotification; import org.opentripplanner.middleware.models.MonitoredTrip; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TripMonitorAlertNotification; @@ -14,6 +15,7 @@ import org.opentripplanner.middleware.otp.response.OtpResponse; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.tripmonitor.JourneyState; +import org.opentripplanner.middleware.triptracker.TravelerPosition; import org.opentripplanner.middleware.triptracker.TripTrackingData; import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.DateTimeUtils; @@ -40,7 +42,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Supplier; -import static org.opentripplanner.middleware.utils.I18nUtils.label; +import static org.opentripplanner.middleware.models.LegTransitionNotification.getLegTransitionNotifyUsers; /** * This job handles the primary functions for checking a {@link MonitoredTrip}, including: @@ -51,9 +53,7 @@ public class CheckMonitoredTrip implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(CheckMonitoredTrip.class); - private final String OTP_UI_URL = ConfigUtils.getConfigPropertyAsText("OTP_UI_URL"); - - private final String OTP_UI_NAME = ConfigUtils.getConfigPropertyAsText("OTP_UI_NAME"); + public boolean IS_TEST = false; public static final int MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS = ConfigUtils.getConfigPropertyAsInt("MAXIMUM_MONITORED_TRIP_ITINERARY_CHECKS", 3); @@ -247,6 +247,32 @@ private boolean isTrackingOngoing() { return trip.journeyState.tripStatus == TripStatus.TRIP_ACTIVE && TripTrackingData.getOngoingTrackedJourney(trip.id) != null; } + /** + * Process leg transition notifications by getting all qualifying users and enqueuing relevant notifications. The + * matching itinerary is required when updating the monitored trip. There is no requirement to match the itinerary + * 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(); + OtpUser tripOwner = getOtpUser(); + Set notifyUsers = getLegTransitionNotifyUsers(trip); + notifyUsers.forEach(observer -> { + if (observer != null) { + enqueueNotification( + new LegTransitionNotification( + tripOwner.getDisplayedName(), + notificationType, + travelerPosition, + I18nUtils.getOtpUserLocale(observer) + ).tripMonitorNotification + ); + sendNotifications(observer); + } + }); + + updateMonitoredTrip(); + } + /** * Find and set the matching itinerary from the OTP response that matches the monitored trip's stored itinerary if a * match exists. @@ -498,8 +524,7 @@ public TripMonitorNotification checkTripForDelay(NotificationType delayType) { } /** - * Compose a message for any enqueued notifications and send to {@link OtpUser} based on their notification - * preferences. + * Send notification to user associated with the trip. */ private void sendNotifications() { OtpUser otpUser = getOtpUser(); @@ -508,6 +533,14 @@ private void sendNotifications() { // TODO: Bugsnag / delete monitored trip? return; } + sendNotifications(otpUser); + } + + /** + * Compose a message for any enqueued notifications and send to {@link OtpUser} based on their notification + * preferences. + */ + private void sendNotifications(OtpUser otpUser) { // Update push notification devices count, which may change asynchronously NotificationUtils.updatePushDevices(otpUser); @@ -525,9 +558,12 @@ private void sendNotifications() { return; } + Locale locale = I18nUtils.getOtpUserLocale(otpUser); String tripNameOrReminder = hasInitialReminder ? initialReminderNotification.body : trip.tripName; + if (tripNameOrReminder == null) { + tripNameOrReminder = Message.TRIP_NAME_UNDEFINED.get(locale); + } - Locale locale = getOtpUserLocale(); // A HashMap is needed instead of a Map for template data to be serialized to the template renderer. Map templateData = new HashMap<>(); templateData.putAll(Map.of( @@ -547,7 +583,7 @@ private void sendNotifications() { boolean successSms = false; if (otpUser.notificationChannel.contains(OtpUser.Notification.EMAIL)) { - successEmail = sendEmail(otpUser, templateData); + successEmail = sendEmail(otpUser, templateData, locale); } if (otpUser.notificationChannel.contains(OtpUser.Notification.PUSH)) { successPush = sendPush(otpUser, templateData); @@ -557,7 +593,7 @@ private void sendNotifications() { } // TODO: better handle below when one of the following fails - if (successEmail || successPush || successSms) { + if (successEmail || successPush || successSms || IS_TEST) { notificationTimestampMillis = DateTimeUtils.currentTimeMillis(); } } @@ -579,8 +615,7 @@ private boolean sendPush(OtpUser otpUser, Map data) { /** * Send notification email in MonitoredTrip template. */ - private boolean sendEmail(OtpUser otpUser, Map data) { - Locale locale = getOtpUserLocale(); + private boolean sendEmail(OtpUser otpUser, Map data, Locale locale) { String subject = NotificationUtils.getTripEmailSubject(otpUser, locale, trip); return NotificationUtils.sendEmail( otpUser, @@ -894,11 +929,15 @@ private boolean updateMonitoredTrip() { return false; } journeyState.matchingItinerary = matchingItinerary; - journeyState.targetDate = targetZonedDateTime.format(DateTimeUtils.DEFAULT_DATE_FORMATTER); + if (targetZonedDateTime != null) { + journeyState.targetDate = targetZonedDateTime.format(DateTimeUtils.DEFAULT_DATE_FORMATTER); + } journeyState.lastCheckedEpochMillis = DateTimeUtils.currentTimeMillis(); // Update notification time if notification successfully sent. if (notificationTimestampMillis != -1) { journeyState.lastNotificationTimeMillis = notificationTimestampMillis; + // Prevent repeated notifications by saving successfully sent notifications. + journeyState.lastNotifications.addAll(notifications); } trip.journeyState = journeyState; Persistence.monitoredTrips.replace(trip.id, trip); diff --git a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/NotificationType.java b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/NotificationType.java index c643e0e11..62d0fa422 100644 --- a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/NotificationType.java +++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/NotificationType.java @@ -11,5 +11,8 @@ public enum NotificationType { ITINERARY_CHANGED, // TODO ALERT_FOUND, ITINERARY_NOT_FOUND, - INITIAL_REMINDER + INITIAL_REMINDER, + MODE_CHANGE_NOTIFICATION, + DEPARTED_NOTIFICATION, + ARRIVED_NOTIFICATION } \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 795a564db..64c090a6b 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.triptracker; import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.models.LegTransitionNotification; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.persistence.Persistence; @@ -80,6 +81,8 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa ); } + LegTransitionNotification.checkForLegTransition(tripStatus, travelerPosition, tripData.trip); + // Provide response. TripInstruction instruction = TravelerLocator.getInstruction(tripStatus, travelerPosition, create); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 1015f6dca..a6e32d926 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -85,7 +85,7 @@ public static TripInstruction getInstruction( /** * Has required walk leg. */ - private static boolean hasRequiredWalkLeg(TravelerPosition travelerPosition) { + public static boolean hasRequiredWalkLeg(TravelerPosition travelerPosition) { return travelerPosition.expectedLeg != null && travelerPosition.expectedLeg.mode.equalsIgnoreCase("walk"); @@ -94,7 +94,7 @@ private static boolean hasRequiredWalkLeg(TravelerPosition travelerPosition) { /** * Has required transit leg. */ - private static boolean hasRequiredTransitLeg(TravelerPosition travelerPosition) { + public static boolean hasRequiredTransitLeg(TravelerPosition travelerPosition) { return travelerPosition.expectedLeg != null && travelerPosition.expectedLeg.transitLeg; @@ -103,7 +103,7 @@ private static boolean hasRequiredTransitLeg(TravelerPosition travelerPosition) /** * The trip instruction can only be provided if the traveler is close to the indicated route. */ - private static boolean hasRequiredTripStatus(TripStatus tripStatus) { + public static boolean hasRequiredTripStatus(TripStatus tripStatus) { return !tripStatus.equals(TripStatus.DEVIATED) && !tripStatus.equals(TripStatus.ENDED); } @@ -322,7 +322,7 @@ private static boolean isPositionPastStep(TravelerPosition travelerPosition, Con /** * Is the traveler approaching the leg destination. */ - private static boolean isApproachingEndOfLeg(TravelerPosition travelerPosition) { + public static boolean isApproachingEndOfLeg(TravelerPosition travelerPosition) { return getDistanceToEndOfLeg(travelerPosition) <= TRIP_INSTRUCTION_UPCOMING_RADIUS; } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 2756f7e3e..53b3328e7 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -48,6 +48,20 @@ public class TravelerPosition { /** The first leg of the trip. **/ public Leg firstLegOfTrip; + public TravelerPosition(Builder builder) { + this.expectedLeg = builder.expectedLeg; + this.currentPosition = builder.currentPosition; + this.speed = builder.speed; + this.firstLegOfTrip = builder.firstLegOfTrip; + if (expectedLeg != null && currentPosition != null) { + this.legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); + } + this.nextLeg = builder.nextLeg; + this.currentTime = builder.currentTime; + this.trackedJourney = builder.trackedJourney; + + } + public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpUser otpUser) { TrackingLocation lastLocation = trackedJourney.locations.get(trackedJourney.locations.size() - 1); currentTime = lastLocation.timestamp.toInstant(); @@ -68,43 +82,61 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU firstLegOfTrip = getFirstLeg(itinerary); } - /** Used for unit testing. */ - public TravelerPosition(Leg expectedLeg, Coordinates currentPosition, int speed) { - this.expectedLeg = expectedLeg; - this.currentPosition = currentPosition; - this.speed = speed; - legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); + /** Computes the current deviation in meters from the expected itinerary. */ + public double getDeviationMeters() { + return getDistanceFromLine(legSegmentFromPosition.start, legSegmentFromPosition.end, 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); - } + /** + * Builder to handle basic unit test requirements. + */ + public static final class Builder { + + private Leg expectedLeg; + private Coordinates currentPosition; + private int speed; + private Leg firstLegOfTrip; + private Leg nextLeg; + private Instant currentTime; + private TrackedJourney trackedJourney; + + public Builder setExpectedLeg(Leg expectedLeg) { + this.expectedLeg = expectedLeg; + return this; + } - /** Used for unit testing. */ - public TravelerPosition(Leg expectedLeg, Coordinates currentPosition, Leg firstLegOfTrip) { - // Anywhere the speed is zero means that speed is not considered for a specific logic. - this(expectedLeg, currentPosition, 0); - this.firstLegOfTrip = firstLegOfTrip; - } + public Builder setCurrentPosition(Coordinates currentPosition) { + this.currentPosition = currentPosition; + return this; + } - /** Used for unit testing. */ - public TravelerPosition(Leg nextLeg, Instant currentTime) { - this.nextLeg = nextLeg; - this.currentTime = currentTime; - } + public Builder setSpeed(int speed) { + this.speed = speed; + return this; + } - /** Used for unit testing. */ - public TravelerPosition(Leg expectedLeg, TrackedJourney trackedJourney, Leg first, Coordinates currentPosition) { - this.expectedLeg = expectedLeg; - this.trackedJourney = trackedJourney; - this.firstLegOfTrip = first; - this.currentPosition = currentPosition; - } + public Builder setFirstLegOfTrip(Leg firstLegOfTrip) { + this.firstLegOfTrip = firstLegOfTrip; + return this; + } - /** Computes the current deviation in meters from the expected itinerary. */ - public double getDeviationMeters() { - return getDistanceFromLine(legSegmentFromPosition.start, legSegmentFromPosition.end, currentPosition); + public Builder setNextLeg(Leg nextLeg) { + this.nextLeg = nextLeg; + return this; + } + + public Builder setCurrentTime(Instant currentTime) { + this.currentTime = currentTime; + return this; + } + + public Builder setTrackedJourney(TrackedJourney trackedJourney) { + this.trackedJourney = trackedJourney; + return this; + } + + public TravelerPosition build() { + return new TravelerPosition(this); + } } } diff --git a/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java b/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java index 265790f21..d28e7d02f 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/NotificationUtils.java @@ -498,8 +498,7 @@ public static String replaceUserNameInFromEmail(String fromEmail, OtpUser otpUse int lastBracketIndex = fromEmail.indexOf('>'); // HACK: If falling back on email, replace the "@" sign so that the user's email does not override the // application email in brackets. - String displayedName = Strings.isBlank(otpUser.name) ? otpUser.email.replace("@", " at ") : otpUser.name; - return String.format("%s %s", displayedName, fromEmail.substring(firstBracketIndex, lastBracketIndex + 1)); + return String.format("%s %s", otpUser.getDisplayedName(), fromEmail.substring(firstBracketIndex, lastBracketIndex + 1)); } /** diff --git a/src/main/resources/Message.properties b/src/main/resources/Message.properties index 397aae44b..c44410aec 100644 --- a/src/main/resources/Message.properties +++ b/src/main/resources/Message.properties @@ -4,7 +4,10 @@ ACCEPT_DEPENDENT_EMAIL_LINK_TEXT = Accept trusted companion ACCEPT_DEPENDENT_EMAIL_SUBJECT = Trusted companion request ACCEPT_DEPENDENT_EMAIL_MANAGE = Manage settings ACCEPT_DEPENDENT_ERROR = Unable to accept trusted companion. +ARRIVED_NOTIFICATION = %s has arrived at %s. LABEL_AND_CONTENT = %s: %s +MODE_CHANGE_NOTIFICATION = %s has arrived at transit stop %s. +DEPARTED_NOTIFICATION = %s has departed %s. SMS_STOP_NOTIFICATIONS = To stop receiving notifications, reply STOP. TRIP_EMAIL_SUBJECT = %s Notification TRIP_EMAIL_SUBJECT_FOR_USER = Trip for %s Notification @@ -32,6 +35,7 @@ TRIP_DELAY_MINUTES = %d minutes TRIP_INVITE_COMPANION = %s added you as a companion for their trip. TRIP_INVITE_PRIMARY_TRAVELER = %s made you the primary traveler on this trip. TRIP_INVITE_OBSERVER = %s added you as an observer for their trip. +TRIP_NAME_UNDEFINED = Trip name undefined TRIP_NOT_FOUND_NOTIFICATION = Your itinerary was not found in today's trip planner results. Please check real-time conditions and plan a new trip. TRIP_NO_LONGER_POSSIBLE_NOTIFICATION = Your itinerary is no longer possible on any monitored day of the week. Please plan and save a new trip. TRIP_REMINDER_NOTIFICATION = Reminder for %s at %s. diff --git a/src/main/resources/Message_fr.properties b/src/main/resources/Message_fr.properties index 1b178923d..ae87d00d3 100644 --- a/src/main/resources/Message_fr.properties +++ b/src/main/resources/Message_fr.properties @@ -4,7 +4,10 @@ ACCEPT_DEPENDENT_EMAIL_LINK_TEXT = Accepter la demande ACCEPT_DEPENDENT_EMAIL_SUBJECT = Demande d'accompagnateur ACCEPT_DEPENDENT_EMAIL_MANAGE = Gérez vos préférences ACCEPT_DEPENDENT_ERROR = La demande d'accompagnateur n'a pas été reçue. +ARRIVED_NOTIFICATION = %s est arrivé·e à %s. LABEL_AND_CONTENT = %s\u00A0: %s +MODE_CHANGE_NOTIFICATION = %s est arrivé·e à l'arrêt %s. +DEPARTED_NOTIFICATION = %s vient de partir de %s. SMS_STOP_NOTIFICATIONS = Pour arrêter ces notifications, envoyez STOP. TRIP_EMAIL_SUBJECT = Notification pour %s TRIP_EMAIL_SUBJECT_FOR_USER = Notification pour le trajet de %s @@ -32,6 +35,7 @@ TRIP_DELAY_MINUTES = %d minutes TRIP_INVITE_COMPANION = %s vous a ajouté comme accompagnateur·trice pour un trajet. TRIP_INVITE_PRIMARY_TRAVELER = %s vous a fait le voyageur principal sur ce trajet. TRIP_INVITE_OBSERVER = %s vous a ajouté comme observateur·trice pour un trajet. +TRIP_NAME_UNDEFINED = Trajet sans nom TRIP_NOT_FOUND_NOTIFICATION = Votre trajet est introuvable aujourd'hui. Veuillez vérifier les conditions en temps-réel et recherchez un nouveau trajet. TRIP_NO_LONGER_POSSIBLE_NOTIFICATION = Votre trajet n'est plus possible dans aucun jour de suivi de la semaine. Veuillez rechercher et enregistrer un nouveau trajet. TRIP_REMINDER_NOTIFICATION = Rappel pour %s à %s. diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 3e8f05d54..646173386 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -59,10 +59,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -93,10 +93,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -126,10 +126,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/admin/user: get: tags: @@ -158,10 +158,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/admin/user" @@ -181,10 +181,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -223,10 +223,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -272,10 +272,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -312,10 +312,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/AdminUser" schema: $ref: "#/definitions/AdminUser" + responseSchema: + $ref: "#/definitions/AdminUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -359,10 +359,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -405,10 +405,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -450,10 +450,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TokenHolder" schema: $ref: "#/definitions/TokenHolder" + responseSchema: + $ref: "#/definitions/TokenHolder" /api/secure/application/fromtoken: get: tags: @@ -467,10 +467,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -501,10 +501,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -534,10 +534,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/application: get: tags: @@ -566,10 +566,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/application" @@ -589,10 +589,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -631,10 +631,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -680,10 +680,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -720,10 +720,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ApiUser" schema: $ref: "#/definitions/ApiUser" + responseSchema: + $ref: "#/definitions/ApiUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -767,11 +767,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/CDPFile" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/CDPFile" @@ -792,11 +792,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/URL" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/URL" @@ -813,10 +813,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -847,10 +847,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -880,10 +880,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/cdp: get: tags: @@ -912,10 +912,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/cdp" @@ -935,10 +935,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -977,10 +977,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1026,10 +1026,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1066,10 +1066,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/CDPUser" schema: $ref: "#/definitions/CDPUser" + responseSchema: + $ref: "#/definitions/CDPUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1113,11 +1113,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/BugsnagEvent" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/BugsnagEvent" @@ -1154,11 +1154,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/ApiUsageResult" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/ApiUsageResult" @@ -1190,10 +1190,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/monitoredcomponent" @@ -1213,10 +1213,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1255,10 +1255,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1304,10 +1304,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1345,10 +1345,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredComponent" schema: $ref: "#/definitions/MonitoredComponent" + responseSchema: + $ref: "#/definitions/MonitoredComponent" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1388,10 +1388,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/ItineraryExistence" schema: $ref: "#/definitions/ItineraryExistence" + responseSchema: + $ref: "#/definitions/ItineraryExistence" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1440,10 +1440,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/monitoredtrip" @@ -1463,10 +1463,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1505,10 +1505,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1554,10 +1554,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1595,10 +1595,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MonitoredTrip" schema: $ref: "#/definitions/MonitoredTrip" + responseSchema: + $ref: "#/definitions/MonitoredTrip" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1701,10 +1701,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1736,10 +1736,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/MobilityProfileLite" schema: $ref: "#/definitions/MobilityProfileLite" + responseSchema: + $ref: "#/definitions/MobilityProfileLite" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1779,10 +1779,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/VerificationResult" schema: $ref: "#/definitions/VerificationResult" + responseSchema: + $ref: "#/definitions/VerificationResult" /api/secure/user/{id}/verify_sms/{code}: post: tags: @@ -1802,10 +1802,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/VerificationResult" schema: $ref: "#/definitions/VerificationResult" + responseSchema: + $ref: "#/definitions/VerificationResult" /api/secure/user/fromtoken: get: tags: @@ -1819,10 +1819,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1853,10 +1853,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1886,10 +1886,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/user: get: tags: @@ -1918,10 +1918,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/user" @@ -1941,10 +1941,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -1983,10 +1983,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -2032,10 +2032,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -2072,10 +2072,10 @@ paths: "200": description: "Successful operation" examples: {} - responseSchema: - $ref: "#/definitions/OtpUser" schema: $ref: "#/definitions/OtpUser" + responseSchema: + $ref: "#/definitions/OtpUser" "400": description: "The request was not formed properly (e.g., some required parameters\ \ may be missing). See the details of the returned response to determine\ @@ -2113,10 +2113,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" schema: $ref: "#/definitions/TrackingResponse" + responseSchema: + $ref: "#/definitions/TrackingResponse" /api/secure/monitoredtrip/updatetracking: post: tags: @@ -2134,10 +2134,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" schema: $ref: "#/definitions/TrackingResponse" + responseSchema: + $ref: "#/definitions/TrackingResponse" /api/secure/monitoredtrip/track: post: tags: @@ -2155,10 +2155,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TrackingResponse" schema: $ref: "#/definitions/TrackingResponse" + responseSchema: + $ref: "#/definitions/TrackingResponse" /api/secure/monitoredtrip/endtracking: post: tags: @@ -2176,10 +2176,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" schema: $ref: "#/definitions/EndTrackingResponse" + responseSchema: + $ref: "#/definitions/EndTrackingResponse" /api/secure/monitoredtrip/forciblyendtracking: post: tags: @@ -2197,10 +2197,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" schema: $ref: "#/definitions/EndTrackingResponse" + responseSchema: + $ref: "#/definitions/EndTrackingResponse" /api/secure/triprequests: get: tags: @@ -2245,10 +2245,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TripRequest" schema: $ref: "#/definitions/TripRequest" + responseSchema: + $ref: "#/definitions/TripRequest" /api/trip-survey/open: get: tags: @@ -2508,6 +2508,9 @@ definitions: - "ALERT_FOUND" - "ITINERARY_NOT_FOUND" - "INITIAL_REMINDER" + - "MODE_CHANGE_NOTIFICATION" + - "DEPARTED_NOTIFICATION" + - "ARRIVED_NOTIFICATION" body: type: "string" EncodedPolyline: diff --git a/src/test/java/org/opentripplanner/middleware/models/LegTransitionNotificationTest.java b/src/test/java/org/opentripplanner/middleware/models/LegTransitionNotificationTest.java new file mode 100644 index 000000000..8d96ee713 --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/models/LegTransitionNotificationTest.java @@ -0,0 +1,126 @@ +package org.opentripplanner.middleware.models; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.middleware.otp.response.Itinerary; +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.testutils.ApiTestUtils; +import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment; +import org.opentripplanner.middleware.testutils.PersistenceTestUtils; +import org.opentripplanner.middleware.tripmonitor.jobs.NotificationType; +import org.opentripplanner.middleware.triptracker.TravelerPosition; +import org.opentripplanner.middleware.utils.Coordinates; + +import java.util.Locale; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.opentripplanner.middleware.testutils.OtpTestUtils.createDefaultItinerary; + +class LegTransitionNotificationTest extends OtpMiddlewareTestEnvironment { + private static OtpUser primary; + private static OtpUser companion; + private static OtpUser observer; + + @BeforeAll + public static void setup() throws Exception { + primary = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("test-primary-user")); + companion = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("test-companion-user")); + observer = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("test-observer-user")); + } + + @AfterAll + public static void tearDown() { + PersistenceTestUtils.deleteOtpUser(false, primary, companion, observer); + } + + @ParameterizedTest + @MethodSource("createLegTransitionNotificationTestCases") + void testLegTransitionNotifications( + NotificationType notificationType, + String travelerName, + TravelerPosition travelerPosition, + String message + ) { + TripMonitorNotification notification = new LegTransitionNotification( + travelerName, + notificationType, + travelerPosition, + Locale.US + ).tripMonitorNotification; + assertNotNull(notification); + assertEquals(message, notification.body); + } + + private static Stream createLegTransitionNotificationTestCases() throws Exception { + String travelerName = "Obi-Wan"; + Itinerary itinerary = createDefaultItinerary(); + Leg expectedLeg = itinerary.legs.get(1); + Coordinates expectedLegDestinationCoords = new Coordinates(expectedLeg.to); + Leg nextLeg = itinerary.legs.get(2); + Coordinates nextLegDepartureCoords = new Coordinates(nextLeg.from); + return Stream.of( + Arguments.of( + NotificationType.MODE_CHANGE_NOTIFICATION, + travelerName, + new TravelerPosition.Builder() + .setExpectedLeg(expectedLeg) + .setNextLeg(nextLeg) + .setCurrentPosition(expectedLegDestinationCoords) + .build(), + "Obi-Wan has arrived at transit stop Pioneer Square South MAX Station." + ), + Arguments.of( + NotificationType.DEPARTED_NOTIFICATION, + travelerName, + new TravelerPosition.Builder() + .setExpectedLeg(expectedLeg) + .setNextLeg(nextLeg) + .setCurrentPosition(nextLegDepartureCoords) + .build(), + "Obi-Wan has departed Providence Park MAX Station." + ), + Arguments.of( + NotificationType.ARRIVED_NOTIFICATION, + travelerName, + new TravelerPosition.Builder() + .setExpectedLeg(expectedLeg) + .setNextLeg(nextLeg) + .setCurrentPosition(expectedLegDestinationCoords) + .build(), + "Obi-Wan has arrived at Pioneer Square South MAX Station." + ) + ); + } + + @ParameterizedTest + @MethodSource("createLegTransitionNotifyUsersTestCases") + void testLegTransitionNotifyUsers( + String tripOwnerUserId, + Set expectedUsers + ) { + MonitoredTrip trip = new MonitoredTrip(); + trip.userId = tripOwnerUserId; + trip.primary = new MobilityProfileLite(primary); + trip.companion = new RelatedUser(companion.email, RelatedUser.RelatedUserStatus.CONFIRMED); + trip.observers.add(new RelatedUser(observer.email, RelatedUser.RelatedUserStatus.CONFIRMED)); + + Set users = LegTransitionNotification.getLegTransitionNotifyUsers(trip); + assertNotNull(users); + assertTrue(users.containsAll(expectedUsers)); + assertEquals(users.size(), expectedUsers.size()); + } + + private static Stream createLegTransitionNotifyUsersTestCases() { + return Stream.of( + Arguments.of(primary.id, Set.of(companion, observer)), + Arguments.of(companion.id, Set.of(primary, observer)) + ); + } +} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java b/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java index 80d90ff00..99b1e02ad 100644 --- a/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java +++ b/src/test/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTripTest.java @@ -10,7 +10,11 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentripplanner.middleware.models.ItineraryExistence; +import org.opentripplanner.middleware.models.MobilityProfileLite; +import org.opentripplanner.middleware.models.RelatedUser; import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.testutils.ApiTestUtils; import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment; import org.opentripplanner.middleware.testutils.OtpTestUtils; import org.opentripplanner.middleware.testutils.PersistenceTestUtils; @@ -23,6 +27,8 @@ import org.opentripplanner.middleware.otp.response.OtpResponse; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.tripmonitor.TripStatus; +import org.opentripplanner.middleware.triptracker.TravelerPosition; +import org.opentripplanner.middleware.utils.Coordinates; import org.opentripplanner.middleware.utils.DateTimeUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,6 +50,7 @@ import static org.hamcrest.text.MatchesPattern.matchesPattern; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -807,4 +814,48 @@ private static Stream createCanUnsnoozeTripCases() { Arguments.of(noonMonday8June2020, tuesday, true) ); } -} + + @Test + void testDuplicateNotifications() throws Exception { + OtpUser observer = PersistenceTestUtils.createUser(ApiTestUtils.generateEmailAddress("test-observer-user")); + + MonitoredTrip monitoredTrip = PersistenceTestUtils.createMonitoredTrip( + user.id, + OtpTestUtils.OTP2_DISPATCHER_PLAN_RESPONSE.clone(), + true, + OtpTestUtils.createDefaultJourneyState() + ); + monitoredTrip.primary = new MobilityProfileLite(user); + monitoredTrip.observers.add(new RelatedUser(observer.email, RelatedUser.RelatedUserStatus.CONFIRMED)); + Persistence.monitoredTrips.replace(monitoredTrip.id, monitoredTrip); + + Leg expectedLeg = monitoredTrip.itinerary.legs.get(1); + Coordinates expectedLegDestinationCoords = new Coordinates(expectedLeg.to); + Leg nextLeg = monitoredTrip.itinerary.legs.get(2); + + TravelerPosition travelerPosition = new TravelerPosition.Builder() + .setExpectedLeg(expectedLeg) + .setNextLeg(nextLeg) + .setCurrentPosition(expectedLegDestinationCoords) + .build(); + + triggerCheckMonitoredTrip(monitoredTrip, travelerPosition); + MonitoredTrip updated = Persistence.monitoredTrips.getById(monitoredTrip.id); + assertNotEquals(-1, updated.journeyState.lastNotificationTimeMillis); + long previousLastNotificationTimeMillis = updated.journeyState.lastNotificationTimeMillis; + + triggerCheckMonitoredTrip(monitoredTrip, travelerPosition); + updated = Persistence.monitoredTrips.getById(monitoredTrip.id); + assertEquals(previousLastNotificationTimeMillis, updated.journeyState.lastNotificationTimeMillis); + + Persistence.monitoredTrips.removeById(monitoredTrip.id); + Persistence.otpUsers.removeById(observer.id); + } + + private void triggerCheckMonitoredTrip(MonitoredTrip monitoredTrip, TravelerPosition travelerPosition) throws CloneNotSupportedException { + CheckMonitoredTrip checkMonitoredTrip = new CheckMonitoredTrip(monitoredTrip, this::mockOtpPlanResponse); + checkMonitoredTrip.IS_TEST = true; + checkMonitoredTrip.targetZonedDateTime = monitoredTrip.tripZonedDateTime(DateTimeUtils.nowAsLocalDate()); + checkMonitoredTrip.processLegTransition(NotificationType.MODE_CHANGE_NOTIFICATION, travelerPosition); + } +} \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index 2095867d7..ae472138d 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -178,7 +178,12 @@ private static Stream createTrace() { @ParameterizedTest @MethodSource("createTurnByTurnTrace") void canTrackTurnByTurn(Leg firstLeg, TraceData traceData) { - TravelerPosition travelerPosition = new TravelerPosition(firstLeg, traceData.position, firstLeg); + TravelerPosition travelerPosition = new TravelerPosition.Builder() + .setExpectedLeg(firstLeg) + .setCurrentPosition(traceData.position) + .setFirstLegOfTrip(firstLeg) + .setSpeed(0) + .build(); TripInstruction tripInstruction = TravelerLocator.getInstruction(traceData.tripStatus, travelerPosition, traceData.isStartOfTrip); assertEquals(traceData.expectedInstruction, tripInstruction != null ? tripInstruction.build() : NO_INSTRUCTION, traceData.message); } @@ -411,7 +416,11 @@ void canTrackTransitRide(TraceData traceData) { transitLeg.intermediateStops = null; } - TravelerPosition travelerPosition = new TravelerPosition(transitLeg, traceData.position, traceData.speed); + TravelerPosition travelerPosition = new TravelerPosition.Builder() + .setExpectedLeg(transitLeg) + .setCurrentPosition(traceData.position) + .setSpeed(traceData.speed) + .build(); TripInstruction tripInstruction = TravelerLocator.getInstruction(traceData.tripStatus, travelerPosition, false); assertEquals(traceData.expectedInstruction, tripInstruction != null ? tripInstruction.build() : NO_INSTRUCTION, traceData.message); } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index bf40edb05..f9e2c4239 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -116,7 +116,11 @@ void shouldCancelBusNotificationForStartOfTrip(boolean expected, Leg expectedLeg Leg first = firstLegBusTransit.legs.get(0); TrackedJourney journey = new TrackedJourney(); journey.busNotificationMessages.put(ROUTE_ID, "{\"msg_type\": 1}"); - TravelerPosition travelerPosition = new TravelerPosition(expectedLeg, journey, first, currentPosition); + TravelerPosition travelerPosition = new TravelerPosition.Builder() + .setExpectedLeg(expectedLeg) + .setTrackedJourney(journey) + .setFirstLegOfTrip(first) + .setCurrentPosition(currentPosition).build(); assertEquals(expected, ManageTripTracking.shouldCancelBusNotificationForStartOfTrip(travelerPosition), message); } @@ -249,24 +253,33 @@ private static Stream createWithinOperationalNotifyWindowTrace() { return Stream.of( Arguments.of( true, - new TravelerPosition(busLeg, busDepartureTime), + new TravelerPosition.Builder() + .setNextLeg(busLeg) + .setCurrentTime(busDepartureTime) + .build(), "Traveler is on schedule, notification can be sent." ), Arguments.of( false, - new TravelerPosition(busLeg, busDepartureTime.plusSeconds(60)), + new TravelerPosition.Builder() + .setNextLeg(busLeg) + .setCurrentTime(busDepartureTime.plusSeconds(60)) + .build(), "Traveler is behind schedule, notification can not be sent." ), Arguments.of( true, - new TravelerPosition(busLeg, busDepartureTime.minusSeconds(60)), + new TravelerPosition.Builder() + .setNextLeg(busLeg) + .setCurrentTime(busDepartureTime.minusSeconds(60)) + .build(), "Traveler is ahead of schedule, but within the notify window." ), Arguments.of(false, - new TravelerPosition( - busLeg, - busDepartureTime.plusSeconds((ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES + 1) * 60) - ), + new TravelerPosition.Builder() + .setNextLeg(busLeg) + .setCurrentTime(busDepartureTime.plusSeconds((ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES + 1) * 60)) + .build(), "Too far ahead of schedule to notify bus operator.") ); } @@ -284,12 +297,18 @@ private static Stream shouldSendBusNotificationAtStartOfTripTrace() { return Stream.of( Arguments.of( true, - new TravelerPosition(busLeg, getBusDepartureTime(busLeg)), + new TravelerPosition.Builder() + .setNextLeg(busLeg) + .setCurrentTime(getBusDepartureTime(busLeg)) + .build(), "Traveler at the start of a trip which starts with a bus leg, should notify." ), Arguments.of( false, - new TravelerPosition(walkLeg, getBusDepartureTime(walkLeg)), + new TravelerPosition.Builder() + .setNextLeg(walkLeg) + .setCurrentTime(getBusDepartureTime(walkLeg)) + .build(), "Traveler at the start of a trip which starts with a walk leg, should not notify." ) );