/* * 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.routing; import com.graphhopper.routing.util.DefaultEdgeFilter; import com.graphhopper.routing.util.FlagEncoder; import com.graphhopper.routing.weighting.Weighting; import com.graphhopper.storage.Graph; import com.graphhopper.storage.NodeAccess; import com.graphhopper.util.*; import com.graphhopper.util.shapes.GHPoint; /** * This class calculates instructions from the edges in a Path. * * @author Peter Karich * @author Robin Boldt * @author jan soe */ public class InstructionsFromEdges implements Path.EdgeVisitor { private final Weighting weighting; private final FlagEncoder encoder; private final NodeAccess nodeAccess; private final Translation tr; private final InstructionList ways; /* * We need three points to make directions * * (1)----(2) * / * / * (0) * * 0 is the node visited at t-2, 1 is the node visited * at t-1 and 2 is the node being visited at instant t. * orientation is the angle of the vector(1->2) expressed * as atan2, while previousOrientation is the angle of the * vector(0->1) * Intuitively, if orientation is smaller than * previousOrientation, then we have to turn right, while * if it is greater we have to turn left. To make this * algorithm work, we need to make the comparison by * considering orientation belonging to the interval * [ - pi + previousOrientation , + pi + previousOrientation ] */ private EdgeIteratorState prevEdge; private double prevLat; private double prevLon; private double doublePrevLat, doublePrevLon; // Lat and Lon of node t-2 private int prevNode; private double prevOrientation; private Instruction prevInstruction; private boolean prevInRoundabout; private String prevName; private InstructionAnnotation prevAnnotation; private EdgeExplorer outEdgeExplorer; private EdgeExplorer crossingExplorer; public InstructionsFromEdges(int tmpNode, Graph graph, Weighting weighting, FlagEncoder encoder, NodeAccess nodeAccess, Translation tr, InstructionList ways) { this.weighting = weighting; this.encoder = encoder; this.nodeAccess = nodeAccess; this.tr = tr; this.ways = ways; prevLat = this.nodeAccess.getLatitude(tmpNode); prevLon = this.nodeAccess.getLongitude(tmpNode); prevNode = -1; prevInRoundabout = false; prevName = null; outEdgeExplorer = graph.createEdgeExplorer(new DefaultEdgeFilter(this.encoder, false, true)); crossingExplorer = graph.createEdgeExplorer(new DefaultEdgeFilter(encoder, true, true)); } @Override public void next(EdgeIteratorState edge, int index, int prevEdgeId) { // baseNode is the current node and adjNode is the next int adjNode = edge.getAdjNode(); int baseNode = edge.getBaseNode(); long flags = edge.getFlags(); double adjLat = nodeAccess.getLatitude(adjNode); double adjLon = nodeAccess.getLongitude(adjNode); double latitude, longitude; PointList wayGeo = edge.fetchWayGeometry(3); boolean isRoundabout = encoder.isBool(flags, FlagEncoder.K_ROUNDABOUT); if (wayGeo.getSize() <= 2) { latitude = adjLat; longitude = adjLon; } else { latitude = wayGeo.getLatitude(1); longitude = wayGeo.getLongitude(1); assert Double.compare(prevLat, nodeAccess.getLatitude(baseNode)) == 0; assert Double.compare(prevLon, nodeAccess.getLongitude(baseNode)) == 0; } String name = edge.getName(); InstructionAnnotation annotation = encoder.getAnnotation(flags, tr); if ((prevName == null) && (!isRoundabout)) // very first instruction (if not in Roundabout) { int sign = Instruction.CONTINUE_ON_STREET; prevInstruction = new Instruction(sign, name, annotation, new PointList(10, nodeAccess.is3D())); ways.add(prevInstruction); prevName = name; prevAnnotation = annotation; } else if (isRoundabout) { // remark: names and annotations within roundabout are ignored if (!prevInRoundabout) //just entered roundabout { int sign = Instruction.USE_ROUNDABOUT; RoundaboutInstruction roundaboutInstruction = new RoundaboutInstruction(sign, name, annotation, new PointList(10, nodeAccess.is3D())); if (prevName != null) { // check if there is an exit at the same node the roundabout was entered EdgeIterator edgeIter = outEdgeExplorer.setBaseNode(baseNode); while (edgeIter.next()) { if ((edgeIter.getAdjNode() != prevNode) && !encoder.isBool(edgeIter.getFlags(), FlagEncoder.K_ROUNDABOUT)) { roundaboutInstruction.increaseExitNumber(); break; } } // previous orientation is last orientation before entering roundabout prevOrientation = Helper.ANGLE_CALC.calcOrientation(doublePrevLat, doublePrevLon, prevLat, prevLon); // calculate direction of entrance turn to determine direction of rotation // right turn == counterclockwise and vice versa double orientation = Helper.ANGLE_CALC.calcOrientation(prevLat, prevLon, latitude, longitude); orientation = Helper.ANGLE_CALC.alignOrientation(prevOrientation, orientation); double delta = (orientation - prevOrientation); roundaboutInstruction.setDirOfRotation(delta); } else // first instructions is roundabout instruction { prevOrientation = Helper.ANGLE_CALC.calcOrientation(prevLat, prevLon, latitude, longitude); prevName = name; prevAnnotation = annotation; } prevInstruction = roundaboutInstruction; ways.add(prevInstruction); } // Add passed exits to instruction. A node is counted if there is at least one outgoing edge // out of the roundabout EdgeIterator edgeIter = outEdgeExplorer.setBaseNode(edge.getAdjNode()); while (edgeIter.next()) { if (!encoder.isBool(edgeIter.getFlags(), FlagEncoder.K_ROUNDABOUT)) { ((RoundaboutInstruction) prevInstruction).increaseExitNumber(); break; } } } else if (prevInRoundabout) //previously in roundabout but not anymore { prevInstruction.setName(name); // calc angle between roundabout entrance and exit double orientation = Helper.ANGLE_CALC.calcOrientation(prevLat, prevLon, latitude, longitude); orientation = Helper.ANGLE_CALC.alignOrientation(prevOrientation, orientation); double deltaInOut = (orientation - prevOrientation); // calculate direction of exit turn to determine direction of rotation // right turn == counterclockwise and vice versa double recentOrientation = Helper.ANGLE_CALC.calcOrientation(doublePrevLat, doublePrevLon, prevLat, prevLon); orientation = Helper.ANGLE_CALC.alignOrientation(recentOrientation, orientation); double deltaOut = (orientation - recentOrientation); prevInstruction = ((RoundaboutInstruction) prevInstruction) .setRadian(deltaInOut) .setDirOfRotation(deltaOut) .setExited(); prevName = name; prevAnnotation = annotation; } else { int sign = getTurn(edge, baseNode, prevNode, adjNode, annotation, name); if (sign != Instruction.IGNORE) { prevInstruction = new Instruction(sign, name, annotation, new PointList(10, nodeAccess.is3D())); ways.add(prevInstruction); prevAnnotation = annotation; } // Updated the prevName, since we don't always create an instruction on name changes the previous // name can be an old name. This leads to incorrect turn instructions due to name changes prevName = name; } updatePointsAndInstruction(edge, wayGeo); if (wayGeo.getSize() <= 2) { doublePrevLat = prevLat; doublePrevLon = prevLon; } else { int beforeLast = wayGeo.getSize() - 2; doublePrevLat = wayGeo.getLatitude(beforeLast); doublePrevLon = wayGeo.getLongitude(beforeLast); } prevInRoundabout = isRoundabout; prevNode = baseNode; prevLat = adjLat; prevLon = adjLon; prevEdge = edge; } @Override public void finish() { if (prevInRoundabout) { // calc angle between roundabout entrance and finish double orientation = Helper.ANGLE_CALC.calcOrientation(doublePrevLat, doublePrevLon, prevLat, prevLon); orientation = Helper.ANGLE_CALC.alignOrientation(prevOrientation, orientation); double delta = (orientation - prevOrientation); ((RoundaboutInstruction) prevInstruction).setRadian(delta); } ways.add(new FinishInstruction(nodeAccess, prevEdge.getAdjNode())); } private int getTurn(EdgeIteratorState edge, int baseNode, int prevNode, int adjNode, InstructionAnnotation annotation, String name) { GHPoint point = InstructionsHelper.getPointForOrientationCalculation(edge, nodeAccess); double lat = point.getLat(); double lon = point.getLon(); prevOrientation = Helper.ANGLE_CALC.calcOrientation(doublePrevLat, doublePrevLon, prevLat, prevLon); int sign = InstructionsHelper.calculateSign(prevLat, prevLon, lat, lon, prevOrientation); boolean forceInstruction = false; if (!annotation.equals(prevAnnotation) && !annotation.isEmpty()) { forceInstruction = true; } InstructionsSurroundingEdges surroundingEdges = new InstructionsSurroundingEdges(prevEdge, edge, encoder, crossingExplorer, nodeAccess, prevNode, baseNode, adjNode); int nrOfPossibleTurns = surroundingEdges.nrOfPossibleTurns(); // there is no other turn possible if (nrOfPossibleTurns <= 1) { return returnForcedInstructionOrIgnore(forceInstruction, sign); } // Very certain, this is a turn if (Math.abs(sign) > 1) { /* * Don't show an instruction if the user is following a street, even though the street is * bending. We should only do this, if following the street is the obvious choice. */ if (InstructionsHelper.isNameSimilar(name, prevName) && surroundingEdges.surroundingStreetsAreSlowerByFactor(2)) { return returnForcedInstructionOrIgnore(forceInstruction, sign); } return sign; } /* The current state is a bit uncertain. So we are going more or less straight sign < 2 So it really depends on the surrounding street if we need a turn instruction or not In most cases this will be a simple follow the current street and we don't necessarily need a turn instruction */ if (prevEdge == null) { // TODO Should we log this case? return sign; } long flag = edge.getFlags(); long prevFlag = prevEdge.getFlags(); boolean surroundingStreetsAreSlower = surroundingEdges.surroundingStreetsAreSlowerByFactor(1); // There is at least one other possibility to turn, and we are almost going straight // Check the other turns if one of them is also going almost straight // If not, we don't need a turn instruction EdgeIteratorState otherContinue = surroundingEdges.getOtherContinue(prevLat, prevLon, prevOrientation); // Signs provide too less detail, so we use the delta for a precise comparision double delta = InstructionsHelper.calculateOrientationDelta(prevLat, prevLon, lat, lon, prevOrientation); // This state is bad! Two streets are going more or less straight // Happens a lot for trunk_links // For _links, comparing flags works quite good, as links usually have different speeds => different flags if (otherContinue != null) { //We are at a fork if (!InstructionsHelper.isNameSimilar(name, prevName) || InstructionsHelper.isNameSimilar(otherContinue.getName(), prevName) || prevFlag != flag || prevFlag == otherContinue.getFlags() || !surroundingStreetsAreSlower) { GHPoint tmpPoint = InstructionsHelper.getPointForOrientationCalculation(otherContinue, nodeAccess); double otherDelta = InstructionsHelper.calculateOrientationDelta(prevLat, prevLon, tmpPoint.getLat(), tmpPoint.getLon(), prevOrientation); if (Math.abs(delta) < .1 && Math.abs(otherDelta) > .15 && InstructionsHelper.isNameSimilar(name, prevName)) { return Instruction.CONTINUE_ON_STREET; } if (otherDelta < delta) { // TODO Use keeps once we have a robust client //return Instruction.KEEP_LEFT; return Instruction.TURN_SLIGHT_LEFT; } else { // TODO Use keeps once we have a robust client //return Instruction.KEEP_RIGHT; return Instruction.TURN_SLIGHT_RIGHT; } } } if (!surroundingStreetsAreSlower) { if (Math.abs(delta) > .4 || surroundingEdges.isLeavingCurrentStreet(prevName, name)) { // Leave the current road -> create instruction return sign; } } return returnForcedInstructionOrIgnore(forceInstruction, sign); } private int returnForcedInstructionOrIgnore(boolean forceInstruction, int sign) { if (forceInstruction) return sign; return Instruction.IGNORE; } private void updatePointsAndInstruction(EdgeIteratorState edge, PointList pl) { // skip adjNode int len = pl.size() - 1; for (int i = 0; i < len; i++) { prevInstruction.getPoints().add(pl, i); } double newDist = edge.getDistance(); prevInstruction.setDistance(newDist + prevInstruction.getDistance()); prevInstruction.setTime(weighting.calcMillis(edge, false, EdgeIterator.NO_EDGE) + prevInstruction.getTime()); } }