org.opentripplanner.graph_builder.module.geometry.GeometryAndBlockProcessor Maven / Gradle / Ivy
Show all versions of otp Show documentation
package org.opentripplanner.graph_builder.module.geometry;
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 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;
import java.util.concurrent.ConcurrentHashMap;
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.ext.flex.trip.FlexTrip;
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.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.fares.FareService;
import org.opentripplanner.routing.fares.FareServiceFactory;
import org.opentripplanner.routing.fares.impl.DefaultFareServiceFactory;
import org.opentripplanner.routing.graph.Graph;
import org.opentripplanner.routing.trippattern.TripTimes;
import org.opentripplanner.util.ProgressTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 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.
*
*
* THREAD SAFETY
* The computation runs in parallel so be careful about threadsafety when modifying the logic here.
*/
public class GeometryAndBlockProcessor {
private static final Logger LOG = LoggerFactory.getLogger(GeometryAndBlockProcessor.class);
private DataImportIssueStore issueStore;
private static GeometryFactory geometryFactory = GeometryUtils.getGeometryFactory();
private OtpTransitService transitService;
// this is threadsafe implementation
private Map geometriesByShapeSegmentKey = new ConcurrentHashMap<>();
// this is threadsafe implementation
private Map geometriesByShapeId = new ConcurrentHashMap<>();
// this is threadsafe implementation
private Map distancesByShapeId = new ConcurrentHashMap<>();
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.
*
* THREAD SAFETY
* The geometries for the trip patterns are computed in parallel. The collections needed for
* this are concurrent implementations and therefore threadsafe but the issue store, the graph,
* the OtpTransitService and others are not.
*
*/
@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 = new ConcurrentHashMap<>();
Collection tripPatterns = transitService.getTripPatterns();
/* 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());
tripPatterns.parallelStream().forEach(tripPattern -> {
tripPattern.scheduledTripsAsStream().forEach(trip -> {
// 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.getScheduledTimetable().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 = new HashMap<>();
/* 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.getScheduledTimetable();
/* TODO: Block semantics seem undefined for frequency trips, so skip them? */
for (TripTimes tripTimes : timetable.getTripTimes()) {
Trip trip = tripTimes.getTrip();
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.getTrip().getId());
continue SERVICE_BLOCK;
}
TripPattern prevPattern = patternForTripTimes.get(prev);
TripPattern currPattern = patternForTripTimes.get(curr);
var fromStop = prevPattern.lastStop();
var toStop = currPattern.firstStop();
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.getTrip(), curr.getTrip()));
}
}
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) {
var isFlexTrip = FlexTrip.containsFlexStops(stopTimes);
// 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 || isFlexTrip) {
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++;
}
CoordinateSequence sequence = new PackedCoordinateSequence.Double(coordinates, 2);
geometry = geometryFactory.createLineString(sequence);
geometriesByShapeId.put(shapeId, geometry);
// If we don't have distances here, we can't calculate them ourselves because we can't
// assume the units will match
if(hasAllDistances) {
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;
}
}