org.opentripplanner.ext.siri.SiriAlertsUpdateHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
package org.opentripplanner.ext.siri;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.Route;
import org.opentripplanner.model.TransitMode;
import org.opentripplanner.model.calendar.ServiceDate;
import org.opentripplanner.routing.alertpatch.AlertUrl;
import org.opentripplanner.routing.alertpatch.EntitySelector;
import org.opentripplanner.routing.alertpatch.StopCondition;
import org.opentripplanner.routing.alertpatch.TimePeriod;
import org.opentripplanner.routing.alertpatch.TransitAlert;
import org.opentripplanner.routing.algorithm.raptoradapter.transit.mappers.DateMapper;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.services.TransitAlertService;
import org.opentripplanner.util.I18NString;
import org.opentripplanner.util.NonLocalizedString;
import org.opentripplanner.util.TranslatedString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.ifopt.siri20.StopPlaceRef;
import uk.org.siri.siri20.AffectedLineStructure;
import uk.org.siri.siri20.AffectedOperatorStructure;
import uk.org.siri.siri20.AffectedRouteStructure;
import uk.org.siri.siri20.AffectedStopPlaceStructure;
import uk.org.siri.siri20.AffectedStopPointStructure;
import uk.org.siri.siri20.AffectedVehicleJourneyStructure;
import uk.org.siri.siri20.AffectsScopeStructure;
import uk.org.siri.siri20.DataFrameRefStructure;
import uk.org.siri.siri20.DefaultedTextStructure;
import uk.org.siri.siri20.FramedVehicleJourneyRefStructure;
import uk.org.siri.siri20.HalfOpenTimestampOutputRangeStructure;
import uk.org.siri.siri20.InfoLinkStructure;
import uk.org.siri.siri20.LineRef;
import uk.org.siri.siri20.NaturalLanguageStringStructure;
import uk.org.siri.siri20.NetworkRefStructure;
import uk.org.siri.siri20.OperatorRefStructure;
import uk.org.siri.siri20.PtSituationElement;
import uk.org.siri.siri20.RoutePointTypeEnumeration;
import uk.org.siri.siri20.ServiceDelivery;
import uk.org.siri.siri20.SituationExchangeDeliveryStructure;
import uk.org.siri.siri20.StopPointRef;
import uk.org.siri.siri20.VehicleJourneyRef;
import uk.org.siri.siri20.WorkflowStatusEnumeration;
/**
* This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange feeds.
* NOTE this cannot handle situations where there are multiple feeds with different IDs (for now it may only work in
* single-feed regions).
*/
public class SiriAlertsUpdateHandler {
private static final Logger LOG = LoggerFactory.getLogger(SiriAlertsUpdateHandler.class);
private TransitAlertService transitAlertService;
/** How long before the posted start of an event it should be displayed to users */
private long earlyStart;
private SiriFuzzyTripMatcher siriFuzzyTripMatcher;
private final String feedId;
private final Graph graph;
private final Set alerts = new HashSet<>();
public SiriAlertsUpdateHandler(String feedId, Graph graph) {
this.feedId = feedId;
this.graph = graph;
}
public void update(ServiceDelivery delivery) {
for (SituationExchangeDeliveryStructure sxDelivery : delivery.getSituationExchangeDeliveries()) {
SituationExchangeDeliveryStructure.Situations situations = sxDelivery.getSituations();
if (situations != null) {
long t1 = System.currentTimeMillis();
int addedCounter = 0;
int expiredCounter = 0;
for (PtSituationElement sxElement : situations.getPtSituationElements()) {
boolean expireSituation = (sxElement.getProgress() != null &&
sxElement.getProgress().equals(WorkflowStatusEnumeration.CLOSED));
String situationNumber;
if (sxElement.getSituationNumber() != null) {
situationNumber = sxElement.getSituationNumber().getValue();
} else {
situationNumber = null;
}
if (expireSituation) {
alerts.removeIf(transitAlert -> transitAlert.getId().equals(situationNumber));
expiredCounter++;
} else {
TransitAlert alert = handleAlert(sxElement);
addedCounter++;
if (alert != null) {
alert.setId(situationNumber);
if (alert.getEntities().isEmpty()) {
LOG.info(
"No match found for Alert - setting Unknown entity for situation with situationNumber {}",
situationNumber
);
alert.addEntity(new EntitySelector.Unknown(
"Alert had no entities that could be handled"));
}
alerts.removeIf(transitAlert -> transitAlert.getId().equals(situationNumber));
alerts.add(alert);
}
}
}
transitAlertService.setAlerts(alerts);
LOG.info("Added {} alerts, expired {} alerts based on {} situations, current alert-count: {}, elapsed time {}ms",
addedCounter, expiredCounter, situations.getPtSituationElements().size(), transitAlertService.getAllAlerts().size(), System.currentTimeMillis()-t1);
}
}
}
private TransitAlert handleAlert(PtSituationElement situation) {
TransitAlert alert = createAlertWithTexts(situation);
if ((alert.alertHeaderText == null || alert.alertHeaderText.toString().isEmpty()) &&
(alert.alertDescriptionText == null || alert.alertDescriptionText.toString().isEmpty()) &&
(alert.alertDetailText == null || alert.alertDetailText.toString().isEmpty())) {
LOG.debug("Empty Alert - ignoring situationNumber: {}", situation.getSituationNumber() != null ? situation.getSituationNumber().getValue():null);
return null;
}
ArrayList periods = new ArrayList<>();
if(situation.getValidityPeriods().size() > 0) {
for (HalfOpenTimestampOutputRangeStructure activePeriod : situation.getValidityPeriods()) {
final long realStart = activePeriod.getStartTime() != null ? getEpochSecond(activePeriod.getStartTime()) : 0;
final long start = activePeriod.getStartTime() != null? realStart - earlyStart : 0;
final long realEnd = activePeriod.getEndTime() != null ? getEpochSecond(activePeriod.getEndTime()) : TimePeriod.OPEN_ENDED;
final long end = activePeriod.getEndTime() != null? realEnd : TimePeriod.OPEN_ENDED;
periods.add(new TimePeriod(start, end));
}
} else {
// Per the GTFS-rt spec, if an alert has no TimeRanges, than it should always be shown.
periods.add(new TimePeriod(0, TimePeriod.OPEN_ENDED));
}
alert.setTimePeriods(periods);
if (situation.getPriority() != null) {
alert.priority = situation.getPriority().intValue();
}
AffectsScopeStructure affectsStructure = situation.getAffects();
if (affectsStructure != null) {
AffectsScopeStructure.Operators operators = affectsStructure.getOperators();
if (operators != null && isNotEmpty(operators.getAffectedOperators())) {
for (AffectedOperatorStructure affectedOperator : operators.getAffectedOperators()) {
OperatorRefStructure operatorRef = affectedOperator.getOperatorRef();
if (operatorRef == null || operatorRef.getValue() == null) {
continue;
}
// SIRI Operators are mapped to OTP Agency, this i probably wrong - but
// I leave this for now.
String agencyId = operatorRef.getValue();
alert.addEntity(new EntitySelector.Agency(new FeedScopedId(feedId, agencyId)));
}
}
AffectsScopeStructure.StopPoints stopPoints = affectsStructure.getStopPoints();
AffectsScopeStructure.StopPlaces stopPlaces = affectsStructure.getStopPlaces();
if (stopPoints != null && isNotEmpty(stopPoints.getAffectedStopPoints())) {
for (AffectedStopPointStructure stopPoint : stopPoints.getAffectedStopPoints()) {
StopPointRef stopPointRef = stopPoint.getStopPointRef();
if (stopPointRef == null || stopPointRef.getValue() == null) {
continue;
}
FeedScopedId stopId = siriFuzzyTripMatcher.getStop(stopPointRef.getValue());
if (stopId == null) {
stopId = new FeedScopedId(feedId, stopPointRef.getValue());
}
alert.addEntity(new EntitySelector.Stop(stopId));
// TODO: is this correct? Should the stop conditions be in the entity selector?
updateStopConditions(alert, stopPoint.getStopConditions());
}
} else if (stopPlaces != null && isNotEmpty(stopPlaces.getAffectedStopPlaces())) {
for (AffectedStopPlaceStructure stopPoint : stopPlaces.getAffectedStopPlaces()) {
StopPlaceRef stopPlace = stopPoint.getStopPlaceRef();
if (stopPlace == null || stopPlace.getValue() == null) {
continue;
}
FeedScopedId stopId = siriFuzzyTripMatcher.getStop(stopPlace.getValue());
if (stopId == null) {
stopId = new FeedScopedId(feedId, stopPlace.getValue());
}
alert.addEntity(new EntitySelector.Stop(stopId));
}
}
AffectsScopeStructure.Networks networks = affectsStructure.getNetworks();
if (networks != null && isNotEmpty(networks.getAffectedNetworks())) {
for (AffectsScopeStructure.Networks.AffectedNetwork affectedNetwork : networks.getAffectedNetworks()) {
List affectedLines = affectedNetwork.getAffectedLines();
if (isNotEmpty(affectedLines)) {
for (AffectedLineStructure line : affectedLines) {
LineRef lineRef = line.getLineRef();
if (lineRef == null || lineRef.getValue() == null) {
continue;
}
List affectedStops = new ArrayList<>();
AffectedLineStructure.Routes routes = line.getRoutes();
// Resolve AffectedStop-ids
if (routes != null) {
for (AffectedRouteStructure route : routes.getAffectedRoutes()) {
if (route.getStopPoints() != null) {
List stopPointsList = route.getStopPoints().getAffectedStopPointsAndLinkProjectionToNextStopPoints();
for (Serializable serializable : stopPointsList) {
if (serializable instanceof AffectedStopPointStructure) {
AffectedStopPointStructure stopPointStructure = (AffectedStopPointStructure) serializable;
affectedStops.add(stopPointStructure);
}
}
}
}
}
FeedScopedId affectedRoute = new FeedScopedId(feedId, lineRef.getValue());
if (! affectedStops.isEmpty()) {
for (AffectedStopPointStructure affectedStop : affectedStops) {
FeedScopedId stop = siriFuzzyTripMatcher.getStop(affectedStop.getStopPointRef().getValue());
if (stop == null) {
stop = new FeedScopedId(feedId, affectedStop.getStopPointRef().getValue());
}
alert.addEntity(new EntitySelector.StopAndRoute(stop, affectedRoute));
// TODO: is this correct? Should the stop conditions be in the entity selector?
updateStopConditions(alert, affectedStop.getStopConditions());
}
} else {
alert.addEntity(new EntitySelector.Route(affectedRoute));
}
}
} else {
NetworkRefStructure networkRef = affectedNetwork.getNetworkRef();
if (networkRef == null || networkRef.getValue() == null) {
continue;
}
String networkId = networkRef.getValue();
// TODO: What to do here?
}
}
}
AffectsScopeStructure.VehicleJourneys vjs = affectsStructure.getVehicleJourneys();
if (vjs != null && isNotEmpty(vjs.getAffectedVehicleJourneies())) {
for (AffectedVehicleJourneyStructure affectedVehicleJourney : vjs.getAffectedVehicleJourneies()) {
String lineRef = null;
if (affectedVehicleJourney.getLineRef() != null) {
lineRef = affectedVehicleJourney.getLineRef().getValue();
}
List affectedStops = new ArrayList<>();
List routes = affectedVehicleJourney.getRoutes();
// Resolve AffectedStop-ids
if (routes != null) {
for (AffectedRouteStructure route : routes) {
if (route.getStopPoints() != null) {
List stopPointsList = route.getStopPoints().getAffectedStopPointsAndLinkProjectionToNextStopPoints();
for (Serializable serializable : stopPointsList) {
if (serializable instanceof AffectedStopPointStructure) {
AffectedStopPointStructure stopPointStructure = (AffectedStopPointStructure) serializable;
affectedStops.add(stopPointStructure);
}
}
}
}
}
List vehicleJourneyReves = affectedVehicleJourney.getVehicleJourneyReves();
if (isNotEmpty(vehicleJourneyReves)) {
for (VehicleJourneyRef vehicleJourneyRef : vehicleJourneyReves) {
List tripIds = new ArrayList<>();
FeedScopedId tripIdFromVehicleJourney = siriFuzzyTripMatcher.getTripId(vehicleJourneyRef.getValue());
ZonedDateTime originAimedDepartureTime = affectedVehicleJourney.getOriginAimedDepartureTime() != null
? affectedVehicleJourney.getOriginAimedDepartureTime()
: ZonedDateTime.now();
ZonedDateTime startOfService = DateMapper.asStartOfService(originAimedDepartureTime);
ServiceDate serviceDate = new ServiceDate(startOfService.toLocalDate());
if (tripIdFromVehicleJourney != null) {
tripIds.add(tripIdFromVehicleJourney);
} else {
tripIds = siriFuzzyTripMatcher.getTripIdForInternalPlanningCodeServiceDateAndMode(
vehicleJourneyRef.getValue(),
serviceDate,
TransitMode.RAIL,
"railReplacementBus"
);
}
for (FeedScopedId tripId : tripIds) {
if (! affectedStops.isEmpty()) {
for (AffectedStopPointStructure affectedStop : affectedStops) {
FeedScopedId stop = siriFuzzyTripMatcher.getStop(affectedStop.getStopPointRef().getValue());
if (stop == null) {
stop = new FeedScopedId(feedId, affectedStop.getStopPointRef().getValue());
}
// Creating unique, deterministic id for the alert
alert.addEntity(new EntitySelector.StopAndTrip(stop, tripId, serviceDate));
// TODO: is this correct? Should the stop conditions be in the entity selector?
updateStopConditions(alert, affectedStop.getStopConditions());
}
} else {
alert.addEntity(new EntitySelector.Trip(tripId, serviceDate));
}
}
}
}
final FramedVehicleJourneyRefStructure framedVehicleJourneyRef = affectedVehicleJourney.getFramedVehicleJourneyRef();
if (framedVehicleJourneyRef != null) {
final DataFrameRefStructure dataFrameRef = framedVehicleJourneyRef.getDataFrameRef();
final String datedVehicleJourneyRef = framedVehicleJourneyRef.getDatedVehicleJourneyRef();
FeedScopedId tripId = siriFuzzyTripMatcher.getTripId(datedVehicleJourneyRef);
if (tripId != null) {
ServiceDate serviceDate = null;
if (dataFrameRef != null && dataFrameRef.getValue() != null) {
ZonedDateTime startOfService = DateMapper.asStartOfService(LocalDate.parse(
dataFrameRef.getValue()), graph.getTimeZone().toZoneId());
serviceDate = new ServiceDate(startOfService.getYear(),
startOfService.getMonthValue(),
startOfService.getDayOfMonth()
);
}
if (!affectedStops.isEmpty()) {
for (AffectedStopPointStructure affectedStop : affectedStops) {
FeedScopedId stop = siriFuzzyTripMatcher.getStop(affectedStop
.getStopPointRef()
.getValue());
if (stop == null) {
stop = new FeedScopedId(feedId,
affectedStop.getStopPointRef().getValue()
);
}
alert.addEntity(new EntitySelector.StopAndTrip(stop, tripId, serviceDate));
}
}
else {
alert.addEntity(new EntitySelector.Trip(tripId, serviceDate));
}
}
}
if (lineRef != null) {
Set affectedRoutes = siriFuzzyTripMatcher.getRoutes(lineRef);
if (affectedRoutes != null) {
for (Route route : affectedRoutes) {
alert.addEntity(new EntitySelector.Route(route.getId()));
}
}
}
}
}
}
if (alert.getStopConditions().isEmpty()) {
updateStopConditions(alert, null);
}
alert.alertType = situation.getReportType();
alert.severity =
SiriSeverityMapper.getAlertSeverityForSiriSeverity(situation.getSeverity());
if (situation.getParticipantRef() != null) {
String codespace = situation.getParticipantRef().getValue();
alert.setFeedId(codespace + ":Authority:" + codespace); //TODO - SIRI: Should probably not assume this codespace -> authority rule
}
return alert;
}
private long getEpochSecond(ZonedDateTime startTime) {
return startTime.toEpochSecond();
}
/*
* Creates alert from PtSituation with all textual content
*/
private TransitAlert createAlertWithTexts(PtSituationElement situation) {
TransitAlert alert = new TransitAlert();
alert.alertDescriptionText = getTranslatedString(situation.getDescriptions());
alert.alertDetailText = getTranslatedString(situation.getDetails());
alert.alertAdviceText = getTranslatedString(situation.getAdvices());
alert.alertHeaderText = getTranslatedString(situation.getSummaries());
alert.alertUrl = getInfoLinkAsString(situation.getInfoLinks());
alert.setAlertUrlList(getInfoLinks(situation.getInfoLinks()));
return alert;
}
/*
* Returns first InfoLink-uri as a String
*/
private I18NString getInfoLinkAsString(PtSituationElement.InfoLinks infoLinks) {
if (infoLinks != null) {
if (isNotEmpty(infoLinks.getInfoLinks())) {
InfoLinkStructure infoLinkStructure = infoLinks.getInfoLinks().get(0);
if (infoLinkStructure != null && infoLinkStructure.getUri() != null) {
return new NonLocalizedString(infoLinkStructure.getUri());
}
}
}
return null;
}
/*
* Returns all InfoLinks
*/
private List getInfoLinks(PtSituationElement.InfoLinks infoLinks) {
List alertUrls = new ArrayList<>();
if (infoLinks != null) {
if (isNotEmpty(infoLinks.getInfoLinks())) {
for (InfoLinkStructure infoLink : infoLinks.getInfoLinks()) {
AlertUrl alertUrl = new AlertUrl();
List labels = infoLink.getLabels();
if (labels != null && !labels.isEmpty()) {
NaturalLanguageStringStructure label = labels.get(0);
alertUrl.label = label.getValue();
}
alertUrl.uri = infoLink.getUri();
alertUrls.add(alertUrl);
}
}
}
return alertUrls;
}
private void updateStopConditions(TransitAlert alertPatch, List stopConditions) {
Set alertStopConditions = new HashSet<>();
if (stopConditions != null) {
for (RoutePointTypeEnumeration stopCondition : stopConditions) {
switch (stopCondition) {
case EXCEPTIONAL_STOP:
alertStopConditions.add(StopCondition.EXCEPTIONAL_STOP);
break;
case DESTINATION:
alertStopConditions.add(StopCondition.DESTINATION);
break;
case NOT_STOPPING:
alertStopConditions.add(StopCondition.NOT_STOPPING);
break;
case REQUEST_STOP:
alertStopConditions.add(StopCondition.REQUEST_STOP);
break;
case START_POINT:
alertStopConditions.add(StopCondition.START_POINT);
break;
}
}
}
if (alertStopConditions.isEmpty()) {
//No StopConditions are set - set default
alertStopConditions.add(StopCondition.START_POINT);
alertStopConditions.add(StopCondition.DESTINATION);
}
alertPatch.getStopConditions().addAll(alertStopConditions);
}
/**
* @return True if list have at least one element. {@code false} is returned if the given list
* is empty or {@code null}.
*/
private boolean isNotEmpty(List list) {
return list != null && !list.isEmpty();
}
/**
* convert a SIRI DefaultedTextStructure to a OTP TranslatedString
*
* @return A TranslatedString containing the same information as the input
*/
private I18NString getTranslatedString(List input) {
Map translations = new HashMap<>();
if (input != null && input.size() > 0) {
for (DefaultedTextStructure textStructure : input) {
String language = "";
String value = "";
if (textStructure.getLang() != null) {
language = textStructure.getLang();
}
if (textStructure.getValue() != null) {
value = textStructure.getValue();
}
translations.put(language, value);
}
} else {
translations.put("", "");
}
return translations.isEmpty() ? null : TranslatedString.getI18NString(translations);
}
public void setTransitAlertService(TransitAlertService transitAlertService) {
this.transitAlertService = transitAlertService;
}
public long getEarlyStart() {
return earlyStart;
}
public void setEarlyStart(long earlyStart) {
this.earlyStart = earlyStart;
}
public void setSiriFuzzyTripMatcher(SiriFuzzyTripMatcher siriFuzzyTripMatcher) {
this.siriFuzzyTripMatcher = siriFuzzyTripMatcher;
}
}