From d08ca52a38ac185a16a24582f09e0e52664e40b1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 20 May 2024 15:30:03 -0400 Subject: [PATCH 01/30] refactor(TravelerLocator): Make getInstruction return TripInstruction instead of string. --- .../triptracker/ManageTripTracking.java | 6 ++++-- .../triptracker/TravelerLocator.java | 21 ++++++------------- .../api/ManageLegTraversalTest.java | 4 ++-- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 10543c3fb..4bb5fb133 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -7,6 +7,7 @@ import org.opentripplanner.middleware.triptracker.response.TrackingResponse; import spark.Request; +import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -68,9 +69,10 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa } // Provide response. + TripInstruction instruction = TravelerLocator.getInstruction(tripStatus, travelerPosition, true); return new TrackingResponse( TRIP_TRACKING_UPDATE_FREQUENCY_SECONDS, - TravelerLocator.getInstruction(tripStatus, travelerPosition, true), + instruction != null ? instruction.build() : NO_INSTRUCTION, trackedJourney.id, tripStatus.name() ); @@ -141,7 +143,7 @@ private static EndTrackingResponse completeJourney(TrackedJourney trackedJourney // Provide response. return new EndTrackingResponse( - TripInstruction.NO_INSTRUCTION, + NO_INSTRUCTION, TripStatus.ENDED.name() ); diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java index 8f7ad20ea..06c8f3b34 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/TravelerLocator.java @@ -10,7 +10,6 @@ import java.util.List; import java.util.stream.Collectors; -import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; 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; @@ -27,27 +26,19 @@ private TravelerLocator() { * Define the instruction based on the traveler's current position compared to expected and nearest points on the * trip. */ - public static String getInstruction( + public static TripInstruction getInstruction( TripStatus tripStatus, TravelerPosition travelerPosition, boolean isStartOfTrip ) { if (hasRequiredWalkLeg(travelerPosition)) { - if (hasRequiredTripStatus(tripStatus)) { - TripInstruction tripInstruction = alignTravelerToTrip(travelerPosition, isStartOfTrip); - if (tripInstruction != null) { - return tripInstruction.build(); - } - } - - if (tripStatus.equals(TripStatus.DEVIATED)) { - TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip); - if (tripInstruction != null) { - return tripInstruction.build(); - } + if (hasRequiredTripStatus(tripStatus)) { // Not DEVIATED and not ENDED. + return alignTravelerToTrip(travelerPosition, isStartOfTrip); + } else if (tripStatus.equals(TripStatus.DEVIATED)) { + return getBackOnTrack(travelerPosition, isStartOfTrip); } } - return NO_INSTRUCTION; + return null; } /** 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 e9d27a2d7..49eddeb8a 100644 --- a/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/controllers/api/ManageLegTraversalTest.java @@ -150,8 +150,8 @@ private static Stream createTrace() { @MethodSource("createTurnByTurnTrace") void canTrackTurnByTurn(TurnTrace turnTrace) { TravelerPosition travelerPosition = new TravelerPosition(turnTrace.itinerary.legs.get(0), turnTrace.position); - String tripInstruction = TravelerLocator.getInstruction(turnTrace.tripStatus, travelerPosition, turnTrace.isStartOfTrip); - assertEquals(turnTrace.expectedInstruction, Objects.requireNonNullElse(tripInstruction, NO_INSTRUCTION), turnTrace.message); + TripInstruction tripInstruction = TravelerLocator.getInstruction(turnTrace.tripStatus, travelerPosition, turnTrace.isStartOfTrip); + assertEquals(turnTrace.expectedInstruction, tripInstruction != null ? tripInstruction.build() : NO_INSTRUCTION, turnTrace.message); } private static Stream createTurnByTurnTrace() { From ab94abae76b83fd039216aea5d5fb9d392439ed6 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 20 May 2024 15:50:23 -0400 Subject: [PATCH 02/30] feat(SegmentsWithInteraction): Add classes for handling segment actions --- .../triptracker/interactions/Interaction.java | 7 ++ .../interactions/SegmentAction.java | 34 ++++++++ .../SegmentsWithInteractions.java | 86 +++++++++++++++++++ .../middleware/utils/JsonUtils.java | 13 +++ 4 files changed, 140 insertions(+) create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java new file mode 100644 index 000000000..9d282353c --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java @@ -0,0 +1,7 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.opentripplanner.middleware.models.OtpUser; + +public interface Interaction { + void triggerAction(SegmentAction segmentAction, OtpUser otpUser); +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java new file mode 100644 index 000000000..2a4555bcc --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java @@ -0,0 +1,34 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.opentripplanner.middleware.triptracker.Segment; +import org.opentripplanner.middleware.utils.Coordinates; + +/** Associates a segment (a pair of coordinates, optionally oriented) to an action or handler. */ +public class SegmentAction { + /** Identifier string for this object. */ + public String id; + + /** The starting coordinated of the segment to which the trigger should be applied. */ + public Coordinates start; + + /** The starting coordinated of the segment to which the trigger should be applied. */ + public Coordinates end; + + /** The fully-qualified Java class to execute. */ + public String trigger; + + public SegmentAction() { + // For persistence. + } + + public SegmentAction(String id, Segment segment) { + this(id, segment, null); + } + + public SegmentAction(String id, Segment segment, String trigger) { + this.id = id; + this.start = segment.start; + this.end = segment.end; + this.trigger = trigger; + } +} diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java new file mode 100644 index 000000000..9b3f600ad --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java @@ -0,0 +1,86 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.otp.response.Step; +import org.opentripplanner.middleware.triptracker.Segment; +import org.opentripplanner.middleware.utils.Coordinates; +import org.opentripplanner.middleware.utils.GeometryUtils; +import org.opentripplanner.middleware.utils.JsonUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; + +/** Holds segments with configured interactions. */ +public class SegmentsWithInteractions { + private static final Logger LOG = LoggerFactory.getLogger(SegmentsWithInteractions.class); + + public static final String DEFAULT_SEGMENTS = "configurations/default/segments.json"; + + public static final SegmentsWithInteractions KNOWN_INTERACTIONS; + + public List segments = new ArrayList<>(); + + static { + try (InputStream stream = new FileInputStream(DEFAULT_SEGMENTS)) { + KNOWN_INTERACTIONS = JsonUtils.getPOJOFromJSON(stream, SegmentsWithInteractions.class); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public SegmentsWithInteractions() { + // For persistence + } + + /** + * @param segment The {@link Segment} to test + * @return The first {@link SegmentAction} found for the given segment + */ + public static SegmentAction getSegmentAction(Segment segment) { + for (SegmentAction a : KNOWN_INTERACTIONS.segments) { + if (segmentMatchesAction(segment, a)) { + return a; + } + } + return null; + } + + public static boolean segmentMatchesAction(Segment segment, SegmentAction action) { + final int MAX_RADIUS = 10; // meters // TODO: get this from config. + return (GeometryUtils.getDistance(segment.start, action.start) <= MAX_RADIUS && GeometryUtils.getDistance(segment.end, action.end) <= MAX_RADIUS) + || + (GeometryUtils.getDistance(segment.start, action.end) <= MAX_RADIUS && GeometryUtils.getDistance(segment.end, action.start) <= MAX_RADIUS); + } + + public static void handleSegmentAction(Segment segment, OtpUser otpUser) { + SegmentAction action = getSegmentAction(segment); + if (action != null) { + try { + Class interactionClass = Class.forName(action.trigger); + Interaction interaction = (Interaction) interactionClass.getDeclaredConstructor().newInstance(); + interaction.triggerAction(action, otpUser); + } catch (Exception e) { + LOG.error("Error instantiating class {}", action.trigger, e); + throw new RuntimeException(e); + } + } + } + + public static void handleSegmentAction(Step step, List steps, OtpUser user) { + int stepIndex = steps.indexOf(step); + if (stepIndex < steps.size() - 1) { + Step stepAfter = steps.get(stepIndex + 1); + Segment segment = new Segment( + new Coordinates(step), + new Coordinates(stepAfter) + ); + handleSegmentAction(segment, user); + } + + } +} diff --git a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java index 56da67c75..b1106697e 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java @@ -16,6 +16,7 @@ import spark.Request; import java.io.IOException; +import java.io.InputStream; import java.util.Arrays; import java.util.List; @@ -70,6 +71,18 @@ public static T getPOJOFromJSON(String json, Class clazz) throws JsonProc } } + /** + * Utility method to parse generic object from stream. + */ + public static T getPOJOFromJSON(InputStream stream, Class clazz) throws IOException { + try { + return mapper.readValue(stream, clazz); + } catch (IOException e) { + LOG.error("Could not parse stream into POJO for class {}", clazz, e); + throw e; + } + } + /** * Check if an {@link HttpResponse} is OK (i.e., the response object not null and HTTP status code is not in the * error range). From 7e3e86f1322a70ea8ddff1b3b4340feb248a51df Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 20 May 2024 16:10:41 -0400 Subject: [PATCH 03/30] refactor(ManageTripTracking): Use leg steps for coordinates to match the segment actions --- .../triptracker/ManageTripTracking.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 4bb5fb133..92d014140 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -1,13 +1,20 @@ package org.opentripplanner.middleware.triptracker; import org.eclipse.jetty.http.HttpStatus; +import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TrackedJourney; +import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.persistence.Persistence; +import org.opentripplanner.middleware.triptracker.interactions.SegmentsWithInteractions; import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse; import org.opentripplanner.middleware.triptracker.response.TrackingResponse; +import org.opentripplanner.middleware.utils.Coordinates; import spark.Request; +import java.util.List; + import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; +import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_UPCOMING_PREFIX; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -70,6 +77,24 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa // Provide response. TripInstruction instruction = TravelerLocator.getInstruction(tripStatus, travelerPosition, true); + + // Perform interactions such as triggering traffic signals when approaching segments so configured. + // It is assumed to be ok to repeatedly perform the interaction. + if (instruction != null && TRIP_INSTRUCTION_UPCOMING_PREFIX.equals(instruction.prefix)) { + OtpUser user = Persistence.otpUsers.getById(tripData.trip.userId); + Step upcomingStep = instruction.legStep; + List steps = travelerPosition.expectedLeg.steps; + int upcomingStepIndex = steps.indexOf(upcomingStep); + if (upcomingStepIndex < steps.size() - 1) { + Step stepAfter = steps.get(upcomingStepIndex + 1); + Segment segment = new Segment( + new Coordinates(upcomingStep), + new Coordinates(stepAfter) + ); + SegmentsWithInteractions.handleSegmentAction(segment, user); + } + } + return new TrackingResponse( TRIP_TRACKING_UPDATE_FREQUENCY_SECONDS, instruction != null ? instruction.build() : NO_INSTRUCTION, From a97317353c1c5bbfaf4ce01f0959c9220efd0312 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 20 May 2024 16:16:05 -0400 Subject: [PATCH 04/30] feat(UsGdotGwinnettTrafficSignalNotifier): Add class for GMAP traffic signals --- .../UsGdotGwinnettTrafficSignalNotifier.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java new file mode 100644 index 000000000..f7e9c2b65 --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -0,0 +1,72 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.eclipse.jetty.http.HttpMethod; +import org.opentripplanner.middleware.models.OtpUser; +import org.opentripplanner.middleware.utils.HttpUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.Map; + +import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsText; + +/** + * Handles notifications to traffic signals managed by + * US Georgia Department of Transportation in Gwinnett County, GA. + */ +public class UsGdotGwinnettTrafficSignalNotifier implements Interaction { + private static final Logger LOG = LoggerFactory.getLogger(UsGdotGwinnettTrafficSignalNotifier.class); + private static final String PED_SIGNAL_CALL_API_HOST = getConfigPropertyAsText("US_GDOT_GWINNETT_PED_SIGNAL_API_HOST"); + private static final String PED_SIGNAL_CALL_API_PATH = getConfigPropertyAsText( + "US_GDOT_GWINNETT_PED_SIGNAL_API_PATH", + "/intersections/%s/crossings/%s/call" + ); + private static final String PED_SIGNAL_CALL_API_KEY = getConfigPropertyAsText("US_GDOT_GWINNETT_PED_SIGNAL_API_KEY"); + + public void triggerAction(SegmentAction segmentAction, OtpUser otpUser) { + String[] idParts = segmentAction.id.split(":"); + String signalId = idParts[0]; + String crossingId = idParts[1]; + triggerPedestrianCall(signalId, crossingId, needsExtendedPhase(otpUser)); + } + + /** Whether a user needs an extended phase or extra time to cross a signaled intersection. */ + public static boolean needsExtendedPhase(OtpUser otpUser) { + // TODO: criteria for extended phase. + return otpUser.mobilityProfile.mobilityMode.equalsIgnoreCase("WChairE"); + } + + /** + * Trigger a pedestrian call for the given traffic signal and given crossing. + * @param signalId The ID of the targeted traffic signal. + * @param crossingId The ID of the crossing to activate at the targeted traffic signal. + */ + public static void triggerPedestrianCall(String signalId, String crossingId, boolean extended) { + if (PED_SIGNAL_CALL_API_HOST == null || PED_SIGNAL_CALL_API_KEY == null) { + LOG.error("Not triggering pedestrian call: Host and key were not configured."); + return; + } + + String pathAndQuery = PED_SIGNAL_CALL_API_HOST + + String.format(PED_SIGNAL_CALL_API_PATH, signalId, crossingId) + + (extended ? "?extended=true" : ""); + try { + Map headers = Map.of("X-API-KEY", PED_SIGNAL_CALL_API_KEY); + var httpResponse = HttpUtils.httpRequestRawResponse( + URI.create(pathAndQuery), + 30, + HttpMethod.POST, + headers, + "" + ); + if (httpResponse.status == 200) { + LOG.info("Triggered pedestrian call {}", pathAndQuery); + } else { + LOG.error("Error {} while triggering pedestrian call", httpResponse.status); + } + } catch (Exception e) { + LOG.error("Could not trigger pedestrian {}", pathAndQuery); + } + } +} From 602331bf2a0f115613d4bb2f787bd5315ae82f23 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 20 May 2024 17:07:21 -0400 Subject: [PATCH 05/30] fix(ManageTripTracking): Include 'immediate' range for triggering, include fix from #234. --- .../middleware/triptracker/ManageTripTracking.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 92d014140..c52b703dc 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -14,6 +14,7 @@ import java.util.List; import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; +import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_IMMEDIATE_PREFIX; import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_UPCOMING_PREFIX; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -76,11 +77,16 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa } // Provide response. - TripInstruction instruction = TravelerLocator.getInstruction(tripStatus, travelerPosition, true); + TripInstruction instruction = TravelerLocator.getInstruction(tripStatus, travelerPosition, create); // Perform interactions such as triggering traffic signals when approaching segments so configured. // It is assumed to be ok to repeatedly perform the interaction. - if (instruction != null && TRIP_INSTRUCTION_UPCOMING_PREFIX.equals(instruction.prefix)) { + if ( + instruction != null && ( + TRIP_INSTRUCTION_UPCOMING_PREFIX.equals(instruction.prefix) || + TRIP_INSTRUCTION_IMMEDIATE_PREFIX.equals(instruction.prefix) + ) + ) { OtpUser user = Persistence.otpUsers.getById(tripData.trip.userId); Step upcomingStep = instruction.legStep; List steps = travelerPosition.expectedLeg.steps; From abb5775561bb6372ff0cdbf73566b4136a90a0c5 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 20 May 2024 17:22:47 -0400 Subject: [PATCH 06/30] refactor(ManageTripTracking): Use instruction distance, leg steps for triggering actions --- .../triptracker/ManageTripTracking.java | 32 ++++--------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index c52b703dc..0ffe55b32 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -1,21 +1,15 @@ package org.opentripplanner.middleware.triptracker; import org.eclipse.jetty.http.HttpStatus; -import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.models.TrackedJourney; -import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.persistence.Persistence; import org.opentripplanner.middleware.triptracker.interactions.SegmentsWithInteractions; import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse; import org.opentripplanner.middleware.triptracker.response.TrackingResponse; -import org.opentripplanner.middleware.utils.Coordinates; import spark.Request; -import java.util.List; - import static org.opentripplanner.middleware.triptracker.TripInstruction.NO_INSTRUCTION; -import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_IMMEDIATE_PREFIX; -import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_UPCOMING_PREFIX; +import static org.opentripplanner.middleware.triptracker.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS; import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt; import static org.opentripplanner.middleware.utils.JsonUtils.logMessageAndHalt; @@ -81,24 +75,12 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa // Perform interactions such as triggering traffic signals when approaching segments so configured. // It is assumed to be ok to repeatedly perform the interaction. - if ( - instruction != null && ( - TRIP_INSTRUCTION_UPCOMING_PREFIX.equals(instruction.prefix) || - TRIP_INSTRUCTION_IMMEDIATE_PREFIX.equals(instruction.prefix) - ) - ) { - OtpUser user = Persistence.otpUsers.getById(tripData.trip.userId); - Step upcomingStep = instruction.legStep; - List steps = travelerPosition.expectedLeg.steps; - int upcomingStepIndex = steps.indexOf(upcomingStep); - if (upcomingStepIndex < steps.size() - 1) { - Step stepAfter = steps.get(upcomingStepIndex + 1); - Segment segment = new Segment( - new Coordinates(upcomingStep), - new Coordinates(stepAfter) - ); - SegmentsWithInteractions.handleSegmentAction(segment, user); - } + if (instruction != null && instruction.distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) { + SegmentsWithInteractions.handleSegmentAction( + instruction.legStep, + travelerPosition.expectedLeg.steps, + Persistence.otpUsers.getById(tripData.trip.userId) + ); } return new TrackingResponse( From 2b0ca8044cd43ba496d9bca4c313a36ffd933a7d Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 10:08:59 -0400 Subject: [PATCH 07/30] refactor(SegmentActions): Rename from SegmentsWithInteractions, use yml instead of json. --- configurations/default/segments.yml | 8 ++++++ .../triptracker/ManageTripTracking.java | 4 +-- ...hInteractions.java => SegmentActions.java} | 25 ++++++++++--------- 3 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 configurations/default/segments.yml rename src/main/java/org/opentripplanner/middleware/triptracker/interactions/{SegmentsWithInteractions.java => SegmentActions.java} (80%) diff --git a/configurations/default/segments.yml b/configurations/default/segments.yml new file mode 100644 index 000000000..929644c83 --- /dev/null +++ b/configurations/default/segments.yml @@ -0,0 +1,8 @@ +- id: 1001:SWSE + start: + lat: 33.95684 + lon: -83.97971 + end: + lat: 33.95653 + lon: -83.97973 + trigger: org.opentripplanner.middleware.triptracker.interactions.UsGdotGwinnettTrafficSignalNotifier diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 0ffe55b32..a1d965a4e 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -3,7 +3,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.persistence.Persistence; -import org.opentripplanner.middleware.triptracker.interactions.SegmentsWithInteractions; +import org.opentripplanner.middleware.triptracker.interactions.SegmentActions; import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse; import org.opentripplanner.middleware.triptracker.response.TrackingResponse; import spark.Request; @@ -76,7 +76,7 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa // Perform interactions such as triggering traffic signals when approaching segments so configured. // It is assumed to be ok to repeatedly perform the interaction. if (instruction != null && instruction.distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) { - SegmentsWithInteractions.handleSegmentAction( + SegmentActions.handleSegmentAction( instruction.legStep, travelerPosition.expectedLeg.steps, Persistence.otpUsers.getById(tripData.trip.userId) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentActions.java similarity index 80% rename from src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java rename to src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentActions.java index 9b3f600ad..95eec5a94 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentsWithInteractions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentActions.java @@ -1,40 +1,41 @@ package org.opentripplanner.middleware.triptracker.interactions; +import com.fasterxml.jackson.databind.JsonNode; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.triptracker.Segment; import org.opentripplanner.middleware.utils.Coordinates; import org.opentripplanner.middleware.utils.GeometryUtils; 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.ArrayList; import java.util.List; /** Holds segments with configured interactions. */ -public class SegmentsWithInteractions { - private static final Logger LOG = LoggerFactory.getLogger(SegmentsWithInteractions.class); +public class SegmentActions { + private static final Logger LOG = LoggerFactory.getLogger(SegmentActions.class); - public static final String DEFAULT_SEGMENTS = "configurations/default/segments.json"; + public static final String DEFAULT_SEGMENTS_FILE = "configurations/default/segments.yml"; - public static final SegmentsWithInteractions KNOWN_INTERACTIONS; - - public List segments = new ArrayList<>(); + private static final List KNOWN_INTERACTIONS; static { - try (InputStream stream = new FileInputStream(DEFAULT_SEGMENTS)) { - KNOWN_INTERACTIONS = JsonUtils.getPOJOFromJSON(stream, SegmentsWithInteractions.class); + try (InputStream stream = new FileInputStream(DEFAULT_SEGMENTS_FILE)) { + JsonNode segmentsYml = YamlUtils.yamlMapper.readTree(stream); + KNOWN_INTERACTIONS = JsonUtils.getPOJOFromJSONAsList(segmentsYml, SegmentAction.class); } catch (IOException e) { + LOG.error("Error parsing segments.yml", e); throw new RuntimeException(e); } } - public SegmentsWithInteractions() { - // For persistence + private SegmentActions() { + // No public constructor } /** @@ -42,7 +43,7 @@ public SegmentsWithInteractions() { * @return The first {@link SegmentAction} found for the given segment */ public static SegmentAction getSegmentAction(Segment segment) { - for (SegmentAction a : KNOWN_INTERACTIONS.segments) { + for (SegmentAction a : KNOWN_INTERACTIONS) { if (segmentMatchesAction(segment, a)) { return a; } From e5acb5200339a00fa0494f3f21bf5dd0bf0793b2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 10:24:11 -0400 Subject: [PATCH 08/30] refactor(JsonUtils): Remove unused code --- .../opentripplanner/middleware/utils/JsonUtils.java | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java index b1106697e..59150f120 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java @@ -71,18 +71,6 @@ public static T getPOJOFromJSON(String json, Class clazz) throws JsonProc } } - /** - * Utility method to parse generic object from stream. - */ - public static T getPOJOFromJSON(InputStream stream, Class clazz) throws IOException { - try { - return mapper.readValue(stream, clazz); - } catch (IOException e) { - LOG.error("Could not parse stream into POJO for class {}", clazz, e); - throw e; - } - } - /** * Check if an {@link HttpResponse} is OK (i.e., the response object not null and HTTP status code is not in the * error range). From 7c45974b132fdab06fc40ad17e703137da57650e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 11:33:32 -0400 Subject: [PATCH 09/30] refactor(TripActions): Rename from SegmentActions --- .../{segments.yml => trip-actions.yml} | 0 .../triptracker/ManageTripTracking.java | 4 ++-- .../{SegmentActions.java => TripActions.java} | 22 +++++++++---------- 3 files changed, 13 insertions(+), 13 deletions(-) rename configurations/default/{segments.yml => trip-actions.yml} (100%) rename src/main/java/org/opentripplanner/middleware/triptracker/interactions/{SegmentActions.java => TripActions.java} (79%) diff --git a/configurations/default/segments.yml b/configurations/default/trip-actions.yml similarity index 100% rename from configurations/default/segments.yml rename to configurations/default/trip-actions.yml diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index a1d965a4e..18d301360 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -3,7 +3,7 @@ import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.TrackedJourney; import org.opentripplanner.middleware.persistence.Persistence; -import org.opentripplanner.middleware.triptracker.interactions.SegmentActions; +import org.opentripplanner.middleware.triptracker.interactions.TripActions; import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse; import org.opentripplanner.middleware.triptracker.response.TrackingResponse; import spark.Request; @@ -76,7 +76,7 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa // Perform interactions such as triggering traffic signals when approaching segments so configured. // It is assumed to be ok to repeatedly perform the interaction. if (instruction != null && instruction.distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) { - SegmentActions.handleSegmentAction( + TripActions.handleSegmentAction( instruction.legStep, travelerPosition.expectedLeg.steps, Persistence.otpUsers.getById(tripData.trip.userId) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java similarity index 79% rename from src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentActions.java rename to src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java index 95eec5a94..7f94b52e8 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java @@ -16,25 +16,25 @@ import java.io.InputStream; import java.util.List; -/** Holds segments with configured interactions. */ -public class SegmentActions { - private static final Logger LOG = LoggerFactory.getLogger(SegmentActions.class); +/** Holds configured trip actions. */ +public class TripActions { + private static final Logger LOG = LoggerFactory.getLogger(TripActions.class); - public static final String DEFAULT_SEGMENTS_FILE = "configurations/default/segments.yml"; + public static final String TRIP_ACTIONS_YML = "configurations/default/trip-actions.yml"; - private static final List KNOWN_INTERACTIONS; + private static final List TRIP_ACTIONS; static { - try (InputStream stream = new FileInputStream(DEFAULT_SEGMENTS_FILE)) { - JsonNode segmentsYml = YamlUtils.yamlMapper.readTree(stream); - KNOWN_INTERACTIONS = JsonUtils.getPOJOFromJSONAsList(segmentsYml, SegmentAction.class); + try (InputStream stream = new FileInputStream(TRIP_ACTIONS_YML)) { + JsonNode tripActionsYml = YamlUtils.yamlMapper.readTree(stream); + TRIP_ACTIONS = JsonUtils.getPOJOFromJSONAsList(tripActionsYml, SegmentAction.class); } catch (IOException e) { - LOG.error("Error parsing segments.yml", e); + LOG.error("Error parsing trip-actions.yml", e); throw new RuntimeException(e); } } - private SegmentActions() { + private TripActions() { // No public constructor } @@ -43,7 +43,7 @@ private SegmentActions() { * @return The first {@link SegmentAction} found for the given segment */ public static SegmentAction getSegmentAction(Segment segment) { - for (SegmentAction a : KNOWN_INTERACTIONS) { + for (SegmentAction a : TRIP_ACTIONS) { if (segmentMatchesAction(segment, a)) { return a; } From 0d0a179f998ac76527a46b35c7600310158c230e Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 15:02:08 -0400 Subject: [PATCH 10/30] docs(README): Add new config params and descrption for trip actions. --- README.md | 39 ++++++++++++++++++++++++++++++ src/main/resources/env.schema.json | 15 ++++++++++++ 2 files changed, 54 insertions(+) diff --git a/README.md b/README.md index b3a675509..b810a92ad 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,42 @@ 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. + +Trip actions are defined in the optional file `trip-actions.yml` in the same configuration folder as `env.yml`. +The file contains a list of actions defined by an ID, start and end coordinates, and a fully-qualified trigger class: + +```yaml +- id: id1 + start: + lat: 33.95684 + lon: -83.97971 + end: + lat: 33.95653 + lon: -83.97973 + trigger: com.example.package.MyTriggerClass +- id: id2 + start: + lat: 33.95584 + lon: -83.97871 + end: + lat: 33.95553 + lon: -83.97873 + trigger: com.example.package.MyTriggerClass +... +``` + +Known trigger classes below are in package `org.opentripplanner.middleware.triptracker.interactions` +and implement its `Interaction` interface: + +| Class | Description | +| ----- | ----------- | +| UsGdotGwinnettTrafficSignalNotifier | Triggers select pedestrian signals 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 +309,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_GDOT_GWINNETT_PED_SIGNAL_API_HOST | string | Optional | http://host.example.com | Host server for the US GDOT Gwinnett County pedestrian signal controller API | +| US_GDOT_GWINNETT_PED_SIGNAL_API_PATH | string | Optional | /intersections/%s/crossings/%s/call | Optional relative path template to trigger a US GDOT Gwinnett County pedestrian signal | +| US_GDOT_GWINNETT_PED_SIGNAL_API_KEY | string | Optional | your-api-key | API key for the US GDOT Gwinnett County pedestrian signal controller | | 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..d896c2fc9 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_GDOT_GWINNETT_PED_SIGNAL_API_HOST": { + "type": "string", + "examples": ["http://host.example.com"], + "description": "Host server for the US GDOT Gwinnett County pedestrian signal controller API" + }, + "US_GDOT_GWINNETT_PED_SIGNAL_API_PATH": { + "type": "string", + "examples": ["/intersections/%s/crossings/%s/call"], + "description": "Optional relative path template to trigger a US GDOT Gwinnett County pedestrian signal" + }, + "US_GDOT_GWINNETT_PED_SIGNAL_API_KEY": { + "type": "string", + "examples": ["your-api-key"], + "description": "API key for the US GDOT Gwinnett County pedestrian signal controller" + }, "VALIDATE_ENVIRONMENT_CONFIG": { "type": "boolean", "examples": ["true"], From 92b9e293cede16d6e6e6bb8889247498ce96c247 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 15:22:20 -0400 Subject: [PATCH 11/30] refactor(JsonUtils): Remove unused import --- .../java/org/opentripplanner/middleware/utils/JsonUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java index 59150f120..56da67c75 100644 --- a/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java +++ b/src/main/java/org/opentripplanner/middleware/utils/JsonUtils.java @@ -16,7 +16,6 @@ import spark.Request; import java.io.IOException; -import java.io.InputStream; import java.util.Arrays; import java.util.List; From bd33032155176ec4a3b37497fe1f9bfa2f44fc49 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 15:22:59 -0400 Subject: [PATCH 12/30] refactor(UsGdotGwinnettTrafficSignalNotifier): Print error object --- .../interactions/UsGdotGwinnettTrafficSignalNotifier.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index f7e9c2b65..76161b37b 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -66,7 +66,7 @@ public static void triggerPedestrianCall(String signalId, String crossingId, boo LOG.error("Error {} while triggering pedestrian call", httpResponse.status); } } catch (Exception e) { - LOG.error("Could not trigger pedestrian {}", pathAndQuery); + LOG.error("Could not trigger pedestrian {}", pathAndQuery, e); } } } From 07dd59ef8b2a3fce12e3aeb724f583b95ea7f869 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 16:42:34 -0400 Subject: [PATCH 13/30] refactor(TripActions): Add segment match test, convert to singleton. --- .../triptracker/ManageTripTracking.java | 2 +- .../interactions/SegmentAction.java | 4 -- .../triptracker/interactions/TripActions.java | 33 +++++----- .../interactions/TripActionsTest.java | 62 +++++++++++++++++++ 4 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 18d301360..bd641bad9 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -76,7 +76,7 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa // Perform interactions such as triggering traffic signals when approaching segments so configured. // It is assumed to be ok to repeatedly perform the interaction. if (instruction != null && instruction.distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) { - TripActions.handleSegmentAction( + TripActions.getDefault().handleSegmentAction( instruction.legStep, travelerPosition.expectedLeg.steps, Persistence.otpUsers.getById(tripData.trip.userId) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java index 2a4555bcc..bcb348e75 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/SegmentAction.java @@ -21,10 +21,6 @@ public SegmentAction() { // For persistence. } - public SegmentAction(String id, Segment segment) { - this(id, segment, null); - } - public SegmentAction(String id, Segment segment, String trigger) { this.id = id; this.start = segment.start; diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java index 7f94b52e8..d6365f65f 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java @@ -22,28 +22,33 @@ public class TripActions { public static final String TRIP_ACTIONS_YML = "configurations/default/trip-actions.yml"; - private static final List TRIP_ACTIONS; + private static TripActions defaultInstance; - static { - try (InputStream stream = new FileInputStream(TRIP_ACTIONS_YML)) { - JsonNode tripActionsYml = YamlUtils.yamlMapper.readTree(stream); - TRIP_ACTIONS = JsonUtils.getPOJOFromJSONAsList(tripActionsYml, SegmentAction.class); - } catch (IOException e) { - LOG.error("Error parsing trip-actions.yml", e); - throw new RuntimeException(e); + private final List segmentActions; + + public static TripActions getDefault() { + if (defaultInstance == null) { + try (InputStream stream = new FileInputStream(TRIP_ACTIONS_YML)) { + JsonNode tripActionsYml = YamlUtils.yamlMapper.readTree(stream); + defaultInstance = new TripActions(JsonUtils.getPOJOFromJSONAsList(tripActionsYml, SegmentAction.class)); + } catch (IOException e) { + LOG.error("Error parsing trip-actions.yml", e); + throw new RuntimeException(e); + } } + return defaultInstance; } - private TripActions() { - // No public constructor + public TripActions(List segmentActions) { + this.segmentActions = segmentActions; } /** * @param segment The {@link Segment} to test * @return The first {@link SegmentAction} found for the given segment */ - public static SegmentAction getSegmentAction(Segment segment) { - for (SegmentAction a : TRIP_ACTIONS) { + public SegmentAction getSegmentAction(Segment segment) { + for (SegmentAction a : segmentActions) { if (segmentMatchesAction(segment, a)) { return a; } @@ -58,7 +63,7 @@ public static boolean segmentMatchesAction(Segment segment, SegmentAction action (GeometryUtils.getDistance(segment.start, action.end) <= MAX_RADIUS && GeometryUtils.getDistance(segment.end, action.start) <= MAX_RADIUS); } - public static void handleSegmentAction(Segment segment, OtpUser otpUser) { + public void handleSegmentAction(Segment segment, OtpUser otpUser) { SegmentAction action = getSegmentAction(segment); if (action != null) { try { @@ -72,7 +77,7 @@ public static void handleSegmentAction(Segment segment, OtpUser otpUser) { } } - public static void handleSegmentAction(Step step, List steps, OtpUser user) { + public void handleSegmentAction(Step step, List steps, OtpUser user) { int stepIndex = steps.indexOf(step); if (stepIndex < steps.size() - 1) { Step stepAfter = steps.get(stepIndex + 1); diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java new file mode 100644 index 000000000..1dc84caba --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java @@ -0,0 +1,62 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.middleware.triptracker.Segment; +import org.opentripplanner.middleware.utils.Coordinates; + +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TripActionsTest { + @ParameterizedTest + @MethodSource("createMatchSegmentCases") + void canMatchSegment(Segment segment, String expectedActionId, String message) { + + TripActions tripActions = new TripActions(List.of( + new SegmentAction( + "segment1", + new Segment( + new Coordinates(33.95684, -83.97971), + new Coordinates(33.95653, -83.97973) + ), + "" + ), + new SegmentAction( + "segment2", + new Segment( + new Coordinates(33.95173, -83.98153), + new Coordinates(33.95154, -83.98121) + ), + "" + ) + )); + + SegmentAction segmentAction = tripActions.getSegmentAction(segment); + assertEquals(expectedActionId, segmentAction != null ? segmentAction.id : null, message); + } + + static Stream createMatchSegmentCases() { + return Stream.of( + Arguments.of( + new Segment( + new Coordinates(33.95444, -83.98013), + new Coordinates(33.95573, -83.97991) + ), + null, + "Segment not near a trip action should not match configured segments" + ), + Arguments.of( + new Segment( + new Coordinates(33.95684, -83.97971), + new Coordinates(33.95653, -83.97973) + ), + "segment1", + "Segment with coords about the same as a trip action should match" + ) + ); + } +} From 35c96629ab11862c84ef7adffb1bc13331e09e36 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 16:59:06 -0400 Subject: [PATCH 14/30] test(TripActions): Add test interaction --- .../interactions/TripActionsTest.java | 52 +++++++++++-------- .../interactions/TrivialTripAction.java | 22 ++++++++ 2 files changed, 53 insertions(+), 21 deletions(-) create mode 100644 src/test/java/org/opentripplanner/middleware/triptracker/interactions/TrivialTripAction.java diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java index 1dc84caba..e3c52ed6d 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java @@ -1,5 +1,6 @@ package org.opentripplanner.middleware.triptracker.interactions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -10,33 +11,42 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; class TripActionsTest { - @ParameterizedTest - @MethodSource("createMatchSegmentCases") - void canMatchSegment(Segment segment, String expectedActionId, String message) { - - TripActions tripActions = new TripActions(List.of( - new SegmentAction( - "segment1", - new Segment( - new Coordinates(33.95684, -83.97971), - new Coordinates(33.95653, -83.97973) - ), - "" + private final TripActions tripActions = new TripActions(List.of( + new SegmentAction( + "segment1", + new Segment( + new Coordinates(33.95684, -83.97971), + new Coordinates(33.95653, -83.97973) ), - new SegmentAction( - "segment2", - new Segment( - new Coordinates(33.95173, -83.98153), - new Coordinates(33.95154, -83.98121) - ), - "" - ) - )); + TrivialTripAction.class.getName() + ), + new SegmentAction( + "segment2", + new Segment( + new Coordinates(33.95173, -83.98153), + new Coordinates(33.95154, -83.98121) + ), + TrivialTripAction.class.getName() + ) + )); + + @BeforeEach + void setUp() { + TrivialTripAction.setLastSegmentId(null); + } + @ParameterizedTest + @MethodSource("createMatchSegmentCases") + void canMatchSegmentAndTriggerAction(Segment segment, String expectedActionId, String message) { SegmentAction segmentAction = tripActions.getSegmentAction(segment); assertEquals(expectedActionId, segmentAction != null ? segmentAction.id : null, message); + + assertNull(TrivialTripAction.getLastSegmentId()); + tripActions.handleSegmentAction(segment, null); + assertEquals(expectedActionId, TrivialTripAction.getLastSegmentId()); } static Stream createMatchSegmentCases() { diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TrivialTripAction.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TrivialTripAction.java new file mode 100644 index 000000000..088d22d6b --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TrivialTripAction.java @@ -0,0 +1,22 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.opentripplanner.middleware.models.OtpUser; + +/** Test interaction class to check that an interaction was triggered. */ +public class TrivialTripAction implements Interaction { + + private static String lastSegmentId; + + public static String getLastSegmentId() { + return lastSegmentId; + } + + public static void setLastSegmentId(String lastSegmentId) { + TrivialTripAction.lastSegmentId = lastSegmentId; + } + + @Override + public void triggerAction(SegmentAction segmentAction, OtpUser otpUser) { + setLastSegmentId(segmentAction.id); + } +} From 22968f083437a1c711926c35e98dec6157102b25 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 17:21:46 -0400 Subject: [PATCH 15/30] refactor(UsGdotGwinnettTrafficSignalNotifier): Move common exception handling to TripActions --- .../triptracker/interactions/Interaction.java | 2 +- .../triptracker/interactions/TripActions.java | 9 ++++-- .../UsGdotGwinnettTrafficSignalNotifier.java | 28 ++++++++----------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java index 9d282353c..39997e0f9 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/Interaction.java @@ -3,5 +3,5 @@ import org.opentripplanner.middleware.models.OtpUser; public interface Interaction { - void triggerAction(SegmentAction segmentAction, OtpUser otpUser); + void triggerAction(SegmentAction segmentAction, OtpUser otpUser) throws Exception; } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java index d6365f65f..f52345354 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java @@ -66,12 +66,17 @@ public static boolean segmentMatchesAction(Segment segment, SegmentAction action public void handleSegmentAction(Segment segment, OtpUser otpUser) { SegmentAction action = getSegmentAction(segment); if (action != null) { + Interaction interaction = null; try { Class interactionClass = Class.forName(action.trigger); - Interaction interaction = (Interaction) interactionClass.getDeclaredConstructor().newInstance(); + interaction = (Interaction) interactionClass.getDeclaredConstructor().newInstance(); interaction.triggerAction(action, otpUser); } catch (Exception e) { - LOG.error("Error instantiating class {}", action.trigger, e); + if (interaction == null) { + LOG.error("Error instantiating class {}", action.trigger, e); + } else { + LOG.error("Could not trigger class {} on action {}", action.trigger, action.id, e); + } throw new RuntimeException(e); } } diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index 76161b37b..0c6579666 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -51,22 +51,18 @@ public static void triggerPedestrianCall(String signalId, String crossingId, boo String pathAndQuery = PED_SIGNAL_CALL_API_HOST + String.format(PED_SIGNAL_CALL_API_PATH, signalId, crossingId) + (extended ? "?extended=true" : ""); - try { - Map headers = Map.of("X-API-KEY", PED_SIGNAL_CALL_API_KEY); - var httpResponse = HttpUtils.httpRequestRawResponse( - URI.create(pathAndQuery), - 30, - HttpMethod.POST, - headers, - "" - ); - if (httpResponse.status == 200) { - LOG.info("Triggered pedestrian call {}", pathAndQuery); - } else { - LOG.error("Error {} while triggering pedestrian call", httpResponse.status); - } - } catch (Exception e) { - LOG.error("Could not trigger pedestrian {}", pathAndQuery, e); + Map headers = Map.of("X-API-KEY", PED_SIGNAL_CALL_API_KEY); + var httpResponse = HttpUtils.httpRequestRawResponse( + URI.create(pathAndQuery), + 30, + HttpMethod.POST, + headers, + "" + ); + if (httpResponse.status == 200) { + LOG.info("Triggered pedestrian call {}", pathAndQuery); + } else { + LOG.error("Error {} while triggering pedestrian call", httpResponse.status); } } } From de8adce3f135b0e29766e2a09b0925cc0e7dc34b Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 18:10:46 -0400 Subject: [PATCH 16/30] test(TripActions): Add tests for handling leg steps. --- .../interactions/TripActionsTest.java | 70 +++++++++++++++---- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java index e3c52ed6d..bb977ea19 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.triptracker.Segment; import org.opentripplanner.middleware.utils.Coordinates; @@ -14,13 +15,15 @@ import static org.junit.jupiter.api.Assertions.assertNull; class TripActionsTest { + public static final Coordinates SEGMENT1_START = new Coordinates(33.95684, -83.97971); + public static final Coordinates SEGMENT1_END = new Coordinates(33.95653, -83.97973); + public static final Coordinates SOME_SEGMENT_END = new Coordinates(33.95573, -83.97991); + public static final Coordinates SOME_SEGMENT_START = new Coordinates(33.95444, -83.98013); + private final TripActions tripActions = new TripActions(List.of( new SegmentAction( "segment1", - new Segment( - new Coordinates(33.95684, -83.97971), - new Coordinates(33.95653, -83.97973) - ), + new Segment(SEGMENT1_START, SEGMENT1_END), TrivialTripAction.class.getName() ), new SegmentAction( @@ -46,26 +49,65 @@ void canMatchSegmentAndTriggerAction(Segment segment, String expectedActionId, S assertNull(TrivialTripAction.getLastSegmentId()); tripActions.handleSegmentAction(segment, null); - assertEquals(expectedActionId, TrivialTripAction.getLastSegmentId()); + assertEquals(expectedActionId, TrivialTripAction.getLastSegmentId(), message); } static Stream createMatchSegmentCases() { return Stream.of( Arguments.of( - new Segment( - new Coordinates(33.95444, -83.98013), - new Coordinates(33.95573, -83.97991) - ), + new Segment(SOME_SEGMENT_START, SOME_SEGMENT_END), null, "Segment not near a trip action should not match configured segments" ), Arguments.of( - new Segment( - new Coordinates(33.95684, -83.97971), - new Coordinates(33.95653, -83.97973) - ), + new Segment(SEGMENT1_START, SEGMENT1_END), "segment1", - "Segment with coords about the same as a trip action should match" + "Segment with coordinates about the same as a trip action should match" + ) + ); + } + + private Step createStep(Coordinates coords) { + Step step = new Step(); + step.lat = coords.lat; + step.lon = coords.lon; + return step; + } + + @ParameterizedTest + @MethodSource("createMatchSegmentFromLegStepsCases") + void canMatchSegmentFromLegSteps(int stepIndex, String expectedActionId, String message) { + Step stepBeforeSegment1 = createStep(SOME_SEGMENT_END); + Step stepOnSegment1 = createStep(SEGMENT1_START); + Step stepAfterSegment1 = createStep(SEGMENT1_END); + + List steps = List.of( + stepBeforeSegment1, + stepOnSegment1, + stepAfterSegment1 + ); + + assertNull(TrivialTripAction.getLastSegmentId()); + tripActions.handleSegmentAction(steps.get(stepIndex), steps, null); + assertEquals(expectedActionId, TrivialTripAction.getLastSegmentId(), message); + } + + static Stream createMatchSegmentFromLegStepsCases() { + return Stream.of( + Arguments.of( + 0, + null, + "Leg step not near a trip action should not match configured segments" + ), + Arguments.of( + 1, + "segment1", + "Leg step with coordinates about the same as a trip action should match" + ), + Arguments.of( + 2, + null, + "Leg step not near a trip action should not match configured segments" ) ); } From acea38f7a24814b076248358e36c2a79d8132ff6 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 21 May 2024 18:18:10 -0400 Subject: [PATCH 17/30] test(TripActions): Add variation to leg step coordinates --- .../middleware/triptracker/interactions/TripActionsTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java index bb977ea19..b91b3575a 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java @@ -69,8 +69,9 @@ static Stream createMatchSegmentCases() { private Step createStep(Coordinates coords) { Step step = new Step(); - step.lat = coords.lat; - step.lon = coords.lon; + // Add tiny offsets to test the threshold. + step.lat = coords.lat + 0.00001; + step.lon = coords.lon + 0.00001; return step; } From 7d929b5af9657e3528f41e8c7d4f00143293f9be Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 28 May 2024 11:18:25 -0400 Subject: [PATCH 18/30] test(UsGdotGw....Notifier) Update ext phase criteria, add tests. --- .../UsGdotGwinnettTrafficSignalNotifier.java | 8 ++- ...GdotGwinnettTrafficSignalNotifierTest.java | 63 +++++++++++++++++++ 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index 0c6579666..bfd103d85 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.triptracker.interactions; import org.eclipse.jetty.http.HttpMethod; +import org.opentripplanner.middleware.models.MobilityProfile; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.utils.HttpUtils; import org.slf4j.Logger; @@ -33,8 +34,11 @@ public void triggerAction(SegmentAction segmentAction, OtpUser otpUser) { /** Whether a user needs an extended phase or extra time to cross a signaled intersection. */ public static boolean needsExtendedPhase(OtpUser otpUser) { - // TODO: criteria for extended phase. - return otpUser.mobilityProfile.mobilityMode.equalsIgnoreCase("WChairE"); + MobilityProfile profile = otpUser.mobilityProfile; + if (profile == null) return false; + + String mode = profile.mobilityMode; + return mode != null && !mode.equalsIgnoreCase("None"); } /** diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java new file mode 100644 index 000000000..1b64d91b0 --- /dev/null +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java @@ -0,0 +1,63 @@ +package org.opentripplanner.middleware.triptracker.interactions; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.opentripplanner.middleware.models.MobilityProfile; +import org.opentripplanner.middleware.models.OtpUser; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +class UsGdotGwinnettTrafficSignalNotifierTest { + + private static final String EXTENDED_PHASE_MESSAGE = + "Extended phase should always be requested, except for the 'None' and null profiles"; + + @Test + void testNeedsExtendedPhaseNullProfileObj() { + OtpUser otpUser = new OtpUser(); + otpUser.mobilityProfile = null; + assertFalse(UsGdotGwinnettTrafficSignalNotifier.needsExtendedPhase(otpUser), EXTENDED_PHASE_MESSAGE); + } + + @ParameterizedTest + @MethodSource("createNeedsExtendedPhaseCases") + void testNeedsExtendedPhase(String mobilityMode, boolean result) { + MobilityProfile mobilityProfile = new MobilityProfile(); + mobilityProfile.mobilityMode = mobilityMode; + OtpUser otpUser = new OtpUser(); + otpUser.mobilityProfile = mobilityProfile; + + assertEquals(result, UsGdotGwinnettTrafficSignalNotifier.needsExtendedPhase(otpUser), EXTENDED_PHASE_MESSAGE); + } + + static Stream createNeedsExtendedPhaseCases() { + return Stream.of( + null, + "None", + "Some", + "Device", + "WChairM", + "WChairE", + "MScooter", + "LowVision", + "Blind", + "Some-LowVision", + "Device-LowVision", + "WChairM-LowVision", + "WChairE-LowVision", + "MScooter-LowVision", + "Some-Blind", + "Device-Blind", + "WChairM-Blind", + "WChairE-Blind", + "MScooter-Blind" + ).map( + m -> Arguments.of(m, m != null && !m.equals("None")) + ); + } +} From 21d34e0d7ff9f7245e16e68974e9c8f7d6f358d5 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 28 May 2024 16:59:43 -0400 Subject: [PATCH 19/30] refactor(UsGdotGw....Notifier) Support injecting test host and keys, add tests. --- .../UsGdotGwinnettTrafficSignalNotifier.java | 47 +++++++++++++++---- ...GdotGwinnettTrafficSignalNotifierTest.java | 28 +++++++++++ 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index bfd103d85..958ca460b 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -25,6 +25,23 @@ public class UsGdotGwinnettTrafficSignalNotifier implements Interaction { ); private static final String PED_SIGNAL_CALL_API_KEY = getConfigPropertyAsText("US_GDOT_GWINNETT_PED_SIGNAL_API_KEY"); + private final String host; + private final String path; + private final String key; + + public UsGdotGwinnettTrafficSignalNotifier() { + host = PED_SIGNAL_CALL_API_HOST; + path = PED_SIGNAL_CALL_API_PATH; + key = PED_SIGNAL_CALL_API_KEY; + } + + public UsGdotGwinnettTrafficSignalNotifier(String host, String path, String key) { + this.host = host; + this.path = path; + this.key = key; + } + + @Override public void triggerAction(SegmentAction segmentAction, OtpUser otpUser) { String[] idParts = segmentAction.id.split(":"); String signalId = idParts[0]; @@ -41,32 +58,42 @@ public static boolean needsExtendedPhase(OtpUser otpUser) { return mode != null && !mode.equalsIgnoreCase("None"); } + /** */ + public String getUrl(String signalId, String crossingId, boolean extended) { + return host + String.format(path, signalId, crossingId) + (extended ? "?extended=true" : ""); + } + + public Map getHeaders() { + return Map.of("X-API-KEY", key); + } + + public String getBody() { + return ""; + } + /** * Trigger a pedestrian call for the given traffic signal and given crossing. * @param signalId The ID of the targeted traffic signal. * @param crossingId The ID of the crossing to activate at the targeted traffic signal. */ - public static void triggerPedestrianCall(String signalId, String crossingId, boolean extended) { - if (PED_SIGNAL_CALL_API_HOST == null || PED_SIGNAL_CALL_API_KEY == null) { - LOG.error("Not triggering pedestrian call: Host and key were not configured."); + public void triggerPedestrianCall(String signalId, String crossingId, boolean extended) { + if (host == null || key == null) { + LOG.error("Not triggering pedestrian call: Host and key are not configured."); return; } - String pathAndQuery = PED_SIGNAL_CALL_API_HOST + - String.format(PED_SIGNAL_CALL_API_PATH, signalId, crossingId) + - (extended ? "?extended=true" : ""); - Map headers = Map.of("X-API-KEY", PED_SIGNAL_CALL_API_KEY); + String pathAndQuery = getUrl(signalId, crossingId, extended); var httpResponse = HttpUtils.httpRequestRawResponse( URI.create(pathAndQuery), 30, HttpMethod.POST, - headers, - "" + getHeaders(), + getBody() ); if (httpResponse.status == 200) { LOG.info("Triggered pedestrian call {}", pathAndQuery); } else { - LOG.error("Error {} while triggering pedestrian call", httpResponse.status); + LOG.error("Error {} while triggering pedestrian call {}", httpResponse.status, pathAndQuery); } } } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java index 1b64d91b0..e8d2f437c 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java @@ -7,15 +7,18 @@ import org.opentripplanner.middleware.models.MobilityProfile; import org.opentripplanner.middleware.models.OtpUser; +import java.util.Map; 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; class UsGdotGwinnettTrafficSignalNotifierTest { private static final String EXTENDED_PHASE_MESSAGE = "Extended phase should always be requested, except for the 'None' and null profiles"; + public static final String DUMMY_KEY = "secret"; @Test void testNeedsExtendedPhaseNullProfileObj() { @@ -60,4 +63,29 @@ static Stream createNeedsExtendedPhaseCases() { m -> Arguments.of(m, m != null && !m.equals("None")) ); } + + private static UsGdotGwinnettTrafficSignalNotifier createNotifier() { + return new UsGdotGwinnettTrafficSignalNotifier( + "http://pedsignal.example.com", + "/signal-path/%s/crossing-path/%s/trigger-path", + DUMMY_KEY + ); + } + + @Test + void testGetUrl() { + String signalId = "signal-12"; + String crossingId = "crossing-114"; + UsGdotGwinnettTrafficSignalNotifier notifier = createNotifier(); + assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path", notifier.getUrl(signalId, crossingId, false)); + assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path?extended=true", notifier.getUrl(signalId, crossingId, true)); + } + + @Test + void testGetHeaders() { + Map headers = createNotifier().getHeaders(); + assertEquals(1, headers.size()); + assertTrue(headers.containsKey("X-API-KEY")); + assertEquals(DUMMY_KEY, headers.get("X-API-KEY")); + } } From a6ea84ce47161f112108a6b3ad60fbcd3c72d01f Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 29 May 2024 13:55:39 -0400 Subject: [PATCH 20/30] refactor(UsGdotGw....Notifier) Prevent requests to queue up, add tests. --- .../UsGdotGwinnettTrafficSignalNotifier.java | 49 +++++++++++++------ .../middleware/utils/AtomicAvailability.java | 24 +++++++++ ...GdotGwinnettTrafficSignalNotifierTest.java | 28 +++++++++-- 3 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 src/main/java/org/opentripplanner/middleware/utils/AtomicAvailability.java diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index 958ca460b..2f536bd1a 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -4,6 +4,7 @@ import org.opentripplanner.middleware.models.MobilityProfile; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.utils.HttpUtils; +import org.opentripplanner.middleware.utils.AtomicAvailability; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,20 +26,25 @@ public class UsGdotGwinnettTrafficSignalNotifier implements Interaction { ); private static final String PED_SIGNAL_CALL_API_KEY = getConfigPropertyAsText("US_GDOT_GWINNETT_PED_SIGNAL_API_KEY"); + private static final AtomicAvailability availability = new AtomicAvailability(); + private final String host; private final String path; private final String key; + private final boolean isTesting; public UsGdotGwinnettTrafficSignalNotifier() { host = PED_SIGNAL_CALL_API_HOST; path = PED_SIGNAL_CALL_API_PATH; key = PED_SIGNAL_CALL_API_KEY; + isTesting = false; } public UsGdotGwinnettTrafficSignalNotifier(String host, String path, String key) { this.host = host; this.path = path; this.key = key; + this.isTesting = true; } @Override @@ -58,7 +64,6 @@ public static boolean needsExtendedPhase(OtpUser otpUser) { return mode != null && !mode.equalsIgnoreCase("None"); } - /** */ public String getUrl(String signalId, String crossingId, boolean extended) { return host + String.format(path, signalId, crossingId) + (extended ? "?extended=true" : ""); } @@ -76,24 +81,38 @@ public String getBody() { * @param signalId The ID of the targeted traffic signal. * @param crossingId The ID of the crossing to activate at the targeted traffic signal. */ - public void triggerPedestrianCall(String signalId, String crossingId, boolean extended) { + public boolean triggerPedestrianCall(String signalId, String crossingId, boolean extended) { if (host == null || key == null) { LOG.error("Not triggering pedestrian call: Host and key are not configured."); - return; + return false; } - String pathAndQuery = getUrl(signalId, crossingId, extended); - var httpResponse = HttpUtils.httpRequestRawResponse( - URI.create(pathAndQuery), - 30, - HttpMethod.POST, - getHeaders(), - getBody() - ); - if (httpResponse.status == 200) { - LOG.info("Triggered pedestrian call {}", pathAndQuery); - } else { - LOG.error("Error {} while triggering pedestrian call {}", httpResponse.status, pathAndQuery); + if (availability.claim()) { + try (availability) { + if (isTesting) { + // For testing, just wait so that other calls can be attempted. + Thread.sleep(2000); + return true; + } else { + String pathAndQuery = getUrl(signalId, crossingId, extended); + var httpResponse = HttpUtils.httpRequestRawResponse( + URI.create(pathAndQuery), + 30, + HttpMethod.POST, + getHeaders(), + getBody() + ); + if (httpResponse.status == 200) { + LOG.info("Triggered pedestrian call {}", pathAndQuery); + return true; + } else { + LOG.error("Error {} while triggering pedestrian call {}", httpResponse.status, pathAndQuery); + } + } + } catch (InterruptedException e) { + // Continue with the rest. + } } + return false; } } diff --git a/src/main/java/org/opentripplanner/middleware/utils/AtomicAvailability.java b/src/main/java/org/opentripplanner/middleware/utils/AtomicAvailability.java new file mode 100644 index 000000000..7eb56f02a --- /dev/null +++ b/src/main/java/org/opentripplanner/middleware/utils/AtomicAvailability.java @@ -0,0 +1,24 @@ +package org.opentripplanner.middleware.utils; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Simple {@link AtomicBoolean} wrapper so that only one thread at a time can assert a claim (e.g. on web requests). + * The claim method can be used in try/finally blocks to ensure that the claim is reset in exception workflows. + */ +public class AtomicAvailability implements AutoCloseable { + private final AtomicBoolean claimed = new AtomicBoolean(); + + public boolean claim() { + return claimed.compareAndSet(false, true); + } + + public void release() { + claimed.set(false); + } + + @Override + public void close() { + release(); + } +} diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java index e8d2f437c..22a7d9eda 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java @@ -16,6 +16,8 @@ class UsGdotGwinnettTrafficSignalNotifierTest { + public static final String SIGNAL_ID = "signal-12"; + public static final String CROSSING_ID = "crossing-114"; private static final String EXTENDED_PHASE_MESSAGE = "Extended phase should always be requested, except for the 'None' and null profiles"; public static final String DUMMY_KEY = "secret"; @@ -74,11 +76,9 @@ private static UsGdotGwinnettTrafficSignalNotifier createNotifier() { @Test void testGetUrl() { - String signalId = "signal-12"; - String crossingId = "crossing-114"; UsGdotGwinnettTrafficSignalNotifier notifier = createNotifier(); - assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path", notifier.getUrl(signalId, crossingId, false)); - assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path?extended=true", notifier.getUrl(signalId, crossingId, true)); + assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path", notifier.getUrl(SIGNAL_ID, CROSSING_ID, false)); + assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path?extended=true", notifier.getUrl(SIGNAL_ID, CROSSING_ID, true)); } @Test @@ -88,4 +88,24 @@ void testGetHeaders() { assertTrue(headers.containsKey("X-API-KEY")); assertEquals(DUMMY_KEY, headers.get("X-API-KEY")); } + + @Test + void testMultipleInvocations() throws InterruptedException { + // Start a separate thread that simulates a long-running request. + Thread thread = new Thread(() -> { + UsGdotGwinnettTrafficSignalNotifier notifier = createNotifier(); + notifier.triggerPedestrianCall(SIGNAL_ID, CROSSING_ID, true); + }); + thread.start(); + + // Attempt to request pedestrian signals after a brief wait, these attempts should fail and return immediately. + Thread.sleep(200); + + for (int i = 0; i < 4; i++) { + UsGdotGwinnettTrafficSignalNotifier notifier = createNotifier(); + assertFalse(notifier.triggerPedestrianCall(SIGNAL_ID, CROSSING_ID, true)); + } + + thread.join(10000); + } } From 726eaa9132a0b39eb8536653cdeae8bfab5902af Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 29 May 2024 14:45:38 -0400 Subject: [PATCH 21/30] style(UsGdotGw....Notifier) Reformat class and tests. --- .../UsGdotGwinnettTrafficSignalNotifier.java | 8 ++++++-- .../UsGdotGwinnettTrafficSignalNotifierTest.java | 10 ++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index 2f536bd1a..7d7dccb63 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -19,12 +19,16 @@ */ public class UsGdotGwinnettTrafficSignalNotifier implements Interaction { private static final Logger LOG = LoggerFactory.getLogger(UsGdotGwinnettTrafficSignalNotifier.class); - private static final String PED_SIGNAL_CALL_API_HOST = getConfigPropertyAsText("US_GDOT_GWINNETT_PED_SIGNAL_API_HOST"); + private static final String PED_SIGNAL_CALL_API_HOST = getConfigPropertyAsText( + "US_GDOT_GWINNETT_PED_SIGNAL_API_HOST" + ); private static final String PED_SIGNAL_CALL_API_PATH = getConfigPropertyAsText( "US_GDOT_GWINNETT_PED_SIGNAL_API_PATH", "/intersections/%s/crossings/%s/call" ); - private static final String PED_SIGNAL_CALL_API_KEY = getConfigPropertyAsText("US_GDOT_GWINNETT_PED_SIGNAL_API_KEY"); + private static final String PED_SIGNAL_CALL_API_KEY = getConfigPropertyAsText( + "US_GDOT_GWINNETT_PED_SIGNAL_API_KEY" + ); private static final AtomicAvailability availability = new AtomicAvailability(); diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java index 22a7d9eda..3d1385f1d 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifierTest.java @@ -77,8 +77,14 @@ private static UsGdotGwinnettTrafficSignalNotifier createNotifier() { @Test void testGetUrl() { UsGdotGwinnettTrafficSignalNotifier notifier = createNotifier(); - assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path", notifier.getUrl(SIGNAL_ID, CROSSING_ID, false)); - assertEquals("http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path?extended=true", notifier.getUrl(SIGNAL_ID, CROSSING_ID, true)); + assertEquals( + "http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path", + notifier.getUrl(SIGNAL_ID, CROSSING_ID, false) + ); + assertEquals( + "http://pedsignal.example.com/signal-path/signal-12/crossing-path/crossing-114/trigger-path?extended=true", + notifier.getUrl(SIGNAL_ID, CROSSING_ID, true) + ); } @Test From 7cd15b5e226e8ddd45b6ae00ca367131f71b79a8 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 29 May 2024 16:36:16 -0400 Subject: [PATCH 22/30] fix(TripActions) Handle walk step to destination in a Leg. --- .../triptracker/interactions/TripActions.java | 3 +-- .../interactions/TripActionsTest.java | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java index f52345354..33a27bc42 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java @@ -84,7 +84,7 @@ public void handleSegmentAction(Segment segment, OtpUser otpUser) { public void handleSegmentAction(Step step, List steps, OtpUser user) { int stepIndex = steps.indexOf(step); - if (stepIndex < steps.size() - 1) { + if (stepIndex >= 0 && stepIndex < steps.size() - 1) { Step stepAfter = steps.get(stepIndex + 1); Segment segment = new Segment( new Coordinates(step), @@ -92,6 +92,5 @@ public void handleSegmentAction(Step step, List steps, OtpUser user) { ); handleSegmentAction(segment, user); } - } } diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java index b91b3575a..37e63eaf5 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/interactions/TripActionsTest.java @@ -8,6 +8,7 @@ import org.opentripplanner.middleware.triptracker.Segment; import org.opentripplanner.middleware.utils.Coordinates; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -82,14 +83,15 @@ void canMatchSegmentFromLegSteps(int stepIndex, String expectedActionId, String Step stepOnSegment1 = createStep(SEGMENT1_START); Step stepAfterSegment1 = createStep(SEGMENT1_END); - List steps = List.of( - stepBeforeSegment1, - stepOnSegment1, - stepAfterSegment1 - ); + // We use an ArrayList here (and not List.of(...)) to match runtime parsing of the OTP response. + List steps = new ArrayList<>(); + steps.add(stepBeforeSegment1); + steps.add(stepOnSegment1); + steps.add(stepAfterSegment1); assertNull(TrivialTripAction.getLastSegmentId()); - tripActions.handleSegmentAction(steps.get(stepIndex), steps, null); + Step step = stepIndex == -1 ? null : steps.get(stepIndex); + tripActions.handleSegmentAction(step, steps, null); assertEquals(expectedActionId, TrivialTripAction.getLastSegmentId(), message); } @@ -109,6 +111,11 @@ static Stream createMatchSegmentFromLegStepsCases() { 2, null, "Leg step not near a trip action should not match configured segments" + ), + Arguments.of( + -1, + null, + "Null leg step not near a trip action should not match configured segments (or cause errors)" ) ); } From 176b5a9acdf5fe2ce09546b71e3673b77470860a Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 31 May 2024 10:07:11 -0400 Subject: [PATCH 23/30] refactor(UsGdotGw...Notifier) Address PR feedback. --- .../UsGdotGwinnettTrafficSignalNotifier.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index 7d7dccb63..a43a265f7 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -1,6 +1,7 @@ package org.opentripplanner.middleware.triptracker.interactions; import org.eclipse.jetty.http.HttpMethod; +import org.eclipse.jetty.http.HttpStatus; import org.opentripplanner.middleware.models.MobilityProfile; import org.opentripplanner.middleware.models.OtpUser; import org.opentripplanner.middleware.utils.HttpUtils; @@ -76,10 +77,6 @@ public Map getHeaders() { return Map.of("X-API-KEY", key); } - public String getBody() { - return ""; - } - /** * Trigger a pedestrian call for the given traffic signal and given crossing. * @param signalId The ID of the targeted traffic signal. @@ -104,9 +101,9 @@ public boolean triggerPedestrianCall(String signalId, String crossingId, boolean 30, HttpMethod.POST, getHeaders(), - getBody() + "" ); - if (httpResponse.status == 200) { + if (httpResponse.status == HttpStatus.OK_200) { LOG.info("Triggered pedestrian call {}", pathAndQuery); return true; } else { From f4b02e2ee2eb7481ddbf858e70213632ca28f3f2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 11 Jun 2024 11:18:18 -0400 Subject: [PATCH 24/30] test(NotifyBusOperator): Adjust types for unit tests. --- .../triptracker/NotifyBusOperatorTest.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java index 87456f799..c8ab41071 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/NotifyBusOperatorTest.java @@ -31,6 +31,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.opentripplanner.middleware.triptracker.TravelerLocator.ACCEPTABLE_AHEAD_OF_SCHEDULE_IN_MINUTES; import static org.opentripplanner.middleware.triptracker.TravelerLocator.getBusDepartureTime; @@ -74,11 +75,14 @@ void canNotifyBusOperatorForScheduledDeparture() { Instant busDepartureTime = getBusDepartureTime(busLeg); trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates(), busDepartureTime); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, walkToBusTransition, createOtpUser()); - String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); + + TripInstruction tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); + assertNotNull(tripInstruction); + TripInstruction expectInstruction = new TripInstruction(busLeg, busDepartureTime, locale); TrackedJourney updated = Persistence.trackedJourneys.getById(trackedJourney.id); assertTrue(updated.busNotificationMessages.containsKey(routeId)); - assertEquals(expectInstruction.build(), tripInstruction); + assertEquals(expectInstruction.build(), tripInstruction.build()); } @Test @@ -93,11 +97,12 @@ void canNotifyBusOperatorForDelayedDeparture() throws CloneNotSupportedException trackedJourney = createAndPersistTrackedJourney(getEndOfWalkLegCoordinates(), timeAtEndOfWalkLeg); TravelerPosition travelerPosition = new TravelerPosition(trackedJourney, itinerary, createOtpUser()); - String tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); + TripInstruction tripInstruction = TravelerLocator.getInstruction(TripStatus.ON_SCHEDULE, travelerPosition, false); + assertNotNull(tripInstruction); Leg busLeg = itinerary.legs.get(1); TripInstruction expectInstruction = new TripInstruction(busLeg, timeAtEndOfWalkLeg, locale); - assertEquals(expectInstruction.build(), tripInstruction); + assertEquals(expectInstruction.build(), tripInstruction.build()); } @Test From df74d3b8c466de6cc8b17f5ec3bc07df6ff495d1 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Thu, 27 Jun 2024 10:43:43 -0400 Subject: [PATCH 25/30] refactor(ManageLegTraversal): Remove unused imports --- .../middleware/triptracker/ManageLegTraversalTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java index 97e42241b..8d7d42e3c 100644 --- a/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java +++ b/src/test/java/org/opentripplanner/middleware/triptracker/ManageLegTraversalTest.java @@ -7,19 +7,12 @@ 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; import org.opentripplanner.middleware.otp.response.Step; import org.opentripplanner.middleware.testutils.CommonTestUtils; -import org.opentripplanner.middleware.triptracker.TripInstruction; -import org.opentripplanner.middleware.triptracker.LegSegment; -import org.opentripplanner.middleware.triptracker.TrackingLocation; -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; @@ -32,7 +25,6 @@ import java.util.Date; import java.util.List; import java.util.Locale; -import java.util.Objects; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; From 018af7856fa93b75f86b4ceb828ee4195e9c63e2 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Wed, 4 Dec 2024 18:05:17 -0500 Subject: [PATCH 26/30] refactor(UsGdot...Notifier): Improve logs. --- .../UsGdotGwinnettTrafficSignalNotifier.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java index a43a265f7..73a5b2d4a 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/UsGdotGwinnettTrafficSignalNotifier.java @@ -96,6 +96,7 @@ public boolean triggerPedestrianCall(String signalId, String crossingId, boolean return true; } else { String pathAndQuery = getUrl(signalId, crossingId, extended); + LOG.info("About to trigger pedestrian call at {}", pathAndQuery); var httpResponse = HttpUtils.httpRequestRawResponse( URI.create(pathAndQuery), 30, @@ -103,15 +104,18 @@ public boolean triggerPedestrianCall(String signalId, String crossingId, boolean getHeaders(), "" ); - if (httpResponse.status == HttpStatus.OK_200) { - LOG.info("Triggered pedestrian call {}", pathAndQuery); + if (httpResponse != null && httpResponse.status == HttpStatus.OK_200) { + LOG.info("Triggered pedestrian call at {}", pathAndQuery); return true; + } else if (httpResponse == null) { + LOG.error("Unable to reach Ped-X server at {}", pathAndQuery); } else { - LOG.error("Error {} while triggering pedestrian call {}", httpResponse.status, pathAndQuery); + LOG.error("Error {} while triggering pedestrian call at {}", httpResponse.status, pathAndQuery); } } } catch (InterruptedException e) { - // Continue with the rest. + // Log and continue with the rest. + LOG.error("Ped-X request was interrupted: {}", e.getMessage()); } } return false; From c5cf9439ae3df323636c840a22d6dbef9a0709a3 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:32:26 -0500 Subject: [PATCH 27/30] fix(typeform/Response): Handle null answers. --- .../middleware/typeform/Response.java | 8 +++++- .../middleware/typeform/ResponseTest.java | 25 ++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/opentripplanner/middleware/typeform/Response.java b/src/main/java/org/opentripplanner/middleware/typeform/Response.java index 9136682c2..c5d7e9aeb 100644 --- a/src/main/java/org/opentripplanner/middleware/typeform/Response.java +++ b/src/main/java/org/opentripplanner/middleware/typeform/Response.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; /** Data structure for TypeForm survey responses. Only including relevant fields. */ @@ -83,7 +84,12 @@ public String toCsvRow() { hidden.notification_id, hidden.trip_id, hidden.user_id, - answers.stream().map(Answer::toCsvContent).collect(Collectors.joining(",")) + answers == null + ? "" + : answers.stream() + .filter(Objects::nonNull) + .map(Answer::toCsvContent) + .collect(Collectors.joining(",")) ); } } diff --git a/src/test/java/org/opentripplanner/middleware/typeform/ResponseTest.java b/src/test/java/org/opentripplanner/middleware/typeform/ResponseTest.java index 821b7ba9a..03c5cc0e4 100644 --- a/src/test/java/org/opentripplanner/middleware/typeform/ResponseTest.java +++ b/src/test/java/org/opentripplanner/middleware/typeform/ResponseTest.java @@ -1,20 +1,33 @@ package org.opentripplanner.middleware.typeform; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import java.util.List; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; public class ResponseTest { - public static final String EXPECTED_CSV_ROW = "response-id-0,completed,2024-10-25T15:37:42Z,2024-10-25T15:46:27Z,notification-id-1,trip-id-2,user-id-3,\"Field1 choice\",\"Field2, ChoiceA;Field2, ChoiceB\",\"Field3 answer\""; + public static final String EXPECTED_CSV_BEGINNING = "response-id-0,completed,2024-10-25T15:37:42Z,2024-10-25T15:46:27Z,notification-id-1,trip-id-2,user-id-3,"; + public static final String EXPECTED_CSV_ENDING = "\"Field1 choice\",\"Field2, ChoiceA;Field2, ChoiceB\",\"Field3 answer\""; + public static final String EXPECTED_CSV_ROW = EXPECTED_CSV_BEGINNING + EXPECTED_CSV_ENDING; + @ParameterizedTest + @MethodSource("createToCsvRowCases") + void toCsvRow(Response response, String expected) { + assertEquals(expected, response.toCsvRow()); + } - @Test - void toCsvRow() { - Response response = makeResponse(); - assertEquals(EXPECTED_CSV_ROW, response.toCsvRow()); + private static Stream createToCsvRowCases() { + Response responseWithoutAnswers = makeResponse(); + responseWithoutAnswers.answers = null; + return Stream.of( + Arguments.of(makeResponse(), EXPECTED_CSV_ROW), + Arguments.of(responseWithoutAnswers, EXPECTED_CSV_BEGINNING) + ); } public static Response makeResponse() { From fb64da87a12980799e63680f6f4cfdb0677c21c0 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Mon, 9 Dec 2024 18:17:11 -0500 Subject: [PATCH 28/30] refactor(TripActions): Add logging. --- .../middleware/triptracker/interactions/TripActions.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java index 33a27bc42..ec36a6e4e 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/interactions/TripActions.java @@ -64,8 +64,10 @@ public static boolean segmentMatchesAction(Segment segment, SegmentAction action } public void handleSegmentAction(Segment segment, OtpUser otpUser) { + LOG.info("Looking for segment action: start: {}, end: {}", segment.start, segment.end); SegmentAction action = getSegmentAction(segment); if (action != null) { + LOG.info("Found segment action: {}", action.id); Interaction interaction = null; try { Class interactionClass = Class.forName(action.trigger); @@ -84,6 +86,7 @@ public void handleSegmentAction(Segment segment, OtpUser otpUser) { public void handleSegmentAction(Step step, List steps, OtpUser user) { int stepIndex = steps.indexOf(step); + LOG.info("Looking for segment actions: step {}/{}", stepIndex, steps.size()); if (stepIndex >= 0 && stepIndex < steps.size() - 1) { Step stepAfter = steps.get(stepIndex + 1); Segment segment = new Segment( From dd2f246d2b04f738bcaf3aff1b60510fa172d6c4 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:56:34 -0500 Subject: [PATCH 29/30] fix(ManageTripTracking): Use profile of primary traveler. --- .../middleware/models/MonitoredTrip.java | 8 +++++ .../triptracker/ManageTripTracking.java | 2 +- .../middleware/models/MonitoredTripTest.java | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java index 4a49703a5..e14d7d996 100644 --- a/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java +++ b/src/main/java/org/opentripplanner/middleware/models/MonitoredTrip.java @@ -489,6 +489,14 @@ public static TripUsers getAddedUsers(MonitoredTrip monitoredTrip, MonitoredTrip return new TripUsers(addedPrimaryTraveler, addedCompanion, addedObservers); } + /** + * @return The id of the primary traveler (the primary user of a trip, + * or the user who created the trip if no primary user has been set). + */ + public String getPrimaryTravelerId() { + return primary != null ? primary.userId : userId; + } + public static class TripUsers { public final RelatedUser companion; public final List observers; diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 803fc6529..15ef5ef20 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -89,7 +89,7 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa TripActions.getDefault().handleSegmentAction( ((SelfLegInstruction)instruction).getLegStep(), travelerPosition.expectedLeg.steps, - Persistence.otpUsers.getById(tripData.trip.userId) + Persistence.otpUsers.getById(tripData.trip.getPrimaryTravelerId()) ); } diff --git a/src/test/java/org/opentripplanner/middleware/models/MonitoredTripTest.java b/src/test/java/org/opentripplanner/middleware/models/MonitoredTripTest.java index a58adec69..2fde790db 100644 --- a/src/test/java/org/opentripplanner/middleware/models/MonitoredTripTest.java +++ b/src/test/java/org/opentripplanner/middleware/models/MonitoredTripTest.java @@ -150,4 +150,36 @@ private static Stream createGetAddedUsersCases() { ) ); } + + @ParameterizedTest + @MethodSource("createGetPrimaryTravelerCases") + void canGetPrimaryTraveler(MobilityProfileLite primary, String creatorId, String expectedId, String message) { + MonitoredTrip trip = new MonitoredTrip(); + trip.userId = creatorId; + trip.primary = primary; + + assertEquals(expectedId, trip.getPrimaryTravelerId(), message); + } + + private static Stream createGetPrimaryTravelerCases() { + final String creatorId = "creator-user-id"; + + MobilityProfileLite primary = new MobilityProfileLite(); + primary.userId = "primary-user-id"; + + return Stream.of( + Arguments.of( + primary, + creatorId, + primary.userId, + "Should return the id of primary if it was set." + ), + Arguments.of( + null, + creatorId, + creatorId, + "Should return the id of the user that created the trip if primary is null." + ) + ); + } } From 55b91ee97b9a90ecb26956bc6b88a0ea9cbe5adc Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup <56846598+binh-dam-ibigroup@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:56:03 -0500 Subject: [PATCH 30/30] fix(ManageTripTracking): Use profile of primary traveler for bus notifications. --- .../middleware/triptracker/ManageTripTracking.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java index 15ef5ef20..795a564db 100644 --- a/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java +++ b/src/main/java/org/opentripplanner/middleware/triptracker/ManageTripTracking.java @@ -64,7 +64,7 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa TravelerPosition travelerPosition = new TravelerPosition( trackedJourney, tripData.trip.journeyState.matchingItinerary, - Persistence.otpUsers.getById(tripData.trip.userId) + Persistence.otpUsers.getById(tripData.trip.getPrimaryTravelerId()) ); TripStatus tripStatus = TripStatus.getTripStatus(travelerPosition); trackedJourney.lastLocation().tripStatus = tripStatus;