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

Open
wants to merge 23 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 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
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,
ARRIVED_NOTIFICATION,
DEPARTED_NOTIFICATION,
LABEL_AND_CONTENT,
MODE_CHANGE_NOTIFICATION,
SMS_STOP_NOTIFICATIONS,
TRIP_EMAIL_SUBJECT,
TRIP_EMAIL_SUBJECT_FOR_USER,
Expand Down Expand Up @@ -47,6 +50,8 @@ public enum Message {
TRIP_INVITE_COMPANION,
TRIP_INVITE_PRIMARY_TRAVELER,
TRIP_INVITE_OBSERVER,
TRIP_NAME_UNDEFINED,
TRIP_TRAVELER_GENERIC_NAME,
TRIP_NOT_FOUND_NOTIFICATION,
TRIP_NO_LONGER_POSSIBLE_NOTIFICATION,
TRIP_REMINDER_NOTIFICATION,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.opentripplanner.middleware.models;

import org.opentripplanner.middleware.i18n.Message;
import org.opentripplanner.middleware.tripmonitor.jobs.NotificationType;
import org.opentripplanner.middleware.triptracker.TravelerPosition;

import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

public class LegTransitionNotification {

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
public TripMonitorNotification createTripMonitorNotification(NotificationType notificationType) {
String body;
switch (notificationType) {
case MODE_CHANGE_NOTIFICATION:
body = String.format(
Message.MODE_CHANGE_NOTIFICATION.get(travelerPosition.locale),
br648 marked this conversation as resolved.
Show resolved Hide resolved
getTravelerName(),
travelerPosition.expectedLeg.mode,
travelerPosition.nextLeg.mode
);
break;
case DEPARTED_NOTIFICATION:
body = String.format(
Message.DEPARTED_NOTIFICATION.get(observerLocale),
getTravelerName(),
travelerPosition.expectedLeg.from.name
);
break;
case ARRIVED_NOTIFICATION:
body = String.format(
Message.ARRIVED_NOTIFICATION.get(travelerPosition.locale),
getTravelerName(),
travelerPosition.expectedLeg.to.name
);
break;
default:
body = null;
}
return (body != null) ? new TripMonitorNotification(notificationType, body) : null;
}

/**
* Get the traveler's name if available, if not provide a generic traveler name.
*/
private String getTravelerName() {
if (travelerName != null) {
return travelerName;
} else {
return Message.TRIP_TRAVELER_GENERIC_NAME.get(observerLocale);
br648 marked this conversation as resolved.
Show resolved Hide resolved
}
}

/**
* Create locale specific notifications.
*/
public static TripMonitorNotification[] createLegTransitionNotifications(
List<NotificationType> legTransitionTypes,
String travelerName,
TravelerPosition travelerPosition,
Locale observerLocale
) {
List<TripMonitorNotification> tripMonitorNotifications = new ArrayList<>();
// Create locale specific notifications.
for (NotificationType legTransitionType : legTransitionTypes) {
LegTransitionNotification legTransitionNotification = new LegTransitionNotification(
travelerName,
legTransitionType,
travelerPosition,
observerLocale
);
if (legTransitionNotification.tripMonitorNotification != null) {
tripMonitorNotifications.add(legTransitionNotification.tripMonitorNotification);
}
}
return tripMonitorNotifications.toArray(new TripMonitorNotification[0]);
br648 marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -34,13 +36,14 @@
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 static org.opentripplanner.middleware.utils.I18nUtils.label;
import static com.mongodb.client.model.Filters.eq;

/**
* This job handles the primary functions for checking a {@link MonitoredTrip}, including:
Expand Down Expand Up @@ -247,6 +250,31 @@ private boolean isTrackingOngoing() {
return trip.journeyState.tripStatus == TripStatus.TRIP_ACTIVE && TripTrackingData.getOngoingTrackedJourney(trip.id) != null;
}

/**
* Process leg transition notifications.
*/
public void processLegTransition(List<NotificationType> legTransitionTypes, TravelerPosition travelerPosition) {
OtpUser otpUser = getOtpUser();
if (otpUser != null) {
otpUser.relatedUsers.forEach(relatedUser -> {
br648 marked this conversation as resolved.
Show resolved Hide resolved
if (relatedUser.isConfirmed()) {
OtpUser observer = Persistence.otpUsers.getOneFiltered(eq("email", relatedUser.email));
if (observer != null) {
enqueueNotification(
br648 marked this conversation as resolved.
Show resolved Hide resolved
LegTransitionNotification.createLegTransitionNotifications(
legTransitionTypes,
otpUser.name,
br648 marked this conversation as resolved.
Show resolved Hide resolved
travelerPosition,
I18nUtils.getOtpUserLocale(observer)
)
);
sendNotifications(observer);
}
}
});
}
}

/**
* 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,8 +526,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();
Expand All @@ -508,6 +535,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);

Expand All @@ -525,9 +560,12 @@ private void sendNotifications() {
return;
}

Locale locale = getOtpUserLocale();
br648 marked this conversation as resolved.
Show resolved Hide resolved
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<String, Object> templateData = new HashMap<>();
templateData.putAll(Map.of(
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_NOTIFICATION,
DEPARTED_NOTIFICATION,
ARRIVED_NOTIFICATION
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa
);
}

TravelerLocator.checkForLegTransition(tripStatus, travelerPosition, tripData.trip);

// Provide response.
return new TrackingResponse(
TRIP_TRACKING_UPDATE_FREQUENCY_SECONDS,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package org.opentripplanner.middleware.triptracker;

import io.leonard.PolylineUtils;
import org.opentripplanner.middleware.models.MonitoredTrip;
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.tripmonitor.jobs.NotificationType;
import org.opentripplanner.middleware.triptracker.instruction.ContinueInstruction;
import org.opentripplanner.middleware.triptracker.instruction.DeviatedInstruction;
import org.opentripplanner.middleware.triptracker.instruction.GetOffHereTransitInstruction;
Expand All @@ -17,6 +20,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 @@ -30,6 +35,9 @@
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.ARRIVED_NOTIFICATION;
import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.MODE_CHANGE_NOTIFICATION;
import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.DEPARTED_NOTIFICATION;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_IMMEDIATE_RADIUS;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS;
Expand All @@ -43,6 +51,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 @@ -91,6 +101,46 @@ public static String getInstruction(
return NO_INSTRUCTION;
}

public static void checkForLegTransition(TripStatus tripStatus, TravelerPosition travelerPosition, MonitoredTrip trip) {
if (
hasRequiredTripStatus(tripStatus) &&
(hasRequiredWalkLeg(travelerPosition) || hasRequiredTransitLeg(travelerPosition))
) {
List<NotificationType> legTransitionTypes = getLegTransitionTypes(travelerPosition);
if (!legTransitionTypes.isEmpty()) {
try {
new CheckMonitoredTrip(trip).processLegTransition(legTransitionTypes, travelerPosition);
} catch (CloneNotSupportedException e) {
LOG.error("Error encountered while checking leg transition.", e);
}
}
}

}

private static List<NotificationType> getLegTransitionTypes(TravelerPosition travelerPosition) {
List<NotificationType> notificationTypes = new ArrayList<>();
if (isAtStartOfLeg(travelerPosition)) {
notificationTypes.add(DEPARTED_NOTIFICATION);
}
if (isAtEndOfLeg(travelerPosition)) {
br648 marked this conversation as resolved.
Show resolved Hide resolved
notificationTypes.add(ARRIVED_NOTIFICATION);
}
if (hasModeChanged(travelerPosition)) {
br648 marked this conversation as resolved.
Show resolved Hide resolved
notificationTypes.add(MODE_CHANGE_NOTIFICATION);
}
return notificationTypes;
}

/**
* 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 isAtEndOfLeg(travelerPosition) && nextLeg != null && !nextLeg.mode.equalsIgnoreCase(expectedLeg.mode);
}

/**
* Has required walk leg.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ public TravelerPosition(Leg expectedLeg, TrackedJourney trackedJourney, Leg firs
this.currentPosition = currentPosition;
}

/** Used for unit testing. */
public TravelerPosition(Leg expectedLeg, Leg nextLeg, Coordinates currentPosition) {
this.expectedLeg = expectedLeg;
this.nextLeg = nextLeg;
this.currentPosition = currentPosition;
}

/** Computes the current deviation in meters from the expected itinerary. */
public double getDeviationMeters() {
return getDistanceFromLine(legSegmentFromPosition.start, legSegmentFromPosition.end, currentPosition);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,4 @@ public static Leg getFirstLeg(Itinerary itinerary) {
.map(legs -> legs.get(0))
.orElse(null);
}
}
}
br648 marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 5 additions & 0 deletions src/main/resources/Message.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 changed transit from %s to %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
Expand Down Expand Up @@ -32,6 +35,8 @@ 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_TRAVELER_GENERIC_NAME = Traveler
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.
Expand Down
5 changes: 5 additions & 0 deletions src/main/resources/Message_fr.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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 = TODO %s.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@binh-dam-ibigroup can you provide the french messages please!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oui ! 36b26af

LABEL_AND_CONTENT = %s\u00A0: %s
MODE_CHANGE_NOTIFICATION = TODO %s TODO %s.
DEPARTED_NOTIFICATION = TODO %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
Expand Down Expand Up @@ -32,6 +35,8 @@ 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 = TODO
TRIP_TRAVELER_GENERIC_NAME = TODO
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.
Expand Down
Loading
Loading