/* This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package org.opentripplanner.routing.edgetype.factory; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.apache.commons.math3.util.FastMath; import org.onebusaway.gtfs.model.Agency; import org.onebusaway.gtfs.model.AgencyAndId; import org.onebusaway.gtfs.model.Frequency; import org.onebusaway.gtfs.model.Pathway; import org.onebusaway.gtfs.model.Route; import org.onebusaway.gtfs.model.ShapePoint; import org.onebusaway.gtfs.model.Stop; import org.onebusaway.gtfs.model.StopTime; import org.onebusaway.gtfs.model.Transfer; import org.onebusaway.gtfs.model.Trip; import org.onebusaway.gtfs.services.GtfsRelationalDao; import org.onebusaway.gtfs.services.calendar.CalendarService; import org.opentripplanner.common.geometry.DistanceLibrary; import org.opentripplanner.common.geometry.GeometryUtils; import org.opentripplanner.common.geometry.PackedCoordinateSequence; import org.opentripplanner.common.geometry.SphericalDistanceLibrary; import org.opentripplanner.common.model.T2; import org.opentripplanner.gbannotation.BogusShapeDistanceTraveled; import org.opentripplanner.gbannotation.BogusShapeGeometry; import org.opentripplanner.gbannotation.BogusShapeGeometryCaught; import org.opentripplanner.gbannotation.HopSpeedFast; import org.opentripplanner.gbannotation.HopSpeedSlow; import org.opentripplanner.gbannotation.HopZeroTime; import org.opentripplanner.gbannotation.NegativeDwellTime; import org.opentripplanner.gbannotation.NegativeHopTime; import org.opentripplanner.gbannotation.StopAtEntrance; import org.opentripplanner.gbannotation.TripDegenerate; import org.opentripplanner.gbannotation.TripUndefinedService; import org.opentripplanner.gtfs.GtfsContext; import org.opentripplanner.gtfs.GtfsLibrary; import org.opentripplanner.routing.core.ServiceIdToNumberService; import org.opentripplanner.routing.core.StopTransfer; import org.opentripplanner.routing.core.TransferTable; import org.opentripplanner.routing.core.TraverseMode; import org.opentripplanner.routing.edgetype.FreeEdge; import org.opentripplanner.routing.edgetype.FrequencyAlight; import org.opentripplanner.routing.edgetype.FrequencyBasedTripPattern; import org.opentripplanner.routing.edgetype.FrequencyBoard; import org.opentripplanner.routing.edgetype.FrequencyDwell; import org.opentripplanner.routing.edgetype.FrequencyHop; import org.opentripplanner.routing.edgetype.HopEdge; import org.opentripplanner.routing.edgetype.PathwayEdge; import org.opentripplanner.routing.edgetype.PatternDwell; import org.opentripplanner.routing.edgetype.PatternHop; import org.opentripplanner.routing.edgetype.PatternInterlineDwell; import org.opentripplanner.routing.edgetype.PreAlightEdge; import org.opentripplanner.routing.edgetype.PreBoardEdge; import org.opentripplanner.routing.edgetype.ScheduledStopPattern; import org.opentripplanner.routing.edgetype.TableTripPattern; import org.opentripplanner.routing.edgetype.TimedTransferEdge; import org.opentripplanner.routing.edgetype.TransferEdge; import org.opentripplanner.routing.edgetype.TransitBoardAlight; import org.opentripplanner.routing.graph.Edge; import org.opentripplanner.routing.graph.Graph; import org.opentripplanner.routing.graph.Vertex; import org.opentripplanner.routing.impl.DefaultFareServiceFactory; import org.opentripplanner.routing.impl.OnBoardDepartServiceImpl; import org.opentripplanner.routing.services.FareService; import org.opentripplanner.routing.services.FareServiceFactory; import org.opentripplanner.routing.services.OnBoardDepartService; import org.opentripplanner.routing.vertextype.PatternArriveVertex; import org.opentripplanner.routing.vertextype.PatternDepartVertex; import org.opentripplanner.routing.vertextype.TransitStop; import org.opentripplanner.routing.vertextype.TransitStopArrive; import org.opentripplanner.routing.vertextype.TransitStopDepart; import org.opentripplanner.routing.vertextype.TransitVertex; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.CoordinateSequence; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import com.vividsolutions.jts.geom.LineString; import com.vividsolutions.jts.linearref.LinearLocation; import com.vividsolutions.jts.linearref.LocationIndexedLine; class InterliningTrip implements Comparable<InterliningTrip> { public Trip trip; public StopTime firstStopTime; public StopTime lastStopTime; TableTripPattern tripPattern; InterliningTrip(Trip trip, List<StopTime> stopTimes, TableTripPattern tripPattern) { this.trip = trip; this.firstStopTime = stopTimes.get(0); this.lastStopTime = stopTimes.get(stopTimes.size() - 1); this.tripPattern = tripPattern; } public int getPatternIndex() { return tripPattern.getTripIndex(trip); } @Override public int compareTo(InterliningTrip o) { return firstStopTime.getArrivalTime() - o.firstStopTime.getArrivalTime(); } @Override public boolean equals(Object o) { if (o instanceof InterliningTrip) { return compareTo((InterliningTrip) o) == 0; } return false; } } class BlockIdAndServiceId { public String blockId; public AgencyAndId serviceId; BlockIdAndServiceId(String blockId, AgencyAndId serviceId) { this.blockId = blockId; this.serviceId = serviceId; } public boolean equals(Object o) { if (o instanceof BlockIdAndServiceId) { BlockIdAndServiceId other = ((BlockIdAndServiceId) o); return other.blockId.equals(blockId) && other.serviceId.equals(serviceId); } return false; } @Override public int hashCode() { return blockId.hashCode() * 31 + serviceId.hashCode(); } } class InterlineSwitchoverKey { public Stop s0, s1; public TableTripPattern pattern1, pattern2; public InterlineSwitchoverKey(Stop s0, Stop s1, TableTripPattern pattern1, TableTripPattern pattern2) { this.s0 = s0; this.s1 = s1; this.pattern1 = pattern1; this.pattern2 = pattern2; } public boolean equals(Object o) { if (o instanceof InterlineSwitchoverKey) { InterlineSwitchoverKey other = (InterlineSwitchoverKey) o; return other.s0.equals(s0) && other.s1.equals(s1) && other.pattern1 == pattern1 && other.pattern2 == pattern2; } return false; } public int hashCode() { return (((s0.hashCode() * 31) + s1.hashCode()) * 31 + pattern1.hashCode()) * 31 + pattern2.hashCode(); } } class IndexedLineSegment { private static final double RADIUS = SphericalDistanceLibrary.RADIUS_OF_EARTH_IN_M; int index; Coordinate start; Coordinate end; private DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); private double lineLength; public IndexedLineSegment(int index, Coordinate start, Coordinate end) { this.index = index; this.start = start; this.end = end; this.lineLength = distanceLibrary.fastDistance(start, end); } // in radians static double bearing(Coordinate c1, Coordinate c2) { double deltaLon = (c2.x - c1.x) * FastMath.PI / 180; double lat1Radians = c1.y * FastMath.PI / 180; double lat2Radians = c2.y * FastMath.PI / 180; double y = FastMath.sin(deltaLon) * FastMath.cos(lat2Radians); double x = FastMath.cos(lat1Radians)*FastMath.sin(lat2Radians) - FastMath.sin(lat1Radians)*FastMath.cos(lat2Radians)*FastMath.cos(deltaLon); return FastMath.atan2(y, x); } double crossTrackError(Coordinate coord) { double distanceFromStart = distanceLibrary.fastDistance(start, coord); double bearingToCoord = bearing(start, coord); double bearingToEnd = bearing(start, end); return FastMath.asin(FastMath.sin(distanceFromStart / RADIUS) * FastMath.sin(bearingToCoord - bearingToEnd)) * RADIUS; } double distance(Coordinate coord) { double cte = crossTrackError(coord); double atd = alongTrackDistance(coord, cte); double inverseAtd = inverseAlongTrackDistance(coord, -cte); double distanceToStart = distanceLibrary.fastDistance(coord, start); double distanceToEnd = distanceLibrary.fastDistance(coord, end); if (distanceToStart < distanceToEnd) { //we might be behind the line start if (inverseAtd > lineLength) { //we are behind line start return distanceToStart; } else { //we are within line return Math.abs(cte); } } else { //we might be after line end if (atd > lineLength) { //we are behind line end, so we that's the nearest point return distanceToEnd; } else { //we are within line return Math.abs(cte); } } } private double inverseAlongTrackDistance(Coordinate coord, double inverseCrossTrackError) { double distanceFromEnd = distanceLibrary.fastDistance(end, coord); double alongTrackDistance = FastMath.acos(FastMath.cos(distanceFromEnd / RADIUS) / FastMath.cos(inverseCrossTrackError / RADIUS)) * RADIUS; return alongTrackDistance; } public double fraction(Coordinate coord) { double cte = crossTrackError(coord); double distanceToStart = distanceLibrary.fastDistance(coord, start); double distanceToEnd = distanceLibrary.fastDistance(coord, end); if (cte < distanceToStart && cte < distanceToEnd) { double atd = alongTrackDistance(coord, cte); return atd / lineLength; } else { if (distanceToStart < distanceToEnd) { return 0; } else { return 1; } } } private double alongTrackDistance(Coordinate coord, double crossTrackError) { double distanceFromStart = distanceLibrary.fastDistance(start, coord); double alongTrackDistance = FastMath.acos(FastMath.cos(distanceFromStart / RADIUS) / FastMath.cos(crossTrackError / RADIUS)) * RADIUS; return alongTrackDistance; } } class IndexedLineSegmentComparator implements Comparator<IndexedLineSegment> { private Coordinate coord; public IndexedLineSegmentComparator(Coordinate coord) { this.coord = coord; } @Override public int compare(IndexedLineSegment a, IndexedLineSegment b) { return (int) FastMath.signum(a.distance(coord) - b.distance(coord)); } } /** * Generates a set of edges from GTFS. */ public class GTFSPatternHopFactory { private static final Logger LOG = LoggerFactory.getLogger(GTFSPatternHopFactory.class); private static GeometryFactory _geometryFactory = GeometryUtils.getGeometryFactory(); private GtfsRelationalDao _dao; private CalendarService _calendarService; private Map<ShapeSegmentKey, LineString> _geometriesByShapeSegmentKey = new HashMap<ShapeSegmentKey, LineString>(); private Map<AgencyAndId, LineString> _geometriesByShapeId = new HashMap<AgencyAndId, LineString>(); private Map<AgencyAndId, double[]> _distancesByShapeId = new HashMap<AgencyAndId, double[]>(); private boolean _deleteUselessDwells = true; private ArrayList<PatternDwell> potentiallyUselessDwells = new ArrayList<PatternDwell> (); private FareServiceFactory fareServiceFactory; private HashMap<BlockIdAndServiceId, List<InterliningTrip>> tripsForBlock = new HashMap<BlockIdAndServiceId, List<InterliningTrip>>(); private Map<InterlineSwitchoverKey, PatternInterlineDwell> interlineDwells = new HashMap<InterlineSwitchoverKey, PatternInterlineDwell>(); HashMap<ScheduledStopPattern, TableTripPattern> patterns = new HashMap<ScheduledStopPattern, TableTripPattern>(); private GtfsStopContext context = new GtfsStopContext(); private int defaultStreetToStopTime; private static final DistanceLibrary distanceLibrary = SphericalDistanceLibrary.getInstance(); private double maxStopToShapeSnapDistance = 150; public GTFSPatternHopFactory(GtfsContext context) { this._dao = context.getDao(); this._calendarService = context.getCalendarService(); } // // There's already a departure at this time on this trip pattern. This means // // that either (a) this will have all the same stop times as that one, and thus // // will be a duplicate of it, or (b) it will have different stops, and thus // // break the assumption that trips are non-overlapping. // if (!tripPattern.stopTimesIdentical(stopTimes, insertionPoint)) { // LOG.warn(GraphBuilderAnnotation.register(graph, // Variety.TRIP_DUPLICATE_DEPARTURE, trip.getId(), // tripPattern.getTrip(insertionPoint))); // simple = true; // createSimpleHops(graph, trip, stopTimes); // } else { // LOG.warn(GraphBuilderAnnotation.register(graph, Variety.TRIP_DUPLICATE, // trip.getId(), tripPattern.getTrip(insertionPoint))); // simple = true; // } /* check stoptimes for negative hops and dwells (midnight crossings?) */ //for (int i = 0; i < stopTimes.size() - 1; i++) { //StopTime st0 = stopTimes.get(i); //StopTime st1 = stopTimes.get(i + 1); // //int dwellTime = st0.getDepartureTime() - st0.getArrivalTime(); //int runningTime = st1.getArrivalTime() - st0.getDepartureTime(); // //if (runningTime < 0) { // LOG.warn(GraphBuilderAnnotation.register(graph, Variety.NEGATIVE_HOP_TIME, st0, st1)); // // break; //} //if (dwellTime < 0) { // LOG.warn(GraphBuilderAnnotation.register(graph, // Variety.NEGATIVE_DWELL_TIME, st0)); // dwellTime = 0; //} // //try { // tripPattern.addHop(i, insertionPoint, st0.getDepartureTime(), // runningTime, st1.getArrivalTime(), dwellTime, // st0.getStopHeadsign(), trip); //} catch (TripOvertakingException e) { // LOG.warn(GraphBuilderAnnotation.register(graph, // Variety.TRIP_OVERTAKING, e.overtaker, e.overtaken, e.stopIndex)); // createSimpleHops(graph, trip, stopTimes); // simple = true; // break; //} //} /** Generate the edges. Assumes that there are already vertices in the graph for the stops. */ public void run(Graph graph) { if (fareServiceFactory == null) { fareServiceFactory = new DefaultFareServiceFactory(); } fareServiceFactory.setDao(_dao); loadStops(graph); loadPathways(graph); loadAgencies(graph); clearCachedData(); LOG.debug("building hops from trips"); Collection<Trip> trips = _dao.getAllTrips(); int tripCount = 0; /* first, record which trips are used by one or more frequency entries */ HashMap<Trip, List<Frequency>> tripFrequencies = new HashMap<Trip, List<Frequency>>(); for(Frequency freq : _dao.getAllFrequencies()) { List<Frequency> freqs = tripFrequencies.get(freq.getTrip()); if(freqs == null) { freqs = new ArrayList<Frequency>(); tripFrequencies.put(freq.getTrip(), freqs); } freqs.add(freq); } /* then loop over all trips handling each one as a frequency-based or scheduled trip */ TRIP : for (Trip trip : trips) { tripCount++; if (tripCount % 100000 == 0) LOG.debug("trips=" + tripCount + "/" + trips.size()); if ( ! _calendarService.getServiceIds().contains(trip.getServiceId())) { LOG.warn(graph.addBuilderAnnotation(new TripUndefinedService(trip))); } /* GTFS stop times frequently contain duplicate, missing, or incorrect entries */ List<StopTime> stopTimes = getNonduplicateStopTimesForTrip(trip); // duplicate stopIds filterStopTimes(stopTimes, graph); // duplicate times (0-time), negative, fast or slow hops interpolateStopTimes(stopTimes); // interpolate between timepoints if (stopTimes.size() < 2) { LOG.warn(graph.addBuilderAnnotation(new TripDegenerate(trip))); continue TRIP; } /* check to see if this trip is used by one or more frequency entries */ List<Frequency> frequencies = tripFrequencies.get(trip); if(frequencies != null) { // before creating frequency-based trips, check for single-instance frequencies. Collections.sort(frequencies, new Comparator<Frequency>() { @Override public int compare(Frequency o1, Frequency o2) { return o1.getStartTime() - o2.getStartTime(); } }); Frequency frequency = frequencies.get(0); if (frequencies.size() > 1 || frequency.getStartTime() != stopTimes.get(0).getDepartureTime() || frequency.getEndTime() - frequency.getStartTime() > frequency.getHeadwaySecs()) { T2<FrequencyBasedTripPattern,List<FrequencyHop>> patternAndHops = makeFrequencyPattern(graph, trip, stopTimes); List<FrequencyHop> hops = patternAndHops.getSecond(); FrequencyBasedTripPattern frequencyPattern = patternAndHops.getFirst(); if (frequencyPattern != null) frequencyPattern.createRanges(frequencies); createGeometry(graph, trip, stopTimes, hops); continue TRIP; } // else fall through and treat this as a normal trip } /* this trip is not frequency-based, add it to the corresponding trip pattern */ // maybe rename ScheduledStopPattern to TripPatternKey? ScheduledStopPattern stopPattern = ScheduledStopPattern.fromTrip(trip, stopTimes); TableTripPattern tripPattern = patterns.get(stopPattern); if (tripPattern == null) { // it's the first time we are encountering this stops+pickups+serviceId combination T2<TableTripPattern, List<PatternHop>> patternAndHops = makePatternVerticesAndEdges(graph, trip, stopPattern, stopTimes); List<PatternHop> hops = patternAndHops.getSecond(); createGeometry(graph, trip, stopTimes, hops); tripPattern = patternAndHops.getFirst(); patterns.put(stopPattern, tripPattern); } tripPattern.addTrip(trip, stopTimes); /* record which block trips belong to so they can be linked up later */ String blockId = trip.getBlockId(); if (blockId != null && !blockId.equals("")) { addTripToInterliningMap(trip, stopTimes, tripPattern); } } // END for loop over trips /* link up interlined trips (where a vehicle continues on to another logical trip) */ for (List<InterliningTrip> blockTrips : tripsForBlock.values()) { if (blockTrips.size() == 1) { continue; // skip blocks of only a single trip } Collections.sort(blockTrips); // sort trips within the block by first arrival time /* iterate over trips in this block/schedule, linking them */ for (int i = 0; i < blockTrips.size() - 1; ++i) { InterliningTrip fromInterlineTrip = blockTrips.get(i); InterliningTrip toInterlineTrip = blockTrips.get(i + 1); Trip fromTrip = fromInterlineTrip.trip; Trip toTrip = toInterlineTrip.trip; StopTime st0 = fromInterlineTrip.lastStopTime; StopTime st1 = toInterlineTrip.firstStopTime; Stop s0 = st0.getStop(); Stop s1 = st1.getStop(); if (st0.getPickupType() == 1) { /* do not create an interline dwell when the last stop on the arriving trip does * not allow pickups, since this probably means that, while two trips share a * block, riders cannot stay on the vehicle during the deadhead */ continue; } Trip fromExemplar = fromInterlineTrip.tripPattern.exemplar; Trip toExemplar = toInterlineTrip.tripPattern.exemplar; // make a key representing all interline dwells between these same vertices InterlineSwitchoverKey dwellKey = new InterlineSwitchoverKey( s0, s1, fromInterlineTrip.tripPattern, toInterlineTrip.tripPattern); // do we already have a PatternInterlineDwell edge for this dwell? PatternInterlineDwell dwell = getInterlineDwell(dwellKey); if (dwell == null) { // create the dwell because it does not exist yet Vertex startJourney = context.patternArriveNodes.get(new T2<Stop, Trip>(s0, fromExemplar)); Vertex endJourney = context.patternDepartNodes.get(new T2<Stop, Trip>(s1, toExemplar)); // toTrip is just an exemplar; dwell edges can contain many trip connections dwell = new PatternInterlineDwell(startJourney, endJourney, toTrip); interlineDwells.put(dwellKey, dwell); } int dwellTime = st1.getDepartureTime() - st0.getArrivalTime(); dwell.addTrip(fromTrip, toTrip, dwellTime, fromInterlineTrip.getPatternIndex(), toInterlineTrip.getPatternIndex()); } } // END loop over interlining blocks loadTransfers(graph); if (_deleteUselessDwells) deleteUselessDwells(graph); // /* this is the wrong place to do this: it should be done on all feeds at once, or at deserialization*/ // LOG.info("begin indexing large patterns"); // for (TableTripPattern tp : context.tripPatternIds.keySet()) { // tp.finish(); // } clearCachedData(); graph.putService(FareService.class, fareServiceFactory.makeFareService()); graph.putService(ServiceIdToNumberService.class, new ServiceIdToNumberService(context.serviceIds)); graph.putService(OnBoardDepartService.class, new OnBoardDepartServiceImpl()); } static int cg = 0; private <T extends Edge & HopEdge> void createGeometry(Graph graph, Trip trip, List<StopTime> stopTimes, List<T> hops) { cg += 1; AgencyAndId shapeId = trip.getShapeId(); if (shapeId == null || shapeId.getId() == null || shapeId.getId().equals("")) return; // this trip has no associated shape_id, bail out // TODO: is this right? don't we want to use the straight-line logic below? /* Detect presence or absence of shape_dist_traveled on a per-trip basis */ StopTime st0 = stopTimes.get(0); boolean hasShapeDist = st0.isShapeDistTraveledSet(); if (hasShapeDist) { // this trip has shape_dist in stop_times for (int i = 0; i < hops.size(); ++i) { Edge hop = hops.get(i); st0 = stopTimes.get(i); StopTime st1 = stopTimes.get(i + 1); ((HopEdge)hop).setGeometry(getHopGeometryViaShapeDistTraveled(graph, shapeId, st0, st1, hop.getFromVertex(), hop.getToVertex())); } return; } LineString shape = getLineStringForShapeId(shapeId); if (shape == null) { // this trip has a shape_id, but no such shape exists, and no shape_dist in stop_times // create straight line segments between stops for each hop for (int i = 0; i < stopTimes.size() - 1; ++i) { st0 = stopTimes.get(i); StopTime st1 = stopTimes.get(i + 1); LineString geometry = createSimpleGeometry(st0.getStop(), st1.getStop()); hops.get(i).setGeometry(geometry); } return; } // This trip does not have shape_dist in stop_times, but does have an associated shape. ArrayList<IndexedLineSegment> segments = new ArrayList<IndexedLineSegment>(); for (int i = 0 ; i < shape.getNumPoints() - 1; ++i) { segments.add(new IndexedLineSegment(i, shape.getCoordinateN(i), shape.getCoordinateN(i + 1))); } // Find possible segment matches for each stop. List<List<IndexedLineSegment>> possibleSegmentsForStop = new ArrayList<List<IndexedLineSegment>>(); int minSegmentIndex = 0; for (int i = 0; i < stopTimes.size() ; ++i) { Stop stop = stopTimes.get(i).getStop(); Coordinate coord = new Coordinate(stop.getLon(), stop.getLat()); List<IndexedLineSegment> stopSegments = new ArrayList<IndexedLineSegment>(); double bestDistance = Double.MAX_VALUE; IndexedLineSegment bestSegment = null; int maxSegmentIndex = -1; int index = -1; int minSegmentIndexForThisStop = -1; for (IndexedLineSegment segment : segments) { index ++; if (segment.index < minSegmentIndex) { continue; } double distance = segment.distance(coord); if (distance < maxStopToShapeSnapDistance) { stopSegments.add(segment); maxSegmentIndex = index; if (minSegmentIndexForThisStop == -1) minSegmentIndexForThisStop = index; } else if (distance < bestDistance) { bestDistance = distance; bestSegment = segment; if (maxSegmentIndex != -1) { maxSegmentIndex = index; } } } if (stopSegments.size() == 0) { //no segments within 150m //fall back to nearest segment stopSegments.add(bestSegment); minSegmentIndex = bestSegment.index; } else { minSegmentIndex = minSegmentIndexForThisStop; Collections.sort(stopSegments, new IndexedLineSegmentComparator(coord)); } for (int j = i - 1; j >= 0; j --) { for (Iterator<IndexedLineSegment> it = possibleSegmentsForStop.get(j).iterator(); it.hasNext(); ) { IndexedLineSegment segment = it.next(); if (segment.index > maxSegmentIndex) { it.remove(); } } } possibleSegmentsForStop.add(stopSegments); } List<LinearLocation> locations = getStopLocations(possibleSegmentsForStop, stopTimes, 0, -1); if (locations == null) { // this only happens on shape which have points very far from // their stop sequence. So we'll fall back to trivial stop-to-stop // linking, even though theoretically we could do better. for (int i = 0; i < stopTimes.size() - 1; ++i) { st0 = stopTimes.get(i); StopTime st1 = stopTimes.get(i + 1); LineString geometry = createSimpleGeometry(st0.getStop(), st1.getStop()); hops.get(i).setGeometry(geometry); //this warning is not strictly correct, but will do LOG.warn(graph.addBuilderAnnotation(new BogusShapeGeometryCaught(shapeId, st0, st1))); } return; } Iterator<LinearLocation> locationIt = locations.iterator(); LinearLocation endLocation = locationIt.next(); double distanceSoFar = 0; int last = 0; for (int i = 0; i < hops.size(); ++i) { Edge hop = hops.get(i); LinearLocation startLocation = endLocation; endLocation = locationIt.next(); //convert from LinearLocation to distance //advance distanceSoFar up to start of segment containing startLocation; //it does not matter at all if this is accurate so long as it is consistent for (int j = last; j < startLocation.getSegmentIndex(); ++j) { Coordinate from = shape.getCoordinateN(j); Coordinate to = shape.getCoordinateN(j + 1); double xd = from.x - to.x; double yd = from.y - to.y; distanceSoFar += FastMath.sqrt(xd * xd + yd * yd); } last = startLocation.getSegmentIndex(); double startIndex = distanceSoFar + startLocation.getSegmentFraction() * startLocation.getSegmentLength(shape); //advance distanceSoFar up to start of segment containing endLocation for (int j = last; j < endLocation.getSegmentIndex(); ++j) { Coordinate from = shape.getCoordinateN(j); Coordinate to = shape.getCoordinateN(j + 1); double xd = from.x - to.x; double yd = from.y - to.y; distanceSoFar += FastMath.sqrt(xd * xd + yd * yd); } last = startLocation.getSegmentIndex(); double endIndex = distanceSoFar + endLocation.getSegmentFraction() * endLocation.getSegmentLength(shape); ShapeSegmentKey key = new ShapeSegmentKey(shapeId, startIndex, endIndex); LineString geometry = _geometriesByShapeSegmentKey.get(key); if (geometry == null) { LocationIndexedLine locationIndexed = new LocationIndexedLine(shape); geometry = (LineString) locationIndexed.extractLine(startLocation, endLocation); // Pack the resulting line string CoordinateSequence sequence = new PackedCoordinateSequence.Double(geometry .getCoordinates(), 2); geometry = _geometryFactory.createLineString(sequence); } ((HopEdge)hop).setGeometry(geometry); } } /** * Find a consistent, increasing list of LinearLocations along a shape for a set of stops. * Handles loops routes. * @return */ private List<LinearLocation> getStopLocations(List<List<IndexedLineSegment>> possibleSegmentsForStop, List<StopTime> stopTimes, int index, int prevSegmentIndex) { if (index == stopTimes.size()) { return new LinkedList<LinearLocation>(); } StopTime st = stopTimes.get(index); Stop stop = st.getStop(); Coordinate stopCoord = new Coordinate(stop.getLon(), stop.getLat()); for (IndexedLineSegment segment : possibleSegmentsForStop.get(index)) { if (segment.index < prevSegmentIndex) { //can't go backwards along line continue; } List<LinearLocation> locations = getStopLocations(possibleSegmentsForStop, stopTimes, index + 1, segment.index); if (locations != null) { LinearLocation location = new LinearLocation(0, segment.index, segment.fraction(stopCoord)); locations.add(0, location); return locations; //we found one! } } return null; } private T2<FrequencyBasedTripPattern, List<FrequencyHop>> makeFrequencyPattern(Graph graph, Trip trip, List<StopTime> stopTimes) { FrequencyBasedTripPattern pattern = new FrequencyBasedTripPattern(trip, stopTimes.size(), getServiceId(trip)); TraverseMode mode = GtfsLibrary.getTraverseMode(trip.getRoute()); int lastStop = stopTimes.size() - 1; int i; StopTime st1 = null; TransitVertex psv0arrive, psv1arrive = null; TransitVertex psv0depart; ArrayList<Edge> createdEdges = new ArrayList<Edge>(); ArrayList<Vertex> createdVertices = new ArrayList<Vertex>(); List<Stop> stops = new ArrayList<Stop>(); int offset = stopTimes.get(0).getDepartureTime(); ArrayList<FrequencyHop> hops = new ArrayList<FrequencyHop>(); for (i = 0; i < lastStop; i++) { StopTime st0 = stopTimes.get(i); Stop s0 = st0.getStop(); stops.add(s0); st1 = stopTimes.get(i + 1); Stop s1 = st1.getStop(); if (i == lastStop - 1) stops.add(s1); int arrivalTime = st1.getArrivalTime() - offset; int departureTime = st0.getDepartureTime() - offset; int dwellTime = st0.getDepartureTime() - st0.getArrivalTime(); int runningTime = arrivalTime - departureTime; if (runningTime < 0) { LOG.warn(graph.addBuilderAnnotation(new NegativeHopTime(st0, st1))); //back out hops and give up for (Edge e: createdEdges) { e.getFromVertex().removeOutgoing(e); e.getToVertex().removeIncoming(e); } for (Vertex v : createdVertices) { graph.removeVertexAndEdges(v); } return null; } // create journey vertices if (i != 0) { //dwells are possible on stops after the first psv0arrive = psv1arrive; if (dwellTime == 0) { //if dwell time is zero, we would like to not create psv0depart //use psv0arrive instead psv0depart = psv0arrive; } else { psv0depart = new PatternDepartVertex(graph, trip, st0); createdVertices.add(psv0depart); FrequencyDwell dwell = new FrequencyDwell(psv0arrive, psv0depart, i, pattern); createdEdges.add(dwell); } } else { psv0depart = new PatternDepartVertex(graph, trip, st0); createdVertices.add(psv0depart); } psv1arrive = new PatternArriveVertex(graph, trip, st1); createdVertices.add(psv1arrive); FrequencyHop hop = new FrequencyHop(psv0depart, psv1arrive, s0, s1, i, pattern); createdEdges.add(hop); hops.add(hop); String headsign = st0.getStopHeadsign(); if (headsign == null) { headsign = trip.getTripHeadsign(); } pattern.addHop(i, departureTime, runningTime, arrivalTime, dwellTime, headsign); TransitStopDepart stopDepart = context.stopDepartNodes.get(s0); TransitStopArrive stopArrive = context.stopArriveNodes.get(s1); final int serviceId = getServiceId(trip); Edge board = new FrequencyBoard(stopDepart, psv0depart, pattern, i, mode, serviceId); Edge alight = new FrequencyAlight(psv1arrive, stopArrive, pattern, i, mode, serviceId); createdEdges.add(board); createdEdges.add(alight); } pattern.setStops(stops); int wheelchair = 0; if (trip.getWheelchairAccessible() == 1) { wheelchair = TableTripPattern.FLAG_WHEELCHAIR_ACCESSIBLE; } int bikes = 0; if ((trip.getRoute().getBikesAllowed() == 2 && trip.getTripBikesAllowed() != 1) || trip.getTripBikesAllowed() == 2) { bikes = TableTripPattern.FLAG_BIKES_ALLOWED; } pattern.setTripFlags(wheelchair | bikes); return new T2<FrequencyBasedTripPattern, List<FrequencyHop>>(pattern, hops); } /** * scan through the given list, looking for clearly incorrect series of stoptimes * and unsetting / annotating them. unsetting the arrival/departure time of clearly incorrect * stoptimes will cause them to be interpolated in the next step. * * @param stopTimes the stoptimes to be filtered (from a single trip) * @param graph the graph where annotations will be registered */ private void filterStopTimes(List<StopTime> stopTimes, Graph graph) { if (stopTimes.size() < 2) return; StopTime st0 = stopTimes.get(0); if (!st0.isDepartureTimeSet() && st0.isArrivalTimeSet()) { /* set depature time if it is missing */ st0.setDepartureTime(st0.getArrivalTime()); } boolean midnightCrossed = false; final int HOUR = 60 * 60; for (int i = 1; i < stopTimes.size(); i++) { boolean st1bogus = false; StopTime st1 = stopTimes.get(i); if (midnightCrossed) { if (st1.isDepartureTimeSet()) st1.setDepartureTime(st1.getDepartureTime() + 24 * HOUR); if (st1.isArrivalTimeSet()) st1.setArrivalTime(st1.getArrivalTime() + 24 * HOUR); } if (!st1.isDepartureTimeSet() && st1.isArrivalTimeSet()) { /* set departure time if it is missing */ st1.setDepartureTime(st1.getArrivalTime()); } /* do not process non-timepoint stoptimes, * which are of course identical to other adjacent non-timepoint stoptimes */ if ( ! (st1.isArrivalTimeSet() && st1.isDepartureTimeSet())) { continue; } int dwellTime = st0.getDepartureTime() - st0.getArrivalTime(); if (dwellTime < 0) { LOG.warn(graph.addBuilderAnnotation(new NegativeDwellTime(st0))); if (st0.getArrivalTime() > 23 * HOUR && st0.getDepartureTime() < 1 * HOUR) { midnightCrossed = true; st0.setDepartureTime(st0.getDepartureTime() + 24 * HOUR); } else { st0.setDepartureTime(st0.getArrivalTime()); } } int runningTime = st1.getArrivalTime() - st0.getDepartureTime(); if (runningTime < 0) { LOG.warn(graph.addBuilderAnnotation(new NegativeHopTime(new StopTime(st0), new StopTime(st1)))); // negative hops are usually caused by incorrect coding of midnight crossings midnightCrossed = true; if (st0.getDepartureTime() > 23 * HOUR && st1.getArrivalTime() < 1 * HOUR) { st1.setArrivalTime(st1.getArrivalTime() + 24 * HOUR); } else { st1.setArrivalTime(st0.getDepartureTime()); } } double hopDistance = distanceLibrary.fastDistance( st0.getStop().getLat(), st0.getStop().getLon(), st1.getStop().getLat(), st1.getStop().getLon()); double hopSpeed = hopDistance/runningTime; /* zero-distance hops are probably not harmful, though they could be better * represented as dwell times if (hopDistance == 0) { LOG.warn(GraphBuilderAnnotation.register(graph, Variety.HOP_ZERO_DISTANCE, runningTime, st1.getTrip().getId(), st1.getStopSequence())); } */ // sanity-check the hop if (st0.getArrivalTime() == st1.getArrivalTime() || st0.getDepartureTime() == st1.getDepartureTime()) { LOG.trace("{} {}", st0, st1); // series of identical stop times at different stops LOG.trace(graph.addBuilderAnnotation(new HopZeroTime((float) hopDistance, st1.getTrip(), st1.getStopSequence()))); // clear stoptimes that are obviously wrong, causing them to later be interpolated /* FIXME (lines commented out because they break routability in multi-feed NYC for some reason -AMB) */ // st1.clearArrivalTime(); // st1.clearDepartureTime(); st1bogus = true; } else if (hopSpeed > 45) { // 45 m/sec ~= 100 miles/hr // elapsed time of 0 will give speed of +inf LOG.trace(graph.addBuilderAnnotation(new HopSpeedFast((float) hopSpeed, (float) hopDistance, st0.getTrip(), st0.getStopSequence()))); } else if (hopSpeed < 0.1) { // 0.1 m/sec ~= 0.2 miles/hr LOG.trace(graph.addBuilderAnnotation(new HopSpeedSlow((float) hopSpeed, (float) hopDistance, st0.getTrip(), st0.getStopSequence()))); } // st0 should reflect the last stoptime that was not clearly incorrect if ( ! st1bogus) st0 = st1; } // END for loop over stop times } private void loadAgencies(Graph graph) { for (Agency agency : _dao.getAllAgencies()) { graph.addAgency(agency); } } private PatternInterlineDwell getInterlineDwell(InterlineSwitchoverKey key) { return interlineDwells.get(key); } private void loadPathways(Graph graph) { for (Pathway pathway : _dao.getAllPathways()) { Vertex fromVertex = context.stopNodes.get(pathway.getFromStop()); Vertex toVertex = context.stopNodes.get(pathway.getToStop()); if (pathway.isWheelchairTraversalTimeSet()) { new PathwayEdge(fromVertex, toVertex, pathway.getTraversalTime(), pathway.getWheelchairTraversalTime()); } else { new PathwayEdge(fromVertex, toVertex, pathway.getTraversalTime()); } } } private void loadStops(Graph graph) { for (Stop stop : _dao.getAllStops()) { if (context.stops.contains(stop.getId())) { continue; } context.stops.add(stop.getId()); //add a vertex representing the stop TransitStop stopVertex = new TransitStop(graph, stop); stopVertex.setStreetToStopTime(defaultStreetToStopTime); context.stopNodes.put(stop, stopVertex); if (stop.getLocationType() != 2) { //add a vertex representing arriving at the stop TransitStopArrive arrive = new TransitStopArrive(graph, stop, stopVertex); context.stopArriveNodes.put(stop, arrive); //add a vertex representing departing from the stop TransitStopDepart depart = new TransitStopDepart(graph, stop, stopVertex); context.stopDepartNodes.put(stop, depart); //add edges from arrive to stop and stop to depart new PreAlightEdge(arrive, stopVertex); new PreBoardEdge(stopVertex, depart); } } } /** Delete dwell edges that take no time, and merge their start and end vertices. */ private void deleteUselessDwells(Graph graph) { int nDwells = potentiallyUselessDwells.size(); int nDeleted = 0; for (PatternDwell dwell : potentiallyUselessDwells) { // useless arrival time arrays are now eliminated in TripTimes constructor (AMB) if (dwell.allDwellsZero()) { dwell.getFromVertex().mergeFrom(graph, dwell.getToVertex()); nDeleted += 1; } } LOG.debug("deleted {} dwell edges / {} candidates, merging arrival and departure vertices.", nDeleted, nDwells); } private void addTripToInterliningMap(Trip trip, List<StopTime> stopTimes, TableTripPattern tripPattern) { String blockId = trip.getBlockId(); BlockIdAndServiceId key = new BlockIdAndServiceId(blockId, trip.getServiceId()); List<InterliningTrip> trips = tripsForBlock.get(key); if (trips == null) { trips = new ArrayList<InterliningTrip>(); tripsForBlock.put(key, trips); } trips.add(new InterliningTrip(trip, stopTimes, tripPattern)); } /** * scan through the given list of stoptimes, interpolating the missing ones. * this is currently done by assuming equidistant stops and constant speed. * while we may not be able to improve the constant speed assumption, * TODO: use route matching (or shape distance etc.) to improve inter-stop distances * * @param stopTimes the stoptimes to be filtered (from a single trip) */ private void interpolateStopTimes(List<StopTime> stopTimes) { int lastStop = stopTimes.size() - 1; int numInterpStops = -1; int departureTime = -1, prevDepartureTime = -1; int interpStep = 0; int i; for (i = 0; i < lastStop; i++) { StopTime st0 = stopTimes.get(i); prevDepartureTime = departureTime; departureTime = st0.getDepartureTime(); /* Interpolate, if necessary, the times of non-timepoint stops */ /* genuine interpolation needed */ if (!(st0.isDepartureTimeSet() && st0.isArrivalTimeSet())) { // figure out how many such stops there are in a row. int j; StopTime st = null; for (j = i + 1; j < lastStop + 1; ++j) { st = stopTimes.get(j); if ((st.isDepartureTimeSet() && st.getDepartureTime() != departureTime) || (st.isArrivalTimeSet() && st.getArrivalTime() != departureTime)) { break; } } if (j == lastStop + 1) { throw new RuntimeException( "Could not interpolate arrival/departure time on stop " + i + " (missing final stop time) on trip " + st0.getTrip()); } numInterpStops = j - i; int arrivalTime; if (st.isArrivalTimeSet()) { arrivalTime = st.getArrivalTime(); } else { arrivalTime = st.getDepartureTime(); } interpStep = (arrivalTime - prevDepartureTime) / (numInterpStops + 1); if (interpStep < 0) { throw new RuntimeException( "trip goes backwards for some reason"); } for (j = i; j < i + numInterpStops; ++j) { //System.out.println("interpolating " + j + " between " + prevDepartureTime + " and " + arrivalTime); departureTime = prevDepartureTime + interpStep * (j - i + 1); st = stopTimes.get(j); if (st.isArrivalTimeSet()) { departureTime = st.getArrivalTime(); } else { st.setArrivalTime(departureTime); } if (!st.isDepartureTimeSet()) { st.setDepartureTime(departureTime); } } i = j - 1; } } } /** * The first time a particular ScheduledStopPattern (stops+pickups+serviceId combination) * is encountered, an empty tripPattern object is created to hold the schedule information. This * method also creates the corresponding PatternStop vertices and PatternBoard/Hop/Alight edges. * StopTimes are passed in instead of Stops only because they are needed for shape distances. * The TripPattern returned is empty; trips should be added to the TripPattern later. */ private T2<TableTripPattern, List<PatternHop>> makePatternVerticesAndEdges(Graph graph, Trip trip, ScheduledStopPattern stopPattern, List<StopTime> stopTimes) { TableTripPattern tripPattern = new TableTripPattern(trip, stopPattern, getServiceId(trip)); // These indexes may be used to make an array-based TimetableResolver if the current // hashmap-based implementation turns out to be insufficient. // Otherwise, they can be replaced with a simple list of tripPatterns, so that their // scheduled timetables can be indexed and compacted once all trips are added. getTripPatternIndex(tripPattern); TraverseMode mode = GtfsLibrary.getTraverseMode(trip.getRoute()); ArrayList<PatternHop> hops = new ArrayList<PatternHop>(); // create journey vertices PatternArriveVertex psv0arrive, psv1arrive = null; PatternDepartVertex psv0depart; for (int hopIndex = 0; hopIndex < stopTimes.size() - 1; hopIndex++) { StopTime st0 = stopTimes.get(hopIndex); Stop s0 = st0.getStop(); StopTime st1 = stopTimes.get(hopIndex + 1); Stop s1 = st1.getStop(); psv0depart = new PatternDepartVertex(graph, tripPattern, st0); context.patternDepartNodes.put(new T2<Stop, Trip>(s0, trip), psv0depart); if (hopIndex != 0) { psv0arrive = psv1arrive; PatternDwell dwell = new PatternDwell(psv0arrive, psv0depart, hopIndex, tripPattern); if (st0.getArrivalTime() == st0.getDepartureTime()) potentiallyUselessDwells.add(dwell); // TODO: verify against old code } psv1arrive = new PatternArriveVertex(graph, tripPattern, st1); context.patternArriveNodes.put(new T2<Stop, Trip>(st1.getStop(), trip), psv1arrive); PatternHop hop = new PatternHop(psv0depart, psv1arrive, s0, s1, hopIndex); hops.add(hop); TransitStopDepart stopDepart = context.stopDepartNodes.get(s0); if (stopDepart == null) { s0 = _dao.getStopForId(new AgencyAndId(s0.getId().getAgencyId(), s0.getParentStation())); stopDepart = context.stopDepartNodes.get(s0); if (stopDepart == null) { LOG.warn(graph.addBuilderAnnotation(new StopAtEntrance(st0, false))); continue; } else { LOG.warn(graph.addBuilderAnnotation(new StopAtEntrance(st0, true))); } } TransitStopArrive stopArrive = context.stopArriveNodes.get(s1); if (stopArrive == null) { s1 = _dao.getStopForId(new AgencyAndId(s1.getId().getAgencyId(), s1.getParentStation())); stopArrive = context.stopArriveNodes.get(s1); if (stopArrive == null) { LOG.warn(graph.addBuilderAnnotation(new StopAtEntrance(st1, false))); continue; } else { LOG.warn(graph.addBuilderAnnotation(new StopAtEntrance(st1, true))); } } stopArrive.getStopVertex().addMode(mode); new TransitBoardAlight(stopDepart, psv0depart, hopIndex, mode); new TransitBoardAlight(psv1arrive, stopArrive, hopIndex, mode); } return new T2<TableTripPattern, List<PatternHop>>(tripPattern, hops); } // originally in makeTripPattern method (duplicate trip adding code and flags code): // tripPattern.setTripFlags(0, // ((trip.getWheelchairAccessible() == 1) ? TripPattern.FLAG_WHEELCHAIR_ACCESSIBLE : 0) // // | // // (((trip.getRoute().getBikesAllowed() == 2 && trip.getTripBikesAllowed() != 1) // || trip.getTripBikesAllowed() == 2) ? TripPattern.FLAG_BIKES_ALLOWED : 0)); // // tripPattern.addHop(i, 0, departureTime, runningTime, arrivalTime, dwellTime, // st0.getStopHeadsign(), trip); // StopTime st1 = null; // for (int i = 0; i < lastStop; i++) { // StopTime st0 = stopTimes.get(i); // Stop s0 = st0.getStop(); // st1 = stopTimes.get(i + 1); // Stop s1 = st1.getStop(); // // int arrivalTime = st1.getArrivalTime(); // int departureTime = st0.getDepartureTime(); // // int dwellTime = st0.getDepartureTime() - st0.getArrivalTime(); // int runningTime = arrivalTime - departureTime ; // // if (runningTime < 0) { // LOG.warn(GraphBuilderAnnotation.register(graph, // Variety.NEGATIVE_HOP_TIME, st0, st1)); // //back out hops and give up // for (Edge e: createdEdges) { // e.getFromVertex().removeOutgoing(e); // e.getToVertex().removeIncoming(e); // } // for (Vertex v : createdVertices) { // graph.removeVertexAndEdges(v); // } // return null; // } // // } // // } private int getServiceId(Trip trip) { AgencyAndId gtfsId = trip.getServiceId(); Integer id = context.serviceIds.get(gtfsId); if (id == null) { id = context.serviceIds.size(); context.serviceIds.put(gtfsId, id); } return id; } private int getTripPatternIndex(TableTripPattern pattern) { // we could probably get away with just a set of tripPatterns since we are not storing // indexes in the patterns themselves. Integer id = context.tripPatternIds.get(pattern); if (id == null) { id = context.serviceIds.size(); context.tripPatternIds.put(pattern, id); } return id; } private void clearCachedData() { LOG.debug("shapes=" + _geometriesByShapeId.size()); LOG.debug("segments=" + _geometriesByShapeSegmentKey.size()); _geometriesByShapeId.clear(); _distancesByShapeId.clear(); _geometriesByShapeSegmentKey.clear(); potentiallyUselessDwells.clear(); } private void loadTransfers(Graph graph) { Collection<Transfer> transfers = _dao.getAllTransfers(); TransferTable transferTable = graph.getTransferTable(); for (Transfer t : transfers) { Stop fromStop = t.getFromStop(); Stop toStop = t.getToStop(); Route fromRoute = t.getFromRoute(); Route toRoute = t.getToRoute(); Trip fromTrip = t.getFromTrip(); Trip toTrip = t.getToTrip(); Vertex fromVertex = context.stopArriveNodes.get(fromStop); Vertex toVertex = context.stopDepartNodes.get(toStop); switch (t.getTransferType()) { case 1: // timed (synchronized) transfer // Handle with edges that bypass the street network. // from and to vertex here are stop_arrive and stop_depart vertices // only add edge when it doesn't exist already boolean hasTimedTransferEdge = false; for (Edge outgoingEdge : fromVertex.getOutgoing()) { if (outgoingEdge instanceof TimedTransferEdge) { if (outgoingEdge.getToVertex() == toVertex) { hasTimedTransferEdge = true; break; } } } if (!hasTimedTransferEdge) { new TimedTransferEdge(fromVertex, toVertex); } // add to transfer table to handle specificity transferTable.addTransferTime(fromStop, toStop, fromRoute, toRoute, fromTrip, toTrip, StopTransfer.TIMED_TRANSFER); break; case 2: // min transfer time transferTable.addTransferTime(fromStop, toStop, fromRoute, toRoute, fromTrip, toTrip, t.getMinTransferTime()); break; case 3: // forbidden transfer transferTable.addTransferTime(fromStop, toStop, fromRoute, toRoute, fromTrip, toTrip, StopTransfer.FORBIDDEN_TRANSFER); break; case 0: default: // preferred transfer transferTable.addTransferTime(fromStop, toStop, fromRoute, toRoute, fromTrip, toTrip, StopTransfer.PREFERRED_TRANSFER); break; } } } private LineString getHopGeometryViaShapeDistTraveled(Graph graph, AgencyAndId shapeId, StopTime st0, StopTime st1, Vertex startJourney, Vertex endJourney) { double startDistance = st0.getShapeDistTraveled(); double endDistance = st1.getShapeDistTraveled(); ShapeSegmentKey key = new ShapeSegmentKey(shapeId, startDistance, endDistance); LineString geometry = _geometriesByShapeSegmentKey.get(key); if (geometry != null) return geometry; double[] distances = getDistanceForShapeId(shapeId); if (distances == null) { LOG.warn(graph.addBuilderAnnotation(new BogusShapeGeometry(shapeId))); return null; } else { LinearLocation startIndex = getSegmentFraction(distances, startDistance); LinearLocation endIndex = getSegmentFraction(distances, endDistance); if (equals(startIndex, endIndex)) { //bogus shape_dist_traveled graph.addBuilderAnnotation(new BogusShapeDistanceTraveled(st1)); return createSimpleGeometry(st0.getStop(), st1.getStop()); } LineString line = getLineStringForShapeId(shapeId); LocationIndexedLine lol = new LocationIndexedLine(line); geometry = getSegmentGeometry(graph, shapeId, lol, startIndex, endIndex, startDistance, endDistance, st0, st1); return geometry; } } private static boolean equals(LinearLocation startIndex, LinearLocation endIndex) { return startIndex.getSegmentIndex() == endIndex.getSegmentIndex() && startIndex.getSegmentFraction() == endIndex.getSegmentFraction() && startIndex.getComponentIndex() == endIndex.getComponentIndex(); } /** create a 2-point linestring (a straight line segment) between the two stops */ private LineString createSimpleGeometry(Stop s0, Stop s1) { Coordinate[] coordinates = new Coordinate[] { new Coordinate(s0.getLon(), s0.getLat()), new Coordinate(s1.getLon(), s1.getLat()) }; CoordinateSequence sequence = new PackedCoordinateSequence.Double(coordinates, 2); return _geometryFactory.createLineString(sequence); } private boolean isValid(Geometry geometry, Stop s0, Stop s1) { Coordinate[] coordinates = geometry.getCoordinates(); if (coordinates.length < 2) { return false; } if (geometry.getLength() == 0) { return false; } for (Coordinate coordinate : coordinates) { if (Double.isNaN(coordinate.x) || Double.isNaN(coordinate.y)) { return false; } } Coordinate geometryStartCoord = coordinates[0]; Coordinate geometryEndCoord = coordinates[coordinates.length - 1]; Coordinate startCoord = new Coordinate(s0.getLon(), s0.getLat()); Coordinate endCoord = new Coordinate(s1.getLon(), s1.getLat()); if (distanceLibrary.fastDistance(startCoord, geometryStartCoord) > maxStopToShapeSnapDistance) { return false; } else if (distanceLibrary.fastDistance(endCoord, geometryEndCoord) > maxStopToShapeSnapDistance) { return false; } return true; } private LineString getSegmentGeometry(Graph graph, AgencyAndId shapeId, LocationIndexedLine locationIndexedLine, LinearLocation startIndex, LinearLocation endIndex, double startDistance, double endDistance, StopTime st0, StopTime st1) { ShapeSegmentKey key = new ShapeSegmentKey(shapeId, startDistance, endDistance); LineString geometry = _geometriesByShapeSegmentKey.get(key); if (geometry == null) { geometry = (LineString) locationIndexedLine.extractLine(startIndex, endIndex); // Pack the resulting line string CoordinateSequence sequence = new PackedCoordinateSequence.Double(geometry .getCoordinates(), 2); geometry = _geometryFactory.createLineString(sequence); if (!isValid(geometry, st0.getStop(), st1.getStop())) { LOG.warn(graph.addBuilderAnnotation(new BogusShapeGeometryCaught(shapeId, st0, st1))); //fall back to trivial geometry geometry = createSimpleGeometry(st0.getStop(), st1.getStop()); } _geometriesByShapeSegmentKey.put(key, (LineString) geometry); } return geometry; } /* * If a shape appears in more than one feed, the shape points will be loaded several * times, and there will be duplicates in the DAO. Filter out duplicates and repeated * coordinates because 1) they are unnecessary, and 2) they define 0-length line segments * which cause JTS location indexed line to return a segment location of NaN, * which we do not want. */ private List<ShapePoint> getUniqueShapePointsForShapeId(AgencyAndId shapeId) { List<ShapePoint> points = _dao.getShapePointsForShapeId(shapeId); ArrayList<ShapePoint> filtered = new ArrayList<ShapePoint>(points.size()); ShapePoint last = null; for (ShapePoint sp : points) { if (last == null || last.getSequence() != sp.getSequence()) { if (last != null && last.getLat() == sp.getLat() && last.getLon() == sp.getLon()) { LOG.trace("pair of identical shape points (skipping): {} {}", last, sp); } else { filtered.add(sp); } } last = sp; } if (filtered.size() != points.size()) { filtered.trimToSize(); return filtered; } else { return points; } } private LineString getLineStringForShapeId(AgencyAndId shapeId) { LineString geometry = _geometriesByShapeId.get(shapeId); if (geometry != null) return geometry; List<ShapePoint> points = getUniqueShapePointsForShapeId(shapeId); if (points.size() < 2) { return null; } Coordinate[] coordinates = new Coordinate[points.size()]; double[] distances = new double[points.size()]; boolean hasAllDistances = true; int i = 0; for (ShapePoint point : points) { coordinates[i] = new Coordinate(point.getLon(), point.getLat()); distances[i] = point.getDistTraveled(); if (!point.isDistTraveledSet()) hasAllDistances = false; i++; } /** * If we don't have distances here, we can't calculate them ourselves because we can't * assume the units will match */ if (!hasAllDistances) { distances = null; } CoordinateSequence sequence = new PackedCoordinateSequence.Double(coordinates, 2); geometry = _geometryFactory.createLineString(sequence); _geometriesByShapeId.put(shapeId, geometry); _distancesByShapeId.put(shapeId, distances); return geometry; } private double[] getDistanceForShapeId(AgencyAndId shapeId) { getLineStringForShapeId(shapeId); return _distancesByShapeId.get(shapeId); } private LinearLocation getSegmentFraction(double[] distances, double distance) { int index = Arrays.binarySearch(distances, distance); if (index < 0) index = -(index + 1); if (index == 0) return new LinearLocation(0, 0.0); if (index == distances.length) return new LinearLocation(distances.length, 0.0); double prevDistance = distances[index - 1]; if (prevDistance == distances[index]) { return new LinearLocation(index - 1, 1.0); } double indexPart = (distance - distances[index - 1]) / (distances[index] - prevDistance); return new LinearLocation(index - 1, indexPart); } /** Filter out (erroneous) series of stop times that refer to the same stop */ private List<StopTime> getNonduplicateStopTimesForTrip(Trip trip) { List<StopTime> unfiltered = _dao.getStopTimesForTrip(trip); ArrayList<StopTime> filtered = new ArrayList<StopTime>(unfiltered.size()); for (StopTime st : unfiltered) { if (filtered.size() > 0) { StopTime lastStopTime = filtered.get(filtered.size() - 1); if (lastStopTime.getStop().equals(st.getStop())) { lastStopTime.setDepartureTime(st.getDepartureTime()); } else { filtered.add(st); } } else { filtered.add(st); } } if (filtered.size() == unfiltered.size()) { return unfiltered; } return filtered; } public void setFareServiceFactory(FareServiceFactory fareServiceFactory) { this.fareServiceFactory = fareServiceFactory; } /** * 1. Create edges between stops and their parent stations. * 2. Create transfer edges between stops which are listed in transfers.txt. * * This is not usually useful, but it's nice for the NYC subway system, where * it's important to provide in-station transfers for fare computation. * * NOTE: this method is only called when transfersTxtDefinesStationPaths is set to * True for a given GFTS feed. */ public void createStationTransfers(Graph graph) { /* 1. Connect stops to their parent stations. */ for (Stop stop : _dao.getAllStops()) { String parentStation = stop.getParentStation(); if (parentStation != null) { Vertex stopVertex = context.stopNodes.get(stop); String agencyId = stop.getId().getAgencyId(); AgencyAndId parentStationId = new AgencyAndId(agencyId, parentStation); Stop parentStop = _dao.getStopForId(parentStationId); Vertex parentStopVertex = context.stopNodes.get(parentStop); new FreeEdge(parentStopVertex, stopVertex); new FreeEdge(stopVertex, parentStopVertex); // Stops with location_type=2 (entrances as defined in the pathways.txt // proposal) have no arrive/depart vertices, hence the null checks. Vertex stopArriveVertex = context.stopArriveNodes.get(stop); Vertex parentStopArriveVertex = context.stopArriveNodes.get(parentStop); if (stopArriveVertex != null && parentStopArriveVertex != null) { new FreeEdge(parentStopArriveVertex, stopArriveVertex); new FreeEdge(stopArriveVertex, parentStopArriveVertex); } Vertex stopDepartVertex = context.stopDepartNodes.get(stop); Vertex parentStopDepartVertex = context.stopDepartNodes.get(parentStop); if (stopDepartVertex != null && parentStopDepartVertex != null) { new FreeEdge(parentStopDepartVertex, stopDepartVertex); new FreeEdge(stopDepartVertex, parentStopDepartVertex); } // TODO: provide a cost for these edges when stations and // stops have different locations } } /* 2. Create transfer edges based on transfers.txt. */ for (Transfer transfer : _dao.getAllTransfers()) { int type = transfer.getTransferType(); if (type == 3) // type 3 = transfer not possible continue; if (transfer.getFromStop().equals(transfer.getToStop())) { continue; } Vertex fromv = context.stopArriveNodes.get(transfer.getFromStop()); Vertex tov = context.stopDepartNodes.get(transfer.getToStop()); double distance = distanceLibrary.distance(fromv.getCoordinate(), tov.getCoordinate()); int time; if (transfer.getTransferType() == 2) { time = transfer.getMinTransferTime(); } else { time = (int) distance; // fixme: handle timed transfers } TransferEdge transferEdge = new TransferEdge(fromv, tov, distance, time); CoordinateSequence sequence = new PackedCoordinateSequence.Double(new Coordinate[] { fromv.getCoordinate(), tov.getCoordinate() }, 2); LineString geometry = _geometryFactory.createLineString(sequence); transferEdge.setGeometry(geometry); } } public int getDefaultStreetToStopTime() { return defaultStreetToStopTime; } public void setDefaultStreetToStopTime(int defaultStreetToStopTime) { this.defaultStreetToStopTime = defaultStreetToStopTime; } /** * You might not want to delete dwell edges when using realtime updates, because new dwells * might be introduced via trip updates. */ public void setDeleteUselessDwells(boolean delete) { this._deleteUselessDwells = delete; } public void setStopContext(GtfsStopContext context) { this.context = context; } public double getMaxStopToShapeSnapDistance() { return maxStopToShapeSnapDistance; } public void setMaxStopToShapeSnapDistance(double maxStopToShapeSnapDistance) { this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance; } }