org.transitime.db.structs.Block.java Source code

Java tutorial

Introduction

Here is the source code for org.transitime.db.structs.Block.java

Source

/* 
 * This file is part of Transitime.org
 * 
 * Transitime.org is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPL) as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * Transitime.org is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Transitime.org .  If not, see <http://www.gnu.org/licenses/>.
 */
package org.transitime.db.structs;

import java.io.Serializable;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
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 javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.OrderColumn;
import javax.persistence.Table;

import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.JDBCException;
import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.annotations.Cascade;
import org.hibernate.annotations.CascadeType;
import org.hibernate.annotations.DynamicUpdate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.transitime.applications.Core;
import org.transitime.configData.CoreConfig;
import org.transitime.core.SpatialMatch;
import org.transitime.db.hibernate.HibernateUtils;
import org.transitime.gtfs.DbConfig;
import org.transitime.utils.IntervalTimer;
import org.transitime.utils.Time;

/**
 * Represents assignment for a vehicle for a day. Obtained by combining
 * data from multiple GTFS files.
 * 
 * Thought a good deal about how to represent all the stops in the block.
 * Wanted to have a list of trip patterns instead of trips because that
 * would be more efficient. But then realized that for each trip need
 * different times and such. Therefore decided to use full list of Trips.
 * This will make the data in the database unfortunately quite large.
 * 
 * @author SkiBu Smith
 */
@Entity(name = "Blocks")
@DynamicUpdate
@Table(name = "Blocks")
public final class Block implements Serializable {

    @Column
    @Id
    private final int configRev;

    @Column(length = HibernateUtils.DEFAULT_ID_SIZE)
    @Id
    private final String blockId;

    @Column(length = HibernateUtils.DEFAULT_ID_SIZE)
    @Id
    private final String serviceId;

    // Start time of block assignment. In seconds from midnight. Can be less
    // than 0 to indicate that block starts before midnight of the current
    // day. Can be greater than one day to indicate that block starts after 
    // midnight of the current day.
    @Column
    private final int startTime;

    // End time of block assignment. In seconds from midnight. Can be less
    // than 0 to indicate that block ends before midnight of the current
    // day. Can be greater than one day to indicate that block ends after 
    // midnight of the current day.
    @Column
    private final int endTime;

    // Need to have a ManyToMany instead of OneToMany relationship
    // for the List of Trips because several Blocks can refer to the 
    // same trip. This is because "UNSCHEDULED" blocks can be created
    // using regular trips. For this situation don't want Hibernate to
    // try to store the same trip twice because then would get a uniqueness
    // violation.
    //
    // Use CascadeType.SAVE_UPDATE so that when the TripPattern is stored   
    // the Paths are automatically stored.
    //
    // Use FetchType.LAZY so that don't read in all trip data at once since
    // that in turn reads in trip pattern and travel time info, which can
    // be voluminous and therefore slow. The trips will be read in when
    // getTrips() is called.
    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "Block_to_Trip_joinTable")
    @OrderColumn(name = "listIndex")
    @Cascade({ CascadeType.SAVE_UPDATE })
    private final List<Trip> trips;

    // Sometimes will get vehicle assignment by routeId. This means that need
    // to know which blocks are associated with a route. Getting the routeIds
    // from the Trip objects is problematic because then all the Trip data
    // needs to be lazy loaded such that if need to look at all blocks then
    // need to load in all trips. That takes too long for when doing debugging.
    // So to speed things up the routeIds for a block are stored here.
    // NOTE: since trying to use serialization need to use ArrayList<> instead
    // of List<> since List<> doesn't implement Serializable.
    @Column(length = 500)
    private final HashSet<String> routeIds;

    // For making sure only lazy load trips collection via one thread
    // at a time.
    private static final Object lazyLoadingSyncObject = new Object();

    // Hibernate requires class to be serializable because has composite Id
    private static final long serialVersionUID = 6511242755235485004L;

    private static final Logger logger = LoggerFactory.getLogger(Block.class);

    /********************** Member Functions **************************/

    /**
     * This constructor called when processing GTFS data and creating a Block to
     * be stored in the database. Note: startTime and endTime could in theory be
     * determined here by looking at first and last TripElements. But what if
     * they haven't been set in GTFS? Want the calling function to do this kind
     * of error checking because it does other error checking and can log issues
     * appropriately.
     * 
     * @param configRev
     * @param blockId
     * @param startTime
     * @param endTime
     * @param trips
     */
    public Block(int configRev, String blockId, String serviceId, int startTime, int endTime, List<Trip> trips) {
        this.configRev = configRev;
        this.blockId = blockId;
        this.serviceId = serviceId;
        this.startTime = startTime;
        this.endTime = endTime;
        this.trips = trips;

        // Obtain the set of route IDs from the trips
        this.routeIds = new HashSet<String>();
        for (Trip trip : trips) {
            this.routeIds.add(trip.getRouteId());
        }
    }

    /**
     * Hibernate requires no-arg constructor
     */
    @SuppressWarnings("unused")
    private Block() {
        this.configRev = -1;
        this.blockId = null;
        this.serviceId = null;
        this.startTime = -1;
        this.endTime = -1;
        this.trips = null;
        this.routeIds = null;
    }

    /**
     * Returns list of Block objects for the specified configRev
     * 
     * @param session
     * @param configRev
     * @return List of Block objects
     * @throws HibernateException
     */
    @SuppressWarnings("unchecked")
    public static List<Block> getBlocks(Session session, int configRev) throws HibernateException {
        String hql = "FROM Blocks " + "    WHERE configRev = :configRev";
        Query query = session.createQuery(hql);
        query.setInteger("configRev", configRev);
        return query.list();
    }

    /**
     * Deletes rev from the Blocks, Trips, and Block_to_Trip_joinTable
     * 
     * @param session
     * @param configRev
     * @return Number of rows deleted
     * @throws HibernateException
     */
    public static int deleteFromRev(Session session, int configRev) throws HibernateException {
        // In a perfect Hibernate world one would simply call on session.delete()
        // for each block and the block to trip join table and the associated
        // trips would be automatically deleted by using the magic of Hibernate.
        // But this means that would have to read in all the Blocks and sub-objects
        // first, which of course takes lots of time and memory, often causing
        // program to crash due to out of memory issue. And since reading in the
        // Trips is supposed to automatically read in associated travel times
        // we would be reading in data that isn't even needed for deletion since
        // don't want to delete travel times (want to reuse them!). Therefore
        // using the much, much faster solution of direct SQL calls. Can't use
        // HQL on the join table since it is not a regularly defined table. 
        //
        // Note: Would be great to see if can actually use HQL and delete the 
        // appropriate Blocks and have the join table and the trips table
        // be automatically updated. I doubt this would work but would be
        // interesting to try if had the time.
        int totalRowsUpdated = 0;

        // Delete configRev data from Block_to_Trip_joinTable
        int rowsUpdated = session
                .createSQLQuery("DELETE FROM Block_to_Trip_joinTable " + "WHERE Blocks_configRev=" + configRev)
                .executeUpdate();
        logger.info("Deleted {} rows from Block_to_Trip_joinTable for " + "configRev={}", rowsUpdated, configRev);
        totalRowsUpdated += rowsUpdated;

        // Delete configRev data from Trips
        rowsUpdated = session.createSQLQuery("DELETE FROM Trips WHERE configRev=" + configRev).executeUpdate();
        logger.info("Deleted {} rows from Trips for configRev={}", rowsUpdated, configRev);
        totalRowsUpdated += rowsUpdated;

        // Delete configRev data from Blocks
        rowsUpdated = session.createSQLQuery("DELETE FROM Blocks WHERE configRev=" + configRev).executeUpdate();
        logger.info("Deleted {} rows from Blocks for configRev={}", rowsUpdated, configRev);
        totalRowsUpdated += rowsUpdated;

        return totalRowsUpdated;
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "Block [" + "configRev=" + configRev + ", blockId=" + blockId + ", serviceId=" + serviceId
                + ", startTime=" + Time.timeOfDayStr(startTime) + ", endTime=" + Time.timeOfDayStr(endTime)
                // Use getTrips() instead of trips to deal with possible lazy 
                // initialization issues
                + ", trips=" + getTrips() + "]";
    }

    /**
     * Like toString() but only outputs core info. Doesn't output
     * the trips except to identify them. This is a big difference
     * from toString() because each Trip has lots of info to output.
     * @return
     */
    public String toShortString() {
        // Create shortened version of Trip info that only includes the trip_id
        String tripsStr = "Trip [";
        for (Trip trip : getTrips()) {
            tripsStr += trip.getId() + ", ";
        }
        tripsStr += "]";
        return "Block [" + "blockId=" + blockId + ", serviceId=" + serviceId + ", configRev=" + configRev
                + ", startTime=" + Time.timeOfDayStr(startTime) + ", endTime=" + Time.timeOfDayStr(endTime)
                + ", trips=" + tripsStr // Use the shortened version
                + "]";
    }

    /**
     * Required by Hibernate
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((blockId == null) ? 0 : blockId.hashCode());
        result = prime * result + configRev;
        result = prime * result + endTime;
        result = prime * result + ((routeIds == null) ? 0 : routeIds.hashCode());
        result = prime * result + ((serviceId == null) ? 0 : serviceId.hashCode());
        result = prime * result + startTime;
        result = prime * result + ((trips == null) ? 0 : trips.hashCode());
        return result;
    }

    /**
     * Required by Hibernate
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Block other = (Block) obj;
        if (blockId == null) {
            if (other.blockId != null)
                return false;
        } else if (!blockId.equals(other.blockId))
            return false;
        if (configRev != other.configRev)
            return false;
        if (endTime != other.endTime)
            return false;
        if (routeIds == null) {
            if (other.routeIds != null)
                return false;
        } else if (!routeIds.equals(other.routeIds))
            return false;
        if (serviceId == null) {
            if (other.serviceId != null)
                return false;
        } else if (!serviceId.equals(other.serviceId))
            return false;
        if (startTime != other.startTime)
            return false;
        if (trips == null) {
            if (other.trips != null)
                return false;
        } else if (!trips.equals(other.trips))
            return false;
        return true;
    }

    /**
     * Finds the trip for the block where secondsIntoDay lies between
     * the end time of the previous trip and the end time of the current
     * trip. Does not take into account wrapping around midnight.
     * 
     * @param secondsIntoDay
     * @return index of trip, or -1 if no match
     */
    private int activeTripIndex(int secondsIntoDay) {
        List<Trip> trips = getTrips();
        int previousTripEndTimeSecs = trips.get(0).getStartTime();
        for (int i = 0; i < trips.size(); ++i) {
            Trip trip = trips.get(i);
            int tripEndTimeSecs = trip.getEndTime();
            if (secondsIntoDay > previousTripEndTimeSecs && secondsIntoDay < tripEndTimeSecs) {
                return i;
            }
        }

        return -1;
    }

    /**
     * Determines which trip is currently active and returns the associated
     * index of that trip such that getTrip(int) can be called to get the trip.
     * Trip is active if the date lies between the end time of the previous trip
     * and the end time of the current trip. To handle wrapping around midnight
     * if there isn't a match initially then will check past midnight and before
     * midnight as well.
     * <p>
     * Often consider a block to be active before its scheduled start time. For
     * this situation the first trip is returned if it is within
     * allowableBeforeTimeSecs before the block is supposed to start.
     * 
     * @param date
     *            The current time, to be used for determining if trip active.
     * @param allowableBeforeTimeSecs
     *            How much before the block time the block is considered to be
     *            active. Needed because often consider a block to be active
     *            before its scheduled start time. For this situation need to
     *            return first trip.
     * @return index of trip, or -1 if no match
     */
    public int activeTripIndex(Date date, int allowableBeforeTimeSecs) {
        // Find matching trip
        int secondsIntoDay = Core.getInstance().getTime().getSecondsIntoDay(date);
        int index = activeTripIndex(secondsIntoDay);

        // If match not found check a day into future and day into past
        if (index < 0) {
            index = activeTripIndex(secondsIntoDay + Time.SEC_PER_DAY);
            if (index < 0)
                index = activeTripIndex(secondsIntoDay - Time.SEC_PER_DAY);
        }

        // Date is not when trip is supposed to be active. But since blocks 
        // are considered to be active allowableBeforeTimeSecs before their
        // scheduled start time if the block is active then must be matching
        // to the first trip, so return 0.
        if (index < 0 && isActive(date, allowableBeforeTimeSecs, -1))
            return 0;

        // Return result
        return index;
    }

    /**
     * Returns true if the service ID for the block is for the time specified by
     * offset parameter. Useful for determining if the service ID for the block
     * is valid for today, yesterday, or for tomorrow.
     *
     * @param date
     *            The date want to see if the block is active for
     * @param offset
     *            Use 0 for today, -Time.DAY_IN_MSECS for yesterday, and
     *            Time.DAY_IN_MSECS for today.
     * @return true if service ID for block as valid for other day
     */
    private boolean serviceClassIsValidForDay(Date date, long offset) {
        long dateToCheck = date.getTime() + offset;
        List<String> currentServiceIds = Core.getInstance().getServiceUtils().getServiceIdsForDay(dateToCheck);

        return currentServiceIds.contains(serviceId);
    }

    /**
     * Returns true if the time of day of the date passed in is between
     * allowableBeforeTimeSecs before the startTime and the endTime for the
     * block. No leeway is provided for the end time. Note: does not look to see
     * if the service associated with the block is active. Only looks at time of
     * day.
     * 
     * @param date
     *            The time checking to see whether block is active for
     * @param allowableBeforeTimeSecs
     *            Block considered active if within this number of seconds
     *            before the block start time. Set to 0 if want to know if date
     *            is actually between the block start and end times.
     * @param allowableAfterStartTimeSecs
     *            If set to value greater than or equal to zero then block
     *            considered active only if within this number of seconds after
     *            the start time. If less then zero then block considered active
     *            up to the block end time.
     * @return True if the block is active for the specified date
     */
    public boolean isActive(Date date, int allowableBeforeTimeSecs, int allowableAfterStartTimeSecs) {
        int secsInDay = Core.getInstance().getTime().getSecondsIntoDay(date);

        // Determine the allowable start and end times for when the block 
        // is to be considered active
        int allowableStartTime = startTime - allowableBeforeTimeSecs;
        int allowableEndTime = allowableAfterStartTimeSecs < 0 ? endTime : startTime + allowableAfterStartTimeSecs;

        // Handle normal situation where times are between midnight in the 
        // morning and midnight in the evening
        boolean serviceClassValidToday = serviceClassIsValidForDay(date, 0);
        if (serviceClassValidToday) {
            if (secsInDay > allowableStartTime && secsInDay < allowableEndTime) {
                return true;
            }
        }

        // If the service class was valid yesterday then try adding 24 hours to
        // the time when checking if block active. This way handle situations
        // past midnight, but only if the block was actually active yesterday.
        boolean serviceClassValidYesterday = serviceClassIsValidForDay(date, -Time.DAY_IN_MSECS);
        if (serviceClassValidYesterday) {
            int secsInDayPastMidnight = secsInDay + Time.DAY_IN_SECS;
            if (secsInDayPastMidnight > allowableStartTime && secsInDayPastMidnight < allowableEndTime) {
                return true;
            }
        }

        // If the service class is valid tomorrow then try subtracting 24 hours
        // from the time when checking if block active. This way handle
        // situations before midnight, but won't include a block that isn't
        // actually active the next day.
        boolean serviceClassValidTomorrow = serviceClassIsValidForDay(date, Time.DAY_IN_MSECS);
        if (serviceClassValidTomorrow) {
            int secsInDayBeforeMidnight = secsInDay - Time.SEC_PER_DAY;
            if (secsInDayBeforeMidnight > allowableStartTime && secsInDayBeforeMidnight < allowableEndTime) {
                return true;
            }
        }

        // It simply ain't active
        return false;
    }

    /**
     * Returns true if the time of day of the date passed in is between
     * allowableBeforeTimeSecs before the startTime and the endTime for the
     * block. No leeway is provided for the end time. Note: does not look to see
     * if the service associated with the block is active. Only looks at time of
     * day.
     * 
     * @param date
     *            The time checking to see whether block is active for
     * @param allowableBeforeTimeSecs
     *            Block considered active if within this number of seconds
     *            before the block start time. Set to 0 if want to know if date
     *            is actually between the block start and end times.
     * @param allowableAfterStartTimeSecs
     *            If set to value greater than or equal to zero then block
     *            considered active only if within this number of seconds after
     *            the start time. If less then zero then block considered active
     *            up to the block end time.
     * @return True if the block is active for the specified date
     */
    public boolean isActive(long epochTime, int allowableBeforeTimeSecs, int allowableAfterStartTimeSecs) {
        return isActive(new Date(epochTime), allowableBeforeTimeSecs, allowableAfterStartTimeSecs);
    }

    /**
     * Returns true if the time of day of the epoch time passed in is between
     * allowableBeforeTimeSecs before the startTime and the endTime for the
     * block. No leeway is provided for the end time. Note: does not look to see
     * if the service associated with the block is active. Only looks at time of
     * day.
     * 
     * @param epochTime
     * @param allowableBeforeTimeSecs
     * @return
     */
    public boolean isActive(long epochTime, int allowableBeforeTimeSecs) {
        return isActive(new Date(epochTime), allowableBeforeTimeSecs, -1);
    }

    /**
     * Returns true if the time of day of the date passed in is between the
     * startTime and the endTime for the block. No leeway is provided. Note:
     * does not look to see if the service associated with the block is active.
     * Only looks at time of day.
     * 
     * @param date
     * @return True if the block is active.
     */
    public boolean isActive(Date date) {
        return isActive(date, 0, -1);
    }

    /**
     * Returns true if the time of day of the date passed in is between the
     * startTime and the endTime for the block. No leeway is provided. Note:
     * does not look to see if the service associated with the block is active.
     * Only looks at time of day.
     * 
     * @param epochTime
     * @return True if the block is active.
     */
    public boolean isActive(long epochTime) {
        return isActive(new Date(epochTime), 0, -1);
    }

    /**
     * Returns true if the time of day of the date passed in is between
     * allowableBeforeTimeSecs before the start time and the start time.
     * 
     * @param date
     * @param allowableBeforeTimeSecs
     * @return true if within allowableBeforeTimeSecs before the start time of
     *         the block
     */
    public boolean isBeforeStartTime(Date date, int allowableBeforeTimeSecs) {
        int secsInDayForAvlReport = Core.getInstance().getTime().getSecondsIntoDay(date);

        return (secsInDayForAvlReport > startTime - allowableBeforeTimeSecs && secsInDayForAvlReport < startTime)
                // also handle where date before midnight but start time is after
                || (secsInDayForAvlReport > startTime + Time.SEC_PER_DAY - allowableBeforeTimeSecs
                        && secsInDayForAvlReport < startTime + Time.SEC_PER_DAY);
    }

    /**
     * If the trip is active at the secsInDayForAvlReport then it is
     * added to the tripsThatMatchTime list. Trip is considered active
     * if it is within start time of trip minus 
     * CoreConfig.getAllowableEarlyForLayoverSeconds() and within the end
     * time of the trip. No leniency is made for the end time since once
     * a trip is over really don't want to assign vehicle to that trip.
     * Yes, vehicles often run late, but that should only be taken account
     * when matching to already predictable vehicle.
     * 
     * @param vehicleId for logging messages
     * @param secsInDayForAvlReport
     * @param trip
     * @param tripsThatMatchTime
     * @return
     */
    private static boolean addTripIfActive(String vehicleId, int secsInDayForAvlReport, Trip trip,
            List<Trip> tripsThatMatchTime) {
        int startTime = trip.getStartTime();
        int endTime = trip.getEndTime();

        int allowableEarlyTimeSecs = CoreConfig.getAllowableEarlyForLayoverSeconds();
        if (secsInDayForAvlReport > startTime - allowableEarlyTimeSecs && secsInDayForAvlReport < endTime) {
            tripsThatMatchTime.add(trip);

            if (logger.isDebugEnabled()) {
                logger.debug(
                        "Determined that for blockId={} that a trip is " + "considered to be active for AVL time. "
                                + "TripId={}, tripIndex={} AVLTime={}, " + "startTime={}, endTime={}, "
                                + "allowableEarlyForLayover={} secs, allowableLate={} secs, " + "vehicleId={}",
                        trip.getBlock().getId(), trip.getId(), trip.getBlock().getTripIndex(trip),
                        Time.timeOfDayStr(secsInDayForAvlReport), Time.timeOfDayStr(trip.getStartTime()),
                        Time.timeOfDayStr(trip.getEndTime()), CoreConfig.getAllowableEarlyForLayoverSeconds(),
                        CoreConfig.getAllowableLateSeconds(), vehicleId);
            }

            return true;
        }

        // Not a match so return false
        return false;
    }

    /**
     * For this block determines which trips are currently active. Should work
     * even for trips that start before midnight or go till after midnight. Trip
     * is considered active if it is within start time of trip minus
     * CoreConfig.getAllowableEarlyForLayoverSeconds() and within the end time
     * of the trip. No leniency is made for the end time since once a trip is
     * over really don't want to assign vehicle to that trip.
     * 
     * @param avlReport
     * @return List of Trips that are active. If none are active an empty list
     *         is returned.
     */
    public List<Trip> getTripsCurrentlyActive(AvlReport avlReport) {
        // Set for returning results
        List<Trip> tripsThatMatchTime = new ArrayList<Trip>();

        // Convenience variable
        String vehicleId = avlReport.getVehicleId();

        // Go through trips and find ones 
        List<Trip> trips = getTrips();
        for (Trip trip : trips) {
            // If time of avlReport is within reasonable time of the trip
            // time then this trip should be returned.  
            int secsInDayForAvlReport = Core.getInstance().getTime().getSecondsIntoDay(avlReport.getDate());

            // If the trip is active then add it to the list of active trips 
            boolean tripIsActive = addTripIfActive(vehicleId, secsInDayForAvlReport, trip, tripsThatMatchTime);

            // if trip wasn't active might be because trip actually starts before
            // midnight so should check for that special case.
            if (!tripIsActive)
                tripIsActive = addTripIfActive(vehicleId, secsInDayForAvlReport - Time.SEC_PER_DAY, trip,
                        tripsThatMatchTime);

            // if trip still wasn't active might be because trip goes past
            // midnight so should check for that special case.
            if (!tripIsActive)
                tripIsActive = addTripIfActive(vehicleId, secsInDayForAvlReport + Time.SEC_PER_DAY, trip,
                        tripsThatMatchTime);
        }

        // Returns results
        return tripsThatMatchTime;
    }

    /***************************** Getter Methods ************************/

    /**
     * @return the blockId
     */
    public String getId() {
        return blockId;
    }

    /**
     * @return the configRev
     */
    public int getConfigRev() {
        return configRev;
    }

    /**
     * @return the serviceId
     */
    public String getServiceId() {
        return serviceId;
    }

    /**
     * @return the startTime in seconds from midnight
     */
    public int getStartTime() {
        return startTime;
    }

    /**
     * @return the endTime in seconds from midnight
     */
    public int getEndTime() {
        return endTime;
    }

    /**
     * Uses lazy initialization to determine the trips for the block.
     * 
     * @return the trips
     */
    public List<Trip> getTrips() {
        // It appears that lazy initialization is problematic when have multiple 
        // simultaneous threads. Get "org.hibernate.AssertionFailure: force 
        // initialize loading collection". Therefore need to make sure that 
        // only loading lazy sub-data serially. Since it is desirable to have
        // trips collection be lazy loaded so that app starts right away without
        // loading all the sub-data for every block assignment need to make
        // sure this is done in a serialized way. Having app not load all data
        // at startup is especially important when debugging.
        if (!Hibernate.isInitialized(trips)) {
            // trips not yet initialized so synchronize so only a single
            // thread can initialize at once and then access something
            // in trips that will cause it to be lazy loaded.
            synchronized (lazyLoadingSyncObject) {
                logger.debug("About to do lazy load for trips data for " + "blockId={} serviceId={}...", blockId,
                        serviceId);
                IntervalTimer timer = new IntervalTimer();

                // Access the collection so that it is lazy loaded.
                // Problems can be difficult to debug so log error along
                // with the SQL.
                try {
                    trips.get(0);
                } catch (JDBCException e) {
                    logger.error("In Block.getTrips() got JDBCException. " + "SQL=\"{}\" msg={}", e.getSQL(),
                            e.getSQLException().getMessage());
                    throw e;
                }
                logger.debug("Finished lazy load for trips data for " + "blockId={} serviceId={}. Took {} msec",
                        blockId, serviceId, timer.elapsedMsec());
            }
        }

        return Collections.unmodifiableList(trips);
    }

    /**
     * So can sync up loading of trip and trip pattern data when trips are all
     * read at once in another class as opposed to through Block.getTrips().
     * 
     * @return
     */
    public static Object getLazyLoadingSyncObject() {
        return lazyLoadingSyncObject;
    }

    /**
     * Returns true if block assignment has no schedule (is frequency based)
     * 
     * @return true if no schedule
     */
    public boolean isNoSchedule() {
        return getTrips().get(0).isNoSchedule();
    }

    /**
     * Returns true if block assignment has a schedule (is not frequency based)
     * 
     * @return true if has schedule
     */
    public boolean hasSchedule() {
        return !isNoSchedule();
    }

    /**
     * Returns the trip specified by the tripIndex
     * 
     * @param tripIndex
     * @return the Trip specified by tripIndex. If index out of range returns
     *         null
     */
    public Trip getTrip(int tripIndex) {
        // If index out of range return null
        if (tripIndex < 0 || tripIndex >= getTrips().size()) {
            return null;
        }

        // Return the specified trip
        return getTrips().get(tripIndex);
    }

    /**
     * Returns the trip for the block as specified by the tripId parameter
     * @param tripId Which trip is to be returned
     * @return The Trip that matches the tripId
     */
    public Trip getTrip(String tripId) {
        for (Trip trip : getTrips())
            if (trip.getId().equals(tripId))
                return trip;

        // The tripId was not found for this block so return null
        return null;
    }

    /**
     * Returns for the specified trip the index into the trips list for the
     * block
     * 
     * @param trip
     *            Specifies which trip looking for
     * @return Index into trips of the specified trip
     */
    public int getTripIndex(Trip trip) {
        List<Trip> tripsList = getTrips();
        for (int i = 0; i < tripsList.size(); ++i) {
            if (tripsList.get(i) == trip)
                return i;
        }
        return -1;
    }

    /**
     * Returns collection of route IDs associated with the Block. 
     * Each route ID will only be included once.
     * @return
     */
    public Set<String> getRouteIds() {
        return routeIds;
        //      Note: previously was getting them from trips but this requires a call
        //      to getTrips() which loads in all the data from db for the trips for
        //      the block, which is quite slow. By generating the routeIds when the
        //      GTFS data is processed and storing them in the db then don't need
        //      the application to load in all trip data, which is great for debugging.      
        //      Set<String> routeIdsSet = new HashSet<String>();      
        //      for (Trip trip : getTrips()) {
        //         routeIdsSet.add(trip.getRouteId());
        //      }      
        //      return routeIdsSet;
    }

    /**
     * Returns the trip patterns associated with the block. Note
     * that these are not in any kind of fixed order since trip
     * patterns are used multiple times in a block. 
     * @return
     */
    public Collection<TripPattern> getTripPatterns() {
        // Create map of trip patterns for this block so that can
        // return just a single copy of each one. Keyed on trip pattern ID
        Map<String, TripPattern> tripPatternsMap = new HashMap<String, TripPattern>();
        for (Trip trip : getTrips()) {
            TripPattern tripPattern = trip.getTripPattern();
            tripPatternsMap.put(tripPattern.getId(), tripPattern);
        }

        // Return the collection of unique trip patterns
        return tripPatternsMap.values();
    }

    /**
     * Returns the travel time for the specified path. Does not include stop 
     * times.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return
     */
    public int getStopPathTravelTime(int tripIndex, int stopPathIndex) {
        Trip trip = getTrip(tripIndex);
        TravelTimesForStopPath travelTimesForPath = trip.getTravelTimesForStopPath(stopPathIndex);

        return travelTimesForPath.getStopPathTravelTimeMsec();
    }

    /**
     * Returns the time in msec for how long expected to be at the stop
     * at the end of the stop path.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return
     */
    public int getPathStopTime(int tripIndex, int stopPathIndex) {
        Trip trip = getTrip(tripIndex);
        TravelTimesForStopPath travelTimesForPath = trip.getTravelTimesForStopPath(stopPathIndex);
        return travelTimesForPath.getStopTimeMsec();
    }

    /**
     * Returns the Vector of the segment specified.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @param segmentIndex
     * @return The Vector for the specified segment or null if the indices are
     * out of range.
     */
    public Vector getSegmentVector(int tripIndex, int stopPathIndex, int segmentIndex) {
        Trip trip = getTrip(tripIndex);
        if (trip == null)
            return null;
        StopPath stopPath = trip.getStopPath(stopPathIndex);
        if (stopPath == null)
            return null;
        Vector segmentVector = stopPath.getSegmentVector(segmentIndex);
        return segmentVector;
    }

    /**
     * Returns number of segments for the path specified
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return Number of segments for specified path or -1 if
     * an index is out of range.
     */
    public int numSegments(int tripIndex, int stopPathIndex) {
        // Determine number of segments
        Trip trip = getTrip(tripIndex);
        if (trip == null)
            return -1;
        StopPath path = trip.getStopPath(stopPathIndex);
        if (path == null)
            return -1;
        return path.getSegmentVectors().size();
    }

    /**
     * Returns number of stopPaths for the trip specified.
     * 
     * @param tripIndex
     * @return Number of stopPaths for specified trip or -1 if index
     * is out of range.
     */
    public int numStopPaths(int tripIndex) {
        Trip trip = getTrip(tripIndex);
        if (trip == null)
            return -1;

        TravelTimesForTrip travelTimesForTrip = trip.getTravelTimes();
        List<TravelTimesForStopPath> travelTimesForStopPaths = travelTimesForTrip.getTravelTimesForStopPaths();
        return travelTimesForStopPaths.size();
    }

    /**
     * Returns the number of trips.
     * 
     * @return
     */
    public int numTrips() {
        return getTrips().size();
    }

    /**
     * Returns true if path is for a stop that is configured to be layover stop
     * for the trip pattern. A layover stop is when a vehicle can leave route
     * path before departing this stop since the driver is taking a break.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return
     */
    public boolean isLayover(int tripIndex, int stopPathIndex) {
        return getStopPath(tripIndex, stopPathIndex).isLayoverStop();
    }

    /**
     * Indicates that vehicle is not supposed to depart the stop until the
     * scheduled departure time.
     * 
     * @return true if a wait stop
     */
    public boolean isWaitStop(int tripIndex, int stopPathIndex) {
        return getStopPath(tripIndex, stopPathIndex).isWaitStop();
    }

    /**
     * Returns the path specified by tripIndex and stopPathIndex params.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return the StopPath or null if tripIndex or stopPathIndex are out of
     *         range.
     */
    public StopPath getStopPath(int tripIndex, int stopPathIndex) {
        // Get the trip
        Trip trip = getTrip(tripIndex);
        if (trip == null) {
            logger.error("In Block.getStopPath() tripIndex={} is out of range " + "(stopPathIndex={}) for block={}",
                    tripIndex, stopPathIndex, this.toShortString());
            return null;
        }

        // Get the stop path
        StopPath stopPath = trip.getStopPath(stopPathIndex);
        if (stopPath == null) {
            logger.error(
                    "In Block.getStopPath() stopPathIndex={} is out of "
                            + "range for tripIndex={} trip={} of block={}",
                    stopPathIndex, tripIndex, trip.toShortString(), this.toShortString());
            return null;
        }

        // Return the stop path
        return stopPath;
    }

    /**
     * Returns the location of the first stop of the block.
     * 
     * @return
     */
    public Location getStartLoc() {
        StopPath firstStopPath = getStopPath(0, 0);
        return firstStopPath.getEndOfPathLocation();
    }

    /**
     * Returns the previous path specified by tripIndex and stopPathIndex
     * params. If wrapping back past beginning of block (where
     * tripIndex becomes negative) then returns null.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return
     */
    public StopPath getPreviousPath(int tripIndex, int stopPathIndex) {
        // First, determine trip and path index for the previous path
        --stopPathIndex;
        if (stopPathIndex < 0) {
            --tripIndex;
            if (tripIndex < 0)
                return null;
            stopPathIndex = getTrip(tripIndex).getTripPattern().getStopPaths().size() - 1;
        }

        // Return the previous path
        return getStopPath(tripIndex, stopPathIndex);
    }

    /**
     * Returns the ScheduleTime for that stop specified by the trip and path
     * indices.
     * 
     * @param tripIndex
     * @param stopPathIndex
     * @return the schedule time for the specified stop. Returns null if no
     * schedule time associated with stop
     */
    public ScheduleTime getScheduleTime(int tripIndex, int stopPathIndex) {
        Trip trip = getTrip(tripIndex);
        return trip.getScheduleTime(stopPathIndex);
    }

    /**
     * Returns true if on last trip of block and within the specified distance
     * of the end of that last trip.
     * 
     * @param match
     * @param distance
     * @return True if within distance of end of block
     */
    public boolean nearEndOfBlock(SpatialMatch match, double distance) {
        // If not last trip of block then not considered near end
        // so return false.
        if (match.getTripIndex() != trips.size() - 1)
            return false;

        return match.withinDistanceOfEndOfTrip(distance);
    }

    /**
     * Returns true if this block assignment should be exclusive, such that when
     * a vehicle is assigned to this block any other vehicles assigned to this
     * block will have their assignments removed.
     * <p>
     * Current it is configured using Java property instead of in the database.
     * 
     * @return True if this block assignment should be exclusively assigned to
     *         only a single vehicle at a time
     */
    public boolean shouldBeExclusive() {
        return CoreConfig.exclusiveBlockAssignments();
    }

    /**
     * For debugging
     * 
     * @param args
     */
    @SuppressWarnings("unused")
    public static void main(String[] args) {
        DbConfig dbConfig = Core.getInstance().getDbConfig();
        Block block1 = dbConfig.getBlock("1", "2105");
        Block block2 = dbConfig.getBlock("2", "2105");
        Block block3 = dbConfig.getBlock("3", "2105");
        try {
            boolean b1 = block1.isActive(Time.parse("5-10-2015 00:00:01"), 90 * Time.MIN_IN_SECS, -1);
            boolean b2 = block2.isActive(Time.parse("5-10-2015 00:00:01"), 90 * Time.MIN_IN_SECS, -1);
            boolean b3 = block3.isActive(Time.parse("5-10-2015 00:00:01"), 90 * Time.MIN_IN_SECS, -1);
            int forSettingBreakpoint = 9;
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }
}