/* * Licensed to GraphHopper GmbH under one or more contributor * license agreements. See the NOTICE file distributed with this work for * additional information regarding copyright ownership. * * GraphHopper GmbH licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.graphhopper.reader.gtfs; import com.conveyal.gtfs.GTFSFeed; import com.conveyal.gtfs.model.Stop; import com.conveyal.gtfs.model.StopTime; import com.google.transit.realtime.GtfsRealtime; import com.graphhopper.*; import com.graphhopper.gtfs.fare.Fares; import com.graphhopper.reader.osm.OSMReader; import com.graphhopper.routing.InstructionsFromEdges; import com.graphhopper.routing.QueryGraph; import com.graphhopper.routing.util.EncodingManager; import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.*; import com.graphhopper.storage.index.LocationIndex; import com.graphhopper.storage.index.LocationIndexTree; import com.graphhopper.storage.index.QueryResult; import com.graphhopper.util.*; import com.graphhopper.util.exceptions.PointNotFoundException; import com.graphhopper.util.shapes.GHPoint; import com.vividsolutions.jts.geom.Coordinate; import com.vividsolutions.jts.geom.Geometry; import com.vividsolutions.jts.geom.GeometryFactory; import org.mapdb.Fun; import java.io.File; import java.io.IOException; import java.time.Duration; import java.time.Instant; import java.time.LocalDateTime; import java.time.format.DateTimeParseException; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import java.util.zip.ZipFile; import static com.graphhopper.reader.gtfs.Label.reverseEdges; import static com.graphhopper.util.Parameters.PT.RANGE_QUERY_END_TIME; public final class GraphHopperGtfs implements GraphHopperAPI { public static class Factory { private final TranslationMap translationMap; private final PtFlagEncoder flagEncoder; private final GraphHopperStorage graphHopperStorage; private final LocationIndex locationIndex; private final GtfsStorage gtfsStorage; private Factory(PtFlagEncoder flagEncoder, TranslationMap translationMap, GraphHopperStorage graphHopperStorage, LocationIndex locationIndex, GtfsStorage gtfsStorage) { this.flagEncoder = flagEncoder; this.translationMap = translationMap; this.graphHopperStorage = graphHopperStorage; this.locationIndex = locationIndex; this.gtfsStorage = gtfsStorage; } public GraphHopperGtfs createWith(GtfsRealtime.FeedMessage realtimeFeed) { return new GraphHopperGtfs(flagEncoder, translationMap, graphHopperStorage, locationIndex, gtfsStorage, RealtimeFeed.fromProtobuf(gtfsStorage, realtimeFeed)); } public GraphHopperGtfs createWithoutRealtimeFeed() { return new GraphHopperGtfs(flagEncoder, translationMap, graphHopperStorage, locationIndex, gtfsStorage, RealtimeFeed.empty()); } } public static Factory createFactory(PtFlagEncoder flagEncoder, TranslationMap translationMap, GraphHopperStorage graphHopperStorage, LocationIndex locationIndex, GtfsStorage gtfsStorage) { return new Factory(flagEncoder, translationMap, graphHopperStorage, locationIndex, gtfsStorage); } private final TranslationMap translationMap; private final PtFlagEncoder flagEncoder; private final GraphHopperStorage graphHopperStorage; private final LocationIndex locationIndex; private final GtfsStorage gtfsStorage; private final RealtimeFeed realtimeFeed; private final GeometryFactory geometryFactory = new GeometryFactory(); private class RequestHandler { private final int maxVisitedNodesForRequest; private final Instant initialTime; private final Instant rangeQueryEndTime; private final boolean arriveBy; private final boolean ignoreTransfers; private final double walkSpeedKmH; private final double maxWalkDistancePerLeg; private final double maxTransferDistancePerLeg; private final PtTravelTimeWeighting weighting; private final GHPoint enter; private final GHPoint exit; private final Translation translation; private final GHResponse response = new GHResponse(); private final QueryGraph queryGraph = new QueryGraph(graphHopperStorage); RequestHandler(GHRequest request) { maxVisitedNodesForRequest = request.getHints().getInt(Parameters.Routing.MAX_VISITED_NODES, Integer.MAX_VALUE); final String departureTimeString = request.getHints().get(Parameters.PT.EARLIEST_DEPARTURE_TIME, ""); try { initialTime = Instant.parse(departureTimeString); } catch (DateTimeParseException e) { throw new IllegalArgumentException(String.format("Illegal value for required parameter %s: [%s]", Parameters.PT.EARLIEST_DEPARTURE_TIME, departureTimeString)); } rangeQueryEndTime = request.getHints().has(Parameters.PT.RANGE_QUERY_END_TIME) ? Instant.parse(request.getHints().get(RANGE_QUERY_END_TIME, "")) : initialTime; arriveBy = request.getHints().getBool(Parameters.PT.ARRIVE_BY, false); ignoreTransfers = request.getHints().getBool(Parameters.PT.IGNORE_TRANSFERS, false); walkSpeedKmH = request.getHints().getDouble(Parameters.PT.WALK_SPEED, 5.0); maxWalkDistancePerLeg = request.getHints().getDouble(Parameters.PT.MAX_WALK_DISTANCE_PER_LEG, Double.MAX_VALUE); maxTransferDistancePerLeg = request.getHints().getDouble(Parameters.PT.MAX_TRANSFER_DISTANCE_PER_LEG, Double.MAX_VALUE); weighting = createPtTravelTimeWeighting(flagEncoder, arriveBy, walkSpeedKmH); translation = translationMap.getWithFallBack(request.getLocale()); if (request.getPoints().size() != 2) { throw new IllegalArgumentException("Exactly 2 points have to be specified, but was:" + request.getPoints().size()); } enter = request.getPoints().get(0); exit = request.getPoints().get(1); } GHResponse route() { StopWatch stopWatch = new StopWatch().start(); QueryResult source = findClosest(enter, 0); QueryResult dest = findClosest(exit, 1); queryGraph.lookup(Arrays.asList(source, dest)); // modifies queryGraph, source and dest! PointList startAndEndpoint = pointListFrom(Arrays.asList(source, dest)); response.addDebugInfo("idLookup:" + stopWatch.stop().getSeconds() + "s"); int startNode; int destNode; if (arriveBy) { startNode = dest.getClosestNode(); destNode = source.getClosestNode(); } else { startNode = source.getClosestNode(); destNode = dest.getClosestNode(); } Set<Label> solutions = findPaths(startNode, destNode); parseSolutionsAndAddToResponse(solutions, startAndEndpoint); return response; } private QueryResult findClosest(GHPoint point, int indexForErrorMessage) { QueryResult source = locationIndex.findClosest(point.lat, point.lon, new EverythingButPt(flagEncoder)); if (!source.isValid()) { throw new PointNotFoundException("Cannot find point: " + point, indexForErrorMessage); } return source; } private void parseSolutionsAndAddToResponse(Set<Label> solutions, PointList waypoints) { for (Label solution : solutions) { response.add(parseSolutionIntoPath(initialTime, arriveBy, flagEncoder, translation, queryGraph, weighting, solution, waypoints)); } response.getAll().sort(Comparator.comparingDouble(PathWrapper::getTime)); } private Set<Label> findPaths(int startNode, int destNode) { StopWatch stopWatch = new StopWatch().start(); GraphExplorer graphExplorer = new GraphExplorer(queryGraph, weighting, flagEncoder, gtfsStorage, realtimeFeed, arriveBy); MultiCriteriaLabelSetting router = new MultiCriteriaLabelSetting(graphExplorer, weighting, arriveBy, maxWalkDistancePerLeg, maxTransferDistancePerLeg, !ignoreTransfers, maxVisitedNodesForRequest); Set<Label> solutions = router.calcPaths(startNode, Collections.singleton(destNode), initialTime, rangeQueryEndTime); response.addDebugInfo("routing:" + stopWatch.stop().getSeconds() + "s"); if (router.getVisitedNodes() >= maxVisitedNodesForRequest) { throw new IllegalArgumentException("No path found - maximum number of nodes exceeded: " + maxVisitedNodesForRequest); } response.getHints().put("visited_nodes.sum", router.getVisitedNodes()); response.getHints().put("visited_nodes.average", router.getVisitedNodes()); if (solutions.isEmpty()) { response.addError(new RuntimeException("No route found")); } return solutions; } } public GraphHopperGtfs(PtFlagEncoder flagEncoder, TranslationMap translationMap, GraphHopperStorage graphHopperStorage, LocationIndex locationIndex, GtfsStorage gtfsStorage, RealtimeFeed realtimeFeed) { this.flagEncoder = flagEncoder; this.translationMap = translationMap; this.graphHopperStorage = graphHopperStorage; this.locationIndex = locationIndex; this.gtfsStorage = gtfsStorage; this.realtimeFeed = realtimeFeed; } public static GtfsStorage createGtfsStorage() { return new GtfsStorage(); } public static GHDirectory createGHDirectory(String graphHopperFolder) { return new GHDirectory(graphHopperFolder, DAType.RAM_STORE); } public static TranslationMap createTranslationMap() { return new TranslationMap().doImport(); } public static GraphHopperStorage createOrLoad(GHDirectory directory, EncodingManager encodingManager, PtFlagEncoder ptFlagEncoder, GtfsStorage gtfsStorage, boolean createWalkNetwork, Collection<String> gtfsFiles, Collection<String> osmFiles) { GraphHopperStorage graphHopperStorage = new GraphHopperStorage(directory, encodingManager, false, gtfsStorage); if (graphHopperStorage.loadExisting()) { return graphHopperStorage; } else { graphHopperStorage.create(1000); for (String osmFile : osmFiles) { OSMReader osmReader = new OSMReader(graphHopperStorage); osmReader.setFile(new File(osmFile)); osmReader.setDontCreateStorage(true); try { osmReader.readGraph(); } catch (IOException e) { throw new RuntimeException(e); } } int id = 0; for (String gtfsFile : gtfsFiles) { try { ((GtfsStorage) graphHopperStorage.getExtension()).loadGtfsFromFile("gtfs_" + id++, new ZipFile(gtfsFile)); } catch (IOException e) { throw new RuntimeException(e); } } if (createWalkNetwork) { FakeWalkNetworkBuilder.buildWalkNetwork(((GtfsStorage) graphHopperStorage.getExtension()).getGtfsFeeds().values(), graphHopperStorage, ptFlagEncoder, Helper.DIST_EARTH); } LocationIndex walkNetworkIndex; if (graphHopperStorage.getNodes() > 0) { walkNetworkIndex = new LocationIndexTree(graphHopperStorage, new RAMDirectory()).prepareIndex(); } else { walkNetworkIndex = new EmptyLocationIndex(); } for (int i = 0; i < id; i++) { new GtfsReader("gtfs_" + i, graphHopperStorage, walkNetworkIndex).readGraph(); } graphHopperStorage.flush(); return graphHopperStorage; } } public static LocationIndex createOrLoadIndex(GHDirectory directory, GraphHopperStorage graphHopperStorage) { LocationIndex locationIndex = new LocationIndexTree(graphHopperStorage, directory); if (!locationIndex.loadExisting()) { locationIndex.prepareIndex(); } return locationIndex; } public boolean load(String graphHopperFolder) { throw new IllegalStateException("We are always loaded, or we wouldn't exist."); } @Override public GHResponse route(GHRequest request) { return new RequestHandler(request).route(); } private static PtTravelTimeWeighting createPtTravelTimeWeighting(PtFlagEncoder encoder, boolean arriveBy, double walkSpeedKmH) { PtTravelTimeWeighting weighting = new PtTravelTimeWeighting(encoder, walkSpeedKmH); if (arriveBy) { weighting = weighting.reverse(); } return weighting; } private PathWrapper parseSolutionIntoPath(Instant initialTime, boolean arriveBy, PtFlagEncoder encoder, Translation tr, QueryGraph queryGraph, PtTravelTimeWeighting weighting, Label solution, PointList waypoints) { PathWrapper path = new PathWrapper(); List<Label.Transition> transitions = new ArrayList<>(); if (arriveBy) { reverseEdges(solution, queryGraph, encoder, false) .forEach(transitions::add); } else { reverseEdges(solution, queryGraph, encoder, true) .forEach(transitions::add); Collections.reverse(transitions); } path.setWaypoints(waypoints); List<List<Label.Transition>> partitions = getPartitions(transitions); final List<Trip.Leg> legs = getLegs(encoder, tr, queryGraph, weighting, partitions); path.getLegs().addAll(legs); final InstructionList instructions = getInstructions(tr, path.getLegs()); path.setInstructions(instructions); PointList pointsList = new PointList(); for (Instruction instruction : instructions) { pointsList.add(instruction.getPoints()); } path.addDebugInfo(String.format("Violations: %d, Last leg dist: %f", solution.nWalkDistanceConstraintViolations, solution.walkDistanceOnCurrentLeg)); path.setPoints(pointsList); path.setDistance(path.getLegs().stream().mapToDouble(Trip.Leg::getDistance).sum()); path.setTime((solution.currentTime - initialTime.toEpochMilli()) * (arriveBy ? -1 : 1)); if (solution.firstPtDepartureTime != Long.MAX_VALUE) { path.setFirstPtLegDeparture(solution.firstPtDepartureTime); } path.setNumChanges((int) path.getLegs().stream() .filter(l -> l instanceof Trip.PtLeg) .filter(l -> !((Trip.PtLeg) l).isInSameVehicleAsPrevious) .count() - 1); com.graphhopper.gtfs.fare.Trip faresTrip = new com.graphhopper.gtfs.fare.Trip(); path.getLegs().stream() .filter(leg -> leg instanceof Trip.PtLeg) .map(leg -> (Trip.PtLeg) leg) .findFirst() .ifPresent(firstPtLeg -> { LocalDateTime firstPtDepartureTime = GtfsHelper.localDateTimeFromDate(firstPtLeg.departureTime); path.getLegs().stream() .filter(leg -> leg instanceof Trip.PtLeg) .map(leg -> (Trip.PtLeg) leg) .map(ptLeg -> { final GTFSFeed gtfsFeed = gtfsStorage.getGtfsFeeds().get(ptLeg.feedId); return new com.graphhopper.gtfs.fare.Trip.Segment(gtfsFeed.trips.get(ptLeg.tripId).route_id, Duration.between(firstPtDepartureTime, GtfsHelper.localDateTimeFromDate(ptLeg.departureTime)).getSeconds(), gtfsFeed.stops.get(ptLeg.boardStop.stop_id).zone_id, gtfsFeed.stops.get(ptLeg.stops.get(ptLeg.stops.size() - 1).stop_id).zone_id, ptLeg.stops.stream().map(s -> gtfsFeed.stops.get(s.stop_id).zone_id).collect(Collectors.toSet())); }) .forEach(faresTrip.segments::add); Fares.cheapestFare(gtfsStorage.getFares(), faresTrip) .ifPresent(amount -> path.setFare(amount.getAmount())); }); return path; } private List<List<Label.Transition>> getPartitions(List<Label.Transition> transitions) { List<List<Label.Transition>> partitions = new ArrayList<>(); partitions.add(new ArrayList<>()); final Iterator<Label.Transition> iterator = transitions.iterator(); partitions.get(partitions.size()-1).add(iterator.next()); iterator.forEachRemaining(transition -> { final List<Label.Transition> previous = partitions.get(partitions.size() - 1); final Label.EdgeLabel previousEdge = previous.get(previous.size() - 1).edge; if (previousEdge != null && (transition.edge.edgeType == GtfsStorage.EdgeType.ENTER_PT || previousEdge.edgeType == GtfsStorage.EdgeType.EXIT_PT)) { final ArrayList<Label.Transition> p = new ArrayList<>(); p.add(new Label.Transition(previous.get(previous.size()-1).label, null)); partitions.add(p); } partitions.get(partitions.size()-1).add(transition); }); return partitions; } private List<Trip.Leg> getLegs(PtFlagEncoder encoder, Translation tr, QueryGraph queryGraph, PtTravelTimeWeighting weighting, List<List<Label.Transition>> partitions) { return partitions.stream().flatMap(partition -> parsePathIntoLegs(partition, queryGraph, encoder, weighting, tr).stream()).collect(Collectors.toList()); } private InstructionList getInstructions(Translation tr, List<Trip.Leg> legs) { final InstructionList instructions = new InstructionList(tr); for (int i = 0; i< legs.size(); ++i) { Trip.Leg leg = legs.get(i); if (leg instanceof Trip.WalkLeg) { final Trip.WalkLeg walkLeg = ((Trip.WalkLeg) leg); instructions.addAll(walkLeg.instructions.subList(0, i < legs.size() - 1 ? walkLeg.instructions.size() - 1 : walkLeg.instructions.size())); } else if (leg instanceof Trip.PtLeg) { final Trip.PtLeg ptLeg = ((Trip.PtLeg) leg); final PointList pl; if (!ptLeg.isInSameVehicleAsPrevious) { pl = new PointList(); final Instruction departureInstruction = new Instruction(Instruction.PT_START_TRIP, ptLeg.trip_headsign, InstructionAnnotation.EMPTY, pl); departureInstruction.setDistance(leg.getDistance()); departureInstruction.setTime(ptLeg.travelTime); instructions.add(departureInstruction); } else { pl = instructions.get(instructions.size()-2).getPoints(); } pl.add(ptLeg.boardStop.geometry.getY(), ptLeg.boardStop.geometry.getX()); for (Trip.Stop stop : ptLeg.stops.subList(0, ptLeg.stops.size()-1)) { pl.add(stop.geometry.getY(), stop.geometry.getX()); } final PointList arrivalPointList = new PointList(); final Trip.Stop arrivalStop = ptLeg.stops.get(ptLeg.stops.size()-1); arrivalPointList.add(arrivalStop.geometry.getY(), arrivalStop.geometry.getX()); Instruction arrivalInstruction = new Instruction(Instruction.PT_END_TRIP, arrivalStop.name, InstructionAnnotation.EMPTY, arrivalPointList); if (ptLeg.isInSameVehicleAsPrevious) { instructions.replaceLast(arrivalInstruction); } else { instructions.add(arrivalInstruction); } } } return instructions; } private PointList pointListFrom(List<QueryResult> queryResults) { PointList waypoints = new PointList(queryResults.size(), true); for (QueryResult qr : queryResults) { waypoints.add(qr.getSnappedPoint()); } return waypoints; } // We are parsing a string of edges into a hierarchical trip. // One could argue that one should never write a parser // by hand, because it is always ugly, but use a parser library. // The code would then read like a specification of what paths through the graph mean. private List<Trip.Leg> parsePathIntoLegs(List<Label.Transition> path, Graph graph, PtFlagEncoder encoder, Weighting weighting, Translation tr) { if (GtfsStorage.EdgeType.ENTER_PT == path.get(1).edge.edgeType) { final GtfsStorage.FeedIdWithTimezone feedIdWithTimezone = gtfsStorage.getTimeZones().get(path.get(1).edge.timeZoneId); final GTFSFeed gtfsFeed = gtfsStorage.getGtfsFeeds().get(feedIdWithTimezone.feedId); List<Trip.Leg> result = new ArrayList<>(); long boardTime = -1; List<Label.Transition> partition = null; for (int i = 1; i < path.size(); i++) { Label.Transition transition = path.get(i); Label.EdgeLabel edge = path.get(i).edge; if (edge.edgeType == GtfsStorage.EdgeType.BOARD) { boardTime = transition.label.currentTime; partition = new ArrayList<>(); } if (partition != null) { partition.add(path.get(i)); } if (EnumSet.of(GtfsStorage.EdgeType.TRANSFER, GtfsStorage.EdgeType.LEAVE_TIME_EXPANDED_NETWORK).contains(edge.edgeType)) { Geometry lineString = lineStringFromEdges(partition); String tripId = gtfsStorage.getExtraStrings().get(partition.get(0).edge.edgeIteratorState.getEdge()); final StopsFromBoardHopDwellEdges stopsFromBoardHopDwellEdges = new StopsFromBoardHopDwellEdges(feedIdWithTimezone.feedId, tripId); partition.stream() .filter(e -> EnumSet.of(GtfsStorage.EdgeType.HOP, GtfsStorage.EdgeType.BOARD, GtfsStorage.EdgeType.DWELL).contains(e.edge.edgeType)) .forEach(stopsFromBoardHopDwellEdges::next); stopsFromBoardHopDwellEdges.finish(); List<Trip.Stop> stops = stopsFromBoardHopDwellEdges.stops; com.conveyal.gtfs.model.Trip trip = gtfsFeed.trips.get(tripId); result.add(new Trip.PtLeg( feedIdWithTimezone.feedId,partition.get(0).edge.nTransfers == 0, stops.get(0), tripId, trip.route_id, edges(partition).map(edgeLabel -> edgeLabel.edgeIteratorState).collect(Collectors.toList()), new Date(boardTime), stops, partition.stream().mapToDouble(t -> t.edge.distance).sum(), path.get(i-1).label.currentTime - boardTime, new Date(path.get(i-1).label.currentTime), lineString)); partition = null; } } return result; } else { InstructionList instructions = new InstructionList(tr); InstructionsFromEdges instructionsFromEdges = new InstructionsFromEdges(path.get(1).edge.edgeIteratorState.getBaseNode(), graph, weighting, weighting.getFlagEncoder(), graph.getNodeAccess(), tr, instructions); int prevEdgeId = -1; for (int i=1; i<path.size(); i++) { EdgeIteratorState edge = path.get(i).edge.edgeIteratorState; instructionsFromEdges.next(edge, i, prevEdgeId); prevEdgeId = edge.getEdge(); } instructionsFromEdges.finish(); final Instant departureTime = Instant.ofEpochMilli(path.get(0).label.currentTime); final Instant arrivalTime = Instant.ofEpochMilli(path.get(path.size() - 1).label.currentTime); return Collections.singletonList(new Trip.WalkLeg( "Walk", Date.from(departureTime), edges(path).map(edgeLabel -> edgeLabel.edgeIteratorState).collect(Collectors.toList()), lineStringFromEdges(path), edges(path).mapToDouble(edgeLabel -> edgeLabel.distance).sum(), instructions.stream().collect(Collectors.toCollection(() -> new InstructionList(tr))), Date.from(arrivalTime))); } } private Stream<Label.EdgeLabel> edges(List<Label.Transition> path) { return path.stream().filter(t -> t.edge != null).map(t -> t.edge); } private Geometry lineStringFromEdges(List<Label.Transition> transitions) { List<Coordinate> coordinates = new ArrayList<>(); final Iterator<Label.Transition> iterator = transitions.iterator(); iterator.next(); coordinates.addAll(toCoordinateArray(iterator.next().edge.edgeIteratorState.fetchWayGeometry(3))); iterator.forEachRemaining(transition -> { coordinates.addAll(toCoordinateArray(transition.edge.edgeIteratorState.fetchWayGeometry(2))); }); return geometryFactory.createLineString(coordinates.toArray(new Coordinate[coordinates.size()])); } public static List<Coordinate> toCoordinateArray(PointList pointList) { List<Coordinate> coordinates = new ArrayList<>(pointList.size()); for (int i=0; i<pointList.size(); i++) { coordinates.add(pointList.getDimension() == 3 ? new Coordinate(pointList.getLon(i), pointList.getLat(i)) : new Coordinate(pointList.getLon(i), pointList.getLat(i), pointList.getEle(i))); } return coordinates; } private class StopsFromBoardHopDwellEdges { private final String tripId; private final List<Trip.Stop> stops = new ArrayList<>(); private final GTFSFeed gtfsFeed; private long arrivalTimeFromHopEdge; private Stop stop = null; StopsFromBoardHopDwellEdges(String feedId, String tripId) { this.tripId = tripId; this.gtfsFeed = gtfsStorage.getGtfsFeeds().get(feedId); } void next(Label.Transition t) { long departureTime; switch (t.edge.edgeType) { case BOARD: stop = findStop(t); departureTime = t.label.currentTime; stops.add(new Trip.Stop(stop.stop_id, stop.stop_name, geometryFactory.createPoint(new Coordinate(stop.stop_lon, stop.stop_lat)), null, Date.from(Instant.ofEpochMilli(departureTime)))); break; case HOP: stop = findStop(t); arrivalTimeFromHopEdge = t.label.currentTime; break; case DWELL: departureTime = t.label.currentTime; stops.add(new Trip.Stop(stop.stop_id, stop.stop_name, geometryFactory.createPoint(new Coordinate(stop.stop_lon, stop.stop_lat)), Date.from(Instant.ofEpochMilli(arrivalTimeFromHopEdge)), Date.from(Instant.ofEpochMilli(departureTime)))); break; default: throw new RuntimeException(); } } private Stop findStop(Label.Transition t) { int stopSequence = gtfsStorage.getStopSequences().get(t.edge.edgeIteratorState.getEdge()); StopTime stopTime = gtfsFeed.stop_times.get(new Fun.Tuple2<>(tripId, stopSequence)); return gtfsFeed.stops.get(stopTime.stop_id); } void finish() { stops.add(new Trip.Stop(stop.stop_id, stop.stop_name, geometryFactory.createPoint(new Coordinate(stop.stop_lon, stop.stop_lat)), Date.from(Instant.ofEpochMilli(arrivalTimeFromHopEdge)), null)); } } }