/* * 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.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Transient; import org.hibernate.HibernateException; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.annotations.DynamicUpdate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.transitime.applications.Core; import org.transitime.db.hibernate.HibernateUtils; import org.transitime.gtfs.TitleFormatter; import org.transitime.gtfs.gtfsStructs.GtfsRoute; import org.transitime.utils.OrderedCollection; import org.transitime.utils.StringUtils; /** * For storing in db information for a route. Based on GTFS information * from routes.txt and other files. * * @author SkiBu Smith * */ @Entity @DynamicUpdate @Table(name="Routes") public class Route implements Serializable { @Column @Id private final int configRev; @Column(length=HibernateUtils.DEFAULT_ID_SIZE) @Id private final String id; @Column(length=10) private final String color; @Column(length=10) private final String textColor; // Not declared final because need to set route order for all // routes that did not have the order configured in the db, // but can only do so once all routes read in and sorted. @Column private Integer routeOrder; @Column private final boolean hidden; @Column(length=2) private final String type; @Column private final String description; // Directly from GTFS data @Column(length=80) private final String shortName; // Directly from GTFS data @Column(length=80) private final String longName; // Processed name combing the GTFS route_short_name and route_long_name @Column(length=80) private final String name; @Embedded private final Extent extent; // Optional parameter that specifies how far away AVL report can be // from segments and still be considered a match. @Column private final Double maxDistance; @Transient // Later will probably want to store this in database, // but not yet sure. This means it is not available to application! private final List<TripPattern> tripPatternsForRoute; // For getStops() @Transient private Collection<Stop> stops = null; // For getPathSegments() @Transient private Collection<Vector> stopPaths = null; // For getOrderedStopsByDirection() @Transient private Map<String, List<String>> orderedStopsPerDirectionMap = null; // For getStopOrder(). // Keeps track of stop order for each direction. Keyed on direction id. // The submap is keyed on stop id and contains list of stop orders. // Need a list since a stop can be in a trip multiple times. @Transient private Map<String, Map<String, List<Integer>>> stopOrderByDirectionMap = null; // Because Hibernate requires objects with composite Ids to be Serializable private static final long serialVersionUID = 9037023420649883860L; private static final Logger logger = LoggerFactory.getLogger(Route.class); /********************** Member Functions **************************/ /** * Constructor. Used for when processing GTFS data. Creates a * Route object that can be written to database. * * @param configRev * @param gtfsRoute * @param tripPatternsForRoute * @param titleFormatter */ public Route(int configRev, GtfsRoute gtfsRoute, List<TripPattern> tripPatternsForRoute, TitleFormatter titleFormatter) { // Because will be writing data to the sandbox in the db this.configRev = configRev; // Here are most of the params from GtfsRoute this.id = gtfsRoute.getRouteId(); this.color = gtfsRoute.getRouteColor(); this.textColor = gtfsRoute.getRouteTextColor(); this.routeOrder = gtfsRoute.getRouteOrder(); this.hidden = gtfsRoute.getHidden(); this.type = gtfsRoute.getRouteType(); this.description = gtfsRoute.getRouteDesc(); this.longName = titleFormatter.processTitle(gtfsRoute.getRouteLongName()); // Handle short name specially. route_short_name is optional // but Transitime uses it as an identifier since route_ids // are not always consistent across schedule changes. Therefore // if the GTFS route_short_name is not set then use the // route_long_name. String shortName = gtfsRoute.getRouteShortName(); this.shortName = shortName != null && !shortName.isEmpty() ? shortName : gtfsRoute.getRouteLongName(); // Get the name of the route. Need to do some fancy processing here because // need to fix the capitalization using the TitleFormatter. This also // does all the regex processing to fix other issues. Might also need // to combine short and long names into a single name. if (gtfsRoute.getRouteLongName() != null && !gtfsRoute.getRouteLongName().isEmpty()) { // route_long_name is set so use it // Prepend the route short name plus a " - ", but only if // route short name is defined and it is short. This way // will end up with a route name like "38 - Geary" but // not "HYDE-POWELL - Hyde Powell". For LA Metro some route short // names to prepend are something like "51/52/352" so need to go up // to 9 characters. Also, only prepend the route short name if // the route long name doesn't already contain it. String shortNameComponent = ""; if (gtfsRoute.getRouteShortName() != null && gtfsRoute.getRouteShortName().length() <= 9 && !this.longName.contains(gtfsRoute.getRouteShortName())) shortNameComponent = gtfsRoute.getRouteShortName() + " - "; this.name = shortNameComponent + this.longName; } else { // route_long_name not set so just use the route_short_name this.name = titleFormatter.processTitle(this.shortName); } this.tripPatternsForRoute = tripPatternsForRoute; this.maxDistance = gtfsRoute.getMaxDistance(); // Determine the extent of the route by looking at the extent // of all of the trip patterns. this.extent = new Extent(); for (TripPattern tp : tripPatternsForRoute) { this.extent.add(tp.getExtent()); } } /** * Needed because Hibernate requires no-arg constructor */ @SuppressWarnings("unused") private Route() { configRev = -1; id = null; color = null; textColor = null; routeOrder = null; hidden = false; type = null; description = null; shortName = null; longName = null; name = null; extent = null; tripPatternsForRoute = null; maxDistance = null; } /** * Deletes rev from the Routes table * * @param session * @param configRev * @return Number of rows deleted * @throws HibernateException */ public static int deleteFromRev(Session session, int configRev) throws HibernateException { // Note that hql uses class name, not the table name String hql = "DELETE Route WHERE configRev=" + configRev; int numUpdates = session.createQuery(hql).executeUpdate(); return numUpdates; } // For dealing with route order private static final int BEGINNING_OF_LIST_ROUTE_ORDER = 1000; private static final int END_OF_LIST_ROUTE_ORDER = 1000000; private boolean atBeginning() { return routeOrder != null && routeOrder < BEGINNING_OF_LIST_ROUTE_ORDER; } private boolean atEnd() { return routeOrder != null && END_OF_LIST_ROUTE_ORDER >= 1000000; } /** * Comparator for sorting Routes into proper order. * * If routeOrder is set and is below 1,000 then the route will be at * beginning of list and will be ordered by routeOrder. If routeOrder is set * and is above 1,000,000 then route will be put at end of list and will be * ordered by routeOrder. If routeOrder is not set then will order by route * short name. If route short name starts with numbers it will be padded by * zeros so that proper numerical order will be used. */ public static final Comparator<Route> routeComparator = new Comparator<Route>() { /** * Returns negative if r1<r2, zero if r1=r2, and positive if r1>r2 */ @Override public int compare(Route r1, Route r2) { // Handle if routeOrder indicates r1 should be at beginning of list if (r1.atBeginning()) { // If r2 also at beginning and it should be before r1... if (r2.atBeginning() && r1.getRouteOrder()>r2.getRouteOrder()) return 1; else return -1; } // Handle if routeOrder indicates r1 should be at end of list if (r1.atEnd()) { // If r2 also at end and it should be after r1... if (r2.atEnd() && r1.getRouteOrder()<r2.getRouteOrder()) return -1; else return 1; } // r1 is in the middle so check to see if r2 is at beginning or end if (r2.atBeginning()) return 1; if (r2.atEnd()) return -1; // Both r1 and r2 don't have a route order to order them by // route name return StringUtils.paddedName(r1.name).compareTo( StringUtils.paddedName(r2.name)); } }; /** * Returns List of Route objects for the specified database revision. * Orders them based on the GTFS route_order extension or the * route short name if route_order not set. * * @param session * @param configRev * @return Map of routes keyed on routeId * @throws HibernateException */ @SuppressWarnings("unchecked") public static List<Route> getRoutes(Session session, int configRev) throws HibernateException { // Get list of routes from database String hql = "FROM Route " + " WHERE configRev = :configRev" + " ORDER BY routeOrder, shortName"; Query query = session.createQuery(hql); query.setInteger("configRev", configRev); List<Route> routesList = query.list(); // Need to set the route order for each route so that can sort // predictions based on distance from stop and route order. For // the routes that didn't have route ordered configured in db // start with 1000 and count on up. int routeOrderForWhenNotConfigured = 1000; for (Route route: routesList) { if (!route.atBeginning() && !route.atEnd()) { route.setRouteOrder(routeOrderForWhenNotConfigured++); } } // Return the list of routes return routesList; } /* (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { // Don't want to output list of full TripPattern objects because // each TripPattern.toString() result is pretty long (list of stops, // extent, etc). Therefore for tripPatternsForRoute just output // a short version of the object. String tripPatternIds = "not set"; if (tripPatternsForRoute != null) { tripPatternIds = "["; for (TripPattern tp : tripPatternsForRoute) tripPatternIds += tp.toShortString() + ", "; tripPatternIds += "]"; } return "Route [" + "configRev=" + configRev + ", id=" + id + ", name=" + name + ", color=" + color + ", textColor=" + textColor + ", routeOrder=" + routeOrder + ", hidden=" + hidden + ", type=" + type + ", description=" + description + ", shortName=" + shortName + ", longName=" + longName + ", extent" + extent + ", tripPatternsForRoute=" + tripPatternIds + "]"; } /** * Needed because have a composite ID for Hibernate storage */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((color == null) ? 0 : color.hashCode()); result = prime * result + configRev; result = prime * result + ((description == null) ? 0 : description.hashCode()); result = prime * result + ((extent == null) ? 0 : extent.hashCode()); result = prime * result + (hidden ? 1231 : 1237); result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); result = prime * result + routeOrder; result = prime * result + ((shortName == null) ? 0 : shortName.hashCode()); result = prime * result + ((longName == null) ? 0 : longName.hashCode()); result = prime * result + ((textColor == null) ? 0 : textColor.hashCode()); result = prime * result + ((tripPatternsForRoute == null) ? 0 : tripPatternsForRoute .hashCode()); result = prime * result + ((type == null) ? 0 : type.hashCode()); return result; } /** * Needed because have a composite ID for Hibernate storage */ @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; Route other = (Route) obj; if (color == null) { if (other.color != null) return false; } else if (!color.equals(other.color)) return false; if (configRev != other.configRev) return false; if (description == null) { if (other.description != null) return false; } else if (!description.equals(other.description)) return false; if (extent == null) { if (other.extent != null) return false; } else if (!extent.equals(other.extent)) return false; if (hidden != other.hidden) return false; if (id == null) { if (other.id != null) return false; } else if (!id.equals(other.id)) return false; if (name == null) { if (other.name != null) return false; } else if (!name.equals(other.name)) return false; if (routeOrder != other.routeOrder) return false; if (shortName == null) { if (other.shortName != null) return false; } else if (!shortName.equals(other.shortName)) return false; if (longName == null) { if (other.longName != null) return false; } else if (!longName.equals(other.longName)) return false; if (textColor == null) { if (other.textColor != null) return false; } else if (!textColor.equals(other.textColor)) return false; if (tripPatternsForRoute == null) { if (other.tripPatternsForRoute != null) return false; } else if (!tripPatternsForRoute.equals(other.tripPatternsForRoute)) return false; if (type == null) { if (other.type != null) return false; } else if (!type.equals(other.type)) return false; return true; } /** * Returns unordered collection of stops associated with route. * <p> * Synchronized because caching stops. * * @return */ public synchronized Collection<Stop> getStops() { // If stop collection already determined then simply return it if (stops != null) return stops; // Get the trip patterns for the route. Can't use the member // variable tripPatternsForRoute since it is only set when the // GTFS data is processed and stored in the db. Since this member // is transient it is not stored in the db and therefore not // available to this client application. But it can be obtained // from the DbConfig. List<TripPattern> tripPatternsForRoute = Core.getInstance().getDbConfig().getTripPatternsForRoute(id); // Stop list not yet determined so determine it now using // trip patterns. Map<String, Stop> stopMap = new HashMap<String, Stop>(); for (TripPattern tripPattern : tripPatternsForRoute) { for (StopPath stopPath : tripPattern.getStopPaths()) { String stopId = stopPath.getStopId(); // If already added this stop then continue to next one if (stopMap.containsKey(stopId)) continue; Stop stop = Core.getInstance().getDbConfig().getStop(stopId); stopMap.put(stopId, stop); } } stops = stopMap.values(); // Return the newly created collection of stops return stops; } /** * Returns the specified trip pattern, or null if that trip pattern doesn't * exist for the route. * * @param tripPatternId * @return */ public TripPattern getTripPattern(String tripPatternId) { List<TripPattern> tripPatternsForRoute = Core.getInstance() .getDbConfig().getTripPatternsForRoute(getId()); for (TripPattern tripPattern : tripPatternsForRoute) { if (tripPattern.getId().equals(tripPatternId)) return tripPattern; } // Never found the specified trip pattern return null; } /** * Returns longest trip pattern for the directionId specified. * Note: gets trip patterns from Core, which means it works * in the core application, not just when processing GTFS data. * * @param directionId * @return */ public TripPattern getLongestTripPatternForDirection(String directionId) { List<TripPattern> tripPatternsForRoute = Core.getInstance() .getDbConfig().getTripPatternsForRoute(getId()); TripPattern longestTripPatternForDir = null; for (TripPattern tripPattern : tripPatternsForRoute) { if (Objects.equals(tripPattern.getDirectionId(), directionId)) { if (longestTripPatternForDir == null || tripPattern.getNumberStopPaths() > longestTripPatternForDir .getNumberStopPaths()) longestTripPatternForDir = tripPattern; } } return longestTripPatternForDir; } /** * Returns the longest trip pattern for each direction ID for the route. * Will typically be two trip patterns since there are usually two * directions per route. * * @return */ public List<TripPattern> getLongestTripPatternForEachDirection() { List<TripPattern> tripPatterns = new ArrayList<TripPattern>(); List<String> directionIds = getDirectionIds(); for (String directionId : directionIds) tripPatterns.add(getLongestTripPatternForDirection(directionId)); return tripPatterns; } /** * Returns list of trip patterns for the directionId specified. * * @param directionId * @return */ public List<TripPattern> getTripPatterns(String directionId) { List<TripPattern> tripPatternsForRoute = Core.getInstance() .getDbConfig().getTripPatternsForRoute(getId()); List<TripPattern> tripPatternsForDir = new ArrayList<TripPattern>(); for (TripPattern tripPattern : tripPatternsForRoute) { if (Objects.equals(tripPattern.getDirectionId(), directionId)) tripPatternsForDir.add(tripPattern); } return tripPatternsForDir; } /** * Returns list of direction IDs for the route. * * @return */ public List<String> getDirectionIds() { List<String> directionIds = new ArrayList<String>(); List<TripPattern> tripPatternsForRoute = Core.getInstance() .getDbConfig().getTripPatternsForRoute(getId()); for (TripPattern tripPattern : tripPatternsForRoute) { String directionId = tripPattern.getDirectionId(); if (!directionIds.contains(directionId)) directionIds.add(directionId); } return directionIds; } /** * Returns unordered collection of path vectors associated with route * @return */ public synchronized Collection<Vector> getPathSegments() { // If stop paths collection already determined then simply return it if (stopPaths != null) return stopPaths; // Get the trip patterns for the route. Can't use the member // variable tripPatternsForRoute since it is only set when the // GTFS data is processed and stored in the db. Since this member // is transient it is not stored in the db and therefore not // available to this client application. But it can be obtained // from the DbConfig. List<TripPattern> tripPatternsForRoute = Core.getInstance().getDbConfig().getTripPatternsForRoute(id); Map<String, StopPath> stopPathMap = new HashMap<String, StopPath>(); for (TripPattern tripPattern : tripPatternsForRoute) { for (StopPath stopPath : tripPattern.getStopPaths()) { String stopPathId = stopPath.getId(); // If already added this stop then continue to next one if (stopPathMap.containsKey(stopPathId)) continue; stopPathMap.put(stopPathId, stopPath); } } // For each of the unique stop paths add the vectors to the collection stopPaths = new ArrayList<Vector>(stopPathMap.values().size()); for (StopPath stopPath : stopPathMap.values()) { for (Vector vector : stopPath.getSegmentVectors()) { stopPaths.add(vector); } } // Return the newly created collection of stop paths return stopPaths; } /** * For each GTFS direction ID returns list of stops that in the appropriate * order for the direction. The appropriate order means that when there are * different trip patterns that the stops that are different will be * inserted appropriately into the list. Synchronized since can be access * through multiple threads. * * @return Map keyed by direction ID and value of List of ordered stop IDs. */ public synchronized Map<String, List<String>> getOrderedStopsByDirection() { // If already determined the stops return the cached map if (orderedStopsPerDirectionMap != null) return orderedStopsPerDirectionMap; // Haven't yet determined ordered stops so do so now orderedStopsPerDirectionMap = new HashMap<String, List<String>>(); // For each direction for (String directionId : getDirectionIds()) { // Determine ordered collection of stops for direction OrderedCollection orderedCollection = new OrderedCollection(); List<TripPattern> tripPatternsForDir = getTripPatterns(directionId); List<List<String>> stopIdsForTripPatternList = new ArrayList<List<String>>(); for (TripPattern tripPattern : tripPatternsForDir) { List<String> stopIdsForTripPattern = tripPattern.getStopIds(); stopIdsForTripPatternList.add(stopIdsForTripPattern); } orderedCollection.add(stopIdsForTripPatternList); orderedStopsPerDirectionMap.put(directionId, orderedCollection.get()); } return orderedStopsPerDirectionMap; } /** * Initializes stopOrderByDirectionMap member if haven't done so yet. Used * by getStopOrder(). Synchronized since can be access through multiple * threads. */ private synchronized void createStopOrderByDirectionMapIfNeedTo() { // If map already created then done if (stopOrderByDirectionMap != null) return; // Map not yet created so create it now stopOrderByDirectionMap = new HashMap<String, Map<String, List<Integer>>>(); Map<String, List<String>> orderedStopsByDirection = getOrderedStopsByDirection(); // Create submap for each direction Id for (String directionId : orderedStopsByDirection.keySet()) { // Create the submap for the direction ID. Map is keyed on stop Id // and contains list of stop order for the stop. Need a list since // a stop can be in a trip multiple times. Map<String, List<Integer>> stopOrderMap = new HashMap<String, List<Integer>>(); stopOrderByDirectionMap.put(directionId, stopOrderMap); // Add each stop for the direction Id List<String> orderedStopsForDirectionList = orderedStopsPerDirectionMap.get(directionId); for (int stopOrder=0; stopOrder<orderedStopsForDirectionList.size(); ++stopOrder) { // Determine stopId (we already have stopOrder) String stopId = orderedStopsForDirectionList.get(stopOrder); // Get list of stop orders for the stop Id List<Integer> stopOrderListForStopId = stopOrderMap.get(stopId); if (stopOrderListForStopId == null) { stopOrderListForStopId = new ArrayList<Integer>(1); stopOrderMap.put(stopId, stopOrderListForStopId); } // For the stopId add the stop order to its list of stop orders stopOrderListForStopId.add(stopOrder); } } } /** * Gets the order for the stop. Important when have multiple trip patterns * for a direction. Allows each stop in the trip to have a unique sequential * stop order so that can save the stop order in the db and then use it for * db queries to order the stop data for a direction for a route. * * @param directionId * @param stopId * @param stopIndex * so that can handle stop being on trip multiple times * @return the stop order for the stop for the direction for the route */ public int getStopOrder(String directionId, String stopId, int stopIndex) { // Make sure initialized createStopOrderByDirectionMapIfNeedTo(); // For the direction get the map of stop orders Map<String, List<Integer>> stopOrderMap = stopOrderByDirectionMap.get(directionId); if (stopOrderMap == null) { logger.error("In Route.getStopOrder() directionId={} is not valid " + "for routeId={}.", directionId, id); return -1; } // For the stop Id for the direction get the list of stop orders List<Integer> stopOrderListForStopId = stopOrderMap.get(stopId); if (stopOrderListForStopId == null) { logger.error("In Route.getStopOrder() stopId={} is not valid " + "for routeId={} directionId={}.", stopId, id, directionId); return -1; } // Return the appropriate stop order. In order to handle situation // where stop might be in trip multiple times need to make sure the // stop order is greater than the stop index. for (int stopOrder : stopOrderListForStopId) { if (stopOrder >= stopIndex) return stopOrder; } // Didn't find appropriate stop order logger.error("In Route.getStopOrder() did not find stop order for " + "for routeId={} directionId={} stopId={} stopIndex={}.", id, directionId, stopId, stopIndex); return -1; } /********************** Getter Methods **************************/ /** * @return the configRev */ public int getConfigRev() { return configRev; } /** * The processed name of the route consisting of the combination of the * route_short_name plus the route_long_name. So get something like "38 - Geary". * * @return the processed name of the route */ public String getName() { return name; } /** * The short name is either specified by route_short_name in the routes.txt * GTFS file or if that is null it will be the long name name. * * @return the short name for the route */ public String getShortName() { return shortName != null ? shortName : name; } /** * The long name is either specified by route_long_name in the routes.txt * GTFS file or if that is null it will be the short name name. * * @return the long name for the route */ public String getLongName() { return longName != null ? longName : name; } /** * @return the id */ public String getId() { return id; } /** * @return the color */ public String getColor() { return color; } /** * @return the textColor */ public String getTextColor() { return textColor; } /** * @return the routeOrder or null if not set */ public Integer getRouteOrder() { return routeOrder; } /** * For setting route order once all routes read in and sorted. * * @param routeOrder * new route order to be set for the route */ public void setRouteOrder(int routeOrder) { this.routeOrder = routeOrder; } /** * @return the hidden */ public boolean isHidden() { return hidden; } /** * @return the type */ public String getType() { return type; } /** * @return the description */ public String getDescription() { return description; } /** * @return the extent of the stops for the route */ public Extent getExtent() { return extent; } /** * For specifying on a per route basis how far AVL report can be from * segment and still have it be considered a match. * * @return the max distance if set, otherwise NaN */ public double getMaxAllowableDistanceFromSegment() { if (maxDistance != null) return maxDistance; else return Double.NaN; } /** * Just for debugging * * @param args */ public static void main(String args[]) { System.out.println(StringUtils.paddedName("Y2")); System.out.println(StringUtils.paddedName("Y101")); System.out.println(StringUtils.paddedName("Y2xx")); System.out.println(StringUtils.paddedName("123SKJFD")); System.out.println(StringUtils.paddedName("123")); System.out.println(StringUtils.paddedName("123.456")); } }