From 954f994631382a8d05795b8ee7ca7f371196e4f1 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 14 May 2024 10:27:43 +0100 Subject: [PATCH 01/16] refactor(Initial bus operator notification work): --- configurations/default/env.yml.tmp | 6 +- .../middleware/models/TrackedJourney.java | 23 ++ .../triptracker/ManageLegTraversal.java | 18 +- .../triptracker/ManageTripTracking.java | 27 +- .../triptracker/NotifyBusOperator.java | 214 ++++++++++ .../triptracker/TravelerLocator.java | 36 +- .../triptracker/TravelerPosition.java | 21 +- .../triptracker/TripInstruction.java | 68 ++- .../api/ManageLegTraversalTest.java | 11 +- .../api/walk-to-bus-transition.json | 386 ++++++++++++++++++ 10 files changed, 773 insertions(+), 37 deletions(-) create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java create mode 100644 src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json diff --git a/configurations/default/env.yml.tmp b/configurations/default/env.yml.tmp index aa92d0cf2..0997a509e 100644 --- a/configurations/default/env.yml.tmp +++ b/configurations/default/env.yml.tmp @@ -89,4 +89,8 @@ TRIP_TRACKING_RAIL_ON_TRACK_RADIUS: 200 # The radius in meters under which an immediate instruction is given. TRIP_INSTRUCTION_IMMEDIATE_RADIUS: 2 # The radius in meters under which an upcoming instruction is given. -TRIP_INSTRUCTION_UPCOMING_RADIUS: 10 \ No newline at end of file +TRIP_INSTRUCTION_UPCOMING_RADIUS: 10 + +BUS_OPERATOR_NOTIFIER_API_URL: https://bus.notifier.example.com +BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY: your-subscription-key +BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES: agency_id:route_id \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java b/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java index a99a2ea49..73f0fbd90 100644 --- a/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java +++ b/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java @@ -8,6 +8,7 @@ import java.util.ArrayList; import java.util.Date; +import java.util.HashMap; import java.util.List; import java.util.Objects; @@ -24,9 +25,12 @@ public class TrackedJourney extends Model { public List locations = new ArrayList<>(); + public static final HashMap busNotificationRequests = new HashMap<>(); + public static final String TRIP_ID_FIELD_NAME = "tripId"; public static final String LOCATIONS_FIELD_NAME = "locations"; + public static final String BUS_NOTIFICATION_REQUESTS_FIELD_NAME = "busNotificationRequests"; public static final String END_TIME_FIELD_NAME = "endTime"; @@ -79,4 +83,23 @@ public int hashCode() { public TrackingLocation lastLocation() { return locations.get(locations.size() - 1); } + + public void updateNotificationRequest(String routeId, String body) { + busNotificationRequests.put(routeId, body); + Persistence.trackedJourneys.updateField( + id, + BUS_NOTIFICATION_REQUESTS_FIELD_NAME, + busNotificationRequests + ); + } + + public void removeNotificationRequest(String routeId) { + busNotificationRequests.remove(routeId); + Persistence.trackedJourneys.updateField( + id, + BUS_NOTIFICATION_REQUESTS_FIELD_NAME, + busNotificationRequests + ); + } + } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageLegTraversal.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageLegTraversal.java index d66dcdf0e..cbe62155c 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageLegTraversal.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageLegTraversal.java @@ -70,11 +70,27 @@ public static Leg getExpectedLeg(Coordinates position, Itinerary itinerary) { return null; } + /** + * Get the leg following the expected leg. + */ + @Nullable + public static Leg getNextLeg(Leg expectedLeg, Itinerary itinerary) { + if (canUseTripLegs(itinerary)) { + for (int i = 0; i < itinerary.legs.size(); i++) { + Leg leg = itinerary.legs.get(i); + if (leg.equals(expectedLeg) && (i + 1 < itinerary.legs.size())) { + return itinerary.legs.get(i + 1); + } + } + } + return null; + } + /** * Get the leg that is nearest to the current position. Note, to be considered when working with transit legs: if * the trip involves traversing a cul-de-sac, the entrance and exit legs would be very close together if not * identical. In this scenario it would be possible for the current position to be attributed to the exit leg, - * therefore missing the instruction at the end of the cul-de-dac. + * therefore missing the instruction at the end of the cul-de-sac. */ private static Leg getNearestLeg(Coordinates position, Itinerary itinerary) { double shortestDistance = Double.MAX_VALUE; diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 5d67219a9..a90a29c68 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -42,7 +42,8 @@ public static StartTrackingResponse startTracking(Request request) { var trackedJourney = new TrackedJourney(payload); TravelerPosition travelerPosition = new TravelerPosition( trackedJourney, - monitoredTrip.journeyState.matchingItinerary + monitoredTrip.journeyState.matchingItinerary, + Auth0Connection.getUserFromRequest(request).otpUser ); TripStatus tripStatus = TripStatus.getTripStatus(travelerPosition); trackedJourney.lastLocation().tripStatus = tripStatus; @@ -75,7 +76,8 @@ public static UpdateTrackingResponse updateTracking(Request request) { try { TravelerPosition travelerPosition = new TravelerPosition( trackedJourney, - monitoredTrip.journeyState.matchingItinerary + monitoredTrip.journeyState.matchingItinerary, + Auth0Connection.getUserFromRequest(request).otpUser ); // Update tracked journey. trackedJourney.update(payload); @@ -110,7 +112,12 @@ public static EndTrackingResponse endTracking(Request request) { if (trackedJourney != null) { var monitoredTrip = Persistence.monitoredTrips.getById(trackedJourney.tripId); if (isTripAssociatedWithUser(request, monitoredTrip)) { - return completeJourney(trackedJourney, false); + TravelerPosition travelerPosition = new TravelerPosition( + trackedJourney, + monitoredTrip.journeyState.matchingItinerary, + Auth0Connection.getUserFromRequest(request).otpUser + ); + return completeJourney(travelerPosition, false); } } } @@ -129,7 +136,12 @@ public static EndTrackingResponse forciblyEndTracking(Request request) { if (trackedJourney != null) { var monitoredTrip = Persistence.monitoredTrips.getById(trackedJourney.tripId); if (isTripAssociatedWithUser(request, monitoredTrip)) { - return completeJourney(trackedJourney, true); + TravelerPosition travelerPosition = new TravelerPosition( + trackedJourney, + monitoredTrip.journeyState.matchingItinerary, + Auth0Connection.getUserFromRequest(request).otpUser + ); + return completeJourney(travelerPosition, true); } } } @@ -137,9 +149,12 @@ public static EndTrackingResponse forciblyEndTracking(Request request) { } /** - * Complete a journey by defining the ending type, time and condition. + * Complete a journey by defining the ending type, time and condition. Also cancel possible upcoming bus + * notification. */ - private static EndTrackingResponse completeJourney(TrackedJourney trackedJourney, boolean isForciblyEnded) { + private static EndTrackingResponse completeJourney(TravelerPosition travelerPosition, boolean isForciblyEnded) { + NotifyBusOperator.cancelNotification(travelerPosition); + TrackedJourney trackedJourney = travelerPosition.trackedJourney; trackedJourney.end(isForciblyEnded); Persistence.trackedJourneys.updateField(trackedJourney.id, TrackedJourney.END_TIME_FIELD_NAME, trackedJourney.endTime); Persistence.trackedJourneys.updateField(trackedJourney.id, TrackedJourney.END_CONDITION_FIELD_NAME, trackedJourney.endCondition); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java new file mode 100644 index 000000000..45e4015d5 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java @@ -0,0 +1,214 @@ +package org.opentripplanner.middleware.triptracker; + +import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.utils.HttpUtils; +import org.opentripplanner.middleware.utils.JsonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.opentripplanner.middleware.triptracker.TripStatus.getSegmentStartTime; +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; + +/** + * If conditions are correct notify a bus operator of a traveler joining the service at a given stop. + */ +public class NotifyBusOperator { + + private NotifyBusOperator() {} + + private static final Logger LOG = LoggerFactory.getLogger(NotifyBusOperator.class); + + private static final String BUS_OPERATOR_NOTIFIER_API_URL + = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_URL", "not-provided"); + + private static final String BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY + = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY", "not-provided"); + + private static final List QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); + + private static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; + + /** + * Headers that are required for each request. + */ + private static final Map BUS_OPERATOR_NOTIFIER_API_HEADERS = Map.of( + "Ocp-Apim-Subscription-Key", BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY, + "Content-Type", "application/json" + ); + + /** + * Get the routes which qualify for bus operator notifying from the configuration. The configuration value is + * expected to be a comma separate list of agency id (as provided by OTP not agency) and route id e.g. + *

+ * GwinnettCountyTransit:360,GwinnettCountyTransit:40,GwinnettCountyTransit:25 + */ + private static List getBusOperatorNotifierQualifyingRoutes() { + String busOperatorNotifierQualifyingRoutes + = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES"); + if (busOperatorNotifierQualifyingRoutes != null) { + return Arrays.asList(busOperatorNotifierQualifyingRoutes.split(",")); + } + return new ArrayList<>(); + } + + /** + * Stage notification to bus operator by making sure all required conditions are met. + */ + public static void sendNotification(TripStatus tripStatus, TravelerPosition travelerPosition) { + try { + if ( + isBusLeg(travelerPosition.nextLeg) && + isWithinOperationalNotifyWindow(tripStatus, travelerPosition) && + hasNotPreviouslyNotifiedBusDriverForRoute(travelerPosition.trackedJourney, travelerPosition.nextLeg.routeId) && + supportsBusOperatorNotification(travelerPosition.nextLeg.routeId) + ) { + var body = createPostBody(travelerPosition); + var httpStatus = doPost(body); + if (httpStatus == HttpStatus.OK_200) { + travelerPosition.trackedJourney.updateNotificationRequest(travelerPosition.nextLeg.routeId, body); + } else { + LOG.error("Error {} while trying to initiate notification to bus operator.", httpStatus); + } + } + } catch (Exception e) { + LOG.error("Could not initiate notification to bus operator.", e); + } + } + + + /** + * Cancel a previously sent notification for the next bus leg. + */ + public static void cancelNotification(TravelerPosition travelerPosition) { + try { + if (isBusLeg(travelerPosition.nextLeg)) { + HashMap busNotificationRequests = travelerPosition.trackedJourney.busNotificationRequests; + if (busNotificationRequests.containsKey(travelerPosition.nextLeg.routeId)) { + var httpStatus = doPost(busNotificationRequests.get(travelerPosition.nextLeg.routeId)); + if (httpStatus == HttpStatus.OK_200) { + travelerPosition.trackedJourney.removeNotificationRequest(travelerPosition.nextLeg.routeId); + } else { + LOG.error("Error {} while trying to cancel notification to bus operator.", httpStatus); + } + } + } + } catch (Exception e) { + LOG.error("Could not cancel notification to bus operator.", e); + } + } + + /** + * Send notification and provide response. The service only provides the HTTP status as a response. + */ + public static int doPost(String body) { + var httpResponse = HttpUtils.httpRequestRawResponse( + URI.create(BUS_OPERATOR_NOTIFIER_API_URL), + 1000, + HttpMethod.POST, + BUS_OPERATOR_NOTIFIER_API_HEADERS, + body + ); + return httpResponse.status; + } + + /** + * Create post body that will be sent to bus notification API. + */ + public static String createPostBody(TravelerPosition travelerPosition) { + return JsonUtils.toJson(new NotifyBody(travelerPosition.currentTime, travelerPosition)); + } + + /** + * Make sure the leg in question is a bus transit leg. + */ + public static boolean isBusLeg(Leg leg) { + return leg != null && leg.mode.equalsIgnoreCase("BUS") && leg.transitLeg; + } + + /** + * Make sure the bus route associated with this leg supports notifying the bus operator. The 'gtfsId' is expected in + * the format agency_id:route_id e.g. GwinnettCountyTransit:360. + */ + public static boolean supportsBusOperatorNotification(String gtfsId) { + return QUALIFYING_BUS_NOTIFIER_ROUTES.contains(gtfsId); + } + + /** + * Has the bus driver already been notified for this journey. The driver must only be notified once. + */ + public static boolean hasNotPreviouslyNotifiedBusDriverForRoute(TrackedJourney trackedJourney, String routeId) { + for (String notifiedRouteId : trackedJourney.busNotificationRequests.keySet()) { + if (notifiedRouteId.equalsIgnoreCase(routeId)) { + return false; + } + } + return true; + } + + /** + * Make sure the traveler is on schedule or ahead of schedule (but not too far) to be within an operational window + * for the bus service. + */ + private static boolean isWithinOperationalNotifyWindow(TripStatus tripStatus, TravelerPosition travelerPosition) { + return + tripStatus.equals(TripStatus.ON_SCHEDULE) || + ( + tripStatus.equals(TripStatus.AHEAD_OF_SCHEDULE) && + ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES >= getMinutesAheadOfSchedule(travelerPosition) + ); + } + + /** + * Get how far ahead in minutes the traveler is from the expected schedule. + */ + public static long getMinutesAheadOfSchedule(TravelerPosition travelerPosition) { + Instant segmentStartTime = travelerPosition + .expectedLeg + .startTime + .toInstant() + .plusSeconds((long) getSegmentStartTime(travelerPosition.legSegmentFromPosition)); + return Duration.between(segmentStartTime, travelerPosition.currentTime).toMinutes(); + } + + /** + * Class containing the expected notify parameters. These will be converted to JSON and make up the body content of + * the request. + *

+ * 'To' fields omitted as they are not need for requests for single transit legs. + */ + public static class NotifyBody { + public Instant timestamp; + public String agency_id; + public String from_route_id; + public String from_trip_id; + public String from_stop_id; + public Instant from_arrival_time; + public String msg_type; + public String mobility_codes; + public boolean trusted_companion; + + public NotifyBody(Instant timestamp, TravelerPosition travelerPosition) { + this.timestamp = timestamp; + this.agency_id = travelerPosition.nextLeg.agencyId; + this.from_route_id = travelerPosition.nextLeg.routeId; + this.from_trip_id = travelerPosition.nextLeg.tripId; + this.from_stop_id = travelerPosition.nextLeg.from.stopId; + this.from_arrival_time = travelerPosition.nextLeg.getScheduledStartTime().toInstant(); + this.msg_type = "1"; + this.mobility_codes = travelerPosition.mobilityMode; + this.trusted_companion = false; + } + } +} \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 8f7ad20ea..02a7a0f00 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -34,14 +34,14 @@ public static String getInstruction( ) { if (hasRequiredWalkLeg(travelerPosition)) { if (hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); + TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip, tripStatus); if (tripInstruction != null) { return tripInstruction.build(); } } if (tripStatus.equals(TripStatus.DEVIATED)) { - TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip); + TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip, tripStatus); if (tripInstruction != null) { return tripInstruction.build(); } @@ -71,9 +71,13 @@ private static boolean hasRequiredTripStatus(TripStatus tripStatus) { * provider this, else suggest the closest street to head towards. */ @Nullable - private static TripInstruction getBackOnTrack(TravelerPosition travelerPosition, boolean isStartOfTrip) { - TripInstruction instruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); - if (instruction != null && instruction.hasInstruction) { + private static TripInstruction getBackOnTrack( + TravelerPosition travelerPosition, + boolean isStartOfTrip, + TripStatus tripStatus + ) { + TripInstruction instruction = alignTravelerToTrip(travelerPosition, isStartOfTrip, tripStatus); + if (instruction != null && instruction.hasInstruction()) { return instruction; } Step nearestStep = snapToStep(travelerPosition); @@ -86,7 +90,11 @@ private static TripInstruction getBackOnTrack(TravelerPosition travelerPosition, * Align the traveler's position to the nearest step or destination. */ @Nullable - public static TripInstruction alignTravelerToTrip(TravelerPosition travelerPosition, boolean isStartOfTrip) { + public static TripInstruction alignTravelerToTrip( + TravelerPosition travelerPosition, + boolean isStartOfTrip, + TripStatus tripStatus + ) { if (isStartOfTrip) { // If the traveler has just started the trip and is within a set distance of the first step. @@ -100,8 +108,14 @@ public static TripInstruction alignTravelerToTrip(TravelerPosition travelerPosit : null; } - if (isApproachingDestination(travelerPosition)) { - return new TripInstruction(getDistanceToDestination(travelerPosition), travelerPosition.expectedLeg.to.name); + if (isApproachingEndOfLeg(travelerPosition)) { + if (NotifyBusOperator.isBusLeg(travelerPosition.nextLeg)) { + // The preceding leg is a bus leg. + NotifyBusOperator.sendNotification(tripStatus, travelerPosition); + // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. + return new TripInstruction(travelerPosition.nextLeg, travelerPosition.currentTime); + } + return new TripInstruction(getDistanceToEndOfLeg(travelerPosition), travelerPosition.expectedLeg.to.name); } Step nextStep = snapToStep(travelerPosition); @@ -133,14 +147,14 @@ private static boolean isPositionPastStep(TravelerPosition travelerPosition, Ste /** * Is the traveler approaching the leg destination. */ - private static boolean isApproachingDestination(TravelerPosition travelerPosition) { - return getDistanceToDestination(travelerPosition) <= TRIP_INSTRUCTION_UPCOMING_RADIUS; + private static boolean isApproachingEndOfLeg(TravelerPosition travelerPosition) { + return getDistanceToEndOfLeg(travelerPosition) <= TRIP_INSTRUCTION_UPCOMING_RADIUS; } /** * Get the distance from the traveler's current position to the leg destination. */ - private static double getDistanceToDestination(TravelerPosition travelerPosition) { + private static double getDistanceToEndOfLeg(TravelerPosition travelerPosition) { Coordinates legDestination = new Coordinates(travelerPosition.expectedLeg.to); return getDistance(travelerPosition.currentPosition, legDestination); } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index d3b0aaf0a..a4e21585f 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -1,5 +1,7 @@ package org.opentripplanner.middleware.triptracker; +import org.opentripplanner.middleware.auth.RequestingUser; +import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.otp.response.Itinerary; import org.opentripplanner.middleware.otp.response.Leg; @@ -8,6 +10,7 @@ import java.time.Instant; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getExpectedLeg; +import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getNextLeg; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getSegmentFromPosition; public class TravelerPosition { @@ -24,12 +27,28 @@ public class TravelerPosition { /** Traveler current time. */ public Instant currentTime; - public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary) { + /** Information held about the traveler's journey. */ + public TrackedJourney trackedJourney; + + /** The leg, if available, after the expected leg. */ + public Leg nextLeg; + + /** Traveler mobility information which is passed on to bus operators. */ + public String mobilityMode; + + public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpUser otpUser) { TrackingLocation lastLocation = trackedJourney.locations.get(trackedJourney.locations.size() - 1); currentTime = lastLocation.timestamp.toInstant(); currentPosition = new Coordinates(lastLocation); expectedLeg = getExpectedLeg(currentPosition, itinerary); + if (expectedLeg != null) { + nextLeg = getNextLeg(expectedLeg, itinerary); + } legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); + this.trackedJourney = trackedJourney; + if (otpUser != null && otpUser.mobilityProfile != null) { + mobilityMode = otpUser.mobilityProfile.mobilityMode; + } } /** Used for unit testing. */ diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java index b43b00f24..a36d83efa 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java @@ -1,11 +1,17 @@ package org.opentripplanner.middleware.triptracker; +import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.otp.response.Step; +import java.time.Duration; +import java.time.Instant; + import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; public class TripInstruction { + public enum TripInstructionType { ON_TRACK, DEVIATED, WAIT_FOR_BUS } + /** The radius in meters under which an immediate instruction is given. */ public static final int TRIP_INSTRUCTION_IMMEDIATE_RADIUS = getConfigPropertyAsInt("TRIP_INSTRUCTION_IMMEDIATE_RADIUS", 2); @@ -37,17 +43,25 @@ public class TripInstruction { /** Name of final destination or street. */ public String locationName; - /** Instruction is for a trip that is on track. */ - private boolean tripOnTrack; + /** Provided if the next leg for the traveler will be a bus transit leg. */ + public Leg busLeg; + + /** The time provided by the traveler */ + public Instant currentTime; - /** If the traveler is within the upcoming radius an instruction will be provided. */ - public boolean hasInstruction; + TripInstructionType tripInstructionType; public TripInstruction(boolean isDestination, double distance) { this.distance = distance; - this.tripOnTrack = true; + this.tripInstructionType = TripInstructionType.ON_TRACK; setPrefix(isDestination); - hasInstruction = distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS; + } + + /** + * If the traveler is within the upcoming radius an instruction will be provided. + */ + public boolean hasInstruction() { + return distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS; } /** @@ -70,9 +84,19 @@ public TripInstruction(double distance, String locationName) { * Deviated instruction. */ public TripInstruction(String locationName) { + this.tripInstructionType = TripInstructionType.DEVIATED; this.locationName = locationName; } + /** + * Provide bus related trip instruction. + */ + public TripInstruction(Leg busLeg, Instant currentTime) { + this.tripInstructionType = TripInstructionType.WAIT_FOR_BUS; + this.busLeg = busLeg; + this.currentTime = currentTime; + } + /** * The prefix is defined depending on the traveler either approaching a step or destination and the predefined * distances from these points. @@ -86,16 +110,19 @@ private void setPrefix(boolean isDestination) { } /** - * Build on track or deviated instruction. + * Build instruction based on the traveler's location. */ public String build() { - if (tripOnTrack) { - return buildOnTrackInstruction(); - } else if (locationName != null) { - // Traveler has deviated. - return String.format("Head to %s", locationName); + switch (tripInstructionType) { + case ON_TRACK: + return buildOnTrackInstruction(); + case DEVIATED: + return String.format("Head to %s", locationName); + case WAIT_FOR_BUS: + return buildWaitForBusInstruction(); + default: + return NO_INSTRUCTION; } - return NO_INSTRUCTION; } /** @@ -109,7 +136,7 @@ public String build() { */ private String buildOnTrackInstruction() { - if (hasInstruction) { + if (hasInstruction()) { if (legStep != null) { String relativeDirection = (legStep.relativeDirection.equals("DEPART")) ? "Head " + legStep.absoluteDirection @@ -121,4 +148,17 @@ private String buildOnTrackInstruction() { } return NO_INSTRUCTION; } + + /** + * Build wait for bus instruction. + */ + private String buildWaitForBusInstruction() { + String routeId = busLeg.routeId.split(":")[1]; + if (busLeg.departureDelay > 0) { + long waitInMinutes = Duration.between(busLeg.getScheduledStartTime(), currentTime).toMinutes(); + return String.format("Wait %s minute(s) for bus %s", waitInMinutes, routeId); + } else { + return String.format("Wait for bus %s scheduled to arrive at %s", routeId, busLeg.getScheduledStartTime()); + } + } } diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java index 30f4e9526..40a5e30b4 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java @@ -7,6 +7,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.middleware.auth.RequestingUser; +import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.otp.response.Itinerary; import org.opentripplanner.middleware.otp.response.Leg; @@ -18,6 +20,7 @@ import org.opentripplanner.middleware.triptracker.TravelerPosition; import org.opentripplanner.middleware.triptracker.TravelerLocator; import org.opentripplanner.middleware.triptracker.TripStatus; +import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.Coordinates; import org.opentripplanner.middleware.utils.DateTimeUtils; import org.opentripplanner.middleware.utils.JsonUtils; @@ -52,6 +55,9 @@ public class ManageLegTraversalTest { @BeforeAll public static void setUp() throws IOException { + // Load configuration. + ConfigUtils.loadConfig(new String[]{}); + busStopToJusticeCenterItinerary = JsonUtils.getPOJOFromJSON( CommonTestUtils.getTestResourceAsString("controllers/api/bus-stop-justice-center-trip.json"), Itinerary.class @@ -72,7 +78,7 @@ void canTrackTrip(String time, double lat, double lon, TripStatus expected, Stri TrackedJourney trackedJourney = new TrackedJourney(); TrackingLocation trackingLocation = new TrackingLocation(time, lat, lon); trackedJourney.locations = List.of(trackingLocation); - TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, busStopToJusticeCenterItinerary); + TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, busStopToJusticeCenterItinerary, new OtpUser()); TripStatus tripStatus = TripStatus.getTripStatus(travelerPosition); assertEquals(expected, tripStatus, message); } @@ -371,9 +377,8 @@ void canTrackLegWithoutDeviating() { new Date(currentTime.toInstant().toEpochMilli()) ) ); - TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, busStopToJusticeCenterItinerary); + TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, busStopToJusticeCenterItinerary, null); TripStatus tripStatus = TripStatus.getTripStatus(travelerPosition); - System.out.println(tripStatus.name()); assertEquals(TripStatus.ON_SCHEDULE.name(), tripStatus.name()); cumulativeTravelTime += legSegment.timeInSegment; currentTime = startOfTrip.plus( diff --git a/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json b/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json new file mode 100644 index 000000000..4baa14c2b --- /dev/null +++ b/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json @@ -0,0 +1,386 @@ +{ + "accessibilityScore": null, + "duration": 2137, + "endTime": 1715348772000, + "legs": [ + { + "accessibilityScore": null, + "agency": null, + "alerts": [], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1922.85, + "dropoffType": "SCHEDULED", + "duration": 1871, + "endTime": 1715348506000, + "fareProducts": [], + "from": { + "lat": 33.9567218, + "lon": -83.9829405, + "name": "Private Residence (Northeast)", + "rentalVehicle": null, + "stop": null, + "vertexType": "NORMAL" + }, + "headsign": null, + "interlineWithPreviousLeg": false, + "intermediateStops": null, + "legGeometry": { + "length": 129, + "points": "gdgnEj|q_O@fABpA@j@?D@T?L?L@f@@R?LBv@?F?F@dA@PDV@@?BAD?BAZAXA??@@B@F@D?DFvB?D?D?L?L?F@D?@?\\?`@?D?F@J@?H?H?F?Bj@FdBDj@?H?H@L?B?FD`BFvA?J?D@F@XAB?@@\\?F?D@b@@F?DB`AFlB?D?H?J@DDdADfA?B?D@PJxBJfBJt@VjA^dABJFJR`@d@n@n@p@FDFFpBjBNB^\\DDNLh@d@CFNDJJHFJHBBD?H?B?FRxBzBPFXVTRDJ~@r@RXl@n@bAbAt@~@FHHJJNt@fABB@Bb@l@BBJRT^`AtB@?" + }, + "mode": "WALK", + "pickupBookingInfo": null, + "pickupType": "SCHEDULED", + "realTime": false, + "realtimeState": null, + "rentedBike": false, + "rideHailingEstimate": null, + "route": null, + "startTime": 1715346635000, + "steps": [ + { + "absoluteDirection": "WEST", + "alerts": [], + "area": false, + "distance": 405.01, + "elevationProfile": [], + "lat": 33.9566839, + "lon": -83.9829385, + "relativeDirection": "DEPART", + "stayOn": false, + "streetName": "sidewalk 634432391 (634432391, 11177444159→11488394695) r0.125 l34.840 tmp r0.936 l32.593" + }, + { + "absoluteDirection": "SOUTH", + "alerts": [], + "area": false, + "distance": 17.43, + "elevationProfile": [], + "lat": 33.9564868, + "lon": -83.9873097, + "relativeDirection": "LEFT", + "stayOn": true, + "streetName": "sidewalk 1229990743 (1229990743, 5987163859→11406239363) ☑" + }, + { + "absoluteDirection": "WEST", + "alerts": [], + "area": false, + "distance": 926.13, + "elevationProfile": [], + "lat": 33.9563302, + "lon": -83.9873003, + "relativeDirection": "RIGHT", + "stayOn": true, + "streetName": "sidewalk 634418500 (634418500, 10052651843→5987155585) ☑" + }, + { + "absoluteDirection": "SOUTHWEST", + "alerts": [], + "area": false, + "distance": 26.49, + "elevationProfile": [], + "lat": 33.9536642, + "lon": -83.9961217, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": false, + "streetName": "crossing over Langley Drive (1201573779, 11140140288→11140140287) r0.335 l8.864" + }, + { + "absoluteDirection": "SOUTH", + "alerts": [], + "area": false, + "distance": 10.9, + "elevationProfile": [], + "lat": 33.9534736, + "lon": -83.9962939, + "relativeDirection": "SLIGHTLY_LEFT", + "stayOn": false, + "streetName": "crossing over turn lane (1201573778, 11142713418→11140140285) r0.754 l8.197" + }, + { + "absoluteDirection": "SOUTHWEST", + "alerts": [], + "area": false, + "distance": 109.69, + "elevationProfile": [], + "lat": 33.9533756, + "lon": -83.9962951, + "relativeDirection": "RIGHT", + "stayOn": false, + "streetName": "sidewalk 1201723672 (1201723672, 7423585497→11141711246) ☑" + }, + { + "absoluteDirection": "SOUTHWEST", + "alerts": [], + "area": false, + "distance": 33.51, + "elevationProfile": [], + "lat": 33.9526332, + "lon": -83.9970512, + "relativeDirection": "SLIGHTLY_RIGHT", + "stayOn": false, + "streetName": "crossing over service road (1201723673, 11141711246→10242855762) r0.544 l18.219" + }, + { + "absoluteDirection": "SOUTHWEST", + "alerts": [], + "area": false, + "distance": 297.38, + "elevationProfile": [], + "lat": 33.9523934, + "lon": -83.9972712, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk 1201723674 (1201723674, 11141711247→11141711257) r0.524 l100.031" + }, + { + "absoluteDirection": "SOUTHWEST", + "alerts": [], + "area": false, + "distance": 30.19, + "elevationProfile": [], + "lat": 33.9504509, + "lon": -83.9994645, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "crossing over Gwinnett Drive (1201573770, 11140140268→7417511007) r0.371 l11.196" + }, + { + "absoluteDirection": "SOUTHWEST", + "alerts": [], + "area": false, + "distance": 66.11, + "elevationProfile": [], + "lat": 33.9502891, + "lon": -83.9997262, + "relativeDirection": "CONTINUE", + "stayOn": false, + "streetName": "sidewalk 1206016159 (1206016159, 7417511008→11140099970) ☑ split r0.316 l66.107" + } + ], + "to": { + "lat": 33.949949, + "lon": -84.000314, + "name": "Gwinnett Central High School IB", + "rentalVehicle": null, + "stop": { + "alerts": [], + "code": "360", + "gtfsId": "GwinnettCountyTransit:360", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzYw" + }, + "vertexType": "TRANSIT" + }, + "transitLeg": false, + "trip": null + }, + { + "accessibilityScore": null, + "agency": { + "alerts": [ + { + "alertDescriptionText": "Expect Delays - Starting on January 22, Atlanta Gas Light will be replacing natural gas pipelines along Buford Highway in Norcross and Peachtree Corners.\n", + "alertHeaderText": "Expect Delays - Starting on January 22, Atlanta Gas Light will be replacing natural gas pipelines along Buford Highway in Norcross and Peachtree Corners.\n", + "alertUrl": null, + "effectiveStartDate": 1705881600, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1MzA" + }, + { + "alertDescriptionText": "This work may result in delays on routes 10A/10B, 20, and 35. Work will take place from 7PM to 5AM Saturday through Friday each week. The final phase of replacement work is expected to begin in late summer 2024.", + "alertHeaderText": "This work may result in delays on routes 10A/10B, 20, and 35. Work will take place from 7PM to 5AM Saturday through Friday each week. The final phase of replacement work is expected to begin in late summer 2024.", + "alertUrl": null, + "effectiveStartDate": 1705881600, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1MzE" + }, + { + "alertDescriptionText": "Delay in Service - Due to mechanical issues, the route 35 outbound from Peachtree Pkwy at the Forum at 8:20am and inbound from Doraville Marta Station at 9:25am is delayed. We apologize for the inconvenience.", + "alertHeaderText": "Delay in Service - Due to mechanical issues, the route 35 outbound from Peachtree Pkwy at the Forum at 8:20am and inbound from Doraville Marta Station at 9:25am is delayed. We apologize for the inconvenience.", + "alertUrl": null, + "effectiveStartDate": 1715299200, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI4MDQ" + }, + { + "alertDescriptionText": "Delay in Service - Due to mechanical issues, the route 35 outbound from Peachtree Parkway and the Forum at 8:20apm and inbound from Doraville Marta Station at 9:25am is delayed. We apologize for the inconvenience.", + "alertHeaderText": "Delay in Service - Due to mechanical issues, the route 35 outbound from Peachtree Parkway and the Forum at 8:20apm and inbound from Doraville Marta Station at 9:25am is delayed. We apologize for the inconvenience.", + "alertUrl": null, + "effectiveStartDate": 1715299200, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI4MDU" + }, + { + "alertDescriptionText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertHeaderText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertUrl": null, + "effectiveStartDate": 1707350400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1ODk" + }, + { + "alertDescriptionText": "Alert – Due to a rehabilitation program at Indian Creek MARTA Station, bus bay closures will be in effect starting April 3, to June 5, 2024. All Ride Gwinnett Route 70 buses will be re-routed. Please follow MARTA signage for the temporary pick-up point.", + "alertHeaderText": "Alert – Due to a rehabilitation program at Indian Creek MARTA Station, bus bay closures will be in effect starting April 3, to June 5, 2024. All Ride Gwinnett Route 70 buses will be re-routed. Please follow MARTA signage for the temporary pick-up point.", + "alertUrl": null, + "effectiveStartDate": 1712016000, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI3MDc" + } + ], + "gtfsId": "GwinnettCountyTransit:GCT", + "id": "GwinnettCountyTransit:GCT", + "name": "Gwinnett County Transit", + "timezone": "America/New_York", + "url": "https://www.ridegwinnett.com/" + }, + "alerts": [ + { + "alertDescriptionText": "Alert – Due to a rehabilitation program at Indian Creek MARTA Station, bus bay closures will be in effect starting April 3, to June 5, 2024. All Ride Gwinnett Route 70 buses will be re-routed. Please follow MARTA signage for the temporary pick-up point.", + "alertHeaderText": "Alert – Due to a rehabilitation program at Indian Creek MARTA Station, bus bay closures will be in effect starting April 3, to June 5, 2024. All Ride Gwinnett Route 70 buses will be re-routed. Please follow MARTA signage for the temporary pick-up point.", + "alertUrl": null, + "effectiveStartDate": 1712016000, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI3MDc" + }, + { + "alertDescriptionText": "Expect Delays - Starting on January 22, Atlanta Gas Light will be replacing natural gas pipelines along Buford Highway in Norcross and Peachtree Corners.\n", + "alertHeaderText": "Expect Delays - Starting on January 22, Atlanta Gas Light will be replacing natural gas pipelines along Buford Highway in Norcross and Peachtree Corners.\n", + "alertUrl": null, + "effectiveStartDate": 1705881600, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1MzA" + }, + { + "alertDescriptionText": "This work may result in delays on routes 10A/10B, 20, and 35. Work will take place from 7PM to 5AM Saturday through Friday each week. The final phase of replacement work is expected to begin in late summer 2024.", + "alertHeaderText": "This work may result in delays on routes 10A/10B, 20, and 35. Work will take place from 7PM to 5AM Saturday through Friday each week. The final phase of replacement work is expected to begin in late summer 2024.", + "alertUrl": null, + "effectiveStartDate": 1705881600, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1MzE" + }, + { + "alertDescriptionText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertHeaderText": "Attention Route 50 Riders: The following outbound bus stops are not active on Route 50. \n-\tWoodward Crossing Blvd & The Complex – Stop 5051\n-\tMall of GA Blvd & Nature Pkwy OB – Stop 5054", + "alertUrl": null, + "effectiveStartDate": 1707350400, + "id": "QWxlcnQ6R3dpbm5ldHRDb3VudHlUcmFuc2l0OjI1ODk" + } + ], + "arrivalDelay": 0, + "departureDelay": 0, + "distance": 1998.77, + "dropoffType": "SCHEDULED", + "duration": 219, + "endTime": 1715348725000, + "from": { + "lat": 33.949949, + "lon": -84.000314, + "name": "Gwinnett Central High School IB", + "rentalVehicle": null, + "stop": { + "alerts": [], + "code": "360", + "gtfsId": "GwinnettCountyTransit:360", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzYw" + }, + "vertexType": "TRANSIT" + }, + "headsign": "Transit Center", + "interlineWithPreviousLeg": false, + "intermediateStops": [ + { + "lat": 33.948037, + "locationType": "STOP", + "lon": -83.99822, + "name": "Gwinnett Dr & HS Football Field", + "stopCode": "355", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzU1" + }, + { + "lat": 33.946601, + "locationType": "STOP", + "lon": -83.997147, + "name": "Gwinnett Dr & Police Security Bldg", + "stopCode": "354", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzU0" + }, + { + "lat": 33.943999, + "locationType": "STOP", + "lon": -83.993521, + "name": "Gwinnett Dr & Stone Mountain St", + "stopCode": "358", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzU4" + }, + { + "lat": 33.943243, + "locationType": "STOP", + "lon": -83.992318, + "name": "Gwinnett Dr & Bridgestone Tire", + "stopCode": "350", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzUw" + }, + { + "lat": 33.941479, + "locationType": "STOP", + "lon": -83.990338, + "name": "Gwinnett Dr & PNC Bank", + "stopCode": "356", + "stopId": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzU2" + } + ], + "legGeometry": { + "length": 31, + "points": "szenEjiu_OACO[Wg@s@uAVMlE{A|FsD????zGwE@???~EwD`HwO@A??tCqF??DGrCaGnAsA|Bu@??nAg@hAiAz@oBN}@OiIp@cGGsD" + }, + "mode": "BUS", + "pickupBookingInfo": null, + "pickupType": "SCHEDULED", + "realTime": true, + "realtimeState": null, + "rentedBike": null, + "rideHailingEstimate": null, + "route": { + "alerts": [], + "color": "AB0634", + "gtfsId": "GwinnettCountyTransit:40", + "id": "GwinnettCountyTransit:40", + "longName": "Lawrenceville - GTC - Sugarloaf Mills", + "shortName": "40", + "textColor": "FFFFFF", + "type": 3 + }, + "startTime": 1715348506000, + "steps": [], + "to": { + "lat": 33.940172, + "lon": -83.984931, + "name": "Gwinnett Dr & Kroger Shopping IB", + "rentalVehicle": null, + "stop": { + "alerts": [], + "code": "353", + "gtfsId": "GwinnettCountyTransit:353", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzUz" + }, + "vertexType": "TRANSIT" + }, + "transitLeg": true, + "trip": { + "arrivalStoptime": { + "stop": { + "gtfsId": "GwinnettCountyTransit:3", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6Mw" + }, + "stopPosition": 3480 + }, + "departureStoptime": { + "stop": { + "gtfsId": "GwinnettCountyTransit:16", + "id": "U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MTY" + }, + "stopPosition": 0 + }, + "gtfsId": "GwinnettCountyTransit:t3A4-b130-sl6", + "id": "VHJpcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6dDNBNC1iMTMwLXNsNg" + } + } + ], + "startTime": 1715346635000, + "transfers": 0, + "waitingTime": 0, + "walkTime": 1918 +} \ No newline at end of file From 376cd3c9a46f90868a5cd3f2e5c597ff358e7636 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 15 May 2024 08:32:47 +0100 Subject: [PATCH 02/16] refactor(Various updates and started on unit tests): --- .../middleware/otp/response/Leg.java | 2 +- .../middleware/otp/response/Route.java | 13 +++++ .../triptracker/NotifyBusOperator.java | 37 ++++++++----- .../triptracker/TravelerPosition.java | 2 +- .../triptracker/TripInstruction.java | 5 +- .../middleware/utils/JsonUtils.java | 8 ++- .../ManageLegTraversalTest.java | 4 +- .../triptracker/NotifyBusOperatorTest.java | 54 +++++++++++++++++++ 8 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Route.java rename src/test/java/org/opentripplanner/middleware/{controllers/api => triptracker}/ManageLegTraversalTest.java (99%) create mode 100644 src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java b/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java index 4b396de78..d92756d52 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java @@ -27,7 +27,7 @@ public class Leg implements Cloneable { public Double distance; public Boolean pathway; public String mode; - public String route; + public Route route; public Boolean interlineWithPreviousLeg; public Place from; public Place to; diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Route.java b/src/main/java/org/opentripplanner/middleware/otp/response/Route.java new file mode 100644 index 000000000..978a48f09 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Route.java @@ -0,0 +1,13 @@ +package org.opentripplanner.middleware.otp.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Route { + public String id; + public String gtfsId; + public String longName; + public String shortName; +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java index 45e4015d5..b5caee869 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java @@ -12,6 +12,8 @@ import java.net.URI; import java.time.Duration; import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -30,13 +32,17 @@ private NotifyBusOperator() {} private static final Logger LOG = LoggerFactory.getLogger(NotifyBusOperator.class); + private static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .withZone(ZoneId.systemDefault()); + private static final String BUS_OPERATOR_NOTIFIER_API_URL = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_URL", "not-provided"); private static final String BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY", "not-provided"); - private static final List QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); + public static List QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); private static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; @@ -63,6 +69,13 @@ private static List getBusOperatorNotifierQualifyingRoutes() { return new ArrayList<>(); } + /** + * Used for unit tests and overrides the qualifying routes held in config. + */ + public static void getBusOperatorNotifierQualifyingRoutes(List routeId) { + QUALIFYING_BUS_NOTIFIER_ROUTES = routeId; + } + /** * Stage notification to bus operator by making sure all required conditions are met. */ @@ -71,13 +84,13 @@ public static void sendNotification(TripStatus tripStatus, TravelerPosition trav if ( isBusLeg(travelerPosition.nextLeg) && isWithinOperationalNotifyWindow(tripStatus, travelerPosition) && - hasNotPreviouslyNotifiedBusDriverForRoute(travelerPosition.trackedJourney, travelerPosition.nextLeg.routeId) && - supportsBusOperatorNotification(travelerPosition.nextLeg.routeId) + hasNotPreviouslyNotifiedBusDriverForRoute(travelerPosition.trackedJourney, travelerPosition.nextLeg.route.id) && + supportsBusOperatorNotification(travelerPosition.nextLeg.route.id) ) { var body = createPostBody(travelerPosition); var httpStatus = doPost(body); if (httpStatus == HttpStatus.OK_200) { - travelerPosition.trackedJourney.updateNotificationRequest(travelerPosition.nextLeg.routeId, body); + travelerPosition.trackedJourney.updateNotificationRequest(travelerPosition.nextLeg.route.id, body); } else { LOG.error("Error {} while trying to initiate notification to bus operator.", httpStatus); } @@ -95,10 +108,10 @@ public static void cancelNotification(TravelerPosition travelerPosition) { try { if (isBusLeg(travelerPosition.nextLeg)) { HashMap busNotificationRequests = travelerPosition.trackedJourney.busNotificationRequests; - if (busNotificationRequests.containsKey(travelerPosition.nextLeg.routeId)) { - var httpStatus = doPost(busNotificationRequests.get(travelerPosition.nextLeg.routeId)); + if (busNotificationRequests.containsKey(travelerPosition.nextLeg.route.id)) { + var httpStatus = doPost(busNotificationRequests.get(travelerPosition.nextLeg.route.id)); if (httpStatus == HttpStatus.OK_200) { - travelerPosition.trackedJourney.removeNotificationRequest(travelerPosition.nextLeg.routeId); + travelerPosition.trackedJourney.removeNotificationRequest(travelerPosition.nextLeg.route.id); } else { LOG.error("Error {} while trying to cancel notification to bus operator.", httpStatus); } @@ -189,23 +202,23 @@ public static long getMinutesAheadOfSchedule(TravelerPosition travelerPosition) * 'To' fields omitted as they are not need for requests for single transit legs. */ public static class NotifyBody { - public Instant timestamp; + public String timestamp; public String agency_id; public String from_route_id; public String from_trip_id; public String from_stop_id; - public Instant from_arrival_time; + public String from_arrival_time; public String msg_type; public String mobility_codes; public boolean trusted_companion; public NotifyBody(Instant timestamp, TravelerPosition travelerPosition) { - this.timestamp = timestamp; + this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(timestamp); this.agency_id = travelerPosition.nextLeg.agencyId; - this.from_route_id = travelerPosition.nextLeg.routeId; + this.from_route_id = travelerPosition.nextLeg.route.id; this.from_trip_id = travelerPosition.nextLeg.tripId; this.from_stop_id = travelerPosition.nextLeg.from.stopId; - this.from_arrival_time = travelerPosition.nextLeg.getScheduledStartTime().toInstant(); + this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(travelerPosition.nextLeg.getScheduledStartTime().toInstant()); this.msg_type = "1"; this.mobility_codes = travelerPosition.mobilityMode; this.trusted_companion = false; diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index a4e21585f..4962eaa18 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -53,8 +53,8 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU /** Used for unit testing. */ public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { - this.expectedLeg = expectedLeg; this.currentPosition = currentPosition; + this.expectedLeg = expectedLeg; legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java index a36d83efa..b73d91483 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java @@ -49,7 +49,8 @@ public enum TripInstructionType { ON_TRACK, DEVIATED, WAIT_FOR_BUS } /** The time provided by the traveler */ public Instant currentTime; - TripInstructionType tripInstructionType; + /** The type of instruction to be provided to te traveler. */ + private final TripInstructionType tripInstructionType; public TripInstruction(boolean isDestination, double distance) { this.distance = distance; @@ -153,7 +154,7 @@ private String buildOnTrackInstruction() { * Build wait for bus instruction. */ private String buildWaitForBusInstruction() { - String routeId = busLeg.routeId.split(":")[1]; + String routeId = busLeg.route.id.split(":")[1]; if (busLeg.departureDelay > 0) { long waitInMinutes = Duration.between(busLeg.getScheduledStartTime(), currentTime).toMinutes(); return String.format("Wait %s minute(s) for bus %s", waitInMinutes, routeId); diff --git a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java index 56da67c75..491c149c6 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.utils; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -26,7 +27,12 @@ public class JsonUtils { private static final Logger LOG = LoggerFactory.getLogger(JsonUtils.class); - private static final ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper; + + static { + mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); + } /** * Serialize an object into a JSON string representation. diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java similarity index 99% rename from src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java rename to src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index 40a5e30b4..df7904e6c 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -1,4 +1,4 @@ -package org.opentripplanner.middleware.controllers.api; +package org.opentripplanner.middleware.triptracker; import io.leonard.PolylineUtils; import io.leonard.Position; @@ -55,7 +55,7 @@ public class ManageLegTraversalTest { @BeforeAll public static void setUp() throws IOException { - // Load configuration. + // Load default env.yml configuration. ConfigUtils.loadConfig(new String[]{}); busStopToJusticeCenterItinerary = JsonUtils.getPOJOFromJSON( diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java new file mode 100644 index 000000000..9fe898fc5 --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -0,0 +1,54 @@ +package org.opentripplanner.middleware.triptracker; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.otp.response.Itinerary; +import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.testutils.CommonTestUtils; +import org.opentripplanner.middleware.utils.ConfigUtils; +import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.JsonUtils; + +import java.io.IOException; +import java.sql.Date; +import java.time.Instant; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; + +class NotifyBusOperatorTest { + + private static Itinerary walkToBusTransition; + + @BeforeAll + public static void setUp() throws IOException { + // Load default env.yml configuration. + ConfigUtils.loadConfig(new String[] {}); + + walkToBusTransition = JsonUtils.getPOJOFromJSON( + CommonTestUtils.getTestResourceAsString("controllers/api/walk-to-bus-transition.json"), + Itinerary.class + ); + } + + @Test + void canNotifyBusOperatorEndToEnd() { + NotifyBusOperator.getBusOperatorNotifierQualifyingRoutes(List.of("GwinnettCountyTransit:40")); + Leg walkLeg = walkToBusTransition.legs.get(0); + Leg busLeg = walkToBusTransition.legs.get(1); + Coordinates legToCoords = new Coordinates(walkLeg.to); + TrackedJourney trackedJourney = new TrackedJourney(); + trackedJourney.locations.add(new TrackingLocation(legToCoords.lat, legToCoords.lon, Date.from(Instant.now()))); + OtpUser otpUser = new OtpUser(); + TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, otpUser); + String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); + TripInstruction expectInstruction = new TripInstruction(busLeg, Instant.now()); + assertEquals(expectInstruction.build(), Objects.requireNonNullElse(tripInstruction, NO_INSTRUCTION), ""); + } + + +} \ No newline at end of file From f66cef01fb583cca78fdcb7a8a2c5e8c1e36b9a7 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 16 May 2024 15:19:34 +0100 Subject: [PATCH 03/16] refactor(Various changes following unit test creation): --- .../middleware/models/TrackedJourney.java | 21 +-- .../middleware/otp/response/Agency.java | 10 ++ .../middleware/otp/response/Leg.java | 4 + .../middleware/otp/response/Place.java | 2 + .../middleware/otp/response/Stop.java | 10 ++ .../middleware/otp/response/Trip.java | 10 ++ .../triptracker/BusOpNotificationMessage.java | 104 +++++++++++++ .../triptracker/NotifyBusOperator.java | 81 +++------- .../triptracker/TripInstruction.java | 26 +++- .../models/MobilityProfileTest.java | 2 + .../triptracker/NotifyBusOperatorTest.java | 146 ++++++++++++++++-- 11 files changed, 330 insertions(+), 86 deletions(-) create mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Agency.java create mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Stop.java create mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Trip.java create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java diff --git a/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java b/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java index a41170f1c..80a66113b 100644 --- a/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java +++ b/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java @@ -8,6 +8,7 @@ import java.util.Date; import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; @JsonIgnoreProperties(ignoreUnknown = true) @@ -23,12 +24,12 @@ public class TrackedJourney extends Model { public List locations = new ArrayList<>(); - public static final HashMap busNotificationRequests = new HashMap<>(); + public Map busNotificationMessages = new HashMap<>(); public static final String TRIP_ID_FIELD_NAME = "tripId"; public static final String LOCATIONS_FIELD_NAME = "locations"; - public static final String BUS_NOTIFICATION_REQUESTS_FIELD_NAME = "busNotificationRequests"; + public static final String BUS_NOTIFICATION_MESSAGES_FIELD_NAME = "busNotificationMessages"; public static final String END_TIME_FIELD_NAME = "endTime"; @@ -82,21 +83,21 @@ public TrackingLocation lastLocation() { return locations.get(locations.size() - 1); } - public void updateNotificationRequest(String routeId, String body) { - busNotificationRequests.put(routeId, body); + public void updateNotificationMessage(String routeId, String body) { + busNotificationMessages.put(routeId, body); Persistence.trackedJourneys.updateField( id, - BUS_NOTIFICATION_REQUESTS_FIELD_NAME, - busNotificationRequests + BUS_NOTIFICATION_MESSAGES_FIELD_NAME, + busNotificationMessages ); } - public void removeNotificationRequest(String routeId) { - busNotificationRequests.remove(routeId); + public void removeNotificationMessage(String routeId) { + busNotificationMessages.remove(routeId); Persistence.trackedJourneys.updateField( id, - BUS_NOTIFICATION_REQUESTS_FIELD_NAME, - busNotificationRequests + BUS_NOTIFICATION_MESSAGES_FIELD_NAME, + busNotificationMessages ); } diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Agency.java b/src/main/java/org/opentripplanner/middleware/otp/response/Agency.java new file mode 100644 index 000000000..16e23b10c --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Agency.java @@ -0,0 +1,10 @@ +package org.opentripplanner.middleware.otp.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Agency { + public String id; +} diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java b/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java index d92756d52..c371ebf39 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java @@ -45,8 +45,12 @@ public class Leg implements Cloneable { public Integer routeType; public String routeId; public String agencyId; + // TODO: Look to remove once OTP2 work has been merged. + public Agency agency; public String tripBlockId; public String tripId; + // TODO: Look to remove once OTP2 work has been merged. + public Trip trip; public String serviceDate; public List interStopGeometry = null; public String routeShortName; diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Place.java b/src/main/java/org/opentripplanner/middleware/otp/response/Place.java index d6b89e314..b5bce8f99 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Place.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Place.java @@ -21,6 +21,8 @@ public class Place implements Cloneable { public String orig; public String vertexType; public String stopId; + // TODO: Look to remove once OTP2 work has been merged. + public Stop stop; public Date arrival; public Integer stopIndex; public Integer stopSequence; diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Stop.java b/src/main/java/org/opentripplanner/middleware/otp/response/Stop.java new file mode 100644 index 000000000..bb4ae9592 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Stop.java @@ -0,0 +1,10 @@ +package org.opentripplanner.middleware.otp.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Stop { + public String id; +} diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Trip.java b/src/main/java/org/opentripplanner/middleware/otp/response/Trip.java new file mode 100644 index 000000000..8f1a08764 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Trip.java @@ -0,0 +1,10 @@ +package org.opentripplanner.middleware.otp.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonIgnoreProperties(ignoreUnknown = true) +public class Trip { + public String id; +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java new file mode 100644 index 000000000..c054a0ef7 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java @@ -0,0 +1,104 @@ +package org.opentripplanner.middleware.triptracker; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Class containing the expected notify parameters. These will be converted to JSON and make up the body content of + * the request. + *

+ * 'To' fields omitted as they are not needed for requests for single transit legs. + */ +public class BusOpNotificationMessage { + + public BusOpNotificationMessage() { + // Required for JSON deserialization. + } + + private static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT = DateTimeFormatter + .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + .withZone(ZoneId.systemDefault()); + + public static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT = DateTimeFormatter + .ofPattern("HH:mm:ss") + .withZone(ZoneId.systemDefault()); + + private static final Map MOBILITY_CODES_LOOKUP = createMobilityCodesLookup(); + + /** + * Create a relationship between mobility modes and mobility codes. + */ + private static Map createMobilityCodesLookup() { + HashMap codes = new HashMap<>(); + codes.put("Device", 1); + codes.put("Mscooter", 2); + codes.put("WchairE", 3); + codes.put("WchairM", 4); + codes.put("Some", 5); + codes.put("LowVision", 6); + codes.put("Blind", 7); + codes.put("Device-LowVision", 8); + codes.put("Mscooter-LowVision", 9); + codes.put("WChairE-LowVision", 10); + codes.put("WChairM-LowVision", 11); + codes.put("Some-LowVision", 12); + codes.put("Device-Blind", 13); + codes.put("Mscooter-Blind", 14); + codes.put("WchairE-Blind", 15); + codes.put("WchairM-Blind", 16); + codes.put("Some-Blind", 17); + return codes; + } + + public String timestamp; + public String agency_id; + public String from_route_id; + public String from_trip_id; + public String from_stop_id; + public String from_arrival_time; + public Integer msg_type; + public List mobility_codes; + public boolean trusted_companion; + + public BusOpNotificationMessage(Instant timestamp, TravelerPosition travelerPosition) { + this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(timestamp); + this.agency_id = removeAgencyPrefix(travelerPosition.nextLeg.agency.id); + this.from_route_id = removeAgencyPrefix(travelerPosition.nextLeg.route.id); + this.from_trip_id = travelerPosition.nextLeg.trip.id; + this.from_stop_id = travelerPosition.nextLeg.from.stop.id; + this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format( + travelerPosition.nextLeg.getScheduledStartTime().toInstant() + ); + // 1 = Notify, 0 = Cancel. + this.msg_type = 1; + this.mobility_codes = getMobilityCode(travelerPosition.mobilityMode); + this.trusted_companion = false; + } + + /** + * Get the second element from value by removing the OTP agency prefix. + * E.g. GwinnettCountyTransit:GCT will return just GCT. + */ + private String removeAgencyPrefix(String value) { + return (value != null) ? value.split(":")[1] : null; + } + + /** + * Get the mobility code that matches the mobility mode. Although the API can accept multiple codes, OTP middleware + * currently only provides one. + */ + private static List getMobilityCode(String mobilityMode) { + List mobilityCodes = new ArrayList<>(); + Integer code = MOBILITY_CODES_LOOKUP.get(mobilityMode); + if (code != null) { + mobilityCodes.add(code); + } + return mobilityCodes; + } + +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java index b5caee869..daa9f0d2d 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java @@ -12,11 +12,8 @@ import java.net.URI; import java.time.Duration; import java.time.Instant; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -28,13 +25,11 @@ */ public class NotifyBusOperator { - private NotifyBusOperator() {} + public NotifyBusOperator() {} - private static final Logger LOG = LoggerFactory.getLogger(NotifyBusOperator.class); + public static boolean IS_TEST = false; - private static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT = DateTimeFormatter - .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneId.systemDefault()); + private static final Logger LOG = LoggerFactory.getLogger(NotifyBusOperator.class); private static final String BUS_OPERATOR_NOTIFIER_API_URL = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_URL", "not-provided"); @@ -44,7 +39,7 @@ private NotifyBusOperator() {} public static List QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); - private static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; + public static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; /** * Headers that are required for each request. @@ -54,6 +49,7 @@ private NotifyBusOperator() {} "Content-Type", "application/json" ); + /** * Get the routes which qualify for bus operator notifying from the configuration. The configuration value is * expected to be a comma separate list of agency id (as provided by OTP not agency) and route id e.g. @@ -69,13 +65,6 @@ private static List getBusOperatorNotifierQualifyingRoutes() { return new ArrayList<>(); } - /** - * Used for unit tests and overrides the qualifying routes held in config. - */ - public static void getBusOperatorNotifierQualifyingRoutes(List routeId) { - QUALIFYING_BUS_NOTIFIER_ROUTES = routeId; - } - /** * Stage notification to bus operator by making sure all required conditions are met. */ @@ -90,7 +79,7 @@ public static void sendNotification(TripStatus tripStatus, TravelerPosition trav var body = createPostBody(travelerPosition); var httpStatus = doPost(body); if (httpStatus == HttpStatus.OK_200) { - travelerPosition.trackedJourney.updateNotificationRequest(travelerPosition.nextLeg.route.id, body); + travelerPosition.trackedJourney.updateNotificationMessage(travelerPosition.nextLeg.route.id, body); } else { LOG.error("Error {} while trying to initiate notification to bus operator.", httpStatus); } @@ -100,18 +89,24 @@ public static void sendNotification(TripStatus tripStatus, TravelerPosition trav } } - /** * Cancel a previously sent notification for the next bus leg. */ public static void cancelNotification(TravelerPosition travelerPosition) { try { - if (isBusLeg(travelerPosition.nextLeg)) { - HashMap busNotificationRequests = travelerPosition.trackedJourney.busNotificationRequests; - if (busNotificationRequests.containsKey(travelerPosition.nextLeg.route.id)) { - var httpStatus = doPost(busNotificationRequests.get(travelerPosition.nextLeg.route.id)); + if (isBusLeg(travelerPosition.nextLeg) && travelerPosition.nextLeg.route.id != null) { + String routeId = travelerPosition.nextLeg.route.id; + Map busNotificationRequests = travelerPosition.trackedJourney.busNotificationMessages; + if (busNotificationRequests.containsKey(routeId)) { + BusOpNotificationMessage busOpNotificationMessage = JsonUtils.getPOJOFromJSON( + busNotificationRequests.get(routeId), + BusOpNotificationMessage.class + ); + // Changed the saved message type from notify to cancel. + busOpNotificationMessage.msg_type = 0; + var httpStatus = doPost(JsonUtils.toJson(busOpNotificationMessage)); if (httpStatus == HttpStatus.OK_200) { - travelerPosition.trackedJourney.removeNotificationRequest(travelerPosition.nextLeg.route.id); + travelerPosition.trackedJourney.removeNotificationMessage(routeId); } else { LOG.error("Error {} while trying to cancel notification to bus operator.", httpStatus); } @@ -126,6 +121,10 @@ public static void cancelNotification(TravelerPosition travelerPosition) { * Send notification and provide response. The service only provides the HTTP status as a response. */ public static int doPost(String body) { + if (IS_TEST) { + // TODO figure out how to mock a static method! + return 200; + } var httpResponse = HttpUtils.httpRequestRawResponse( URI.create(BUS_OPERATOR_NOTIFIER_API_URL), 1000, @@ -140,7 +139,7 @@ public static int doPost(String body) { * Create post body that will be sent to bus notification API. */ public static String createPostBody(TravelerPosition travelerPosition) { - return JsonUtils.toJson(new NotifyBody(travelerPosition.currentTime, travelerPosition)); + return JsonUtils.toJson(new BusOpNotificationMessage(travelerPosition.currentTime, travelerPosition)); } /** @@ -162,7 +161,7 @@ public static boolean supportsBusOperatorNotification(String gtfsId) { * Has the bus driver already been notified for this journey. The driver must only be notified once. */ public static boolean hasNotPreviouslyNotifiedBusDriverForRoute(TrackedJourney trackedJourney, String routeId) { - for (String notifiedRouteId : trackedJourney.busNotificationRequests.keySet()) { + for (String notifiedRouteId : trackedJourney.busNotificationMessages.keySet()) { if (notifiedRouteId.equalsIgnoreCase(routeId)) { return false; } @@ -174,7 +173,7 @@ public static boolean hasNotPreviouslyNotifiedBusDriverForRoute(TrackedJourney t * Make sure the traveler is on schedule or ahead of schedule (but not too far) to be within an operational window * for the bus service. */ - private static boolean isWithinOperationalNotifyWindow(TripStatus tripStatus, TravelerPosition travelerPosition) { + public static boolean isWithinOperationalNotifyWindow(TripStatus tripStatus, TravelerPosition travelerPosition) { return tripStatus.equals(TripStatus.ON_SCHEDULE) || ( @@ -194,34 +193,4 @@ public static long getMinutesAheadOfSchedule(TravelerPosition travelerPosition) .plusSeconds((long) getSegmentStartTime(travelerPosition.legSegmentFromPosition)); return Duration.between(segmentStartTime, travelerPosition.currentTime).toMinutes(); } - - /** - * Class containing the expected notify parameters. These will be converted to JSON and make up the body content of - * the request. - *

- * 'To' fields omitted as they are not need for requests for single transit legs. - */ - public static class NotifyBody { - public String timestamp; - public String agency_id; - public String from_route_id; - public String from_trip_id; - public String from_stop_id; - public String from_arrival_time; - public String msg_type; - public String mobility_codes; - public boolean trusted_companion; - - public NotifyBody(Instant timestamp, TravelerPosition travelerPosition) { - this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(timestamp); - this.agency_id = travelerPosition.nextLeg.agencyId; - this.from_route_id = travelerPosition.nextLeg.route.id; - this.from_trip_id = travelerPosition.nextLeg.tripId; - this.from_stop_id = travelerPosition.nextLeg.from.stopId; - this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(travelerPosition.nextLeg.getScheduledStartTime().toInstant()); - this.msg_type = "1"; - this.mobility_codes = travelerPosition.mobilityMode; - this.trusted_companion = false; - } - } } \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java index b73d91483..bf03bb9a8 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java @@ -2,10 +2,12 @@ import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.otp.response.Step; +import org.opentripplanner.middleware.utils.DateTimeUtils; import java.time.Duration; import java.time.Instant; +import static org.opentripplanner.middleware.triptracker.BusOpNotificationMessage.BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; public class TripInstruction { @@ -156,10 +158,28 @@ private String buildOnTrackInstruction() { private String buildWaitForBusInstruction() { String routeId = busLeg.route.id.split(":")[1]; if (busLeg.departureDelay > 0) { - long waitInMinutes = Duration.between(busLeg.getScheduledStartTime(), currentTime).toMinutes(); - return String.format("Wait %s minute(s) for bus %s", waitInMinutes, routeId); + long waitInMinutes = Duration + .between(currentTime.atZone(DateTimeUtils.getOtpZoneId()), busLeg.getScheduledStartTime()) + .toMinutes(); + return String.format("Wait%s for bus %s", getReadableMinutes(waitInMinutes), routeId); } else { - return String.format("Wait for bus %s scheduled to arrive at %s", routeId, busLeg.getScheduledStartTime()); + return String.format( + "Wait for bus %s scheduled to arrive at %s", + routeId, + BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format(busLeg.getScheduledStartTime()) + ); } } + + /** + * Get the number of minutes to wait for a bus. If the wait is zero (or less than zero!) return empty string. + */ + private String getReadableMinutes(long waitInMinutes) { + if (waitInMinutes == 1) { + return String.format(" %s minute", waitInMinutes); + } else if (waitInMinutes > 1) { + return String.format(" %s minutes", waitInMinutes); + } + return ""; + } } diff --git a/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java b/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java index 5fe9bfbaf..446515c36 100644 --- a/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java +++ b/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java @@ -36,6 +36,7 @@ public void testModes(Set devices, String mode) { var prof = new MobilityProfile(); prof.mobilityDevices = devices; prof.updateMobilityMode(); + System.out.println(prof.mobilityMode); Assertions.assertEquals(mode, prof.mobilityMode); } @@ -55,6 +56,7 @@ public void testModesVision(MobilityProfile.VisionLimitation limitation, Set createWithinOperationalNotifyWindowTrace() { + Leg walkLeg = walkToBusTransition.legs.get(0); + Instant timeAtEndOfWalkLeg = walkLeg.endTime.toInstant(); + TrackedJourney trackedJourney = createAndPersistTrackedJourney( + false, + getEndOfWalkLegCoordinates(), + timeAtEndOfWalkLeg + ); + TravelerPosition travelerPosition1 = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); + trackedJourney = createAndPersistTrackedJourney( + false, + getEndOfWalkLegCoordinates(), + timeAtEndOfWalkLeg.plusSeconds(60 * ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES) + ); + TravelerPosition travelerPosition2 = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); + return Stream.of( + Arguments.of(true, TripStatus.ON_SCHEDULE, null, "Traveler is on schedule, notification can be sent."), + Arguments.of(false, TripStatus.BEHIND_SCHEDULE, null, "Traveler is behind schedule, notification can not be sent."), + Arguments.of(true, TripStatus.AHEAD_OF_SCHEDULE, travelerPosition1, "Traveler is ahead of schedule, but within the notify window."), + Arguments.of(false, TripStatus.AHEAD_OF_SCHEDULE, travelerPosition2, "Too far ahead of schedule to notify bus operator.") + ); + } + private static OtpUser createOtpUser() { + MobilityProfile mobilityProfile = new MobilityProfile(); + mobilityProfile.mobilityMode = "WchairE"; + OtpUser otpUser = new OtpUser(); + otpUser.mobilityProfile = mobilityProfile; + return otpUser; + } + + private static TrackedJourney createAndPersistTrackedJourney(Coordinates legToCoords) { + return createAndPersistTrackedJourney(true, legToCoords, Instant.now()); + } + + private static TrackedJourney createAndPersistTrackedJourney(boolean persist, Coordinates legToCoords, Instant dateTime) { + trackedJourney = new TrackedJourney(); + trackedJourney.locations.add(new TrackingLocation(legToCoords.lat, legToCoords.lon, Date.from(dateTime))); + if (persist) Persistence.trackedJourneys.create(trackedJourney); + return trackedJourney; + } + + private static Coordinates getEndOfWalkLegCoordinates() { + Leg walkLeg = walkToBusTransition.legs.get(0); + return new Coordinates(walkLeg.to); + } } \ No newline at end of file From 6ba71914196a35621d038409262252e23e356da1 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 30 May 2024 08:52:16 +0100 Subject: [PATCH 04/16] refactor(Addressed PR feedback): Various updates to meet suggested changes --- .../default/bus-notifier-actions.yml | 2 + configurations/default/env.yml.tmp | 6 +- .../triptracker/ManageTripTracking.java | 19 ++-- .../triptracker/TravelerLocator.java | 11 +- .../triptracker/TravelerPosition.java | 2 +- .../triptracker/TripInstruction.java | 8 +- .../busnotifiers/AgencyAction.java | 19 ++++ .../busnotifiers/BusOperatorActions.java | 107 ++++++++++++++++++ .../busnotifiers/BusOperatorInteraction.java | 11 ++ ...RideGwinnettBusOpNotificationMessage.java} | 59 +++++----- .../UsRideGwinnettNotifyBusOperator.java} | 73 ++++++------ .../middleware/utils/ItineraryUtils.java | 43 +++++++ .../triptracker/NotifyBusOperatorTest.java | 25 ++-- 13 files changed, 295 insertions(+), 90 deletions(-) create mode 100644 configurations/default/bus-notifier-actions.yml create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/AgencyAction.java create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java rename src/main/java/org/opentripplanner/middleware/triptracker/{BusOpNotificationMessage.java => interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java} (57%) rename src/main/java/org/opentripplanner/middleware/triptracker/{NotifyBusOperator.java => interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java} (66%) diff --git a/configurations/default/bus-notifier-actions.yml b/configurations/default/bus-notifier-actions.yml new file mode 100644 index 000000000..ed9772e5f --- /dev/null +++ b/configurations/default/bus-notifier-actions.yml @@ -0,0 +1,2 @@ +- agencyId: GCT + trigger: org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator \ No newline at end of file diff --git a/configurations/default/env.yml.tmp b/configurations/default/env.yml.tmp index 0997a509e..f9eae14f3 100644 --- a/configurations/default/env.yml.tmp +++ b/configurations/default/env.yml.tmp @@ -91,6 +91,6 @@ TRIP_INSTRUCTION_IMMEDIATE_RADIUS: 2 # The radius in meters under which an upcoming instruction is given. TRIP_INSTRUCTION_UPCOMING_RADIUS: 10 -BUS_OPERATOR_NOTIFIER_API_URL: https://bus.notifier.example.com -BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY: your-subscription-key -BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES: agency_id:route_id \ No newline at end of file +US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL: https://bus.notifier.example.com +US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY: your-key +US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES: agency_id:route_id \ 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 2b61aa5a1..d0833f7ff 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -1,14 +1,16 @@ package org.opentripplanner.middleware.triptracker; import org.eclipse.jetty.http.HttpStatus; -import org.opentripplanner.middleware.auth.Auth0Connection; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator; import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse; import org.opentripplanner.middleware.triptracker.response.TrackingResponse; import spark.Request; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; +import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; public class ManageTripTracking { @@ -54,8 +56,7 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa TravelerPosition travelerPosition = new TravelerPosition( trackedJourney, tripData.trip.journeyState.matchingItinerary, - Auth0Connection.getUserFromRequest(request).otpUser - + Persistence.otpUsers.getById(tripData.trip.userId) ); TripStatus tripStatus = TripStatus.getTripStatus(travelerPosition); trackedJourney.lastLocation().tripStatus = tripStatus; @@ -111,7 +112,7 @@ public static TrackingResponse startOrUpdateTracking(Request request) { public static EndTrackingResponse endTracking(Request request) { TripTrackingData tripData = TripTrackingData.fromRequestJourneyId(request); if (tripData != null) { - return completeJourney(request, tripData, false); + return completeJourney(tripData, false); } return null; } @@ -125,7 +126,7 @@ public static EndTrackingResponse forciblyEndTracking(Request request) { TripTrackingData tripData = TripTrackingData.fromRequestTripId(request); if (tripData != null) { if (tripData.journey != null) { - return completeJourney(request, tripData, true); + return completeJourney(tripData, true); } else { logMessageAndHalt(request, HttpStatus.BAD_REQUEST_400, "Journey for provided trip id does not exist!"); return null; @@ -138,13 +139,15 @@ public static EndTrackingResponse forciblyEndTracking(Request request) { * Complete a journey by defining the ending type, time and condition. Also cancel possible upcoming bus * notification. */ - private static EndTrackingResponse completeJourney(Request request, TripTrackingData tripData, boolean isForciblyEnded) { + private static EndTrackingResponse completeJourney(TripTrackingData tripData, boolean isForciblyEnded) { TravelerPosition travelerPosition = new TravelerPosition( tripData.journey, tripData.trip.journeyState.matchingItinerary, - Auth0Connection.getUserFromRequest(request).otpUser + Persistence.otpUsers.getById(tripData.trip.userId) ); - NotifyBusOperator.cancelNotification(travelerPosition); + BusOperatorActions + .getDefault() + .handleCancelNotificationAction(travelerPosition); TrackedJourney trackedJourney = travelerPosition.trackedJourney; trackedJourney.end(isForciblyEnded); Persistence.trackedJourneys.updateField(trackedJourney.id, TrackedJourney.END_TIME_FIELD_NAME, trackedJourney.endTime); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 02a7a0f00..a798de024 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -3,6 +3,7 @@ import io.leonard.PolylineUtils; import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.otp.response.Step; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; import org.opentripplanner.middleware.utils.Coordinates; import javax.annotation.Nullable; @@ -14,6 +15,8 @@ import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS; import static org.opentripplanner.middleware.utils.GeometryUtils.getDistance; import static org.opentripplanner.middleware.utils.GeometryUtils.isPointBetween; +import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; /** * Locate the traveler in relation to the nearest step or destination and provide the appropriate instructions. @@ -109,9 +112,11 @@ public static TripInstruction alignTravelerToTrip( } if (isApproachingEndOfLeg(travelerPosition)) { - if (NotifyBusOperator.isBusLeg(travelerPosition.nextLeg)) { - // The preceding leg is a bus leg. - NotifyBusOperator.sendNotification(tripStatus, travelerPosition); + if (isBusLeg(travelerPosition.nextLeg)) { + // The upcoming leg is a bus leg. + BusOperatorActions + .getDefault() + .handleSendNotificationAction(tripStatus, travelerPosition); // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. return new TripInstruction(travelerPosition.nextLeg, travelerPosition.currentTime); } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 4962eaa18..a4e21585f 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -53,8 +53,8 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU /** Used for unit testing. */ public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { - this.currentPosition = currentPosition; this.expectedLeg = expectedLeg; + this.currentPosition = currentPosition; legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java index bf03bb9a8..bdb52b75a 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java @@ -7,8 +7,10 @@ import java.time.Duration; import java.time.Instant; -import static org.opentripplanner.middleware.triptracker.BusOpNotificationMessage.BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT; +import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettBusOpNotificationMessage.BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; public class TripInstruction { @@ -51,7 +53,7 @@ public enum TripInstructionType { ON_TRACK, DEVIATED, WAIT_FOR_BUS } /** The time provided by the traveler */ public Instant currentTime; - /** The type of instruction to be provided to te traveler. */ + /** The type of instruction to be provided to the traveler. */ private final TripInstructionType tripInstructionType; public TripInstruction(boolean isDestination, double distance) { @@ -156,7 +158,7 @@ private String buildOnTrackInstruction() { * Build wait for bus instruction. */ private String buildWaitForBusInstruction() { - String routeId = busLeg.route.id.split(":")[1]; + String routeId = removeAgencyPrefix(getRouteIdFromLeg(busLeg)); if (busLeg.departureDelay > 0) { long waitInMinutes = Duration .between(currentTime.atZone(DateTimeUtils.getOtpZoneId()), busLeg.getScheduledStartTime()) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/AgencyAction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/AgencyAction.java new file mode 100644 index 000000000..da9370e8b --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/AgencyAction.java @@ -0,0 +1,19 @@ +package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; + +/** Associates an agency to the correct bus notification handler. */ +public class AgencyAction { + + /** Agency id. */ + public String agencyId; + + /** The fully-qualified Java class to execute. */ + public String trigger; + + public AgencyAction() { + } + + public AgencyAction(String agencyId, String trigger) { + this.agencyId = agencyId; + this.trigger = trigger; + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java new file mode 100644 index 000000000..11acf5703 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java @@ -0,0 +1,107 @@ +package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; + +import com.fasterxml.jackson.databind.JsonNode; +import org.opentripplanner.middleware.triptracker.TravelerPosition; +import org.opentripplanner.middleware.triptracker.TripStatus; +import org.opentripplanner.middleware.utils.JsonUtils; +import org.opentripplanner.middleware.utils.YamlUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +import static org.opentripplanner.middleware.utils.ItineraryUtils.getAgencyIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; + +/** Holds configured bus notification actions. */ +public class BusOperatorActions { + private static final Logger LOG = LoggerFactory.getLogger(BusOperatorActions.class); + + public static final String BUS_NOTIFIER_ACTIONS_YML = "configurations/default/bus-notifier-actions.yml"; + + private static BusOperatorActions defaultInstance; + + private final List agencyActions; + + public static BusOperatorActions getDefault() { + if (defaultInstance == null) { + try (InputStream stream = new FileInputStream(BUS_NOTIFIER_ACTIONS_YML)) { + JsonNode busNotifierActionsYml = YamlUtils.yamlMapper.readTree(stream); + defaultInstance = new BusOperatorActions(JsonUtils.getPOJOFromJSONAsList(busNotifierActionsYml, AgencyAction.class)); + } catch (IOException e) { + LOG.error("Error parsing trip-actions.yml", e); + throw new RuntimeException(e); + } + } + return defaultInstance; + } + + public BusOperatorActions(List agencyActions) { + this.agencyActions = agencyActions; + } + + /** + * Get the action that matches the given agency id. + */ + public AgencyAction getAgencyAction(TravelerPosition travelerPosition) { + String agencyId = removeAgencyPrefix(getAgencyIdFromLeg(travelerPosition.nextLeg)); + if (agencyId != null) { + for (AgencyAction agencyAction : agencyActions) { + if (agencyAction.agencyId.equalsIgnoreCase(agencyId)) { + return agencyAction; + } + } + } + return null; + } + + /** + * Get the correct action for agency and send notification. + */ + public void handleSendNotificationAction(TripStatus tripStatus, TravelerPosition travelerPosition) { + AgencyAction action = getAgencyAction(travelerPosition); + if (action != null) { + BusOperatorInteraction interaction = getBusOperatorInteraction(action); + try { + interaction.sendNotification(tripStatus, travelerPosition); + } catch (Exception e) { + LOG.error("Could not trigger class {} for agency {}", action.trigger, action.agencyId, e); + throw new RuntimeException(e); + } + } + } + + /** + * Get the correct action for agency and cancel notification. + */ + public void handleCancelNotificationAction(TravelerPosition travelerPosition) { + AgencyAction action = getAgencyAction(travelerPosition); + if (action != null) { + BusOperatorInteraction interaction = getBusOperatorInteraction(action); + try { + interaction.cancelNotification(travelerPosition); + } catch (Exception e) { + LOG.error("Could not trigger class {} for agency {}", action.trigger, action.agencyId, e); + throw new RuntimeException(e); + } + } + } + + /** + * Get the bus operator class for the correct agency. + */ + private BusOperatorInteraction getBusOperatorInteraction(AgencyAction action) { + BusOperatorInteraction interaction; + try { + Class interactionClass = Class.forName(action.trigger); + interaction = (BusOperatorInteraction) interactionClass.getDeclaredConstructor().newInstance(); + } catch (Exception e) { + LOG.error("Error instantiating class {}", action.trigger, e); + throw new RuntimeException(e); + } + return interaction; + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java new file mode 100644 index 000000000..b73d40495 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorInteraction.java @@ -0,0 +1,11 @@ +package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; + +import org.opentripplanner.middleware.triptracker.TravelerPosition; +import org.opentripplanner.middleware.triptracker.TripStatus; + +public interface BusOperatorInteraction { + + void sendNotification(TripStatus tripStatus, TravelerPosition travelerPosition); + + void cancelNotification(TravelerPosition travelerPosition); +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java similarity index 57% rename from src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java rename to src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java index c054a0ef7..f6ce43190 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/BusOpNotificationMessage.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java @@ -1,32 +1,40 @@ -package org.opentripplanner.middleware.triptracker; +package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; + +import org.opentripplanner.middleware.triptracker.TravelerPosition; import java.time.Instant; -import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import static org.opentripplanner.middleware.utils.DateTimeUtils.getOtpZoneId; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getAgencyIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getStopIdFromPlace; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getTripIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; + /** * Class containing the expected notify parameters. These will be converted to JSON and make up the body content of * the request. *

* 'To' fields omitted as they are not needed for requests for single transit legs. */ -public class BusOpNotificationMessage { +public class UsRideGwinnettBusOpNotificationMessage { - public BusOpNotificationMessage() { + public UsRideGwinnettBusOpNotificationMessage() { // Required for JSON deserialization. } private static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(ZoneId.systemDefault()); + .withZone(getOtpZoneId()); public static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT = DateTimeFormatter .ofPattern("HH:mm:ss") - .withZone(ZoneId.systemDefault()); + .withZone(getOtpZoneId()); private static final Map MOBILITY_CODES_LOOKUP = createMobilityCodesLookup(); @@ -36,21 +44,21 @@ public BusOpNotificationMessage() { private static Map createMobilityCodesLookup() { HashMap codes = new HashMap<>(); codes.put("Device", 1); - codes.put("Mscooter", 2); - codes.put("WchairE", 3); - codes.put("WchairM", 4); + codes.put("MScooter", 2); + codes.put("WChairE", 3); + codes.put("WChairM", 4); codes.put("Some", 5); codes.put("LowVision", 6); codes.put("Blind", 7); codes.put("Device-LowVision", 8); - codes.put("Mscooter-LowVision", 9); + codes.put("MScooter-LowVision", 9); codes.put("WChairE-LowVision", 10); codes.put("WChairM-LowVision", 11); codes.put("Some-LowVision", 12); codes.put("Device-Blind", 13); - codes.put("Mscooter-Blind", 14); - codes.put("WchairE-Blind", 15); - codes.put("WchairM-Blind", 16); + codes.put("MScooter-Blind", 14); + codes.put("WChairE-Blind", 15); + codes.put("WChairM-Blind", 16); codes.put("Some-Blind", 17); return codes; } @@ -65,14 +73,15 @@ private static Map createMobilityCodesLookup() { public List mobility_codes; public boolean trusted_companion; - public BusOpNotificationMessage(Instant timestamp, TravelerPosition travelerPosition) { + public UsRideGwinnettBusOpNotificationMessage(Instant timestamp, TravelerPosition travelerPosition) { + var nextLeg = travelerPosition.nextLeg; this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(timestamp); - this.agency_id = removeAgencyPrefix(travelerPosition.nextLeg.agency.id); - this.from_route_id = removeAgencyPrefix(travelerPosition.nextLeg.route.id); - this.from_trip_id = travelerPosition.nextLeg.trip.id; - this.from_stop_id = travelerPosition.nextLeg.from.stop.id; + this.agency_id = removeAgencyPrefix(getAgencyIdFromLeg(nextLeg)); + this.from_route_id = removeAgencyPrefix(getRouteIdFromLeg(nextLeg)); + this.from_trip_id = getTripIdFromLeg(nextLeg); + this.from_stop_id = getStopIdFromPlace(nextLeg.from); this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format( - travelerPosition.nextLeg.getScheduledStartTime().toInstant() + nextLeg.getScheduledStartTime().toInstant() ); // 1 = Notify, 0 = Cancel. this.msg_type = 1; @@ -81,16 +90,8 @@ public BusOpNotificationMessage(Instant timestamp, TravelerPosition travelerPosi } /** - * Get the second element from value by removing the OTP agency prefix. - * E.g. GwinnettCountyTransit:GCT will return just GCT. - */ - private String removeAgencyPrefix(String value) { - return (value != null) ? value.split(":")[1] : null; - } - - /** - * Get the mobility code that matches the mobility mode. Although the API can accept multiple codes, OTP middleware - * currently only provides one. + * Get the mobility code that matches the mobility mode. The API can accept multiple codes (probably to cover + * multiple travelers at the same stop), but the OTP middleware currently only provides one. */ private static List getMobilityCode(String mobilityMode) { List mobilityCodes = new ArrayList<>(); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java similarity index 66% rename from src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java rename to src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java index daa9f0d2d..bbf4f70b0 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/NotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java @@ -1,9 +1,11 @@ -package org.opentripplanner.middleware.triptracker; +package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.otp.response.Leg; +import org.opentripplanner.middleware.triptracker.TravelerPosition; +import org.opentripplanner.middleware.triptracker.TripStatus; import org.opentripplanner.middleware.utils.HttpUtils; import org.opentripplanner.middleware.utils.JsonUtils; import org.slf4j.Logger; @@ -19,25 +21,27 @@ import static org.opentripplanner.middleware.triptracker.TripStatus.getSegmentStartTime; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteIdFromLeg; +import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; /** * If conditions are correct notify a bus operator of a traveler joining the service at a given stop. */ -public class NotifyBusOperator { +public class UsRideGwinnettNotifyBusOperator implements BusOperatorInteraction { - public NotifyBusOperator() {} + public UsRideGwinnettNotifyBusOperator() {} public static boolean IS_TEST = false; - private static final Logger LOG = LoggerFactory.getLogger(NotifyBusOperator.class); + private static final Logger LOG = LoggerFactory.getLogger(UsRideGwinnettNotifyBusOperator.class); - private static final String BUS_OPERATOR_NOTIFIER_API_URL - = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_URL", "not-provided"); + private static final String US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL + = getConfigPropertyAsText("US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL", "not-provided"); - private static final String BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY - = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY", "not-provided"); + private static final String US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY + = getConfigPropertyAsText("US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY", "not-provided"); - public static List QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); + public static List US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); public static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; @@ -45,7 +49,7 @@ public NotifyBusOperator() {} * Headers that are required for each request. */ private static final Map BUS_OPERATOR_NOTIFIER_API_HEADERS = Map.of( - "Ocp-Apim-Subscription-Key", BUS_OPERATOR_NOTIFIER_API_SUBSCRIPTION_KEY, + "Ocp-Apim-Subscription-Key", US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY, "Content-Type", "application/json" ); @@ -58,7 +62,7 @@ public NotifyBusOperator() {} */ private static List getBusOperatorNotifierQualifyingRoutes() { String busOperatorNotifierQualifyingRoutes - = getConfigPropertyAsText("BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES"); + = getConfigPropertyAsText("US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES"); if (busOperatorNotifierQualifyingRoutes != null) { return Arrays.asList(busOperatorNotifierQualifyingRoutes.split(",")); } @@ -68,18 +72,22 @@ private static List getBusOperatorNotifierQualifyingRoutes() { /** * Stage notification to bus operator by making sure all required conditions are met. */ - public static void sendNotification(TripStatus tripStatus, TravelerPosition travelerPosition) { + public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPosition) { + var routeId = getRouteIdFromLeg(travelerPosition.nextLeg); try { if ( isBusLeg(travelerPosition.nextLeg) && isWithinOperationalNotifyWindow(tripStatus, travelerPosition) && - hasNotPreviouslyNotifiedBusDriverForRoute(travelerPosition.trackedJourney, travelerPosition.nextLeg.route.id) && - supportsBusOperatorNotification(travelerPosition.nextLeg.route.id) + hasNotPreviouslyNotifiedBusDriverForRoute(travelerPosition.trackedJourney, routeId) && + supportsBusOperatorNotification(routeId) ) { + // Immediately set the notification state to pending, so that subsequent calls don't initiate another + // request before this one completes. + travelerPosition.trackedJourney.updateNotificationMessage(routeId, "pending"); var body = createPostBody(travelerPosition); var httpStatus = doPost(body); if (httpStatus == HttpStatus.OK_200) { - travelerPosition.trackedJourney.updateNotificationMessage(travelerPosition.nextLeg.route.id, body); + travelerPosition.trackedJourney.updateNotificationMessage(routeId, body); } else { LOG.error("Error {} while trying to initiate notification to bus operator.", httpStatus); } @@ -92,19 +100,19 @@ public static void sendNotification(TripStatus tripStatus, TravelerPosition trav /** * Cancel a previously sent notification for the next bus leg. */ - public static void cancelNotification(TravelerPosition travelerPosition) { + public void cancelNotification(TravelerPosition travelerPosition) { + var routeId = getRouteIdFromLeg(travelerPosition.nextLeg); try { - if (isBusLeg(travelerPosition.nextLeg) && travelerPosition.nextLeg.route.id != null) { - String routeId = travelerPosition.nextLeg.route.id; + if (isBusLeg(travelerPosition.nextLeg) && routeId != null) { Map busNotificationRequests = travelerPosition.trackedJourney.busNotificationMessages; if (busNotificationRequests.containsKey(routeId)) { - BusOpNotificationMessage busOpNotificationMessage = JsonUtils.getPOJOFromJSON( + UsRideGwinnettBusOpNotificationMessage usRideGwinnettBusOpNotificationMessage = JsonUtils.getPOJOFromJSON( busNotificationRequests.get(routeId), - BusOpNotificationMessage.class + UsRideGwinnettBusOpNotificationMessage.class ); // Changed the saved message type from notify to cancel. - busOpNotificationMessage.msg_type = 0; - var httpStatus = doPost(JsonUtils.toJson(busOpNotificationMessage)); + usRideGwinnettBusOpNotificationMessage.msg_type = 0; + var httpStatus = doPost(JsonUtils.toJson(usRideGwinnettBusOpNotificationMessage)); if (httpStatus == HttpStatus.OK_200) { travelerPosition.trackedJourney.removeNotificationMessage(routeId); } else { @@ -126,7 +134,7 @@ public static int doPost(String body) { return 200; } var httpResponse = HttpUtils.httpRequestRawResponse( - URI.create(BUS_OPERATOR_NOTIFIER_API_URL), + URI.create(US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL), 1000, HttpMethod.POST, BUS_OPERATOR_NOTIFIER_API_HEADERS, @@ -139,26 +147,23 @@ public static int doPost(String body) { * Create post body that will be sent to bus notification API. */ public static String createPostBody(TravelerPosition travelerPosition) { - return JsonUtils.toJson(new BusOpNotificationMessage(travelerPosition.currentTime, travelerPosition)); - } - - /** - * Make sure the leg in question is a bus transit leg. - */ - public static boolean isBusLeg(Leg leg) { - return leg != null && leg.mode.equalsIgnoreCase("BUS") && leg.transitLeg; + return JsonUtils.toJson(new UsRideGwinnettBusOpNotificationMessage(travelerPosition.currentTime, travelerPosition)); } /** * Make sure the bus route associated with this leg supports notifying the bus operator. The 'gtfsId' is expected in - * the format agency_id:route_id e.g. GwinnettCountyTransit:360. + * the format agency_id:route_id e.g. GwinnettCountyTransit:360. If no routes are defined it is assumed that all + * routes support notification. */ public static boolean supportsBusOperatorNotification(String gtfsId) { - return QUALIFYING_BUS_NOTIFIER_ROUTES.contains(gtfsId); + return + US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES.isEmpty() || + US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES.contains(gtfsId); } /** - * Has the bus driver already been notified for this journey. The driver must only be notified once. + * Has the bus driver already been notified or in the process of being notified for this journey. + * The driver must only be notified once. */ public static boolean hasNotPreviouslyNotifiedBusDriverForRoute(TrackedJourney trackedJourney, String routeId) { for (String notifiedRouteId : trackedJourney.busNotificationMessages.keySet()) { diff --git a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java index 7250f795d..8b790b608 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java @@ -318,4 +318,47 @@ private static boolean timeOfDayMatches(ZonedDateTime zonedDateTimeA, ZonedDateT zonedDateTimeA.getMinute() == zonedDateTimeB.getMinute() && zonedDateTimeA.getSecond() == zonedDateTimeB.getSecond(); } + + /** + * Make sure the leg in question is a bus transit leg. + */ + public static boolean isBusLeg(Leg leg) { + return leg != null && leg.mode.equalsIgnoreCase("BUS") && leg.transitLeg; + } + + /** + * Get the second element from the OTP agency id by removing the OTP agency prefix. + * E.g. GwinnettCountyTransit:GCT will return just GCT. + */ + public static String removeAgencyPrefix(String otpAgencyId) { + return (otpAgencyId != null) ? otpAgencyId.split(":")[1] : null; + } + + /** + * Get the route id from leg. + */ + public static String getRouteIdFromLeg(Leg leg) { + return (leg != null) ? leg.route.id : null; + } + + /** + * Get the agency id from leg. + */ + public static String getAgencyIdFromLeg(Leg leg) { + return (leg != null) ? leg.agency.id : null; + } + + /** + * Get the trip id from leg. + */ + public static String getTripIdFromLeg(Leg leg) { + return (leg != null) ? leg.trip.id : null; + } + + /** + * Get the stop id from place. + */ + public static String getStopIdFromPlace(Place place) { + return (place != null) ? place.stop.id : null; + } } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index dcb18bca3..c8a2c7290 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -14,6 +14,9 @@ import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.testutils.CommonTestUtils; import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.AgencyAction; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator; import org.opentripplanner.middleware.utils.Coordinates; import org.opentripplanner.middleware.utils.JsonUtils; @@ -26,7 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.opentripplanner.middleware.triptracker.NotifyBusOperator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; +import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { @@ -36,14 +39,18 @@ class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { private static final String routeId = "GwinnettCountyTransit:40"; + private final BusOperatorActions busOperatorActions = new BusOperatorActions(List.of( + new AgencyAction("GCT", UsRideGwinnettNotifyBusOperator.class.getName()) + )); + @BeforeAll public static void setUp() throws IOException { walkToBusTransition = JsonUtils.getPOJOFromJSON( CommonTestUtils.getTestResourceAsString("controllers/api/walk-to-bus-transition.json"), Itinerary.class ); - NotifyBusOperator.IS_TEST = true; - NotifyBusOperator.QUALIFYING_BUS_NOTIFIER_ROUTES = List.of(routeId); + UsRideGwinnettNotifyBusOperator.IS_TEST = true; + UsRideGwinnettNotifyBusOperator.US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES = List.of(routeId); } @AfterEach @@ -88,10 +95,10 @@ void canNotifyBusOperatorForDelayedDeparture() throws CloneNotSupportedException void canCancelBusOperatorNotification() { trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates()); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - NotifyBusOperator.sendNotification(TripStatus.ON_SCHEDULE, travelerPosition); + busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); - NotifyBusOperator.cancelNotification(travelerPosition); + busOperatorActions.handleCancelNotificationAction(travelerPosition); updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertFalse(updated.busNotificationMessages.containsKey(routeId)); } @@ -100,10 +107,10 @@ void canCancelBusOperatorNotification() { void canNotifyBusOperatorOnlyOnce() { trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates()); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - NotifyBusOperator.sendNotification(TripStatus.ON_SCHEDULE, travelerPosition); + busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); - assertFalse(NotifyBusOperator.hasNotPreviouslyNotifiedBusDriverForRoute(trackedJourney, routeId)); + assertFalse(UsRideGwinnettNotifyBusOperator.hasNotPreviouslyNotifiedBusDriverForRoute(trackedJourney, routeId)); } @ParameterizedTest @@ -114,7 +121,7 @@ void isWithinOperationalNotifyWindow( TravelerPosition travelerPosition, String message ) { - assertEquals(expected, NotifyBusOperator.isWithinOperationalNotifyWindow(tripStatus, travelerPosition), message); + assertEquals(expected, UsRideGwinnettNotifyBusOperator.isWithinOperationalNotifyWindow(tripStatus, travelerPosition), message); } private static Stream createWithinOperationalNotifyWindowTrace() { @@ -142,7 +149,7 @@ private static Stream createWithinOperationalNotifyWindowTrace() { private static OtpUser createOtpUser() { MobilityProfile mobilityProfile = new MobilityProfile(); - mobilityProfile.mobilityMode = "WchairE"; + mobilityProfile.mobilityMode = "WChairE"; OtpUser otpUser = new OtpUser(); otpUser.mobilityProfile = mobilityProfile; return otpUser; From a3505049c3635ac814967be6b2bfb4af22e41db4 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 30 May 2024 17:28:29 +0100 Subject: [PATCH 05/16] refactor(Further changes to address PR feedback): --- .../middleware/models/TrackedJourney.java | 10 - .../middleware/otp/response/Agency.java | 10 - .../middleware/otp/response/Leg.java | 5 - .../middleware/otp/response/Place.java | 2 - .../middleware/otp/response/Route.java | 13 - .../middleware/otp/response/Trip.java | 10 - .../tripmonitor/jobs/CheckMonitoredTrip.java | 4 +- .../triptracker/TravelerLocator.java | 14 +- .../triptracker/TravelerPosition.java | 12 +- .../triptracker/TripInstruction.java | 60 +++-- .../middleware/triptracker/TripStatus.java | 16 +- ...sRideGwinnettBusOpNotificationMessage.java | 6 +- .../UsRideGwinnettNotifyBusOperator.java | 52 ++-- .../middleware/utils/I18nUtils.java | 9 + .../middleware/utils/ItineraryUtils.java | 15 +- .../latest-spark-swagger-output.yaml | 234 +++++++++--------- .../models/MobilityProfileTest.java | 1 - .../triptracker/ManageLegTraversalTest.java | 29 ++- .../triptracker/NotifyBusOperatorTest.java | 19 +- .../api/walk-to-bus-transition.json | 5 + 20 files changed, 271 insertions(+), 255 deletions(-) delete mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Agency.java delete mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Route.java delete mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Trip.java diff --git a/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java b/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java index 80a66113b..a6b4ed5eb 100644 --- a/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java +++ b/src/main/java/org/opentripplanner/middleware/models/TrackedJourney.java @@ -91,14 +91,4 @@ public void updateNotificationMessage(String routeId, String body) { busNotificationMessages ); } - - public void removeNotificationMessage(String routeId) { - busNotificationMessages.remove(routeId); - Persistence.trackedJourneys.updateField( - id, - BUS_NOTIFICATION_MESSAGES_FIELD_NAME, - busNotificationMessages - ); - } - } diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Agency.java b/src/main/java/org/opentripplanner/middleware/otp/response/Agency.java deleted file mode 100644 index 16e23b10c..000000000 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Agency.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.opentripplanner.middleware.otp.response; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Agency { - public String id; -} diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java b/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java index c371ebf39..b60071a0d 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Leg.java @@ -27,7 +27,6 @@ public class Leg implements Cloneable { public Double distance; public Boolean pathway; public String mode; - public Route route; public Boolean interlineWithPreviousLeg; public Place from; public Place to; @@ -45,12 +44,8 @@ public class Leg implements Cloneable { public Integer routeType; public String routeId; public String agencyId; - // TODO: Look to remove once OTP2 work has been merged. - public Agency agency; public String tripBlockId; public String tripId; - // TODO: Look to remove once OTP2 work has been merged. - public Trip trip; public String serviceDate; public List interStopGeometry = null; public String routeShortName; diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Place.java b/src/main/java/org/opentripplanner/middleware/otp/response/Place.java index b5bce8f99..d6b89e314 100644 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Place.java +++ b/src/main/java/org/opentripplanner/middleware/otp/response/Place.java @@ -21,8 +21,6 @@ public class Place implements Cloneable { public String orig; public String vertexType; public String stopId; - // TODO: Look to remove once OTP2 work has been merged. - public Stop stop; public Date arrival; public Integer stopIndex; public Integer stopSequence; diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Route.java b/src/main/java/org/opentripplanner/middleware/otp/response/Route.java deleted file mode 100644 index 978a48f09..000000000 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Route.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.opentripplanner.middleware.otp.response; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Route { - public String id; - public String gtfsId; - public String longName; - public String shortName; -} diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Trip.java b/src/main/java/org/opentripplanner/middleware/otp/response/Trip.java deleted file mode 100644 index 8f1a08764..000000000 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Trip.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.opentripplanner.middleware.otp.response; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Trip { - public String id; -} 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 9ba7f3f60..5318bd66e 100644 --- a/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/tripmonitor/jobs/CheckMonitoredTrip.java @@ -20,6 +20,7 @@ import org.opentripplanner.middleware.triptracker.TripTrackingData; import org.opentripplanner.middleware.utils.ConfigUtils; import org.opentripplanner.middleware.utils.DateTimeUtils; +import org.opentripplanner.middleware.utils.I18nUtils; import org.opentripplanner.middleware.utils.ItineraryUtils; import org.opentripplanner.middleware.utils.NotificationUtils; import org.slf4j.Logger; @@ -894,8 +895,7 @@ private OtpUser getOtpUser() { * Retrieves and caches the user on first call (assuming the user for a trip does not change). */ private Locale getOtpUserLocale() { - OtpUser user = getOtpUser(); - return Locale.forLanguageTag(user == null || user.preferredLocale == null ? "en-US" : user.preferredLocale); + return I18nUtils.getOtpUserLocale(getOtpUser()); } private String getTripUrl() { diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index a798de024..9bdf6dadd 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -9,6 +9,7 @@ import javax.annotation.Nullable; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.stream.Collectors; import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; @@ -85,7 +86,7 @@ private static TripInstruction getBackOnTrack( } Step nearestStep = snapToStep(travelerPosition); return (nearestStep != null) - ? new TripInstruction(nearestStep.streetName) + ? new TripInstruction(nearestStep.streetName, travelerPosition.locale) : null; } @@ -98,7 +99,7 @@ public static TripInstruction alignTravelerToTrip( boolean isStartOfTrip, TripStatus tripStatus ) { - + Locale locale = travelerPosition.locale; if (isStartOfTrip) { // If the traveler has just started the trip and is within a set distance of the first step. Step firstStep = travelerPosition.expectedLeg.steps.get(0); @@ -107,7 +108,7 @@ public static TripInstruction alignTravelerToTrip( } double distance = getDistance(travelerPosition.currentPosition, new Coordinates(firstStep)); return (distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) - ? new TripInstruction(distance, firstStep) + ? new TripInstruction(distance, firstStep, locale) : null; } @@ -118,16 +119,17 @@ public static TripInstruction alignTravelerToTrip( .getDefault() .handleSendNotificationAction(tripStatus, travelerPosition); // Regardless of whether the notification is sent or qualifies, provide a 'wait for bus' instruction. - return new TripInstruction(travelerPosition.nextLeg, travelerPosition.currentTime); + return new TripInstruction(travelerPosition.nextLeg, travelerPosition.currentTime, locale); } - return new TripInstruction(getDistanceToEndOfLeg(travelerPosition), travelerPosition.expectedLeg.to.name); + return new TripInstruction(getDistanceToEndOfLeg(travelerPosition), travelerPosition.expectedLeg.to.name, locale); } Step nextStep = snapToStep(travelerPosition); if (nextStep != null && !isPositionPastStep(travelerPosition, nextStep)) { return new TripInstruction( getDistance(travelerPosition.currentPosition, new Coordinates(nextStep)), - nextStep + nextStep, + locale ); } return null; diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index a4e21585f..97feb8b49 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -6,8 +6,10 @@ import org.opentripplanner.middleware.otp.response.Itinerary; import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.I18nUtils; import java.time.Instant; +import java.util.Locale; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getExpectedLeg; import static org.opentripplanner.middleware.triptracker.ManageLegTraversal.getNextLeg; @@ -36,6 +38,9 @@ public class TravelerPosition { /** Traveler mobility information which is passed on to bus operators. */ public String mobilityMode; + /** The traveler's locale. */ + public Locale locale; + public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpUser otpUser) { TrackingLocation lastLocation = trackedJourney.locations.get(trackedJourney.locations.size() - 1); currentTime = lastLocation.timestamp.toInstant(); @@ -46,8 +51,11 @@ public TravelerPosition(TrackedJourney trackedJourney, Itinerary itinerary, OtpU } legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); this.trackedJourney = trackedJourney; - if (otpUser != null && otpUser.mobilityProfile != null) { - mobilityMode = otpUser.mobilityProfile.mobilityMode; + if (otpUser != null) { + if (otpUser.mobilityProfile != null) { + mobilityMode = otpUser.mobilityProfile.mobilityMode; + } + this.locale = I18nUtils.getOtpUserLocale(otpUser); } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java index bdb52b75a..001cbed66 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java @@ -4,13 +4,13 @@ import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.utils.DateTimeUtils; +import java.util.Date; import java.time.Duration; import java.time.Instant; +import java.util.Locale; -import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettBusOpNotificationMessage.BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; -import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteIdFromLeg; -import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; +import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteShortNameFromLeg; public class TripInstruction { @@ -56,9 +56,13 @@ public enum TripInstructionType { ON_TRACK, DEVIATED, WAIT_FOR_BUS } /** The type of instruction to be provided to the traveler. */ private final TripInstructionType tripInstructionType; - public TripInstruction(boolean isDestination, double distance) { + /** The traveler's locale. */ + private final Locale locale; + + public TripInstruction(boolean isDestination, double distance, Locale locale) { this.distance = distance; this.tripInstructionType = TripInstructionType.ON_TRACK; + this.locale = locale; setPrefix(isDestination); } @@ -72,34 +76,36 @@ public boolean hasInstruction() { /** * On track instruction to step. */ - public TripInstruction(double distance, Step legStep) { - this(false, distance); + public TripInstruction(double distance, Step legStep, Locale locale) { + this(false, distance, locale); this.legStep = legStep; } /** * On track instruction to destination. */ - public TripInstruction(double distance, String locationName) { - this(true, distance); + public TripInstruction(double distance, String locationName, Locale locale) { + this(true, distance, locale); this.locationName = locationName; } /** * Deviated instruction. */ - public TripInstruction(String locationName) { + public TripInstruction(String locationName, Locale locale) { this.tripInstructionType = TripInstructionType.DEVIATED; this.locationName = locationName; + this.locale = locale; } /** * Provide bus related trip instruction. */ - public TripInstruction(Leg busLeg, Instant currentTime) { + public TripInstruction(Leg busLeg, Instant currentTime, Locale locale) { this.tripInstructionType = TripInstructionType.WAIT_FOR_BUS; this.busLeg = busLeg; this.currentTime = currentTime; + this.locale = locale; } /** @@ -158,19 +164,23 @@ private String buildOnTrackInstruction() { * Build wait for bus instruction. */ private String buildWaitForBusInstruction() { - String routeId = removeAgencyPrefix(getRouteIdFromLeg(busLeg)); - if (busLeg.departureDelay > 0) { - long waitInMinutes = Duration - .between(currentTime.atZone(DateTimeUtils.getOtpZoneId()), busLeg.getScheduledStartTime()) - .toMinutes(); - return String.format("Wait%s for bus %s", getReadableMinutes(waitInMinutes), routeId); - } else { - return String.format( - "Wait for bus %s scheduled to arrive at %s", - routeId, - BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format(busLeg.getScheduledStartTime()) - ); - } + String routeShortName = getRouteShortNameFromLeg(busLeg); + long delayInMinutes = busLeg.departureDelay; + long absoluteMinutes = Math.abs(delayInMinutes); + long waitInMinutes = Duration + .between(currentTime.atZone(DateTimeUtils.getOtpZoneId()), busLeg.getScheduledStartTime()) + .toMinutes(); + String delayInfo = (delayInMinutes > 0) ? "late" : "early"; + String arrivalInfo = (absoluteMinutes <= 1) + ? ", on time" + : String.format("now %s %s", getReadableMinutes(delayInMinutes), delayInfo); + return String.format( + "Wait %s for your bus, route %s, scheduled at %s %s", + getReadableMinutes(waitInMinutes), + routeShortName, + DateTimeUtils.formatShortDate(Date.from(busLeg.getScheduledStartTime().toInstant()), locale), + arrivalInfo + ); } /** @@ -178,9 +188,9 @@ private String buildWaitForBusInstruction() { */ private String getReadableMinutes(long waitInMinutes) { if (waitInMinutes == 1) { - return String.format(" %s minute", waitInMinutes); + return String.format("%s minute", waitInMinutes); } else if (waitInMinutes > 1) { - return String.format(" %s minutes", waitInMinutes); + return String.format("%s minutes", waitInMinutes); } return ""; } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripStatus.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripStatus.java index 8a619a8cd..0f4f60459 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripStatus.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripStatus.java @@ -62,11 +62,7 @@ public static TripStatus getTripStatus(TravelerPosition travelerPosition) { travelerPosition.legSegmentFromPosition != null && isWithinModeRadius(travelerPosition) ) { - Instant segmentStartTime = travelerPosition - .expectedLeg - .startTime - .toInstant() - .plusSeconds((long) getSegmentStartTime(travelerPosition.legSegmentFromPosition)); + Instant segmentStartTime = getSegmentStartTime(travelerPosition); Instant segmentEndTime = travelerPosition .expectedLeg .startTime @@ -83,10 +79,18 @@ public static TripStatus getTripStatus(TravelerPosition travelerPosition) { return TripStatus.DEVIATED; } - public static double getSegmentStartTime(LegSegment legSegmentFromPosition) { + public static double getLegSegmentStartTime(LegSegment legSegmentFromPosition) { return legSegmentFromPosition.cumulativeTime - legSegmentFromPosition.timeInSegment; } + public static Instant getSegmentStartTime(TravelerPosition travelerPosition) { + return travelerPosition + .expectedLeg + .startTime + .toInstant() + .plusSeconds((long) getLegSegmentStartTime(travelerPosition.legSegmentFromPosition)); + } + /** * Checks if the traveler's position is with an acceptable distance of the mode type. */ diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java index f6ce43190..9f1bc97c1 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java @@ -28,10 +28,12 @@ public UsRideGwinnettBusOpNotificationMessage() { // Required for JSON deserialization. } + /** This is the date format required by the API. */ private static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT = DateTimeFormatter .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") .withZone(getOtpZoneId()); + /** This is the time format required by the API. */ public static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT = DateTimeFormatter .ofPattern("HH:mm:ss") .withZone(getOtpZoneId()); @@ -78,8 +80,8 @@ public UsRideGwinnettBusOpNotificationMessage(Instant timestamp, TravelerPositio this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(timestamp); this.agency_id = removeAgencyPrefix(getAgencyIdFromLeg(nextLeg)); this.from_route_id = removeAgencyPrefix(getRouteIdFromLeg(nextLeg)); - this.from_trip_id = getTripIdFromLeg(nextLeg); - this.from_stop_id = getStopIdFromPlace(nextLeg.from); + this.from_trip_id = removeAgencyPrefix(getTripIdFromLeg(nextLeg)); + this.from_stop_id = removeAgencyPrefix(getStopIdFromPlace(nextLeg.from)); this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format( nextLeg.getScheduledStartTime().toInstant() ); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java index bbf4f70b0..246b8c059 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java @@ -1,9 +1,9 @@ package org.opentripplanner.middleware.triptracker.interactions.busnotifiers; +import com.fasterxml.jackson.core.JsonProcessingException; import org.eclipse.jetty.http.HttpMethod; import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; -import org.opentripplanner.middleware.otp.response.Leg; import org.opentripplanner.middleware.triptracker.TravelerPosition; import org.opentripplanner.middleware.triptracker.TripStatus; import org.opentripplanner.middleware.utils.HttpUtils; @@ -13,13 +13,11 @@ import java.net.URI; import java.time.Duration; -import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; -import static org.opentripplanner.middleware.triptracker.TripStatus.getSegmentStartTime; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteIdFromLeg; import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; @@ -78,7 +76,7 @@ public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPos if ( isBusLeg(travelerPosition.nextLeg) && isWithinOperationalNotifyWindow(tripStatus, travelerPosition) && - hasNotPreviouslyNotifiedBusDriverForRoute(travelerPosition.trackedJourney, routeId) && + hasNotSentNotificationForRoute(travelerPosition.trackedJourney, routeId) && supportsBusOperatorNotification(routeId) ) { // Immediately set the notification state to pending, so that subsequent calls don't initiate another @@ -103,18 +101,21 @@ public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPos public void cancelNotification(TravelerPosition travelerPosition) { var routeId = getRouteIdFromLeg(travelerPosition.nextLeg); try { - if (isBusLeg(travelerPosition.nextLeg) && routeId != null) { + if ( + isBusLeg(travelerPosition.nextLeg) && routeId != null && + hasNotCancelledNotificationForRoute(travelerPosition.trackedJourney, routeId) + ) { Map busNotificationRequests = travelerPosition.trackedJourney.busNotificationMessages; if (busNotificationRequests.containsKey(routeId)) { - UsRideGwinnettBusOpNotificationMessage usRideGwinnettBusOpNotificationMessage = JsonUtils.getPOJOFromJSON( + UsRideGwinnettBusOpNotificationMessage body = JsonUtils.getPOJOFromJSON( busNotificationRequests.get(routeId), UsRideGwinnettBusOpNotificationMessage.class ); // Changed the saved message type from notify to cancel. - usRideGwinnettBusOpNotificationMessage.msg_type = 0; - var httpStatus = doPost(JsonUtils.toJson(usRideGwinnettBusOpNotificationMessage)); + body.msg_type = 0; + var httpStatus = doPost(JsonUtils.toJson(body)); if (httpStatus == HttpStatus.OK_200) { - travelerPosition.trackedJourney.removeNotificationMessage(routeId); + travelerPosition.trackedJourney.updateNotificationMessage(routeId, JsonUtils.toJson(body)); } else { LOG.error("Error {} while trying to cancel notification to bus operator.", httpStatus); } @@ -165,13 +166,25 @@ public static boolean supportsBusOperatorNotification(String gtfsId) { * Has the bus driver already been notified or in the process of being notified for this journey. * The driver must only be notified once. */ - public static boolean hasNotPreviouslyNotifiedBusDriverForRoute(TrackedJourney trackedJourney, String routeId) { - for (String notifiedRouteId : trackedJourney.busNotificationMessages.keySet()) { - if (notifiedRouteId.equalsIgnoreCase(routeId)) { - return false; - } + public static boolean hasNotSentNotificationForRoute(TrackedJourney trackedJourney, String routeId) { + return !trackedJourney.busNotificationMessages.containsKey(routeId); + } + + /** + * Has a previous notification already been cancelled. + */ + public static boolean hasNotCancelledNotificationForRoute(TrackedJourney trackedJourney, String routeId) throws JsonProcessingException { + String messageBody = trackedJourney.busNotificationMessages.get(routeId); + if (messageBody == null) { + // It should not be possible to get here because a notification must exist before it can be cancelled. + return false; } - return true; + UsRideGwinnettBusOpNotificationMessage message = getNotificationMessage(messageBody); + return message.msg_type != 1; + } + + public static UsRideGwinnettBusOpNotificationMessage getNotificationMessage(String body) throws JsonProcessingException { + return JsonUtils.getPOJOFromJSON(body, UsRideGwinnettBusOpNotificationMessage.class); } /** @@ -191,11 +204,8 @@ public static boolean isWithinOperationalNotifyWindow(TripStatus tripStatus, Tra * Get how far ahead in minutes the traveler is from the expected schedule. */ public static long getMinutesAheadOfSchedule(TravelerPosition travelerPosition) { - Instant segmentStartTime = travelerPosition - .expectedLeg - .startTime - .toInstant() - .plusSeconds((long) getSegmentStartTime(travelerPosition.legSegmentFromPosition)); - return Duration.between(segmentStartTime, travelerPosition.currentTime).toMinutes(); + return Duration + .between(TripStatus.getSegmentStartTime(travelerPosition), travelerPosition.currentTime) + .toMinutes(); } } \ No newline at end of file diff --git a/src/main/java/org/opentripplanner/middleware/utils/I18nUtils.java b/src/main/java/org/opentripplanner/middleware/utils/I18nUtils.java index 3e031a168..c5c523844 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/I18nUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/I18nUtils.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.utils; import org.opentripplanner.middleware.i18n.Message; +import org.opentripplanner.middleware.models.OtpUser; import java.util.Collection; import java.util.Locale; @@ -22,4 +23,12 @@ public static String label(String labelText, String content, Locale locale) { public static String label(String labelText, Locale locale) { return label(labelText, "", locale); } + + /** + * Get the OTP user's locale. + */ + public static Locale getOtpUserLocale(OtpUser user) { + return Locale.forLanguageTag(user == null || user.preferredLocale == null ? "en-US" : user.preferredLocale); + } + } diff --git a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java index 8b790b608..551b78173 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java @@ -338,27 +338,34 @@ public static String removeAgencyPrefix(String otpAgencyId) { * Get the route id from leg. */ public static String getRouteIdFromLeg(Leg leg) { - return (leg != null) ? leg.route.id : null; + return (leg != null) ? leg.routeId : null; } /** * Get the agency id from leg. */ public static String getAgencyIdFromLeg(Leg leg) { - return (leg != null) ? leg.agency.id : null; + return (leg != null) ? leg.agencyId : null; } /** * Get the trip id from leg. */ public static String getTripIdFromLeg(Leg leg) { - return (leg != null) ? leg.trip.id : null; + return (leg != null) ? leg.tripId : null; } /** * Get the stop id from place. */ public static String getStopIdFromPlace(Place place) { - return (place != null) ? place.stop.id : null; + return (place != null) ? place.stopId : null; + } + + /** + * Get the route short name from leg. + */ + public static String getRouteShortNameFromLeg(Leg leg) { + return leg.routeShortName; } } diff --git a/src/main/resources/latest-spark-swagger-output.yaml b/src/main/resources/latest-spark-swagger-output.yaml index 99a37c317..f3acd91cf 100644 --- a/src/main/resources/latest-spark-swagger-output.yaml +++ b/src/main/resources/latest-spark-swagger-output.yaml @@ -56,10 +56,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\ @@ -90,10 +90,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\ @@ -123,10 +123,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/admin/user: get: tags: @@ -155,10 +155,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/admin/user" @@ -178,10 +178,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\ @@ -220,10 +220,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\ @@ -269,10 +269,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\ @@ -309,10 +309,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\ @@ -356,10 +356,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\ @@ -402,10 +402,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\ @@ -447,10 +447,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: @@ -464,10 +464,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\ @@ -498,10 +498,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\ @@ -531,10 +531,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/application: get: tags: @@ -563,10 +563,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/application" @@ -586,10 +586,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\ @@ -628,10 +628,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\ @@ -677,10 +677,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\ @@ -717,10 +717,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\ @@ -754,10 +754,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\ @@ -788,10 +788,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\ @@ -821,10 +821,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/cdp: get: tags: @@ -853,10 +853,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/cdp" @@ -876,10 +876,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\ @@ -918,10 +918,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\ @@ -967,10 +967,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\ @@ -1007,10 +1007,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\ @@ -1050,10 +1050,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\ @@ -1102,10 +1102,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/monitoredtrip" @@ -1125,10 +1125,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\ @@ -1167,10 +1167,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\ @@ -1216,10 +1216,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\ @@ -1257,10 +1257,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\ @@ -1298,10 +1298,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: @@ -1319,10 +1319,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: @@ -1340,10 +1340,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: @@ -1361,10 +1361,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: @@ -1382,10 +1382,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/EndTrackingResponse" schema: $ref: "#/definitions/EndTrackingResponse" + responseSchema: + $ref: "#/definitions/EndTrackingResponse" /api/secure/triprequests: get: tags: @@ -1430,10 +1430,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/TripRequest" schema: $ref: "#/definitions/TripRequest" + responseSchema: + $ref: "#/definitions/TripRequest" /api/secure/monitoredcomponent: get: tags: @@ -1462,10 +1462,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/monitoredcomponent" @@ -1485,10 +1485,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\ @@ -1527,10 +1527,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\ @@ -1576,10 +1576,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\ @@ -1617,10 +1617,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\ @@ -1660,10 +1660,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: @@ -1683,10 +1683,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: @@ -1700,10 +1700,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\ @@ -1734,10 +1734,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\ @@ -1767,10 +1767,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/Job" schema: $ref: "#/definitions/Job" + responseSchema: + $ref: "#/definitions/Job" /api/secure/user: get: tags: @@ -1799,10 +1799,10 @@ paths: responses: "200": description: "successful operation" - responseSchema: - $ref: "#/definitions/ResponseList" schema: $ref: "#/definitions/ResponseList" + responseSchema: + $ref: "#/definitions/ResponseList" post: tags: - "api/secure/user" @@ -1822,10 +1822,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\ @@ -1864,10 +1864,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\ @@ -1913,10 +1913,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\ @@ -1953,10 +1953,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\ @@ -2010,11 +2010,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/ApiUsageResult" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/ApiUsageResult" @@ -2041,11 +2041,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/BugsnagEvent" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/BugsnagEvent" @@ -2072,11 +2072,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/CDPFile" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/CDPFile" @@ -2097,11 +2097,11 @@ paths: responses: "200": description: "successful operation" - responseSchema: + schema: type: "array" items: $ref: "#/definitions/URL" - schema: + responseSchema: type: "array" items: $ref: "#/definitions/URL" @@ -2398,8 +2398,6 @@ definitions: type: "boolean" mode: type: "string" - route: - type: "string" interlineWithPreviousLeg: type: "boolean" from: diff --git a/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java b/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java index 446515c36..043f742d5 100644 --- a/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java +++ b/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java @@ -36,7 +36,6 @@ public void testModes(Set devices, String mode) { var prof = new MobilityProfile(); prof.mobilityDevices = devices; prof.updateMobilityMode(); - System.out.println(prof.mobilityMode); Assertions.assertEquals(mode, prof.mobilityMode); } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index cff89b405..f17ac54cf 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -31,6 +31,7 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; +import java.util.Locale; import java.util.Objects; import java.util.stream.Stream; @@ -51,6 +52,8 @@ public class ManageLegTraversalTest { private static Itinerary adairAvenueToMonroeDriveItinerary; + private static final Locale locale = Locale.US; + @BeforeAll public static void setUp() throws IOException { // Load default env.yml configuration. @@ -189,7 +192,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( originCoords, - new TripInstruction(10, adairAvenueNortheastStep).build(), + new TripInstruction(10, adairAvenueNortheastStep, locale).build(), true, "Just started the trip and near to the instruction for the first step. " ) @@ -197,7 +200,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( originCoords, - new TripInstruction(10, adairAvenueNortheastStep).build(), + new TripInstruction(10, adairAvenueNortheastStep, locale).build(), false, "Coming up on first instruction." ) @@ -205,7 +208,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( adairAvenueNortheastCoords, - new TripInstruction(2, adairAvenueNortheastStep).build(), + new TripInstruction(2, adairAvenueNortheastStep, locale).build(), false, "On first instruction." ) @@ -214,7 +217,7 @@ private static Stream createTurnByTurnTrace() { new TurnTrace( TripStatus.DEVIATED, createPoint(adairAvenueNortheastCoords, 12, NORTH_WEST_BEARING), - new TripInstruction(adairAvenueNortheastStep.streetName).build(), + new TripInstruction(adairAvenueNortheastStep.streetName, locale).build(), false, "Deviated to the north of east to west path. Suggest path to head towards." ) @@ -223,7 +226,7 @@ private static Stream createTurnByTurnTrace() { new TurnTrace( TripStatus.DEVIATED, createPoint(adairAvenueNortheastCoords, 12, SOUTH_WEST_BEARING), - new TripInstruction(adairAvenueNortheastStep.streetName).build(), + new TripInstruction(adairAvenueNortheastStep.streetName, locale).build(), false, "Deviated to the south of east to west path. Suggest path to head towards." ) @@ -240,7 +243,7 @@ private static Stream createTurnByTurnTrace() { new TurnTrace( TripStatus.DEVIATED, createPoint(virginiaCircleNortheastCoords, 8, NORTH_BEARING), - new TripInstruction(9, virginiaCircleNortheastStep).build(), + new TripInstruction(9, virginiaCircleNortheastStep, locale).build(), false, "Deviated from path, but within the upcoming radius of second instruction." ) @@ -248,7 +251,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( virginiaCircleNortheastCoords, - new TripInstruction(0, virginiaCircleNortheastStep).build(), + new TripInstruction(0, virginiaCircleNortheastStep, locale).build(), false, "On second instruction." ) @@ -257,7 +260,7 @@ private static Stream createTurnByTurnTrace() { new TurnTrace( TripStatus.DEVIATED, createPoint(ponceDeLeonPlaceNortheastCoords, 8, NORTH_WEST_BEARING), - new TripInstruction(10, ponceDeLeonPlaceNortheastStep).build(), + new TripInstruction(10, ponceDeLeonPlaceNortheastStep, locale).build(), false, "Deviated to the west of south to north path. Suggest path to head towards." ) @@ -266,7 +269,7 @@ private static Stream createTurnByTurnTrace() { new TurnTrace( TripStatus.DEVIATED, createPoint(ponceDeLeonPlaceNortheastCoords, 8, NORTH_EAST_BEARING), - new TripInstruction(10, ponceDeLeonPlaceNortheastStep).build(), + new TripInstruction(10, ponceDeLeonPlaceNortheastStep, locale).build(), false, "Deviated to the east of south to north path. Suggest path to head towards." ) @@ -274,7 +277,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( createPoint(pointBeforeTurn, 8, calculateBearing(pointBeforeTurn, virginiaAvenuePoint)), - new TripInstruction(10, virginiaAvenueNortheastStep).build(), + new TripInstruction(10, virginiaAvenueNortheastStep, locale).build(), false, "Approaching left turn on Virginia Avenue (Test to make sure turn is not missed)." ) @@ -282,7 +285,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( createPoint(pointBeforeTurn, 17, calculateBearing(pointBeforeTurn, virginiaAvenuePoint)), - new TripInstruction(2, virginiaAvenueNortheastStep).build(), + new TripInstruction(2, virginiaAvenueNortheastStep, locale).build(), false, "Turn left on to Virginia Avenue (Test to make sure turn is not missed)." ) @@ -298,7 +301,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( createPoint(destinationCoords, 8, SOUTH_BEARING), - new TripInstruction(10, destinationName).build(), + new TripInstruction(10, destinationName, locale).build(), false, "Coming up on destination instruction." ) @@ -306,7 +309,7 @@ private static Stream createTurnByTurnTrace() { Arguments.of( new TurnTrace( destinationCoords, - new TripInstruction(2, destinationName).build(), + new TripInstruction(2, destinationName, locale).build(), false, "On destination instruction." ) diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index c8a2c7290..ea5174ed0 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.middleware.triptracker; +import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -16,6 +17,7 @@ import org.opentripplanner.middleware.testutils.OtpMiddlewareTestEnvironment; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.AgencyAction; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; +import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettBusOpNotificationMessage; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator; import org.opentripplanner.middleware.utils.Coordinates; import org.opentripplanner.middleware.utils.JsonUtils; @@ -24,12 +26,14 @@ import java.sql.Date; import java.time.Instant; import java.util.List; +import java.util.Locale; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; +import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.getNotificationMessage; class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { @@ -39,12 +43,15 @@ class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { private static final String routeId = "GwinnettCountyTransit:40"; + private static final Locale locale = Locale.US; + private final BusOperatorActions busOperatorActions = new BusOperatorActions(List.of( new AgencyAction("GCT", UsRideGwinnettNotifyBusOperator.class.getName()) )); @BeforeAll public static void setUp() throws IOException { + // This itinerary is from OTP2 and has been modified to work with OTP1 to avoid breaking changes. walkToBusTransition = JsonUtils.getPOJOFromJSON( CommonTestUtils.getTestResourceAsString("controllers/api/walk-to-bus-transition.json"), Itinerary.class @@ -66,7 +73,7 @@ void canNotifyBusOperatorForScheduledDeparture() { TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); Leg busLeg = walkToBusTransition.legs.get(1); - TripInstruction expectInstruction = new TripInstruction(busLeg, Instant.now()); + TripInstruction expectInstruction = new TripInstruction(busLeg, Instant.now(), locale); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); assertEquals(expectInstruction.build(), tripInstruction); @@ -87,12 +94,12 @@ void canNotifyBusOperatorForDelayedDeparture() throws CloneNotSupportedException String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); Leg busLeg = itinerary.legs.get(1); - TripInstruction expectInstruction = new TripInstruction(busLeg, timeAtEndOfWalkLeg); + TripInstruction expectInstruction = new TripInstruction(busLeg, timeAtEndOfWalkLeg, locale); assertEquals(expectInstruction.build(), tripInstruction); } @Test - void canCancelBusOperatorNotification() { + void canCancelBusOperatorNotification() throws JsonProcessingException { trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates()); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); @@ -100,7 +107,9 @@ void canCancelBusOperatorNotification() { assertTrue(updated.busNotificationMessages.containsKey(routeId)); busOperatorActions.handleCancelNotificationAction(travelerPosition); updated = Persistence.trackedJourneys.getById(trackedJourney.id); - assertFalse(updated.busNotificationMessages.containsKey(routeId)); + String messageBody = updated.busNotificationMessages.get(routeId); + UsRideGwinnettBusOpNotificationMessage message = getNotificationMessage(messageBody); + assertTrue(message.msg_type == 1); } @Test @@ -110,7 +119,7 @@ void canNotifyBusOperatorOnlyOnce() { busOperatorActions.handleSendNotificationAction(TripStatus.ON_SCHEDULE, travelerPosition); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); - assertFalse(UsRideGwinnettNotifyBusOperator.hasNotPreviouslyNotifiedBusDriverForRoute(trackedJourney, routeId)); + assertFalse(UsRideGwinnettNotifyBusOperator.hasNotSentNotificationForRoute(trackedJourney, routeId)); } @ParameterizedTest diff --git a/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json b/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json index 4baa14c2b..c5038100e 100644 --- a/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json +++ b/src/test/resources/org/opentripplanner/middleware/controllers/api/walk-to-bus-transition.json @@ -178,6 +178,7 @@ }, { "accessibilityScore": null, + "agencyId": "GwinnettCountyTransit:GCT", "agency": { "alerts": [ { @@ -270,6 +271,7 @@ "lon": -84.000314, "name": "Gwinnett Central High School IB", "rentalVehicle": null, + "stopId": "GwinnettCountyTransit:U3RvcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6MzYw", "stop": { "alerts": [], "code": "360", @@ -333,6 +335,8 @@ "realtimeState": null, "rentedBike": null, "rideHailingEstimate": null, + "routeId": "GwinnettCountyTransit:40", + "routeShortName": "40", "route": { "alerts": [], "color": "AB0634", @@ -359,6 +363,7 @@ "vertexType": "TRANSIT" }, "transitLeg": true, + "tripId": "GwinnettCountyTransit:VHJpcDpHd2lubmV0dENvdW50eVRyYW5zaXQ6dDNBNC1iMTMwLXNsNg", "trip": { "arrivalStoptime": { "stop": { From cacbddb07551f3c0015c8740e0109ca024ac7a79 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 30 May 2024 17:40:57 +0100 Subject: [PATCH 06/16] refactor(Removed redundant Stop class): --- .../opentripplanner/middleware/otp/response/Stop.java | 10 ---------- 1 file changed, 10 deletions(-) delete mode 100644 src/main/java/org/opentripplanner/middleware/otp/response/Stop.java diff --git a/src/main/java/org/opentripplanner/middleware/otp/response/Stop.java b/src/main/java/org/opentripplanner/middleware/otp/response/Stop.java deleted file mode 100644 index bb4ae9592..000000000 --- a/src/main/java/org/opentripplanner/middleware/otp/response/Stop.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.opentripplanner.middleware.otp.response; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonInclude; - -@JsonInclude(JsonInclude.Include.NON_NULL) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Stop { - public String id; -} From ae2f0dbfd40ee14eef87bbadd624458864864609 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 31 May 2024 09:03:26 +0100 Subject: [PATCH 07/16] refactor(Addressed PR feedback): --- .../triptracker/TravelerLocator.java | 1 - .../triptracker/TripInstruction.java | 8 +++--- .../busnotifiers/BusOperatorActions.java | 6 +++-- ...sRideGwinnettBusOpNotificationMessage.java | 1 - .../UsRideGwinnettNotifyBusOperator.java | 27 +++++++++---------- .../middleware/utils/ItineraryUtils.java | 6 ++--- .../models/MobilityProfileTest.java | 3 +-- 7 files changed, 25 insertions(+), 27 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 9bdf6dadd..0db27ef57 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -17,7 +17,6 @@ import static org.opentripplanner.middleware.utils.GeometryUtils.getDistance; import static org.opentripplanner.middleware.utils.GeometryUtils.isPointBetween; import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; -import static org.opentripplanner.middleware.utils.ItineraryUtils.removeAgencyPrefix; /** * Locate the traveler in relation to the nearest step or destination and provide the appropriate instructions. diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java index 001cbed66..64d7254af 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripInstruction.java @@ -173,9 +173,9 @@ private String buildWaitForBusInstruction() { String delayInfo = (delayInMinutes > 0) ? "late" : "early"; String arrivalInfo = (absoluteMinutes <= 1) ? ", on time" - : String.format("now %s %s", getReadableMinutes(delayInMinutes), delayInfo); + : String.format(" now%s %s", getReadableMinutes(delayInMinutes), delayInfo); return String.format( - "Wait %s for your bus, route %s, scheduled at %s %s", + "Wait%s for your bus, route %s, scheduled at %s%s", getReadableMinutes(waitInMinutes), routeShortName, DateTimeUtils.formatShortDate(Date.from(busLeg.getScheduledStartTime().toInstant()), locale), @@ -188,9 +188,9 @@ private String buildWaitForBusInstruction() { */ private String getReadableMinutes(long waitInMinutes) { if (waitInMinutes == 1) { - return String.format("%s minute", waitInMinutes); + return String.format(" %s minute", waitInMinutes); } else if (waitInMinutes > 1) { - return String.format("%s minutes", waitInMinutes); + return String.format(" %s minutes", waitInMinutes); } return ""; } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java index 11acf5703..35502e549 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/BusOperatorActions.java @@ -30,9 +30,11 @@ public static BusOperatorActions getDefault() { if (defaultInstance == null) { try (InputStream stream = new FileInputStream(BUS_NOTIFIER_ACTIONS_YML)) { JsonNode busNotifierActionsYml = YamlUtils.yamlMapper.readTree(stream); - defaultInstance = new BusOperatorActions(JsonUtils.getPOJOFromJSONAsList(busNotifierActionsYml, AgencyAction.class)); + defaultInstance = new BusOperatorActions( + JsonUtils.getPOJOFromJSONAsList(busNotifierActionsYml, AgencyAction.class) + ); } catch (IOException e) { - LOG.error("Error parsing trip-actions.yml", e); + LOG.error("Error parsing bus-notifier-actions.yml", e); throw new RuntimeException(e); } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java index 9f1bc97c1..594ec646a 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java @@ -103,5 +103,4 @@ private static List getMobilityCode(String mobilityMode) { } return mobilityCodes; } - } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java index 246b8c059..263d3b328 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java @@ -23,12 +23,10 @@ import static org.opentripplanner.middleware.utils.ItineraryUtils.isBusLeg; /** - * If conditions are correct notify a bus operator of a traveler joining the service at a given stop. + * If conditions are correct, notify a bus operator of a traveler waiting to board at a given stop. */ public class UsRideGwinnettNotifyBusOperator implements BusOperatorInteraction { - public UsRideGwinnettNotifyBusOperator() {} - public static boolean IS_TEST = false; private static final Logger LOG = LoggerFactory.getLogger(UsRideGwinnettNotifyBusOperator.class); @@ -87,11 +85,11 @@ public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPos if (httpStatus == HttpStatus.OK_200) { travelerPosition.trackedJourney.updateNotificationMessage(routeId, body); } else { - LOG.error("Error {} while trying to initiate notification to bus operator.", httpStatus); + LOG.error("Error {} while trying to initiate Ride Gwinnett notification to bus operator.", httpStatus); } } } catch (Exception e) { - LOG.error("Could not initiate notification to bus operator.", e); + LOG.error("Could not initiate Ride Gwinnett notification to bus operator.", e); } } @@ -117,12 +115,12 @@ public void cancelNotification(TravelerPosition travelerPosition) { if (httpStatus == HttpStatus.OK_200) { travelerPosition.trackedJourney.updateNotificationMessage(routeId, JsonUtils.toJson(body)); } else { - LOG.error("Error {} while trying to cancel notification to bus operator.", httpStatus); + LOG.error("Error {} while trying to cancel Ride Gwinnett notification to bus operator.", httpStatus); } } } } catch (Exception e) { - LOG.error("Could not cancel notification to bus operator.", e); + LOG.error("Could not cancel Ride Gwinnett notification to bus operator.", e); } } @@ -131,8 +129,7 @@ public void cancelNotification(TravelerPosition travelerPosition) { */ public static int doPost(String body) { if (IS_TEST) { - // TODO figure out how to mock a static method! - return 200; + return HttpStatus.OK_200; } var httpResponse = HttpUtils.httpRequestRawResponse( URI.create(US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL), @@ -156,10 +153,10 @@ public static String createPostBody(TravelerPosition travelerPosition) { * the format agency_id:route_id e.g. GwinnettCountyTransit:360. If no routes are defined it is assumed that all * routes support notification. */ - public static boolean supportsBusOperatorNotification(String gtfsId) { + public static boolean supportsBusOperatorNotification(String routeId) { return US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES.isEmpty() || - US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES.contains(gtfsId); + US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES.contains(routeId); } /** @@ -173,11 +170,13 @@ public static boolean hasNotSentNotificationForRoute(TrackedJourney trackedJourn /** * Has a previous notification already been cancelled. */ - public static boolean hasNotCancelledNotificationForRoute(TrackedJourney trackedJourney, String routeId) throws JsonProcessingException { + public static boolean hasNotCancelledNotificationForRoute( + TrackedJourney trackedJourney, + String routeId + ) throws JsonProcessingException { String messageBody = trackedJourney.busNotificationMessages.get(routeId); if (messageBody == null) { - // It should not be possible to get here because a notification must exist before it can be cancelled. - return false; + throw new IllegalStateException("A notification must exist before it can be cancelled!"); } UsRideGwinnettBusOpNotificationMessage message = getNotificationMessage(messageBody); return message.msg_type != 1; diff --git a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java index 551b78173..b7d669730 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/ItineraryUtils.java @@ -327,11 +327,11 @@ public static boolean isBusLeg(Leg leg) { } /** - * Get the second element from the OTP agency id by removing the OTP agency prefix. + * Get the second element from the OTP id by removing the OTP agency prefix. * E.g. GwinnettCountyTransit:GCT will return just GCT. */ - public static String removeAgencyPrefix(String otpAgencyId) { - return (otpAgencyId != null) ? otpAgencyId.split(":")[1] : null; + public static String removeAgencyPrefix(String idParts) { + return (idParts != null) ? idParts.split(":")[1] : null; } /** diff --git a/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java b/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java index 043f742d5..ea9c095ee 100644 --- a/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java +++ b/src/test/java/org/opentripplanner/middleware/models/MobilityProfileTest.java @@ -15,7 +15,7 @@ public class MobilityProfileTest { // The mobility modes tested are tightly coupled with algorithms in the // Georgia Tech Mobility Profile Configuration / Logical Flow document, as - // implemented in the MobilityPorfile#updateMobilityMode() method. Changes + // implemented in the MobilityProfile#updateMobilityMode() method. Changes // to that document must be reflected in that method and in these tests. private static Stream provideModes() { @@ -55,7 +55,6 @@ public void testModesVision(MobilityProfile.VisionLimitation limitation, Set Date: Fri, 31 May 2024 11:51:27 +0100 Subject: [PATCH 08/16] refactor(Updated documenation files): Updated readme and env.schema.json --- README.md | 26 ++++++++++++++++++++++++++ src/main/resources/env.schema.json | 15 +++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/README.md b/README.md index b3a675509..ef320ab1e 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,29 @@ The follow parameters are used to interact with an OTP server. | OTP_API_ROOT | This is the address of the OTP server, including the root path to the OTP API, to which all OTP related requests will be sent to. | http://otp-server.example.com/otp | | OTP_PLAN_ENDPOINT | This defines the plan endpoint part of the requesting URL. If a request is made to this, the assumption is that a plan request has been made and that the response should be processed accordingly. | /plan | +### Trip Actions + +OTP-middleware supports triggering certain actions when someone activates live tracking of a monitored trip +and reaches a location or is about to enter a path. Actions include location-sensitive API calls to notify various services. +In the context of live trip tracking, actions may include notifying transit vehicle operators or triggering traffic signals. + +#### Bus Notify Actions + +Bus notifier actions are defined in the optional file `bus-notifier-actions.yml` in the same configuration folder as `env.yml`. +The file contains a list of actions defined by an agency ID and a fully-qualified trigger class: + +```yaml +- agencyId: id1 + trigger: com.example.package.MyTriggerClass +``` + +Known trigger classes below are in package `org.opentripplanner.middleware.triptracker.interactions.busnotifiers` +and implement its `BusOperatorInteraction` interface: + +| Class | Description | +| ----- |------------------------------------------------------------------------------| +| UsRideGwinnettNotifyBusOperator | Triggers select route bus operator notifications in Gwinnett County, GA, USA | + ### Monitored Components This application allows you to monitor various system components (e.g., OTP API, OTP UI, and Data Tools) that work together @@ -273,4 +296,7 @@ The special E2E client settings should be defined in `env.yml`: | TRIP_INSTRUCTION_UPCOMING_RADIUS | integer | Optional | 10 | The radius in meters under which an upcoming instruction is given. | | TWILIO_ACCOUNT_SID | string | Optional | your-account-sid | Twilio settings available at: https://twilio.com/user/account | | TWILIO_AUTH_TOKEN | string | Optional | your-auth-token | Twilio settings available at: https://twilio.com/user/account | +| US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL | string | Optional | http://host.example.com | US Ride Gwinnett County bus notifier API. | +| US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY | string | Optional | your-api-key | API key for the US Ride Gwinnett County bus notifier API. | +| US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES | string | Optional | agency_id:route_id | A comma separated list of US Ride Gwinnett County routes that can be notified. | | VALIDATE_ENVIRONMENT_CONFIG | boolean | Optional | true | If set to false, the validation of the env.yml file against this schema will be skipped. | diff --git a/src/main/resources/env.schema.json b/src/main/resources/env.schema.json index f3cb43458..e70d7654d 100644 --- a/src/main/resources/env.schema.json +++ b/src/main/resources/env.schema.json @@ -277,6 +277,21 @@ "examples": ["your-auth-token"], "description": "Twilio settings available at: https://twilio.com/user/account" }, + "US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL": { + "type": "string", + "examples": ["http://host.example.com"], + "description": "US Ride Gwinnett County bus notifier API." + }, + "US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY": { + "type": "string", + "examples": ["your-api-key"], + "description": "API key for the US Ride Gwinnett County bus notifier API." + }, + "US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES": { + "type": "string", + "examples": ["agency_id:route_id"], + "description": "A comma separated list of US Ride Gwinnett County routes that can be notified." + }, "VALIDATE_ENVIRONMENT_CONFIG": { "type": "boolean", "examples": ["true"], From 56e6c1553f1c9d0af609ad45a2890aff2d341aaf Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 5 Jun 2024 10:35:02 +0100 Subject: [PATCH 09/16] refactor(Refactored within operational notify window): Check is now based on comparing traveler time --- .../triptracker/TravelerLocator.java | 37 +++++++++- .../triptracker/TravelerPosition.java | 7 +- .../UsRideGwinnettNotifyBusOperator.java | 27 -------- .../triptracker/NotifyBusOperatorTest.java | 69 ++++++++++--------- 4 files changed, 77 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 440eeb93b..c512c15ce 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -5,8 +5,12 @@ import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions; import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.DateTimeUtils; import javax.annotation.Nullable; +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import java.util.Locale; @@ -23,6 +27,8 @@ */ public class TravelerLocator { + public static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; + private TravelerLocator() { } @@ -101,8 +107,7 @@ public static TripInstruction alignTravelerToTrip( Locale locale = travelerPosition.locale; if (isApproachingEndOfLeg(travelerPosition)) { - if (isBusLeg(travelerPosition.nextLeg)) { - // The upcoming leg is a bus leg. + if (isBusLeg(travelerPosition.nextLeg) && isWithinOperationalNotifyWindow(travelerPosition)) { BusOperatorActions .getDefault() .handleSendNotificationAction(tripStatus, travelerPosition); @@ -146,6 +151,34 @@ private static boolean isApproachingEndOfLeg(TravelerPosition travelerPosition) return getDistanceToEndOfLeg(travelerPosition) <= TRIP_INSTRUCTION_UPCOMING_RADIUS; } + /** + * Make sure the traveler is on schedule or ahead of schedule (but not too far) to be within an operational window + * for the bus service. + */ + public static boolean isWithinOperationalNotifyWindow(TravelerPosition travelerPosition) { + var busDepartureTime = getBusDepartureTime(travelerPosition.nextLeg); + return + (travelerPosition.currentTime.equals(busDepartureTime) || travelerPosition.currentTime.isBefore(busDepartureTime)) && + ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES >= getMinutesAheadOfDeparture(travelerPosition.currentTime, busDepartureTime); + } + + /** + * Get how far ahead in minutes the traveler is from the bus departure time. + */ + public static long getMinutesAheadOfDeparture(Instant currentTime, Instant busDepartureTime) { + return Duration.between(busDepartureTime, currentTime).toMinutes(); + } + + /** + * Get the bus departure time. + */ + public static Instant getBusDepartureTime(Leg busLeg) { + return ZonedDateTime.ofInstant( + busLeg.startTime.toInstant().plusSeconds(busLeg.departureDelay), + DateTimeUtils.getOtpZoneId() + ).toInstant(); + } + /** * Get the distance from the traveler's current position to the leg destination. */ diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java index 97feb8b49..43fdd988c 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerPosition.java @@ -1,6 +1,5 @@ package org.opentripplanner.middleware.triptracker; -import org.opentripplanner.middleware.auth.RequestingUser; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.otp.response.Itinerary; @@ -65,4 +64,10 @@ public TravelerPosition(Leg expectedLeg, Coordinates currentPosition) { this.currentPosition = currentPosition; legSegmentFromPosition = getSegmentFromPosition(expectedLeg, currentPosition); } + + /** Used for unit testing. */ + public TravelerPosition(Leg nextLeg, Instant currentTime) { + this.nextLeg = nextLeg; + this.currentTime = currentTime; + } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java index 263d3b328..319226acc 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettNotifyBusOperator.java @@ -12,7 +12,6 @@ import org.slf4j.LoggerFactory; import java.net.URI; -import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -39,8 +38,6 @@ public class UsRideGwinnettNotifyBusOperator implements BusOperatorInteraction { public static List US_RIDE_GWINNETT_QUALIFYING_BUS_NOTIFIER_ROUTES = getBusOperatorNotifierQualifyingRoutes(); - public static final int ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES = 15; - /** * Headers that are required for each request. */ @@ -72,8 +69,6 @@ public void sendNotification(TripStatus tripStatus, TravelerPosition travelerPos var routeId = getRouteIdFromLeg(travelerPosition.nextLeg); try { if ( - isBusLeg(travelerPosition.nextLeg) && - isWithinOperationalNotifyWindow(tripStatus, travelerPosition) && hasNotSentNotificationForRoute(travelerPosition.trackedJourney, routeId) && supportsBusOperatorNotification(routeId) ) { @@ -185,26 +180,4 @@ public static boolean hasNotCancelledNotificationForRoute( public static UsRideGwinnettBusOpNotificationMessage getNotificationMessage(String body) throws JsonProcessingException { return JsonUtils.getPOJOFromJSON(body, UsRideGwinnettBusOpNotificationMessage.class); } - - /** - * Make sure the traveler is on schedule or ahead of schedule (but not too far) to be within an operational window - * for the bus service. - */ - public static boolean isWithinOperationalNotifyWindow(TripStatus tripStatus, TravelerPosition travelerPosition) { - return - tripStatus.equals(TripStatus.ON_SCHEDULE) || - ( - tripStatus.equals(TripStatus.AHEAD_OF_SCHEDULE) && - ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES >= getMinutesAheadOfSchedule(travelerPosition) - ); - } - - /** - * Get how far ahead in minutes the traveler is from the expected schedule. - */ - public static long getMinutesAheadOfSchedule(TravelerPosition travelerPosition) { - return Duration - .between(TripStatus.getSegmentStartTime(travelerPosition), travelerPosition.currentTime) - .toMinutes(); - } } \ No newline at end of file diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index ea5174ed0..ad6ac3254 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -32,7 +32,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; +import static org.opentripplanner.middleware.triptracker.TravelerLocator.getBusDepartureTime; import static org.opentripplanner.middleware.triptracker.interactions.busnotifiers.UsRideGwinnettNotifyBusOperator.getNotificationMessage; class NotifyBusOperatorTest extends OtpMiddlewareTestEnvironment { @@ -69,11 +70,12 @@ public void tearDown() { @Test void canNotifyBusOperatorForScheduledDeparture() { - trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates()); + Leg busLeg = walkToBusTransition.legs.get(1); + Instant busDepartureTime = getBusDepartureTime(busLeg); + trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates(), busDepartureTime); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); - Leg busLeg = walkToBusTransition.legs.get(1); - TripInstruction expectInstruction = new TripInstruction(busLeg, Instant.now(), locale); + TripInstruction expectInstruction = new TripInstruction(busLeg, busDepartureTime, locale); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); assertEquals(expectInstruction.build(), tripInstruction); @@ -89,7 +91,7 @@ void canNotifyBusOperatorForDelayedDeparture() throws CloneNotSupportedException Instant timeAtEndOfWalkLeg = walkLeg.endTime.toInstant(); timeAtEndOfWalkLeg = timeAtEndOfWalkLeg.minusSeconds(120); - trackedJourney = createAndPersistTrackedJourney(true, getEndOfWalkLegCoordinates(), timeAtEndOfWalkLeg); + trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates(), timeAtEndOfWalkLeg); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, itinerary, createOtpUser()); String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); @@ -124,35 +126,36 @@ void canNotifyBusOperatorOnlyOnce() { @ParameterizedTest @MethodSource("createWithinOperationalNotifyWindowTrace") - void isWithinOperationalNotifyWindow( - boolean expected, - TripStatus tripStatus, - TravelerPosition travelerPosition, - String message - ) { - assertEquals(expected, UsRideGwinnettNotifyBusOperator.isWithinOperationalNotifyWindow(tripStatus, travelerPosition), message); + void isWithinOperationalNotifyWindow(boolean expected, TravelerPosition travelerPosition,String message) { + assertEquals(expected, TravelerLocator.isWithinOperationalNotifyWindow(travelerPosition), message); } private static Stream createWithinOperationalNotifyWindowTrace() { - Leg walkLeg = walkToBusTransition.legs.get(0); - Instant timeAtEndOfWalkLeg = walkLeg.endTime.toInstant(); - TrackedJourney trackedJourney = createAndPersistTrackedJourney( - false, - getEndOfWalkLegCoordinates(), - timeAtEndOfWalkLeg - ); - TravelerPosition travelerPosition1 = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - trackedJourney = createAndPersistTrackedJourney( - false, - getEndOfWalkLegCoordinates(), - timeAtEndOfWalkLeg.plusSeconds(60 * ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES) - ); - TravelerPosition travelerPosition2 = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); + var busLeg = walkToBusTransition.legs.get(1); + var busDepartureTime = getBusDepartureTime(busLeg); + return Stream.of( - Arguments.of(true, TripStatus.ON_SCHEDULE, null, "Traveler is on schedule, notification can be sent."), - Arguments.of(false, TripStatus.BEHIND_SCHEDULE, null, "Traveler is behind schedule, notification can not be sent."), - Arguments.of(true, TripStatus.AHEAD_OF_SCHEDULE, travelerPosition1, "Traveler is ahead of schedule, but within the notify window."), - Arguments.of(false, TripStatus.AHEAD_OF_SCHEDULE, travelerPosition2, "Too far ahead of schedule to notify bus operator.") + Arguments.of( + true, + new TravelerPosition(busLeg, busDepartureTime), + "Traveler is on schedule, notification can be sent." + ), + Arguments.of( + false, + new TravelerPosition(busLeg, busDepartureTime.plusSeconds(60)), + "Traveler is behind schedule, notification can not be sent." + ), + Arguments.of( + true, + new TravelerPosition(busLeg, busDepartureTime.minusSeconds(60)), + "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) + ), + "Too far ahead of schedule to notify bus operator.") ); } @@ -165,13 +168,13 @@ private static OtpUser createOtpUser() { } private static TrackedJourney createAndPersistTrackedJourney(Coordinates legToCoords) { - return createAndPersistTrackedJourney(true, legToCoords, Instant.now()); + return createAndPersistTrackedJourney(legToCoords, Instant.now()); } - private static TrackedJourney createAndPersistTrackedJourney(boolean persist, Coordinates legToCoords, Instant dateTime) { + private static TrackedJourney createAndPersistTrackedJourney(Coordinates legToCoords, Instant dateTime) { trackedJourney = new TrackedJourney(); trackedJourney.locations.add(new TrackingLocation(legToCoords.lat, legToCoords.lon, Date.from(dateTime))); - if (persist) Persistence.trackedJourneys.create(trackedJourney); + Persistence.trackedJourneys.create(trackedJourney); return trackedJourney; } From cdddcaaecbc36eb65bb780e04a34b1286195f651 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:20:18 -0400 Subject: [PATCH 10/16] fix(TripTrackingData): Convert location timestamps to milliseconds. --- .../middleware/triptracker/TripTrackingData.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java index 7ac96ffbf..7f9fb56bc 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java @@ -10,7 +10,10 @@ import org.opentripplanner.middleware.triptracker.payload.GeneralPayload; import spark.Request; +import java.time.Instant; +import java.util.Date; import java.util.List; +import java.util.stream.Collectors; import static com.mongodb.client.model.Filters.eq; import static org.opentripplanner.middleware.utils.JsonUtils.getPOJOFromRequestBody; @@ -88,12 +91,22 @@ public static TripTrackingData fromRequestTripId(Request request) { if (payload != null) { var monitoredTrip = Persistence.monitoredTrips.getById(payload.tripId); if (isTripAssociatedWithUser(request, monitoredTrip)) { - return new TripTrackingData(monitoredTrip, getOngoingTrackedJourney(payload.tripId), payload.getLocations()); + List locationsMillis = getTrackingLocationsMillis(payload); + return new TripTrackingData(monitoredTrip, getOngoingTrackedJourney(payload.tripId), locationsMillis); } } return null; } + /** HACK: Convert locations so that the time stamp is in milliseconds not seconds. */ + private static List getTrackingLocationsMillis(GeneralPayload payload) { + return payload + .getLocations() + .stream() + .map(l -> new TrackingLocation(l.bearing, l.lat, l.lon, l.speed, Date.from(Instant.ofEpochMilli(l.timestamp.getTime() * 1000)))) + .collect(Collectors.toList()); + } + /** Obtain trip, journey, and locations from the journey id contained in the request. */ public static TripTrackingData fromRequestJourneyId(Request request) { GeneralPayload payload = getPayloadFromRequest(request); From 416e6ae8a7e515e73c6d5bcc26aa0e16c87fc932 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:23:26 -0400 Subject: [PATCH 11/16] refactor(TripTrackingData): Use ofEpochSecond. --- .../middleware/triptracker/TripTrackingData.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java index 7f9fb56bc..7b42f64ac 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java @@ -103,7 +103,7 @@ private static List getTrackingLocationsMillis(GeneralPayload return payload .getLocations() .stream() - .map(l -> new TrackingLocation(l.bearing, l.lat, l.lon, l.speed, Date.from(Instant.ofEpochMilli(l.timestamp.getTime() * 1000)))) + .map(l -> new TrackingLocation(l.bearing, l.lat, l.lon, l.speed, Date.from(Instant.ofEpochSecond(l.timestamp.getTime())))) .collect(Collectors.toList()); } From 50ff02c1aa1299caf58f658304b36f0fca6acdd7 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:48:04 -0400 Subject: [PATCH 12/16] refactor(TripTrackingData): Handle null payload locations. --- .../middleware/triptracker/TripTrackingData.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java index 7b42f64ac..5907cb449 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java @@ -11,6 +11,7 @@ import spark.Request; import java.time.Instant; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Collectors; @@ -100,8 +101,9 @@ public static TripTrackingData fromRequestTripId(Request request) { /** HACK: Convert locations so that the time stamp is in milliseconds not seconds. */ private static List getTrackingLocationsMillis(GeneralPayload payload) { - return payload - .getLocations() + List rawLocations = payload.getLocations(); + if (rawLocations == null) rawLocations = new ArrayList<>(); + return rawLocations .stream() .map(l -> new TrackingLocation(l.bearing, l.lat, l.lon, l.speed, Date.from(Instant.ofEpochSecond(l.timestamp.getTime())))) .collect(Collectors.toList()); From d9bb37ea180aeff2823e366d55717829e0ef53ce Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 6 Jun 2024 07:38:14 +0100 Subject: [PATCH 13/16] refactor(Added to stop id to bus op notification message): --- .../busnotifiers/UsRideGwinnettBusOpNotificationMessage.java | 2 ++ .../middleware/triptracker/NotifyBusOperatorTest.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java index 594ec646a..b74381232 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java @@ -70,6 +70,7 @@ private static Map createMobilityCodesLookup() { public String from_route_id; public String from_trip_id; public String from_stop_id; + public String to_stop_id; public String from_arrival_time; public Integer msg_type; public List mobility_codes; @@ -82,6 +83,7 @@ public UsRideGwinnettBusOpNotificationMessage(Instant timestamp, TravelerPositio this.from_route_id = removeAgencyPrefix(getRouteIdFromLeg(nextLeg)); this.from_trip_id = removeAgencyPrefix(getTripIdFromLeg(nextLeg)); this.from_stop_id = removeAgencyPrefix(getStopIdFromPlace(nextLeg.from)); + this.to_stop_id = removeAgencyPrefix(getStopIdFromPlace(nextLeg.to)); this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format( nextLeg.getScheduledStartTime().toInstant() ); diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index ad6ac3254..87456f799 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -111,7 +111,7 @@ void canCancelBusOperatorNotification() throws JsonProcessingException { updated = Persistence.trackedJourneys.getById(trackedJourney.id); String messageBody = updated.busNotificationMessages.get(routeId); UsRideGwinnettBusOpNotificationMessage message = getNotificationMessage(messageBody); - assertTrue(message.msg_type == 1); + assertEquals(1, message.msg_type); } @Test From fcd913b2e8dc9d06cd0dc29884f8e39b1f954fa5 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 6 Jun 2024 09:08:35 +0100 Subject: [PATCH 14/16] refactor(Tidied a couple of things up and updating unit tests): --- .../triptracker/TrackingLocation.java | 5 ++++ .../triptracker/TripTrackingData.java | 19 +++++++++------ .../middleware/utils/DateTimeUtils.java | 7 ++++++ .../api/TrackedTripControllerTest.java | 24 ++++++++++++------- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TrackingLocation.java b/src/main/java/org/opentripplanner/middleware/triptracker/TrackingLocation.java index c31340863..087dc6694 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TrackingLocation.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TrackingLocation.java @@ -42,4 +42,9 @@ public TrackingLocation(Double lat, Double lon, Date timestamp) { public TrackingLocation(Instant instant, double lat, double lon) { this(lat, lon, new Date(instant.toEpochMilli())); } + + /** Used in testing **/ + public TrackingLocation(Date timestamp, double lat, double lon) { + this(lat, lon, timestamp); + } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java index 5907cb449..ae03e23b1 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TripTrackingData.java @@ -10,13 +10,12 @@ import org.opentripplanner.middleware.triptracker.payload.GeneralPayload; import spark.Request; -import java.time.Instant; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.stream.Collectors; import static com.mongodb.client.model.Filters.eq; +import static org.opentripplanner.middleware.utils.DateTimeUtils.convertDateFromSecondsToMillis; import static org.opentripplanner.middleware.utils.JsonUtils.getPOJOFromRequestBody; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -92,20 +91,26 @@ public static TripTrackingData fromRequestTripId(Request request) { if (payload != null) { var monitoredTrip = Persistence.monitoredTrips.getById(payload.tripId); if (isTripAssociatedWithUser(request, monitoredTrip)) { - List locationsMillis = getTrackingLocationsMillis(payload); - return new TripTrackingData(monitoredTrip, getOngoingTrackedJourney(payload.tripId), locationsMillis); + return new TripTrackingData( + monitoredTrip, + getOngoingTrackedJourney(payload.tripId), + getTrackingLocations(payload) + ); } } return null; } - /** HACK: Convert locations so that the time stamp is in milliseconds not seconds. */ - private static List getTrackingLocationsMillis(GeneralPayload payload) { + /** + * Get the tracking locations and convert the timestamp from seconds (as provided by the mobile app) into + * milliseconds. + */ + private static List getTrackingLocations(GeneralPayload payload) { List rawLocations = payload.getLocations(); if (rawLocations == null) rawLocations = new ArrayList<>(); return rawLocations .stream() - .map(l -> new TrackingLocation(l.bearing, l.lat, l.lon, l.speed, Date.from(Instant.ofEpochSecond(l.timestamp.getTime())))) + .map(l -> new TrackingLocation(l.bearing, l.lat, l.lon, l.speed, convertDateFromSecondsToMillis(l.timestamp))) .collect(Collectors.toList()); } diff --git a/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java b/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java index c748450a9..fd989ecb9 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/DateTimeUtils.java @@ -294,4 +294,11 @@ public static List getHoursBetween(LocalDateTime start, LocalDate public static LocalDateTime getPreviousWholeHourFromNow() { return LocalDateTime.now().truncatedTo(ChronoUnit.HOURS).minusHours(1); } + + /** + * Converts a date provided in seconds to a date in milliseconds. + */ + public static Date convertDateFromSecondsToMillis(Date date) { + return Date.from(Instant.ofEpochSecond(date.getTime())); + } } diff --git a/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java b/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java index acd1be155..2cd0c8303 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/TrackedTripControllerTest.java @@ -26,8 +26,10 @@ import org.opentripplanner.middleware.triptracker.ManageTripTracking; import org.opentripplanner.middleware.triptracker.TrackingLocation; import org.opentripplanner.middleware.triptracker.TripStatus; +import org.opentripplanner.middleware.triptracker.TripTrackingData; import org.opentripplanner.middleware.triptracker.payload.EndTrackingPayload; import org.opentripplanner.middleware.triptracker.payload.ForceEndTrackingPayload; +import org.opentripplanner.middleware.triptracker.payload.GeneralPayload; import org.opentripplanner.middleware.triptracker.payload.StartTrackingPayload; import org.opentripplanner.middleware.triptracker.payload.TrackPayload; import org.opentripplanner.middleware.triptracker.payload.UpdatedTrackingPayload; @@ -388,21 +390,17 @@ private StartTrackingPayload createStartTrackingPayload() { } private StartTrackingPayload createStartTrackingPayload(String monitorTripId) { - return createStartTrackingPayload(monitorTripId, 24.1111111111111, -79.2222222222222, new Date().getTime()); - } - - private StartTrackingPayload createStartTrackingPayload(String monitorTripId, double lat, double lon, long timestamp) { var payload = new StartTrackingPayload(); payload.tripId = monitorTripId; - payload.location = new TrackingLocation(90, lat, lon, 29, new Date(timestamp)); + payload.location = new TrackingLocation(90, 24.1111111111111, -79.2222222222222, 29, getDateAndConvertToSeconds()); return payload; } private static List createTrackingLocations() { return List.of( - new TrackingLocation(90, 24.1111111111111, -79.2222222222222, 29, new Date()), - new TrackingLocation(90, 28.5398938204469, -81.3772773742676, 30, new Date()), - new TrackingLocation(90, 29.5398938204469, -80.3772773742676, 31, new Date()) + new TrackingLocation(90, 24.1111111111111, -79.2222222222222, 29, getDateAndConvertToSeconds()), + new TrackingLocation(90, 28.5398938204469, -81.3772773742676, 30, getDateAndConvertToSeconds()), + new TrackingLocation(90, 29.5398938204469, -80.3772773742676, 31, getDateAndConvertToSeconds()) ); } @@ -421,7 +419,7 @@ private TrackPayload createTrackPayload(List locations) { } private TrackPayload createTrackPayload(Coordinates coords) { - return createTrackPayload(List.of(new TrackingLocation(Instant.now(), coords.lat, coords.lon))); + return createTrackPayload(List.of(new TrackingLocation(getDateAndConvertToSeconds(), coords.lat, coords.lon))); } private EndTrackingPayload createEndTrackingPayload(String journeyId) { @@ -435,4 +433,12 @@ private ForceEndTrackingPayload createForceEndTrackingPayload(String monitorTrip payload.tripId = monitorTripId; return payload; } + + /** + * The mobile app sends timestamps in seconds which is then converted into milliseconds in {@link TripTrackingData}. + * To represent this in testing, provide the time in seconds from epoch. + */ + private static Date getDateAndConvertToSeconds() { + return new Date(new Date().getTime() / 1000); + } } From bbfa26f154d08da4f36f887232a3418e70da467d Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 7 Jun 2024 08:29:51 +0100 Subject: [PATCH 15/16] refactor(UsRideGwinnettBusOpNotificationMessage.java): Converted the UTC timestamp into a local time --- .../UsRideGwinnettBusOpNotificationMessage.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java index b74381232..20e3d96c0 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java @@ -3,6 +3,7 @@ import org.opentripplanner.middleware.triptracker.TravelerPosition; import java.time.Instant; +import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; @@ -78,14 +79,16 @@ private static Map createMobilityCodesLookup() { public UsRideGwinnettBusOpNotificationMessage(Instant timestamp, TravelerPosition travelerPosition) { var nextLeg = travelerPosition.nextLeg; - this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(timestamp); + this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format( + ZonedDateTime.ofInstant(timestamp, getOtpZoneId()) + ); this.agency_id = removeAgencyPrefix(getAgencyIdFromLeg(nextLeg)); this.from_route_id = removeAgencyPrefix(getRouteIdFromLeg(nextLeg)); this.from_trip_id = removeAgencyPrefix(getTripIdFromLeg(nextLeg)); this.from_stop_id = removeAgencyPrefix(getStopIdFromPlace(nextLeg.from)); this.to_stop_id = removeAgencyPrefix(getStopIdFromPlace(nextLeg.to)); this.from_arrival_time = BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT.format( - nextLeg.getScheduledStartTime().toInstant() + nextLeg.getScheduledStartTime() ); // 1 = Notify, 0 = Cancel. this.msg_type = 1; From 10a32a718ea66488902d3808799ca2ff40ab440b Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 10 Jun 2024 10:51:45 +0100 Subject: [PATCH 16/16] refactor(UsRideGwinnettBusOpNotificationMessage.java): Update to provide the timestamp in UTC --- .../UsRideGwinnettBusOpNotificationMessage.java | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java index 20e3d96c0..1881ad96b 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/busnotifiers/UsRideGwinnettBusOpNotificationMessage.java @@ -3,7 +3,7 @@ import org.opentripplanner.middleware.triptracker.TravelerPosition; import java.time.Instant; -import java.time.ZonedDateTime; +import java.time.ZoneOffset; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.HashMap; @@ -29,10 +29,9 @@ public UsRideGwinnettBusOpNotificationMessage() { // Required for JSON deserialization. } - /** This is the date format required by the API. */ + /** This is the date format required by the API. The date/time must be provided in UTC. */ private static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT = DateTimeFormatter - .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - .withZone(getOtpZoneId()); + .ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); /** This is the time format required by the API. */ public static final DateTimeFormatter BUS_OPERATOR_NOTIFIER_API_TIME_FORMAT = DateTimeFormatter @@ -77,11 +76,9 @@ private static Map createMobilityCodesLookup() { public List mobility_codes; public boolean trusted_companion; - public UsRideGwinnettBusOpNotificationMessage(Instant timestamp, TravelerPosition travelerPosition) { + public UsRideGwinnettBusOpNotificationMessage(Instant currentTime, TravelerPosition travelerPosition) { var nextLeg = travelerPosition.nextLeg; - this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format( - ZonedDateTime.ofInstant(timestamp, getOtpZoneId()) - ); + this.timestamp = BUS_OPERATOR_NOTIFIER_API_DATE_FORMAT.format(currentTime.atZone(ZoneOffset.UTC)); this.agency_id = removeAgencyPrefix(getAgencyIdFromLeg(nextLeg)); this.from_route_id = removeAgencyPrefix(getRouteIdFromLeg(nextLeg)); this.from_trip_id = removeAgencyPrefix(getTripIdFromLeg(nextLeg));