org.onebusaway.transit_data_federation.impl.realtime.SiriLikeRealtimeSource.java Source code

Java tutorial

Introduction

Here is the source code for org.onebusaway.transit_data_federation.impl.realtime.SiriLikeRealtimeSource.java

Source

/**
 * Copyright (C) 2016 Cambridge Systematics, 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;

// todo refactor this
import static org.onebusaway.transit_data_federation.testing.UnitTestingSupport.blockConfiguration;

import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.lang.StringUtils;
import org.onebusaway.geospatial.model.CoordinatePoint;
import org.onebusaway.gtfs.model.AgencyAndId;
import org.onebusaway.gtfs.model.calendar.ServiceDate;
import org.onebusaway.realtime.api.VehicleLocationListener;
import org.onebusaway.realtime.api.VehicleLocationRecord;
import org.onebusaway.transit_data_federation.impl.realtime.SiriLikeRealtimeSource.NodesAndTimestamp;
import org.onebusaway.transit_data_federation.impl.realtime.gtfs_realtime.MonitoredResult;
import org.onebusaway.transit_data_federation.impl.transit_graph.TransitGraphDaoImpl;
import org.onebusaway.transit_data_federation.services.blocks.BlockGeospatialService;
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.transit_graph.BlockConfigurationEntry;
import org.onebusaway.transit_data_federation.services.transit_graph.ServiceIdActivation;
import org.onebusaway.transit_data_federation.services.transit_graph.TripEntry;
import org.onebusaway.util.SystemTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Inject data that is in a SIRI like format, but has enough differences
 * that a custom parser is required.
 *
 */
public class SiriLikeRealtimeSource {

    private static final Logger _log = LoggerFactory.getLogger(SiriLikeRealtimeSource.class);
    private static SimpleDateFormat ISO_DATE_SHORT_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
    private static SimpleDateFormat ISO_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSSSSSXXX");
    public List<String> _routeList = new ArrayList<String>();
    private XPathExpression mvjExpression;
    private XPathExpression tripIdExpression;
    private XPathExpression serviceDateExpression;
    private XPathExpression recordedAtExpression;
    private XPathExpression vehicleIdExpression;
    private XPathExpression latExpression;
    private XPathExpression lonExpression;
    private TransitGraphDaoImpl _transitGraphDao;
    private BlockGeospatialService _blockGeospatialService;
    private VehicleLocationListener _vehicleLocationListener;
    private ScheduledExecutorService _scheduledExecutorService;
    private ScheduledFuture<?> _refreshTask;
    private DocumentBuilderFactory factory;
    private DocumentBuilder builder;
    private String _agency;
    private String _baseUrl;
    private String _apiKey;

    private int _refreshInterval = 30;

    public void setApiKey(String key) {
        _apiKey = key;
    }

    public String getApiKey() {
        return _apiKey;
    }

    public void setBaseUrl(String url) {
        _baseUrl = url;
    }

    public String getUrl() {
        return _baseUrl;
    }

    public void setAgency(String agencyId) {
        _agency = agencyId;
    }

    private String getAgency() {
        return _agency;
    }

    public void setRefreshInterval(int refreshInterval) {
        _refreshInterval = refreshInterval;
    }

    @Autowired
    public void setTransitGraphDao(TransitGraphDaoImpl transitGraphDao) {
        _transitGraphDao = transitGraphDao;
    }

    @Autowired
    public void setBlockGeospatialService(BlockGeospatialService blockGeospatialService) {
        _blockGeospatialService = blockGeospatialService;
    }

    @Autowired
    public void setVehicleLocationListener(VehicleLocationListener vehicleLocationListener) {
        _vehicleLocationListener = vehicleLocationListener;
    }

    @Autowired
    public void setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) {
        _scheduledExecutorService = scheduledExecutorService;
    }

    @PostConstruct
    public void setup() throws Exception {
        ISO_DATE_FORMAT.setLenient(false);
        ISO_DATE_SHORT_FORMAT.setLenient(false);
        factory = DocumentBuilderFactory.newInstance();
        builder = factory.newDocumentBuilder();

        XPathFactory xPathfactory = XPathFactory.newInstance();
        XPath xpath = xPathfactory.newXPath();

        mvjExpression = xpath.compile("/Siri/VehicleMonitoringDelivery/VehicleActivity/MonitoredVehicleJourney");
        tripIdExpression = xpath.compile("FramedVehicleJourneyRef/DatedVehicleJourneyRef/text()");
        serviceDateExpression = xpath.compile("FramedVehicleJourneyRef/DataFrameRef/text()");
        recordedAtExpression = xpath
                .compile("/Siri/VehicleMonitoringDelivery/VehicleActivity/RecordedAtTime/text()");
        vehicleIdExpression = xpath.compile("VehicleRef/text()");
        latExpression = xpath.compile("VehicleLocation/Latitude/text()");
        lonExpression = xpath.compile("VehicleLocation/Longitude/text()");
        if (_refreshInterval > 0) {
            _refreshTask = _scheduledExecutorService.scheduleAtFixedRate(new RefreshTask(), 0, _refreshInterval,
                    TimeUnit.SECONDS);
        }
    }

    @PreDestroy
    public void stop() {
        if (_refreshTask != null) {
            _refreshTask.cancel(true);
            _refreshTask = null;
        }
    }

    public void setRoutes(List<String> routes) {
        _routeList = routes;
    }

    public List<String> getRoutes() {
        // for now routes are statically configured via spring
        return _routeList;
    }

    public void refresh() throws Exception {
        MonitoredResult result = new MonitoredResult();
        handleUpdates(result);
    }

    private synchronized void handleUpdates(MonitoredResult result) throws Exception {
        int vehicles = 0;
        int trips = 0;
        for (String route : getRoutes()) {
            NodesAndTimestamp nodesAndTimestamp = parseVehicles(
                    new URL(constructUrl(route, getApiKey(), getUrl())));
            _log.debug("found " + nodesAndTimestamp.getNodes().size() + " nodes");
            for (Node n : nodesAndTimestamp.getNodes()) {
                vehicles++;
                VehicleLocationRecord vlr = parse(n, nodesAndTimestamp.getTimestamp());
                if (vlr != null) {
                    trips++;
                    result.addMatchedTripId(vlr.getTripId().getId());
                    try {
                        _vehicleLocationListener.handleVehicleLocationRecord(vlr);
                    } catch (Exception any) {
                        // bury
                    }
                }
            }
        }
        _log.info("updated " + trips + " of " + vehicles + " for agency " + getAgency());
    }

    private String constructUrl(String route, String apiKey, String url) {
        return url + "?route=" + route + "&usertoken=" + apiKey;
    }

    // process URL into a series of fragments representing vehicle activity
    public NodesAndTimestamp parseVehicles(URL url) throws Exception {
        List<Node> vehicles = new ArrayList<Node>();
        Document doc = builder.parse(url.toString());
        String recordedAtStr = (String) recordedAtExpression.evaluate(doc, XPathConstants.STRING);
        long timestamp = parseDate(recordedAtStr).getTime();
        _log.debug("timestamp=" + new Date(timestamp) + " for date " + recordedAtStr);
        NodeList nl = (NodeList) this.mvjExpression.evaluate(doc, XPathConstants.NODESET);
        if (nl == null || nl.getLength() == 0) {
            _log.debug("no nodes found");
            return new NodesAndTimestamp(vehicles, timestamp);
        }
        for (int i = 0; i < nl.getLength(); i++) {
            vehicles.add(nl.item(i));
        }
        return new NodesAndTimestamp(vehicles, timestamp);
    }

    public VehicleLocationRecord parse(Node node, long timestamp) throws Exception {
        String tripId = (String) tripIdExpression.evaluate(node, XPathConstants.STRING);
        if (tripId == null) {
            _log.error("no trip for node=" + node);
            return null;
        }
        if (timestamp == 0) {
            timestamp = SystemTime.currentTimeMillis();
        }

        String serviceDateStr = (String) serviceDateExpression.evaluate(node, XPathConstants.STRING);
        Date serviceDate = parseServiceDate(serviceDateStr);

        VehicleLocationRecord vlr = new VehicleLocationRecord();
        try {
            vlr.setTimeOfLocationUpdate(timestamp);
            vlr.setTimeOfRecord(timestamp);
            vlr.setTripId(new AgencyAndId(getAgency(), tripId));
            vlr.setServiceDate(serviceDate.getTime());
            vlr.setVehicleId(new AgencyAndId(getAgency(),
                    (String) vehicleIdExpression.evaluate(node, XPathConstants.STRING)));
            vlr.setCurrentLocationLat(asDouble(latExpression.evaluate(node, XPathConstants.STRING)));
            vlr.setCurrentLocationLon(asDouble(lonExpression.evaluate(node, XPathConstants.STRING)));
            Integer scheduleDeviation = calculateScheduleDeviation(vlr);
            if (scheduleDeviation != null) {
                vlr.setScheduleDeviation(scheduleDeviation);
            }
        } catch (NumberFormatException nfe) {
            _log.info("caught nfe", nfe);
            return null;
        }
        _log.debug("return vlr=" + vlr);
        return vlr;
    }

    protected Integer calculateScheduleDeviation(VehicleLocationRecord vlr) {

        TripEntry tripEntry = this._transitGraphDao.getTripEntryForId(vlr.getTripId());
        // unit tests don't have a populated transit graph so fall back on scheduled time from feed
        if (tripEntry != null) {
            // todo this is a side effect
            vlr.setBlockId(tripEntry.getBlock().getId());
            long time = vlr.getTimeOfLocationUpdate() / 1000;
            double lat = vlr.getCurrentLocationLat();
            double lon = vlr.getCurrentLocationLon();
            long serviceDateTime = vlr.getServiceDate();
            long effectiveScheduleTimeSeconds = getEffectiveScheduleTime(tripEntry, lat, lon, time,
                    serviceDateTime);
            long effectiveScheduleTime = effectiveScheduleTimeSeconds + (serviceDateTime / 1000);
            int deviation = (int) (time - effectiveScheduleTime);
            _log.debug("deviation(" + vlr.getVehicleId() + " is " + deviation + " time=" + new Date(time * 1000)
                    + ", effectiveScheduleTime=" + new Date(effectiveScheduleTime * 1000));
            return deviation;
        }

        return null;
    }

    private long getEffectiveScheduleTime(TripEntry trip, double lat, double lon, long timestamp,
            long serviceDate) {

        ServiceIdActivation serviceIds = new ServiceIdActivation(trip.getServiceId());
        // todo!
        BlockConfigurationEntry blockConfig = blockConfiguration(trip.getBlock(), serviceIds, trip);
        BlockInstance block = new BlockInstance(blockConfig, serviceDate);
        CoordinatePoint location = new CoordinatePoint(lat, lon);

        ScheduledBlockLocation loc = _blockGeospatialService.getBestScheduledBlockLocationForLocation(block,
                location, timestamp, 0, trip.getTotalTripDistance());

        return loc.getScheduledTime();
    }

    private double asDouble(Object obj) {
        String s = (String) obj;
        return Double.parseDouble(s);
    }

    public Date parseShortDate(String s) throws Exception {
        return ISO_DATE_SHORT_FORMAT.parse(s);
    }

    public Date parseDate(String s) throws Exception {
        int endPos = "yyyy-MM-ddTHH:mm:ss.SSS".length();
        // we can't convince Java's Simple Date to parse millisecond to 7 digit precision
        s = s.substring(0, endPos - 1) + s.substring((s.length() - 7), s.length());

        return ISO_DATE_FORMAT.parse(s);
    }

    public Date parseServiceDate(String serviceDateStr) throws Exception {
        Date d = ISO_DATE_SHORT_FORMAT.parse(serviceDateStr);
        return new ServiceDate(d).getAsDate();
    }

    private class RefreshTask implements Runnable {

        @Override
        public void run() {
            try {
                refresh();
            } catch (Throwable ex) {
                _log.warn("Error updating from GTFS-realtime data sources", ex);
            }
        }
    }

    public static class NodesAndTimestamp {
        private List<Node> _nodes;
        private long _timestamp;

        public NodesAndTimestamp(List<Node> nodes, long timestamp) {
            this._nodes = nodes;
            this._timestamp = timestamp;
        }

        public List<Node> getNodes() {
            return _nodes;
        }

        public long getTimestamp() {
            return _timestamp;
        }
    }

}