-
Notifications
You must be signed in to change notification settings - Fork 1
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
Post deviated trip notification #260
base: dev
Are you sure you want to change the base?
Changes from all commits
65b62db
9fc6822
6507d5a
44c456e
9e19b1e
462639b
1f31842
2e2473f
2432090
c00ac92
ca2b8aa
20fdb33
ab5a82f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,6 +23,7 @@ | |
import org.opentripplanner.middleware.otp.OtpVersion; | ||
import org.opentripplanner.middleware.persistence.Persistence; | ||
import org.opentripplanner.middleware.tripmonitor.jobs.MonitorAllTripsJob; | ||
import org.opentripplanner.middleware.triptracker.TripSurveySenderJob; | ||
import org.opentripplanner.middleware.utils.ConfigUtils; | ||
import org.opentripplanner.middleware.utils.HttpUtils; | ||
import org.opentripplanner.middleware.utils.Scheduler; | ||
|
@@ -84,6 +85,16 @@ public static void main(String[] args) throws IOException, InterruptedException | |
1, | ||
TimeUnit.MINUTES | ||
); | ||
|
||
// Schedule recurring job for post-trip surveys, once every few hours | ||
// TODO: Determine whether this should go in some other process. | ||
TripSurveySenderJob tripSurveySenderJob = new TripSurveySenderJob(); | ||
Scheduler.scheduleJob( | ||
tripSurveySenderJob, | ||
0, | ||
12, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to send notifications potentially in the early hours of the morning? Do we care? Might get a better response in working hours. |
||
TimeUnit.HOURS | ||
); | ||
} | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ public enum Notification { | |
public static final String AUTH0_SCOPE = "otp-user"; | ||
private static final long serialVersionUID = 1L; | ||
private static final Logger LOG = LoggerFactory.getLogger(OtpUser.class); | ||
public static final String LAST_TRIP_SURVEY_NOTIF_SENT_FIELD = "lastTripSurveyNotificationSent"; | ||
|
||
/** Whether the user would like accessible routes by default. */ | ||
public boolean accessibilityRoutingByDefault; | ||
|
@@ -76,6 +77,9 @@ public enum Notification { | |
/** Whether to store the user's trip history (user must opt in). */ | ||
public boolean storeTripHistory; | ||
|
||
/** When the last post-trip survey notification was sent. */ | ||
public Date lastTripSurveyNotificationSent; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only referenced in tests. I think it is updated via the field reference above, so is required at a DB level? |
||
|
||
@JsonIgnore | ||
/** If this user was created by an {@link ApiUser}, this parameter will match the {@link ApiUser}'s id */ | ||
public String applicationId; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package org.opentripplanner.middleware.triptracker; | ||
|
||
import com.mongodb.client.model.Filters; | ||
import org.bson.conversions.Bson; | ||
import org.opentripplanner.middleware.models.MonitoredTrip; | ||
import org.opentripplanner.middleware.models.OtpUser; | ||
import org.opentripplanner.middleware.models.TrackedJourney; | ||
import org.opentripplanner.middleware.persistence.Persistence; | ||
import org.opentripplanner.middleware.utils.DateTimeUtils; | ||
import org.opentripplanner.middleware.utils.I18nUtils; | ||
import org.opentripplanner.middleware.utils.NotificationUtils; | ||
import org.slf4j.Logger; | ||
import org.slf4j.LoggerFactory; | ||
|
||
import java.time.Instant; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.ArrayList; | ||
import java.util.Comparator; | ||
import java.util.Date; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
import java.util.function.Function; | ||
import java.util.stream.Collectors; | ||
|
||
import static org.opentripplanner.middleware.controllers.api.ApiController.ID_FIELD_NAME; | ||
import static org.opentripplanner.middleware.models.MonitoredTrip.USER_ID_FIELD_NAME; | ||
import static org.opentripplanner.middleware.models.OtpUser.LAST_TRIP_SURVEY_NOTIF_SENT_FIELD; | ||
import static org.opentripplanner.middleware.models.TrackedJourney.END_CONDITION_FIELD_NAME; | ||
import static org.opentripplanner.middleware.models.TrackedJourney.END_TIME_FIELD_NAME; | ||
import static org.opentripplanner.middleware.models.TrackedJourney.FORCIBLY_TERMINATED; | ||
import static org.opentripplanner.middleware.models.TrackedJourney.TERMINATED_BY_USER; | ||
|
||
/** | ||
* This job will analyze completed trips with deviations and send survey notifications about select trips. | ||
*/ | ||
public class TripSurveySenderJob implements Runnable { | ||
private static final Logger LOG = LoggerFactory.getLogger(TripSurveySenderJob.class); | ||
|
||
@Override | ||
public void run() { | ||
long start = System.currentTimeMillis(); | ||
LOG.info("TripSurveySenderJob started"); | ||
|
||
// Pick users for which the last survey notification was sent more than a week ago. | ||
List<OtpUser> usersWithNotificationsOverAWeekAgo = getUsersWithNotificationsOverAWeekAgo(); | ||
|
||
// Collect journeys that were completed/terminated in the past 24-48 hrs. (skip ongoing journeys). | ||
List<TrackedJourney> journeysCompletedInPast24To48Hours = getCompletedJourneysInPast24To48Hours(); | ||
|
||
// Map users to journeys. | ||
Map<OtpUser, List<TrackedJourney>> usersToJourneys = mapJourneysToUsers(journeysCompletedInPast24To48Hours, usersWithNotificationsOverAWeekAgo); | ||
|
||
for (Map.Entry<OtpUser, List<TrackedJourney>> entry : usersToJourneys.entrySet()) { | ||
// Find journey with the largest total deviation. | ||
Optional<TrackedJourney> optJourney = selectMostDeviatedJourney(entry.getValue()); | ||
if (optJourney.isPresent()) { | ||
// Send push notification about that journey. | ||
OtpUser otpUser = entry.getKey(); | ||
TrackedJourney journey = optJourney.get(); | ||
MonitoredTrip trip = journey.trip; | ||
Map<String, Object> data = new HashMap<>(); | ||
data.put("tripDay", DateTimeUtils.makeOtpZonedDateTime(journey.startTime).getDayOfWeek()); | ||
data.put("tripTime", DateTimeUtils.formatShortDate(trip.itinerary.startTime, I18nUtils.getOtpUserLocale(otpUser))); | ||
NotificationUtils.sendPush(otpUser, "PostTripSurveyPush.ftl", data, trip.tripName, trip.id); | ||
|
||
// Store time of last sent survey notification for user. | ||
Persistence.otpUsers.updateField(otpUser.id, LAST_TRIP_SURVEY_NOTIF_SENT_FIELD, new Date()); | ||
} | ||
} | ||
|
||
LOG.info("TripSurveySenderJob completed in {} sec", (System.currentTimeMillis() - start) / 1000); | ||
} | ||
|
||
/** | ||
* Get users whose last trip survey notification was at least a week ago. | ||
*/ | ||
public static List<OtpUser> getUsersWithNotificationsOverAWeekAgo() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is or should there be a option to opt-out of receiving surveys? |
||
Date aWeekAgo = Date.from(Instant.now().minus(7, ChronoUnit.DAYS)); | ||
Bson dateFilter = Filters.lte(LAST_TRIP_SURVEY_NOTIF_SENT_FIELD, aWeekAgo); | ||
Bson surveyNotSentFilter = Filters.not(Filters.exists(LAST_TRIP_SURVEY_NOTIF_SENT_FIELD)); | ||
Bson overallFilter = Filters.or(dateFilter, surveyNotSentFilter); | ||
|
||
return Persistence.otpUsers.getFiltered(overallFilter).into(new ArrayList<>()); | ||
} | ||
|
||
/** | ||
* Gets tracked journeys for all users that were completed in the past 24 hours. | ||
*/ | ||
public static List<TrackedJourney> getCompletedJourneysInPast24To48Hours() { | ||
Date twentyFourHoursAgo = Date.from(Instant.now().minus(24, ChronoUnit.HOURS)); | ||
Date fortyEightHoursAgo = Date.from(Instant.now().minus(48, ChronoUnit.HOURS)); | ||
Bson dateFilter = Filters.and( | ||
Filters.gte(END_TIME_FIELD_NAME, fortyEightHoursAgo), | ||
Filters.lte(END_TIME_FIELD_NAME, twentyFourHoursAgo) | ||
); | ||
Bson completeFilter = Filters.eq(END_CONDITION_FIELD_NAME, TERMINATED_BY_USER); | ||
Bson terminatedFilter = Filters.eq(END_CONDITION_FIELD_NAME, FORCIBLY_TERMINATED); | ||
Bson overallFilter = Filters.and(dateFilter, Filters.or(completeFilter, terminatedFilter)); | ||
|
||
return Persistence.trackedJourneys.getFiltered(overallFilter).into(new ArrayList<>()); | ||
} | ||
|
||
/** | ||
* Gets the trips for the given journeys and users. | ||
*/ | ||
public static List<MonitoredTrip> getTripsForJourneysAndUsers(List<TrackedJourney> journeys, List<OtpUser> otpUsers) { | ||
Set<String> tripIds = journeys.stream().map(j -> j.tripId).collect(Collectors.toSet()); | ||
Set<String> userIds = otpUsers.stream().map(u -> u.id).collect(Collectors.toSet()); | ||
|
||
Bson tripIdFilter = Filters.in(ID_FIELD_NAME, tripIds); | ||
Bson userIdFilter = Filters.in(USER_ID_FIELD_NAME, userIds); | ||
Bson overallFilter = Filters.and(tripIdFilter, userIdFilter); | ||
|
||
return Persistence.monitoredTrips.getFiltered(overallFilter).into(new ArrayList<>()); | ||
} | ||
|
||
/** | ||
* Map journeys to users. | ||
*/ | ||
public static Map<OtpUser, List<TrackedJourney>> mapJourneysToUsers(List<TrackedJourney> journeys, List<OtpUser> otpUsers) { | ||
List<MonitoredTrip> trips = getTripsForJourneysAndUsers(journeys, otpUsers); | ||
|
||
Map<String, OtpUser> userMap = otpUsers.stream().collect(Collectors.toMap(u -> u.id, Function.identity())); | ||
|
||
HashMap<OtpUser, List<TrackedJourney>> map = new HashMap<>(); | ||
for (MonitoredTrip trip : trips) { | ||
List<TrackedJourney> journeyList = map.computeIfAbsent(userMap.get(trip.userId), u -> new ArrayList<>()); | ||
for (TrackedJourney journey : journeys) { | ||
if (trip.id.equals(journey.tripId)) { | ||
journey.trip = trip; | ||
journeyList.add(journey); | ||
} | ||
} | ||
} | ||
|
||
return map; | ||
} | ||
|
||
public static Optional<TrackedJourney> selectMostDeviatedJourney(List<TrackedJourney> journeys) { | ||
if (journeys == null) return Optional.empty(); | ||
return journeys.stream().max(Comparator.comparingDouble(j -> j.totalDeviation)); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the trigger location is fine. Perhaps follow the approach of
ConnectedDataManager.scheduleTripHistoryUploadJob();
and have the schduler in the class. It might also be benefical to have the ability to disable this via a config property.