org.onebusaway.nyc.vehicle_tracking.impl.inference.VehicleInferenceInstance.java Source code

Java tutorial

Introduction

Here is the source code for org.onebusaway.nyc.vehicle_tracking.impl.inference.VehicleInferenceInstance.java

Source

/**
 * Copyright (c) 2011 Metropolitan Transportation Authority
 * 
 * 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.nyc.vehicle_tracking.impl.inference;

import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

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.nyc.transit_data.model.NycQueuedInferredLocationBean;
import org.onebusaway.nyc.transit_data.model.NycVehicleManagementStatusBean;
import org.onebusaway.nyc.transit_data_federation.bundle.tasks.stif.model.RunTripEntry;
import org.onebusaway.nyc.transit_data_federation.model.tdm.OperatorAssignmentItem;
import org.onebusaway.nyc.transit_data_federation.services.nyc.BaseLocationService;
import org.onebusaway.nyc.transit_data_federation.services.nyc.DestinationSignCodeService;
import org.onebusaway.nyc.transit_data_federation.services.nyc.RunService;
import org.onebusaway.nyc.transit_data_federation.services.tdm.OperatorAssignmentService;
import org.onebusaway.nyc.util.configuration.ConfigurationService;
import org.onebusaway.nyc.vehicle_tracking.impl.inference.state.BlockStateObservation;
import org.onebusaway.nyc.vehicle_tracking.impl.inference.state.JourneyPhaseSummary;
import org.onebusaway.nyc.vehicle_tracking.impl.inference.state.JourneyState;
import org.onebusaway.nyc.vehicle_tracking.impl.inference.state.MotionState;
import org.onebusaway.nyc.vehicle_tracking.impl.inference.state.VehicleState;
import org.onebusaway.nyc.vehicle_tracking.impl.particlefilter.BadProbabilityParticleFilterException;
import org.onebusaway.nyc.vehicle_tracking.impl.particlefilter.Particle;
import org.onebusaway.nyc.vehicle_tracking.impl.particlefilter.ParticleFilter;
import org.onebusaway.nyc.vehicle_tracking.impl.particlefilter.ParticleFilterException;
import org.onebusaway.nyc.vehicle_tracking.impl.particlefilter.ParticleFilterModel;
import org.onebusaway.nyc.vehicle_tracking.model.NycRawLocationRecord;
import org.onebusaway.nyc.vehicle_tracking.model.NycTestInferredLocationRecord;
import org.onebusaway.nyc.vehicle_tracking.model.library.RecordLibrary;
import org.onebusaway.nyc.vehicle_tracking.model.simulator.VehicleLocationDetails;
import org.onebusaway.realtime.api.EVehiclePhase;
import org.onebusaway.transit_data_federation.services.AgencyAndIdLibrary;
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.BlockEntry;
import org.onebusaway.transit_data_federation.services.transit_graph.BlockTripEntry;
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 com.google.common.collect.HashMultiset;
import com.google.common.collect.Multiset;
import com.google.common.collect.Sets;
import com.google.common.collect.TreeMultimap;
import com.google.common.collect.TreeMultiset;

public class VehicleInferenceInstance {

    private static Logger _log = LoggerFactory.getLogger(VehicleInferenceInstance.class);

    @Autowired
    private ConfigurationService _configurationService;

    @Autowired
    private BlockStateService _blockStateService;

    private DestinationSignCodeService _destinationSignCodeService;

    private BaseLocationService _baseLocationService;

    private OperatorAssignmentService _operatorAssignmentService;

    private RunService _runService;

    private long _automaticResetWindow = 20 * 60 * 1000;

    private Observation _previousObservation = null;

    private String _lastValidDestinationSignCode = null;

    private long _lastUpdateTime = 0;

    private long _lastLocationUpdateTime = 0;

    private NycTestInferredLocationRecord _nycTestInferredLocationRecord;

    private Multiset<Particle> _badParticles;

    private ParticleFilter<Observation> _particleFilter;

    public void setModel(ParticleFilterModel<Observation> model) {
        _particleFilter = new ParticleFilter<Observation>(model);
    }

    @Autowired
    public void setRunService(RunService runService) {
        _runService = runService;
    }

    @Autowired
    public void setOperatorAssignmentService(OperatorAssignmentService operatorAssignmentService) {
        _operatorAssignmentService = operatorAssignmentService;
    }

    public OperatorAssignmentService getOperatorAssignmentService() {
        return _operatorAssignmentService;
    }

    @Autowired
    public void setDestinationSignCodeService(DestinationSignCodeService destinationSignCodeService) {
        _destinationSignCodeService = destinationSignCodeService;
    }

    @Autowired
    public void setBaseLocationService(BaseLocationService baseLocationService) {
        _baseLocationService = baseLocationService;
    }

    /**
     * If we haven't received a GPS update in the specified window, the inference
     * engine is reset no matter what
     * 
     * @param automaticResetWindow
     */
    public void setAutomaticResetWindow(long automaticResetWindow) {
        _automaticResetWindow = automaticResetWindow;
    }

    /**
     * Process a NycRawLocationRecord, usually from a bus or the simulator.
     * 
     * @param record
     * @return true if the resulting inferred location record was successfully processed,
     *         otherwise false
     */
    public boolean handleUpdate(NycRawLocationRecord record) {

        /**
         * Choose the best timestamp based on device timestamp and received
         * timestamp
         */
        final long timestamp = RecordLibrary.getBestTimestamp(record.getTime(), record.getTimeReceived());
        _lastUpdateTime = timestamp;

        /**
         * If this record occurs BEFORE, or at the same time as, the most recent update, 
         * we take special action
         */
        if (timestamp <= _particleFilter.getTimeOfLastUpdated()) {
            final long backInTime = (long) (_particleFilter.getTimeOfLastUpdated() - timestamp);

            /**
             * If the difference is large, we reset the particle filter. Otherwise, we
             * simply ignore the out-of-order record
             */
            if (backInTime > 5 * 60 * 1000) {
                _log.info("resetting filter: time diff");
                _previousObservation = null;
                _particleFilter.reset();
            } else {
                _log.info("out-of-order record.  skipping update.");
                return false;
            }
        }

        /**
         * If it's been a while since we've last seen a record, reset the particle
         * filter and forget the previous observation
         */
        Boolean reportedRunIdChange = null;
        Boolean operatorIdChange = null;

        if (_previousObservation != null) {
            /**
             * We use an absolute value here, since we also want to reset if we go
             * back in time as well
             */
            final long delta = Math.abs(timestamp - _previousObservation.getTime());
            final NycRawLocationRecord lastRecord = _previousObservation.getRecord();

            reportedRunIdChange = !StringUtils.equals(lastRecord.getRunId(), record.getRunId());
            operatorIdChange = !StringUtils.equals(lastRecord.getOperatorId(), record.getOperatorId());

            if (delta > _automaticResetWindow) {
                _log.info("resetting inference for vid=" + record.getVehicleId() + " since it's been "
                        + (delta / 1000) + " seconds since the previous update");
                _previousObservation = null;
                _particleFilter.reset();
            }
        }

        /**
         * Recall that a vehicle might send a location update with missing lat-lon
         * if it's sitting at the curb with the engine turned off.
         */
        final boolean latlonMissing = record.locationDataIsMissing();
        if (latlonMissing) {
            /**
             * If we don't have a previous record, we can't use the previous lat-lon
             * to replace the missing values
             */
            if (_previousObservation == null) {
                _log.info("missing previous observation and current lat/lon:" + record.getVehicleId()
                        + ", skipping update.");
                return false;
            }

            final NycRawLocationRecord previousRecord = _previousObservation.getRecord();
            record.setLatitude(previousRecord.getLatitude());
            record.setLongitude(previousRecord.getLongitude());
        }

        final CoordinatePoint location = new CoordinatePoint(record.getLatitude(), record.getLongitude());

        /**
         * Sometimes, DSCs take the form "  11", where there is whitespace in there.
         * Let's clean it up.
         */
        String dsc = record.getDestinationSignCode();
        if (dsc != null) {
            dsc = dsc.trim();
            record.setDestinationSignCode(dsc);
        }

        String lastValidDestinationSignCode = null;

        if (dsc != null && !_destinationSignCodeService.isMissingDestinationSignCode(dsc)) {
            lastValidDestinationSignCode = dsc;
        } else if (_previousObservation != null) {
            lastValidDestinationSignCode = _lastValidDestinationSignCode;
        }

        final boolean atBase = _baseLocationService.getBaseNameForLocation(location) != null;
        final boolean atTerminal = false;
        final boolean outOfService = _destinationSignCodeService
                .isOutOfServiceDestinationSignCode(lastValidDestinationSignCode);
        final boolean hasValidDsc = !_destinationSignCodeService
                .isMissingDestinationSignCode(lastValidDestinationSignCode)
                && !_destinationSignCodeService.isUnknownDestinationSignCode(lastValidDestinationSignCode);

        Set<AgencyAndId> routeIds = new HashSet<AgencyAndId>();
        if (_previousObservation == null
                || !StringUtils.equals(_lastValidDestinationSignCode, lastValidDestinationSignCode)) {
            routeIds = _destinationSignCodeService
                    .getRouteCollectionIdsForDestinationSignCode(lastValidDestinationSignCode);
        } else {
            routeIds = _previousObservation.getDscImpliedRouteCollections();
        }

        RunResults runResults = null;
        if (_previousObservation == null || operatorIdChange == Boolean.TRUE
                || reportedRunIdChange == Boolean.TRUE) {
            runResults = findRunIdMatches(record);
        } else {
            runResults = updateRunIdMatches(record, _previousObservation.getRunResults());
        }

        final Observation observation = new Observation(timestamp, record, lastValidDestinationSignCode, atBase,
                atTerminal, outOfService, hasValidDsc, _previousObservation, routeIds, runResults);

        if (_previousObservation != null)
            _previousObservation.clearPreviousObservation();

        _previousObservation = observation;
        _nycTestInferredLocationRecord = null;
        _lastValidDestinationSignCode = lastValidDestinationSignCode;

        if (!latlonMissing)
            _lastLocationUpdateTime = timestamp;

        try {
            _particleFilter.updateFilter(timestamp, observation);
        } catch (final BadProbabilityParticleFilterException ex) {
            /**
             * If the particle filter hangs, we try one hard reset to see if that will
             * fix it
             */
            _log.warn("particle filter crashed for record - attempting reset: time=" + record.getTime()
                    + " timeReceived=" + record.getTimeReceived() + " vehicleId=" + record.getVehicleId()
                    + " cause=" + ex.getMessage());

            if (_badParticles == null)
                _badParticles = _particleFilter.getWeightedParticles();

            _particleFilter.reset();

            try {
                _particleFilter.updateFilter(timestamp, observation);
            } catch (final ParticleFilterException ex2) {
                _log.warn("particle filter crashed again: time=" + record.getTime() + " timeReceived="
                        + record.getTimeReceived() + " vehicleId=" + record.getVehicleId());
                throw new IllegalStateException(ex2);
            }
        } catch (final ParticleFilterException ex) {
            throw new IllegalStateException(ex);
        }

        return true;
    }

    /**
     * Pass a previous inference result as if it was ours, from the simulator.
     * 
     * @param record
     * @return true if the resulting inferred location record should be passed on,
     *         otherwise false.
     */
    public synchronized boolean handleBypassUpdate(NycTestInferredLocationRecord record) {
        _previousObservation = null;
        _nycTestInferredLocationRecord = record;
        _lastUpdateTime = record.getTimestamp();

        if (!record.locationDataIsMissing())
            _lastLocationUpdateTime = record.getTimestamp();

        return true;
    }

    /****
     * Simulator/debugging methods
     */
    public synchronized Multiset<Particle> getPreviousParticles() {
        return HashMultiset.create(_particleFilter.getWeightedParticles());
    }

    public synchronized Multiset<Particle> getCurrentParticles() {
        return HashMultiset.create(_particleFilter.getWeightedParticles());
    }

    public synchronized Multiset<Particle> getCurrentSampledParticles() {
        return HashMultiset.create(_particleFilter.getSampledParticles());
    }

    public synchronized List<JourneyPhaseSummary> getJourneySummaries() {
        final Particle particle = _particleFilter.getMostLikelyParticle();
        if (particle == null)
            return Collections.emptyList();
        final VehicleState state = particle.getData();
        return state.getJourneySummaries();
    }

    public NycTestInferredLocationRecord getCurrentState() {
        if (_previousObservation != null)
            return getMostRecentParticleAsNycTestInferredLocationRecord();
        else if (_nycTestInferredLocationRecord != null)
            return _nycTestInferredLocationRecord;
        else
            return null;
    }

    public ParticleFilter<Observation> getFilter() {
        return _particleFilter;
    }

    public VehicleLocationDetails getDetails() {
        final VehicleLocationDetails details = new VehicleLocationDetails();

        setLastRecordForDetails(details);

        if (_badParticles != null)
            details.setParticleFilterFailure(true);

        final Multiset<Particle> particles = TreeMultiset.create();
        particles.addAll(getCurrentParticles());
        details.setParticles(particles);

        return details;
    }

    public VehicleLocationDetails getBadParticleDetails() {
        final VehicleLocationDetails details = new VehicleLocationDetails();

        setLastRecordForDetails(details);

        if (_badParticles != null)
            details.setParticleFilterFailure(true);

        final Multiset<Particle> particles = HashMultiset.create();
        if (_badParticles != null)
            particles.addAll(_badParticles);
        details.setParticles(particles);

        return details;
    }

    /****
     * Service methods
     */
    public NycQueuedInferredLocationBean getCurrentStateAsNycQueuedInferredLocationBean() {
        final NycTestInferredLocationRecord tilr = getCurrentState();
        final NycQueuedInferredLocationBean record = RecordLibrary
                .getNycTestInferredLocationRecordAsNycQueuedInferredLocationBean(tilr);

        final Particle particle = _particleFilter.getMostLikelyParticle();
        if (particle == null)
            return null;

        final VehicleState state = particle.getData();
        final BlockStateObservation blockState = state.getBlockStateObservation();
        final Observation obs = state.getObservation();
        final NycRawLocationRecord nycRawRecord = obs.getRecord();
        record.setBearing(nycRawRecord.getBearing());

        if (blockState != null) {
            final ScheduledBlockLocation blockLocation = blockState.getBlockState().getBlockLocation();

            // set sched. dev. if we have a match in UTS and are therefore comfortable
            // saying that this schedule deviation is a true match to the schedule.
            if (blockState.isRunFormal()) {
                final int deviation = (int) ((record.getRecordTimestamp() - record.getServiceDate()) / 1000
                        - blockLocation.getScheduledTime());

                record.setScheduleDeviation(deviation);
            } else {
                record.setScheduleDeviation(null);
            }

            // distance along trip
            final BlockTripEntry activeTrip = blockLocation.getActiveTrip();
            final double distanceAlongTrip = blockLocation.getDistanceAlongBlock()
                    - activeTrip.getDistanceAlongBlock();
            record.setDistanceAlongTrip(distanceAlongTrip);
        }

        return record;
    }

    public NycVehicleManagementStatusBean getCurrentManagementState() {
        final NycVehicleManagementStatusBean record = new NycVehicleManagementStatusBean();

        final Particle particle = _particleFilter.getMostLikelyParticle();
        if (particle == null)
            return null;

        final VehicleState state = particle.getData();
        final Observation obs = state.getObservation();
        final NycRawLocationRecord nycRawRecord = obs.getRecord();
        final BlockStateObservation blockState = state.getBlockStateObservation();

        record.setUUID(nycRawRecord.getUuid());
        record.setInferenceIsEnabled(true);
        record.setLastUpdateTime(_lastUpdateTime);
        record.setLastLocationUpdateTime(_lastLocationUpdateTime);
        record.setAssignedRunId(obs.getOpAssignedRunId());
        record.setMostRecentObservedDestinationSignCode(nycRawRecord.getDestinationSignCode());
        record.setLastObservedLatitude(nycRawRecord.getLatitude());
        record.setLastObservedLongitude(nycRawRecord.getLongitude());
        record.setEmergencyFlag(nycRawRecord.isEmergencyFlag());

        if (blockState != null) {
            record.setLastInferredDestinationSignCode(blockState.getBlockState().getDestinationSignCode());
            record.setInferredRunId(blockState.getBlockState().getRunId());
            record.setInferenceIsFormal(blockState.isRunFormal());
        }

        return record;
    }

    /****
     * Private Methods
     ****/
    private void setLastRecordForDetails(VehicleLocationDetails details) {
        NycRawLocationRecord lastRecord = null;
        if (_previousObservation != null)
            lastRecord = _previousObservation.getRecord();

        if (lastRecord == null && _nycTestInferredLocationRecord != null) {
            lastRecord = new NycRawLocationRecord();
            lastRecord.setDestinationSignCode(_nycTestInferredLocationRecord.getDsc());
            lastRecord.setTime(_nycTestInferredLocationRecord.getTimestamp());
            lastRecord.setTimeReceived(_nycTestInferredLocationRecord.getTimestamp());
            lastRecord.setLatitude(_nycTestInferredLocationRecord.getLat());
            lastRecord.setLongitude(_nycTestInferredLocationRecord.getLon());
            lastRecord.setOperatorId(_nycTestInferredLocationRecord.getOperatorId());
            final String[] runInfo = StringUtils
                    .splitByWholeSeparator(_nycTestInferredLocationRecord.getReportedRunId(), "-");

            if (runInfo != null && runInfo.length > 0) {
                lastRecord.setRunRouteId(runInfo[0]);
                if (runInfo.length > 1)
                    lastRecord.setRunNumber(runInfo[1]);
            }
        }

        details.setLastObservation(lastRecord);
    }

    /**
     * This method rechecks the operator assignment and returns either the old
     * run-results or new ones.
     * 
     * @param observation
     * @param results
     * @return
     */
    private RunResults updateRunIdMatches(NycRawLocationRecord observation, RunResults results) {

        final Date obsDate = new Date(observation.getTime());

        String opAssignedRunId = null;
        final String operatorId = observation.getOperatorId();

        final boolean noOperatorIdGiven = StringUtils.isEmpty(operatorId)
                || StringUtils.containsOnly(operatorId, "0");

        final Set<AgencyAndId> routeIds = Sets.newHashSet();
        if (!noOperatorIdGiven) {
            try {
                final OperatorAssignmentItem oai = _operatorAssignmentService
                        .getOperatorAssignmentItemForServiceDate(new ServiceDate(obsDate),
                                new AgencyAndId(observation.getVehicleId().getAgencyId(), operatorId));

                if (oai != null) {
                    if (_runService.isValidRunId(oai.getRunId())) {
                        opAssignedRunId = oai.getRunId();

                        /*
                         * same results; we're done
                         */
                        if (opAssignedRunId.equals(results.getAssignedRunId()))
                            return results;

                        /*
                         * new assigned run-id; recompute the routes
                         */
                        routeIds.addAll(_runService.getRoutesForRunId(opAssignedRunId));
                        for (final String runId : results.getFuzzyMatches()) {
                            routeIds.addAll(_runService.getRoutesForRunId(runId));
                        }
                        return new RunResults(opAssignedRunId, results.getFuzzyMatches(),
                                results.getBestFuzzyDist(), routeIds);
                    }
                }
            } catch (final Exception e) {
                _log.warn(e.getMessage());
            }
        }
        /*
         * if we're here, then the op-assignment call probably failed, so just
         * return the old results
         */
        return results;
    }

    private RunResults findRunIdMatches(NycRawLocationRecord observation) {
        final Date obsDate = new Date(observation.getTime());

        String opAssignedRunId = null;
        final String operatorId = observation.getOperatorId();

        final boolean noOperatorIdGiven = StringUtils.isEmpty(operatorId)
                || StringUtils.containsOnly(operatorId, "0");

        final Set<AgencyAndId> routeIds = Sets.newHashSet();
        if (!noOperatorIdGiven) {
            try {
                final OperatorAssignmentItem oai = _operatorAssignmentService
                        .getOperatorAssignmentItemForServiceDate(new ServiceDate(obsDate),
                                new AgencyAndId(observation.getVehicleId().getAgencyId(), operatorId));

                if (oai != null) {
                    if (_runService.isValidRunId(oai.getRunId())) {
                        opAssignedRunId = oai.getRunId();
                        routeIds.addAll(_runService.getRoutesForRunId(opAssignedRunId));
                    }
                }
            } catch (final Exception e) {
                _log.warn(e.getMessage());
            }
        }

        Set<String> fuzzyMatches = Collections.emptySet();
        final String reportedRunId = observation.getRunId();
        Integer bestFuzzyDistance = null;
        if (StringUtils.isEmpty(opAssignedRunId) && !noOperatorIdGiven) {
            _log.info("no assigned run found for operator=" + operatorId);
        }

        if (StringUtils.isNotEmpty(reportedRunId)
                && !StringUtils.containsOnly(reportedRunId, new char[] { '0', '-' })) {

            try {
                final TreeMultimap<Integer, String> fuzzyReportedMatches = _runService
                        .getBestRunIdsForFuzzyId(reportedRunId);
                if (fuzzyReportedMatches.isEmpty()) {
                    _log.info("couldn't find a fuzzy match for reported runId=" + reportedRunId);
                } else {
                    bestFuzzyDistance = fuzzyReportedMatches.keySet().first();
                    if (bestFuzzyDistance <= 0) {
                        fuzzyMatches = fuzzyReportedMatches.get(bestFuzzyDistance);
                        for (final String runId : fuzzyMatches) {
                            routeIds.addAll(_runService.getRoutesForRunId(runId));
                        }
                    }
                }
            } catch (final IllegalArgumentException ex) {
                _log.warn(ex.getMessage());
            }
        }

        return new RunResults(opAssignedRunId, fuzzyMatches, bestFuzzyDistance, routeIds);
    }

    /**
     * Returns the most recent inference result as an NycTestInferredLocationRecord, i.e. simulator output.
     * @return
     */
    private NycTestInferredLocationRecord getMostRecentParticleAsNycTestInferredLocationRecord() {
        final Particle particle = _particleFilter.getMostLikelyParticle();
        if (particle == null)
            return null;

        final VehicleState state = particle.getData();
        final MotionState motionState = state.getMotionState();
        final JourneyState journeyState = state.getJourneyState();
        final BlockStateObservation blockState = state.getBlockStateObservation();
        final Observation obs = state.getObservation();
        final CoordinatePoint location = obs.getLocation();
        final NycRawLocationRecord nycRecord = obs.getRecord();

        final NycTestInferredLocationRecord record = new NycTestInferredLocationRecord();
        record.setLat(location.getLat());
        record.setLon(location.getLon());
        record.setTimestamp((long) particle.getTimestamp());
        record.setDsc(nycRecord.getDestinationSignCode());
        record.setOperatorId(nycRecord.getOperatorId());
        record.setReportedRunId(RunTripEntry.createId(nycRecord.getRunRouteId(), nycRecord.getRunNumber()));

        final EVehiclePhase phase = journeyState.getPhase();
        if (phase != null)
            record.setInferredPhase(phase.name());
        else
            record.setInferredPhase(EVehiclePhase.UNKNOWN.name());

        final Set<String> statusFields = new HashSet<String>();

        /*
         * This should make sure these are populated. (will show prev. values when
         * record lat/lon are zero)
         */
        record.setInferredBlockLat(location.getLat());
        record.setInferredBlockLon(location.getLon());

        if (blockState != null) {
            record.setInferredRunId(blockState.getBlockState().getRunId());
            record.setInferredIsRunFormal(blockState.isRunFormal());

            // formality match exposed to TDS
            if (blockState.isRunFormal()) {
                statusFields.add("blockInf");
            }

            final BlockInstance blockInstance = blockState.getBlockState().getBlockInstance();
            final BlockConfigurationEntry blockConfig = blockInstance.getBlock();
            final BlockEntry block = blockConfig.getBlock();

            record.setInferredBlockId(AgencyAndIdLibrary.convertToString(block.getId()));
            record.setInferredServiceDate(blockInstance.getServiceDate());

            final ScheduledBlockLocation blockLocation = blockState.getBlockState().getBlockLocation();
            record.setInferredDistanceAlongBlock(blockLocation.getDistanceAlongBlock());
            record.setInferredScheduleTime(blockLocation.getScheduledTime());

            final BlockTripEntry activeTrip = blockLocation.getActiveTrip();
            if (activeTrip != null) {
                final TripEntry trip = activeTrip.getTrip();
                record.setInferredTripId(AgencyAndIdLibrary.convertToString(trip.getId()));
            }

            final CoordinatePoint locationAlongBlock = blockLocation.getLocation();
            if (locationAlongBlock != null && (EVehiclePhase.IN_PROGRESS.equals(phase)
                    || phase.toLabel().toUpperCase().startsWith("LAYOVER_"))) {
                record.setInferredBlockLat(locationAlongBlock.getLat());
                record.setInferredBlockLon(locationAlongBlock.getLon());
            }

            if (EVehiclePhase.IN_PROGRESS.equals(phase)) {
                final int secondsSinceLastMotion = (int) ((particle.getTimestamp()
                        - motionState.getLastInMotionTime()) / 1000);
                if (secondsSinceLastMotion > _configurationService
                        .getConfigurationValueAsInteger("display.stalledTimeout", 900))
                    statusFields.add("stalled");
            } else {
                final boolean vehicleIsDetourEligible = _blockStateService.locationIsEligibleForDetour(activeTrip,
                        location);

                // vehicles on detour should be in_progress with status=deviated 
                if (state.getJourneyState().getIsDetour() && vehicleIsDetourEligible) {
                    // remap this journey state/phase to IN_PROGRESS to conform to 
                    // previous pilot project semantics.
                    if (EVehiclePhase.DEADHEAD_DURING.equals(phase)) {
                        record.setInferredPhase(EVehiclePhase.IN_PROGRESS.name());
                        statusFields.add("deviated");
                    }
                }
            }

            record.setInferredDsc(blockState.getBlockState().getDestinationSignCode());
        }

        // Set the status field
        if (!statusFields.isEmpty()) {
            record.setInferredStatus(StringUtils.join(statusFields, "+"));
        } else {
            record.setInferredStatus("default");
        }

        if (StringUtils.isBlank(record.getInferredDsc()))
            record.setInferredDsc("0000");

        return record;
    }

}