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.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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy