
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
The newest version!
package org.opentripplanner.ext.siri;
import java.time.Duration;
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.ext.siri.mapper.AffectsMapper;
import org.opentripplanner.ext.siri.mapper.SiriSeverityMapper;
import org.opentripplanner.framework.i18n.I18NString;
import org.opentripplanner.framework.i18n.NonLocalizedString;
import org.opentripplanner.framework.i18n.TranslatedString;
import org.opentripplanner.routing.alertpatch.AlertUrl;
import org.opentripplanner.routing.alertpatch.EntitySelector;
import org.opentripplanner.routing.alertpatch.TimePeriod;
import org.opentripplanner.routing.alertpatch.TransitAlert;
import org.opentripplanner.routing.alertpatch.TransitAlertBuilder;
import org.opentripplanner.routing.services.TransitAlertService;
import org.opentripplanner.transit.model.framework.FeedScopedId;
import org.opentripplanner.updater.RealTimeUpdateContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import uk.org.siri.siri20.DefaultedTextStructure;
import uk.org.siri.siri20.HalfOpenTimestampOutputRangeStructure;
import uk.org.siri.siri20.InfoLinkStructure;
import uk.org.siri.siri20.NaturalLanguageStringStructure;
import uk.org.siri.siri20.PtSituationElement;
import uk.org.siri.siri20.ServiceDelivery;
import uk.org.siri.siri20.SituationExchangeDeliveryStructure;
import uk.org.siri.siri20.WorkflowStatusEnumeration;
/**
* This updater applies the equivalent of GTFS Alerts, but from SIRI Situation Exchange (SX) feeds.
* As the incoming SIRI SX messages are mapped to internal TransitAlerts, their FeedScopedIds will
* be the single feed ID associated with this update handler, plus the situation number provided in
* the SIRI SX message.
* This class cannot handle situations where incoming messages are being applied to multiple static
* feeds with different IDs. For now it may only work in single-feed regions. A possible workaround
* is to assign the same feed ID to multiple static feeds where it is known that their entity IDs
* are all drawn from the same namespace (i.e. they are functionally fragments of the same feed).
* TODO RT_AB: Internal FeedScopedId creation strategy should probably be pluggable or configurable.
* TG has indicated this is a necessary condition for moving this updater out of sandbox.
* TODO RT_AB: The name should be clarified, as there is no such thing as "SIRI Alerts", and it
* is referencing the internal model concept of "Alerts" which are derived from GTFS terminology.
*/
public class SiriAlertsUpdateHandler {
private static final Logger LOG = LoggerFactory.getLogger(SiriAlertsUpdateHandler.class);
private final String feedId;
private final Set alerts = new HashSet<>();
private final TransitAlertService transitAlertService;
private final Duration earlyStart;
/**
* @param earlyStart display the alerts to users this long before their activePeriod begins
*/
public SiriAlertsUpdateHandler(
String feedId,
TransitAlertService transitAlertService,
Duration earlyStart
) {
this.feedId = feedId;
this.transitAlertService = transitAlertService;
this.earlyStart = earlyStart;
}
public void update(ServiceDelivery delivery, RealTimeUpdateContext context) {
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)
);
if (sxElement.getSituationNumber() == null) {
continue;
}
String situationNumber = sxElement.getSituationNumber().getValue();
FeedScopedId id = new FeedScopedId(feedId, situationNumber);
if (expireSituation) {
alerts.removeIf(transitAlert -> transitAlert.getId().equals(id));
expiredCounter++;
} else {
TransitAlert alert = null;
try {
alert = mapSituationToAlert(sxElement, context);
addedCounter++;
} catch (Exception e) {
LOG.info(
"Caught exception when processing situation with situationNumber {}: {}",
situationNumber,
e
);
}
if (alert != null) {
alerts.removeIf(transitAlert -> transitAlert.getId().equals(id));
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
);
}
}
}
/**
* Build an internal model Alert from an incoming SIRI situation exchange element.
* May return null if the header, description, and detail text are all empty or missing in the
* SIRI message. In all other cases it will return a valid TransitAlert instance.
*/
private TransitAlert mapSituationToAlert(
PtSituationElement situation,
RealTimeUpdateContext context
) {
TransitAlertBuilder alert = createAlertWithTexts(situation);
if (
(alert.headerText() == null || alert.headerText().toString().isEmpty()) &&
(alert.descriptionText() == null || alert.descriptionText().toString().isEmpty()) &&
(alert.detailText() == null || alert.detailText().toString().isEmpty())
) {
LOG.debug(
"Empty Alert - ignoring situationNumber: {}",
situation.getSituationNumber() != null ? situation.getSituationNumber().getValue() : null
);
return null;
}
if (situation.getCreationTime() != null) {
alert.withCreationTime(situation.getCreationTime());
}
if (situation.getVersionedAtTime() != null) {
alert.withUpdatedTime(situation.getVersionedAtTime());
}
if (situation.getVersion() != null && situation.getVersion().getValue() != null) {
alert.withVersion(situation.getVersion().getValue().intValue());
}
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.toSeconds()
: 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.addTimePeriods(periods);
if (situation.getPriority() != null) {
alert.withPriority(situation.getPriority().intValue());
}
alert.addEntites(
new AffectsMapper(feedId, context.siriFuzzyTripMatcher(), context.transitService())
.mapAffects(situation.getAffects())
);
if (alert.entities().isEmpty()) {
LOG.info(
"No match found for Alert - setting Unknown entity for situation with situationNumber {}",
alert.getId()
);
alert.addEntity(new EntitySelector.Unknown("Alert had no entities that could be handled"));
}
alert.withType(situation.getReportType());
alert.withSeverity(SiriSeverityMapper.getAlertSeverityForSiriSeverity(situation.getSeverity()));
if (situation.getParticipantRef() != null) {
alert.withSiriCodespace(situation.getParticipantRef().getValue());
}
return alert.build();
}
private long getEpochSecond(ZonedDateTime startTime) {
return startTime.toEpochSecond();
}
/*
* Creates a builder for an internal model TransitAlert. The builder is pre-filled with all
* textual content from the supplied SIRI PtSituation. The builder also has the feed scoped ID
* pre-set to the single feed ID associated with this update handler, plus the situation number
* provided in the SIRI PtSituation.
*/
private TransitAlertBuilder createAlertWithTexts(PtSituationElement situation) {
return TransitAlert
.of(new FeedScopedId(feedId, situation.getSituationNumber().getValue()))
.withDescriptionText(getTranslatedString(situation.getDescriptions()))
.withDetailText(getTranslatedString(situation.getDetails()))
.withAdviceText(getTranslatedString(situation.getAdvices()))
.withHeaderText(getTranslatedString(situation.getSummaries()))
.withUrl(getInfoLinkAsString(situation.getInfoLinks()))
.addSiriUrls(getInfoLinks(situation.getInfoLinks()));
}
/*
* 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;
}
/**
* @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, false, true);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy