All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor Maven / Gradle / Ivy

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.graph_builder.module.geometry;

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 org.apache.commons.math3.util.FastMath;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.CoordinateSequence;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.linearref.LinearLocation;
import org.locationtech.jts.linearref.LocationIndexedLine;
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.DataImportIssueStore;
import org.opentripplanner.graph_builder.issues.BogusShapeDistanceTraveled;
import org.opentripplanner.graph_builder.issues.BogusShapeGeometry;
import org.opentripplanner.graph_builder.issues.BogusShapeGeometryCaught;
import org.opentripplanner.gtfs.GtfsContext;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.OtpTransitService;
import org.opentripplanner.model.ShapePoint;
import org.opentripplanner.model.Stop;
import org.opentripplanner.model.StopLocation;
import org.opentripplanner.model.StopTime;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.model.Trip;
import org.opentripplanner.model.TripPattern;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.impl.DefaultFareServiceFactory;
import org.opentripplanner.routing.services.FareService;
import org.opentripplanner.routing.services.FareServiceFactory;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.util.ProgressTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Once transit model entities have been loaded into the graph, this post-processes them to extract and prepare
 * geometries. It also does some other postprocessing involving fares and interlined blocks.
 */
public class GeometryAndBlockProcessor {

    private static final Logger LOG = LoggerFactory.getLogger(GeometryAndBlockProcessor.class);

    private DataImportIssueStore issueStore;

    private static GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();

    private OtpTransitService transitService;

    private Map geometriesByShapeSegmentKey = new HashMap();

    private Map geometriesByShapeId = new HashMap();

    private Map distancesByShapeId = new HashMap<>();

    private FareServiceFactory fareServiceFactory;

    private final double maxStopToShapeSnapDistance;

    private final int maxInterlineDistance;

    public GeometryAndBlockProcessor (GtfsContext context) {
        this(context.getTransitService(), null, -1, -1);
    }

    public GeometryAndBlockProcessor (
            // TODO OTP2 - Operate on the builder, not the transit service and move the executon of
            //           - this to where the builder is in context.
            OtpTransitService transitService,
            // TODO OTP2 - This does not belong here - Do geometry and blocks have anything with
            //           - a FareService.
            FareServiceFactory fareServiceFactory,
            double maxStopToShapeSnapDistance,
            int maxInterlineDistance
    ) {
        this.transitService = transitService;
        this.fareServiceFactory = fareServiceFactory != null ? fareServiceFactory : new DefaultFareServiceFactory();
        this.maxStopToShapeSnapDistance = maxStopToShapeSnapDistance > 0 ? maxStopToShapeSnapDistance : 150;
        this.maxInterlineDistance = maxInterlineDistance > 0 ? maxInterlineDistance : 200;
    }

    // TODO OTP2 - Instead of exposing the graph (the entire world) to this class, this class should
    //           - Create a datastructure and return it, then that should be injected into the graph.
    public void run(Graph graph) {
        run(graph, new DataImportIssueStore(false));
    }

    /** Generate the edges. Assumes that there are already vertices in the graph for the stops. */
    @SuppressWarnings("Convert2MethodRef")
    public void run(Graph graph, DataImportIssueStore issueStore) {
        this.issueStore = issueStore;

        fareServiceFactory.processGtfs(transitService);

        /* Assign 0-based numeric codes to all GTFS service IDs. */
        for (FeedScopedId serviceId : transitService.getAllServiceIds()) {
            // TODO: FIX Service code collision for multiple feeds.
            graph.getServiceCodes().put(serviceId, graph.getServiceCodes().size());
        }

        LOG.info("Processing geometries and blocks on graph...");

        // Wwe have to build the hop geometries 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. In the current design, a trip pattern does not have a single geometry, but
        // one per hop, so we store them in an array.
        Map geometriesByTripPattern = Maps.newHashMap();

        Collection tripPatterns = transitService.getTripPatterns();

        /* Generate unique short IDs for all the TableTripPatterns. */
        if (!TripPattern.idsAreUniqueAndNotNull(tripPatterns)) {
            TripPattern.generateUniqueIds(tripPatterns);
        }

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

        /* Loop over all new TripPatterns, creating edges, setting the service codes and geometries, etc. */
        ProgressTracker progress = ProgressTracker.track(
                "Generate TripPattern geometries",
                100,
                tripPatterns.size()
        );
        LOG.info(progress.startMessage());

        for (TripPattern tripPattern : tripPatterns) {
            for (Trip trip : tripPattern.getTrips()) {
                // 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(trip.getShapeId(), transitService.getStopTimesForTrip(trip)));
                }
            }
            //Keep lambda! A method-ref would causes incorrect class and line number to be logged
            progress.step(m -> LOG.info(m));
        }
        LOG.info(progress.completeMessage());

        /* Loop over all new TripPatterns setting the service codes and geometries, etc. */
        for (TripPattern tripPattern : tripPatterns) {
            LineString[] hopGeometries = geometriesByTripPattern.get(tripPattern);
            if (hopGeometries != null) {
                // Make a single unified geometry, and also store the per-hop split geometries.
                tripPattern.setHopGeometries(hopGeometries);
            }
            tripPattern.setServiceCodes(graph.getServiceCodes()); // TODO this could be more elegant

            // Store the tripPattern in the Graph so it will be serialized and usable in routing.
            graph.tripPatternForId.put(tripPattern.getId(), tripPattern);
        }

        /* Identify interlined trips and create the necessary edges. */
        interline(tripPatterns, 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) {
            tableTripPattern.scheduledTimetable.finish();
        }

        graph.putService(FareService.class, fareServiceFactory.makeFareService());
    }

    /**
     * Identify interlined trips (where a physical vehicle continues on to another logical trip)
     * and update the TripPatterns accordingly.
     */
    private void interline(Collection tripPatterns, Graph graph) {

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

        /* TripTimes grouped by the block ID and service ID of their trips. Must be a ListMultimap to allow sorting. */
        ListMultimap 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> 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 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 {} 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);
                    Stop fromStop = prevPattern.getStop(prevPattern.getStops().size() - 1);
                    Stop toStop = currPattern.getStop(0);
                    double teleportationDistance = SphericalDistanceLibrary.fastDistance(
                            fromStop.getLat(),
                            fromStop.getLon(),
                            toStop.getLat(),
                            toStop.getLon()
                    );
                    if (teleportationDistance > maxInterlineDistance) {
                        // FIXME Trimet data contains a lot of these -- in their data, two trips sharing a block ID just
                        // means that they are served by the same vehicle, not that interlining is automatically allowed.
                        // see #1654
                        // LOG.error(graph.addBuilderAnnotation(new InterliningTeleport(prev.trip, block.blockId, (int)teleportationDistance)));
                        // Only skip this particular interline edge; there may be other valid ones in the block.
                    } else {
                        interlines.put(new P2<>(prevPattern, currPattern),
                                new P2<>(prev.trip, curr.trip));
                    }
                }
                prev = curr;
            }
        }

        // Copy all interline relationships into the field holding them in the graph.
        // TODO: verify whether we need to be keeping track of patterns at all here, or could just accumulate trip-trip relationships.
        for (P2 patterns : interlines.keySet()) {
            for (P2 trips : interlines.get(patterns)) {
                graph.interlinedTrips.put(trips.first, trips.second);
            }
        }
        LOG.info("Done finding interlining trips.");
    }

    /**
     * Creates a set of geometries for a single trip, considering the GTFS shapes.txt,
     * The geometry is broken down into one geometry per inter-stop segment ("hop"). We also need a shape for the entire
     * trip and tripPattern, but given the complexity of the existing code for generating hop geometries, we will create
     * the full-trip geometry by simply concatenating the hop geometries.
     *
     * This geometry will in fact be used for an entire set of trips in a trip pattern. Technically one of the trips
     * with exactly the same sequence of stops could follow a different route on the streets, but that's very uncommon.
     */
    private LineString[] createGeometry(FeedScopedId shapeId, List stopTimes) {

        if (hasShapeDist(shapeId, stopTimes)) {
            // this trip has shape_dist in stop_times
            return getHopGeometriesViaShapeDistTravelled(stopTimes, shapeId);
        }

        LineString shapeLineString = getLineStringForShapeId(shapeId);
        if (shapeLineString == 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
            return createStraightLineHopeGeometries(stopTimes, shapeId);
        }

        List locations = getLinearLocations(stopTimes, shapeLineString);
        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.
            return createStraightLineHopeGeometries(stopTimes, shapeId);
        }

        return getGeometriesByShape(stopTimes, shapeId, shapeLineString, locations);
    }

    private boolean hasShapeDist(FeedScopedId shapeId, List stopTimes) {
        StopTime st0 = stopTimes.get(0);
        return st0.isShapeDistTraveledSet() && getDistanceForShapeId(shapeId) != null;
    }

    private LineString[] getGeometriesByShape(
        List stopTimes, FeedScopedId shapeId, LineString shape,
        List locations
    ) {
        LineString[] geoms = new LineString[stopTimes.size() - 1];
        Iterator 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;
    }

    private List getLinearLocations(List stopTimes, LineString shape) {
        // This trip does not have shape_dist in stop_times, but does have an associated shape.
        ArrayList segments = new ArrayList<>();
        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> possibleSegmentsForStop = new ArrayList<>();
        int minSegmentIndex = 0;
        for (int i = 0; i < stopTimes.size() ; ++i) {
            StopLocation stop = stopTimes.get(i).getStop();
            Coordinate coord = stop.getCoordinate().asJtsCoordinate();
            List stopSegments = new ArrayList<>();
            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 && bestSegment != null) {
                //no segments within 150m
                //fall back to nearest segment
                stopSegments.add(bestSegment);
                minSegmentIndex = bestSegment.index;
            } else {
                minSegmentIndex = minSegmentIndexForThisStop;
                stopSegments.sort(new IndexedLineSegmentComparator(coord));
            }

            for (int j = i - 1; j >= 0; j --) {
                for (Iterator it = possibleSegmentsForStop.get(j).iterator(); it.hasNext(); ) {
                    IndexedLineSegment segment = it.next();
                    if (segment.index > maxSegmentIndex) {
                        it.remove();
                    }
                }
            }
            possibleSegmentsForStop.add(stopSegments);
        }

        return getStopLocations(possibleSegmentsForStop, stopTimes, 0, -1);
    }

    private LineString[] createStraightLineHopeGeometries(
        List stopTimes, FeedScopedId shapeId
    ) {
        LineString[] geoms = new LineString[stopTimes.size() - 1];
        StopTime st0;
        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
            issueStore.add(new BogusShapeGeometryCaught(shapeId, st0, st1));
        }
        return geoms;
    }

    private LineString[] getHopGeometriesViaShapeDistTravelled(
        List stopTimes, FeedScopedId shapeId
    ) {
        LineString[] geoms = new LineString[stopTimes.size() - 1];
        StopTime st0;
        for (int i = 0; i < stopTimes.size() - 1; ++i) {
            st0 = stopTimes.get(i);
            StopTime st1 = stopTimes.get(i + 1);
            geoms[i] = getHopGeometryViaShapeDistTraveled(shapeId, st0, st1);
        }
        return geoms;
    }

    /**
     * Find a consistent, increasing list of LinearLocations along a shape for a set of stops.
     * Handles loops routes.
     */
    private List getStopLocations(List> possibleSegmentsForStop,
            List stopTimes, int index, int prevSegmentIndex) {

        if (index == stopTimes.size()) {
            return new LinkedList<>();
        }

        StopTime st = stopTimes.get(index);
        StopLocation stop = st.getStop();
        Coordinate stopCoord = stop.getCoordinate().asJtsCoordinate();

        for (IndexedLineSegment segment : possibleSegmentsForStop.get(index)) {
            if (segment.index < prevSegmentIndex) {
                //can't go backwards along line
                continue;
            }
            List 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 LineString getHopGeometryViaShapeDistTraveled(FeedScopedId 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) {
            issueStore.add(new BogusShapeGeometry(shapeId));
            return null;
        } else {
            LinearLocation startIndex = getSegmentFraction(distances, startDistance);
            LinearLocation endIndex = getSegmentFraction(distances, endDistance);

            if (equals(startIndex, endIndex)) {
                //bogus shape_dist_traveled
                issueStore.add(new BogusShapeDistanceTraveled(st1));
                return createSimpleGeometry(st0.getStop(), st1.getStop());
            }
            LineString line = getLineStringForShapeId(shapeId);
            LocationIndexedLine lol = new LocationIndexedLine(line);

            geometry = getSegmentGeometry(
                    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(StopLocation s0, StopLocation s1) {

        Coordinate[] coordinates = new Coordinate[] {
                s0.getCoordinate().asJtsCoordinate(),
                s1.getCoordinate().asJtsCoordinate()
        };
        CoordinateSequence sequence = new PackedCoordinateSequence.Double(coordinates, 2);

        return geometryFactory.createLineString(sequence);
    }

    private boolean isValid(Geometry geometry, StopLocation s0, StopLocation 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 = s0.getCoordinate().asJtsCoordinate();
        Coordinate endCoord = s1.getCoordinate().asJtsCoordinate();
        if (SphericalDistanceLibrary.fastDistance(startCoord, geometryStartCoord) > maxStopToShapeSnapDistance) {
            return false;
        } else if (SphericalDistanceLibrary.fastDistance(endCoord, geometryEndCoord) > maxStopToShapeSnapDistance) {
            return false;
        }
        return true;
    }

    private LineString getSegmentGeometry(FeedScopedId 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())) {
                issueStore.add(new BogusShapeGeometryCaught(shapeId, st0, st1));
                //fall back to trivial geometry
                geometry = createSimpleGeometry(st0.getStop(), st1.getStop());
            }
            geometriesByShapeSegmentKey.put(key, 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 getUniqueShapePointsForShapeId(FeedScopedId shapeId) {
        List points = transitService.getShapePointsForShapeId(shapeId);
        ArrayList filtered = new ArrayList<>(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 new ArrayList<>(points);
        }
    }

    private LineString getLineStringForShapeId(FeedScopedId shapeId) {

        LineString geometry = geometriesByShapeId.get(shapeId);

        if (geometry != null)
            return geometry;

        List 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(FeedScopedId 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);
    }

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




© 2015 - 2024 Weber Informatics LLC | Privacy Policy