/** * Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org> * Copyright (C) 2011 Google, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.onebusaway.transit_data_federation.impl.realtime.siri; import java.util.ArrayList; import java.util.Date; import java.util.List; import javax.xml.datatype.Duration; import org.onebusaway.collections.CollectionsLibrary; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.realtime.api.VehicleLocationListener; import org.onebusaway.realtime.api.VehicleLocationRecord; import org.onebusaway.siri.AffectedApplicationStructure; import org.onebusaway.siri.OneBusAwayAffects; import org.onebusaway.siri.OneBusAwayAffectsStructure.Applications; import org.onebusaway.siri.OneBusAwayConsequence; import org.onebusaway.siri.core.ESiriModuleType; import org.onebusaway.transit_data.model.service_alerts.ESeverity; import org.onebusaway.transit_data_federation.impl.service_alerts.ServiceAlertLibrary; import org.onebusaway.transit_data_federation.services.AgencyAndIdLibrary; import org.onebusaway.transit_data_federation.services.blocks.BlockCalendarService; import org.onebusaway.transit_data_federation.services.blocks.BlockInstance; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.Affects; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.Consequence; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.Consequence.Effect; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.Id; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.ServiceAlert; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.ServiceAlert.Cause; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.TimeRange; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.TranslatedString; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlerts.TranslatedString.Translation; import org.onebusaway.transit_data_federation.services.service_alerts.ServiceAlertsService; import org.onebusaway.transit_data_federation.services.transit_graph.BlockEntry; import org.onebusaway.transit_data_federation.services.transit_graph.TransitGraphDao; import org.onebusaway.transit_data_federation.services.transit_graph.TripEntry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import uk.org.siri.siri.AbstractServiceDeliveryStructure; import uk.org.siri.siri.AffectedCallStructure; import uk.org.siri.siri.AffectedOperatorStructure; import uk.org.siri.siri.AffectedStopPointStructure; import uk.org.siri.siri.AffectedVehicleJourneyStructure; import uk.org.siri.siri.AffectedVehicleJourneyStructure.Calls; import uk.org.siri.siri.AffectsScopeStructure; import uk.org.siri.siri.AffectsScopeStructure.Operators; import uk.org.siri.siri.AffectsScopeStructure.StopPoints; import uk.org.siri.siri.AffectsScopeStructure.VehicleJourneys; import uk.org.siri.siri.BlockRefStructure; import uk.org.siri.siri.DefaultedTextStructure; import uk.org.siri.siri.EntryQualifierStructure; import uk.org.siri.siri.ExtensionsStructure; import uk.org.siri.siri.FramedVehicleJourneyRefStructure; import uk.org.siri.siri.HalfOpenTimestampRangeStructure; import uk.org.siri.siri.LocationStructure; import uk.org.siri.siri.OperatorRefStructure; import uk.org.siri.siri.PtConsequenceStructure; import uk.org.siri.siri.PtConsequencesStructure; import uk.org.siri.siri.PtSituationElementStructure; import uk.org.siri.siri.ServiceConditionEnumeration; import uk.org.siri.siri.ServiceDelivery; import uk.org.siri.siri.SeverityEnumeration; import uk.org.siri.siri.SituationExchangeDeliveryStructure; import uk.org.siri.siri.SituationExchangeDeliveryStructure.Situations; import uk.org.siri.siri.StopPointRefStructure; import uk.org.siri.siri.VehicleActivityStructure; import uk.org.siri.siri.VehicleActivityStructure.MonitoredVehicleJourney; import uk.org.siri.siri.VehicleJourneyRefStructure; import uk.org.siri.siri.VehicleMonitoringDeliveryStructure; import uk.org.siri.siri.VehicleRefStructure; import uk.org.siri.siri.WorkflowStatusEnumeration; @Component public class SiriService { private static final Logger _log = LoggerFactory.getLogger(SiriService.class); private TransitGraphDao _transitGraphDao; private ServiceAlertsService _serviceAlertsService; private VehicleLocationListener _vehicleLocationListener; private BlockCalendarService _blockCalendarService; /** * Time, in minutes, */ private int _blockInstanceSearchWindow = 30; @Autowired public void setTransitGraphDao(TransitGraphDao transitGraphDao) { _transitGraphDao = transitGraphDao; } @Autowired public void setBlockCalendarService(BlockCalendarService blockCalendarService) { _blockCalendarService = blockCalendarService; } @Autowired public void setServiceAlertService(ServiceAlertsService serviceAlertsService) { _serviceAlertsService = serviceAlertsService; } @Autowired public void set(VehicleLocationListener vehicleLocationListener) { _vehicleLocationListener = vehicleLocationListener; } /** * @param blockInstanceSearchWindow time, in minutes */ public void setBlockInstanceSearchWindow(int blockInstanceSearchWindow) { _blockInstanceSearchWindow = blockInstanceSearchWindow; } public synchronized void handleServiceDelivery( ServiceDelivery serviceDelivery, AbstractServiceDeliveryStructure deliveryForModule, ESiriModuleType moduleType, SiriEndpointDetails endpointDetails) { switch (moduleType) { case VEHICLE_MONITORING: handleVehicleMonitoring(serviceDelivery, (VehicleMonitoringDeliveryStructure) deliveryForModule, endpointDetails); break; case SITUATION_EXCHANGE: handleSituationExchange(serviceDelivery, (SituationExchangeDeliveryStructure) deliveryForModule, endpointDetails); break; } } /**** * Private Methods ****/ private void handleVehicleMonitoring(ServiceDelivery serviceDelivery, VehicleMonitoringDeliveryStructure deliveryForModule, SiriEndpointDetails endpointDetails) { List<VehicleLocationRecord> records = new ArrayList<VehicleLocationRecord>(); Date now = new Date(); long timeFrom = now.getTime() - _blockInstanceSearchWindow * 60 * 1000; long timeTo = now.getTime() + _blockInstanceSearchWindow * 60 * 1000; for (VehicleActivityStructure vehicleActivity : deliveryForModule.getVehicleActivity()) { Date time = vehicleActivity.getRecordedAtTime(); if (time == null) time = now; MonitoredVehicleJourney mvj = vehicleActivity.getMonitoredVehicleJourney(); Duration delay = mvj.getDelay(); if (delay == null) continue; VehicleRefStructure vehicleRef = mvj.getVehicleRef(); if (vehicleRef == null || vehicleRef.getValue() == null) continue; BlockEntry block = getBlockForMonitoredVehicleJourney(mvj, endpointDetails); if (block == null) { TripEntry trip = getTripForMonitoredVehicleJourney(mvj, endpointDetails); if (trip != null) block = trip.getBlock(); } if (block == null) continue; List<BlockInstance> instances = _blockCalendarService.getActiveBlocks( block.getId(), timeFrom, timeTo); // TODO : We currently assume that a block won't overlap with itself if (instances.size() != 1) continue; BlockInstance instance = instances.get(0); VehicleLocationRecord r = new VehicleLocationRecord(); r.setTimeOfRecord(time.getTime()); r.setServiceDate(instance.getServiceDate()); r.setBlockId(block.getId()); String agencyId = block.getId().getAgencyId(); r.setVehicleId(new AgencyAndId(agencyId, vehicleRef.getValue())); r.setScheduleDeviation(delay.getTimeInMillis(now) / 1000); LocationStructure location = mvj.getVehicleLocation(); if (location != null) { r.setCurrentLocationLat(location.getLatitude().doubleValue()); r.setCurrentLocationLon(location.getLongitude().doubleValue()); } records.add(r); } if (!records.isEmpty()) _vehicleLocationListener.handleVehicleLocationRecords(records); } private BlockEntry getBlockForMonitoredVehicleJourney( MonitoredVehicleJourney mvj, SiriEndpointDetails endpointDetails) { BlockRefStructure blockRef = mvj.getBlockRef(); if (blockRef == null || blockRef.getValue() == null) return null; for (String agencyId : endpointDetails.getDefaultAgencyIds()) { AgencyAndId blockId = new AgencyAndId(agencyId, blockRef.getValue()); BlockEntry blockEntry = _transitGraphDao.getBlockEntryForId(blockId); if (blockEntry != null) return blockEntry; } /** * Try parsing the id itself */ try { AgencyAndId blockId = AgencyAndId.convertFromString(blockRef.getValue()); return _transitGraphDao.getBlockEntryForId(blockId); } catch (IllegalArgumentException ex) { return null; } } private TripEntry getTripForMonitoredVehicleJourney( MonitoredVehicleJourney mvj, SiriEndpointDetails endpointDetails) { FramedVehicleJourneyRefStructure fvjRef = mvj.getFramedVehicleJourneyRef(); if (fvjRef == null || fvjRef.getDatedVehicleJourneyRef() == null) return null; for (String agencyId : endpointDetails.getDefaultAgencyIds()) { AgencyAndId tripId = new AgencyAndId(agencyId, fvjRef.getDatedVehicleJourneyRef()); TripEntry tripEntry = _transitGraphDao.getTripEntryForId(tripId); if (tripEntry != null) return tripEntry; } /** * Try parsing the id itself */ try { AgencyAndId tripId = AgencyAndId.convertFromString(fvjRef.getDatedVehicleJourneyRef()); return _transitGraphDao.getTripEntryForId(tripId); } catch (IllegalArgumentException ex) { return null; } } private void handleSituationExchange(ServiceDelivery serviceDelivery, SituationExchangeDeliveryStructure sxDelivery, SiriEndpointDetails endpointDetails) { Situations situations = sxDelivery.getSituations(); if (situations == null) return; List<ServiceAlert.Builder> serviceAlertsToUpdate = new ArrayList<ServiceAlert.Builder>(); List<AgencyAndId> serviceAlertIdsToRemove = new ArrayList<AgencyAndId>(); for (PtSituationElementStructure ptSituation : situations.getPtSituationElement()) { ServiceAlert.Builder serviceAlert = getPtSituationAsServiceAlert( ptSituation, endpointDetails); WorkflowStatusEnumeration progress = ptSituation.getProgress(); boolean remove = (progress != null && (progress == WorkflowStatusEnumeration.CLOSING || progress == WorkflowStatusEnumeration.CLOSED)); if (remove) { AgencyAndId situationId = ServiceAlertLibrary.agencyAndId(serviceAlert.getId()); serviceAlertIdsToRemove.add(situationId); } else { serviceAlertsToUpdate.add(serviceAlert); } } String defaultAgencyId = null; if (!CollectionsLibrary.isEmpty(endpointDetails.getDefaultAgencyIds())) defaultAgencyId = endpointDetails.getDefaultAgencyIds().get(0); for (ServiceAlert.Builder serviceAlert : serviceAlertsToUpdate) _serviceAlertsService.createOrUpdateServiceAlert(serviceAlert, defaultAgencyId); _serviceAlertsService.removeServiceAlerts(serviceAlertIdsToRemove); } private ServiceAlert.Builder getPtSituationAsServiceAlert( PtSituationElementStructure ptSituation, SiriEndpointDetails endpointDetails) { ServiceAlert.Builder serviceAlert = ServiceAlert.newBuilder(); EntryQualifierStructure serviceAlertNumber = ptSituation.getSituationNumber(); String situationId = serviceAlertNumber.getValue(); if (!endpointDetails.getDefaultAgencyIds().isEmpty()) { String agencyId = endpointDetails.getDefaultAgencyIds().get(0); serviceAlert.setId(ServiceAlertLibrary.id(agencyId, situationId)); } else { AgencyAndId id = AgencyAndIdLibrary.convertFromString(situationId); serviceAlert.setId(ServiceAlertLibrary.id(id)); } handleDescriptions(ptSituation, serviceAlert); handleOtherFields(ptSituation, serviceAlert); handlReasons(ptSituation, serviceAlert); handleAffects(ptSituation, serviceAlert); handleConsequences(ptSituation, serviceAlert); return serviceAlert; } private void handleDescriptions(PtSituationElementStructure ptSituation, ServiceAlert.Builder serviceAlert) { TranslatedString summary = translation(ptSituation.getSummary()); if (summary != null) serviceAlert.setSummary(summary); TranslatedString description = translation(ptSituation.getDescription()); if (description != null) serviceAlert.setDescription(description); } private void handleOtherFields(PtSituationElementStructure ptSituation, ServiceAlert.Builder serviceAlert) { SeverityEnumeration severity = ptSituation.getSeverity(); if (severity != null) { ESeverity severityEnum = ESeverity.valueOfTpegCode(severity.value()); serviceAlert.setSeverity(ServiceAlertLibrary.convertSeverity(severityEnum)); } if (ptSituation.getPublicationWindow() != null) { HalfOpenTimestampRangeStructure window = ptSituation.getPublicationWindow(); TimeRange.Builder range = TimeRange.newBuilder(); if (window.getStartTime() != null) range.setStart(window.getStartTime().getTime()); if (window.getEndTime() != null) range.setEnd(window.getEndTime().getTime()); if (range.hasStart() || range.hasEnd()) serviceAlert.addPublicationWindow(range); } } private void handlReasons(PtSituationElementStructure ptSituation, ServiceAlert.Builder serviceAlert) { Cause cause = getReasonAsCause(ptSituation); if (cause != null) serviceAlert.setCause(cause); } private Cause getReasonAsCause(PtSituationElementStructure ptSituation) { if (ptSituation.getEnvironmentReason() != null) return Cause.WEATHER; if (ptSituation.getEquipmentReason() != null) { switch (ptSituation.getEquipmentReason()) { case CONSTRUCTION_WORK: return Cause.CONSTRUCTION; case CLOSED_FOR_MAINTENANCE: case MAINTENANCE_WORK: case EMERGENCY_ENGINEERING_WORK: case LATE_FINISH_TO_ENGINEERING_WORK: case REPAIR_WORK: return Cause.MAINTENANCE; default: return Cause.TECHNICAL_PROBLEM; } } if (ptSituation.getPersonnelReason() != null) { switch (ptSituation.getPersonnelReason()) { case INDUSTRIAL_ACTION: case UNOFFICIAL_INDUSTRIAL_ACTION: return Cause.STRIKE; } return Cause.OTHER_CAUSE; } /** * There are really so many possibilities here that it's tricky to translate * them all */ if (ptSituation.getMiscellaneousReason() != null) { switch (ptSituation.getMiscellaneousReason()) { case ACCIDENT: case COLLISION: return Cause.ACCIDENT; case DEMONSTRATION: case MARCH: return Cause.DEMONSTRATION; case PERSON_ILL_ON_VEHICLE: case FATALITY: return Cause.MEDICAL_EMERGENCY; case POLICE_REQUEST: case BOMB_ALERT: case CIVIL_EMERGENCY: case EMERGENCY_SERVICES: case EMERGENCY_SERVICES_CALL: return Cause.POLICE_ACTIVITY; } } return null; } /**** * Affects ****/ protected void handleAffects(PtSituationElementStructure ptSituation, ServiceAlert.Builder serviceAlert) { AffectsScopeStructure affectsStructure = ptSituation.getAffects(); if (affectsStructure == null) return; Operators operators = affectsStructure.getOperators(); if (operators != null && !CollectionsLibrary.isEmpty(operators.getAffectedOperator())) { for (AffectedOperatorStructure operator : operators.getAffectedOperator()) { OperatorRefStructure operatorRef = operator.getOperatorRef(); if (operatorRef == null || operatorRef.getValue() == null) continue; String agencyId = operatorRef.getValue(); Affects.Builder affects = Affects.newBuilder(); affects.setAgencyId(agencyId); serviceAlert.addAffects(affects); } } StopPoints stopPoints = affectsStructure.getStopPoints(); if (stopPoints != null && !CollectionsLibrary.isEmpty(stopPoints.getAffectedStopPoint())) { for (AffectedStopPointStructure stopPoint : stopPoints.getAffectedStopPoint()) { StopPointRefStructure stopRef = stopPoint.getStopPointRef(); if (stopRef == null || stopRef.getValue() == null) continue; AgencyAndId stopId = AgencyAndIdLibrary.convertFromString(stopRef.getValue()); Id id = ServiceAlertLibrary.id(stopId); Affects.Builder affects = Affects.newBuilder(); affects.setStopId(id); serviceAlert.addAffects(affects); } } VehicleJourneys vjs = affectsStructure.getVehicleJourneys(); if (vjs != null && !CollectionsLibrary.isEmpty(vjs.getAffectedVehicleJourney())) { for (AffectedVehicleJourneyStructure vj : vjs.getAffectedVehicleJourney()) { Affects.Builder affects = Affects.newBuilder(); if (vj.getLineRef() != null) { AgencyAndId routeId = AgencyAndIdLibrary.convertFromString(vj.getLineRef().getValue()); Id id = ServiceAlertLibrary.id(routeId); affects.setRouteId(id); } if (vj.getDirectionRef() != null) affects.setDirectionId(vj.getDirectionRef().getValue()); List<VehicleJourneyRefStructure> tripRefs = vj.getVehicleJourneyRef(); Calls stopRefs = vj.getCalls(); boolean hasTripRefs = !CollectionsLibrary.isEmpty(tripRefs); boolean hasStopRefs = stopRefs != null && !CollectionsLibrary.isEmpty(stopRefs.getCall()); if (!(hasTripRefs || hasStopRefs)) { if (affects.hasRouteId()) serviceAlert.addAffects(affects); } else if (hasTripRefs && hasStopRefs) { for (VehicleJourneyRefStructure vjRef : vj.getVehicleJourneyRef()) { AgencyAndId tripId = AgencyAndIdLibrary.convertFromString(vjRef.getValue()); affects.setTripId(ServiceAlertLibrary.id(tripId)); for (AffectedCallStructure call : stopRefs.getCall()) { AgencyAndId stopId = AgencyAndIdLibrary.convertFromString(call.getStopPointRef().getValue()); affects.setStopId(ServiceAlertLibrary.id(stopId)); serviceAlert.addAffects(affects); } } } else if (hasTripRefs) { for (VehicleJourneyRefStructure vjRef : vj.getVehicleJourneyRef()) { AgencyAndId tripId = AgencyAndIdLibrary.convertFromString(vjRef.getValue()); affects.setTripId(ServiceAlertLibrary.id(tripId)); serviceAlert.addAffects(affects); } } else { for (AffectedCallStructure call : stopRefs.getCall()) { AgencyAndId stopId = AgencyAndIdLibrary.convertFromString(call.getStopPointRef().getValue()); affects.setStopId(ServiceAlertLibrary.id(stopId)); serviceAlert.addAffects(affects); } } } } ExtensionsStructure extension = affectsStructure.getExtensions(); if (extension != null && extension.getAny() != null) { Object ext = extension.getAny(); if (ext instanceof OneBusAwayAffects) { OneBusAwayAffects obaAffects = (OneBusAwayAffects) ext; Applications applications = obaAffects.getApplications(); if (applications != null && !CollectionsLibrary.isEmpty(applications.getAffectedApplication())) { List<AffectedApplicationStructure> apps = applications.getAffectedApplication(); for (AffectedApplicationStructure sApp : apps) { Affects.Builder affects = Affects.newBuilder(); affects.setApplicationId(sApp.getApiKey()); serviceAlert.addAffects(affects); } } } } } private void handleConsequences(PtSituationElementStructure ptSituation, ServiceAlert.Builder serviceAlert) { PtConsequencesStructure consequences = ptSituation.getConsequences(); if (consequences == null || consequences.getConsequence() == null) return; for (PtConsequenceStructure consequence : consequences.getConsequence()) { Consequence.Builder builder = Consequence.newBuilder(); if (consequence.getCondition() != null) builder.setEffect(getConditionAsEffect(consequence.getCondition())); ExtensionsStructure extensions = consequence.getExtensions(); if (extensions != null) { Object obj = extensions.getAny(); if (obj instanceof OneBusAwayConsequence) { OneBusAwayConsequence obaConsequence = (OneBusAwayConsequence) obj; if (obaConsequence.getDiversionPath() != null) builder.setDetourPath(obaConsequence.getDiversionPath()); } } if (builder.hasDetourPath() || builder.hasEffect()) serviceAlert.addConsequence(builder); } } private Effect getConditionAsEffect(ServiceConditionEnumeration condition) { switch (condition) { case CANCELLED: case NO_SERVICE: return Effect.NO_SERVICE; case DELAYED: return Effect.SIGNIFICANT_DELAYS; case DIVERTED: return Effect.DETOUR; case ADDITIONAL_SERVICE: case EXTENDED_SERVICE: case SHUTTLE_SERVICE: case SPECIAL_SERVICE: case REPLACEMENT_SERVICE: return Effect.ADDITIONAL_SERVICE; case DISRUPTED: case INTERMITTENT_SERVICE: case SHORT_FORMED_SERVICE: return Effect.REDUCED_SERVICE; case ALTERED: case ARRIVES_EARLY: case REPLACEMENT_TRANSPORT: case SPLITTING_TRAIN: return Effect.MODIFIED_SERVICE; case ON_TIME: case FULL_LENGTH_SERVICE: case NORMAL_SERVICE: return Effect.OTHER_EFFECT; case UNDEFINED_SERVICE_INFORMATION: case UNKNOWN: return Effect.UNKNOWN_EFFECT; default: _log.warn("unknown condition: " + condition); return Effect.UNKNOWN_EFFECT; } } private TranslatedString translation(DefaultedTextStructure text) { if (text == null) return null; String value = text.getValue(); if (value == null) return null; Translation.Builder translation = Translation.newBuilder(); translation.setText(value); if (text.getLang() != null) translation.setLanguage(text.getLang()); TranslatedString.Builder tsBuilder = TranslatedString.newBuilder(); tsBuilder.addTranslation(translation); return tsBuilder.build(); } }