Skip to content

Commit

Permalink
refactor(Fixed merge conflicts and moved leg trans notify test into o…
Browse files Browse the repository at this point in the history
…wn class):
  • Loading branch information
br648 committed Dec 13, 2024
2 parents c5582e1 + 5369efb commit 2638f04
Show file tree
Hide file tree
Showing 23 changed files with 832 additions and 136 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,35 @@ OTP-middleware supports triggering certain actions when someone activates live t
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 |
#### Bus Notify Actions

Bus notifier actions are defined in the optional file `bus-notifier-actions.yml` in the same configuration folder as `env.yml`.
Expand Down Expand Up @@ -306,6 +335,9 @@ The special E2E client settings should be defined in `env.yml`:
| TWILIO_ACCOUNT_SID | string | Optional | your-account-sid | Twilio settings available at: https://twilio.com/user/account |
| TRUSTED_COMPANION_CONFIRMATION_PAGE_URL | string | Optional | https://otp-server.example.com/trusted/confirmation | URL to the trusted companion confirmation page. This page should support handling an error URL parameter. |
| 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 |
| US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_URL | string | Optional | http://host.example.com | US RideGwinnett bus notifier API. |
| US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_API_KEY | string | Optional | your-api-key | API key for the US RideGwinnett bus notifier API. |
| US_RIDE_GWINNETT_BUS_OPERATOR_NOTIFIER_QUALIFYING_ROUTES | string | Optional | agency_id:route_id | A comma separated list of US RideGwinnett routes that can be notified. |
Expand Down
8 changes: 8 additions & 0 deletions configurations/default/trip-actions.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,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<RelatedUser> observers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import static com.mongodb.client.model.Filters.eq;
import static org.opentripplanner.middleware.models.LegTransitionNotification.getLegTransitionNotifyUsers;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
import org.opentripplanner.middleware.models.TrackedJourney;
import org.opentripplanner.middleware.otp.response.Leg;
import org.opentripplanner.middleware.persistence.Persistence;
import org.opentripplanner.middleware.triptracker.instruction.SelfLegInstruction;
import org.opentripplanner.middleware.triptracker.interactions.TripActions;
import org.opentripplanner.middleware.triptracker.instruction.TripInstruction;
import org.opentripplanner.middleware.triptracker.interactions.busnotifiers.BusOperatorActions;
import org.opentripplanner.middleware.triptracker.response.EndTrackingResponse;
import org.opentripplanner.middleware.triptracker.response.TrackingResponse;
import spark.Request;

import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS;
import static org.opentripplanner.middleware.triptracker.TravelerLocator.isAtStartOfLeg;
import static org.opentripplanner.middleware.utils.ConfigUtils.getConfigPropertyAsInt;
import static org.opentripplanner.middleware.utils.ItineraryUtils.getRouteGtfsIdFromLeg;
Expand Down Expand Up @@ -60,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;
Expand All @@ -79,9 +83,21 @@ private static TrackingResponse doUpdateTracking(Request request, TripTrackingDa
TravelerLocator.checkForLegTransition(tripStatus, travelerPosition, tripData.trip);

// Provide response.
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 instanceof SelfLegInstruction && instruction.distance <= TRIP_INSTRUCTION_UPCOMING_RADIUS) {
TripActions.getDefault().handleSegmentAction(
((SelfLegInstruction)instruction).getLegStep(),
travelerPosition.expectedLeg.steps,
Persistence.otpUsers.getById(tripData.trip.getPrimaryTravelerId())
);
}

return new TrackingResponse(
TRIP_TRACKING_UPDATE_FREQUENCY_SECONDS,
TravelerLocator.getInstruction(tripStatus, travelerPosition, create),
instruction != null ? instruction.build() : NO_INSTRUCTION,
trackedJourney.id,
tripStatus.name()
);
Expand Down Expand Up @@ -165,7 +181,7 @@ private static EndTrackingResponse completeJourney(TripTrackingData tripData, bo
);

return new EndTrackingResponse(
TripInstruction.NO_INSTRUCTION,
NO_INSTRUCTION,
TripStatus.ENDED.name()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@
import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.ARRIVED_NOTIFICATION;
import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.ARRIVED_AND_MODE_CHANGE_NOTIFICATION;
import static org.opentripplanner.middleware.tripmonitor.jobs.NotificationType.DEPARTED_NOTIFICATION;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.NO_INSTRUCTION;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_IMMEDIATE_RADIUS;
import static org.opentripplanner.middleware.triptracker.instruction.TripInstruction.TRIP_INSTRUCTION_UPCOMING_RADIUS;
import static org.opentripplanner.middleware.utils.GeometryUtils.getDistance;
Expand All @@ -64,41 +63,33 @@ 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, false);
if (tripInstruction != null) {
return tripInstruction.build();
}
if (tripInstruction != null) return tripInstruction;
}

if (tripStatus.equals(TripStatus.DEVIATED)) {
TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip);
if (tripInstruction != null) {
return tripInstruction.build();
}
if (tripInstruction != null) return tripInstruction;
}
} else if (hasRequiredTransitLeg(travelerPosition)) {
if (hasRequiredTripStatus(tripStatus)) {
TripInstruction tripInstruction = alignTravelerToTransitTrip(travelerPosition);
if (tripInstruction != null) {
return tripInstruction.build();
}
if (tripInstruction != null) return tripInstruction;
}

if (tripStatus.equals(TripStatus.DEVIATED)) {
TripInstruction tripInstruction = getBackOnTrack(travelerPosition, isStartOfTrip);
if (tripInstruction != null) {
return tripInstruction.build();
}
if (tripInstruction != null) return tripInstruction;
}
}
return NO_INSTRUCTION;
return null;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ public class SelfLegInstruction extends TripInstruction {
/** Step aligned with traveler's position. */
protected Step legStep;

public Step getLegStep() {
return legStep;
}

}
Original file line number Diff line number Diff line change
@@ -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) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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, String trigger) {
this.id = id;
this.start = segment.start;
this.end = segment.end;
this.trigger = trigger;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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.List;

/** Holds configured trip actions. */
public class TripActions {
private static final Logger LOG = LoggerFactory.getLogger(TripActions.class);

public static final String TRIP_ACTIONS_YML = "configurations/default/trip-actions.yml";

private static TripActions defaultInstance;

private final List<SegmentAction> 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;
}

public TripActions(List<SegmentAction> segmentActions) {
this.segmentActions = segmentActions;
}

/**
* @param segment The {@link Segment} to test
* @return The first {@link SegmentAction} found for the given segment
*/
public SegmentAction getSegmentAction(Segment segment) {
for (SegmentAction a : segmentActions) {
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 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);
interaction = (Interaction) interactionClass.getDeclaredConstructor().newInstance();
interaction.triggerAction(action, otpUser);
} catch (Exception 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);
}
}
}

public void handleSegmentAction(Step step, List<Step> 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(
new Coordinates(step),
new Coordinates(stepAfter)
);
handleSegmentAction(segment, user);
}
}
}
Loading

0 comments on commit 2638f04

Please sign in to comment.