-
Notifications
You must be signed in to change notification settings - Fork 2
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 12 commits
9fc6822
6507d5a
44c456e
9e19b1e
462639b
1f31842
2e2473f
2432090
c00ac92
ca2b8aa
20fdb33
ab5a82f
828afdc
3ed298b
fa6b838
72ecf80
e46abca
25d19f9
be37335
653b62c
eb59c11
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. 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. Morning local time sounds good. Do you know how to do that with this scheduler? 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. Workout the diff between now and the next 9am in seconds and use that as the initial delay. Time unit used by the scheduler would have to be seconds. 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. Change on when notifications are sent: within 20-30 minutes of trip completion. |
||
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? 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. Yes, this field is needed to keep track of when a post-travel notification was sent when subsequent trips are completed. |
||
|
||
@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? 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. No opt-out of post-travel surveys at the moment. |
||
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)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<#-- | ||
This is a template for push notifications content for notifications | ||
after an OTP user takes a monitored trip and completes travel. | ||
Note the following character limitations by mobile OS: | ||
- iOS: 178 characters over up to 4 lines, | ||
- Android: 240 characters (excluding notification title). | ||
The max length is thus 178 characters. | ||
--> | ||
How was your trip on ${tripDay} at ${tripTime}? Tap for a quick survey. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package org.opentripplanner.middleware.models; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.opentripplanner.middleware.triptracker.TrackingLocation; | ||
|
||
import java.util.stream.Collectors; | ||
import java.util.stream.Stream; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
|
||
class TrackedJourneyTest { | ||
@Test | ||
void canComputeTotalDeviation() { | ||
TrackedJourney journey = new TrackedJourney(); | ||
journey.locations = null; | ||
assertEquals(-1.0, journey.computeTotalDeviation()); | ||
|
||
journey.locations = Stream | ||
.of(11.0, 23.0, 6.4) | ||
.map(d -> { | ||
TrackingLocation location = new TrackingLocation(); | ||
location.deviationMeters = d; | ||
return location; | ||
}) | ||
.collect(Collectors.toList()); | ||
assertEquals(40.4, journey.computeTotalDeviation()); | ||
} | ||
} |
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.