/**
* Copyright (C) 2011 Brian Ferris <bdferris@onebusaway.org>
*
* 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.orbcad;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.onebusaway.csv_entities.EntityHandler;
import org.onebusaway.geospatial.services.SphericalGeometryLibrary;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.realtime.api.EVehiclePhase;
import org.onebusaway.realtime.api.VehicleLocationListener;
import org.onebusaway.realtime.api.VehicleLocationRecord;
import org.onebusaway.transit_data_federation.impl.realtime.gtfs_realtime.MonitoredDataSource;
import org.onebusaway.transit_data_federation.impl.realtime.gtfs_realtime.MonitoredResult;
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.blocks.ScheduledBlockLocation;
import org.onebusaway.transit_data_federation.services.blocks.ScheduledBlockLocationService;
import org.onebusaway.transit_data_federation.services.transit_graph.BlockConfigurationEntry;
import org.onebusaway.transit_data_federation.services.transit_graph.BlockEntry;
import org.onebusaway.transit_data_federation.services.transit_graph.BlockTripEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedResource;
@ManagedResource("org.onebusaway.transit_data_federation.impl.realtime.orbcad:name=OrbcadRecordFtpSource")
public abstract class AbstractOrbcadRecordSource implements MonitoredDataSource {
private static final int REFRESH_INTERVAL_IN_SECONDS = 30;
private static Logger _log = LoggerFactory.getLogger(AbstractOrbcadRecordSource.class);
protected ScheduledExecutorService _executor = Executors.newSingleThreadScheduledExecutor();
private List<VehicleLocationRecord> _records = new ArrayList<VehicleLocationRecord>();
private int _refreshInterval = REFRESH_INTERVAL_IN_SECONDS;
private long _lastRefresh = 0;
private VehicleLocationListener _vehicleLocationListener;
private Map<String, List<String>> _blockIdMapping = new HashMap<String, List<String>>();
private BlockCalendarService _blockCalendarService;
private ScheduledBlockLocationService _scheduledBlockLocationService;
protected List<String> _agencyIds;
private File _blockIdMappingFile;
private double _motionThreshold = 30;
/****
*
****/
private Map<AgencyAndId, VehicleLocationRecord> _lastRecordByVehicleId = new HashMap<AgencyAndId, VehicleLocationRecord>();
/****
* Statistics
****/
private transient int _recordsTotal = 0;
private transient int _recordsWithoutScheduleDeviation = 0;
private transient int _recordsWithoutBlockId = 0;
private transient int _recordsWithoutBlockIdInGraph = 0;
private transient int _recordsWithoutServiceDate = 0;
private transient int _recordsValid = 0;
private MonitoredResult _monitoredResult, _currentResult = new MonitoredResult();
public void setRefreshInterval(int refreshIntervalInSeconds) {
_refreshInterval = refreshIntervalInSeconds;
}
public void setAgencyId(String agencyId) {
_agencyIds = Arrays.asList(agencyId);
}
public void setAgencyIds(List<String> agencyIds) {
_agencyIds = agencyIds;
}
public List<String> getAgencyIds() {
return _agencyIds;
}
public void setBlockIdMappingFile(File blockIdMappingFile) {
_blockIdMappingFile = blockIdMappingFile;
}
@Autowired
public void setVehicleLocationListener(
VehicleLocationListener vehicleLocationListener) {
_vehicleLocationListener = vehicleLocationListener;
}
@Autowired
public void setBlockCalendarService(BlockCalendarService blockCalendarService) {
_blockCalendarService = blockCalendarService;
}
@Autowired
public void setScheduledBlockLocationService(
ScheduledBlockLocationService scheduledBlockLocationService) {
_scheduledBlockLocationService = scheduledBlockLocationService;
}
public MonitoredResult getMonitoredResult() {
return _monitoredResult;
}
/****
* JMX Attributes
***/
@ManagedAttribute
public int getRecordsTotal() {
return _recordsTotal;
}
@ManagedAttribute
public int getRecordWithoutScheduleDeviation() {
return _recordsWithoutScheduleDeviation;
}
@ManagedAttribute
public int getRecordsWithoutBlockId() {
return _recordsWithoutBlockId;
}
@ManagedAttribute
public int getRecordsWithoutBlockIdInGraph() {
return _recordsWithoutBlockIdInGraph;
}
@ManagedAttribute
public int getRecordsWithoutServiceDate() {
return _recordsWithoutServiceDate;
}
@ManagedAttribute
public int getRecordsValid() {
return _recordsValid;
}
/****
* Setup and Teardown
****/
protected void start() throws SocketException, IOException {
loadBlockIdMapping();
setup();
_executor.scheduleAtFixedRate(new AvlRefreshTask(), 5,
_refreshInterval / 2, TimeUnit.SECONDS);
}
protected void stop() throws IOException {
_executor.shutdown();
}
/****
* Protected Methods
****/
protected void setup() {
}
protected abstract void handleRefresh() throws IOException;
/****
* Private Methods
****/
private void loadBlockIdMapping() throws FileNotFoundException, IOException {
try {
if (_blockIdMappingFile == null)
return;
BufferedReader reader = new BufferedReader(new FileReader(
_blockIdMappingFile));
String line = null;
while ((line = reader.readLine()) != null) {
int index = line.indexOf(',');
String from = line.substring(0, index);
String to = line.substring(index + 1);
List<String> toValues = _blockIdMapping.get(from);
if (toValues == null) {
toValues = new ArrayList<String>();
_blockIdMapping.put(from, toValues);
}
toValues.add(to);
}
reader.close();
} catch (Throwable ex) {
_log.warn("error loading block id mapping from file " + _blockIdMapping,
ex);
}
}
private BlockInstance getBlockInstanceForRecord(OrbcadRecord record) {
long recordTime = record.getTime() * 1000;
long timeFrom = recordTime - 30 * 60 * 1000;
long timeTo = recordTime + 30 * 60 * 1000;
List<AgencyAndId> blockIds = getBlockIdsForRecord(record);
List<BlockInstance> allInstances = new ArrayList<BlockInstance>();
for (AgencyAndId blockId : blockIds) {
List<BlockInstance> instances = _blockCalendarService.getActiveBlocks(
blockId, timeFrom, timeTo);
allInstances.addAll(instances);
}
// TODO : We currently assume we don't have overlapping blocks.
if (allInstances.size() != 1)
return null;
return allInstances.get(0);
}
private List<AgencyAndId> getBlockIdsForRecord(OrbcadRecord record) {
List<AgencyAndId> blockIds = new ArrayList<AgencyAndId>();
String rawBlockId = Integer.toString(record.getBlock());
List<String> rawBlockIds = _blockIdMapping.get(rawBlockId);
if (rawBlockIds == null)
rawBlockIds = Arrays.asList(rawBlockId);
for (String agencyId : _agencyIds) {
for (String rawId : rawBlockIds) {
blockIds.add(new AgencyAndId(agencyId, rawId));
}
}
return blockIds;
}
protected class AvlRefreshTask implements Runnable {
public void run() {
try {
_log.debug("checking if we need to refresh");
synchronized (this) {
long t = System.currentTimeMillis();
if (_lastRefresh + _refreshInterval * 1000 > t)
return;
_lastRefresh = t;
}
_log.debug("refresh requested");
preHandleRefresh();
handleRefresh();
postHandleRefresh();
try {
_vehicleLocationListener.handleVehicleLocationRecords(_records);
} catch (Throwable ex) {
_log.warn("error passing schedule adherence records to listener", ex);
}
_records.clear();
_log.debug("refresh complete");
} catch (Throwable ex) {
_log.warn("error refreshing data", ex);
}
}
private void preHandleRefresh() {
_currentResult = new MonitoredResult();
_currentResult.setAgencyIds(_agencyIds);
}
private void postHandleRefresh() {
_log.debug("" + _agencyIds + ": runningCount=" + _recordsTotal + ", currentCount=" + _currentResult.getRecordsTotal());
if (_currentResult.getRecordsTotal() > 0) {
// only consider it a successful update if we got some records
// ftp impl may not have a new file to download
_monitoredResult = _currentResult;
}
}
}
protected class RecordHandler implements EntityHandler {
@Override
public void handleEntity(Object bean) {
OrbcadRecord record = (OrbcadRecord) bean;
_recordsTotal++;
_currentResult.addRecordTotal();
if (!record.hasScheduleDeviation()) {
_recordsWithoutScheduleDeviation++;
return;
}
if (record.getBlock() == 0) {
_recordsWithoutBlockId++;
return;
}
BlockInstance blockInstance = getBlockInstanceForRecord(record);
if (blockInstance == null) {
_recordsWithoutServiceDate++;
return;
}
BlockConfigurationEntry blockConfig = blockInstance.getBlock();
BlockEntry block = blockConfig.getBlock();
AgencyAndId blockId = block.getId();
VehicleLocationRecord message = new VehicleLocationRecord();
message.setBlockId(blockId);
message.setServiceDate(blockInstance.getServiceDate());
message.setTimeOfRecord(record.getTime() * 1000);
message.setTimeOfLocationUpdate(message.getTimeOfRecord());
// In Orbcad, +scheduleDeviation means the bus is early and -schedule
// deviation means bus is late, which is opposite the
// ScheduleAdherenceRecord convention
message.setScheduleDeviation(-record.getScheduleDeviation());
message.setVehicleId(new AgencyAndId(blockId.getAgencyId(),
Integer.toString(record.getVehicleId())));
if (record.hasLat() && record.hasLon()) {
message.setCurrentLocationLat(record.getLat());
message.setCurrentLocationLon(record.getLon());
}
int effectiveScheduleTime = (int) (record.getTime() - blockInstance.getServiceDate() / 1000);
int adjustedScheduleTime = effectiveScheduleTime
- record.getScheduleDeviation();
ScheduledBlockLocation location = _scheduledBlockLocationService.getScheduledBlockLocationFromScheduledTime(
blockConfig, adjustedScheduleTime);
if (location != null) {
message.setDistanceAlongBlock(location.getDistanceAlongBlock());
BlockTripEntry activeTrip = location.getActiveTrip();
if (activeTrip != null) {
message.setTripId(activeTrip.getTrip().getId());
_currentResult.addMatchedTripId(activeTrip.getTrip().getId().toString());
} else {
_log.error("invalid trip for location=" + location);
_currentResult.addUnmatchedTripId(blockId.toString()); // this isn't exactly right
}
// Are we at the start of the block?
if (location.getDistanceAlongBlock() == 0) {
VehicleLocationRecord lastRecord = _lastRecordByVehicleId.get(message.getVehicleId());
boolean inMotion = true;
if (lastRecord != null && lastRecord.isCurrentLocationSet()
&& message.isCurrentLocationSet()) {
double d = SphericalGeometryLibrary.distance(
lastRecord.getCurrentLocationLat(),
lastRecord.getCurrentLocationLon(),
message.getCurrentLocationLat(),
message.getCurrentLocationLon());
inMotion = d > _motionThreshold;
}
if (inMotion)
message.setPhase(EVehiclePhase.DEADHEAD_BEFORE);
else
message.setPhase(EVehiclePhase.LAYOVER_BEFORE);
}
}
_records.add(message);
_recordsValid++;
_lastRecordByVehicleId.put(message.getVehicleId(), message);
}
}
}