/*
* Copyright (C) 2014 Kurt Raschke <kurt@kurtraschke.com>
* Copyright (C) 2012 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 com.kurtraschke.wmata.gtfsrealtime;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.Alerts;
import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.TripUpdates;
import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeGuiceBindingTypes.VehiclePositions;
import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeIncrementalUpdate;
import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeLibrary;
import org.onebusaway.gtfs_realtime.exporter.GtfsRealtimeSink;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Sets;
import com.google.transit.realtime.GtfsRealtime.Alert;
import com.google.transit.realtime.GtfsRealtime.EntitySelector;
import com.google.transit.realtime.GtfsRealtime.FeedEntity;
import com.google.transit.realtime.GtfsRealtime.Position;
import com.google.transit.realtime.GtfsRealtime.TripDescriptor;
import com.google.transit.realtime.GtfsRealtime.TripUpdate;
import com.google.transit.realtime.GtfsRealtime.VehicleDescriptor;
import com.google.transit.realtime.GtfsRealtime.VehiclePosition;
import com.kurtraschke.wmata.gtfsrealtime.api.alerts.Item;
import com.kurtraschke.wmata.gtfsrealtime.api.buspositions.BusPosition;
import com.kurtraschke.wmata.gtfsrealtime.services.WMATAAPIService;
import com.kurtraschke.wmata.gtfsrealtime.services.WMATARouteMapperService;
import com.kurtraschke.wmata.gtfsrealtime.services.WMATATripMapperService;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Element;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
/**
* This class produces GTFS-realtime trip updates and vehicle positions by
* periodically polling the custom WMATA vehicle data API and converting the
* resulting vehicle data into the GTFS-realtime format.
*
*
*/
@Singleton
public class GTFSRealtimeProviderImpl {
private static final Logger _log = LoggerFactory.getLogger(GTFSRealtimeProviderImpl.class);
private ScheduledExecutorService _executor;
private WMATAAPIService _api;
private WMATARouteMapperService _routeMapperService;
private WMATATripMapperService _tripMapperService;
private CacheManager _cacheManager;
private Cache _alertIDCache;
private GtfsRealtimeSink _vehiclePositionsSink;
private GtfsRealtimeSink _tripUpdatesSink;
private GtfsRealtimeSink _alertsSink;
private Map<String, Date> lastUpdateByVehicle = new HashMap<>();
private Map<UUID, Date> lastUpdateByAlert = new HashMap<>();
private int _vehicleRefreshInterval;
private int _alertRefreshInterval;
@Inject
public void setVehiclePositionsSink(@VehiclePositions
GtfsRealtimeSink sink) {
_vehiclePositionsSink = sink;
}
@Inject
public void setTripUpdateSink(@TripUpdates
GtfsRealtimeSink sink) {
_tripUpdatesSink = sink;
}
@Inject
public void setAlertsSink(@Alerts
GtfsRealtimeSink sink) {
_alertsSink = sink;
}
@Inject
public void setWMATAAPIService(WMATAAPIService api) {
_api = api;
}
@Inject
public void setWMATARouteMapperService(WMATARouteMapperService mapperService) {
_routeMapperService = mapperService;
}
@Inject
public void setWMATATripMapperService(WMATATripMapperService tripMapperService) {
_tripMapperService = tripMapperService;
}
@Inject
public void setCacheManager(CacheManager cacheManager) {
_cacheManager = cacheManager;
}
@Inject
public void setAlertIDCache(@Named("caches.alertID")
Cache alertIDCache) {
_alertIDCache = alertIDCache;
}
@Inject
public void setVehicleRefreshInterval(@Named("refreshInterval.vehicles")
int vehicleRefreshInterval) {
_vehicleRefreshInterval = vehicleRefreshInterval;
}
@Inject
public void setAlertRefreshInterval(@Named("refreshInterval.alerts")
int alertRefreshInterval) {
_alertRefreshInterval = alertRefreshInterval;
}
/**
* The start method automatically starts up a recurring task that periodically
* downloads the latest vehicle and alert data from the WMATA API and
* processes them.
*/
@PostConstruct
public void start() {
_log.info("Starting GTFS-realtime service");
_executor = Executors.newSingleThreadScheduledExecutor();
_executor.scheduleWithFixedDelay(new VehiclesRefreshTask(), 0,
_vehicleRefreshInterval, TimeUnit.SECONDS);
_executor.scheduleWithFixedDelay(new AlertsRefreshTask(), 0,
_alertRefreshInterval, TimeUnit.SECONDS);
}
/**
* The stop method cancels the recurring vehicle data downloader task.
*/
@PreDestroy
public void stop() {
_log.info("Stopping GTFS-realtime service");
_executor.shutdownNow();
_cacheManager.shutdown();
}
/**
* This method downloads the latest vehicle data, processes each vehicle in
* turn, and create a GTFS-realtime feed of trip updates and vehicle positions
* as a result.
*
* @throws WMATAAPIException
*/
private void refreshVehicles() throws WMATAAPIException {
/**
* We download the vehicle details as an array of objects.
*/
List<BusPosition> busPositions = _api.downloadBusPositions().getBusPositions();
/**
* We iterate over every vehicle object.
*/
for (BusPosition bp : busPositions) {
// checkConsistency(bp);
if ((!lastUpdateByVehicle.containsKey(bp.getVehicleID()))
|| bp.getDateTime().after(lastUpdateByVehicle.get(bp.getVehicleID()))) {
try {
processVehicle(bp);
} catch (Exception e) {
_log.warn(
"Error constructing update for vehicle " + bp.getVehicleID()
+ " on route " + bp.getRouteID() + " to "
+ bp.getTripHeadsign(), e);
}
}
}
_log.info("vehicles extracted: " + busPositions.size());
}
private void checkConsistency(BusPosition bp) {
boolean endAfterStart;
boolean timestampWithinTrip;
endAfterStart = bp.getTripEndTime().after(bp.getTripStartTime());
timestampWithinTrip = bp.getDateTime().after(bp.getTripStartTime())
&& bp.getDateTime().before(bp.getTripEndTime());
if (!endAfterStart || !timestampWithinTrip) {
StringBuilder sb = new StringBuilder();
sb.append("Update for vehicle ");
sb.append(bp.getVehicleID());
sb.append(" on route ");
sb.append(bp.getRouteID());
sb.append(" is inconsistent: ");
if (!endAfterStart) {
sb.append("\nTrip end time precedes trip start time");
}
if (!timestampWithinTrip) {
sb.append("\nUpdate timestamp not between trip start and end times");
}
_log.warn(sb.toString());
}
}
private void processVehicle(BusPosition bp) throws WMATAAPIException {
String route = bp.getRouteID();
String vehicle = bp.getVehicleID();
Date dateTime = bp.getDateTime();
float lat = bp.getLat();
float lon = bp.getLon();
float deviation = bp.getDeviation();
AgencyAndId gtfsRouteID;
AgencyAndId gtfsTripID = null;
gtfsRouteID = _routeMapperService.getRouteMapping(route);
if (gtfsRouteID != null) {
gtfsTripID = _tripMapperService.getTripMapping(bp);
}
/**
* We construct a TripDescriptor and VehicleDescriptor, which will be used
* in both trip updates and vehicle positions to identify the trip and
* vehicle.
*/
TripDescriptor.Builder tripDescriptor = TripDescriptor.newBuilder();
if (gtfsRouteID != null) {
tripDescriptor.setRouteId(gtfsRouteID.getId());
}
if (gtfsTripID != null) {
tripDescriptor.setTripId(gtfsTripID.getId());
}
VehicleDescriptor.Builder vehicleDescriptor = VehicleDescriptor.newBuilder();
vehicleDescriptor.setId(vehicle);
/**
* To construct our TripUpdate, we create a stop-time arrival event for the
* first stop for the vehicle, with the specified arrival delay. We add the
* stop-time update to a TripUpdate builder, along with the trip and vehicle
* descriptors.
*/
if (gtfsTripID != null) {
// WMATA API is positive for delay, negative for early (in minutes)
// GTFS-realtime is positive for delay, negative for early (in seconds)
int delay = Math.round(deviation * 60);
TripUpdate.Builder tripUpdate = TripUpdate.newBuilder();
tripUpdate.setDelay(delay);
tripUpdate.setTrip(tripDescriptor);
tripUpdate.setVehicle(vehicleDescriptor);
/**
* Create a new feed entity to wrap the trip update and add it to the
* GTFS-realtime trip updates feed.
*/
FeedEntity.Builder tripUpdateEntity = FeedEntity.newBuilder();
tripUpdateEntity.setId(vehicle);
tripUpdateEntity.setTripUpdate(tripUpdate);
GtfsRealtimeIncrementalUpdate tripUpdateUpdate = new GtfsRealtimeIncrementalUpdate();
tripUpdateUpdate.addUpdatedEntity(tripUpdateEntity.build());
_tripUpdatesSink.handleIncrementalUpdate(tripUpdateUpdate);
}
/**
* To construct our VehiclePosition, we create a position for the vehicle.
* We add the position to a VehiclePosition builder, along with the trip and
* vehicle descriptors.
*/
Position.Builder position = Position.newBuilder();
position.setLatitude(lat);
position.setLongitude(lon);
VehiclePosition.Builder vehiclePosition = VehiclePosition.newBuilder();
vehiclePosition.setTimestamp(dateTime.getTime() / 1000L);
vehiclePosition.setPosition(position);
vehiclePosition.setTrip(tripDescriptor);
vehiclePosition.setVehicle(vehicleDescriptor);
/**
* Create a new feed entity to wrap the vehicle position and add it to the
* GTFS-realtime vehicle positions feed.
*/
FeedEntity.Builder vehiclePositionEntity = FeedEntity.newBuilder();
vehiclePositionEntity.setId(vehicle);
vehiclePositionEntity.setVehicle(vehiclePosition);
GtfsRealtimeIncrementalUpdate vehiclePositionUpdate = new GtfsRealtimeIncrementalUpdate();
vehiclePositionUpdate.addUpdatedEntity(vehiclePositionEntity.build());
_vehiclePositionsSink.handleIncrementalUpdate(vehiclePositionUpdate);
lastUpdateByVehicle.put(bp.getVehicleID(), bp.getDateTime());
}
private void refreshAlerts() throws WMATAAPIException {
List<Item> busAlerts = _api.downloadBusAlerts().getChannel().getItems();
List<Item> railAlerts = _api.downloadRailAlerts().getChannel().getItems();
Set<UUID> currentAlertIDs = new HashSet<>();
for (Item theAlert : Iterables.concat(busAlerts, railAlerts)) {
if ((!lastUpdateByAlert.containsKey(theAlert.getGuid()))
|| theAlert.getPubDate().after(
lastUpdateByAlert.get(theAlert.getGuid()))) {
Alert.Builder alert = Alert.newBuilder();
alert.setDescriptionText(GtfsRealtimeLibrary.getTextAsTranslatedString(theAlert.getDescription()));
String[] routes = theAlert.getTitle().split(", ");
for (String route : routes) {
AgencyAndId gtfsRoute = _routeMapperService.getRouteMapping(route);
if (gtfsRoute != null) {
EntitySelector.Builder entity = EntitySelector.newBuilder();
entity.setRouteId(gtfsRoute.getId());
alert.addInformedEntity(entity);
}
}
if (alert.getInformedEntityCount() > 0) {
FeedEntity.Builder alertEntity = FeedEntity.newBuilder();
alertEntity.setId(theAlert.getGuid().toString());
alertEntity.setAlert(alert);
GtfsRealtimeIncrementalUpdate alertUpdate = new GtfsRealtimeIncrementalUpdate();
alertUpdate.addUpdatedEntity(alertEntity.build());
_alertsSink.handleIncrementalUpdate(alertUpdate);
_alertIDCache.put(new Element(theAlert.getGuid(), null));
lastUpdateByAlert.put(theAlert.getGuid(), theAlert.getPubDate());
}
}
currentAlertIDs.add(theAlert.getGuid());
}
/*
* Flush the cache manually so we do not lose any deleted alert IDs in the
* event of an unclean shutdown.
*/
_alertIDCache.flush();
@SuppressWarnings("unchecked")
ImmutableSet<UUID> allAlertIDs = ImmutableSet.copyOf(_alertIDCache.getKeysWithExpiryCheck());
/*
* If an alert was in the feed previously, and is not now, then add it back
* to the feed with the isDeleted flag set, so clients will remove it from
* their UI.
*/
for (UUID removedAlert : Sets.difference(allAlertIDs, currentAlertIDs)) {
GtfsRealtimeIncrementalUpdate alertUpdate = new GtfsRealtimeIncrementalUpdate();
alertUpdate.addDeletedEntity(removedAlert.toString());
_alertsSink.handleIncrementalUpdate(alertUpdate);
lastUpdateByAlert.remove(removedAlert);
}
_log.info("alerts extracted: " + (railAlerts.size() + busAlerts.size()));
}
/**
* Task that will download new vehicle data from the remote data source when
* executed.
*/
private class VehiclesRefreshTask implements Runnable {
@Override
public void run() {
try {
_log.info("Refreshing vehicles");
refreshVehicles();
} catch (Exception ex) {
_log.warn("Error in vehicle refresh task", ex);
}
}
}
/**
* Task that will download new alert data from the remote data source when
* executed.
*/
private class AlertsRefreshTask implements Runnable {
@Override
public void run() {
try {
_log.info("Refreshing alerts");
refreshAlerts();
} catch (Exception ex) {
_log.warn("Error in alert refresh task", ex);
}
}
}
}