Package org.opentripplanner.routing.edgetype.factory

Source Code of org.opentripplanner.routing.edgetype.factory.IndexedLineSegment

/* 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.P2;
import org.opentripplanner.graph_builder.annotation.BogusShapeDistanceTraveled;
import org.opentripplanner.graph_builder.annotation.BogusShapeGeometry;
import org.opentripplanner.graph_builder.annotation.BogusShapeGeometryCaught;
import org.opentripplanner.graph_builder.annotation.HopSpeedFast;
import org.opentripplanner.graph_builder.annotation.HopSpeedSlow;
import org.opentripplanner.graph_builder.annotation.HopZeroTime;
import org.opentripplanner.graph_builder.annotation.NegativeDwellTime;
import org.opentripplanner.graph_builder.annotation.NegativeHopTime;
import org.opentripplanner.graph_builder.annotation.NonStationParentStation;
import org.opentripplanner.graph_builder.annotation.RepeatedStops;
import org.opentripplanner.graph_builder.annotation.TripDegenerate;
import org.opentripplanner.graph_builder.annotation.TripUndefinedService;
import org.opentripplanner.gtfs.GtfsContext;
import org.opentripplanner.model.StopPattern;
import org.opentripplanner.routing.core.StopTransfer;
import org.opentripplanner.routing.core.TransferTable;
import org.opentripplanner.routing.edgetype.FreeEdge;
import org.opentripplanner.routing.edgetype.PathwayEdge;
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.StationStopEdge;
import org.opentripplanner.routing.edgetype.TimedTransferEdge;
import org.opentripplanner.routing.edgetype.Timetable;
import org.opentripplanner.routing.edgetype.TransferEdge;
import org.opentripplanner.routing.edgetype.TripPattern;
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.trippattern.FrequencyEntry;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.routing.vertextype.TransitStation;
import org.opentripplanner.routing.vertextype.TransitStationStop;
import org.opentripplanner.routing.vertextype.TransitStop;
import org.opentripplanner.routing.vertextype.TransitStopArrive;
import org.opentripplanner.routing.vertextype.TransitStopDepart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.beust.jcommander.internal.Maps;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Multimap;
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;

// Filtering out (removing) stoptimes from a trip forces us to either have two copies of that list,
// or do all the steps within one loop over trips. It would be clearer if there were multiple loops over the trips.

/** A wrapper class for Trips that allows them to be sorted. */
class InterliningTrip  implements Comparable<InterliningTrip> {
    public Trip trip;
    public StopTime firstStopTime;
    public StopTime lastStopTime;
    TripPattern tripPattern;

    InterliningTrip(Trip trip, List<StopTime> stopTimes, TripPattern 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;
    }
   
}

/**
* This compound key object is used when grouping interlining trips together by (serviceId, blockId).
*/
class BlockIdAndServiceId {
    public String blockId;
    public AgencyAndId serviceId;

    BlockIdAndServiceId(Trip trip) {
        this.blockId = trip.getBlockId();
        this.serviceId = trip.getServiceId();
    }
   
    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();
    }
}

/* TODO Move this stuff into the geometry library */
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 final int SECONDS_IN_HOUR = 60 * 60; // rename to seconds in hour

    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 FareServiceFactory fareServiceFactory;

    private Map<StopPattern, TripPattern> tripPatterns = Maps.newHashMap();

    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();
    }
   
    public GTFSPatternHopFactory() {
        this._dao = null;
        this._calendarService = null;
    }

    /** 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);
       
        // TODO: Why are we loading stops? The Javadoc above says this method assumes stops are aleady loaded.
        loadStops(graph);
        loadPathways(graph);
        loadAgencies(graph);
        // TODO: Why is there cached "data", and why are we clearing it? Due to a general lack of comments, I have no idea.
        // Perhaps it is to allow name collisions with previously loaded feeds.
        clearCachedData();

        /* Assign 0-based numeric codes to all GTFS service IDs. */
        for (AgencyAndId serviceId : _dao.getAllServiceIds()) {
            // TODO: FIX Service code collision for multiple feeds.
            graph.serviceCodes.put(serviceId, graph.serviceCodes.size());
        }
       
        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.
         * These trips will be ignored for the purposes of non-frequency routing, and
         * all the frequency entries referencing the same trip can be added at once to the same
         * Timetable/TripPattern.
         */
        ListMultimap<Trip, Frequency> frequenciesForTrip = ArrayListMultimap.create();       
        for(Frequency freq : _dao.getAllFrequencies()) {
            frequenciesForTrip.put(freq.getTrip(), freq);
        }
       
        /* Then loop over all trips, handling each one as a frequency-based or scheduled trip. */
        int freqCount = 0;
        int nonFreqCount = 0;
       
        /* The hops don't actually exist when we build their geometries, but we have to build their geometries
         * below, before we throw away the modified stopTimes, saving only the tripTimes (which don't have enough
         * information to build a geometry). So we keep them here.
         *
         *  A trip pattern actually does not have a single geometry, but one per hop, so we store an array.
         */
        Map<TripPattern, LineString[]> geometriesByTripPattern = Maps.newHashMap();
       
        TRIP : for (Trip trip : trips) {
            if (++tripCount % 100000 == 0) {
                LOG.debug("loading trips {}/{}", tripCount, trips.size());
            }

            // TODO: move to a validator module
            if ( ! _calendarService.getServiceIds().contains(trip.getServiceId())) {
                LOG.warn(graph.addBuilderAnnotation(new TripUndefinedService(trip)));
            }

            /* Fetch the stop times for this trip. Copy the list since it's immutable. */
            List<StopTime> stopTimes = new ArrayList<StopTime>(_dao.getStopTimesForTrip(trip));

            /* GTFS stop times frequently contain duplicate, missing, or incorrect entries. Repair them. */
            if (removeRepeatedStops(stopTimes)) {
                LOG.warn(graph.addBuilderAnnotation(new RepeatedStops(trip)));
            }
            filterStopTimes(stopTimes, graph);
            interpolateStopTimes(stopTimes);  
           
            /* If after filtering this trip does not contain at least 2 stoptimes, it does not serve any purpose. */
            if (stopTimes.size() < 2) {
                LOG.warn(graph.addBuilderAnnotation(new TripDegenerate(trip)));
                continue TRIP;
            }

            /* Get the existing TripPattern for this filtered StopPattern, or create one. */
            StopPattern stopPattern = new StopPattern(stopTimes);
            TripPattern tripPattern = tripPatterns.get(stopPattern);
            if (tripPattern == null) {
                tripPattern = new TripPattern(trip.getRoute(), stopPattern);
                tripPatterns.put(stopPattern, tripPattern);
            }

            /* Create a TripTimes object for this list of stoptimes, which form one trip. */
            TripTimes tripTimes = new TripTimes(trip, stopTimes, graph.deduplicator);

            /* If this trip is referenced by one or more lines in frequencies.txt, wrap it in a FrequencyEntry. */
            List<Frequency> frequencies = frequenciesForTrip.get(trip);
            if (frequencies != null && !(frequencies.isEmpty())) {
                for (Frequency freq : frequencies) {
                    tripPattern.add(new FrequencyEntry(freq, tripTimes));
                    freqCount++;
                }
                // TODO replace: createGeometry(graph, trip, stopTimes, hops);
            }

            /* This trip was not frequency-based. Add the TripTimes directly to the TripPattern's scheduled timetable. */
            else {
                tripPattern.add(tripTimes);
                nonFreqCount++;
            }
           
            // create geometries if they aren't already created
            // note that this is not only done on new trip patterns, because it is possible that
            // there would be a trip pattern with no geometry yet because it failed some of these tests
            if (!geometriesByTripPattern.containsKey(tripPattern) &&
                    trip.getShapeId() != null && trip.getShapeId().getId() != null &&
                    !trip.getShapeId().getId().equals("")) {
                // save the geometry to later be applied to the hops
                geometriesByTripPattern.put(tripPattern,  createGeometry(graph, trip, stopTimes));
            }


        } // end foreach TRIP
        LOG.info("Added {} frequency-based and {} single-trip timetable entries.", freqCount, nonFreqCount);

        /* Generate unique human-readable names for all the TableTripPatterns. */
        TripPattern.generateUniqueNames(tripPatterns.values());

        /* Generate unique short IDs for all the TableTripPatterns. */
        TripPattern.generateUniqueIds(tripPatterns.values());

        /* Loop over all new TripPatterns, setting the service codes. */
        for (TripPattern tripPattern : tripPatterns.values()) {
            tripPattern.makePatternVerticesAndEdges(graph, context);
           
            // add the geometries
            LineString[] geom = geometriesByTripPattern.get(tripPattern);
            if (geom != null) {
                for (int i = 0; i < tripPattern.hopEdges.length; i++) {
                    tripPattern.hopEdges[i].setGeometry(geom[i]);
                }
            }
           
            tripPattern.setServiceCodes(graph.serviceCodes); // TODO this could be more elegant
        }

        /* Identify interlined trips and create the necessary edges. */
        interline(tripPatterns.values());

        /* Interpret the transfers explicitly defined in transfers.txt. */
        loadTransfers(graph);

        /* Is this the wrong place to do this? It should be done on all feeds at once, or at deserialization. */
        // it is already done at deserialization, but standalone mode allows using graphs without serializing them.
        for (TripPattern tableTripPattern : tripPatterns.values()) {
            tableTripPattern.scheduledTimetable.finish();
        }
       
        clearCachedData(); // eh?
        graph.putService(FareService.class, fareServiceFactory.makeFareService());
        graph.putService(OnBoardDepartService.class, new OnBoardDepartServiceImpl());
    }

    /**
     * Identify interlined trips (where a physical vehicle continues on to another logical trip)
     * and update the TripPatterns accordingly. This must be called after all the pattern edges and vertices
     * are already created, because it creates interline dwell edges between existing pattern arrive/depart vertices.
     */
    private void interline (Collection<TripPattern> tripPatterns) {

        /* Record which Pattern each interlined TripTimes belongs to. */
        Map<TripTimes, TripPattern> patternForTripTimes = Maps.newHashMap();

        /* TripTimes grouped by the block ID and service ID of their trips. Must be a ListMultimap to allow sorting. */
        ListMultimap<BlockIdAndServiceId, TripTimes> tripTimesForBlock = ArrayListMultimap.create();

        LOG.info("Finding interlining trips based on block IDs.");
        for (TripPattern pattern : tripPatterns) {
            Timetable timetable = pattern.scheduledTimetable;
            /* TODO: Block semantics seem undefined for frequency trips, so skip them? */
            for (TripTimes tripTimes : timetable.tripTimes) {
                Trip trip = tripTimes.trip;
                if ( ! Strings.isNullOrEmpty(trip.getBlockId())) {
                    tripTimesForBlock.put(new BlockIdAndServiceId(trip), tripTimes);
                    // For space efficiency, only record times that are part of a block.
                    patternForTripTimes.put(tripTimes, pattern);
                }
            }
        }

        /* Associate pairs of TripPatterns with lists of trips that continue from one pattern to the other. */
        Multimap<P2<TripPattern>, P2<Trip>> interlines = ArrayListMultimap.create();

        /*
          Sort trips within each block by first departure time, then iterate over trips in this block and service,
          linking them. Has no effect on single-trip blocks.
         */
        SERVICE_BLOCK :
        for (BlockIdAndServiceId block : tripTimesForBlock.keySet()) {
            List<TripTimes> blockTripTimes = tripTimesForBlock.get(block);
            Collections.sort(blockTripTimes);
            TripTimes prev = null;
            for (TripTimes curr : blockTripTimes) {
                if (prev != null) {
                    if (prev.getDepartureTime(prev.getNumStops() - 1) > curr.getArrivalTime(0)) {
                        LOG.error("Trip times within block {} on service {} are not increasing on service {} after trip {}.",
                                block.blockId, block.serviceId, prev.trip.getId());
                        continue SERVICE_BLOCK;
                    }
                    TripPattern prevPattern = patternForTripTimes.get(prev);
                    TripPattern currPattern = patternForTripTimes.get(curr);
                    interlines.put(new P2<TripPattern>(prevPattern, currPattern), new P2<Trip>(prev.trip, curr.trip));
                }
                prev = curr;
            }
        }

        /*
          Create the PatternInterlineDwell edges linking together TripPatterns.
          All the pattern vertices and edges must already have been created.
         */
        for (P2<TripPattern> patterns : interlines.keySet()) {
            TripPattern prevPattern = patterns.first;
            TripPattern nextPattern = patterns.second;
            // This is a single (uni-directional) edge which may be traversed forward and backward.
            PatternInterlineDwell edge = new PatternInterlineDwell(prevPattern, nextPattern);
            for (P2<Trip> trips : interlines.get(patterns)) {
                edge.add(trips.first, trips.second);
            }
        }
        LOG.info("Done finding interlining trips and creating the corresponding edges.");
    }

    private LineString[] createGeometry(Graph graph, Trip trip, List<StopTime> stopTimes) {
        AgencyAndId shapeId = trip.getShapeId();
       
        // one less geometry than stoptime as these are hops not stops (fencepost problem)
        LineString[] geoms = new LineString[stopTimes.size() - 1];
       
        /* 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 < stopTimes.size() - 1; ++i) {
                st0 = stopTimes.get(i);
                StopTime st1 = stopTimes.get(i + 1);
                geoms[i] = getHopGeometryViaShapeDistTraveled(graph, shapeId, st0, st1);
            }
            return geoms;
        }
        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());
                geoms[i] = geometry;
            }
            return geoms;
        }
        // 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());
                geoms[i] = geometry;
                //this warning is not strictly correct, but will do
                LOG.warn(graph.addBuilderAnnotation(new BogusShapeGeometryCaught(shapeId, st0, st1)));
            }
            return geoms;
        }

        Iterator<LinearLocation> locationIt = locations.iterator();
        LinearLocation endLocation = locationIt.next();
        double distanceSoFar = 0;
        int last = 0;
        for (int i = 0; i < stopTimes.size() - 1; ++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);
            }
            geoms[i] = geometry;
        }
       
        return geoms;
    }

    /**
     * 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;
    }

    /**
     * Scan through the given list, looking for clearly incorrect series of stoptimes and unsetting
     * them. This includes duplicate times (0-time hops), as well as negative, fast or slow hops.
     * Unsetting the arrival/departure time of clearly incorrect stoptimes will cause them to be
     * interpolated in the next step. Annotations are also added to the graph to reveal the problems
     * to the user.
     *
     * @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);

        /* Set departure time if it is missing */
        if (!st0.isDepartureTimeSet() && st0.isArrivalTimeSet()) {
            st0.setDepartureTime(st0.getArrivalTime());
        }

        /* If the feed does not specify any timepoints, we want to mark all times that are present as timepoints. */
        boolean hasTimepoints = false;
        for (StopTime stopTime : stopTimes) {
            if (stopTime.getTimepoint() == 1) {
                hasTimepoints = true;
                break;
            }
        }
        // TODO verify that the first (and last?) stop should always be considered a timepoint.
        if (!hasTimepoints) st0.setTimepoint(1);

        /* Indicates that stop times in this trip are being shifted forward one day. */
        boolean midnightCrossed = false;
       
        for (int i = 1; i < stopTimes.size(); i++) {
            boolean st1bogus = false;
            StopTime st1 = stopTimes.get(i);

            /* If the feed did not specify any timepoints, mark all times that are present as timepoints. */
            if ( !hasTimepoints && (st1.isDepartureTimeSet() || st1.isArrivalTimeSet())) {
                st1.setTimepoint(1);
            }

            if (midnightCrossed) {
                if (st1.isDepartureTimeSet())
                    st1.setDepartureTime(st1.getDepartureTime() + 24 * SECONDS_IN_HOUR);
                if (st1.isArrivalTimeSet())
                    st1.setArrivalTime(st1.getArrivalTime() + 24 * SECONDS_IN_HOUR);
            }
            /* Set departure time if it is missing. */
            // TODO: doc: what if arrival time is missing?
            if (!st1.isDepartureTimeSet() && st1.isArrivalTimeSet()) {
                st1.setDepartureTime(st1.getArrivalTime());
            }
            /* Do not process (skip over) non-timepoint stoptimes, leaving them in place for interpolation. */
            // All non-timepoint stoptimes in a series will have identical arrival and departure values of MISSING_VALUE.
            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 * SECONDS_IN_HOUR && st0.getDepartureTime() < 1 * SECONDS_IN_HOUR) {
                    midnightCrossed = true;
                    st0.setDepartureTime(st0.getDepartureTime() + 24 * SECONDS_IN_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 * SECONDS_IN_HOUR && st1.getArrivalTime() < 1 * SECONDS_IN_HOUR) {
                    st1.setArrivalTime(st1.getArrivalTime() + 24 * SECONDS_IN_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 void loadPathways(Graph graph) {
        for (Pathway pathway : _dao.getAllPathways()) {
            Vertex fromVertex = context.stationStopNodes.get(pathway.getFromStop());
            Vertex toVertex = context.stationStopNodes.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())) {
                LOG.error("Skipping stop {} because we already loaded an identical ID.", stop.getId());
                continue;
            }
            context.stops.add(stop.getId());

            int locationType = stop.getLocationType();

            //add a vertex representing the stop
            if (locationType == 1) {
                context.stationStopNodes.put(stop, new TransitStation(graph, stop));
            } else {
                TransitStop stopVertex = new TransitStop(graph, stop);
                stopVertex.setStreetToStopTime(defaultStreetToStopTime);
                context.stationStopNodes.put(stop, stopVertex);

                if (locationType != 2) {
                    // Add a vertex representing arriving at the stop
                    TransitStopArrive arrive = new TransitStopArrive(graph, stop, stopVertex);
                    // FIXME no need for this context anymore, we just put references to these nodes in the stop vertices themselves.
                    context.stopArriveNodes.put(stop, arrive);
                    stopVertex.arriveVertex = arrive;

                    // Add a vertex representing departing from the stop
                    TransitStopDepart depart = new TransitStopDepart(graph, stop, stopVertex);
                    // FIXME no need for this context anymore, we just put references to these nodes in the stop vertices themselves.
                    context.stopDepartNodes.put(stop, depart);
                    stopVertex.departVertex = depart;

                    // Add edges from arrive to stop and stop to depart
                    new PreAlightEdge(arrive, stopVertex);
                    new PreBoardEdge(stopVertex, depart);
                }
            }
        }
    }

    /**
     * Scan through the given list of stoptimes, interpolating the missing (unset) ones.
     * This is currently done by assuming equidistant stops and constant speed.
     * While we may not be able to improve the constant speed assumption, we can
     * TODO: use route matching (or shape distance etc.) to improve inter-stop distances
     * 
     * @param stopTimes the stoptimes (from a single trip) to be interpolated
     */
    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;
            }
        }
    }

    private void clearCachedData() {
        LOG.debug("shapes=" + _geometriesByShapeId.size());
        LOG.debug("segments=" + _geometriesByShapeSegmentKey.size());
        _geometriesByShapeId.clear();
        _distancesByShapeId.clear();
        _geometriesByShapeSegmentKey.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) {

        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 any series of stop times that refer to the same stop. This is very inefficient in
     * an array-backed list, but we are assuming that this is a rare occurrence. The alternative is
     * to copy every list of stop times during filtering.
     *
     * TODO: OBA GFTS makes the stoptime lists unmodifiable, so this will not work.
     * We need to copy any modified list.
     *
     * @return whether any repeated stops were filtered out.
     */
    private boolean removeRepeatedStops (List<StopTime> stopTimes) {
        boolean filtered = false;
        StopTime prev = null;
        Iterator<StopTime> it = stopTimes.iterator();
        while (it.hasNext()) {
            StopTime st = it.next();
            if (prev != null) {
                if (prev.getStop().equals(st.getStop())) {
                    // OBA gives us unmodifiable lists, but we have copied them.
                    prev.setDepartureTime(st.getDepartureTime());
                    it.remove();
                    filtered = true;
                }
            }
            prev = st;
        }
        return filtered;
    }

    public void setFareServiceFactory(FareServiceFactory fareServiceFactory) {
        this.fareServiceFactory = fareServiceFactory;
    }

    /**
     * Create bidirectional, "free" edges (zero-time, low-cost edges) between stops and their
     * parent stations. This is used to produce implicit transfers between all stops that are
     * part of the same parent station. It was introduced as a workaround to allow in-station
     * transfers for underground/grade-separated transportation systems like the NYC subway (where
     * it's important to provide in-station transfers for fare computation).
     *
     * This step used to be automatically applied whenever transfers.txt was used to create
     * transfers (rathen than or in addition to transfers through the street netowrk),
     * but has been separated out since it is really a separate process.
     */
    public void createParentStationTransfers () {
        for (Stop stop : _dao.getAllStops()) {
            String parentStation = stop.getParentStation();
            if (parentStation != null) {
                Vertex stopVertex = context.stationStopNodes.get(stop);

                String agencyId = stop.getId().getAgencyId();
                AgencyAndId parentStationId = new AgencyAndId(agencyId, parentStation);

                Stop parentStop = _dao.getStopForId(parentStationId);
                Vertex parentStopVertex = context.stationStopNodes.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
            }
        }
    }
   
    /**
     * Links the vertices representing parent stops to their child stops bidirectionally. This is
     * not intended to provide implicit transfers (i.e. child stop to parent station to another
     * child stop) but instead to allow beginning or ending a path (itinerary) at a parent station.
     *
     * Currently this linking is only intended for use in the long distance path service. The
     * pathparsers should ensure that it is effectively ignored in other path services, and even in
     * the long distance path service anywhere but the beginning or end of a path.
     */
    public void linkStopsToParentStations(Graph graph) {
        for (Stop stop : _dao.getAllStops()) {
            String parentStation = stop.getParentStation();
            if (parentStation != null) {
                TransitStop stopVertex = (TransitStop) context.stationStopNodes.get(stop);
                String agencyId = stop.getId().getAgencyId();
                AgencyAndId parentStationId = new AgencyAndId(agencyId, parentStation);
                Stop parentStop = _dao.getStopForId(parentStationId);
                if(context.stationStopNodes.get(parentStop) instanceof TransitStation) {
                    TransitStation parentStopVertex = (TransitStation)
                            context.stationStopNodes.get(parentStop);
                    new StationStopEdge(parentStopVertex, stopVertex);
                    new StationStopEdge(stopVertex, parentStopVertex);
                } else {
                    LOG.warn(graph.addBuilderAnnotation(new NonStationParentStation(stopVertex)));
                }
            }
        }       
    }
   
    /**
     * Create transfer edges between stops which are listed in transfers.txt.
     *
     * NOTE: this method is only called when transfersTxtDefinesStationPaths is set to
     * True for a given GFTS feed.
     */
    public void createTransfersTxtTransfers() {

        /* 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;
            }
            TransitStationStop fromv = context.stationStopNodes.get(transfer.getFromStop());
            TransitStationStop tov = context.stationStopNodes.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;
    }

    public void setStopContext(GtfsStopContext context) {
        this.context = context;
    }


    public double getMaxStopToShapeSnapDistance() {
        return maxStopToShapeSnapDistance;
    }


    public void setMaxStopToShapeSnapDistance(double maxStopToShapeSnapDistance) {
        this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance;
    }

}
TOP

Related Classes of org.opentripplanner.routing.edgetype.factory.IndexedLineSegment

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.